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/Database Updates/Default_Camera.sql b/Database Updates/Default_Camera.sql new file mode 100644 index 00000000..63240398 --- /dev/null +++ b/Database Updates/Default_Camera.sql @@ -0,0 +1,71 @@ +-- ============================================================ +-- Camera - Database Setup +-- Run this SQL manually before using the camera feature. +-- ============================================================ + +-- ----------------------------------------- +-- Table: camera_web (stores published photos) +-- ----------------------------------------- +CREATE TABLE IF NOT EXISTS `camera_web` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `user_id` INT(11) NOT NULL, + `room_id` INT(11) NOT NULL DEFAULT 0, + `timestamp` INT(11) NOT NULL DEFAULT 0, + `url` VARCHAR(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + INDEX `idx_camera_web_user_id` (`user_id`), + INDEX `idx_camera_web_timestamp` (`timestamp`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ----------------------------------------- +-- Emulator Settings for Camera +-- ----------------------------------------- +-- Uses INSERT IGNORE so existing values are not overwritten. + +-- Base URL where camera photos are served (include trailing slash) +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.url', 'http://localhost/camera/'); + +-- Filesystem path where full-size camera photos are saved (include trailing slash) +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('imager.location.output.camera', '/path/to/www/camera/'); + +-- Filesystem path where room thumbnail images are saved (include trailing slash) +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('imager.location.output.thumbnail', '/path/to/www/thumbnails/'); + +-- Item ID for the wall photo item (must exist in items_base with interaction type "external_image") +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.item_id', '0'); + +-- Price in credits to purchase a photo as a wall item +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.credits', '2'); + +-- Price in seasonal points to purchase a photo as a wall item +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.points', '0'); + +-- Price in seasonal points to publish a photo to the web +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.price.points.publish', '1'); + +-- JSON template for photo item extradata +-- Available placeholders: %timestamp%, %room_id%, %url%, %id% +INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES +('camera.extradata', '{"t":"%timestamp%","u":"%id%","m":"","s":"%room_id%","w":"%url%"}'); + +-- ----------------------------------------- +-- Emulator Texts for Camera +-- ----------------------------------------- +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.permission', 'You do not have permission to use the camera.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.wait', 'Please wait %seconds% more seconds before taking another photo.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.error.creation', 'An error occurred while processing your photo. Please try again.'); + +INSERT IGNORE INTO `emulator_texts` (`key`, `value`) VALUES +('camera.daily.limit', 'You have reached the daily photo limit. Try again tomorrow.'); 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/habbohotel/items/interactions/games/football/InteractionRebugFootball.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/football/InteractionRebugFootball.java index 98666d50..4974c8fc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/football/InteractionRebugFootball.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/games/football/InteractionRebugFootball.java @@ -1,23 +1,24 @@ package com.eu.habbo.habbohotel.items.interactions.games.football; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionDefault; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomLayout; +import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.threading.runnables.RebugKickBallAction; +import com.eu.habbo.util.pathfinding.Direction8; import java.sql.ResultSet; import java.sql.SQLException; -/** - * Rebug-style football interaction. - * Uses simplified momentum-decay physics with 180-degree bounce. - * Set interaction_type to "rebug_football" on a ball item to use this instead of the default football physics. - */ public class InteractionRebugFootball extends InteractionDefault { private RebugKickBallAction currentThread; + private Direction8 lastDribbleDirection; public InteractionRebugFootball(ResultSet set, Item baseItem) throws SQLException { super(set, baseItem); @@ -43,12 +44,65 @@ public class InteractionRebugFootball extends InteractionDefault { public void onWalkOn(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { super.onWalkOn(roomUnit, room, objects); + Direction8 userDir = Direction8.getDirection(roomUnit.getBodyRotation().getValue()); + this.lastDribbleDirection = userDir; + + RoomTile goal = roomUnit.getGoal(); + if (goal != null && goal.x == this.getX() && goal.y == this.getY()) { + this.kick(room, roomUnit, 55); + } else { + this.kick(room, roomUnit, 0); + } + } + + @Override + public void onWalkOff(RoomUnit roomUnit, Room room, Object[] objects) throws Exception { + super.onWalkOff(roomUnit, room, objects); + + if (objects != null && objects.length >= 2 && objects[1] instanceof RoomTile && objects[0] instanceof RoomTile) { + RoomTile fromTile = (RoomTile) objects[0]; + RoomTile nextTile = (RoomTile) objects[1]; + + int dx = nextTile.x - fromTile.x; + int dy = nextTile.y - fromTile.y; + Direction8 walkDir = Direction8.fromDelta(dx, dy); + + if (this.lastDribbleDirection != null && walkDir.getRot() == this.lastDribbleDirection.getRot()) { + this.kick(room, roomUnit, 55); + return; + } + } + + if (this.currentThread != null) { + this.currentThread.dead = true; + this.currentThread = null; + } + this.setExtradata("0"); + room.sendComposer(new ItemStateComposer(this).compose()); + } + + @Override + public void onClick(GameClient client, Room room, Object[] objects) throws Exception { + super.onClick(client, room, objects); + + if (client == null) return; + RoomUnit unit = client.getHabbo().getRoomUnit(); + if (RoomLayout.tilesAdjecent(unit.getCurrentLocation(), room.getLayout().getTile(this.getX(), this.getY()))) { + this.kick(room, unit, 55); + } + } + + private void kick(Room room, RoomUnit kicker, int momentum) { + boolean wasMoving = this.currentThread != null && !this.currentThread.dead && !this.currentThread.isDribble(); + if (this.currentThread != null) { this.currentThread.dead = true; } - boolean hasPath = !roomUnit.getPath().isEmpty(); - this.currentThread = new RebugKickBallAction(this, room, roomUnit, hasPath); + Direction8 direction = Direction8.getDirection(kicker.getBodyRotation().getValue()); + boolean zigzag = wasMoving && momentum > 0; + + this.currentThread = new RebugKickBallAction(this, room, direction, momentum, zigzag); Emulator.getThreading().run(this.currentThread, 50); } } 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 16294e82..323abce6 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 @@ -158,6 +158,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; @@ -241,6 +242,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; @@ -1079,7 +1081,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); @@ -1128,7 +1130,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) { @@ -1410,6 +1413,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 466f8abf..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 @@ -448,6 +448,48 @@ 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 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; + } + + // 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..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,9 +91,41 @@ public class RoomTileManager { tallestItem = item; } + 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; + } + } + 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 (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; + 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 (this.room.isAllowUnderpass() && !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/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..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 @@ -1,37 +1,227 @@ 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 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 { + 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; + @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(CAMERA_RENDER_DELAY - (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")); } + // 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)) { + 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)) { + theImage = ImageIO.read(in); + } catch (IOException e) { + handleImageProcessingError(habbo); + 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"; + 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); + 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.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) { + 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; + } + + /** + * 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 c9c1bc12..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 @@ -1,37 +1,167 @@ 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 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 { + 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; + @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(CAMERA_RENDER_DELAY - (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(); + + // 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)) { + 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) { + 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; + } + + File imageFile = new File(Emulator.getConfig().getValue("imager.location.output.thumbnail") + room.getId() + ".png"); + try { + ImageIO.write(theImage, "png", imageFile); + } catch (IOException e) { + LOGGER.error("Failed to write thumbnail for room {}", room.getId(), e); + 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; + } + + /** + * 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(); + } + } + } + } +} 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; } 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 00154dde..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; @@ -161,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 diff --git a/Emulator/src/main/java/com/eu/habbo/threading/runnables/RebugKickBallAction.java b/Emulator/src/main/java/com/eu/habbo/threading/runnables/RebugKickBallAction.java index 55694364..7bc761e9 100644 --- a/Emulator/src/main/java/com/eu/habbo/threading/runnables/RebugKickBallAction.java +++ b/Emulator/src/main/java/com/eu/habbo/threading/runnables/RebugKickBallAction.java @@ -4,39 +4,56 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomTile; import com.eu.habbo.habbohotel.rooms.RoomTileState; -import com.eu.habbo.habbohotel.rooms.RoomUnit; +import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.outgoing.rooms.items.FloorItemOnRollerComposer; +import com.eu.habbo.messages.outgoing.rooms.items.ItemStateComposer; import com.eu.habbo.util.pathfinding.Direction8; import gnu.trove.set.hash.THashSet; -/** - * Alternative football physics based on the Rebug plugin. - * Uses momentum decay (ball slows down over time) and simple 180-degree bounce. - */ public class RebugKickBallAction implements Runnable { private final HabboItem ball; private final Room room; private Direction8 direction; private int momentum; + private boolean isDribble; public boolean dead = false; - public RebugKickBallAction(HabboItem ball, Room room, RoomUnit kicker, boolean hasPath) { + private final boolean zigzag; + private Direction8 zigzagA; + private Direction8 zigzagB; + private boolean zigzagSide = false; + + private int tilesSinceBounce = -1; + + public RebugKickBallAction(HabboItem ball, Room room, Direction8 direction, int momentum) { + this(ball, room, direction, momentum, false); + } + + public RebugKickBallAction(HabboItem ball, Room room, Direction8 direction, int momentum, boolean zigzag) { this.ball = ball; this.room = room; - this.direction = Direction8.fromDelta( - ball.getX() - kicker.getX(), - ball.getY() - kicker.getY() - ); - this.momentum = hasPath ? 55 : 0; + this.direction = direction; + this.momentum = momentum; + this.isDribble = (momentum == 0); + this.zigzag = zigzag && !this.isDribble; + + if (this.zigzag) { + this.zigzagA = direction.rotateDirection45Degrees(false); + this.zigzagB = direction.rotateDirection45Degrees(true); + } + } + + public boolean isDribble() { + return this.isDribble; } private boolean isTileBlocked(int x, int y) { RoomTile tile = this.room.getLayout().getTile((short) x, (short) y); if (tile == null) return true; - if (tile.hasUnits()) return true; - return tile.getState() != RoomTileState.OPEN; + if (tile.getState() != RoomTileState.OPEN) return true; + return x == this.room.getLayout().getDoorX() && y == this.room.getLayout().getDoorY(); } @Override @@ -44,36 +61,109 @@ public class RebugKickBallAction implements Runnable { if (this.dead || !this.room.isLoaded()) return; try { - int nextX = this.ball.getX() + this.direction.getDiffX(); - int nextY = this.ball.getY() + this.direction.getDiffY(); + int nextX; + int nextY; + Direction8 moveDir; - if (isTileBlocked(nextX, nextY)) { - this.direction = this.direction.rotateDirection180Degrees(); + if (this.zigzag) { + Direction8 preferred = this.zigzagSide ? this.zigzagB : this.zigzagA; + Direction8 fallback = this.zigzagSide ? this.zigzagA : this.zigzagB; + + nextX = this.ball.getX() + preferred.getDiffX(); + nextY = this.ball.getY() + preferred.getDiffY(); + + if (isTileBlocked(nextX, nextY)) { + nextX = this.ball.getX() + fallback.getDiffX(); + nextY = this.ball.getY() + fallback.getDiffY(); + + if (isTileBlocked(nextX, nextY)) { + nextX = this.ball.getX() + this.direction.getDiffX(); + nextY = this.ball.getY() + this.direction.getDiffY(); + + if (isTileBlocked(nextX, nextY)) { + this.stopBall(); + return; + } + moveDir = this.direction; + } else { + moveDir = fallback; + } + } else { + moveDir = preferred; + this.zigzagSide = !this.zigzagSide; + } + } else { nextX = this.ball.getX() + this.direction.getDiffX(); nextY = this.ball.getY() + this.direction.getDiffY(); + + if (isTileBlocked(nextX, nextY)) { + int dx = this.direction.getDiffX(); + int dy = this.direction.getDiffY(); + + if (dx != 0 && dy != 0) { + boolean xBlocked = isTileBlocked(this.ball.getX() + dx, this.ball.getY()); + boolean yBlocked = isTileBlocked(this.ball.getX(), this.ball.getY() + dy); + + if (xBlocked && !yBlocked) { + this.direction = Direction8.fromDelta(-dx, dy); + } else if (!xBlocked && yBlocked) { + this.direction = Direction8.fromDelta(dx, -dy); + } else { + this.direction = this.direction.rotateDirection180Degrees(); + } + } else { + this.direction = this.direction.rotateDirection180Degrees(); + } + + this.tilesSinceBounce = 0; + nextX = this.ball.getX() + this.direction.getDiffX(); + nextY = this.ball.getY() + this.direction.getDiffY(); + } + moveDir = this.direction; } RoomTile nextTile = this.room.getLayout().getTile((short) nextX, (short) nextY); - if (nextTile == null) return; + if (nextTile == null) { + this.stopBall(); + return; + } RoomTile oldTile = this.room.getLayout().getTile(this.ball.getX(), this.ball.getY()); double oldZ = this.ball.getZ(); - this.ball.setRotation(this.direction.getRot()); + this.ball.setRotation(moveDir.getRot()); this.ball.setX(nextTile.x); this.ball.setY(nextTile.y); this.ball.setZ(nextTile.getStackHeight()); this.ball.needsUpdate(true); - // Schedule next movement based on momentum - long delay = getDelayForMomentum(this.momentum); - if (delay > 0) { - Emulator.getThreading().run(this, delay); + if (!this.zigzag && this.tilesSinceBounce >= 0) { + this.tilesSinceBounce++; } - // Update tiles - this.room.updateTile(oldTile); - this.room.updateTile(nextTile); + if (!this.zigzag && this.tilesSinceBounce > 1 && !this.isDribble) { + THashSet habbos = this.room.getHabbosAt(nextTile.x, nextTile.y); + if (!habbos.isEmpty()) { + this.direction = this.direction.rotateDirection180Degrees(); + this.tilesSinceBounce = 0; + } + } + + this.ball.setExtradata(this.isDribble ? "2" : "5"); + this.room.sendComposer(new ItemStateComposer(this.ball).compose()); + + this.momentum -= 11; + + if (!this.isDribble) { + long delay = getDelayForMomentum(this.momentum); + if (delay > 0) { + Emulator.getThreading().run(this, delay); + } else { + this.stopBall(); + } + } else { + this.dead = true; + } THashSet oldItems = this.room.getItemsAt(oldTile); if (oldItems != null && !oldItems.isEmpty()) { @@ -81,18 +171,23 @@ public class RebugKickBallAction implements Runnable { } this.room.getItemsAt(nextTile).add(this.ball); - // Send rolling animation + this.room.updateTile(oldTile); + this.room.updateTile(nextTile); + this.room.sendComposer(new FloorItemOnRollerComposer( this.ball, null, oldTile, oldZ, nextTile, this.ball.getZ(), 0.0D, this.room ).compose()); - - // Decay momentum - this.momentum -= 11; } catch (Exception e) { - this.dead = true; + this.stopBall(); } } + private void stopBall() { + this.dead = true; + this.ball.setExtradata("0"); + this.room.sendComposer(new ItemStateComposer(this.ball).compose()); + } + private long getDelayForMomentum(int momentum) { switch (momentum) { case 55: return 100L; diff --git a/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar b/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar index 34273136..7c845e2b 100644 Binary files a/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar and b/Latest_Compiled_Version/Habbo-4.0.5-jar-with-dependencies.jar differ