feat(housekeeping): rooms domain — find/search + open/close/mute/kick-all/transfer/delete

Eight new incoming handlers + two new outgoing composers cover the
full rooms-domain HK panel.

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

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

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

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

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

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

* Incoming 9114 HousekeepingKickAllFromRoomEvent — Room.ejectAll().

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

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

`mvn compile` clean.
This commit is contained in:
simoleo89
2026-05-24 11:24:45 +02:00
committed by simoleo89
parent 525c124fa5
commit a1749c9eda
12 changed files with 482 additions and 0 deletions
@@ -728,5 +728,12 @@ public class PacketManager {
this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.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.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class);
this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.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);
} }
} }
@@ -472,4 +472,11 @@ public class Incoming {
public static final int HousekeepingSetUserRankEvent = 9107; public static final int HousekeepingSetUserRankEvent = 9107;
public static final int HousekeepingTradeLockUserEvent = 9108; public static final int HousekeepingTradeLockUserEvent = 9108;
public static final int HousekeepingResetUserPasswordEvent = 9109; 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;
} }
@@ -0,0 +1,68 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Permanently delete a room. Mirrors the minimum-viable subset of
* RequestDeleteRoomEvent: eject all users from the live room, dispose
* + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup
* is intentionally skipped on this slice — leftover rows in those
* tables become orphans but don't crash the emulator; a follow-up
* pass can cascade once we have a HK audit-log row to attach the
* orphan-cleanup to.
*/
public class HousekeepingDeleteRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.delete";
@Override
public int getRatelimit() {
return 2000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room != null) {
room.ejectAll();
room.preventUnloading = false;
room.dispose();
Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) {
statement.setInt(1, roomId);
int rows = statement.executeUpdate();
if (rows == 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
} catch (SQLException e) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
return;
}
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomDetailComposer;
public class HousekeepingFindRoomByIdEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingRoomDetailComposer(null));
return;
}
// loadRoom covers both the in-memory cache (already-loaded rooms)
// and the offline path (SELECT * FROM rooms WHERE id=?). Pass
// false for loadData so we don't pull furni/bots/pets just to
// render an HK panel summary — getOwnerName / getUserCount work
// on the pre-loaded skeleton.
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
this.client.sendResponse(new HousekeepingRoomDetailComposer(room));
}
}
@@ -0,0 +1,41 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
public class HousekeepingKickAllFromRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.kick_all";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.ejectAll();
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,50 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Toggle room-wide mute. Habbo's Room.setMuted is boolean, not duration-
* scoped, so the wire `minutes` arg picks the semantic: minutes==0 =>
* unmute, minutes>0 => mute. An emulator-side scheduled unmute could
* use the value as a timer, but for now the mute stays until the
* operator unmutes manually — the minutes is reserved as a forward-
* compat field on the wire.
*/
public class HousekeepingMuteRoomEvent extends MessageHandler {
private static final String ACTION_KEY = "room.mute";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
int minutes = this.packet.readInt();
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.setMuted(minutes > 0);
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, ""));
}
}
@@ -0,0 +1,49 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomState;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
/**
* Toggle the room state between OPEN (open) and LOCKED (closed). The
* client picks which transition it wants via the boolean — true => OPEN,
* false => LOCKED. Persists state through `Room.save()` so the change
* outlives an unload.
*/
public class HousekeepingRoomStateEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
int roomId = this.packet.readInt();
boolean open = this.packet.readBoolean();
String actionKey = open ? "room.open" : "room.close";
if (roomId <= 0) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
return;
}
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false);
if (room == null) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.room_not_found"));
return;
}
room.setState(open ? RoomState.OPEN : RoomState.LOCKED);
room.save();
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, ""));
}
}
@@ -0,0 +1,75 @@
package com.eu.habbo.messages.incoming.housekeeping;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomListComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Search rooms by name. `exactMatch=true` => `name = ?` (used by the
* findByName autocomplete that wants a unique hit). `exactMatch=false`
* => `name LIKE concat(?, '%')` (used by the prefix dropdown).
*
* Both branches go through the same packet because the wire shape is
* identical — the client picks which mode it wants by toggling the
* boolean.
*/
public class HousekeepingSearchRoomsEvent extends MessageHandler {
private static final int HARD_LIMIT = 50;
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
return;
}
String query = this.packet.readString();
boolean exactMatch = this.packet.readBoolean();
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
if (query == null) query = "";
query = query.trim();
if (query.isEmpty()) {
this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>()));
return;
}
String sql = exactMatch
? "SELECT id FROM rooms WHERE name = ? LIMIT ?"
: "SELECT id FROM rooms WHERE name LIKE ? LIMIT ?";
List<Room> rooms = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, exactMatch ? query : query + "%");
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false);
if (room != null) rooms.add(room);
}
}
} catch (SQLException ignored) {
// fall through with whatever we collected before the failure
}
this.client.sendResponse(new HousekeepingRoomListComposer(rooms));
}
}
@@ -0,0 +1,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, ""));
}
}
@@ -589,5 +589,7 @@ public class Outgoing {
// Housekeeping (in-client admin panel) — IDs 9200..9299 reserved // Housekeeping (in-client admin panel) — IDs 9200..9299 reserved
public static final int HousekeepingUserDetailComposer = 9200; public static final int HousekeepingUserDetailComposer = 9200;
public static final int HousekeepingActionResultComposer = 9201; public static final int HousekeepingActionResultComposer = 9201;
public static final int HousekeepingRoomDetailComposer = 9202;
public static final int HousekeepingRoomListComposer = 9203;
} }
@@ -0,0 +1,49 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomState;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class HousekeepingRoomDetailComposer extends MessageComposer {
private final Room room;
public HousekeepingRoomDetailComposer(Room room) {
this.room = room;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingRoomDetailComposer);
if (this.room == null) {
this.response.appendBoolean(false);
return this.response;
}
this.response.appendBoolean(true);
appendRoomFields(this.response, this.room);
return this.response;
}
/** Shared by HousekeepingRoomListComposer too. */
public static void appendRoomFields(ServerMessage response, Room room) {
response.appendInt(room.getId());
response.appendString(safe(room.getName()));
response.appendString(safe(room.getDescription()));
response.appendInt(room.getOwnerId());
response.appendString(safe(room.getOwnerName()));
response.appendInt(room.getUserCount());
response.appendInt(room.getUsersMax());
response.appendBoolean(room.getState() != null && room.getState() != RoomState.OPEN);
response.appendBoolean(room.isMuted());
response.appendBoolean(room.isPublicRoom());
response.appendInt(0); // createdAt — Room doesn't expose; left as 0 until a schema-side timestamp surfaces.
}
private static String safe(String value) {
return value != null ? value : "";
}
}
@@ -0,0 +1,30 @@
package com.eu.habbo.messages.outgoing.housekeeping;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import java.util.List;
public class HousekeepingRoomListComposer extends MessageComposer {
private final List<Room> rooms;
public HousekeepingRoomListComposer(List<Room> rooms) {
this.rooms = rooms;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.HousekeepingRoomListComposer);
this.response.appendInt(this.rooms != null ? this.rooms.size() : 0);
if (this.rooms != null) {
for (Room room : this.rooms) {
HousekeepingRoomDetailComposer.appendRoomFields(this.response, room);
}
}
return this.response;
}
}