Merge upstream/main into main

This commit is contained in:
Lorenzune
2026-03-18 17:32:16 +01:00
38 changed files with 825 additions and 908 deletions
@@ -0,0 +1 @@
ALTER TABLE `rooms` ADD COLUMN `allow_underpass` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `move_diagonally`;
+71
View File
@@ -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.');
@@ -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;
}
@@ -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());
@@ -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;
}
}
@@ -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());
@@ -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;
}
}
@@ -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);
}
}
@@ -158,6 +158,7 @@ public class Room implements Comparable<Room>, 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<Room>, 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<Room>, 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<Room>, 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<Room>, 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;
}
@@ -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.
*/
@@ -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;
@@ -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<HabboItem> 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<HabboItem> 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;
}
@@ -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);
@@ -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() : ""));
}
}
}
@@ -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"));
}
}
}
}
@@ -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<ImageReader> 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"));
}
}
@@ -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();
}
}
}
}
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<ImageReader> 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();
}
}
}
}
}
@@ -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());
@@ -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;
}
@@ -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<SocketChannel>() {
@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();
}
}
}
@@ -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<Object> 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));
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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);
}
@@ -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<Short, Class<? extends CameraIncomingMessage>> 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<? extends CameraIncomingMessage> 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);
}
}
}
}
@@ -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;
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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);
}
}
}
}
}
@@ -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());
}
}
}
@@ -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());
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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");
@@ -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);
}
}
@@ -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<Habbo> 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<HabboItem> 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;