Compare commits

...

36 Commits

Author SHA1 Message Date
github-actions[bot] e772686c4b 🆙 Bump version to 4.2.22 [skip ci] 2026-05-28 09:05:33 +00:00
DuckieTM a00f7b01f5 Merge pull request #130 from duckietm/dev
Dev
2026-05-28 11:04:35 +02:00
duckietm 6b4089cace 🆙 small typo in SQL 2026-05-28 11:04:01 +02:00
duckietm 9ea7acf05c 🆙 Update for Frank 2026-05-28 10:53:50 +02:00
duckietm bab43af41e 🆕 Frank the BOT
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
2026-05-28 10:41:25 +02:00
github-actions[bot] 55b38e7b85 🆙 Bump version to 4.2.21 [skip ci] 2026-05-27 13:39:01 +00:00
DuckieTM 4a96c5baaf Merge pull request #128 from duckietm/dev
Dev
2026-05-27 15:37:57 +02:00
duckietm 539c5b5b96 🆙 Fix BOTS in catalog and inventory 2026-05-27 13:46:17 +02:00
duckietm 7b7154e68f 🆙 Fix search and buy #1 2026-05-27 11:34:55 +02:00
duckietm 4aabb738a3 🆙 Added missing Table for the HK 2026-05-27 09:47:30 +02:00
github-actions[bot] 691dc42627 🆙 Bump version to 4.2.20 [skip ci] 2026-05-27 07:43:14 +00:00
DuckieTM 226873c1fb Merge pull request #127 from duckietm/dev
Dev
2026-05-27 09:42:21 +02:00
duckietm a06a204b39 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-05-27 09:37:51 +02:00
duckietm e213609609 🆕 Added Pickup furni to the floorplan 2026-05-27 09:37:49 +02:00
DuckieTM 44d38b8661 🆙 SQL update 2026-05-26 22:18:02 +02:00
github-actions[bot] ccadb81970 🆙 Bump version to 4.2.19 [skip ci] 2026-05-26 15:16:04 +00:00
DuckieTM 0a3a940946 Merge pull request #125 from duckietm/dev
🆙 Small fix floorplan
2026-05-26 17:15:07 +02:00
duckietm 4613fbe80c 🆙 Small fix floorplan 2026-05-26 17:14:49 +02:00
github-actions[bot] 9328f4a355 🆙 Bump version to 4.2.18 [skip ci] 2026-05-26 14:37:17 +00:00
DuckieTM da8b947ddf Merge pull request #124 from duckietm/dev
🆕 Brand new Floorplan
2026-05-26 16:36:17 +02:00
duckietm b9658d0407 🆕 Brand new Floorplan 2026-05-26 16:35:58 +02:00
DuckieTM 68d3731393 Merge pull request #123 from duckietm/dev
Dev
2026-05-26 12:53:07 +02:00
duckietm 4ef4ed1a96 🆙 Enable HK in client with permissions 2026-05-26 12:52:49 +02:00
DuckieTM c20e273a2c Merge pull request #120 from simoleo89/feat/housekeeping-packets
feat(housekeeping): in-client admin packet handlers
2026-05-26 10:44:13 +02:00
simoleo89 dac09e92d1 fix(housekeeping): hash reset password with BCrypt, not SHA-256
`HousekeepingResetUserPasswordEvent` was writing a SHA-256 hex digest
into `users.password`, but the Nitro auth path
(`SessionEndpoints` / `AccountChangeEndpoints` → `AuthHttpUtil.checkPassword`)
only does `BCrypt.checkpw`. A SHA-256 hex string doesn't start with
`$2…$`, so jbcrypt throws `IllegalArgumentException`, `checkPassword`
returns false, and operators saw "credenziali invalide" on every
account whose password had been reset from the in-client panel.

Switch to `BCrypt.hashpw(plain, BCrypt.gensalt(10))` — same idiom
already used by `SessionEndpoints.java:351` and
`AccountChangeEndpoints.java:98`. Cost 10 (vs 12 there) is fine for a
server-generated 12-char random password: gensalt(10) keeps the
operator-facing reset snappy and the output is identical-shape
(`$2a$…`) to what jbcrypt 0.4 already accepts.

Side-effects:
- drops the `MessageDigest` / `NoSuchAlgorithmException` /
  `StandardCharsets` imports and the local `sha256Hex` helper
- repurposes the existing `housekeeping.error.hash_failed` key for
  `BCrypt.gensalt`'s only failure mode (invalid cost / log_rounds out
  of range) so the client error surface is unchanged
- updates the file javadoc to stop telling future readers to "swap the
  MessageDigest constant" — Arcturus itself only verifies BCrypt

Companion of duckietm/Nitro-V3#157 (`feat/housekeeping-panel`). The
client/UI is untouched — packet 9200, the action-result reveal card,
the copy button, and the plaintext flow through `message` are all
unchanged.
2026-05-24 22:25:16 +02:00
simoleo89 fbf979419e feat(housekeeping): hotel alert + dashboard + audit log
Closes out the HK panel server-side surface.

* Incoming 9127 HousekeepingSendHotelAlertEvent — broadcast a
  StaffAlertWithLinkComposer to every online user that hasn't
  set blockStaffAlerts. Composed once, fanned out by reference;
  empty-message guard returns `housekeeping.error.alert_empty`.

* Outgoing 9206 HousekeepingDashboardComposer + Incoming 9128
  HousekeepingGetDashboardEvent — single round trip with the
  aggregated counters: online / total users + active / total
  rooms + pending support tickets + sanctions in the last 24h +
  approximate emulator uptime + a version string. Active-rooms
  is derived from RoomManager.getActiveRooms().getUserCount()>0
  to avoid counting idle preloaded rooms. Peak online today /
  all-time aren't tracked yet, so they currently echo the live
  online count as a best-effort placeholder.

* Outgoing 9207 HousekeepingActionLogComposer + Incoming 9129
  HousekeepingListActionLogEvent — read the optional
  housekeeping_log table. If the table isn't there the SQL
  exception is swallowed and an empty list goes back, so the
  panel renders a no-entries view rather than crashing. Schema
  is documented in the handler's javadoc; operators who want
  audit run a single CREATE TABLE then the HK panel populates
  from new writes (writes are a follow-up — every HK handler
  will eventually append a row).

`mvn package` clean — the final fat jar lands in
Latest_Compiled_Version/ after the build finishes.
2026-05-24 16:31:01 +02:00
simoleo89 6126c35779 feat(housekeeping): economy domain — credits/currency/items/hc
* Incoming 9117 HousekeepingGiveCreditsEvent — Habbo.giveCredits for
  online (ships UserCreditsComposer) or UPDATE users.credits for offline.

* Incoming 9118 HousekeepingGiveCurrencyEvent — generic across the
  non-credits currencies. currencyType 0 => duckets/pixels (givePixels),
  5 => diamonds (givePoints(5,n)), anything else routes through
  givePoints(type,n). Offline path INSERT ... ON DUPLICATE KEY UPDATE
  users_currency.

* Incoming 9119 HousekeepingGrantItemEvent — batch-INSERT N rows into
  the items table with item_id = base furni id. Capped at 100 per call
  so a typo can't bury the DB. Online inventory refresh deferred — the
  user picks the new items up on next hand-inventory open or relog.

* Incoming 9120 HousekeepingSetHcSubscriptionEvent — extends
  users_settings.club_expire_timestamp by `days*86400`. Stacks on top
  of the existing expiry if it's still in the future, otherwise starts
  from now. days==0 clamps to now (effective cancel).

All four reuse HousekeepingActionResultComposer (no new outgoing
composer this slice).

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 a1749c9eda feat(housekeeping): rooms domain — find/search + open/close/mute/kick-all/transfer/delete
Eight new incoming handlers + two new outgoing composers cover the
full rooms-domain HK panel.

* Outgoing 9202 HousekeepingRoomDetailComposer — single room with a
  leading `found` boolean. Writes the IHousekeepingRoom shape via a
  static `appendRoomFields` that HousekeepingRoomListComposer shares.

* Outgoing 9203 HousekeepingRoomListComposer — `count` then N rooms.
  Used for both find-by-name (exact match, up to 50) and the prefix
  autocomplete dropdown (up to 8).

* Incoming 9110 HousekeepingFindRoomByIdEvent — loadRoom(id, false)
  covers both the in-memory cache and the offline `SELECT * FROM rooms`
  path. No `loadData` so HK doesn't pull furni/bots/pets just to
  render a summary.

* Incoming 9111 HousekeepingSearchRoomsEvent — (query, exactMatch,
  limit). Branches between `name = ?` and `name LIKE ?` so the same
  wire packet serves both the autocomplete and the exact-find flows.
  Hard-capped to 50.

* Incoming 9112 HousekeepingRoomStateEvent — (roomId, open). Toggles
  Room.setState(OPEN | LOCKED) and persists via Room.save(). One
  packet covers both the open and close API endpoints.

* Incoming 9113 HousekeepingMuteRoomEvent — (roomId, minutes). Room.
  setMuted is a boolean, so minutes==0 unmutes and minutes>0 mutes.
  A scheduled auto-unmute is left for a future slice; the wire field
  is reserved.

* Incoming 9114 HousekeepingKickAllFromRoomEvent — Room.ejectAll().

* Incoming 9115 HousekeepingTransferRoomOwnershipEvent — UPDATEs both
  rooms.owner_id and rooms.owner_name so the navigator cached name
  doesn't go stale. Validates the new owner exists via
  HabboManager.getHabboInfo before touching the row.

* Incoming 9116 HousekeepingDeleteRoomEvent — ejectAll + dispose +
  uncacheRoom + DELETE FROM rooms, mirroring the minimum-viable
  subset of RequestDeleteRoomEvent. Pets/guild/custom-layout cleanup
  is skipped on this slice (orphans don't crash the emulator).

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 525c124fa5 feat(housekeeping): set-rank + trade-lock + reset-password
Closes out the users-domain HK actions.

* Incoming 9107 HousekeepingSetUserRankEvent — (userId, rankId).
  Validates the rank exists in `permission_ranks`, UPDATEs users.rank,
  and if the target is online rebinds their HabboInfo to the fresh
  Rank object and ships a UserPermissionsComposer so server-side
  hasPermission() and the client's useHasPermission(key) consumers
  re-render against the new permissions without a relog.

* Incoming 9108 HousekeepingTradeLockUserEvent — (userId, hours,
  reason). Writes `users_settings.trade_locked_until = now + hours*3600`
  so the lock survives logout/login. Online targets also get their
  in-memory HabboStats.allowTrade cleared and an optional alert.

* Incoming 9109 HousekeepingResetUserPasswordEvent — (userId).
  Generates a 12-char alphanumeric (SecureRandom over a curated
  ambiguity-free alphabet), writes its SHA-256 hex to users.password
  (the column is varchar(64) — already sized for SHA-256 hex) and
  blanks auth_ticket so any live SSO ticket can't bypass the reset.
  Plaintext is returned to the operator in the action-result
  message — they relay it out-of-band. If your CMS uses a hash other
  than SHA-256, swap the MessageDigest.getInstance constant.

`mvn compile` clean.
2026-05-24 16:29:55 +02:00
simoleo89 57087a31f2 fix(housekeeping): emit localizable error keys instead of bare slugs
Every HK action handler returned bare error slugs (\"invalid_input\",
\"user_offline\", \"no_active_ban\", \"target_unkickable\", \"ban_failed\",
\"user_not_found\") in HousekeepingActionResultComposer.message. The
client's `localizeOrPassthrough` only treats a value as a translation
key when it contains a dot, so those bare slugs were rendered raw in
the status banner and the toast — ugly and untranslatable.

Re-prefix all error messages with `housekeeping.error.` so the EN +
IT dictionaries can resolve them. Success path is unchanged (server
sends empty string, client falls back to `housekeeping.action.success`).

Companion dictionary entries land on the client side.
2026-05-24 16:29:55 +02:00
simoleo89 c4b3295a45 feat(housekeeping): force-disconnect-user packet
Incoming 9106 HousekeepingForceDisconnectUserEvent — (userId, reason).
Sends the optional reason as a Habbo.alert, dispatches the action ack
BEFORE calling target.disconnect() so the result lands on the wire
before the target's socket closes, then drops the session. Online-only;
offline target returns `user_offline`.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 418c753e6c feat(housekeeping): mute-user + kick-user packets
Incoming 9104 HousekeepingMuteUserEvent — (userId, reason, minutes).
Unlike ModToolSanctionMute which takes a fixed-bucket minutes arg
from a CFH context, this one applies an arbitrary in-session mute via
Habbo.mute(seconds, false). Mute is online-only (the live Habbo object
holds the remaining seconds), so an offline target returns ok=false
with `user_offline`. The reason string, if non-empty, is delivered via
Habbo.alert so the muted user sees why.

Incoming 9105 HousekeepingKickUserEvent — (userId, reason). Replicates
the ModToolManager.kick body (leave room + alert) locally so HK doesn't
piggyback on ACC_SUPPORTTOOL the way ModToolManager.kick does — keeps
the permission model `acc_housekeeping`-only. Respects ACC_UNKICKABLE
the same way the legacy path does.

Both reuse HousekeepingActionResultComposer with their own actionKey
(user.mute / user.kick).

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 8419f11883 feat(housekeeping): unban-user packet
Incoming 9103 HousekeepingUnbanUserEvent — reads userId, resolves
the username via HabboManager.getHabboInfo(int) (covers both online
and offline paths in one call), then dispatches to
ModToolManager.unban(username) which clears all active rows from
the `bans` table for that user.

Reuses HousekeepingActionResultComposer with actionKey `user.unban`.
If the user never had an active ban the SQL UPDATE matches zero rows
and the handler responds with `ok: false, message: 'no_active_ban'`
— from a UI standpoint that's a no-op, not an error.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 1a0d783ff7 feat(housekeeping): ban-user with arbitrary duration + ack composer
Adds two new packets:

* Incoming 9102 HousekeepingBanUserEvent — reads (userId, reason,
  hours). Unlike ModToolSanctionBanEvent which only accepts the four
  fixed Habbo-protocol banType buckets (18h / 7d / 30d / 100y), this
  one converts the hours arg straight to seconds and feeds them into
  ModToolManager.ban with ModToolBanType.ACCOUNT and cfhTopic=0.
  Duration is clamped to 100 years to keep it inside `int` range.

* Outgoing 9201 HousekeepingActionResultComposer — generic ack
  for any HK action (ban / mute / kick / give-credits / room-close /
  …). Wire shape is (actionKey, ok, actionId, message). The
  actionKey lets the client filter multiple in-flight actions to
  the right Promise via `accept`, so concurrent admin operations
  don't cross-resolve.

actionId here is the target user id because ModToolBan doesn't
expose the `bans` autoinc id on the object — there's a TODO to swap
this for a dedicated housekeeping_log row id once that table goes in.

Same ACC_HOUSEKEEPING permission gate as the find-user packets, so
operators only need to grant the permission once.

`mvn compile` clean.
2026-05-24 16:29:54 +02:00
simoleo89 655e039df7 feat(housekeeping): find-user-by-id packet + acc_housekeeping gate
Adds Incoming 9101 HousekeepingFindUserByIdEvent, which replies on
the existing HousekeepingUserDetailComposer (Outgoing 9200) — the
composer is shape-agnostic about how the lookup was issued, so the
two find-* handlers share the same response packet.

The by-id handler uses HabboManager.getHabboInfo(int) directly, which
already covers both the online (in-memory hashmap) and offline (SQL
LIMIT 1 on users) branches in one call. The by-name path still has
to do online + offline manually because the equivalent String overload
doesn't exist as an instance method, only as a static.

Also introduces Permission.ACC_HOUSEKEEPING ("acc_housekeeping") so
the in-client housekeeping panel doesn't piggyback on ACC_SUPPORTTOOL.
Both HK handlers now gate on the new permission; the toolbar UI on
the client side was already checking `acc_housekeeping`, so this
closes the loop. Operators must add the permission to
permission_definitions for the desired rank:

  INSERT INTO permission_definitions
    (permission_key, max_value, comment,
     rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
  VALUES
    ('acc_housekeeping', 1,
     'Allows access to the in-client Housekeeping admin panel ...',
     0, 0, 0, 0, 0, 0, 1)
  ON DUPLICATE KEY UPDATE rank_7 = 1, comment = VALUES(comment);

`mvn package` clean (Habbo-4.2.12-jar-with-dependencies.jar).
2026-05-24 16:29:54 +02:00
simoleo89 7726691cde feat(housekeeping): add find-user-by-name packet pair
First wire-level packet for the in-client housekeeping admin panel.
Adds an incoming handler (Incoming 9100) that resolves a username to
a HabboInfo (online via the HabboManager hashmap, offline via the
users-table SQL fallback) and replies with HousekeepingUserDetail
(Outgoing 9200) containing id / username / motto / look / rank / rank
name / online / lastOnline / credits / duckets / diamonds / email /
ipLogin / isBanned. Active-mute and active-trade-lock are written as
trailing booleans (currently false) so the renderer parser can pick
them up later behind a bytesAvailable guard once those manager APIs
are surfaced offline-side.

Permission gate is ACC_SUPPORTTOOL — same one ModTools already uses.
Avoids adding a new column to the permissions table on this slice; a
dedicated ACC_HOUSEKEEPING permission can be introduced later when
the destructive HK operations (give-credits, delete-room, reset-pwd)
go in.

Reserves the 9100..9199 / 9200..9299 ID blocks for the rest of the
HK packet surface (search-prefix, find-by-id, ban/mute/kick with
arbitrary duration, room actions, economy, catalog admin, dashboard
aggregate, audit log read).

`mvn compile` clean on Habbo 4.2.12.
2026-05-24 16:29:54 +02:00
48 changed files with 2687 additions and 159 deletions
@@ -0,0 +1,17 @@
INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) VALUES ('acc_housekeeping', '1', 'Allow housekeeping in the client', '0', '0', '0', '0', '0', '0', '1');
CREATE TABLE IF NOT EXISTS `housekeeping_log` (
`id` INT NOT NULL AUTO_INCREMENT,
`timestamp` INT NOT NULL,
`actor_id` INT NOT NULL,
`actor_name` VARCHAR(64) NOT NULL DEFAULT '',
`target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
`target_id` INT NOT NULL DEFAULT 0,
`target_label` VARCHAR(128) NOT NULL DEFAULT '',
`action` VARCHAR(64) NOT NULL DEFAULT '',
`detail` VARCHAR(500) NOT NULL DEFAULT '',
`success` TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+70
View File
@@ -0,0 +1,70 @@
ALTER TABLE `bots`
MODIFY COLUMN `type` ENUM('generic','visitor_log','bartender','weapons_dealer','frank')
NOT NULL DEFAULT 'generic';
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`)
VALUES
('acc_bot_frank', 1, 'Required to purchase the Frank mascot bot from the catalog.',
0, 0, 0, 0, 0, 0, 1)
ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`);
CREATE TABLE IF NOT EXISTS `bot_chat_responses` (
`id` INT NOT NULL AUTO_INCREMENT,
`bot_type` VARCHAR(32) NOT NULL,
`keys` VARCHAR(255) NOT NULL COMMENT 'semicolon-separated trigger words',
`responses` TEXT NOT NULL COMMENT 'newline-separated replies; bot picks one at random',
PRIMARY KEY (`id`),
KEY `bot_type` (`bot_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
('frank', '__door_triggers', 'show me the door\nkick me\ni want to leave\nlet me out'),
('frank', '__door_lines', 'Right this way - mind the step!\nAnd out you go. Come back soon!\nAllow me to escort you to the exit.\nThere''s the door. Farewell, true believer!'),
('frank', '__busy_whisper', 'Sorry, I am currently busy. Please wait until I am available.'),
('frank', 'frank', 'Hello, I''m Frank! Welcome to Habbo.'),
('frank', 'help', 'What do you need help with?'),
('frank', 'thanks;thank you', 'Just doing my job, true believer!'),
('frank', 'new', 'Welcome to Habbo! I hope you have a great time here.'),
('frank', 'rooms', 'Looking for somewhere fun? Try the Navigator - thousands of rooms to explore!'),
('frank', 'sulake', 'Sulake is the company behind Habbo. Take a look: https://www.sulake.com'),
('frank', 'vip;hc', 'VIP gets you more outfits, more furni, more everything. Worth it!'),
('frank', 'music', 'Snoop Dogg, Frank Sinatra and a little Beethoven on Sundays.'),
('frank', 'movie', 'I''m a Casablanca man. Black and white films are an underrated art.'),
('frank', 'game', 'Battleship. Always Battleship.'),
('frank', 'snowstorm', 'Honestly? I''m terrible at Snowstorm. Don''t tell anyone.'),
('frank', 'furni', 'Best furniture maker in town - hands down, the folks at Sulake.'),
('frank', 'animal;cat;pet','I have a cat called Mr. Whiskers. He runs the place, really.'),
('frank', 'miranda', 'Miranda. The love of my life. Don''t get me started.'),
('frank', 'frank black', 'Named after the man himself. Frank Black is a hero of mine.'),
('frank', 'life', 'Life is like a bowl of popcorn - warm, salty and buttery.'),
('frank', 'job;work', 'I''m sure you can find work in one of the guest rooms!'),
('frank', 'snouthill', 'Snouthill... so many memories.'),
('frank', 'wife', 'I had a wife once. She broke my stereo.'),
('frank', 'baseball', 'Oh, I used to love to go down to the old ball park and watch Christy Mathewson and Honus Wagner at bat.'),
('frank', 'mark', 'I don''t trust Mark.'),
('frank', 'vietnam', 'Vietnam? Don''t ask. Worst trip of my life.'),
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
('sunflower', 21),
('cola;habbo cola', 32),
('rose', 1000),
('book', 20),
('tea', 6),
('coffee', 1),
('migraine;headache;pills', 34),
('radioactive liquid;radioactive', 36),
('turkey;can of turkey', 38);
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
-- After the SQL please go to the catalog_items table and change the page_id to where your BOTS are located
INSERT IGNORE INTO `items_base` (`id`, `sprite_id`, `item_name`, `public_name`, `width`, `length`, `stack_height`, `allow_stack`, `allow_sit`, `allow_lay`, `allow_walk`, `allow_gift`, `allow_trade`, `allow_recycle`, `allow_marketplace_sell`, `allow_inventory_stack`, `type`, `interaction_type`, `interaction_modes_count`, `vending_ids`, `multiheight`, `customparams`)
VALUES (99001, 0, 'bot_frank', 'Frank', 1, 1, 0.00, '0', '0', '0', '1', '0', '0', '0', '0', '0', 'r', 'default', 1, '0', '0', 'name:Frank;motto:Welcome to Habbo!;figure:hr-3499-33.sh-290-90.ch-3971-72-73.lg-270-73.hd-205-1-1.fa-1206-67.ha-3409-73-72;gender:M');
INSERT IGNORE INTO `catalog_items` (`item_ids`, `page_id`, `offer_id`, `catalog_name`, `cost_credits`, `cost_points`, `points_type`, `amount`, `extradata`)
VALUES ('99001', 1, 99001, 'Frank', 0, 0, 0, 1, '0');
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.17</version>
<version>4.2.22</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -539,5 +539,9 @@ public class Bot implements Runnable {
}
}
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
public short[] getOwnerActionIds() {
return DEFAULT_OWNER_ACTION_IDS;
}
}
@@ -41,6 +41,7 @@ public class BotManager {
addBotDefinition("generic", Bot.class);
addBotDefinition("bartender", ButlerBot.class);
addBotDefinition("visitor_log", VisitorBot.class);
addBotDefinition(FrankBot.BOT_TYPE, FrankBot.class);
this.reload();
@@ -0,0 +1,449 @@
package com.eu.habbo.habbohotel.bots;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserWhisperComposer;
import com.eu.habbo.threading.runnables.RoomUnitWalkToLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
public class FrankBot extends ButlerBot {
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
public static final String BOT_TYPE = "frank";
public static final String PERMISSION_USE = "acc_bot_frank";
private static final String KEY_DOOR_LINES = "__door_lines";
private static final String KEY_BUSY_WHISPER = "__busy_whisper";
private static final String KEY_DOOR_TRIGGERS = "__door_triggers";
private static final List<String> DEFAULT_DOOR_LINES = List.of(
"Right this way - mind the step!",
"And out you go. Come back soon!",
"Allow me to escort you to the exit.",
"There's the door. Farewell, true believer!"
);
private static final String DEFAULT_BUSY_WHISPER =
"Sorry, I am currently busy. Please wait until I am available.";
private static final Pattern DEFAULT_DOOR_PATTERN = Pattern.compile(
"\\b(show me the door|kick me|i want to leave|let me out)\\b");
private static final ConcurrentHashMap<Pattern, List<String>> chatResponses = new ConcurrentHashMap<>();
private static volatile List<String> doorLines = DEFAULT_DOOR_LINES;
private static volatile String busyWhisper = DEFAULT_BUSY_WHISPER;
private static volatile Pattern doorTriggerPattern = DEFAULT_DOOR_PATTERN;
private static final Random RANDOM = new Random();
private static final int MAX_CHAT_KEYWORDS = 256;
private static final int MAX_DOOR_TRIGGERS = 32;
private static final int MAX_MESSAGE_LEN = 256;
private static final long BUSY_WHISPER_COOLDOWN_MS = 5000L;
private volatile RoomTile homeTile;
private volatile RoomUserRotation homeRotation;
private final AtomicBoolean busy = new AtomicBoolean(false);
private final AtomicBoolean returnScheduled = new AtomicBoolean(false);
private final ConcurrentHashMap<Integer, Long> lastBusyWhisperAt = new ConcurrentHashMap<>();
public FrankBot(ResultSet set) throws SQLException {
super(set);
}
public FrankBot(Bot bot) {
super(bot);
}
@Override
public void onPlace(Habbo habbo, Room room) {
super.onPlace(habbo, room);
if (this.getRoomUnit() != null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
}
private static final short[] FRANK_OWNER_ACTIONS = new short[0];
@Override
public short[] getOwnerActionIds() {
return FRANK_OWNER_ACTIONS;
}
public static void initialise() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement();
ResultSet set = statement.executeQuery("SELECT `keys`, `responses` FROM bot_chat_responses WHERE bot_type = '" + BOT_TYPE + "'")) {
while (set.next()) {
String keysRaw = set.getString("keys");
String responsesRaw = set.getString("responses");
if (keysRaw == null || responsesRaw == null) continue;
List<String> responses = new ArrayList<>();
for (String line : responsesRaw.split("\n")) {
String trimmed = line.trim();
if (!trimmed.isEmpty()) responses.add(trimmed);
}
if (responses.isEmpty()) continue;
String firstKey = keysRaw.split(";", 2)[0].trim();
if (firstKey.startsWith("__")) {
switch (firstKey) {
case KEY_DOOR_LINES:
doorLines = new CopyOnWriteArrayList<>(responses);
break;
case KEY_BUSY_WHISPER:
busyWhisper = responses.get(0);
break;
case KEY_DOOR_TRIGGERS:
doorTriggerPattern = buildDoorTriggerPattern(responses);
break;
default:
LOGGER.warn("FrankBot: unknown system key '{}', ignored", firstKey);
}
continue;
}
List<String> shared = new CopyOnWriteArrayList<>(responses);
for (String key : keysRaw.split(";")) {
if (chatResponses.size() >= MAX_CHAT_KEYWORDS) {
LOGGER.warn("FrankBot: chat keyword cap ({}) reached, remaining rows ignored",
MAX_CHAT_KEYWORDS);
break;
}
String k = key == null ? "" : key.trim().toLowerCase();
if (k.isEmpty()) continue;
try {
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(k) + "\\b");
chatResponses.put(pattern, shared);
} catch (Exception e) {
LOGGER.error("Failed to compile Frank chat keyword pattern: {}", k, e);
}
}
}
} catch (SQLException e) {
LOGGER.warn("FrankBot: could not load bot_chat_responses ({}). Frank will still serve items.", e.getMessage());
}
ButlerBot.initialise();
}
public static void dispose() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
busyWhisper = DEFAULT_BUSY_WHISPER;
doorTriggerPattern = DEFAULT_DOOR_PATTERN;
ButlerBot.dispose();
}
private static Pattern buildDoorTriggerPattern(List<String> triggers) {
StringBuilder sb = new StringBuilder("\\b(");
boolean first = true;
int count = 0;
for (String trigger : triggers) {
if (count >= MAX_DOOR_TRIGGERS) {
LOGGER.warn("FrankBot: door trigger cap ({}) reached, extra entries ignored",
MAX_DOOR_TRIGGERS);
break;
}
String t = trigger == null ? "" : trigger.trim().toLowerCase();
if (t.isEmpty()) continue;
if (!first) sb.append('|');
sb.append(Pattern.quote(t));
first = false;
count++;
}
sb.append(")\\b");
if (first) return DEFAULT_DOOR_PATTERN;
try {
return Pattern.compile(sb.toString());
} catch (Exception e) {
LOGGER.error("FrankBot: failed to compile door trigger pattern from {}, falling back to default", triggers, e);
return DEFAULT_DOOR_PATTERN;
}
}
@Override
public void onUserSay(final RoomChatMessage message) {
Room currentRoom = this.getRoom();
if (currentRoom == null) return;
Habbo asker = message.getHabbo();
if (asker == null || asker.getClient() == null) return;
if (this.getRoomUnit() == null) return;
String raw = message.getUnfilteredMessage();
if (raw != null && raw.length() > MAX_MESSAGE_LEN) return;
if (this.homeTile == null) {
this.homeTile = this.getRoomUnit().getCurrentLocation();
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
if (this.busy.get() || this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
if (raw != null) {
double distance = this.getRoomUnit().getCurrentLocation().distance(asker.getRoomUnit().getCurrentLocation());
int commandDistance = Emulator.getConfig().getInt("hotel.bot.butler.commanddistance");
if (distance <= commandDistance) {
String lower = raw.toLowerCase();
if (doorTriggerPattern.matcher(lower).find()) {
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
this.showToTheDoor(asker);
return;
}
for (java.util.Map.Entry<Pattern, List<String>> entry : chatResponses.entrySet()) {
if (entry.getKey().matcher(lower).find()) {
List<String> options = entry.getValue();
if (options.isEmpty()) continue;
String reply = options.get(RANDOM.nextInt(options.size()));
this.talk(reply);
return;
}
}
}
}
if (!this.busy.compareAndSet(false, true)) {
this.whisperThrottled(asker, busyWhisper);
return;
}
super.onUserSay(message);
this.schedulePostServeReturn(currentRoom.getId(), 0);
}
private void whisperThrottled(Habbo target, String text) {
if (target == null || text == null || text.isEmpty() || this.getRoomUnit() == null) return;
int userId = target.getHabboInfo().getId();
long now = System.currentTimeMillis();
Long last = lastBusyWhisperAt.get(userId);
if (last != null && (now - last) < BUSY_WHISPER_COOLDOWN_MS) return;
lastBusyWhisperAt.put(userId, now);
RoomChatMessage msg = new RoomChatMessage(text, this.getRoomUnit(), RoomChatMessageBubbles.BOT);
target.getClient().sendResponse(new RoomUserWhisperComposer(msg));
}
private void showToTheDoor(final Habbo target) {
final Room room = this.getRoom();
if (room == null || room.getLayout() == null || target == null) {
this.busy.set(false);
return;
}
final RoomTile doorTile = room.getLayout().getDoorTile();
if (doorTile == null) {
this.busy.set(false);
return;
}
this.lookAt(target);
List<String> lines = doorLines;
String line = lines.isEmpty() ? DEFAULT_DOOR_LINES.get(RANDOM.nextInt(DEFAULT_DOOR_LINES.size()))
: lines.get(RANDOM.nextInt(lines.size()));
this.talk(line);
final int targetId = target.getHabboInfo().getId();
final int roomId = room.getId();
final AtomicBoolean fired = new AtomicBoolean(false);
final Runnable kickThenReturn = () -> {
if (!fired.compareAndSet(false, true)) return;
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) {
this.busy.set(false);
return;
}
Habbo stillHere = currentRoom.getHabbo(targetId);
if (stillHere != null) {
currentRoom.kickHabbo(stillHere, false);
}
this.scheduleReturnHome(targetId, roomId, 0);
};
if (this.getRoomUnit().canWalk() && !this.getRoomUnit().getCurrentLocation().equals(doorTile)) {
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(kickThenReturn);
List<Runnable> onFail = new ArrayList<>();
onFail.add(() -> Emulator.getThreading().run(kickThenReturn, 1500));
this.getRoomUnit().setGoalLocation(doorTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), doorTile, room, onArrive, onFail));
} else {
Emulator.getThreading().run(kickThenReturn, 1500);
}
}
private static final int RETURN_HOME_POLL_MS = 500;
private static final int RETURN_HOME_MAX_WAIT_MS = 8000;
private static final int POST_SERVE_POLL_MS = 750;
private static final int POST_SERVE_MAX_WAIT_MS = 30000;
private void schedulePostServeReturn(final int roomId, final int waitedMs) {
if (waitedMs == 0 && !this.returnScheduled.compareAndSet(false, true)) {
return;
}
if (waitedMs >= POST_SERVE_MAX_WAIT_MS) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
Emulator.getThreading().run(() -> {
Room r = this.getRoom();
if (r == null || r.getId() != roomId || this.getRoomUnit() == null || this.homeTile == null) {
this.returnScheduled.set(false);
this.busy.set(false);
return;
}
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null && this.getRoomUnit().getBodyRotation() != this.homeRotation) {
this.getRoomUnit().setRotation(this.homeRotation);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
} else {
this.busy.set(false);
}
this.returnScheduled.set(false);
return;
}
boolean stillWalking = this.getRoomUnit().hasStatus(RoomUnitStatus.MOVE)
|| (this.getRoomUnit().getPath() != null && !this.getRoomUnit().getPath().isEmpty());
if (stillWalking) {
this.schedulePostServeReturn(roomId, waitedMs + POST_SERVE_POLL_MS);
return;
}
this.returnScheduled.set(false);
this.returnHome(-1, false);
}, POST_SERVE_POLL_MS);
}
private void scheduleReturnHome(final int kickedHabboId, final int roomId, final int waitedMs) {
Room currentRoom = this.getRoom();
if (currentRoom == null || currentRoom.getId() != roomId) return;
boolean stillEscorting = currentRoom.getHabbo(kickedHabboId) != null;
if (!stillEscorting || waitedMs >= RETURN_HOME_MAX_WAIT_MS) {
this.returnHome(kickedHabboId, true);
return;
}
Emulator.getThreading().run(
() -> this.scheduleReturnHome(kickedHabboId, roomId, waitedMs + RETURN_HOME_POLL_MS),
RETURN_HOME_POLL_MS);
}
private void returnHome(int kickedHabboId, boolean alwaysTeleport) {
final Room room = this.getRoom();
if (room == null || this.homeTile == null || this.getRoomUnit() == null) {
this.busy.set(false);
return;
}
final Runnable teleportHome = () -> {
Room r = this.getRoom();
if (r == null || this.getRoomUnit() == null) return;
double homeZ = r.getTopHeightAt(this.homeTile.x, this.homeTile.y);
this.getRoomUnit().stopWalking();
this.getRoomUnit().setZ(homeZ);
this.getRoomUnit().setLocation(this.homeTile);
this.getRoomUnit().setPreviousLocationZ(homeZ);
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
}
this.getRoomUnit().statusUpdate(true);
r.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
this.persistPosition();
};
if (this.getRoomUnit().getCurrentLocation().equals(this.homeTile)) {
if (this.homeRotation != null) {
this.getRoomUnit().setRotation(this.homeRotation);
room.sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
return;
}
boolean hasOtherWatchers = false;
for (Habbo h : room.getCurrentHabbos().values()) {
if (h.getHabboInfo().getId() != kickedHabboId) {
hasOtherWatchers = true;
break;
}
}
if (alwaysTeleport || !hasOtherWatchers || !this.getRoomUnit().canWalk()) {
teleportHome.run();
return;
}
List<Runnable> onArrive = new ArrayList<>();
onArrive.add(() -> {
if (this.homeRotation != null && this.getRoom() != null) {
this.getRoomUnit().setRotation(this.homeRotation);
this.getRoom().sendComposer(new RoomUserStatusComposer(this.getRoomUnit()).compose());
}
this.persistPosition();
});
List<Runnable> onFail = new ArrayList<>();
onFail.add(teleportHome);
this.getRoomUnit().setGoalLocation(this.homeTile);
Emulator.getThreading().run(
new RoomUnitWalkToLocation(this.getRoomUnit(), this.homeTile, room, onArrive, onFail));
}
private void persistPosition() {
this.needsUpdate(true);
this.run();
this.busy.set(false);
}
}
@@ -1046,10 +1046,22 @@ public class CatalogManager {
for (Item baseItem : item.getBaseItems()) {
for (int k = 0; k < item.getItemAmount(baseItem.getId()); k++) {
if (baseItem.getName().startsWith("rentable_bot_") || baseItem.getName().startsWith("bot_")) {
String baseName = baseItem.getName();
String type = item.getName().replace("rentable_bot_", "");
type = type.replace("bot_", "");
type = type.replace("visitor_logger", "visitor_log");
// Permission gate keyed on the canonical base-item name
// (admin-controlled but stable), not the catalog page name
// which can be renamed and bypass the check.
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
habbo.getClient().sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
return;
}
}
THashMap<String, String> data = new THashMap<>();
for (String s : item.getExtradata().split(";")) {
@@ -48,6 +48,12 @@ public class Item implements ISerialize {
return item.getName().toLowerCase().startsWith("a0 pet");
}
public static boolean isBot(Item item) {
if (item == null) return false;
String name = item.getName();
return name != null && (name.startsWith("bot_") || name.startsWith("rentable_bot_"));
}
public static double getCurrentHeight(HabboItem item) {
if (item instanceof InteractionMultiHeight && item.getBaseItem().getMultiHeights().length > 0) {
if (item.getExtradata().isEmpty()) {
@@ -8,6 +8,7 @@ public class Permission {
public static String ACC_SEE_WHISPERS = "acc_see_whispers";
public static String ACC_SEE_TENTCHAT = "acc_see_tentchat";
public static String ACC_SUPERWIRED = "acc_superwired";
public static String ACC_HOUSEKEEPING = "acc_housekeeping";
public static String ACC_SUPPORTTOOL = "acc_supporttool";
public static String ACC_UNKICKABLE = "acc_unkickable";
public static String ACC_GUILDGATE = "acc_guildgate";
@@ -89,6 +89,7 @@ public class HabboStats implements Runnable {
public long lastTradeTimestamp = Emulator.getIntUnixTimestamp();
public long lastGiftTimestamp = Emulator.getIntUnixTimestamp();
public long lastPurchaseTimestamp = Emulator.getIntUnixTimestamp();
public long lastFloorplanSaveTimestamp = 0;
public int uiFlags;
public boolean hasGottenDefaultSavedSearches;
private HabboInfo habboInfo;
@@ -719,5 +719,31 @@ public class PacketManager {
this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class);
this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class);
this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class);
// Housekeeping (in-client admin panel)
this.registerHandler(Incoming.HousekeepingFindUserByNameEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByNameEvent.class);
this.registerHandler(Incoming.HousekeepingFindUserByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByIdEvent.class);
this.registerHandler(Incoming.HousekeepingBanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingBanUserEvent.class);
this.registerHandler(Incoming.HousekeepingUnbanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingUnbanUserEvent.class);
this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class);
this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class);
this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class);
this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class);
this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class);
this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class);
this.registerHandler(Incoming.HousekeepingFindRoomByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindRoomByIdEvent.class);
this.registerHandler(Incoming.HousekeepingSearchRoomsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSearchRoomsEvent.class);
this.registerHandler(Incoming.HousekeepingRoomStateEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingRoomStateEvent.class);
this.registerHandler(Incoming.HousekeepingMuteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteRoomEvent.class);
this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class);
this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class);
this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class);
this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class);
this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class);
this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class);
this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class);
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
}
}
@@ -460,4 +460,30 @@ public class Incoming {
public static final int YouTubeRoomPlayEvent = 8001;
public static final int YouTubeRoomWatchingEvent = 8002;
public static final int YouTubeRoomSettingsEvent = 8003;
// Housekeeping (in-client admin panel) IDs 9100..9199 reserved
public static final int HousekeepingFindUserByNameEvent = 9100;
public static final int HousekeepingFindUserByIdEvent = 9101;
public static final int HousekeepingBanUserEvent = 9102;
public static final int HousekeepingUnbanUserEvent = 9103;
public static final int HousekeepingMuteUserEvent = 9104;
public static final int HousekeepingKickUserEvent = 9105;
public static final int HousekeepingForceDisconnectUserEvent = 9106;
public static final int HousekeepingSetUserRankEvent = 9107;
public static final int HousekeepingTradeLockUserEvent = 9108;
public static final int HousekeepingResetUserPasswordEvent = 9109;
public static final int HousekeepingFindRoomByIdEvent = 9110;
public static final int HousekeepingSearchRoomsEvent = 9111;
public static final int HousekeepingRoomStateEvent = 9112;
public static final int HousekeepingMuteRoomEvent = 9113;
public static final int HousekeepingKickAllFromRoomEvent = 9114;
public static final int HousekeepingTransferRoomOwnershipEvent = 9115;
public static final int HousekeepingDeleteRoomEvent = 9116;
public static final int HousekeepingGiveCreditsEvent = 9117;
public static final int HousekeepingGiveCurrencyEvent = 9118;
public static final int HousekeepingGrantItemEvent = 9119;
public static final int HousekeepingSetHcSubscriptionEvent = 9120;
public static final int HousekeepingSendHotelAlertEvent = 9121;
public static final int HousekeepingGetDashboardEvent = 9122;
public static final int HousekeepingListActionLogEvent = 9123;
}
@@ -175,6 +175,19 @@ public class CatalogBuyItemAsGiftEvent extends MessageHandler {
CatalogItem item = page.getCatalogItem(itemId);
// Search-results gift sends the catalog offer_id as
// itemId, not catalog_items.id - see the same fix in
// CatalogBuyItemEvent. Fall back to scanning the
// page for the matching offer_id.
if (item == null) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) {
item = candidate;
break;
}
}
}
if (item == null) {
LOGGER.debug("catalog item null -> {}", itemId);
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
@@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.bots.BotManager;
import com.eu.habbo.habbohotel.catalog.*;
import com.eu.habbo.habbohotel.catalog.layouts.*;
import com.eu.habbo.habbohotel.items.FurnitureType;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.pets.PetManager;
import com.eu.habbo.habbohotel.rooms.BuildersClubRoomSupport;
@@ -201,15 +202,48 @@ public class CatalogBuyItemEvent extends MessageHandler {
else
item = page.getCatalogItem(itemId);
// temp patch, can a dev with better knowledge than me look into this asap pls.
if (page instanceof BotsLayout) {
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS) && this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
return;
// Search-results buy sends the catalog offer_id as itemId
// (FurnitureOffer.offerId is derived from furnidata's
// purchaseOfferId, which matches `catalog_items.offer_id`),
// not the `catalog_items.id` primary key that getCatalogItem
// expects. Fall back to scanning the page for the matching
// offer_id so the search buy flow works.
if (item == null && !(page instanceof RecentPurchasesLayout)) {
for (CatalogItem candidate : page.getCatalogItems().valueCollection()) {
if (candidate != null && candidate.getOfferId() == itemId) {
item = candidate;
break;
}
}
}
if (page instanceof PetsLayout) {
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
// Inventory cap check based on the actual base items the
// purchase will create, not the page layout - bots/pets
// can legitimately live on bundle pages, search results,
// recent-purchases, etc., and the layout-instanceof check
// missed all those paths. Mirrors the bot/pet branches
// inside CatalogManager.purchaseItem (Item.isBot / isPet
// and the same prefix check) so detection stays in sync.
boolean itemHasBot = false;
boolean itemHasPet = false;
if (item != null) {
for (Item baseItem : item.getBaseItems()) {
if (baseItem == null) continue;
if (Item.isBot(baseItem)) itemHasBot = true;
if (Item.isPet(baseItem)) itemHasPet = true;
}
}
if (itemHasBot && !this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_BOTS)
&& this.client.getHabbo().getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
return;
}
if (itemHasPet) {
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS)
&& this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
return;
}
@@ -4,23 +4,36 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.*;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer;
import com.eu.habbo.messages.outgoing.inventory.AddHabboItemComposer;
import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer;
import com.eu.habbo.messages.outgoing.rooms.ForwardToRoomComposer;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Pattern;
public class FloorPlanEditorSaveEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(FloorPlanEditorSaveEvent.class);
public static int MAXIMUM_FLOORPLAN_WIDTH_LENGTH = 64;
public static int MAXIMUM_FLOORPLAN_SIZE = 64 * 64;
private static final int SAVE_COOLDOWN_SECONDS = 3;
private static final int MAX_AUTO_PICKUP_ITEMS = 500;
private static final Pattern ALLOWED_MAP_CHARS = Pattern.compile("[a-zA-Z0-9\r]+");
@Override
public int getRatelimit() {
return 500;
@@ -38,153 +51,244 @@ public class FloorPlanEditorSaveEvent extends MessageHandler {
if (room == null)
return;
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
StringJoiner errors = new StringJoiner("<br />");
String map = this.packet.readString();
map = map.replace("X", "x");
if (!(room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER))) {
return;
}
String[] mapRows = map.split("\r");
long now = Emulator.getIntUnixTimestamp();
if (now - this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp < SAVE_COOLDOWN_SECONDS) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, "Please wait a few seconds before saving again."));
return;
}
int firstRowSize = mapRows[0].length();
StringJoiner errors = new StringJoiner("<br />");
String map = this.packet.readString();
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
if (!map.matches("[a-zA-Z0-9\r]+")) errors.add("${notification.floorplan_editor.error.title}");
if (map == null || map.length() > MAXIMUM_FLOORPLAN_SIZE) {
LOGGER.warn("Floorplan save rejected (oversize): user={} room={} mapLen={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map == null ? 0 : map.length());
return;
}
Arrays.stream(mapRows)
.filter(line -> line.length() != firstRowSize)
.findAny()
.ifPresent(s -> errors.add("(General): Line " + (Arrays.asList(mapRows).indexOf(s) + 1) + " is of different length than line 1"));
if (!ALLOWED_MAP_CHARS.matcher(map).matches()) {
LOGGER.warn("Floorplan save rejected (illegal chars): user={} room={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId());
return;
}
if (map.isEmpty() || map.replace("x", "").replace("\r", "").isEmpty()) {
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
map = map.replace("X", "x");
String[] mapRows = map.split("\r");
if (mapRows.length == 0 || mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
return;
}
int firstRowSize = mapRows[0].length();
if (firstRowSize == 0 || firstRowSize > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) {
return;
}
for (String row : mapRows) {
if (row.length() != firstRowSize) {
return;
}
}
if (Emulator.getConfig().getBoolean("hotel.room.floorplan.check.enabled")) {
if (map.replace("x", "").replace("\r", "").isEmpty()) {
errors.add("${notification.floorplan_editor.error.message.effective_height_is_0}");
}
}
int doorX = this.packet.readInt();
int doorY = this.packet.readInt();
if (doorX < 0 || doorX >= firstRowSize || doorY < 0 || doorY >= mapRows.length) {
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
} else if (mapRows[doorY].charAt(doorX) == 'x') {
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
}
int doorRotation = this.packet.readInt();
if (doorRotation < 0 || doorRotation > 7) {
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
}
int wallSize = this.packet.readInt();
if (wallSize < -2 || wallSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
}
int floorSize = this.packet.readInt();
if (floorSize < -2 || floorSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
}
int wallHeight = -1;
if (this.packet.bytesAvailable() >= 4)
wallHeight = this.packet.readInt();
if (wallHeight < -1 || wallHeight > 15) {
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
}
boolean autoPickup = false;
if (this.packet.bytesAvailable() >= 1) {
autoPickup = this.packet.readBoolean();
}
if (errors.length() > 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
return;
}
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
THashSet<RoomTile> new_tileList = new THashSet<>();
THashSet<HabboItem> itemsToPickup = new THashSet<>();
int blockedX = -1;
int blockedY = -1;
blockingRoomItemScan:
for (int y = 0; y < mapRows.length; y++) {
for (int x = 0; x < firstRowSize; x++) {
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
new_tileList.add(tile);
String square = String.valueOf(mapRows[y].charAt(x));
short height;
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
if (autoPickup) {
THashSet<HabboItem> here = room.getItemsAt(x, y);
if (here != null) itemsToPickup.addAll(here);
continue;
}
blockedX = x;
blockedY = y;
break blockingRoomItemScan;
}
if (map.length() > MAXIMUM_FLOORPLAN_SIZE) {
errors.add("${notification.floorplan_editor.error.message.too_large_area}");
}
if (mapRows.length > MAXIMUM_FLOORPLAN_WIDTH_LENGTH) errors.add("${notification.floorplan_editor.error.message.too_large_height}");
else if (Arrays.stream(mapRows).anyMatch(l -> l.length() > MAXIMUM_FLOORPLAN_WIDTH_LENGTH || l.isEmpty())) errors.add("${notification.floorplan_editor.error.message.too_large_width}");
if (errors.length() > 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
try {
if (square.isEmpty()) {
height = 0;
} else if (Emulator.isNumeric(square)) {
height = Short.parseShort(square);
} else {
int idx = "abcdefghijklmnopqrstuvwxyz".indexOf(square.toLowerCase());
if (idx < 0) {
return;
}
height = (short) Math.min(26, 10 + idx);
}
} catch (NumberFormatException e) {
return;
}
}
int doorX = this.packet.readInt();
int doorY = this.packet.readInt();
if (doorX < 0 || doorX > firstRowSize || doorY < 0 || doorY >= mapRows.length) {
errors.add("${notification.floorplan_editor.error.message.entry_tile_outside_map}");
}
if (doorY < mapRows.length && doorX < mapRows[doorY].length() && mapRows[doorY].charAt(doorX) == 'x') {
errors.add("${notification.floorplan_editor.error.message.entry_not_on_tile}");
}
int doorRotation = this.packet.readInt();
if (doorRotation < 0 || doorRotation > 7) {
errors.add("${notification.floorplan_editor.error.message.invalid_entry_tile_direction}");
}
int wallSize = this.packet.readInt();
if (wallSize < -2 || wallSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_wall_thickness}");
}
int floorSize = this.packet.readInt();
if (floorSize < -2 || floorSize > 1) {
errors.add("${notification.floorplan_editor.error.message.invalid_floor_thickness}");
}
int wallHeight = -1;
if (this.packet.bytesAvailable() >= 4)
wallHeight = this.packet.readInt();
if (wallHeight < -1 || wallHeight > 15) {
errors.add("${notification.floorplan_editor.error.message.invalid_walls_fixed_height}");
}
THashSet<RoomTile> locked_tileList = room.getLockedTiles();
THashSet<RoomTile> new_tileList = new THashSet<>();
blockingRoomItemScan:
for (int y = 0; y < mapRows.length; y++) {
for (int x = 0; x < firstRowSize; x++) {
RoomTile tile = room.getLayout().getTile((short) x, (short) y);
new_tileList.add(tile);
String square = String.valueOf(mapRows[y].charAt(x));
short height;
if (square.equalsIgnoreCase("x") && room.getTopItemAt(x, y) != null) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
break blockingRoomItemScan;
} else {
if (square.isEmpty()) {
height = 0;
} else if (Emulator.isNumeric(square)) {
height = Short.parseShort(square);
} else {
height = (short) (10 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".indexOf(square.toUpperCase()));
}
}
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
break blockingRoomItemScan;
if (tile != null && tile.state != RoomTileState.INVALID && height != tile.z && room.getTopItemAt(x, y) != null) {
if (autoPickup) {
THashSet<HabboItem> here = room.getItemsAt(x, y);
if (here != null) itemsToPickup.addAll(here);
continue;
}
blockedX = x;
blockedY = y;
break blockingRoomItemScan;
}
}
}
if (blockedX < 0) {
locked_tileList.removeAll(new_tileList);
if (!locked_tileList.isEmpty()) {
errors.add("${notification.floorplan_editor.error.message.change_blocked_by_room_item}");
if (autoPickup) {
for (RoomTile lt : locked_tileList) {
THashSet<HabboItem> here = room.getItemsAt(lt.x, lt.y);
if (here != null) itemsToPickup.addAll(here);
}
} else {
RoomTile first = locked_tileList.iterator().next();
blockedX = first.x;
blockedY = first.y;
}
}
}
if (blockedX >= 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
"${notification.floorplan_editor.error.message.change_blocked_by_room_item} (" + blockedX + ", " + blockedY + ")"));
return;
}
if (errors.length() > 0) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key, errors.toString()));
if (autoPickup && !itemsToPickup.isEmpty()) {
if (itemsToPickup.size() > MAX_AUTO_PICKUP_ITEMS) {
LOGGER.warn("Floorplan auto-pickup rejected (over cap): user={} room={} itemCount={} cap={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), MAX_AUTO_PICKUP_ITEMS);
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FLOORPLAN_EDITOR_ERROR.key,
"Too many items would be picked up (" + itemsToPickup.size() + " > " + MAX_AUTO_PICKUP_ITEMS + "). Remove some furniture manually and save again."));
return;
}
RoomLayout layout = room.getLayout();
if (layout instanceof CustomRoomLayout) {
layout.setDoorX((short) doorX);
layout.setDoorY((short) doorY);
layout.setDoorDirection(doorRotation);
layout.setHeightmap(map);
layout.parse();
if (layout.getDoorTile() == null) {
this.client.getHabbo().alert("Error");
((CustomRoomLayout) layout).needsUpdate(false);
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
return;
}
((CustomRoomLayout) layout).needsUpdate(true);
Emulator.getThreading().run((CustomRoomLayout) layout);
} else {
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
Map<Integer, ArrayList<HabboItem>> byOwner = new HashMap<>();
for (HabboItem itm : itemsToPickup) {
if (itm == null) continue;
byOwner.computeIfAbsent(itm.getUserId(), k -> new ArrayList<>()).add(itm);
room.pickUpItem(itm, null);
}
if (layout != null) {
room.setHasCustomLayout(true);
room.setNeedsUpdate(true);
room.setLayout(layout);
room.setWallSize(wallSize);
room.setFloorSize(floorSize);
room.setWallHeight(wallHeight);
room.save();
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
habbos.addAll(room.getHabbos());
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
for (Habbo habbo : habbos) {
habbo.getClient().sendResponse(message);
for (Map.Entry<Integer, ArrayList<HabboItem>> entry : byOwner.entrySet()) {
Habbo owner = Emulator.getGameEnvironment().getHabboManager().getHabbo(entry.getKey());
if (owner == null) continue;
for (HabboItem itm : entry.getValue()) {
owner.getClient().sendResponse(new AddHabboItemComposer(itm));
}
owner.getClient().sendResponse(new InventoryRefreshComposer());
}
LOGGER.info("Floorplan auto-pickup: user={} room={} itemCount={} owners={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), itemsToPickup.size(), byOwner.size());
}
RoomLayout layout = room.getLayout();
if (layout instanceof CustomRoomLayout) {
layout.setDoorX((short) doorX);
layout.setDoorY((short) doorY);
layout.setDoorDirection(doorRotation);
layout.setHeightmap(map);
layout.parse();
if (layout.getDoorTile() == null) {
this.client.getHabbo().alert("Error");
((CustomRoomLayout) layout).needsUpdate(false);
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
return;
}
((CustomRoomLayout) layout).needsUpdate(true);
Emulator.getThreading().run((CustomRoomLayout) layout);
} else {
layout = Emulator.getGameEnvironment().getRoomManager().insertCustomLayout(room, map, doorX, doorY, doorRotation);
}
if (layout != null) {
room.setHasCustomLayout(true);
room.setNeedsUpdate(true);
room.setLayout(layout);
room.setWallSize(wallSize);
room.setFloorSize(floorSize);
room.setWallHeight(wallHeight);
room.save();
this.client.getHabbo().getHabboStats().lastFloorplanSaveTimestamp = now;
LOGGER.info("Floorplan saved: user={} room={} mapLen={} rows={} cols={}",
this.client.getHabbo().getHabboInfo().getId(), room.getId(), map.length(), mapRows.length, firstRowSize);
Collection<Habbo> habbos = new ArrayList<>(room.getUserCount());
habbos.addAll(room.getHabbos());
Emulator.getGameEnvironment().getRoomManager().unloadRoom(room);
room = Emulator.getGameEnvironment().getRoomManager().loadRoom(room.getId());
ServerMessage message = new ForwardToRoomComposer(room.getId()).compose();
for (Habbo habbo : habbos) {
habbo.getClient().sendResponse(message);
}
}
}
@@ -0,0 +1,61 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolBan;
import com.eu.habbo.habbohotel.modtool.ModToolBanType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.util.List;
/**
* Apply an arbitrary-duration account ban. Duration is taken in hours
* from the wire and converted to seconds for ModToolManager.ban
* unlike ModToolSanctionBanEvent which only accepts the four fixed
* Habbo-protocol banType buckets.
*/
public class HousekeepingBanUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.ban";
private static final int SECONDS_IN_HOUR = 3600;
// 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban.
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
int hours = this.packet.readInt();
if (userId <= 0 || hours <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
List<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
if (bans == null || bans.isEmpty()) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed"));
return;
}
// ModToolBan doesn't expose the `bans` table autoinc id on the
// object, so we return the target user id as the actionId it's
// the only stable handle the client can use until a dedicated
// housekeeping_log row id supersedes it.
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,68 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Permanently delete a room. Mirrors the minimum-viable subset of
* RequestDeleteRoomEvent: eject all users from the live room, dispose
* + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup
* is intentionally skipped on this slice leftover rows in those
* tables become orphans but don't crash the emulator; a follow-up
* pass can cascade once we have a HK audit-log row to attach the
* orphan-cleanup to.
*/
public class HousekeepingDeleteRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.delete";
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room != null) {
room.ejectAll();
room.preventUnloading = false;
room.dispose();
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) {
statement.setInt(1, roomId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomDetailComposer;
public class HousekeepingFindRoomByIdEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingRoomDetailComposer(null));
return;
}
// loadRoom covers both the in-memory cache (already-loaded rooms)
// and the offline path (SELECT * FROM rooms WHERE id=?). Pass
// false for loadData so we don't pull furni/bots/pets just to
// render an HK panel summary getOwnerName / getUserCount work
// on the pre-loaded skeleton.
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
this.client.sendResponse(new HousekeepingRoomDetailComposer(room));
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
public class HousekeepingFindUserByIdEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
if (userId <= 0) {
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
return;
}
// HabboManager.getHabboInfo(int) returns the in-memory record for
// online users and falls through to the offline SQL lookup
// otherwise, so a single call covers both branches.
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer;
public class HousekeepingFindUserByNameEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
String username = this.packet.readString();
if (username == null || username.isEmpty()) {
this.client.sendResponse(new HousekeepingUserDetailComposer(null));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(username);
HabboInfo info = online != null ? online.getHabboInfo() : HabboManager.getOfflineHabboInfo(username);
this.client.sendResponse(new HousekeepingUserDetailComposer(info));
}
}
@@ -0,0 +1,54 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Force-close the session of an online user. Unlike kick (which only
* removes them from the current room), this drops their socket. Equivalent
* to /disconnect in command form but issued through the HK panel.
*/
public class HousekeepingForceDisconnectUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.disconnect";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
if (userId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (target == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
return;
}
if (reason != null && !reason.isEmpty()) {
target.alert(reason);
}
// ACK first so the action result lands before the target's socket
// closes (otherwise an alerted user on the same emulator thread may
// already be torn down when we try to write).
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
target.disconnect();
}
}
@@ -0,0 +1,91 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingDashboardComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class HousekeepingGetDashboardEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int onlineUsers = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().size();
int activeRooms = 0;
int totalUsers = 0;
int totalRooms = 0;
int pendingTickets = 0;
int sanctionsLast24h = 0;
int now = Emulator.getIntUnixTimestamp();
// activeRooms = loaded rooms with at least one user
try {
for (var room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
if (room != null && room.getUserCount() > 0) activeRooms++;
}
} catch (Exception ignored) {
// fall through with 0
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM users");
ResultSet rs = statement.executeQuery()) {
if (rs.next()) totalUsers = rs.getInt(1);
}
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM rooms");
ResultSet rs = statement.executeQuery()) {
if (rs.next()) totalRooms = rs.getInt(1);
}
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM support_tickets WHERE state = 0");
ResultSet rs = statement.executeQuery()) {
if (rs.next()) pendingTickets = rs.getInt(1);
}
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM bans WHERE timestamp > ?")) {
statement.setInt(1, now - (24 * 3600));
try (ResultSet rs = statement.executeQuery()) {
if (rs.next()) sanctionsLast24h = rs.getInt(1);
}
}
} catch (SQLException ignored) {
// Surface 0s rather than failing the whole dashboard on a missing
// optional table the HK panel can render partial data.
}
int uptime = (int) ((System.currentTimeMillis() - HOUSEKEEPING_BOOT_MILLIS) / 1000);
String version = "Arcturus-Morningstar-Extended";
this.client.sendResponse(new HousekeepingDashboardComposer(
onlineUsers,
totalUsers,
activeRooms,
totalRooms,
onlineUsers, // peakOnlineToday not tracked, use current as best-effort
onlineUsers, // peakOnlineAllTime same
pendingTickets,
sanctionsLast24h,
Math.max(uptime, 0),
version
));
}
// Approximate uptime captured at class-load time rather than emu startup
// (Emulator.java doesn't expose a public startup timestamp). For HK panel
// headline metrics this is close enough; if tighter accuracy is needed
// later, plumb Emulator.startup through and read it here.
private static final long HOUSEKEEPING_BOOT_MILLIS = System.currentTimeMillis();
}
@@ -0,0 +1,62 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class HousekeepingGiveCreditsEvent extends MessageHandler {
private static final String ACTION_KEY = "user.give_credits";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int amount = this.packet.readInt();
if (userId <= 0 || amount == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
// giveCredits already pushes UserCreditsComposer and persists via the
// standard HabboInfo write path; nothing extra needed for the online branch.
online.giveCredits(amount);
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) {
statement.setInt(1, amount);
statement.setInt(2, userId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,74 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Generic non-credits currency grant. Wire field `currencyType`:
* 0 => duckets / pixels, 5 => diamonds, 101 => seasonal-primary.
* Online users go through Habbo.givePoints / givePixels which dispatches
* a UserCurrencyComposer; offline goes straight to `users_currency`.
*/
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
private static final int CURRENCY_DUCKETS = 0;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int currencyType = this.packet.readInt();
int amount = this.packet.readInt();
String actionKey = "user.give_currency_" + currencyType;
if (userId <= 0 || amount == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
// givePixels writes users_currency type=0 and ships UserCurrency;
// givePoints(type, amount) is the generalised path for everything else.
if (currencyType == CURRENCY_DUCKETS) {
online.givePixels(amount);
} else {
online.givePoints(currencyType, amount);
}
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)")) {
statement.setInt(1, userId);
statement.setInt(2, currencyType);
statement.setInt(3, amount);
statement.executeUpdate();
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
}
}
@@ -0,0 +1,62 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Grant a furni item (by items_base id) `quantity` times. Each row in
* the `items` table is one furni unit; quantity > 1 just batches the
* insert. The online user's HabboInventory isn't proactively refreshed
* they'll see the new items next time they open the hand inventory
* (or after a relog).
*/
public class HousekeepingGrantItemEvent extends MessageHandler {
private static final String ACTION_KEY = "user.grant_item";
private static final int MAX_QUANTITY_PER_CALL = 100;
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int itemId = this.packet.readInt();
int quantity = this.packet.readInt();
if (userId <= 0 || itemId <= 0 || quantity <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
if (quantity > MAX_QUANTITY_PER_CALL) {
quantity = MAX_QUANTITY_PER_CALL;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) {
for (int i = 0; i < quantity; i++) {
statement.setInt(1, userId);
statement.setInt(2, itemId);
statement.addBatch();
}
statement.executeBatch();
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.economy_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
public class HousekeepingKickAllFromRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.kick_all";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.ejectAll();
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,60 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Kick a user out of their current room. Mirrors ModToolManager.kick
* (leave room + alert), but the legacy method gates on ACC_SUPPORTTOOL,
* which would force HK operators to also hold the support-tool permission.
* Replicating the few lines locally keeps the HK module self-gated on
* ACC_HOUSEKEEPING.
*/
public class HousekeepingKickUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.kick";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
if (userId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (target == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
return;
}
if (target.hasPermission(Permission.ACC_UNKICKABLE)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable"));
return;
}
if (target.getHabboInfo().getCurrentRoom() != null) {
Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom());
}
if (reason != null && !reason.isEmpty()) {
target.alert(reason);
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,85 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionLogComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Read the housekeeping_log audit table. The table isn't part of the
* base FullDatabase.sql yet operators who want audit have to create
* it once:
*
* CREATE TABLE IF NOT EXISTS `housekeeping_log` (
* `id` INT NOT NULL AUTO_INCREMENT,
* `timestamp` INT NOT NULL,
* `actor_id` INT NOT NULL,
* `actor_name` VARCHAR(64) NOT NULL DEFAULT '',
* `target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
* `target_id` INT NOT NULL DEFAULT 0,
* `target_label` VARCHAR(128) NOT NULL DEFAULT '',
* `action` VARCHAR(64) NOT NULL DEFAULT '',
* `detail` VARCHAR(500) NOT NULL DEFAULT '',
* `success` TINYINT NOT NULL DEFAULT 1,
* PRIMARY KEY (`id`), KEY `timestamp` (`timestamp`)
* ) ENGINE=InnoDB;
*
* If the table is missing we swallow the SQL error and return an empty
* list the panel just shows "no audit entries" instead of breaking.
* Writing into the table is a follow-up: each HK handler will append
* a row once the table exists; for now the listing is read-only.
*/
public class HousekeepingListActionLogEvent extends MessageHandler {
private static final int HARD_LIMIT = 500;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
List<HousekeepingActionLogComposer.Row> rows = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT id, timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success " +
"FROM housekeeping_log ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, limit);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
rows.add(new HousekeepingActionLogComposer.Row(
rs.getInt("id"),
rs.getInt("timestamp"),
rs.getInt("actor_id"),
rs.getString("actor_name"),
rs.getString("target_type"),
rs.getInt("target_id"),
rs.getString("target_label"),
rs.getString("action"),
rs.getString("detail"),
rs.getInt("success") == 1
));
}
}
} catch (SQLException ignored) {
// table absent return empty list, log via emu logger left to the panel UI
}
this.client.sendResponse(new HousekeepingActionLogComposer(rows));
}
}
@@ -0,0 +1,50 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Toggle room-wide mute. Habbo's Room.setMuted is boolean, not duration-
* scoped, so the wire `minutes` arg picks the semantic: minutes==0 =>
* unmute, minutes>0 => mute. An emulator-side scheduled unmute could
* use the value as a timer, but for now the mute stays until the
* operator unmutes manually the minutes is reserved as a forward-
* compat field on the wire.
*/
public class HousekeepingMuteRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.mute";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
int minutes = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.setMuted(minutes > 0);
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,55 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Apply an arbitrary-duration in-room mute. Habbo.mute is a session-only
* mute (it stores remaining seconds on the live Habbo object), so the
* target must be online for the action to take effect when the target
* isn't online the handler returns ok=false with `user_offline` so the
* UI can fall back to ModToolSanctionMute or surface a clear error.
*/
public class HousekeepingMuteUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.mute";
private static final int SECONDS_IN_MINUTE = 60;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
String reason = this.packet.readString();
int minutes = this.packet.readInt();
if (userId <= 0 || minutes <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (target == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline"));
return;
}
target.mute(minutes * SECONDS_IN_MINUTE, false);
if (reason != null && !reason.isEmpty()) {
target.alert(reason);
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,89 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import org.mindrot.jbcrypt.BCrypt;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Reset a user's password to a fresh random 12-character alphanumeric
* string. Persists a BCrypt `$2a$` hash of the new password into
* `users.password` (matches what `AuthHttpUtil.checkPassword` /
* `SessionEndpoints` / `AccountChangeEndpoints` already write and read),
* clears `auth_ticket` so any active session can't be re-used to bypass
* the reset, and ships the PLAINTEXT new password back to the operator
* in the action-result `message` so they can communicate it out-of-band.
*/
public class HousekeepingResetUserPasswordEvent extends MessageHandler {
private static final String ACTION_KEY = "user.reset_password";
private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
private static final int PASSWORD_LENGTH = 12;
private static final int BCRYPT_COST = 10;
private static final SecureRandom RNG = new SecureRandom();
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
if (userId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
String plain = randomPassword();
String hash;
try {
hash = BCrypt.hashpw(plain, BCrypt.gensalt(BCRYPT_COST));
} catch (IllegalArgumentException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed"));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users SET password = ?, auth_ticket = '' WHERE id = ? LIMIT 1")) {
statement.setString(1, hash);
statement.setInt(2, userId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
// Plaintext flows through `message` the client surfaces it via the
// status banner so the operator can read it once. SSL is on the
// operator: the only secure transport for the WS is wss://.
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain));
}
private static String randomPassword() {
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
for (int i = 0; i < PASSWORD_LENGTH; i++) {
sb.append(PASSWORD_ALPHABET.charAt(RNG.nextInt(PASSWORD_ALPHABET.length())));
}
return sb.toString();
}
}
@@ -0,0 +1,49 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomState;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Toggle the room state between OPEN (open) and LOCKED (closed). The
* client picks which transition it wants via the boolean true => OPEN,
* false => LOCKED. Persists state through `Room.save()` so the change
* outlives an unload.
*/
public class HousekeepingRoomStateEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
boolean open = this.packet.readBoolean();
String actionKey = open ? "room.open" : "room.close";
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.setState(open ? RoomState.OPEN : RoomState.LOCKED);
room.save();
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, ""));
}
}
@@ -0,0 +1,75 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomListComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Search rooms by name. `exactMatch=true` => `name = ?` (used by the
* findByName autocomplete that wants a unique hit). `exactMatch=false`
* => `name LIKE concat(?, '%')` (used by the prefix dropdown).
*
* Both branches go through the same packet because the wire shape is
* identical the client picks which mode it wants by toggling the
* boolean.
*/
public class HousekeepingSearchRoomsEvent extends MessageHandler {
private static final int HARD_LIMIT = 50;
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
String query = this.packet.readString();
boolean exactMatch = this.packet.readBoolean();
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
if (query == null) query = "";
query = query.trim();
if (query.isEmpty()) {
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
return;
}
String sql = exactMatch
? "SELECT id FROM rooms WHERE name = ? LIMIT ?"
: "SELECT id FROM rooms WHERE name LIKE ? LIMIT ?";
List<Room> rooms = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, exactMatch ? query : query + "%");
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false);
if (room != null) rooms.add(room);
}
}
} catch (SQLException ignored) {
// fall through with whatever we collected before the failure
}
this.client.sendResponse(new HousekeepingRoomListComposer(rooms));
}
}
@@ -0,0 +1,58 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.StaffAlertWithLinkComposer;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.util.Map;
/**
* Mirrors :ha staff alert with sender attribution, broadcast to
* every online user whose `blockStaffAlerts` flag isn't set. Composed
* once and forwarded by reference (sendResponse compiles to the same
* underlying buffer) so the broadcast is O(N habbos) wire writes,
* not O(N) compose calls.
*/
public class HousekeepingSendHotelAlertEvent extends MessageHandler {
private static final String ACTION_KEY = "hotel.alert";
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
String message = this.packet.readString();
if (message == null || message.trim().isEmpty()) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
return;
}
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
int reached = 0;
for (Map.Entry<Integer, Habbo> entry : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().entrySet()) {
Habbo habbo = entry.getValue();
if (habbo == null || habbo.getClient() == null) continue;
if (habbo.getHabboStats() != null && habbo.getHabboStats().blockStaffAlerts) continue;
habbo.getClient().sendResponse(broadcast);
reached++;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
}
}
@@ -0,0 +1,76 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Extend a user's HC by `days`. Adds to the existing club_expire_timestamp
* if it's still in the future, otherwise stretches from `now`. Days==0
* means cancel the active subscription (timestamp clamped to `now`).
*/
public class HousekeepingSetHcSubscriptionEvent extends MessageHandler {
private static final String ACTION_KEY = "user.set_hc";
private static final int SECONDS_IN_DAY = 24 * 3600;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int days = this.packet.readInt();
if (userId <= 0 || days < 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
int now = Emulator.getIntUnixTimestamp();
int newExpire;
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (days == 0) {
newExpire = now;
} else if (online != null) {
int current = online.getHabboStats().getClubExpireTimestamp();
newExpire = (current > now ? current : now) + (days * SECONDS_IN_DAY);
} else {
newExpire = now + (days * SECONDS_IN_DAY); // best-effort offline; can't read previous expiry cheaply
}
if (online != null) {
online.getHabboStats().setClubExpireTimestamp(newExpire);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) {
statement.setInt(1, newExpire);
statement.setInt(2, userId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,71 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.permissions.PermissionsManager;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class HousekeepingSetUserRankEvent extends MessageHandler {
private static final String ACTION_KEY = "user.set_rank";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int rankId = this.packet.readInt();
if (userId <= 0 || rankId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager();
if (!permissions.rankExists(rankId)) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_not_found"));
return;
}
Rank rank = permissions.getRank(rankId);
// Persist for the offline path. Online users get their in-memory
// HabboInfo.rank rebound below so server-side hasPermission()
// checks land on the new permission set without a relogin.
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users SET rank = ? WHERE id = ? LIMIT 1")) {
statement.setInt(1, rankId);
statement.setInt(2, userId);
statement.execute();
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
online.getHabboInfo().setRank(rank);
// Ship the refreshed permissions snapshot same payload the
// :update_permissions command emits when a rank is rebound.
online.getClient().sendResponse(new UserPermissionsComposer(online));
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,77 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Apply an arbitrary-duration trade lock. Writes
* `users_settings.trade_locked_until = now + hours*3600` so the lock
* survives logout/login that column is the canonical timestamp the
* mod-tool user-info composer queries on. Online users also get their
* in-memory HabboStats.allowTrade flag cleared so the lock takes
* effect on the active session without waiting for a relog.
*/
public class HousekeepingTradeLockUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.trade_lock";
private static final int SECONDS_IN_HOUR = 3600;
private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600;
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
int hours = this.packet.readInt();
String reason = this.packet.readString();
if (userId <= 0 || hours <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
int lockedUntil = Emulator.getIntUnixTimestamp() + duration;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) {
statement.setInt(1, lockedUntil);
statement.setInt(2, userId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (online != null) {
online.getHabboStats().setAllowTrade(false);
if (reason != null && !reason.isEmpty()) {
online.alert(reason);
}
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
@@ -0,0 +1,67 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Transfer ownership of a room to a different user. Updates both
* `rooms.owner_id` and `rooms.owner_name` so the cached owner name on
* the navigator stays in sync without forcing a relog. The room is
* touched via direct SQL rather than via Room.setOwnerId() because
* the room may not be loaded.
*/
public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler {
private static final String ACTION_KEY = "room.transfer";
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
int newOwnerId = this.packet.readInt();
if (roomId <= 0 || newOwnerId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId);
if (newOwner == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.new_owner_not_found"));
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET owner_id = ?, owner_name = ? WHERE id = ? LIMIT 1")) {
statement.setInt(1, newOwnerId);
statement.setString(2, newOwner.getUsername());
statement.setInt(3, roomId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,44 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
public class HousekeepingUnbanUserEvent extends MessageHandler {
private static final String ACTION_KEY = "user.unban";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int userId = this.packet.readInt();
if (userId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId);
if (info == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found"));
return;
}
// ModToolManager.unban only takes a username; the SQL UPDATE
// happens against active bans (ban_expire > now), so calling it
// on a never-banned user is a benign no-op that returns false.
boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername());
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban"));
}
}
@@ -34,6 +34,9 @@ public class BotSaveSettingsEvent extends MessageHandler {
if (bot == null)
return;
if (bot.getOwnerActionIds().length == 0)
return;
int settingId = this.packet.readInt();
switch (settingId) {
@@ -586,4 +586,12 @@ public class Outgoing {
public static final int YouTubeRoomWatchersComposer = 8002;
public static final int YouTubeRoomSettingsComposer = 8003;
// Housekeeping (in-client admin panel) IDs 9200..9299 reserved
public static final int HousekeepingUserDetailComposer = 9200;
public static final int HousekeepingActionResultComposer = 9201;
public static final int HousekeepingRoomDetailComposer = 9202;
public static final int HousekeepingRoomListComposer = 9203;
public static final int HousekeepingDashboardComposer = 9204;
public static final int HousekeepingActionLogComposer = 9205;
}
@@ -0,0 +1,65 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import java.util.List;
public class HousekeepingActionLogComposer extends MessageComposer {
public static class Row {
public final int id;
public final int timestamp;
public final int actorId;
public final String actorName;
public final String targetType;
public final int targetId;
public final String targetLabel;
public final String action;
public final String detail;
public final boolean success;
public Row(int id, int timestamp, int actorId, String actorName, String targetType, int targetId,
String targetLabel, String action, String detail, boolean success) {
this.id = id;
this.timestamp = timestamp;
this.actorId = actorId;
this.actorName = actorName != null ? actorName : "";
this.targetType = targetType != null ? targetType : "user";
this.targetId = targetId;
this.targetLabel = targetLabel != null ? targetLabel : "";
this.action = action != null ? action : "";
this.detail = detail != null ? detail : "";
this.success = success;
}
}
private final List<Row> rows;
public HousekeepingActionLogComposer(List<Row> rows) {
this.rows = rows;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingActionLogComposer);
this.response.appendInt(this.rows != null ? this.rows.size() : 0);
if (this.rows != null) {
for (Row r : this.rows) {
this.response.appendInt(r.id);
this.response.appendInt(r.timestamp);
this.response.appendInt(r.actorId);
this.response.appendString(r.actorName);
this.response.appendString(r.targetType);
this.response.appendInt(r.targetId);
this.response.appendString(r.targetLabel);
this.response.appendString(r.action);
this.response.appendString(r.detail);
this.response.appendBoolean(r.success);
}
}
return this.response;
}
}
@@ -0,0 +1,36 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
/**
* Generic ack for any housekeeping action (ban, mute, kick, give-credits,
* room-close, ). The client matches it back to the originating call via
* the `actionKey` field, which lets multiple in-flight actions share the
* same event stream without ordering bugs.
*/
public class HousekeepingActionResultComposer extends MessageComposer {
private final String actionKey;
private final boolean ok;
private final int actionId;
private final String message;
public HousekeepingActionResultComposer(String actionKey, boolean ok, int actionId, String message) {
this.actionKey = actionKey != null ? actionKey : "";
this.ok = ok;
this.actionId = actionId;
this.message = message != null ? message : "";
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingActionResultComposer);
this.response.appendString(this.actionKey);
this.response.appendBoolean(this.ok);
this.response.appendInt(this.actionId);
this.response.appendString(this.message);
return this.response;
}
}
@@ -0,0 +1,50 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class HousekeepingDashboardComposer extends MessageComposer {
private final int onlineUsers;
private final int totalUsers;
private final int activeRooms;
private final int totalRooms;
private final int peakOnlineToday;
private final int peakOnlineAllTime;
private final int pendingTickets;
private final int sanctionsLast24h;
private final int serverUptimeSeconds;
private final String serverVersion;
public HousekeepingDashboardComposer(int onlineUsers, int totalUsers, int activeRooms, int totalRooms,
int peakOnlineToday, int peakOnlineAllTime, int pendingTickets,
int sanctionsLast24h, int serverUptimeSeconds, String serverVersion) {
this.onlineUsers = onlineUsers;
this.totalUsers = totalUsers;
this.activeRooms = activeRooms;
this.totalRooms = totalRooms;
this.peakOnlineToday = peakOnlineToday;
this.peakOnlineAllTime = peakOnlineAllTime;
this.pendingTickets = pendingTickets;
this.sanctionsLast24h = sanctionsLast24h;
this.serverUptimeSeconds = serverUptimeSeconds;
this.serverVersion = serverVersion != null ? serverVersion : "";
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingDashboardComposer);
this.response.appendInt(this.onlineUsers);
this.response.appendInt(this.totalUsers);
this.response.appendInt(this.activeRooms);
this.response.appendInt(this.totalRooms);
this.response.appendInt(this.peakOnlineToday);
this.response.appendInt(this.peakOnlineAllTime);
this.response.appendInt(this.pendingTickets);
this.response.appendInt(this.sanctionsLast24h);
this.response.appendInt(this.serverUptimeSeconds);
this.response.appendString(this.serverVersion);
return this.response;
}
}
@@ -0,0 +1,49 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomState;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class HousekeepingRoomDetailComposer extends MessageComposer {
private final Room room;
public HousekeepingRoomDetailComposer(Room room) {
this.room = room;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingRoomDetailComposer);
if (this.room == null) {
this.response.appendBoolean(false);
return this.response;
}
this.response.appendBoolean(true);
appendRoomFields(this.response, this.room);
return this.response;
}
/** Shared by HousekeepingRoomListComposer too. */
public static void appendRoomFields(ServerMessage response, Room room) {
response.appendInt(room.getId());
response.appendString(safe(room.getName()));
response.appendString(safe(room.getDescription()));
response.appendInt(room.getOwnerId());
response.appendString(safe(room.getOwnerName()));
response.appendInt(room.getUserCount());
response.appendInt(room.getUsersMax());
response.appendBoolean(room.getState() != null && room.getState() != RoomState.OPEN);
response.appendBoolean(room.isMuted());
response.appendBoolean(room.isPublicRoom());
response.appendInt(0); // createdAt Room doesn't expose; left as 0 until a schema-side timestamp surfaces.
}
private static String safe(String value) {
return value != null ? value : "";
}
}
@@ -0,0 +1,30 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import java.util.List;
public class HousekeepingRoomListComposer extends MessageComposer {
private final List<Room> rooms;
public HousekeepingRoomListComposer(List<Room> rooms) {
this.rooms = rooms;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingRoomListComposer);
this.response.appendInt(this.rooms != null ? this.rooms.size() : 0);
if (this.rooms != null) {
for (Room room : this.rooms) {
HousekeepingRoomDetailComposer.appendRoomFields(this.response, room);
}
}
return this.response;
}
}
@@ -0,0 +1,59 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolBan;
import com.eu.habbo.habbohotel.permissions.Rank;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class HousekeepingUserDetailComposer extends MessageComposer {
private static final int CURRENCY_DUCKETS = 0;
private static final int CURRENCY_DIAMONDS = 5;
private final HabboInfo info;
public HousekeepingUserDetailComposer(HabboInfo info) {
this.info = info;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingUserDetailComposer);
if (this.info == null) {
this.response.appendBoolean(false);
return this.response;
}
Rank rank = this.info.getRank();
ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().checkForBan(this.info.getId());
this.response.appendBoolean(true);
this.response.appendInt(this.info.getId());
this.response.appendString(safe(this.info.getUsername()));
this.response.appendString(safe(this.info.getMotto()));
this.response.appendString(safe(this.info.getLook()));
this.response.appendInt(rank != null ? rank.getId() : 0);
this.response.appendString(rank != null ? safe(rank.getName()) : "");
this.response.appendBoolean(this.info.isOnline());
this.response.appendInt(this.info.getLastOnline());
this.response.appendInt(this.info.getCredits());
this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DUCKETS));
this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DIAMONDS));
this.response.appendString(safe(this.info.getMail()));
this.response.appendString(safe(this.info.getIpLogin()));
this.response.appendBoolean(ban != null);
// Mute / trade-lock surface as future packet extensions; see the
// optional-trailing-field parser pattern on the renderer side.
this.response.appendBoolean(false);
this.response.appendBoolean(false);
return this.response;
}
private static String safe(String value) {
return value != null ? value : "";
}
}
@@ -86,7 +86,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(habbo.getHabboInfo().getId());
this.response.appendString(habbo.getHabboInfo().getUsername());
this.response.appendString(habbo.getHabboInfo().getMotto());
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
this.response.appendInt(habbo.getHabboInfo().getInfostandBg());
this.response.appendInt(habbo.getHabboInfo().getInfostandStand());
this.response.appendInt(habbo.getHabboInfo().getInfostandOverlay());
this.response.appendInt(habbo.getHabboInfo().getInfostandCardBg());
@@ -129,7 +129,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(0 - this.bot.getId());
this.response.appendString(this.bot.getName());
this.response.appendString(this.bot.getMotto());
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
@@ -143,17 +143,11 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(this.bot.getGender().name().toUpperCase());
this.response.appendInt(this.bot.getOwnerId());
this.response.appendString(this.bot.getOwnerName());
this.response.appendInt(10);
this.response.appendShort(0);
this.response.appendShort(1);
this.response.appendShort(2);
this.response.appendShort(3);
this.response.appendShort(4);
this.response.appendShort(5);
this.response.appendShort(6);
this.response.appendShort(7);
this.response.appendShort(8);
this.response.appendShort(9);
short[] singleActions = this.bot.getOwnerActionIds();
this.response.appendInt(singleActions.length);
for (short action : singleActions) {
this.response.appendShort(action);
}
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);
@@ -163,10 +157,10 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(0 - bot.getId());
this.response.appendString(bot.getName());
this.response.appendString(bot.getMotto());
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendInt(0);
this.response.appendString(bot.getFigure());
this.response.appendInt(bot.getRoomUnit().getId());
this.response.appendInt(bot.getRoomUnit().getX());
@@ -177,17 +171,11 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendString(bot.getGender().name().toUpperCase());
this.response.appendInt(bot.getOwnerId());
this.response.appendString(bot.getOwnerName());
this.response.appendInt(10);
this.response.appendShort(0);
this.response.appendShort(1);
this.response.appendShort(2);
this.response.appendShort(3);
this.response.appendShort(4);
this.response.appendShort(5);
this.response.appendShort(6);
this.response.appendShort(7);
this.response.appendShort(8);
this.response.appendShort(9);
short[] listActions = bot.getOwnerActionIds();
this.response.appendInt(listActions.length);
for (short action : listActions) {
this.response.appendShort(action);
}
this.response.appendString("unknown");
this.response.appendInt(0);
this.response.appendInt(0);