From e75fe83c808927cd33c3dd3d97e6eb0b573fdd73 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 18:00:27 +0100 Subject: [PATCH 1/4] 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/4] 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; } From a056bc4b79a31c7c187ada6aac6c6b15d97f76b2 Mon Sep 17 00:00:00 2001 From: duckietm Date: Tue, 17 Mar 2026 17:03:59 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=86=95=20Camera=20Now=20in=20the=20Em?= =?UTF-8?q?u=20-=20remove=20all=20camera=20stuff=20from=202002=20?= =?UTF-8?q?=F0=9F=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/eu/habbo/Emulator.java | 31 +++-- .../core/consolecommands/ConsoleCommand.java | 1 - .../ConsoleReconnectCameraCommand.java | 19 --- .../habbohotel/commands/CommandHandler.java | 1 - .../commands/ConnectCameraCommand.java | 15 -- .../camera/CameraPublishToWebEvent.java | 54 ++++---- .../incoming/camera/CameraPurchaseEvent.java | 49 ++++--- .../camera/CameraRoomPictureEvent.java | 125 ++++++++++++++--- .../camera/CameraRoomThumbnailEvent.java | 107 +++++++++++---- .../habbo/networking/camera/CameraClient.java | 92 ------------- .../networking/camera/CameraDecoder.java | 29 ---- .../networking/camera/CameraHandler.java | 50 ------- .../camera/CameraIncomingMessage.java | 63 --------- .../networking/camera/CameraMessage.java | 18 --- .../camera/CameraOutgoingMessage.java | 128 ------------------ .../camera/CameraPacketHandler.java | 48 ------- .../messages/CameraOutgoingHeaders.java | 6 - .../CameraAuthenticationTicketEvent.java | 21 --- .../incoming/CameraLoginStatusEvent.java | 56 -------- .../incoming/CameraResultURLEvent.java | 56 -------- .../CameraRoomThumbnailGeneratedEvent.java | 25 ---- .../incoming/CameraUpdateNotification.java | 37 ----- .../outgoing/CameraLoginComposer.java | 19 --- .../outgoing/CameraRenderImageComposer.java | 36 ----- .../com/eu/habbo/plugin/PluginManager.java | 8 -- .../runnables/CameraClientAutoReconnect.java | 36 ----- 26 files changed, 263 insertions(+), 867 deletions(-) delete mode 100644 Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleReconnectCameraCommand.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ConnectCameraCommand.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraClient.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraDecoder.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraHandler.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraIncomingMessage.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraMessage.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraOutgoingMessage.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/CameraPacketHandler.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/CameraOutgoingHeaders.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraAuthenticationTicketEvent.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraLoginStatusEvent.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraResultURLEvent.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraRoomThumbnailGeneratedEvent.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraUpdateNotification.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraLoginComposer.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraRenderImageComposer.java delete mode 100644 Emulator/src/main/java/com/eu/habbo/threading/runnables/CameraClientAutoReconnect.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 20605be6..69f1deab 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -7,7 +7,6 @@ import com.eu.habbo.core.*; import com.eu.habbo.core.consolecommands.ConsoleCommand; import com.eu.habbo.database.Database; import com.eu.habbo.habbohotel.GameEnvironment; -import com.eu.habbo.networking.camera.CameraClient; import com.eu.habbo.networking.gameserver.GameServer; import com.eu.habbo.networking.rconserver.RCONServer; import com.eu.habbo.plugin.PluginManager; @@ -64,7 +63,6 @@ public final class Emulator { private static TextsManager texts; private static GameServer gameServer; private static RCONServer rconServer; - private static CameraClient cameraClient; private static Logging logging; private static Database database; private static DatabaseLogger databaseLogger; @@ -132,6 +130,26 @@ public final class Emulator { Emulator.pluginManager.reload(); Emulator.getPluginManager().fireEvent(new EmulatorConfigUpdatedEvent()); Emulator.texts = new TextsManager(); + + Emulator.config.register("camera.url", "http://localhost/camera/"); + Emulator.config.register("imager.location.output.camera", "/public/camera/"); + Emulator.config.register("imager.location.output.thumbnail", "/public/camera/thumbnails/"); + Emulator.config.register("camera.price.points.publish", "1"); + Emulator.config.register("camera.price.points.publish.type", "5"); + Emulator.config.register("camera.publish.delay", "180"); + Emulator.config.register("camera.price.credits", "2"); + Emulator.config.register("camera.price.points", "0"); + Emulator.config.register("camera.price.points.type", "5"); + Emulator.config.register("camera.render.delay", "5"); + Emulator.texts.register("camera.permission", "You don't have permission to use the camera!"); + Emulator.texts.register("camera.wait", "Please wait %seconds% seconds before making another picture."); + Emulator.texts.register("camera.error.creation", "Failed to create your picture. *sadpanda*"); + + File thumbnailDir = new File(Emulator.config.getValue("imager.location.output.thumbnail")); + if (!thumbnailDir.exists()) { + thumbnailDir.mkdirs(); + } + new CleanerThread(); Emulator.gameServer = new GameServer(getConfig().getValue("game.host", "127.0.0.1"), getConfig().getInt("game.port", 30000)); Emulator.rconServer = new RCONServer(getConfig().getValue("rcon.host", "127.0.0.1"), getConfig().getInt("rcon.port", 30001)); @@ -231,7 +249,6 @@ public final class Emulator { if (Emulator.pluginManager != null) tryShutdown(() -> Emulator.pluginManager.fireEvent(new EmulatorStartShutdownEvent())); - if (Emulator.cameraClient != null) tryShutdown(() -> Emulator.cameraClient.disconnect()); if (Emulator.rconServer != null) tryShutdown(() -> Emulator.rconServer.stop()); if (Emulator.gameEnvironment != null) tryShutdown(() -> Emulator.gameEnvironment.dispose()); if (Emulator.pluginManager != null) @@ -318,14 +335,6 @@ public final class Emulator { return badgeImager; } - public static CameraClient getCameraClient() { - return cameraClient; - } - - public static synchronized void setCameraClient(CameraClient client) { - cameraClient = client; - } - public static int getTimeStarted() { return timeStarted; } diff --git a/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleCommand.java b/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleCommand.java index 8bdccca6..b600f8a9 100644 --- a/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleCommand.java @@ -20,7 +20,6 @@ public abstract class ConsoleCommand { addCommand(new ConsoleShutdownCommand()); addCommand(new ConsoleInfoCommand()); addCommand(new ConsoleTestCommand()); - addCommand(new ConsoleReconnectCameraCommand()); addCommand(new ShowInteractionsCommand()); addCommand(new ShowRCONCommands()); addCommand(new ThankyouArcturusCommand()); diff --git a/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleReconnectCameraCommand.java b/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleReconnectCameraCommand.java deleted file mode 100644 index 587130d1..00000000 --- a/Emulator/src/main/java/com/eu/habbo/core/consolecommands/ConsoleReconnectCameraCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eu.habbo.core.consolecommands; - -import com.eu.habbo.networking.camera.CameraClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ConsoleReconnectCameraCommand extends ConsoleCommand { - private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleReconnectCameraCommand.class); - - public ConsoleReconnectCameraCommand() { - super("camera", "Attempt to reconnect to the camera server."); - } - - @Override - public void handle(String[] args) throws Exception { - LOGGER.info("Connecting to the camera..."); - CameraClient.attemptReconnect = true; - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java index 44e119b3..956b0430 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandHandler.java @@ -187,7 +187,6 @@ public class CommandHandler { addCommand(new ChangeNameCommand()); addCommand(new ChatTypeCommand()); addCommand(new CommandsCommand()); - addCommand(new ConnectCameraCommand()); addCommand(new ControlCommand()); addCommand(new CoordsCommand()); addCommand(new CreditsCommand()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ConnectCameraCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ConnectCameraCommand.java deleted file mode 100644 index 34ba2b0b..00000000 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/ConnectCameraCommand.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.eu.habbo.habbohotel.commands; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.gameclients.GameClient; - -public class ConnectCameraCommand extends Command { - public ConnectCameraCommand() { - super("cmd_connect_camera", Emulator.getTexts().getValue("commands.keys.cmd_connect_camera").split(";")); - } - - @Override - public boolean handle(GameClient gameClient, String[] params) throws Exception { - return false; - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPublishToWebEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPublishToWebEvent.java index 9d63b55a..913450d9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPublishToWebEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPublishToWebEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.camera; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.camera.CameraPublishWaitMessageComposer; import com.eu.habbo.messages.outgoing.catalog.NotEnoughPointsTypeComposer; @@ -16,49 +17,54 @@ import java.sql.SQLException; public class CameraPublishToWebEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(CameraPublishToWebEvent.class); - public static int CAMERA_PUBLISH_POINTS = 5; - public static int CAMERA_PUBLISH_POINTS_TYPE = 0; + public static int CAMERA_PUBLISH_POINTS = 1; + public static int CAMERA_PUBLISH_POINTS_TYPE = 5; + public static int CAMERA_PUBLISH_DELAY = 180; @Override - public void handle() throws Exception { + public void handle() { Habbo habbo = this.client.getHabbo(); - if (habbo == null) return; - if (habbo.getHabboInfo().getPhotoTimestamp() == 0) return; - if (habbo.getHabboInfo().getPhotoJSON().isEmpty()) return; - if (!habbo.getHabboInfo().getPhotoJSON().contains(habbo.getHabboInfo().getPhotoTimestamp() + "")) return; - if (habbo.getHabboInfo().getCurrencyAmount(CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS_TYPE) < CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS) { - this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS)); + HabboInfo habboInfo = habbo.getHabboInfo(); + + int points = habboInfo.getCurrencyAmount(CAMERA_PUBLISH_POINTS_TYPE); + if (points < CAMERA_PUBLISH_POINTS) { + String currencyName = Emulator.getTexts().getValue("seasonal.name." + CAMERA_PUBLISH_POINTS_TYPE, "currency"); + habbo.alert("You don't have enough " + currencyName + "!"); + this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CAMERA_PUBLISH_POINTS_TYPE)); return; } - int timestamp = Emulator.getIntUnixTimestamp(); + int photoTimestamp = habboInfo.getPhotoTimestamp(); + String photoJSON = habboInfo.getPhotoJSON(); + if (photoTimestamp == 0 || photoJSON.isEmpty() || !photoJSON.contains(Integer.toString(photoTimestamp))) + return; - boolean isOk = false; - int cooldownLeft = Math.max(0, Emulator.getConfig().getInt("camera.publish.delay") - (timestamp - this.client.getHabbo().getHabboInfo().getWebPublishTimestamp())); + int currentTimestamp = Emulator.getIntUnixTimestamp(); + int timeSinceLastPublish = currentTimestamp - habboInfo.getWebPublishTimestamp(); - if (cooldownLeft == 0) { - UserPublishPictureEvent publishPictureEvent = new UserPublishPictureEvent(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getPhotoURL(), timestamp, this.client.getHabbo().getHabboInfo().getPhotoRoomId()); + if (timeSinceLastPublish < CAMERA_PUBLISH_DELAY) { + int wait = CAMERA_PUBLISH_DELAY - timeSinceLastPublish; + this.client.sendResponse(new CameraPublishWaitMessageComposer(false, wait, habboInfo.getPhotoURL())); + } else { + UserPublishPictureEvent publishPictureEvent = new UserPublishPictureEvent(habbo, habboInfo.getPhotoURL(), currentTimestamp, habboInfo.getPhotoRoomId()); if (!Emulator.getPluginManager().fireEvent(publishPictureEvent).isCancelled()) { - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO camera_web (user_id, room_id, timestamp, url) VALUES (?, ?, ?, ?)")) { - statement.setInt(1, this.client.getHabbo().getHabboInfo().getId()); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO camera_web (user_id, room_id, timestamp, url) VALUES (?, ?, ?, ?)")) { + statement.setInt(1, habboInfo.getId()); statement.setInt(2, publishPictureEvent.roomId); statement.setInt(3, publishPictureEvent.timestamp); statement.setString(4, publishPictureEvent.URL); statement.execute(); - - this.client.getHabbo().getHabboInfo().setWebPublishTimestamp(timestamp); - this.client.getHabbo().givePoints(CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS_TYPE, -CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS); - - isOk = true; + habboInfo.setWebPublishTimestamp(currentTimestamp); + habbo.givePoints(CAMERA_PUBLISH_POINTS_TYPE, -CAMERA_PUBLISH_POINTS); } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); } } + this.client.sendResponse(new CameraPublishWaitMessageComposer(true, 0, "")); } - - this.client.sendResponse(new CameraPublishWaitMessageComposer(isOk, cooldownLeft, isOk ? this.client.getHabbo().getHabboInfo().getPhotoURL() : "")); } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPurchaseEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPurchaseEvent.java index 173f3ca3..b042cdf7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPurchaseEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraPurchaseEvent.java @@ -2,6 +2,9 @@ package com.eu.habbo.messages.incoming.camera; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.achievements.AchievementManager; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.camera.CameraPurchaseSuccesfullComposer; @@ -11,47 +14,53 @@ import com.eu.habbo.messages.outgoing.inventory.InventoryRefreshComposer; import com.eu.habbo.plugin.events.users.UserPurchasePictureEvent; public class CameraPurchaseEvent extends MessageHandler { - public static int CAMERA_PURCHASE_CREDITS = 5; - public static int CAMERA_PURCHASE_POINTS = 5; - public static int CAMERA_PURCHASE_POINTS_TYPE = 0; + public static int CAMERA_PURCHASE_CREDITS = 2; + public static int CAMERA_PURCHASE_POINTS = 0; + public static int CAMERA_PURCHASE_POINTS_TYPE = 5; @Override - public void handle() throws Exception { - if (this.client.getHabbo().getHabboInfo().getCredits() < CameraPurchaseEvent.CAMERA_PURCHASE_CREDITS) { + public void handle() { + Habbo habbo = this.client.getHabbo(); + HabboInfo habboInfo = habbo.getHabboInfo(); + + if (habboInfo.getCredits() < CAMERA_PURCHASE_CREDITS) { + habbo.alert("You don't have enough credits!"); this.client.sendResponse(new NotEnoughPointsTypeComposer(true, false, 0)); return; } - if (this.client.getHabbo().getHabboInfo().getCurrencyAmount(CameraPurchaseEvent.CAMERA_PURCHASE_POINTS_TYPE) < CameraPurchaseEvent.CAMERA_PURCHASE_POINTS) { - this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CameraPurchaseEvent.CAMERA_PURCHASE_POINTS_TYPE)); + if (habboInfo.getCurrencyAmount(CAMERA_PURCHASE_POINTS_TYPE) < CAMERA_PURCHASE_POINTS) { + String alertMessage = "You don't have enough " + Emulator.getTexts().getValue("seasonal.name." + CAMERA_PURCHASE_POINTS_TYPE, "currency") + "!"; + habbo.alert(alertMessage); + this.client.sendResponse(new NotEnoughPointsTypeComposer(false, true, CAMERA_PURCHASE_POINTS_TYPE)); return; } - if (this.client.getHabbo().getHabboInfo().getPhotoTimestamp() == 0) return; - if (this.client.getHabbo().getHabboInfo().getPhotoJSON().isEmpty()) return; - if (!this.client.getHabbo().getHabboInfo().getPhotoJSON().contains(this.client.getHabbo().getHabboInfo().getPhotoTimestamp() + "")) + if (habboInfo.getPhotoTimestamp() == 0 || habboInfo.getPhotoJSON().isEmpty() + || !habboInfo.getPhotoJSON().contains(Integer.toString(habboInfo.getPhotoTimestamp()))) return; - if (Emulator.getPluginManager().fireEvent(new UserPurchasePictureEvent(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getPhotoURL(), this.client.getHabbo().getHabboInfo().getCurrentRoom().getId(), this.client.getHabbo().getHabboInfo().getPhotoTimestamp())).isCancelled()) { + if (Emulator.getPluginManager().fireEvent(new UserPurchasePictureEvent(habbo, habboInfo.getPhotoURL(), habboInfo.getCurrentRoom().getId(), habboInfo.getPhotoTimestamp())).isCancelled()) return; - } - HabboItem photoItem = Emulator.getGameEnvironment().getItemManager().createItem(this.client.getHabbo().getHabboInfo().getId(), Emulator.getGameEnvironment().getItemManager().getItem(Emulator.getConfig().getInt("camera.item_id")), 0, 0, this.client.getHabbo().getHabboInfo().getPhotoJSON()); + Item item = Emulator.getGameEnvironment().getItemManager().getItem(Emulator.getConfig().getInt("camera.item_id")); + if (item == null || !item.getInteractionType().getName().equals("external_image")) + return; + HabboItem photoItem = Emulator.getGameEnvironment().getItemManager().createItem(habboInfo.getId(), item, 0, 0, habboInfo.getPhotoJSON()); if (photoItem != null) { - photoItem.setExtradata(photoItem.getExtradata().replace("%id%", photoItem.getId() + "")); + photoItem.setExtradata(photoItem.getExtradata().replace("%id%", Integer.toString(photoItem.getId()))); photoItem.needsUpdate(true); - - this.client.getHabbo().getInventory().getItemsComponent().addItem(photoItem); + habbo.getInventory().getItemsComponent().addItem(photoItem); this.client.sendResponse(new CameraPurchaseSuccesfullComposer()); this.client.sendResponse(new AddHabboItemComposer(photoItem)); this.client.sendResponse(new InventoryRefreshComposer()); - this.client.getHabbo().giveCredits(-CameraPurchaseEvent.CAMERA_PURCHASE_CREDITS); - this.client.getHabbo().givePoints(CameraPurchaseEvent.CAMERA_PURCHASE_POINTS_TYPE, -CameraPurchaseEvent.CAMERA_PURCHASE_POINTS); + habbo.giveCredits(-CAMERA_PURCHASE_CREDITS); + habbo.givePoints(CAMERA_PURCHASE_POINTS_TYPE, -CAMERA_PURCHASE_POINTS); - AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("CameraPhotoCount")); + AchievementManager.progressAchievement(habbo, Emulator.getGameEnvironment().getAchievementManager().getAchievement("CameraPhotoCount")); } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java index 15506037..0dbb60be 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java @@ -1,37 +1,120 @@ package com.eu.habbo.messages.incoming.camera; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.HabboStats; import com.eu.habbo.messages.incoming.MessageHandler; -import com.eu.habbo.networking.camera.CameraClient; -import com.eu.habbo.networking.camera.messages.outgoing.CameraRenderImageComposer; -import com.eu.habbo.util.crypto.ZIP; +import com.eu.habbo.messages.outgoing.camera.CameraURLComposer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufUtil; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; public class CameraRoomPictureEvent extends MessageHandler { + public static int CAMERA_RENDER_DELAY = 5; + + private ByteBuf image = null; + @Override - public void handle() throws Exception { - if (!this.client.getHabbo().hasPermission("acc_camera")) { - this.client.getHabbo().alert(Emulator.getTexts().getValue("camera.permission")); + public void handle() { + try { + this.make(); + } finally { + if (this.image != null) { + this.image.release(); + } + } + } + + private void make() { + Habbo habbo = this.client.getHabbo(); + if (!habbo.hasPermission("acc_camera")) { + habbo.alert(Emulator.getTexts().getValue("camera.permission")); return; } - if (CameraClient.isLoggedIn) { - this.packet.getBuffer().readFloat(); + HabboInfo habboInfo = habbo.getHabboInfo(); + HabboStats habboStats = habbo.getHabboStats(); + int timestamp = Emulator.getIntUnixTimestamp(); - byte[] data = this.packet.getBuffer().readBytes(this.packet.getBuffer().readableBytes()).array(); - - String content = new String(ZIP.inflate(data)); - CameraRenderImageComposer composer = new CameraRenderImageComposer(this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getCurrentRoom().getBackgroundTonerColor().getRGB(), 320, 320, content); - this.client.getHabbo().getHabboInfo().setPhotoJSON(Emulator.getConfig().getValue("camera.extradata").replace("%timestamp%", composer.timestamp + "")); - this.client.getHabbo().getHabboInfo().setPhotoTimestamp(composer.timestamp); - - if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { - this.client.getHabbo().getHabboInfo().setPhotoRoomId(this.client.getHabbo().getHabboInfo().getCurrentRoom().getId()); + if (habboStats.cache.containsKey("camera_render_cooldown")) { + int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown"); + if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { + String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); + habbo.alert(alertMessage); + if (habboInfo.getPhotoURL() != null) { + String[] splittedPhotoURL = habboInfo.getPhotoURL().split("/"); + if (splittedPhotoURL.length > 0) { + this.client.sendResponse(new CameraURLComposer(splittedPhotoURL[splittedPhotoURL.length - 1])); + } + } + return; } - - Emulator.getCameraClient().sendMessage(composer); - } else { - this.client.getHabbo().alert(Emulator.getTexts().getValue("camera.disabled")); } + habboStats.cache.put("camera_render_cooldown", timestamp); + Room room = habboInfo.getCurrentRoom(); + if (room == null) return; + + int count = this.packet.readInt(); + this.image = this.packet.getBuffer().readBytes(count); + if (this.image == null) return; + + byte[] imageBytes = ByteBufUtil.getBytes(this.image, 0, 4, true); + if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) return; + + BufferedImage theImage; + try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { + theImage = ImageIO.read(in); + } catch (IOException e) { + handleImageProcessingError(habbo); + return; + } + + String fileName = habboInfo.getId() + "_" + timestamp; + String URL = fileName + ".png"; + String URLsmall = fileName + "_small.png"; + String base = Emulator.getConfig().getValue("camera.url"); + String json = Emulator.getConfig().getValue("camera.extradata") + .replace("%timestamp%", Integer.toString(timestamp)) + .replace("%room_id%", Integer.toString(room.getId())) + .replace("%url%", base + URL); + habboInfo.setPhotoURL(base + URL); + habboInfo.setPhotoTimestamp(timestamp); + habboInfo.setPhotoRoomId(room.getId()); + habboInfo.setPhotoJSON(json); + + File imageFile = new File(Emulator.getConfig().getValue("imager.location.output.camera") + URL); + File smallImageFile = new File(Emulator.getConfig().getValue("imager.location.output.camera") + URLsmall); + + try { + ImageIO.write(theImage, "png", imageFile); + Image smallImage = theImage.getScaledInstance(theImage.getWidth(null) / 2, theImage.getHeight(null) / 2, Image.SCALE_SMOOTH); + BufferedImage bi = new BufferedImage(smallImage.getWidth(null), smallImage.getHeight(null), BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics2D = bi.createGraphics(); + graphics2D.drawImage(smallImage, 0, 0, null); + graphics2D.dispose(); + ImageIO.write(bi, "png", smallImageFile); + } catch (IOException e) { + handleImageProcessingError(habbo); + return; + } + + this.client.sendResponse(new CameraURLComposer(URL)); + } + + private boolean isPNG(byte[] bytes) { + return bytes[0] == (byte) 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47; + } + + private void handleImageProcessingError(Habbo habbo) { + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java index c9c1bc12..c2adc118 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java @@ -1,37 +1,90 @@ package com.eu.habbo.messages.incoming.camera; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.HabboStats; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.camera.CameraRoomThumbnailSavedComposer; -import com.eu.habbo.networking.camera.CameraClient; -import com.eu.habbo.networking.camera.messages.outgoing.CameraRenderImageComposer; -import com.eu.habbo.util.crypto.ZIP; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufUtil; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; public class CameraRoomThumbnailEvent extends MessageHandler { + public static int CAMERA_RENDER_DELAY = 5; + + private ByteBuf image = null; + @Override - public void handle() throws Exception { - if (!this.client.getHabbo().hasPermission("acc_camera")) { - this.client.getHabbo().alert(Emulator.getTexts().getValue("camera.permission")); - return; - } - - if (!this.client.getHabbo().getHabboInfo().getCurrentRoom().isOwner(this.client.getHabbo())) - return; - - if (CameraClient.isLoggedIn) { - this.packet.getBuffer().readFloat(); - byte[] data = this.packet.getBuffer().readBytes(this.packet.getBuffer().readableBytes()).array(); - String content = new String(ZIP.inflate(data)); - - CameraRenderImageComposer composer = new CameraRenderImageComposer(this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getCurrentRoom().getBackgroundTonerColor().getRGB(), 110, 110, content); - - this.client.getHabbo().getHabboInfo().setPhotoJSON(Emulator.getConfig().getValue("camera.extradata").replace("%timestamp%", composer.timestamp + "")); - this.client.getHabbo().getHabboInfo().setPhotoTimestamp(composer.timestamp); - - Emulator.getCameraClient().sendMessage(composer); - } else { - this.client.sendResponse(new CameraRoomThumbnailSavedComposer()); - this.client.getHabbo().alert(Emulator.getTexts().getValue("camera.disabled")); + public void handle() { + try { + this.make(); + } finally { + if (this.image != null) { + this.image.release(); + } } } -} \ No newline at end of file + + private void make() { + Habbo habbo = this.client.getHabbo(); + if (!habbo.hasPermission("acc_camera")) { + habbo.alert(Emulator.getTexts().getValue("camera.permission")); + return; + } + + HabboStats habboStats = habbo.getHabboStats(); + int timestamp = Emulator.getIntUnixTimestamp(); + + if (habboStats.cache.containsKey("camera_render_cooldown")) { + int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown"); + if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { + String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); + habbo.alert(alertMessage); + return; + } + } + + habboStats.cache.put("camera_render_cooldown", timestamp); + HabboInfo habboInfo = habbo.getHabboInfo(); + Room room = habboInfo.getCurrentRoom(); + if (room == null || !room.isOwner(habbo)) return; + + int count = this.packet.readInt(); + this.image = this.packet.getBuffer().readBytes(count); + if (this.image == null || !isValidImage(this.image)) return; + + BufferedImage theImage; + try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { + theImage = ImageIO.read(in); + } catch (IOException e) { + e.printStackTrace(); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + + File imageFile = new File(Emulator.getConfig().getValue("imager.location.output.thumbnail") + room.getId() + ".png"); + try { + ImageIO.write(theImage, "png", imageFile); + } catch (IOException e) { + e.printStackTrace(); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + + this.client.sendResponse(new CameraRoomThumbnailSavedComposer()); + } + + private boolean isValidImage(ByteBuf imageBuffer) { + byte[] imageBytes = ByteBufUtil.getBytes(imageBuffer, 0, 4, true); + return imageBytes != null && imageBytes.length >= 4 + && imageBytes[0] == (byte) 0x89 && imageBytes[1] == 0x50 + && imageBytes[2] == 0x4E && imageBytes[3] == 0x47; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraClient.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraClient.java deleted file mode 100644 index 5635d9c9..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraClient.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.eu.habbo.networking.camera; - -import com.eu.habbo.networking.camera.messages.outgoing.CameraLoginComposer; -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.UnpooledByteBufAllocator; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CameraClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(CameraClient.class); - - private static final String host = "google.com"; - private static final int port = 1232; - public static ChannelFuture channelFuture; - public static boolean isLoggedIn = false; - public static boolean attemptReconnect = true; - private static Channel channel; - private final Bootstrap bootstrap = new Bootstrap(); - - public CameraClient() { - - EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); - this.bootstrap.group(eventLoopGroup); - this.bootstrap.channel(NioSocketChannel.class); - this.bootstrap.option(ChannelOption.TCP_NODELAY, true); - this.bootstrap.option(ChannelOption.SO_KEEPALIVE, false); - this.bootstrap.handler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ch.pipeline().addLast(new CameraDecoder()); - ch.pipeline().addLast(new CameraHandler()); - } - }); - this.bootstrap.option(ChannelOption.SO_RCVBUF, 5120); - this.bootstrap.option(ChannelOption.SO_REUSEADDR, true); - this.bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(5120)); - this.bootstrap.option(ChannelOption.ALLOCATOR, new UnpooledByteBufAllocator(false)); - } - - public void connect() { - CameraClient.channelFuture = this.bootstrap.connect(host, port); - - while (!CameraClient.channelFuture.isDone()) { - } - - if (CameraClient.channelFuture.isSuccess()) { - CameraClient.attemptReconnect = false; - CameraClient.channel = channelFuture.channel(); - LOGGER.info("Connected to the Camera Server. Attempting to login."); - this.sendMessage(new CameraLoginComposer()); - } else { - LOGGER.error("Failed to connect to the Camera Server. Server unreachable."); - CameraClient.channel = null; - CameraClient.channelFuture.channel().close(); - CameraClient.channelFuture = null; - CameraClient.attemptReconnect = true; - } - } - - public void disconnect() { - if (channelFuture != null) { - try { - channelFuture.channel().close().sync(); - channelFuture = null; - } catch (Exception e) { - e.printStackTrace(); - } - } - - channel = null; - isLoggedIn = false; - - LOGGER.info("Disconnected from the camera server."); - } - - public void sendMessage(CameraOutgoingMessage outgoingMessage) { - try { - if (isLoggedIn || outgoingMessage instanceof CameraLoginComposer) { - outgoingMessage.compose(channel); - channel.write(outgoingMessage.get().copy(), channel.voidPromise()); - channel.flush(); - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraDecoder.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraDecoder.java deleted file mode 100644 index 7d095616..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraDecoder.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.eu.habbo.networking.camera; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ByteToMessageDecoder; - -import java.util.List; - -class CameraDecoder extends ByteToMessageDecoder { - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List objects) { - int readerIndex = byteBuf.readerIndex(); - if (byteBuf.readableBytes() < 6) { - byteBuf.readerIndex(readerIndex); - return; - } - - int length = byteBuf.readInt(); - byteBuf.readerIndex(readerIndex); - - if (byteBuf.readableBytes() < (length)) { - byteBuf.readerIndex(readerIndex); - return; - } - - byteBuf.readerIndex(readerIndex); - objects.add(byteBuf.readBytes(length + 4)); - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraHandler.java deleted file mode 100644 index 6d8abb2d..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.eu.habbo.networking.camera; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; - -public class CameraHandler extends ChannelInboundHandlerAdapter { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - try { - ByteBuf message = (ByteBuf) msg; - ((ByteBuf) msg).readerIndex(0); - int length = message.readInt(); - - ByteBuf b = Unpooled.wrappedBuffer(message.readBytes(length)); - - short header = b.readShort(); - - try { - CameraPacketHandler.instance().handle(ctx.channel(), header, b); - } catch (Exception e) { - - } finally { - try { - - b.release(); - } catch (Exception e) { - } - try { - - ((ByteBuf) msg).release(); - } catch (Exception e) { - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - CameraClient.attemptReconnect = true; - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraIncomingMessage.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraIncomingMessage.java deleted file mode 100644 index 4625a2ba..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraIncomingMessage.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.eu.habbo.networking.camera; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; - -import java.nio.charset.Charset; - -public abstract class CameraIncomingMessage extends CameraMessage { - public CameraIncomingMessage(Short header, ByteBuf body) { - super(header); - this.buffer.writerIndex(0).writeBytes(body); - } - - public int readShort() { - return this.buffer.readShort(); - } - - public Integer readInt() { - try { - return this.buffer.readInt(); - } catch (Exception e) { - } - - return 0; - } - - public boolean readBoolean() { - try { - return this.buffer.readByte() == 1; - } catch (Exception e) { - } - - return false; - } - - - public String readString() { - try { - int length = this.readInt(); - byte[] data = new byte[length]; - this.buffer.readBytes(data); - return new String(data); - } catch (Exception e) { - return ""; - } - } - - public String getMessageBody() { - String consoleText = this.buffer.toString(Charset.defaultCharset()); - - for (int i = -1; i < 31; i++) { - consoleText = consoleText.replace(Character.toString((char) i), "[" + i + "]"); - } - - return consoleText; - } - - public int bytesAvailable() { - return this.buffer.readableBytes(); - } - - public abstract void handle(Channel client) throws Exception; -} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraMessage.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraMessage.java deleted file mode 100644 index fbcceb17..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraMessage.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.eu.habbo.networking.camera; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; - -public class CameraMessage { - protected final short header; - protected final ByteBuf buffer; - - public CameraMessage(short header) { - this.header = header; - this.buffer = Unpooled.buffer(); - } - - public short getHeader() { - return this.header; - } -} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraOutgoingMessage.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraOutgoingMessage.java deleted file mode 100644 index a1b3f083..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraOutgoingMessage.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.eu.habbo.networking.camera; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufOutputStream; -import io.netty.channel.Channel; - -import java.io.IOException; -import java.nio.charset.Charset; - -public abstract class CameraOutgoingMessage extends CameraMessage { - private final ByteBufOutputStream stream; - - public CameraOutgoingMessage(short header) { - super(header); - - this.stream = new ByteBufOutputStream(this.buffer); - try { - this.stream.writeInt(0); - this.stream.writeShort(header); - } catch (Exception e) { - } - } - - public void appendRawBytes(byte[] bytes) { - try { - this.stream.write(bytes); - } catch (IOException e) { - } - } - - public void appendString(String obj) { - try { - byte[] data = obj.getBytes(); - this.stream.writeInt(data.length); - this.stream.write(data); - } catch (IOException e) { - } - } - - public void appendChar(int obj) { - try { - this.stream.writeChar(obj); - } catch (IOException e) { - } - } - - public void appendChars(Object obj) { - try { - this.stream.writeChars(obj.toString()); - } catch (IOException e) { - } - } - - public void appendInt32(Integer obj) { - try { - this.stream.writeInt(obj); - } catch (IOException e) { - } - } - - public void appendInt32(Byte obj) { - try { - this.stream.writeInt((int) obj); - } catch (IOException e) { - } - } - - public void appendInt32(Boolean obj) { - try { - this.stream.writeInt(obj ? 1 : 0); - } catch (IOException e) { - } - } - - public void appendShort(int obj) { - try { - this.stream.writeShort((short) obj); - } catch (IOException e) { - } - } - - public void appendByte(Integer b) { - try { - this.stream.writeByte(b); - } catch (IOException e) { - } - } - - public void appendBoolean(Boolean obj) { - try { - this.stream.writeBoolean(obj); - } catch (IOException e) { - } - } - - public CameraOutgoingMessage appendResponse(CameraOutgoingMessage obj) { - try { - this.stream.write(obj.get().array()); - } catch (IOException e) { - } - - return this; - } - - public String getBodyString() { - ByteBuf buffer = this.stream.buffer().duplicate(); - - buffer.setInt(0, buffer.writerIndex() - 4); - - String consoleText = buffer.toString(Charset.forName("UTF-8")); - - for (int i = 0; i < 14; i++) { - consoleText = consoleText.replace(Character.toString((char) i), "[" + i + "]"); - } - - buffer.discardSomeReadBytes(); - - return consoleText; - } - - public ByteBuf get() { - this.buffer.setInt(0, this.buffer.writerIndex() - 4); - - return this.buffer.copy(); - } - - public abstract void compose(Channel channel); -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraPacketHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraPacketHandler.java deleted file mode 100644 index b0dc0582..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/CameraPacketHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.eu.habbo.networking.camera; - -import com.eu.habbo.networking.camera.messages.incoming.*; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; - -public class CameraPacketHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(CameraPacketHandler.class); - - private static CameraPacketHandler INSTANCE; - private final HashMap> packetDefinitions; - - public CameraPacketHandler() { - this.packetDefinitions = new HashMap<>(); - - this.packetDefinitions.put((short) 1, CameraLoginStatusEvent.class); - this.packetDefinitions.put((short) 2, CameraResultURLEvent.class); - this.packetDefinitions.put((short) 3, CameraRoomThumbnailGeneratedEvent.class); - this.packetDefinitions.put((short) 4, CameraUpdateNotification.class); - this.packetDefinitions.put((short) 5, CameraAuthenticationTicketEvent.class); - } - - public static CameraPacketHandler instance() { - if (INSTANCE == null) { - INSTANCE = new CameraPacketHandler(); - } - - return INSTANCE; - } - - public void handle(Channel channel, short i, ByteBuf ii) { - Class declaredClass = this.packetDefinitions.get(i); - - if (declaredClass != null) { - try { - CameraIncomingMessage message = declaredClass.getDeclaredConstructor(new Class[]{Short.class, ByteBuf.class}).newInstance(i, ii); - message.handle(channel); - message.buffer.release(); - } catch (Exception e) { - LOGGER.error("Caught exception", e); - } - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/CameraOutgoingHeaders.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/CameraOutgoingHeaders.java deleted file mode 100644 index e9892031..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/CameraOutgoingHeaders.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.eu.habbo.networking.camera.messages; - -public class CameraOutgoingHeaders { - public final static short LoginComposer = 1; - public final static short RenderImageComposer = 2; -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraAuthenticationTicketEvent.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraAuthenticationTicketEvent.java deleted file mode 100644 index 08f10147..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraAuthenticationTicketEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.eu.habbo.networking.camera.messages.incoming; - -import com.eu.habbo.messages.outgoing.gamecenter.basejump.BaseJumpLoadGameComposer; -import com.eu.habbo.networking.camera.CameraIncomingMessage; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; - -public class CameraAuthenticationTicketEvent extends CameraIncomingMessage { - public CameraAuthenticationTicketEvent(Short header, ByteBuf body) { - super(header, body); - } - - @Override - public void handle(Channel client) throws Exception { - String ticket = this.readString(); - - if (ticket.startsWith("FASTFOOD")) { - BaseJumpLoadGameComposer.FASTFOOD_KEY = ticket; - } - } -} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraLoginStatusEvent.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraLoginStatusEvent.java deleted file mode 100644 index a13a5092..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraLoginStatusEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.eu.habbo.networking.camera.messages.incoming; - -import com.eu.habbo.networking.camera.CameraClient; -import com.eu.habbo.networking.camera.CameraIncomingMessage; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CameraLoginStatusEvent extends CameraIncomingMessage { - - private static final Logger LOGGER = LoggerFactory.getLogger(CameraLoginStatusEvent.class); - - public final static int LOGIN_OK = 0; - public final static int LOGIN_ERROR = 1; - public final static int NO_ACCOUNT = 2; - public final static int ALREADY_LOGGED_IN = 3; - public final static int BANNED = 4; - public final static int OLD_BUILD = 5; - public final static int NO_CAMERA_SUBSCRIPTION = 6; - - public CameraLoginStatusEvent(Short header, ByteBuf body) { - super(header, body); - } - - @Override - public void handle(Channel client) throws Exception { - int status = this.readInt(); - - if (status == LOGIN_ERROR) { - LOGGER.error("Failed to login to Camera Server: Incorrect Details"); - } else if (status == NO_ACCOUNT) { - LOGGER.error("Failed to login to Camera Server: No Account Found. Register for free on the Arcturus Forums! Visit http://arcturus.pw/"); - } else if (status == BANNED) { - LOGGER.error("Sorry but you seem to be banned from the Arcturus forums and therefor cant use the Camera Server :'("); - } else if (status == ALREADY_LOGGED_IN) { - LOGGER.error("You seem to be already connected to the Camera Server"); - } else if (status == OLD_BUILD) { - LOGGER.error("This version of Arcturus Emulator is no longer supported by the Camera Server. Upgrade your emulator."); - } else if (status == NO_CAMERA_SUBSCRIPTION) { - LOGGER.error("You don't have a Camera Subscription and therefor cannot use the camera!"); - LOGGER.error("Please consider making a donation to keep this project going. The emulator can be used free of charge!"); - LOGGER.error("A trial version is available for $2.5. A year subscription is only $10 and a permanent subscription is $25."); - LOGGER.error("By donating this subscription you support the development of the emulator you are using :)"); - LOGGER.error("Visit http://arcturus.pw/mysubscriptions.php to buy your subscription!"); - LOGGER.error("Please Consider getting a subscription. Regards: The General"); - } - - if (status == LOGIN_OK) { - CameraClient.isLoggedIn = true; - LOGGER.info("Succesfully connected to the Arcturus Camera Server!"); - } else { - CameraClient.attemptReconnect = false; - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraResultURLEvent.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraResultURLEvent.java deleted file mode 100644 index 61e0f012..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraResultURLEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.eu.habbo.networking.camera.messages.incoming; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.achievements.AchievementManager; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.outgoing.camera.CameraURLComposer; -import com.eu.habbo.networking.camera.CameraIncomingMessage; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; - -public class CameraResultURLEvent extends CameraIncomingMessage { - public final static int STATUS_OK = 0; - public final static int STATUS_ERROR = 1; - - public CameraResultURLEvent(Short header, ByteBuf body) { - super(header, body); - } - - @Override - public void handle(Channel client) throws Exception { - int userId = this.readInt(); - int status = this.readInt(); - String URL = this.readString(); - - if (!Emulator.getConfig().getBoolean("camera.use.https", true)) { - URL = URL.replace("https://", "http://"); - } - - int roomId = this.readInt(); - int timestamp = this.readInt(); - - Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); - - if (status == STATUS_ERROR) { - if (habbo != null) { - habbo.getHabboInfo().setPhotoTimestamp(0); - habbo.getHabboInfo().setPhotoJSON(""); - habbo.getHabboInfo().setPhotoURL(""); - - habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); - return; - } - } - - if (status == STATUS_OK) { - if (habbo != null) { - if (timestamp == habbo.getHabboInfo().getPhotoTimestamp()) { - AchievementManager.progressAchievement(habbo, Emulator.getGameEnvironment().getAchievementManager().getAchievement("CameraPhotoCount"), 1); - habbo.getClient().sendResponse(new CameraURLComposer(URL)); - habbo.getHabboInfo().setPhotoJSON(habbo.getHabboInfo().getPhotoJSON().replace("%room_id%", roomId + "").replace("%url%", URL)); - habbo.getHabboInfo().setPhotoURL(URL); - } - } - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraRoomThumbnailGeneratedEvent.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraRoomThumbnailGeneratedEvent.java deleted file mode 100644 index 96783a2d..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraRoomThumbnailGeneratedEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.eu.habbo.networking.camera.messages.incoming; - -import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.users.Habbo; -import com.eu.habbo.messages.outgoing.camera.CameraRoomThumbnailSavedComposer; -import com.eu.habbo.networking.camera.CameraIncomingMessage; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; - -public class CameraRoomThumbnailGeneratedEvent extends CameraIncomingMessage { - public CameraRoomThumbnailGeneratedEvent(Short header, ByteBuf body) { - super(header, body); - } - - @Override - public void handle(Channel client) throws Exception { - int userId = this.readInt(); - - Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); - - if (habbo != null) { - habbo.getClient().sendResponse(new CameraRoomThumbnailSavedComposer()); - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraUpdateNotification.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraUpdateNotification.java deleted file mode 100644 index 358e8e1c..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/incoming/CameraUpdateNotification.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.eu.habbo.networking.camera.messages.incoming; - -import com.eu.habbo.Emulator; -import com.eu.habbo.messages.outgoing.generic.alerts.GenericAlertComposer; -import com.eu.habbo.networking.camera.CameraIncomingMessage; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CameraUpdateNotification extends CameraIncomingMessage { - - private static final Logger LOGGER = LoggerFactory.getLogger(CameraUpdateNotification.class); - - public CameraUpdateNotification(Short header, ByteBuf body) { - super(header, body); - } - - @Override - public void handle(Channel client) throws Exception { - boolean alert = this.readBoolean(); - String message = this.readString(); - int type = this.readInt(); - - if (type == 0) { - LOGGER.info("Camera update: {}", message); - } else if (type == 1) { - LOGGER.warn("Camera update: {}", message); - } else if (type == 2) { - LOGGER.error("Camera update: {}", message); - } - - if (alert) { - Emulator.getGameServer().getGameClientManager().sendBroadcastResponse(new GenericAlertComposer(message).compose()); - } - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraLoginComposer.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraLoginComposer.java deleted file mode 100644 index f8b97a0f..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraLoginComposer.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.eu.habbo.networking.camera.messages.outgoing; - -import com.eu.habbo.Emulator; -import com.eu.habbo.networking.camera.CameraOutgoingMessage; -import com.eu.habbo.networking.camera.messages.CameraOutgoingHeaders; -import io.netty.channel.Channel; - -public class CameraLoginComposer extends CameraOutgoingMessage { - public CameraLoginComposer() { - super(CameraOutgoingHeaders.LoginComposer); - } - - @Override - public void compose(Channel channel) { - this.appendString(Emulator.getConfig().getValue("username").trim()); - this.appendString(Emulator.getConfig().getValue("password").trim()); - this.appendString(Emulator.version); - } -} \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraRenderImageComposer.java b/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraRenderImageComposer.java deleted file mode 100644 index 07c10871..00000000 --- a/Emulator/src/main/java/com/eu/habbo/networking/camera/messages/outgoing/CameraRenderImageComposer.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.eu.habbo.networking.camera.messages.outgoing; - -import com.eu.habbo.Emulator; -import com.eu.habbo.networking.camera.CameraOutgoingMessage; -import com.eu.habbo.networking.camera.messages.CameraOutgoingHeaders; -import io.netty.channel.Channel; - -public class CameraRenderImageComposer extends CameraOutgoingMessage { - public final int timestamp; - final int userId; - final int backgroundColor; - final int width; - final int height; - final String JSON; - - public CameraRenderImageComposer(int userId, int backgroundColor, int width, int height, String json) { - super(CameraOutgoingHeaders.RenderImageComposer); - - this.userId = userId; - this.timestamp = Emulator.getIntUnixTimestamp(); - this.backgroundColor = backgroundColor; - this.width = width; - this.height = height; - this.JSON = json; - } - - @Override - public void compose(Channel channel) { - this.appendInt32(this.userId); - this.appendInt32(this.timestamp); - this.appendInt32(this.backgroundColor); - this.appendInt32(this.width); - this.appendInt32(this.height); - this.appendString(this.JSON); - } -} \ No newline at end of file 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 36b8b768..d4bbc1c7 100644 --- a/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java +++ b/Emulator/src/main/java/com/eu/habbo/plugin/PluginManager.java @@ -28,8 +28,6 @@ import com.eu.habbo.habbohotel.wired.core.WiredEngine; import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.highscores.WiredHighscoreManager; import com.eu.habbo.messages.PacketManager; -import com.eu.habbo.messages.incoming.camera.CameraPublishToWebEvent; -import com.eu.habbo.messages.incoming.camera.CameraPurchaseEvent; import com.eu.habbo.messages.incoming.catalog.CheckPetNameEvent; import com.eu.habbo.messages.incoming.floorplaneditor.FloorPlanEditorSaveEvent; import com.eu.habbo.messages.incoming.hotelview.HotelViewRequestLTDAvailabilityEvent; @@ -84,7 +82,6 @@ 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"); @@ -162,11 +159,6 @@ public class PluginManager { ChangeNameCheckUsernameEvent.VALID_CHARACTERS = Emulator.getConfig().getValue("allowed.username.characters", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-=!?@:,."); - CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS = Emulator.getConfig().getInt("camera.price.points.publish", 5); - CameraPublishToWebEvent.CAMERA_PUBLISH_POINTS_TYPE = Emulator.getConfig().getInt("camera.price.points.publish.type", 0); - CameraPurchaseEvent.CAMERA_PURCHASE_CREDITS = Emulator.getConfig().getInt("camera.price.credits", 5); - CameraPurchaseEvent.CAMERA_PURCHASE_POINTS = Emulator.getConfig().getInt("camera.price.points", 5); - CameraPurchaseEvent.CAMERA_PURCHASE_POINTS_TYPE = Emulator.getConfig().getInt("camera.price.points.type", 0); BuyRoomPromotionEvent.ROOM_PROMOTION_BADGE = Emulator.getConfig().getValue("room.promotion.badge", "RADZZ"); BotManager.MAXIMUM_BOT_INVENTORY_SIZE = Emulator.getConfig().getInt("hotel.bots.max.inventory"); diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/CameraClientAutoReconnect.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/CameraClientAutoReconnect.java deleted file mode 100644 index a3b31144..00000000 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/CameraClientAutoReconnect.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.eu.habbo.threading.runnables; - -import com.eu.habbo.Emulator; -import com.eu.habbo.networking.camera.CameraClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CameraClientAutoReconnect implements Runnable { - - private static final Logger LOGGER = LoggerFactory.getLogger(CameraClientAutoReconnect.class); - - @Override - public void run() { - if (CameraClient.attemptReconnect && !Emulator.isShuttingDown) { - if (!(CameraClient.channelFuture != null && CameraClient.channelFuture.channel().isRegistered())) { - LOGGER.info("Attempting to connect to the Camera server."); - if (Emulator.getCameraClient() != null) { - Emulator.getCameraClient().disconnect(); - } else { - Emulator.setCameraClient(new CameraClient()); - } - - try { - Emulator.getCameraClient().connect(); - } catch (Exception e) { - LOGGER.error("Failed to start the camera client.", e); - } - } else { - CameraClient.attemptReconnect = false; - LOGGER.info("Already connected to the camera. Reconnecting not needed!"); - } - } - - Emulator.getThreading().run(this, 5000); - } -} \ No newline at end of file From 2b95f446ddabc12d02074148bcee82894c944b1f Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 18 Mar 2026 07:34:18 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=86=99=20Security=20update=20Camera?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add file size limits (2MB photos, 1MB thumbnails) with pre-read rejection - Add PNG dimension validation via IHDR header before full decode (prevents decompression bombs) - Double-check decoded dimensions against max 1024x1024 (photos) / 640x640 (thumbnails) - Increase render cooldown from 5s to 15s - Add daily render quota (50/day per user) with 24h rolling window - Fix cooldown message to show remaining seconds correctly - Add structured logging for all rejected uploads - Replace e.printStackTrace() with proper SLF4J logging --- .../camera/CameraRoomPictureEvent.java | 119 +++++++++++++++++- .../camera/CameraRoomThumbnailEvent.java | 87 ++++++++++++- 2 files changed, 195 insertions(+), 11 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java index 0dbb60be..af8b4d0e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomPictureEvent.java @@ -10,15 +10,26 @@ import com.eu.habbo.messages.outgoing.camera.CameraURLComposer; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.util.Iterator; public class CameraRoomPictureEvent extends MessageHandler { - public static int CAMERA_RENDER_DELAY = 5; + private static final Logger LOGGER = LoggerFactory.getLogger(CameraRoomPictureEvent.class); + + public static int CAMERA_RENDER_DELAY = 15; + public static int MAX_IMAGE_BYTES = 2 * 1024 * 1024; // 2 MB max upload + public static int MAX_IMAGE_WIDTH = 1024; + public static int MAX_IMAGE_HEIGHT = 1024; + public static int MAX_DAILY_RENDERS = 50; private ByteBuf image = null; @@ -47,7 +58,7 @@ public class CameraRoomPictureEvent extends MessageHandler { if (habboStats.cache.containsKey("camera_render_cooldown")) { int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown"); if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { - String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); + String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(CAMERA_RENDER_DELAY - (timestamp - cameraTimestamp))); habbo.alert(alertMessage); if (habboInfo.getPhotoURL() != null) { String[] splittedPhotoURL = habboInfo.getPhotoURL().split("/"); @@ -59,16 +70,55 @@ public class CameraRoomPictureEvent extends MessageHandler { } } + // Daily render quota check + int dailyRenderCount = getDailyRenderCount(habboStats, timestamp); + if (dailyRenderCount >= MAX_DAILY_RENDERS) { + habbo.alert(Emulator.getTexts().getValue("camera.daily.limit", "You have reached the daily photo limit. Try again tomorrow.")); + return; + } + incrementDailyRenderCount(habboStats, timestamp, dailyRenderCount); + habboStats.cache.put("camera_render_cooldown", timestamp); Room room = habboInfo.getCurrentRoom(); if (room == null) return; int count = this.packet.readInt(); + + // Reject oversized payloads before reading + if (count <= 0 || count > MAX_IMAGE_BYTES) { + LOGGER.warn("User {} attempted camera upload with invalid size: {} bytes", habboInfo.getUsername(), count); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + this.image = this.packet.getBuffer().readBytes(count); if (this.image == null) return; byte[] imageBytes = ByteBufUtil.getBytes(this.image, 0, 4, true); - if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) return; + if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) { + LOGGER.warn("User {} attempted camera upload with non-PNG data", habboInfo.getUsername()); + return; + } + + // Validate image dimensions before fully decoding + int[] dimensions; + try { + dimensions = readPNGDimensions(this.image); + } catch (IOException e) { + LOGGER.warn("User {} uploaded image with unreadable dimensions", habboInfo.getUsername()); + handleImageProcessingError(habbo); + return; + } + + if (dimensions == null || dimensions[0] <= 0 || dimensions[1] <= 0 + || dimensions[0] > MAX_IMAGE_WIDTH || dimensions[1] > MAX_IMAGE_HEIGHT) { + LOGGER.warn("User {} attempted camera upload with invalid dimensions: {}x{}", + habboInfo.getUsername(), + dimensions != null ? dimensions[0] : "null", + dimensions != null ? dimensions[1] : "null"); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } BufferedImage theImage; try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { @@ -78,6 +128,19 @@ public class CameraRoomPictureEvent extends MessageHandler { return; } + if (theImage == null) { + LOGGER.warn("User {} uploaded image that could not be decoded", habboInfo.getUsername()); + handleImageProcessingError(habbo); + return; + } + + // Double-check decoded dimensions match expectations + if (theImage.getWidth() > MAX_IMAGE_WIDTH || theImage.getHeight() > MAX_IMAGE_HEIGHT) { + LOGGER.warn("User {} decoded image exceeds dimension limits: {}x{}", habboInfo.getUsername(), theImage.getWidth(), theImage.getHeight()); + handleImageProcessingError(habbo); + return; + } + String fileName = habboInfo.getId() + "_" + timestamp; String URL = fileName + ".png"; String URLsmall = fileName + "_small.png"; @@ -96,10 +159,12 @@ public class CameraRoomPictureEvent extends MessageHandler { try { ImageIO.write(theImage, "png", imageFile); - Image smallImage = theImage.getScaledInstance(theImage.getWidth(null) / 2, theImage.getHeight(null) / 2, Image.SCALE_SMOOTH); - BufferedImage bi = new BufferedImage(smallImage.getWidth(null), smallImage.getHeight(null), BufferedImage.TYPE_INT_ARGB); + int smallWidth = theImage.getWidth(null) / 2; + int smallHeight = theImage.getHeight(null) / 2; + BufferedImage bi = new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics2D = bi.createGraphics(); - graphics2D.drawImage(smallImage, 0, 0, null); + graphics2D.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + graphics2D.drawImage(theImage, 0, 0, smallWidth, smallHeight, null); graphics2D.dispose(); ImageIO.write(bi, "png", smallImageFile); } catch (IOException e) { @@ -114,6 +179,48 @@ public class CameraRoomPictureEvent extends MessageHandler { return bytes[0] == (byte) 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47; } + /** + * Read PNG dimensions from the IHDR chunk without fully decoding the image. + * This prevents decompression bomb attacks by checking dimensions before allocation. + */ + private int[] readPNGDimensions(ByteBuf buf) throws IOException { + try (ByteBufInputStream in = new ByteBufInputStream(buf.duplicate())) { + try (ImageInputStream iis = ImageIO.createImageInputStream(in)) { + Iterator readers = ImageIO.getImageReaders(iis); + if (!readers.hasNext()) return null; + + ImageReader reader = readers.next(); + try { + reader.setInput(iis); + int width = reader.getWidth(0); + int height = reader.getHeight(0); + return new int[]{width, height}; + } finally { + reader.dispose(); + } + } + } + } + + private int getDailyRenderCount(HabboStats stats, int currentTimestamp) { + if (!stats.cache.containsKey("camera_daily_count") || !stats.cache.containsKey("camera_daily_reset")) { + return 0; + } + int resetTimestamp = (Integer) stats.cache.get("camera_daily_reset"); + // Reset counter if more than 24 hours have passed + if (currentTimestamp - resetTimestamp >= 86400) { + return 0; + } + return (Integer) stats.cache.get("camera_daily_count"); + } + + private void incrementDailyRenderCount(HabboStats stats, int currentTimestamp, int currentCount) { + if (currentCount == 0) { + stats.cache.put("camera_daily_reset", currentTimestamp); + } + stats.cache.put("camera_daily_count", currentCount + 1); + } + private void handleImageProcessingError(Habbo habbo) { habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java index c2adc118..ba630619 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/camera/CameraRoomThumbnailEvent.java @@ -10,14 +10,24 @@ import com.eu.habbo.messages.outgoing.camera.CameraRoomThumbnailSavedComposer; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.util.Iterator; public class CameraRoomThumbnailEvent extends MessageHandler { - public static int CAMERA_RENDER_DELAY = 5; + private static final Logger LOGGER = LoggerFactory.getLogger(CameraRoomThumbnailEvent.class); + + public static int CAMERA_RENDER_DELAY = 15; + public static int MAX_THUMBNAIL_BYTES = 1024 * 1024; // 1 MB max for thumbnails + public static int MAX_THUMBNAIL_WIDTH = 640; + public static int MAX_THUMBNAIL_HEIGHT = 640; private ByteBuf image = null; @@ -45,7 +55,7 @@ public class CameraRoomThumbnailEvent extends MessageHandler { if (habboStats.cache.containsKey("camera_render_cooldown")) { int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown"); if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) { - String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp)); + String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(CAMERA_RENDER_DELAY - (timestamp - cameraTimestamp))); habbo.alert(alertMessage); return; } @@ -57,14 +67,58 @@ public class CameraRoomThumbnailEvent extends MessageHandler { if (room == null || !room.isOwner(habbo)) return; int count = this.packet.readInt(); + + // Reject oversized payloads before reading + if (count <= 0 || count > MAX_THUMBNAIL_BYTES) { + LOGGER.warn("User {} attempted thumbnail upload with invalid size: {} bytes", habboInfo.getUsername(), count); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + this.image = this.packet.getBuffer().readBytes(count); - if (this.image == null || !isValidImage(this.image)) return; + if (this.image == null || !isValidImage(this.image)) { + LOGGER.warn("User {} attempted thumbnail upload with non-PNG data", habboInfo.getUsername()); + return; + } + + // Validate dimensions before fully decoding (prevents decompression bombs) + int[] dimensions; + try { + dimensions = readPNGDimensions(this.image); + } catch (IOException e) { + LOGGER.warn("User {} uploaded thumbnail with unreadable dimensions", habboInfo.getUsername()); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + + if (dimensions == null || dimensions[0] <= 0 || dimensions[1] <= 0 + || dimensions[0] > MAX_THUMBNAIL_WIDTH || dimensions[1] > MAX_THUMBNAIL_HEIGHT) { + LOGGER.warn("User {} attempted thumbnail upload with invalid dimensions: {}x{}", + habboInfo.getUsername(), + dimensions != null ? dimensions[0] : "null", + dimensions != null ? dimensions[1] : "null"); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } BufferedImage theImage; try (ByteBufInputStream in = new ByteBufInputStream(this.image)) { theImage = ImageIO.read(in); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to decode thumbnail from user {}", habboInfo.getUsername(), e); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + + if (theImage == null) { + LOGGER.warn("User {} uploaded thumbnail that could not be decoded", habboInfo.getUsername()); + habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); + return; + } + + // Double-check decoded dimensions + if (theImage.getWidth() > MAX_THUMBNAIL_WIDTH || theImage.getHeight() > MAX_THUMBNAIL_HEIGHT) { + LOGGER.warn("User {} decoded thumbnail exceeds dimension limits: {}x{}", habboInfo.getUsername(), theImage.getWidth(), theImage.getHeight()); habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); return; } @@ -73,7 +127,7 @@ public class CameraRoomThumbnailEvent extends MessageHandler { try { ImageIO.write(theImage, "png", imageFile); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to write thumbnail for room {}", room.getId(), e); habbo.alert(Emulator.getTexts().getValue("camera.error.creation")); return; } @@ -87,4 +141,27 @@ public class CameraRoomThumbnailEvent extends MessageHandler { && imageBytes[0] == (byte) 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47; } + + /** + * Read PNG dimensions from the IHDR chunk without fully decoding the image. + * This prevents decompression bomb attacks by checking dimensions before allocation. + */ + private int[] readPNGDimensions(ByteBuf buf) throws IOException { + try (ByteBufInputStream in = new ByteBufInputStream(buf.duplicate())) { + try (ImageInputStream iis = ImageIO.createImageInputStream(in)) { + Iterator readers = ImageIO.getImageReaders(iis); + if (!readers.hasNext()) return null; + + ImageReader reader = readers.next(); + try { + reader.setInput(iis); + int width = reader.getWidth(0); + int height = reader.getHeight(0); + return new int[]{width, height}; + } finally { + reader.dispose(); + } + } + } + } }