From 7726691cde5ef6773fcc64d99900075ae34d845b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 10:39:04 +0200 Subject: [PATCH 01/13] feat(housekeeping): add find-user-by-name packet pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 3 + .../eu/habbo/messages/incoming/Incoming.java | 3 + .../HousekeepingFindUserByNameEvent.java | 35 +++++++++++ .../eu/habbo/messages/outgoing/Outgoing.java | 3 + .../HousekeepingUserDetailComposer.java | 59 +++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 0d38b908..9b421f10 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -716,5 +716,8 @@ 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 77ef0064..5264de12 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -460,4 +460,7 @@ 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java new file mode 100644 index 00000000..acae3c6e --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java @@ -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_SUPPORTTOOL)) { + 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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 45d8c986..902421c5 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -586,4 +586,7 @@ 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; + } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java new file mode 100644 index 00000000..8718e7f3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java @@ -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 : ""; + } +} From 655e039df7c1b04c073fe26144d2987e66b66d94 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 10:47:54 +0200 Subject: [PATCH 02/13] feat(housekeeping): find-user-by-id packet + acc_housekeeping gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../habbohotel/permissions/Permission.java | 1 + .../com/eu/habbo/messages/PacketManager.java | 1 + .../eu/habbo/messages/incoming/Incoming.java | 1 + .../HousekeepingFindUserByIdEvent.java | 35 +++++++++++++++++++ .../HousekeepingFindUserByNameEvent.java | 2 +- 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java index 11e042e2..674c6909 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java @@ -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"; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 9b421f10..3edb7266 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -719,5 +719,6 @@ public class PacketManager { // 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 5264de12..07cb7617 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -463,4 +463,5 @@ public class Incoming { // Housekeeping (in-client admin panel) — IDs 9100..9199 reserved public static final int HousekeepingFindUserByNameEvent = 9100; + public static final int HousekeepingFindUserByIdEvent = 9101; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java new file mode 100644 index 00000000..c78735a9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java @@ -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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java index acae3c6e..070733c6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java @@ -16,7 +16,7 @@ public class HousekeepingFindUserByNameEvent extends MessageHandler { @Override public void handle() throws Exception { - if (!this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { return; } From 1a0d783ff767faea8270e8739ca718e30bfdbfe6 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 10:53:38 +0200 Subject: [PATCH 03/13] feat(housekeeping): ban-user with arbitrary duration + ack composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 1 + .../eu/habbo/messages/incoming/Incoming.java | 1 + .../HousekeepingBanUserEvent.java | 61 +++++++++++++++++++ .../eu/habbo/messages/outgoing/Outgoing.java | 1 + .../HousekeepingActionResultComposer.java | 36 +++++++++++ 5 files changed, 100 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 3edb7266..a81b7e31 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -720,5 +720,6 @@ public class PacketManager { // 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 07cb7617..f0e29f46 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -464,4 +464,5 @@ public class Incoming { // 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java new file mode 100644 index 00000000..d8666f7b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java @@ -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, "invalid_input")); + return; + } + + long durationLong = (long) hours * SECONDS_IN_HOUR; + int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong; + + List 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, "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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 902421c5..aee805e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -588,5 +588,6 @@ public class Outgoing { // Housekeeping (in-client admin panel) — IDs 9200..9299 reserved public static final int HousekeepingUserDetailComposer = 9200; + public static final int HousekeepingActionResultComposer = 9201; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java new file mode 100644 index 00000000..30fdfa9c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java @@ -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; + } +} From 8419f118835cfbd8ba391c6e40968b0195d2f227 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 10:56:06 +0200 Subject: [PATCH 04/13] feat(housekeeping): unban-user packet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 1 + .../eu/habbo/messages/incoming/Incoming.java | 1 + .../HousekeepingUnbanUserEvent.java | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index a81b7e31..f5dfafa9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -721,5 +721,6 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index f0e29f46..aaafcf87 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -465,4 +465,5 @@ public class Incoming { public static final int HousekeepingFindUserByNameEvent = 9100; public static final int HousekeepingFindUserByIdEvent = 9101; public static final int HousekeepingBanUserEvent = 9102; + public static final int HousekeepingUnbanUserEvent = 9103; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java new file mode 100644 index 00000000..e402a729 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java @@ -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, "invalid_input")); + return; + } + + HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (info == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "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 ? "" : "no_active_ban")); + } +} From 418c753e6ce910bac54e566624b931a652c34f9f Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:00:01 +0200 Subject: [PATCH 05/13] feat(housekeeping): mute-user + kick-user packets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 2 + .../eu/habbo/messages/incoming/Incoming.java | 2 + .../HousekeepingKickUserEvent.java | 60 +++++++++++++++++++ .../HousekeepingMuteUserEvent.java | 55 +++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index f5dfafa9..971101e6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -722,5 +722,7 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index aaafcf87..194bb517 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -466,4 +466,6 @@ public class Incoming { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java new file mode 100644 index 00000000..acc5d96c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java @@ -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, "invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "user_offline")); + return; + } + + if (target.hasPermission(Permission.ACC_UNKICKABLE)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java new file mode 100644 index 00000000..ad6cba32 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java @@ -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, "invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "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, "")); + } +} From c4b3295a454746ccc8335b4d1cf8d75510851b27 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:03:32 +0200 Subject: [PATCH 06/13] feat(housekeeping): force-disconnect-user packet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 1 + .../eu/habbo/messages/incoming/Incoming.java | 1 + .../HousekeepingForceDisconnectUserEvent.java | 54 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 971101e6..bc45b669 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -724,5 +724,6 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 194bb517..a43ba1ab 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -468,4 +468,5 @@ public class Incoming { public static final int HousekeepingUnbanUserEvent = 9103; public static final int HousekeepingMuteUserEvent = 9104; public static final int HousekeepingKickUserEvent = 9105; + public static final int HousekeepingForceDisconnectUserEvent = 9106; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java new file mode 100644 index 00000000..5a5193c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java @@ -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, "invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "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(); + } +} From 57087a31f2620516a2df4f508e34e883637657b4 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:06:37 +0200 Subject: [PATCH 07/13] fix(housekeeping): emit localizable error keys instead of bare slugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../incoming/housekeeping/HousekeepingBanUserEvent.java | 4 ++-- .../housekeeping/HousekeepingForceDisconnectUserEvent.java | 4 ++-- .../incoming/housekeeping/HousekeepingKickUserEvent.java | 6 +++--- .../incoming/housekeeping/HousekeepingMuteUserEvent.java | 4 ++-- .../incoming/housekeeping/HousekeepingUnbanUserEvent.java | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java index d8666f7b..4b2c1619 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java @@ -37,7 +37,7 @@ public class HousekeepingBanUserEvent extends MessageHandler { int hours = this.packet.readInt(); if (userId <= 0 || hours <= 0) { - this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "invalid_input")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -48,7 +48,7 @@ public class HousekeepingBanUserEvent extends MessageHandler { .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, "ban_failed")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java index 5a5193c7..8e642ffa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java @@ -29,14 +29,14 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler { String reason = this.packet.readString(); if (userId <= 0) { - this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "invalid_input")); + 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, "user_offline")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java index acc5d96c..57bc8341 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java @@ -31,19 +31,19 @@ public class HousekeepingKickUserEvent extends MessageHandler { String reason = this.packet.readString(); if (userId <= 0) { - this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "invalid_input")); + 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, "user_offline")); + 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, "target_unkickable")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java index ad6cba32..43f12eb1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java @@ -33,14 +33,14 @@ public class HousekeepingMuteUserEvent extends MessageHandler { int minutes = this.packet.readInt(); if (userId <= 0 || minutes <= 0) { - this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "invalid_input")); + 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, "user_offline")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java index e402a729..74ed38e4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java @@ -23,14 +23,14 @@ public class HousekeepingUnbanUserEvent extends MessageHandler { int userId = this.packet.readInt(); if (userId <= 0) { - this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "invalid_input")); + 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, "user_not_found")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); return; } @@ -39,6 +39,6 @@ public class HousekeepingUnbanUserEvent extends MessageHandler { // 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 ? "" : "no_active_ban")); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban")); } } From 525c124fa570551f376e392db4cfb7ff710346da Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:17:42 +0200 Subject: [PATCH 08/13] feat(housekeeping): set-rank + trade-lock + reset-password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 3 + .../eu/habbo/messages/incoming/Incoming.java | 3 + .../HousekeepingResetUserPasswordEvent.java | 104 ++++++++++++++++++ .../HousekeepingSetUserRankEvent.java | 71 ++++++++++++ .../HousekeepingTradeLockUserEvent.java | 77 +++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index bc45b669..00f69eb4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -725,5 +725,8 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index a43ba1ab..5f4a1a58 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -469,4 +469,7 @@ public class Incoming { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java new file mode 100644 index 00000000..1f1a57ba --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java @@ -0,0 +1,104 @@ +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.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +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 the SHA-256 hex of the new password into + * `users.password` (varchar(64) — sized to hold SHA-256 hex), 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. + * + * If your CMS uses a hash other than SHA-256 (bcrypt / argon2 / SHA-1), + * swap the MessageDigest constant — the rest of the flow is hash-agnostic. + */ +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 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 = sha256Hex(plain); + } catch (NoSuchAlgorithmException 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(); + } + + private static String sha256Hex(String plain) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(plain.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(digest.length * 2); + + for (byte b : digest) { + hex.append(String.format("%02x", b)); + } + + return hex.toString(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java new file mode 100644 index 00000000..67f35d8a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java new file mode 100644 index 00000000..cf961c15 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java @@ -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, "")); + } +} From a1749c9eda3ee11db80a62d7142c5c6457b98102 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:24:45 +0200 Subject: [PATCH 09/13] =?UTF-8?q?feat(housekeeping):=20rooms=20domain=20?= =?UTF-8?q?=E2=80=94=20find/search=20+=20open/close/mute/kick-all/transfer?= =?UTF-8?q?/delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 7 ++ .../eu/habbo/messages/incoming/Incoming.java | 7 ++ .../HousekeepingDeleteRoomEvent.java | 68 +++++++++++++++++ .../HousekeepingFindRoomByIdEvent.java | 37 +++++++++ .../HousekeepingKickAllFromRoomEvent.java | 41 ++++++++++ .../HousekeepingMuteRoomEvent.java | 50 +++++++++++++ .../HousekeepingRoomStateEvent.java | 49 ++++++++++++ .../HousekeepingSearchRoomsEvent.java | 75 +++++++++++++++++++ ...ousekeepingTransferRoomOwnershipEvent.java | 67 +++++++++++++++++ .../eu/habbo/messages/outgoing/Outgoing.java | 2 + .../HousekeepingRoomDetailComposer.java | 49 ++++++++++++ .../HousekeepingRoomListComposer.java | 30 ++++++++ 12 files changed, 482 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 00f69eb4..564a8a7a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -728,5 +728,12 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 5f4a1a58..aeb4a766 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -472,4 +472,11 @@ public class Incoming { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java new file mode 100644 index 00000000..9e85deb5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java new file mode 100644 index 00000000..dc9267c4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java @@ -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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java new file mode 100644 index 00000000..76ae8e82 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java new file mode 100644 index 00000000..b471affc --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java new file mode 100644 index 00000000..ea902db8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java new file mode 100644 index 00000000..985250c9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java @@ -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 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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java new file mode 100644 index 00000000..70c6bf80 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index aee805e8..0904698a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -589,5 +589,7 @@ public class Outgoing { // 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java new file mode 100644 index 00000000..b3bd4285 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java @@ -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 : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java new file mode 100644 index 00000000..7fc6b81f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java @@ -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 rooms; + + public HousekeepingRoomListComposer(List 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; + } +} From 6126c35779f4635b1c659b87999753aba4a92849 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:30:50 +0200 Subject: [PATCH 10/13] =?UTF-8?q?feat(housekeeping):=20economy=20domain=20?= =?UTF-8?q?=E2=80=94=20credits/currency/items/hc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../com/eu/habbo/messages/PacketManager.java | 4 + .../eu/habbo/messages/incoming/Incoming.java | 4 + .../HousekeepingGiveCreditsEvent.java | 62 +++++++++++++++ .../HousekeepingGiveCurrencyEvent.java | 74 ++++++++++++++++++ .../HousekeepingGrantItemEvent.java | 62 +++++++++++++++ .../HousekeepingSetHcSubscriptionEvent.java | 76 +++++++++++++++++++ 6 files changed, 282 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 564a8a7a..b7ec88f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -735,5 +735,9 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index aeb4a766..bf9dcfa8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -479,4 +479,8 @@ public class Incoming { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java new file mode 100644 index 00000000..e0d16e2d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java new file mode 100644 index 00000000..ba347b93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java new file mode 100644 index 00000000..cf1ecb6b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java @@ -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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java new file mode 100644 index 00000000..9facf4f4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java @@ -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, "")); + } +} From fbf979419e5bec7cb8c8df122d8872873c57ca52 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 11:52:36 +0200 Subject: [PATCH 11/13] feat(housekeeping): hotel alert + dashboard + audit log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../com/eu/habbo/messages/PacketManager.java | 3 + .../eu/habbo/messages/incoming/Incoming.java | 3 + .../HousekeepingGetDashboardEvent.java | 91 +++++++++++++++++++ .../HousekeepingListActionLogEvent.java | 85 +++++++++++++++++ .../HousekeepingSendHotelAlertEvent.java | 58 ++++++++++++ .../eu/habbo/messages/outgoing/Outgoing.java | 2 + .../HousekeepingActionLogComposer.java | 65 +++++++++++++ .../HousekeepingDashboardComposer.java | 50 ++++++++++ 8 files changed, 357 insertions(+) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index b7ec88f4..e5887e01 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -739,5 +739,8 @@ public class PacketManager { 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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index bf9dcfa8..4222bdfa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -483,4 +483,7 @@ public class Incoming { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java new file mode 100644 index 00000000..c243c297 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java @@ -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(); +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java new file mode 100644 index 00000000..10979346 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java @@ -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 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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java new file mode 100644 index 00000000..e3a83daa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java @@ -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 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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 0904698a..3e6bf2ec 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -591,5 +591,7 @@ public class Outgoing { 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java new file mode 100644 index 00000000..6fafa0e0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java @@ -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 rows; + + public HousekeepingActionLogComposer(List 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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java new file mode 100644 index 00000000..694ae6bf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java @@ -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; + } +} From dac09e92d112f9ab8af674d857299ac8898f8551 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 22:25:16 +0200 Subject: [PATCH 12/13] fix(housekeeping): hash reset password with BCrypt, not SHA-256 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- .../HousekeepingResetUserPasswordEvent.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java index 1f1a57ba..14142002 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java @@ -4,10 +4,8 @@ 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.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.sql.Connection; import java.sql.PreparedStatement; @@ -15,19 +13,18 @@ import java.sql.SQLException; /** * Reset a user's password to a fresh random 12-character alphanumeric - * string. Persists the SHA-256 hex of the new password into - * `users.password` (varchar(64) — sized to hold SHA-256 hex), 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. - * - * If your CMS uses a hash other than SHA-256 (bcrypt / argon2 / SHA-1), - * swap the MessageDigest constant — the rest of the flow is hash-agnostic. + * 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(); @@ -53,8 +50,8 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler { String hash; try { - hash = sha256Hex(plain); - } catch (NoSuchAlgorithmException e) { + 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; } @@ -89,16 +86,4 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler { return sb.toString(); } - - private static String sha256Hex(String plain) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(plain.getBytes(StandardCharsets.UTF_8)); - StringBuilder hex = new StringBuilder(digest.length * 2); - - for (byte b : digest) { - hex.append(String.format("%02x", b)); - } - - return hex.toString(); - } } From 4ef4ed1a966629e3d85e2dc9c1fbc6b9861c0d5d Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 26 May 2026 12:52:49 +0200 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=86=99=20Enable=20HK=20in=20client?= =?UTF-8?q?=20with=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Database Updates/006_Allow_Housekeeping_in_Client.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 Database Updates/006_Allow_Housekeeping_in_Client.sql diff --git a/Database Updates/006_Allow_Housekeeping_in_Client.sql b/Database Updates/006_Allow_Housekeeping_in_Client.sql new file mode 100644 index 00000000..8d401928 --- /dev/null +++ b/Database Updates/006_Allow_Housekeeping_in_Client.sql @@ -0,0 +1 @@ +INSERT INTO `camwijsnew`.`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');