From e75fe83c808927cd33c3dd3d97e6eb0b573fdd73 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 18:00:27 +0100 Subject: [PATCH 1/2] feat(pathfinder): add configurable underpass walk-under-furniture support Allow avatars to walk under furniture items placed at a configurable height threshold (default 1.5). When a blocking item is elevated enough above the walk surface, the tile is treated as walkable. Changes: - RoomLayout: add UNDERPASS_HEIGHT static field (default 1.5) - PluginManager: load pathfinder.underpass.height from emulator config - RoomTileManager: add getUnderpassWalkHeight() method and underpass checks in tile state calculation, stack height, and canWalkAt() - RoomItemManager: add getWalkableItemAt() method that returns the correct walkable item considering underpass clearance - RoomUnit: use getWalkableItemAt() in movement cycle for accurate item resolution when walking under elevated furniture Co-Authored-By: medievalshell --- .../habbohotel/rooms/RoomItemManager.java | 37 ++++++++++++ .../eu/habbo/habbohotel/rooms/RoomLayout.java | 1 + .../habbohotel/rooms/RoomTileManager.java | 57 ++++++++++++++++++- .../eu/habbo/habbohotel/rooms/RoomUnit.java | 2 +- .../com/eu/habbo/plugin/PluginManager.java | 1 + 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index 466f8abf..2a178b7b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -448,6 +448,43 @@ public class RoomItemManager { return highestItem; } + /** + * Gets the top walkable item at a position, considering underpass. + * If the topmost item is elevated enough to walk under, returns the highest item at walk surface level instead. + */ + public HabboItem getWalkableItemAt(int x, int y) { + HabboItem topItem = this.getTopItemAt(x, y); + if (topItem == null) { + return null; + } + + // If the top item is walkable, just return it + if (topItem.isWalkable() || topItem.getBaseItem().allowWalk() || topItem.getBaseItem().allowSit() || topItem.getBaseItem().allowLay()) { + return topItem; + } + + // Check for underpass: get the walk surface height + double walkSurface = this.room.getLayout() != null ? this.room.getLayout().getHeightAtSquare(x, y) : 0; + HabboItem walkSurfaceItem = null; + + for (HabboItem item : this.getItemsAt(x, y)) { + if (item.isWalkable() || item.getBaseItem().allowWalk() || item.getBaseItem().allowSit() || item.getBaseItem().allowLay()) { + double itemTop = item.getZ() + Item.getCurrentHeight(item); + if (itemTop > walkSurface) { + walkSurface = itemTop; + walkSurfaceItem = item; + } + } + } + + // If there's enough clearance under the top blocking item, return the walk surface item + if (topItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { + return walkSurfaceItem; + } + + return topItem; + } + /** * Gets the top item from a set of tiles. */ diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java index f555eaa4..531f5704 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomLayout.java @@ -19,6 +19,7 @@ public class RoomLayout { protected static final int DIAGONALMOVEMENTCOST = 14; public static double MAXIMUM_STEP_HEIGHT = 1.5; public static boolean ALLOW_FALLING = true; + public static double UNDERPASS_HEIGHT = 1.5; public boolean CANMOVEDIAGONALY = true; private String name; private short doorX; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java index 05b8557c..7f73dcf9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java @@ -91,9 +91,41 @@ public class RoomTileManager { tallestItem = item; } + if (result == RoomTileState.BLOCKED && tallestItem != null) { + double walkSurface = this.getUnderpassWalkHeight(tile, items, exclude); + if (tallestItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { + result = RoomTileState.OPEN; + } + } + return result; } + /** + * Calculates the walk surface height for underpass checks. + * Returns the floor height or the top of the highest walkable item below any blocking items. + */ + private double getUnderpassWalkHeight(RoomTile tile, THashSet items, HabboItem exclude) { + RoomLayout layout = this.room.getLayout(); + double walkHeight = layout != null ? layout.getHeightAtSquare(tile.x, tile.y) : 0; + + if (items != null) { + for (HabboItem item : items) { + if (exclude != null && item == exclude) { + continue; + } + if (item.isWalkable() || item.getBaseItem().allowWalk() || item.getBaseItem().allowSit() || item.getBaseItem().allowLay()) { + double itemTop = item.getZ() + Item.getCurrentHeight(item); + if (itemTop > walkHeight) { + walkHeight = itemTop; + } + } + } + } + + return walkHeight; + } + /** * Determines the tile state based on a specific item. */ @@ -193,7 +225,22 @@ public class RoomTileManager { HabboItem item = this.room.getItemManager().getTopItemAt(x, y, exclude); if (item != null) { canStack = item.getBaseItem().allowStack(); - height = item.getZ() + (item.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(item)); + double itemTop = item.getZ() + (item.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(item)); + + // Underpass: if the top item is blocking but high enough to walk under, use floor height + if (!item.isWalkable() && !item.getBaseItem().allowWalk() && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay()) { + RoomLayout layout2 = this.room.getLayout(); + RoomTile tile = layout2 != null ? layout2.getTile(x, y) : null; + THashSet allItems = tile != null ? this.room.getItemManager().getItemsAt(tile) : null; + double walkSurface = this.getUnderpassWalkHeight(tile, allItems, exclude); + if (item.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { + height = walkSurface; + } else { + height = itemTop; + } + } else { + height = itemTop; + } } if (calculateHeightmap) { @@ -396,6 +443,14 @@ public class RoomTileManager { } } + // Underpass: if top item blocks but is high enough, allow walking under + if (!canWalk && topItem != null) { + double walkSurface = this.getUnderpassWalkHeight(roomTile, items, null); + if (topItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { + canWalk = true; + } + } + return canWalk; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java index 2f88edb7..e7a08aea 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomUnit.java @@ -235,7 +235,7 @@ public class RoomUnit { } } - HabboItem item = room.getTopItemAt(next.x, next.y); + HabboItem item = room.getItemManager().getWalkableItemAt(next.x, next.y); boolean canSitNextTile = room.canSitAt(next.x, next.y); boolean canLayNextTile = room.canLayAt(next.x, next.y); diff --git a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java index 00154dde..36b8b768 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -84,6 +84,7 @@ public class PluginManager { Room.MUTEAREA_CAN_WHISPER = Emulator.getConfig().getBoolean("room.chat.mutearea.allow_whisper", false); RoomChatMessage.SAVE_ROOM_CHATS = Emulator.getConfig().getBoolean("save.room.chats", false); RoomLayout.MAXIMUM_STEP_HEIGHT = Emulator.getConfig().getDouble("pathfinder.step.maximum.height", 1.1); + RoomLayout.UNDERPASS_HEIGHT = Emulator.getConfig().getDouble("pathfinder.underpass.height", 1.5); RoomLayout.ALLOW_FALLING = Emulator.getConfig().getBoolean("pathfinder.step.allow.falling", true); RoomTrade.TRADING_ENABLED = Emulator.getConfig().getBoolean("hotel.trading.enabled") && !ShutdownEmulator.instantiated; RoomTrade.TRADING_REQUIRES_PERK = Emulator.getConfig().getBoolean("hotel.trading.requires.perk"); From b0f3f1488db87fa6bbe34aff241fa00dc68f4af5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 17 Mar 2026 13:38:43 +0100 Subject: [PATCH 2/2] feat(pathfinder): add per-room underpass setting via room settings UI Add allow_underpass as a per-room boolean setting that controls whether avatars can walk under elevated furniture. When disabled (default), the room behaves normally with blocking items. When enabled, items elevated above the UNDERPASS_HEIGHT threshold allow walking underneath. Changes: - Room: add allowUnderpass field with DB load/save - RoomTileManager: make all 3 underpass checks conditional on room setting - RoomItemManager: getWalkableItemAt() falls back when underpass disabled - RoomSettingsComposer/SaveEvent: send/receive the flag in room settings packet - SQL migration: add allow_underpass column to rooms table Co-Authored-By: medievalshell --- Database Updates/17032026_allow_underpass.sql | 1 + .../java/com/eu/habbo/habbohotel/rooms/Room.java | 15 +++++++++++++-- .../habbo/habbohotel/rooms/RoomItemManager.java | 5 +++++ .../habbo/habbohotel/rooms/RoomTileManager.java | 6 +++--- .../incoming/rooms/RoomSettingsSaveEvent.java | 5 +++++ .../outgoing/rooms/RoomSettingsComposer.java | 1 + 6 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 Database Updates/17032026_allow_underpass.sql diff --git a/Database Updates/17032026_allow_underpass.sql b/Database Updates/17032026_allow_underpass.sql new file mode 100644 index 00000000..c5c7dcac --- /dev/null +++ b/Database Updates/17032026_allow_underpass.sql @@ -0,0 +1 @@ +ALTER TABLE `rooms` ADD COLUMN `allow_underpass` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `move_diagonally`; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index e47431ed..01e81ce6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -156,6 +156,7 @@ public class Room implements Comparable, ISerialize, Runnable { private volatile boolean promoted; private volatile int tradeMode; private volatile boolean moveDiagonally; + private volatile boolean allowUnderpass; private volatile boolean jukeboxActive; private volatile boolean hideWired; private RoomPromotion promotion; @@ -239,6 +240,7 @@ public class Room implements Comparable, ISerialize, Runnable { this.tradeMode = set.getInt("trade_mode"); this.moveDiagonally = set.getString("move_diagonally").equals("1"); + this.allowUnderpass = set.getString("allow_underpass").equals("1"); this.preLoaded = true; this.allowBotsWalk = true; @@ -1077,7 +1079,7 @@ public class Room implements Comparable, ISerialize, Runnable { if (this.needsUpdate) { try (Connection connection = Emulator.getDatabase().getDataSource() .getConnection(); PreparedStatement statement = connection.prepareStatement( - "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ? WHERE id = ?")) { + "UPDATE rooms SET name = ?, description = ?, password = ?, state = ?, users_max = ?, category = ?, score = ?, paper_floor = ?, paper_wall = ?, paper_landscape = ?, thickness_wall = ?, wall_height = ?, thickness_floor = ?, moodlight_data = ?, tags = ?, allow_other_pets = ?, allow_other_pets_eat = ?, allow_walkthrough = ?, allow_hidewall = ?, chat_mode = ?, chat_weight = ?, chat_speed = ?, chat_hearing_distance = ?, chat_protection =?, who_can_mute = ?, who_can_kick = ?, who_can_ban = ?, poll_id = ?, guild_id = ?, roller_speed = ?, override_model = ?, is_staff_picked = ?, promoted = ?, trade_mode = ?, move_diagonally = ?, owner_id = ?, owner_name = ?, jukebox_active = ?, hidewired = ?, allow_underpass = ? WHERE id = ?")) { statement.setString(1, this.name); statement.setString(2, this.description); statement.setString(3, this.password); @@ -1126,7 +1128,8 @@ public class Room implements Comparable, ISerialize, Runnable { statement.setString(37, this.ownerName); statement.setString(38, this.jukeboxActive ? "1" : "0"); statement.setString(39, this.hideWired ? "1" : "0"); - statement.setInt(40, this.id); + statement.setString(40, this.allowUnderpass ? "1" : "0"); + statement.setInt(41, this.id); statement.executeUpdate(); this.needsUpdate = false; } catch (SQLException e) { @@ -1408,6 +1411,14 @@ public class Room implements Comparable, ISerialize, Runnable { this.allowWalkthrough = allowWalkthrough; } + public boolean isAllowUnderpass() { + return this.allowUnderpass; + } + + public void setAllowUnderpass(boolean allowUnderpass) { + this.allowUnderpass = allowUnderpass; + } + public boolean isAllowBotsWalk() { return this.allowBotsWalk; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java index 2a178b7b..3f1d8ccf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomItemManager.java @@ -458,6 +458,11 @@ public class RoomItemManager { return null; } + // If underpass is disabled for this room, just return the top item + if (!this.room.isAllowUnderpass()) { + return topItem; + } + // If the top item is walkable, just return it if (topItem.isWalkable() || topItem.getBaseItem().allowWalk() || topItem.getBaseItem().allowSit() || topItem.getBaseItem().allowLay()) { return topItem; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java index 7f73dcf9..828abb80 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTileManager.java @@ -91,7 +91,7 @@ public class RoomTileManager { tallestItem = item; } - if (result == RoomTileState.BLOCKED && tallestItem != null) { + if (this.room.isAllowUnderpass() && result == RoomTileState.BLOCKED && tallestItem != null) { double walkSurface = this.getUnderpassWalkHeight(tile, items, exclude); if (tallestItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { result = RoomTileState.OPEN; @@ -228,7 +228,7 @@ public class RoomTileManager { double itemTop = item.getZ() + (item.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(item)); // Underpass: if the top item is blocking but high enough to walk under, use floor height - if (!item.isWalkable() && !item.getBaseItem().allowWalk() && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay()) { + if (this.room.isAllowUnderpass() && !item.isWalkable() && !item.getBaseItem().allowWalk() && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay()) { RoomLayout layout2 = this.room.getLayout(); RoomTile tile = layout2 != null ? layout2.getTile(x, y) : null; THashSet allItems = tile != null ? this.room.getItemManager().getItemsAt(tile) : null; @@ -444,7 +444,7 @@ public class RoomTileManager { } // Underpass: if top item blocks but is high enough, allow walking under - if (!canWalk && topItem != null) { + if (this.room.isAllowUnderpass() && !canWalk && topItem != null) { double walkSurface = this.getUnderpassWalkHeight(roomTile, items, null); if (topItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) { canWalk = true; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java index eda4253e..8200b5f8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java @@ -129,6 +129,11 @@ public class RoomSettingsSaveEvent extends MessageHandler { room.setChatSpeed(this.packet.readInt()); room.setChatDistance(Math.abs(this.packet.readInt())); room.setChatProtection(this.packet.readInt()); + + if (this.packet.bytesAvailable() > 0) { + room.setAllowUnderpass(this.packet.readBoolean()); + } + room.setNeedsUpdate(true); room.sendComposer(new RoomThicknessComposer(room).compose()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomSettingsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomSettingsComposer.java index 4b02d945..e1970ac7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomSettingsComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/RoomSettingsComposer.java @@ -52,6 +52,7 @@ public class RoomSettingsComposer extends MessageComposer { this.response.appendInt(this.room.getMuteOption()); this.response.appendInt(this.room.getKickOption()); this.response.appendInt(this.room.getBanOption()); + this.response.appendInt(this.room.isAllowUnderpass() ? 1 : 0); return this.response; }