diff --git a/Database Updates/010_furnidata_edit_log.sql b/Database Updates/010_furnidata_edit_log.sql
index d6a6a991..0aae35e0 100644
--- a/Database Updates/010_furnidata_edit_log.sql
+++ b/Database Updates/010_furnidata_edit_log.sql
@@ -19,4 +19,50 @@ CREATE TABLE IF NOT EXISTS `furnidata_edit_log` (
INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
('items.furnidata.edit.backup.keep','10'),
-('items.furnidata.edit.ratelimit.ms','2000');
+('items.furnidata.edit.ratelimit.ms','2000'),
+-- Server-authoritative furni names (source of truth = furnidata JSON)
+('items.furnidata.names.enabled','true'),
+('items.furnidata.path',''),
+('items.furnidata.max.bytes','67108864'),
+-- Live-reload watcher
+('items.furnidata.watch.enabled','true'),
+('items.furnidata.watch.debounce.ms','750'),
+('items.furnidata.watch.min.interval.ms','5000'),
+('items.furnidata.delta.cap','500'),
+-- Furni editor: import official names/descriptions from Habbo
+('furni.editor.import.url','https://www.habbo.com/gamedata/furnidata_json/1'),
+('furni.editor.import.cache.ms','600000');
+
+START TRANSACTION;
+DROP TEMPORARY TABLE IF EXISTS cleanup_furnidata_settings;
+CREATE TEMPORARY TABLE cleanup_furnidata_settings (
+ `key` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL PRIMARY KEY
+);
+INSERT INTO cleanup_furnidata_settings (`key`) VALUES
+ ('items.furnidata.names.enabled'),
+ ('items.furnidata.path'),
+ ('items.furnidata.max.bytes'),
+ ('items.furnidata.watch.enabled'),
+ ('items.furnidata.watch.debounce.ms'),
+ ('items.furnidata.watch.min.interval.ms'),
+ ('items.furnidata.delta.cap'),
+ ('furni.editor.import.url'),
+ ('furni.editor.import.cache.ms');
+
+-- Preview rows that will be removed.
+SELECT es.`key`, es.`value`
+FROM emulator_settings es
+JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`
+ORDER BY es.`key`;
+
+DELETE es
+FROM emulator_settings es
+JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
+
+-- Preview remaining matching rows inside the transaction.
+SELECT COUNT(*) AS remaining_furnidata_settings
+FROM emulator_settings es
+JOIN cleanup_furnidata_settings cfs ON cfs.`key` = es.`key`;
+
+-- Safe default. Change to COMMIT after reviewing the preview.
+ROLLBACK;
diff --git a/Database Updates/011_navigator_group_filter.sql b/Database Updates/011_navigator_group_filter.sql
new file mode 100644
index 00000000..510244c6
--- /dev/null
+++ b/Database Updates/011_navigator_group_filter.sql
@@ -0,0 +1,9 @@
+-- Navigator search filters - companion to the gameserver fix for the catalog
+-- 'Find groups to join!' button (navigator/search/hotel_view/group:).
+
+INSERT IGNORE INTO `navigator_filter` (`key`, `field`, `compare`, `database_query`) VALUES
+('anything', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
+('roomname', 'getName', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.name LIKE ?'),
+('owner', 'getOwnerName', 'equals_ignore_case', 'SELECT rooms.* FROM rooms WHERE rooms.owner_name LIKE ?'),
+('tag', 'getTags', 'contains', 'SELECT rooms.* FROM rooms WHERE rooms.tags LIKE ?'),
+('group', 'getGuildName', 'contains', 'SELECT rooms.* FROM rooms INNER JOIN guilds ON guilds.room_id = rooms.id WHERE guilds.name LIKE ?');
diff --git a/Database Updates/Own_Database_RunFirst/021_furnidata_config.sql b/Database Updates/Own_Database_RunFirst/021_furnidata_config.sql
deleted file mode 100644
index 9a41bae4..00000000
--- a/Database Updates/Own_Database_RunFirst/021_furnidata_config.sql
+++ /dev/null
@@ -1,27 +0,0 @@
--- 021_furnidata_config.sql
--- Seeds the furnidata feature config keys read at runtime by
--- FurnitureTextProvider / FurnidataReader / FurnidataWatcher and
--- FurniEditorImportTextEvent. Without these rows a fresh install logs
--- "Config key not found" for each (ConfigurationManager logs ERROR even
--- when a default is supplied) and the values are not editable from the DB.
---
--- Notes:
--- * *.enabled keys are read via Boolean.parseBoolean → use true/false (NOT 1/0).
--- * items.furnidata.path is intentionally empty: when blank the source is
--- derived from furni.editor.asset.base.path (seeded by 004_furni_editor.sql)
--- → /furnidata (split-tier) or /FurnitureData.json (single file).
--- * Editor write-path keys (items.furnidata.edit.*) are seeded by 020.
-
-INSERT IGNORE INTO `emulator_settings` (`key`,`value`) VALUES
--- Server-authoritative furni names (source of truth = furnidata JSON)
-('items.furnidata.names.enabled','true'),
-('items.furnidata.path',''),
-('items.furnidata.max.bytes','67108864'),
--- Live-reload watcher
-('items.furnidata.watch.enabled','true'),
-('items.furnidata.watch.debounce.ms','750'),
-('items.furnidata.watch.min.interval.ms','5000'),
-('items.furnidata.delta.cap','500'),
--- Furni editor: import official names/descriptions from Habbo
-('furni.editor.import.url','https://www.habbo.it/gamedata/furnidata_json/1'),
-('furni.editor.import.cache.ms','600000');
diff --git a/Database Updates/Set Rooms wallitems/fix_room_paint_null.sql b/Database Updates/Set Rooms wallitems/fix_room_paint_null.sql
new file mode 100644
index 00000000..6f9fd7d5
--- /dev/null
+++ b/Database Updates/Set Rooms wallitems/fix_room_paint_null.sql
@@ -0,0 +1,15 @@
+-- Fix NULL room paint columns
+--
+-- Some legacy/imported rooms have NULL in paper_wall / paper_floor / paper_landscape.
+-- The server compares these with .equals("0.0") on room entry, which throws a
+-- NullPointerException (RoomManager.openRoom) and prevents the room from loading.
+-- This normalizes existing NULL values and re-enforces the NOT NULL DEFAULT '0.0'
+-- constraint so it cannot happen again.
+
+UPDATE `rooms` SET `paper_wall` = '0.0' WHERE `paper_wall` IS NULL;
+UPDATE `rooms` SET `paper_floor` = '0.0' WHERE `paper_floor` IS NULL;
+UPDATE `rooms` SET `paper_landscape` = '0.0' WHERE `paper_landscape` IS NULL;
+
+ALTER TABLE `rooms` MODIFY COLUMN `paper_wall` VARCHAR(50) NOT NULL DEFAULT '0.0';
+ALTER TABLE `rooms` MODIFY COLUMN `paper_floor` VARCHAR(50) NOT NULL DEFAULT '0.0';
+ALTER TABLE `rooms` MODIFY COLUMN `paper_landscape` VARCHAR(50) NOT NULL DEFAULT '0.0';
diff --git a/Emulator/pom.xml b/Emulator/pom.xml
index 022cec42..ada1fd06 100644
--- a/Emulator/pom.xml
+++ b/Emulator/pom.xml
@@ -66,7 +66,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.2.5
+ 3.5.2
@@ -83,21 +83,21 @@
io.netty
netty-all
- 4.1.115.Final
+ 4.2.15.Final
com.google.code.gson
gson
- 2.11.0
+ 2.14.0
org.mariadb.jdbc
mariadb-java-client
- 3.5.1
+ 3.5.8
runtime
@@ -113,7 +113,7 @@
com.zaxxer
HikariCP
- 6.2.1
+ 7.0.2
compile
@@ -121,7 +121,7 @@
org.apache.commons
commons-lang3
- 3.17.0
+ 3.20.0
compile
@@ -137,7 +137,7 @@
org.jsoup
jsoup
- 1.18.3
+ 1.22.2
compile
@@ -145,14 +145,14 @@
org.slf4j
slf4j-api
- 2.0.16
+ 2.0.18
ch.qos.logback
logback-classic
- 1.5.15
+ 1.5.34
compile
@@ -160,14 +160,7 @@
org.fusesource.jansi
jansi
- 2.4.1
-
-
-
-
- joda-time
- joda-time
- 2.13.0
+ 2.4.3
org.junit.jupiter
junit-jupiter
- 5.10.2
+ 6.1.0
test
diff --git a/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java b/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java
index acd50764..813f8f15 100644
--- a/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java
+++ b/Emulator/src/main/java/com/eu/habbo/database/DatabasePool.java
@@ -79,6 +79,14 @@ class DatabasePool {
databaseConfiguration.addDataSourceProperty("useLocalSessionState", "true");
databaseConfiguration.addDataSourceProperty("cacheResultSetMetadata", "true");
databaseConfiguration.addDataSourceProperty("elideSetAutoCommits", "true");
+
+ // Fail fast instead of pinning a pooled connection (and its worker
+ // thread) indefinitely on a stalled/slow MariaDB. HikariCP's
+ // connectionTimeout only bounds the pool *borrow*; these bound the
+ // actual socket/connect round-trip. Overridable via db.params.
+ databaseConfiguration.addDataSourceProperty("socketTimeout", "30000");
+ databaseConfiguration.addDataSourceProperty("connectTimeout", "10000");
+ databaseConfiguration.addDataSourceProperty("tcpKeepAlive", "true");
databaseConfiguration.addDataSourceProperty("maintainTimeStats", "false");
databaseConfiguration.setPoolName("HabboHikariPool");
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java
index 7e5ecdb8..fcc5ba99 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/achievements/AchievementManager.java
@@ -100,9 +100,9 @@ public class AchievementManager {
if (oldLevel != null && (oldLevel.level == achievement.levels.size() && currentProgress >= oldLevel.progress)) //Maximum achievement gotten.
return;
- habbo.getHabboStats().setProgress(achievement, currentProgress + amount);
+ int newProgress = habbo.getHabboStats().incrementProgress(achievement, amount);
- AchievementLevel newLevel = achievement.getLevelForProgress(currentProgress + amount);
+ AchievementLevel newLevel = achievement.getLevelForProgress(newProgress);
if (AchievementManager.TALENTTRACK_ENABLED) {
for (TalentTrackType type : TalentTrackType.values()) {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java
index ff2b1d72..7fd3ee93 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/BotManager.java
@@ -188,7 +188,11 @@ public class BotManager {
if (pickedUpEvent.isCancelled())
return;
- if (habbo == null || (bot.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_ANYROOMOWNER))) {
+ Room currentRoom = habbo != null ? habbo.getHabboInfo().getCurrentRoom() : null;
+ if (habbo == null
+ || bot.getOwnerId() == habbo.getHabboInfo().getId()
+ || habbo.hasPermission(Permission.ACC_ANYROOMOWNER)
+ || (currentRoom != null && (currentRoom.getOwnerId() == habbo.getHabboInfo().getId() || habbo.hasPermission(Permission.ACC_PLACEFURNI)))) {
if (habbo != null && !habbo.hasPermission(Permission.ACC_UNLIMITED_BOTS) && habbo.getInventory().getBotsComponent().getBots().size() >= BotManager.MAXIMUM_BOT_INVENTORY_SIZE) {
habbo.alert(Emulator.getTexts().getValue("error.bots.max.inventory").replace("%amount%", BotManager.MAXIMUM_BOT_INVENTORY_SIZE + ""));
return;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java
index edbcd4a3..e7952892 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java
@@ -171,8 +171,9 @@ public class MarketPlace {
statement.setInt(paramIndex++, maxPrice);
}
if (!search.isEmpty()) {
- statement.setString(paramIndex++, "%" + search + "%");
- statement.setString(paramIndex++, "%" + search + "%");
+ String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
+ statement.setString(paramIndex++, likeSearch);
+ statement.setString(paramIndex++, likeSearch);
}
try (ResultSet set = statement.executeQuery()) {
@@ -278,8 +279,9 @@ public class MarketPlace {
return;
}
+ int soldTimestamp = Emulator.getIntUnixTimestamp();
try (PreparedStatement updateOffer = connection.prepareStatement("UPDATE marketplace_items SET state = 2, sold_timestamp = ? WHERE id = ? AND state = 1")) {
- updateOffer.setInt(1, Emulator.getIntUnixTimestamp());
+ updateOffer.setInt(1, soldTimestamp);
updateOffer.setInt(2, offerId);
int updated = updateOffer.executeUpdate();
if (updated == 0) {
@@ -306,7 +308,11 @@ public class MarketPlace {
client.sendResponse(new MarketplaceBuyErrorComposer(MarketplaceBuyErrorComposer.REFRESH, 0, offerId, price));
if (habbo != null) {
- habbo.getInventory().getOffer(offerId).setState(MarketPlaceState.SOLD);
+ MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId);
+ if (offer != null) {
+ offer.setState(MarketPlaceState.SOLD);
+ offer.setSoldTimestamp(soldTimestamp);
+ }
}
}
}
@@ -368,6 +374,11 @@ public class MarketPlace {
event.item.setFromGift(false);
MarketPlaceOffer offer = new MarketPlaceOffer(event.item, event.price, client.getHabbo());
+ if (!offer.isPersisted()) {
+ LOGGER.warn("Marketplace offer insert failed for user {} item {}", client.getHabbo().getHabboInfo().getId(), event.item.getId());
+ return false;
+ }
+
client.getHabbo().getInventory().addMarketplaceOffer(offer);
client.getHabbo().getInventory().getItemsComponent().removeHabboItem(event.item);
item.setUserId(-1);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceOffer.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceOffer.java
index 37bd2ab1..ccf0dda1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceOffer.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceOffer.java
@@ -98,6 +98,10 @@ public class MarketPlaceOffer implements Runnable {
return this.offerId;
}
+ public boolean isPersisted() {
+ return this.offerId > 0;
+ }
+
public void setOfferId(int offerId) {
this.offerId = offerId;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java
index c95a5b9a..90b2ea57 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java
@@ -9,6 +9,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager;
+import java.util.List;
+
public class BanCommand extends Command {
public BanCommand() {
super("cmd_ban", Emulator.getTexts().getValue("commands.keys.cmd_ban").split(";"));
@@ -72,7 +74,13 @@ public class BanCommand extends Command {
}
}
- ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1).get(0);
+ List bans = Emulator.getGameEnvironment().getModToolManager().ban(target.getId(), gameClient.getHabbo(), reason.toString(), banTime, ModToolBanType.ACCOUNT, -1);
+ if (bans == null || bans.isEmpty()) {
+ gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.user_offline"), RoomChatMessageBubbles.ALERT);
+ return true;
+ }
+
+ ModToolBan ban = bans.get(0);
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_ban.ban_issued").replace("%user%", target.getUsername()).replace("%time%", ban.expireDate - Emulator.getIntUnixTimestamp() + "").replace("%reason%", ban.reason), RoomChatMessageBubbles.ALERT);
return true;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java
index e37bb1e0..8622cec9 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager;
+import java.util.List;
+
public class IPBanCommand extends Command {
public final static int TEN_YEARS = 315569260;
@@ -50,12 +52,12 @@ public class IPBanCommand extends Command {
return true;
}
- Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
- count++;
+ List> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
+ count += bans != null ? bans.size() : 0;
for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(habbo.getIpLogin())) {
if (h != null) {
- count++;
- Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
+ bans = Emulator.getGameEnvironment().getModToolManager().ban(h.getHabboInfo().getId(), gameClient.getHabbo(), reason.toString(), TEN_YEARS, ModToolBanType.IP, -1);
+ count += bans != null ? bans.size() : 0;
}
}
} else {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java
index 56f5b0fb..29918bb9 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java
@@ -8,6 +8,8 @@ import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager;
+import java.util.List;
+
public class MachineBanCommand extends Command {
public MachineBanCommand() {
super("cmd_machine_ban", Emulator.getTexts().getValue("commands.keys.cmd_machine_ban").split(";"));
@@ -46,7 +48,8 @@ public class MachineBanCommand extends Command {
return true;
}
- count = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1).size();
+ List> bans = Emulator.getGameEnvironment().getModToolManager().ban(habbo.getId(), gameClient.getHabbo(), reason.toString(), IPBanCommand.TEN_YEARS, ModToolBanType.MACHINE, -1);
+ count = bans != null ? bans.size() : 0;
} else {
@@ -58,4 +61,4 @@ public class MachineBanCommand extends Command {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java
index 7ffe5228..87d5fb3d 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java
@@ -149,6 +149,10 @@ public class GameClient {
}
public void dispose() {
+ this.dispose(true);
+ }
+
+ public void dispose(boolean allowSessionResume) {
try {
this.channel.close();
@@ -161,7 +165,7 @@ public class GameClient {
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
// Try to park the habbo in the grace period instead of immediate disconnect
- boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
+ boolean parked = allowSessionResume && SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
if (!parked) {
// No grace period configured — immediate disconnect as before
@@ -177,4 +181,4 @@ public class GameClient {
LOGGER.error("Caught exception", e);
}
}
-}
\ No newline at end of file
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java
index cd0602cb..d07e33b7 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClientManager.java
@@ -43,14 +43,34 @@ public class GameClientManager {
public void disposeClient(GameClient client) {
- this.disposeClient(client.getChannel());
+ if (client == null) {
+ return;
+ }
+
+ this.disposeClient(client.getChannel(), true);
+ }
+
+ public void forceDisposeClient(GameClient client) {
+ if (client == null) {
+ return;
+ }
+
+ this.disposeClient(client.getChannel(), false);
}
private void disposeClient(Channel channel) {
+ this.disposeClient(channel, true);
+ }
+
+ private void disposeClient(Channel channel, boolean allowSessionResume) {
+ if (channel == null) {
+ return;
+ }
+
GameClient client = channel.attr(GameServerAttributes.CLIENT).get();
if (client != null) {
- client.dispose();
+ client.dispose(allowSessionResume);
}
channel.deregister();
channel.attr(GameServerAttributes.CLIENT).set(null);
@@ -190,4 +210,4 @@ public class GameClientManager {
CFKeepAlive();
}, 30000);
}
-}
\ No newline at end of file
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java
index e8099bbc..5052168f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java
@@ -71,6 +71,15 @@ public class SessionResumeManager {
}
}, graceSeconds * 1000);
+ if (future == null) {
+ // The scheduler refused the grace-expiry task (pool saturated or
+ // shutting down). Parking now would leave a GhostSession that nothing
+ // can ever reap (the Habbo + room refs pinned for the JVM lifetime),
+ // so disconnect immediately instead.
+ performFullDisconnect(habbo);
+ return false;
+ }
+
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
applyPausedEffect(habbo);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java
index 85349452..b835a5f5 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java
@@ -421,9 +421,9 @@ public class GuildManager {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
statement.setInt(1, guild.getId());
- statement.setString(2, "%" + query + "%");
+ statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
statement.setInt(3, page * 14);
- statement.setInt(4, (page * 14) + 14);
+ statement.setInt(4, 14);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java
index 0bc0c265..54517e4e 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java
@@ -101,21 +101,27 @@ public class ForumThread implements Runnable, ISerialize {
if (statement.executeUpdate() < 1)
return null;
- ResultSet set = statement.getGeneratedKeys();
- if (set.next()) {
- int threadId = set.getInt(1);
- createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
- cacheThread(createdThread);
-
- ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
- createdThread.addComment(comment);
-
- Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
+ try (ResultSet set = statement.getGeneratedKeys()) {
+ if (set.next()) {
+ int threadId = set.getInt(1);
+ createdThread = new ForumThread(threadId, guild.getId(), opener.getHabboInfo().getId(), subject, 0, timestamp, timestamp, ForumThreadState.OPEN, false, false, 0, null);
+ cacheThread(createdThread);
+ }
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
+ // ForumThreadComment.create() opens its OWN connection; do it after the
+ // thread's connection has been released to avoid holding two pooled
+ // connections simultaneously per forum-thread creation.
+ if (createdThread != null) {
+ ForumThreadComment comment = ForumThreadComment.create(createdThread, opener, message);
+ createdThread.addComment(comment);
+
+ Emulator.getPluginManager().fireEvent(new GuildForumThreadCreated(createdThread));
+ }
+
return createdThread;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThreadComment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThreadComment.java
index c404ddb5..99b6cf61 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThreadComment.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThreadComment.java
@@ -98,12 +98,13 @@ public class ForumThreadComment implements Runnable, ISerialize {
if (statement.executeUpdate() < 1)
return null;
- ResultSet set = statement.getGeneratedKeys();
- if (set.next()) {
- int commentId = set.getInt(1);
- createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
+ try (ResultSet set = statement.getGeneratedKeys()) {
+ if (set.next()) {
+ int commentId = set.getInt(1);
+ createdComment = new ForumThreadComment(commentId, thread.getThreadId(), poster.getHabboInfo().getId(), message, timestamp, ForumThreadState.OPEN, 0);
- Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
+ Emulator.getPluginManager().fireEvent(new GuildForumThreadCommentCreated(createdComment));
+ }
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java
index 0b3aa615..cc844510 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnidataWatcher.java
@@ -115,32 +115,39 @@ public class FurnidataWatcher {
}
}
- private void onChange() {
+ private void onChange() throws InterruptedException {
+ // Re-index under the shared furnidata lock so the watcher and editor
+ // writes never swap the index concurrently. The lock is released before
+ // the throttle/broadcast below so a slow broadcast can't stall editor saves.
+ List delta;
FurnidataLock.LOCK.lock();
try {
Path source = this.provider.getSource();
if (source == null) return;
-
- List delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
- if (delta.isEmpty()) return;
-
- long now = System.currentTimeMillis();
- if (now - this.lastBroadcast < this.minIntervalMs) {
- LOGGER.info("FurnidataWatcher: {} changes indexed but broadcast skipped (min interval) — clients update on next change or reconnect", delta.size());
- return;
- }
- this.lastBroadcast = now;
-
- FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
- ? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
- : new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
-
- broadcast(composer);
- LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
- delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
+ delta = this.provider.reindex(new FurnidataReader(source, this.maxBytes).read());
} finally {
FurnidataLock.LOCK.unlock();
}
+ if (delta.isEmpty()) return;
+
+ // Min-interval throttle: the index has already been swapped, so we must
+ // not drop this delta (the next reindex would diff against the updated
+ // index and never re-emit it). Instead, defer the broadcast until the
+ // interval elapses. Running on a dedicated daemon thread, sleeping is
+ // safe; file events arriving meanwhile coalesce into the next cycle.
+ long sinceLast = System.currentTimeMillis() - this.lastBroadcast;
+ if (sinceLast < this.minIntervalMs) {
+ Thread.sleep(this.minIntervalMs - sinceLast);
+ }
+ this.lastBroadcast = System.currentTimeMillis();
+
+ FurnitureDataReloadComposer composer = (delta.size() > this.deltaCap)
+ ? new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_RELOAD_HINT, List.of())
+ : new FurnitureDataReloadComposer(FurnitureDataReloadComposer.MODE_DELTA, delta);
+
+ broadcast(composer);
+ LOGGER.info("FurnidataWatcher: broadcast {} ({} entries)",
+ delta.size() > this.deltaCap ? "reload-hint" : "delta", delta.size());
}
private void broadcast(FurnitureDataReloadComposer composer) {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java
index f0ca1719..12478e75 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java
@@ -123,7 +123,7 @@ public class Item implements ISerialize {
if (!set.getString("vending_ids").isEmpty()) {
this.vendingItems = new TIntArrayList();
- String[] vendingIds = set.getString("vending_ids").replace(";", ",").split(",");
+ String[] vendingIds = set.getString("vending_ids").replace(";", ",").replace(".", ",").split(",");
for (String s : vendingIds) {
this.vendingItems.add(Integer.parseInt(s.replace(" ", "")));
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionGift.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionGift.java
index f00252f2..8a7ec640 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionGift.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionGift.java
@@ -13,11 +13,13 @@ import org.slf4j.LoggerFactory;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicBoolean;
public class InteractionGift extends HabboItem {
private static final Logger LOGGER = LoggerFactory.getLogger(InteractionGift.class);
public boolean explode = false;
+ private final AtomicBoolean opening = new AtomicBoolean(false);
private int[] itemId;
private int colorId = 0;
private int ribbonId = 0;
@@ -46,6 +48,15 @@ public class InteractionGift extends HabboItem {
}
}
+ /**
+ * Claims the right to open this gift, returning true exactly once. Guards
+ * against two near-simultaneous OpenRecycleBox packets both scheduling an
+ * (async, delayed) OpenGift before the wrapper is removed from the room.
+ */
+ public boolean tryStartOpening() {
+ return this.opening.compareAndSet(false, true);
+ }
+
@Override
public void serializeExtradata(ServerMessage serverMessage) {
//serverMessage.appendInt(this.colorId * 1000 + this.ribbonId);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java
index 6054066e..ffbf6996 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java
@@ -18,6 +18,7 @@ import java.sql.SQLException;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Base abstract class for all wired furniture items (triggers, effects, conditions, extras).
@@ -61,7 +62,11 @@ public abstract class InteractionWired extends InteractionDefault {
*/
private static final long CACHE_EXPIRY_MS = 5 * 60 * 1000;
- private long cooldown;
+ private volatile long cooldown;
+ // Ensures one box is processed by a single thread at a time, so the
+ // cooldown check-and-set in WiredHandler can't double-fire when a packet
+ // thread and the room cycle thread trigger the same box concurrently.
+ private final AtomicBoolean processing = new AtomicBoolean(false);
private final ConcurrentHashMap userExecutionCache = new ConcurrentHashMap<>();
InteractionWired(ResultSet set, Item baseItem) throws SQLException {
@@ -149,6 +154,15 @@ public abstract class InteractionWired extends InteractionDefault {
this.cooldown = newMillis;
}
+ /** Claims exclusive processing of this box; returns false if another thread is already in it. */
+ public boolean tryBeginProcessing() {
+ return this.processing.compareAndSet(false, true);
+ }
+
+ public void endProcessing() {
+ this.processing.set(false);
+ }
+
@Override
public boolean allowWiredResetState() {
return false;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
index 78880aab..075efac1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/pets/InteractionPetBreedingNest.java
@@ -190,6 +190,14 @@ public class InteractionPetBreedingNest extends HabboItem {
}
public void breed(Habbo habbo, String name, int petOneId, int petTwoId) {
+ // Guard before the destructive delete below: a crafted packet can call
+ // this on a nest that isn't full, which would delete the nest furni and
+ // then NPE on petOne/petTwo in the async runnable (losing the furni).
+ if (habbo == null || this.petOne == null || this.petTwo == null
+ || habbo.getHabboInfo().getCurrentRoom() == null) {
+ return;
+ }
+
Emulator.getThreading().run(new QueryDeleteHabboItem(this.getId()));
this.setExtradata("2");
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java
index 6cd6e951..98010557 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java
@@ -65,8 +65,14 @@ public class WiredConditionHabboCount extends InteractionWiredCondition {
} else {
String[] data = wiredData.split(":");
- this.lowerLimit = Integer.parseInt(data[0]);
- this.upperLimit = Integer.parseInt(data[1]);
+ if (data.length >= 2) {
+ try {
+ this.lowerLimit = Integer.parseInt(data[0].trim());
+ this.upperLimit = Integer.parseInt(data[1].trim());
+ } catch (NumberFormatException ignored) {
+ // malformed legacy data — keep the constructed defaults
+ }
+ }
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java
index ddc0477e..36e721fc 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java
@@ -263,22 +263,29 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition
} else {
String[] data = wiredData.split(":");
- int itemCount = Integer.parseInt(data[0]);
+ if (data.length >= 5) {
+ try {
+ int itemCount = Integer.parseInt(data[0]);
- String[] items = data[1].split(";");
+ String[] items = data[1].split(";");
- for (int i = 0; i < itemCount; i++) {
- String[] stuff = items[i].split("-");
+ for (int i = 0; i < itemCount && i < items.length; i++) {
+ String[] stuff = items[i].split("-");
- if (stuff.length >= 6)
- this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5])));
- else if (stuff.length >= 5)
- this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
+ if (stuff.length >= 6)
+ this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5])));
+ else if (stuff.length >= 5)
+ this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4])));
+ }
+
+ this.state = data[2].equals("1");
+ this.direction = data[3].equals("1");
+ this.position = data[4].equals("1");
+ } catch (NumberFormatException ignored) {
+ // malformed legacy data — keep whatever was parsed plus defaults
+ }
}
- this.state = data[2].equals("1");
- this.direction = data[3].equals("1");
- this.position = data[4].equals("1");
this.altitude = false;
this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
this.quantifier = QUANTIFIER_ALL;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java
index a2bb7056..e993d19f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java
@@ -64,8 +64,14 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition {
this.userSource = data.userSource;
} else {
String[] data = wiredData.split(":");
- this.lowerLimit = Integer.parseInt(data[0]);
- this.upperLimit = Integer.parseInt(data[1]);
+ if (data.length >= 2) {
+ try {
+ this.lowerLimit = Integer.parseInt(data[0].trim());
+ this.upperLimit = Integer.parseInt(data[1].trim());
+ } catch (NumberFormatException ignored) {
+ // malformed legacy data — keep the constructed defaults
+ }
+ }
this.userSource = WiredSourceUtil.SOURCE_TRIGGER;
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java
index 52bbe142..8ef38fe1 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectMoveRotateFurni.java
@@ -190,10 +190,15 @@ public class WiredEffectMoveRotateFurni extends InteractionWiredEffect implement
}
for (String s : data[3].split("\r")) {
- HabboItem item = room.getHabboItem(Integer.parseInt(s));
+ if (s.trim().isEmpty()) continue;
+ try {
+ HabboItem item = room.getHabboItem(Integer.parseInt(s.trim()));
- if (item != null)
- this.items.add(item);
+ if (item != null)
+ this.items.add(item);
+ } catch (NumberFormatException ignored) {
+ // skip malformed furni id token
+ }
}
}
this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java
index 92c60e46..6a5e4b59 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java
@@ -151,7 +151,7 @@ public class WiredEffectTeleport extends InteractionWiredEffect {
@Override
public boolean execute(InteractionWiredTrigger object) {
if (!object.isTriggeredByRoomUnit()) {
- invalidTriggers.add(object.getId());
+ invalidTriggers.add(object.getBaseItem().getSpriteId());
}
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java
index 359e463b..7973090d 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectUserFurniBase.java
@@ -252,7 +252,7 @@ public abstract class WiredEffectUserFurniBase extends InteractionWiredEffect {
@Override
public boolean execute(InteractionWiredTrigger object) {
if (!object.isTriggeredByRoomUnit()) {
- invalidTriggers.add(object.getId());
+ invalidTriggers.add(object.getBaseItem().getSpriteId());
}
return true;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java
index d8de5ab4..344524dc 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/extra/WiredVariableReferenceSupport.java
@@ -227,6 +227,18 @@ public final class WiredVariableReferenceSupport {
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
}
+ /**
+ * Drops all cached shared-variable assignments belonging to a room. Both
+ * caches are keyed "roomId:itemId[:userId]", so the trailing colon makes the
+ * prefix match the exact room id. Called on room dispose so the static caches
+ * don't retain entries for the JVM lifetime.
+ */
+ public static void invalidateRoom(int roomId) {
+ String prefix = roomId + ":";
+ USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
+ ROOM_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
+ }
+
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
if (reference == null || !reference.isRoomReference()) {
return null;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java
index f60c1b49..5dacc0cc 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java
@@ -53,7 +53,7 @@ public class Messenger {
public static THashSet searchUsers(String username) {
THashSet users = new THashSet<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
- statement.setString(1, username + "%");
+ statement.setString(1, com.eu.habbo.util.SqlLikeEscaper.escape(username) + "%");
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
users.add(new MessengerBuddy(set, false));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java
new file mode 100644
index 00000000..0507115e
--- /dev/null
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/HousekeepingAuditLog.java
@@ -0,0 +1,97 @@
+package com.eu.habbo.habbohotel.modtool;
+
+import com.eu.habbo.Emulator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/**
+ * Append-only audit trail for privileged housekeeping/admin actions (rank grants,
+ * currency grants, etc.). There was previously no record of which operator did
+ * what to whom. Writes are dispatched off the calling thread; the backing table
+ * is created on first use so no manual migration is required.
+ */
+public final class HousekeepingAuditLog {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(HousekeepingAuditLog.class);
+
+ private static volatile boolean tableReady = false;
+
+ private HousekeepingAuditLog() {
+ }
+
+ /**
+ * Records a privileged action asynchronously.
+ *
+ * @param operatorId the acting staff member's user id
+ * @param operatorName the acting staff member's username
+ * @param action a short action key, e.g. {@code "user.set_rank"}
+ * @param targetUserId the affected user's id (0 if not applicable)
+ * @param detail free-form detail, e.g. {@code "rankId=6"} (capped to 512 chars)
+ * @param ip the operator's IP, for correlation
+ */
+ public static void log(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
+ Emulator.getThreading().run(() -> writeEntry(operatorId, operatorName, action, targetUserId, detail, ip));
+ }
+
+ private static void writeEntry(int operatorId, String operatorName, String action, int targetUserId, String detail, String ip) {
+ ensureTable();
+
+ try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
+ PreparedStatement statement = connection.prepareStatement(
+ "INSERT INTO housekeeping_log (operator_id, operator_name, action, target_user_id, detail, ip, timestamp) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?)")) {
+ statement.setInt(1, operatorId);
+ statement.setString(2, operatorName != null ? operatorName : "");
+ statement.setString(3, action != null ? action : "");
+ statement.setInt(4, targetUserId);
+ statement.setString(5, truncate(detail));
+ statement.setString(6, ip != null ? ip : "");
+ statement.setInt(7, Emulator.getIntUnixTimestamp());
+ statement.execute();
+ } catch (SQLException e) {
+ LOGGER.error("Failed to write housekeeping audit log entry", e);
+ }
+ }
+
+ private static String truncate(String detail) {
+ if (detail == null) return "";
+ return detail.length() > 512 ? detail.substring(0, 512) : detail;
+ }
+
+ private static void ensureTable() {
+ if (tableReady) {
+ return;
+ }
+ synchronized (HousekeepingAuditLog.class) {
+ if (tableReady) {
+ return;
+ }
+ try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute(
+ "CREATE TABLE IF NOT EXISTS housekeeping_log (" +
+ "id INT UNSIGNED NOT NULL AUTO_INCREMENT, " +
+ "operator_id INT NOT NULL, " +
+ "operator_name VARCHAR(64) NOT NULL DEFAULT '', " +
+ "action VARCHAR(64) NOT NULL, " +
+ "target_user_id INT NOT NULL DEFAULT 0, " +
+ "detail VARCHAR(512) NOT NULL DEFAULT '', " +
+ "ip VARCHAR(64) NOT NULL DEFAULT '', " +
+ "timestamp INT NOT NULL, " +
+ "PRIMARY KEY (id), " +
+ "KEY idx_operator (operator_id), " +
+ "KEY idx_target (target_user_id), " +
+ "KEY idx_timestamp (timestamp)" +
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+ tableReady = true;
+ } catch (SQLException e) {
+ LOGGER.error("Failed to create housekeeping_log table", e);
+ }
+ }
+ }
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java
index 03546504..0f59478d 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java
@@ -378,7 +378,9 @@ public class ModToolManager {
statement.setString(6, reason);
statement.setString(7, type.getType());
- try (ResultSet set = statement.executeQuery()) {
+ statement.executeUpdate();
+
+ try (ResultSet set = statement.getGeneratedKeys()) {
if (set.next()) {
try (PreparedStatement selectBanStatement = connection.prepareStatement("SELECT * FROM bans WHERE id = ? LIMIT 1")) {
selectBanStatement.setInt(1, set.getInt(1));
@@ -434,6 +436,10 @@ public class ModToolManager {
Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUserId);
HabboInfo offlineInfo = target != null ? target.getHabboInfo() : HabboManager.getOfflineHabboInfo(targetUserId);
+ if (offlineInfo == null) {
+ return bans;
+ }
+
if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) {
return bans;
}
@@ -454,7 +460,7 @@ public class ModToolManager {
bans.add(ban);
if (target != null) {
- Emulator.getGameServer().getGameClientManager().disposeClient(target.getClient());
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(target.getClient());
}
if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) {
@@ -465,7 +471,7 @@ public class ModToolManager {
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
Emulator.getThreading().run(ban);
bans.add(ban);
- Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
}
}
@@ -477,7 +483,7 @@ public class ModToolManager {
Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban));
Emulator.getThreading().run(ban);
bans.add(ban);
- Emulator.getGameServer().getGameClientManager().disposeClient(h.getClient());
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(h.getClient());
}
}
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 0e4f2d87..be7ada12 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
@@ -213,16 +213,16 @@ public class Room implements Comparable, ISerialize, Runnable {
public java.util.Set getYoutubeWatchers() { return this.youtubeWatchers; }
public void setYoutubeVideo(String videoId, String senderName, java.util.List playlist) {
- this.youtubeCurrentVideo = videoId;
- this.youtubeSenderName = senderName;
- this.youtubePlaylist.clear();
- if (playlist != null) this.youtubePlaylist.addAll(playlist);
+ this.youtubeCurrentVideo = videoId;
+ this.youtubeSenderName = senderName;
+ this.youtubePlaylist.clear();
+ if (playlist != null) this.youtubePlaylist.addAll(playlist);
}
public void clearYoutubeVideo() {
- this.youtubeCurrentVideo = "";
- this.youtubeSenderName = "";
- this.youtubePlaylist.clear();
+ this.youtubeCurrentVideo = "";
+ this.youtubeSenderName = "";
+ this.youtubePlaylist.clear();
}
public final THashMap cache;
@@ -239,9 +239,9 @@ public class Room implements Comparable, ISerialize, Runnable {
this.usersMax = set.getInt("users_max");
this.score = set.getInt("score");
this.category = set.getInt("category");
- this.floorPaint = set.getString("paper_floor");
- this.wallPaint = set.getString("paper_wall");
- this.backgroundPaint = set.getString("paper_landscape");
+ this.floorPaint = set.getString("paper_floor") == null ? "0.0" : set.getString("paper_floor");
+ this.wallPaint = set.getString("paper_wall") == null ? "0.0" : set.getString("paper_wall");
+ this.backgroundPaint = set.getString("paper_landscape") == null ? "0.0" : set.getString("paper_landscape");
this.wallSize = set.getInt("thickness_wall");
this.wallHeight = set.getInt("wall_height");
this.floorSize = set.getInt("thickness_floor");
@@ -464,7 +464,7 @@ public class Room implements Comparable, ISerialize, Runnable {
if (this.loaded || this.loadingInProgress || !this.preLoaded) {
return;
}
-
+
this.loadingInProgress = true;
this.loadingFuture = CompletableFuture.runAsync(() -> {
this.loadDataInternal();
@@ -484,7 +484,7 @@ public class Room implements Comparable, ISerialize, Runnable {
}
future = this.loadingFuture;
}
-
+
if (future != null) {
try {
future.join();
@@ -499,7 +499,7 @@ public class Room implements Comparable, ISerialize, Runnable {
public void loadData() {
CompletableFuture futureToWait = null;
boolean shouldLoad = false;
-
+
synchronized (this.loadLock) {
if (this.loadingInProgress) {
// Get the future to wait on outside the lock
@@ -509,7 +509,7 @@ public class Room implements Comparable, ISerialize, Runnable {
shouldLoad = true;
}
}
-
+
// Wait for existing load outside the lock
if (futureToWait != null) {
try {
@@ -519,7 +519,7 @@ public class Room implements Comparable, ISerialize, Runnable {
}
return;
}
-
+
// Load if needed
if (shouldLoad) {
this.loadDataInternal();
@@ -559,7 +559,7 @@ public class Room implements Comparable, ISerialize, Runnable {
CompletableFuture.runAsync(() -> {
try (Connection promoConnection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement stmt = promoConnection.prepareStatement(
- "SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
+ "SELECT * FROM room_promotions WHERE room_id = ? AND end_timestamp > ? LIMIT 1")) {
stmt.setInt(1, this.id);
stmt.setInt(2, Emulator.getIntUnixTimestamp());
try (ResultSet promoSet = stmt.executeQuery()) {
@@ -654,7 +654,7 @@ public class Room implements Comparable, ISerialize, Runnable {
}
this.roomCycleTask = Emulator.getThreading().getService()
- .scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS);
+ .scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS);
} catch (Exception e) {
LOGGER.error("Caught exception during room load", e);
}
@@ -673,7 +673,7 @@ public class Room implements Comparable, ISerialize, Runnable {
item.setExtradata("1");
this.updateItem(item);
}
-
+
// Set loaded flag with lock
synchronized (this.loadLock) {
this.loaded = true;
@@ -690,7 +690,7 @@ public class Room implements Comparable, ISerialize, Runnable {
this.layout = Emulator.getGameEnvironment().getRoomManager().loadCustomLayout(this);
} else {
this.layout = Emulator.getGameEnvironment().getRoomManager()
- .loadLayout(this.layoutName, this);
+ .loadLayout(this.layoutName, this);
}
}
}
@@ -722,7 +722,7 @@ public class Room implements Comparable, ISerialize, Runnable {
this.unitManager.clearBots();
try (PreparedStatement statement = connection.prepareStatement(
- "SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) {
+ "SELECT users.username AS owner_name, bots.* FROM bots INNER JOIN users ON bots.user_id = users.id WHERE room_id = ?")) {
statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
@@ -733,11 +733,11 @@ public class Room implements Comparable, ISerialize, Runnable {
b.setRoomUnit(new RoomUnit());
b.getRoomUnit().setPathFinderRoom(this);
b.getRoomUnit()
- .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
+ .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
if (b.getRoomUnit().getCurrentLocation() == null) {
b.getRoomUnit().setLocation(this.getLayout().getDoorTile());
b.getRoomUnit()
- .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
+ .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
} else {
b.getRoomUnit().setZ(set.getDouble("z"));
b.getRoomUnit().setPreviousLocationZ(set.getDouble("z"));
@@ -761,7 +761,7 @@ public class Room implements Comparable, ISerialize, Runnable {
this.unitManager.clearPets();
try (PreparedStatement statement = connection.prepareStatement(
- "SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) {
+ "SELECT users.username as pet_owner_name, users_pets.* FROM users_pets INNER JOIN users ON users_pets.user_id = users.id WHERE room_id = ?")) {
statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
@@ -771,11 +771,11 @@ public class Room implements Comparable, ISerialize, Runnable {
pet.setRoomUnit(new RoomUnit());
pet.getRoomUnit().setPathFinderRoom(this);
pet.getRoomUnit()
- .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
+ .setLocation(this.layout.getTile((short) set.getInt("x"), (short) set.getInt("y")));
if (pet.getRoomUnit().getCurrentLocation() == null) {
pet.getRoomUnit().setLocation(this.getLayout().getDoorTile());
pet.getRoomUnit()
- .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
+ .setRotation(RoomUserRotation.fromValue(this.getLayout().getDoorDirection()));
} else {
pet.getRoomUnit().setZ(set.getDouble("z"));
pet.getRoomUnit().setRotation(RoomUserRotation.values()[set.getInt("rot")]);
@@ -849,7 +849,7 @@ public class Room implements Comparable, ISerialize, Runnable {
THashSet updatedTiles = new THashSet<>();
Rectangle rectangle = RoomLayout.getRectangle(item.getX(), item.getY(),
- item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation());
+ item.getBaseItem().getWidth(), item.getBaseItem().getLength(), item.getRotation());
for (short x = (short) rectangle.x; x < rectangle.x + rectangle.getWidth(); x++) {
for (short y = (short) rectangle.y; y < rectangle.y + rectangle.getHeight(); y++) {
@@ -878,7 +878,7 @@ public class Room implements Comparable, ISerialize, Runnable {
}
Habbo habbo = (picker != null && picker.getHabboInfo().getId() == item.getId() ? picker
- : Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId()));
+ : Emulator.getGameServer().getGameClientManager().getHabbo(item.getUserId()));
if (!trackedBuildersClubItem && habbo != null) {
habbo.getInventory().getItemsComponent().addItem(item);
habbo.getClient().sendResponse(new AddHabboItemComposer(item));
@@ -1116,7 +1116,7 @@ public class Room implements Comparable, ISerialize, Runnable {
message.appendInt(this.category);
String[] tags = Arrays.stream(this.tags.split(";")).filter(t -> !t.isEmpty())
- .toArray(String[]::new);
+ .toArray(String[]::new);
message.appendInt(tags.length);
for (String s : tags) {
message.appendString(s);
@@ -1183,8 +1183,8 @@ public class Room implements Comparable, ISerialize, Runnable {
public void save() {
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 = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
+ .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 = ?, allow_underpass = ?, youtube_enabled = ?, builders_club_trial_locked = ?, builders_club_original_state = ? WHERE id = ?")) {
statement.setString(1, this.name);
statement.setString(2, this.description);
statement.setString(3, this.password);
@@ -1252,8 +1252,8 @@ public class Room implements Comparable, ISerialize, Runnable {
*/
public void updateDatabaseUserCount() {
try (Connection connection = Emulator.getDatabase().getDataSource()
- .getConnection(); PreparedStatement statement = connection.prepareStatement(
- "UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) {
+ .getConnection(); PreparedStatement statement = connection.prepareStatement(
+ "UPDATE rooms SET users = ? WHERE id = ? LIMIT 1")) {
statement.setInt(1, this.getUserCount());
statement.setInt(2, this.id);
statement.executeUpdate();
@@ -1493,6 +1493,10 @@ public class Room implements Comparable, ISerialize, Runnable {
return this.getGuildId() != 0;
}
+ public boolean belongsToGuild() {
+ return this.guild > 0;
+ }
+
public void setGuild(int guild) {
this.guild = guild;
}
@@ -1600,7 +1604,7 @@ public class Room implements Comparable, ISerialize, Runnable {
if (extraData.length == 4) {
if (extraData[0].equalsIgnoreCase("1")) {
return Color.getHSBColor(Integer.parseInt(extraData[1]),
- Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3]));
+ Integer.parseInt(extraData[2]), Integer.parseInt(extraData[3]));
}
}
}
@@ -1707,7 +1711,7 @@ public class Room implements Comparable, ISerialize, Runnable {
public String[] filterAnything() {
return new String[]{this.getOwnerName(), this.getGuildName(), this.getDescription(),
- this.getPromotionDesc()};
+ this.getPromotionDesc()};
}
public long getCycleTimestamp() {
@@ -1914,7 +1918,7 @@ public class Room implements Comparable, ISerialize, Runnable {
// If the broadcast sender leaves, stop the broadcast for everyone
if (!this.youtubeCurrentVideo.isEmpty()
- && habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) {
+ && habbo.getHabboInfo().getUsername().equals(this.youtubeSenderName)) {
this.clearYoutubeVideo();
this.sendComposer(new com.eu.habbo.messages.outgoing.rooms.youtube.YouTubeRoomBroadcastComposer("", "", java.util.Collections.emptyList()).compose());
}
@@ -2059,7 +2063,7 @@ public class Room implements Comparable, ISerialize, Runnable {
}
public void talk(final Habbo habbo, final RoomChatMessage roomChatMessage, RoomChatType chatType,
- boolean ignoreWired) {
+ boolean ignoreWired) {
this.chatManager.talk(habbo, roomChatMessage, chatType, ignoreWired);
}
@@ -2204,7 +2208,7 @@ public class Room implements Comparable, ISerialize, Runnable {
private void loadRights(Connection connection) {
this.rights.clear();
try (PreparedStatement statement = connection.prepareStatement(
- "SELECT user_id FROM room_rights WHERE room_id = ?")) {
+ "SELECT user_id FROM room_rights WHERE room_id = ?")) {
statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
@@ -2220,7 +2224,7 @@ public class Room implements Comparable, ISerialize, Runnable {
this.bannedHabbos.clear();
try (PreparedStatement statement = connection.prepareStatement(
- "SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) {
+ "SELECT users.username, users.id, room_bans.* FROM room_bans INNER JOIN users ON room_bans.user_id = users.id WHERE ends > ? AND room_bans.room_id = ?")) {
statement.setInt(1, Emulator.getIntUnixTimestamp());
statement.setInt(2, this.id);
try (ResultSet set = statement.executeQuery()) {
@@ -2338,24 +2342,24 @@ public class Room implements Comparable, ISerialize, Runnable {
this.wiredSettingsLoaded = true;
Emulator.getThreading().run(() -> {
- try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
- PreparedStatement statement = connection.prepareStatement(
- "INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
- statement.setInt(1, finalId);
- statement.setInt(2, finalInspectMask);
- statement.setInt(3, finalModifyMask);
- statement.executeUpdate();
- } catch (SQLException e) {
- synchronized (this.wiredSettingsLock) {
- if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
- this.wiredInspectMask = previousInspectMask;
- this.wiredModifyMask = previousModifyMask;
- }
+ try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
+ PreparedStatement statement = connection.prepareStatement(
+ "INSERT INTO room_wired_settings (room_id, inspect_mask, modify_mask) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE inspect_mask = VALUES(inspect_mask), modify_mask = VALUES(modify_mask)")) {
+ statement.setInt(1, finalId);
+ statement.setInt(2, finalInspectMask);
+ statement.setInt(3, finalModifyMask);
+ statement.executeUpdate();
+ } catch (SQLException e) {
+ synchronized (this.wiredSettingsLock) {
+ if (this.wiredInspectMask == finalInspectMask && this.wiredModifyMask == finalModifyMask) {
+ this.wiredInspectMask = previousInspectMask;
+ this.wiredModifyMask = previousModifyMask;
}
- LOGGER.error("Caught SQL exception while saving wired room settings", e);
}
+ LOGGER.error("Caught SQL exception while saving wired room settings", e);
+ }
});
-
+
this.pushWiredSettingsToCurrentHabbos();
return true;
}
@@ -2430,7 +2434,7 @@ public class Room implements Comparable, ISerialize, Runnable {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
- "SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) {
+ "SELECT inspect_mask, modify_mask FROM room_wired_settings WHERE room_id = ? LIMIT 1")) {
statement.setInt(1, this.id);
try (ResultSet set = statement.executeQuery()) {
@@ -2530,15 +2534,15 @@ public class Room implements Comparable, ISerialize, Runnable {
}
if (habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT) || !habbo.getRoomUnit()
- .canForcePosture()) {
+ .canForcePosture()) {
return;
}
this.dance(habbo, DanceType.NONE);
habbo.getRoomUnit().cmdSit = true;
habbo.getRoomUnit().setBodyRotation(
- RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
- - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
+ RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
+ - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
habbo.getRoomUnit().setStatus(RoomUnitStatus.SIT, 0.5 + "");
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
WiredManager.triggerUserPerformsAction(this, habbo.getRoomUnit(), WiredUserActionType.SIT, -1);
@@ -2552,11 +2556,11 @@ public class Room implements Comparable, ISerialize, Runnable {
HabboItem item = this.getTopItemAt(habbo.getRoomUnit().getX(), habbo.getRoomUnit().getY());
if (item == null || !item.getBaseItem().allowSit() || !item.getBaseItem().allowLay()) {
boolean wasSittingOrLaying = habbo.getRoomUnit().hasStatus(RoomUnitStatus.SIT)
- || habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY);
+ || habbo.getRoomUnit().hasStatus(RoomUnitStatus.LAY);
habbo.getRoomUnit().cmdStand = true;
habbo.getRoomUnit().setBodyRotation(
- RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
- - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
+ RoomUserRotation.values()[habbo.getRoomUnit().getBodyRotation().getValue()
+ - habbo.getRoomUnit().getBodyRotation().getValue() % 2]);
habbo.getRoomUnit().removeStatus(RoomUnitStatus.SIT);
habbo.getRoomUnit().removeStatus(RoomUnitStatus.LAY);
this.sendComposer(new RoomUserStatusComposer(habbo.getRoomUnit()).compose());
@@ -2584,38 +2588,38 @@ public class Room implements Comparable, ISerialize, Runnable {
}
public void updateItem(HabboItem item) {
- if (this.isLoaded()) {
- if (item != null && item.getRoomId() == this.id) {
- if (item.getBaseItem() != null) {
- if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
- this.sendComposer(new FloorItemUpdateComposer(item).compose());
- this.updateTiles(this.getLayout()
- .getTilesAt(this.layout.getTile(item.getX(), item.getY()),
- item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
- item.getRotation()));
+ if (this.isLoaded()) {
+ if (item != null && item.getRoomId() == this.id) {
+ if (item.getBaseItem() != null) {
+ if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
+ this.sendComposer(new FloorItemUpdateComposer(item).compose());
+ this.updateTiles(this.getLayout()
+ .getTilesAt(this.layout.getTile(item.getX(), item.getY()),
+ item.getBaseItem().getWidth(), item.getBaseItem().getLength(),
+ item.getRotation()));
- if (RoomAreaHideSupport.isControllerItem(item)) {
- RoomAreaHideSupport.sendState(this, item);
- }
- } else if (item.getBaseItem().getType() == FurnitureType.WALL) {
- this.sendComposer(new WallItemUpdateComposer(item).compose());
+ if (RoomAreaHideSupport.isControllerItem(item)) {
+ RoomAreaHideSupport.sendState(this, item);
}
+ } else if (item.getBaseItem().getType() == FurnitureType.WALL) {
+ this.sendComposer(new WallItemUpdateComposer(item).compose());
}
}
+ }
}
}
public void updateItemState(HabboItem item) {
- if (item != null && RoomAreaHideSupport.isControllerItem(item)) {
- this.updateItem(item);
- return;
- }
+ if (item != null && RoomAreaHideSupport.isControllerItem(item)) {
+ this.updateItem(item);
+ return;
+ }
- if (!item.isLimited()) {
- this.sendComposer(new ItemStateComposer(item).compose());
- } else {
- this.sendComposer(new FloorItemUpdateComposer(item).compose());
- }
+ if (!item.isLimited()) {
+ this.sendComposer(new ItemStateComposer(item).compose());
+ } else {
+ this.sendComposer(new FloorItemUpdateComposer(item).compose());
+ }
if (item.getBaseItem().getType() == FurnitureType.FLOOR) {
if (this.layout == null) {
@@ -2623,8 +2627,8 @@ public class Room implements Comparable, ISerialize, Runnable {
}
this.updateTiles(this.getLayout()
- .getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
- item.getBaseItem().getLength(), item.getRotation()));
+ .getTilesAt(this.layout.getTile(item.getX(), item.getY()), item.getBaseItem().getWidth(),
+ item.getBaseItem().getLength(), item.getRotation()));
if (item instanceof InteractionMultiHeight) {
((InteractionMultiHeight) item).updateUnitsOnItem(this);
@@ -2632,12 +2636,12 @@ public class Room implements Comparable, ISerialize, Runnable {
}
if (item.getBaseItem().getType() == FurnitureType.FLOOR
- && (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) {
+ && (RoomConfInvisSupport.isControllerItem(item) || RoomConfInvisSupport.isTarget(item))) {
RoomConfInvisSupport.sendState(this);
}
if (item.getBaseItem().getType() == FurnitureType.FLOOR
- && RoomHanditemBlockSupport.isControllerItem(item)) {
+ && RoomHanditemBlockSupport.isControllerItem(item)) {
RoomHanditemBlockSupport.sendState(this);
}
}
@@ -2671,18 +2675,18 @@ public class Room implements Comparable, ISerialize, Runnable {
public void refreshGuild(Guild guild) {
if (guild.getRoomId() == this.id) {
THashSet members = Emulator.getGameEnvironment().getGuildManager()
- .getGuildMembers(guild.getId());
+ .getGuildMembers(guild.getId());
for (Habbo habbo : this.getHabbos()) {
Optional member = members.stream()
- .filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny();
+ .filter(m -> m.getUserId() == habbo.getHabboInfo().getId()).findAny();
if (!member.isPresent()) {
continue;
}
habbo.getClient()
- .sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get()));
+ .sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, member.get()));
}
}
@@ -2717,7 +2721,7 @@ public class Room implements Comparable, ISerialize, Runnable {
if (habbo.getHabboInfo().getCurrentRoom() == this) {
if (habbo.getHabboInfo().getId() != this.ownerId) {
if (!(habbo.hasPermission(Permission.ACC_ANYROOMOWNER) || habbo.hasPermission(
- Permission.ACC_MOVEROTATE))) {
+ Permission.ACC_MOVEROTATE))) {
this.refreshRightsForHabbo(habbo);
}
}
@@ -2803,18 +2807,18 @@ public class Room implements Comparable, ISerialize, Runnable {
}
} else {
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
- this.roomSpecialTypes.getTriggers()).compose());
+ this.roomSpecialTypes.getTriggers()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
- this.roomSpecialTypes.getEffects()).compose());
+ this.roomSpecialTypes.getEffects()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
- this.roomSpecialTypes.getConditions()).compose());
+ this.roomSpecialTypes.getConditions()).compose());
this.sendComposer(new RoomFloorItemsComposer(this.itemManager.getFurniOwnerNames(),
- this.roomSpecialTypes.getExtras()).compose());
+ this.roomSpecialTypes.getExtras()).compose());
}
}
public FurnitureMovementError canPlaceFurnitureAt(HabboItem item, Habbo habbo, RoomTile tile,
- int rotation) {
+ int rotation) {
return this.itemManager.canPlaceFurnitureAt(item, habbo, tile, rotation);
}
@@ -2823,17 +2827,17 @@ public class Room implements Comparable, ISerialize, Runnable {
}
public FurnitureMovementError furnitureFitsAt(RoomTile tile, HabboItem item, int rotation,
- boolean checkForUnits) {
+ boolean checkForUnits) {
return this.itemManager.furnitureFitsAt(tile, item, rotation, checkForUnits);
}
public FurnitureMovementError furnitureFitsAtWithPhysics(RoomTile tile, HabboItem item, int rotation,
- boolean checkForUnits, WiredMovementPhysics physics) {
+ boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.furnitureFitsAtWithPhysics(tile, item, rotation, checkForUnits, physics);
}
public FurnitureMovementError placeFloorFurniAt(HabboItem item, RoomTile tile, int rotation,
- Habbo owner) {
+ Habbo owner) {
return this.itemManager.placeFloorFurniAt(item, tile, rotation, owner);
}
@@ -2842,17 +2846,17 @@ public class Room implements Comparable, ISerialize, Runnable {
}
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
- Habbo actor) {
+ Habbo actor) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor);
}
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
- Habbo actor, boolean sendUpdates) {
+ Habbo actor, boolean sendUpdates) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates);
}
public FurnitureMovementError moveFurniTo(HabboItem item, RoomTile tile, int rotation,
- Habbo actor, boolean sendUpdates, boolean checkForUnits) {
+ Habbo actor, boolean sendUpdates, boolean checkForUnits) {
return this.itemManager.moveFurniTo(item, tile, rotation, actor, sendUpdates, checkForUnits);
}
@@ -2869,12 +2873,12 @@ public class Room implements Comparable, ISerialize, Runnable {
}
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation,
- Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
+ Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, actor, sendUpdates, checkForUnits, physics);
}
public FurnitureMovementError moveFurniToWithPhysics(HabboItem item, RoomTile tile, int rotation, double z,
- Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
+ Habbo actor, boolean sendUpdates, boolean checkForUnits, WiredMovementPhysics physics) {
return this.itemManager.moveFurniToWithPhysics(item, tile, rotation, z, actor, sendUpdates, checkForUnits, physics);
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
index f79ca2b9..da3ccc3a 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomCycleManager.java
@@ -300,15 +300,20 @@ public class RoomCycleManager {
return;
}
- TIntObjectIterator botIterator = currentBots.iterator();
- for (int i = currentBots.size(); i-- > 0; ) {
+ // Snapshot under the map monitor (currentBots is a synchronizedMap whose
+ // iterator isn't concurrency-safe), then cycle OFF-lock. Holding the
+ // monitor across the whole tick would block bot place/pickup and room
+ // dispose for the tick duration AND invert the lock order vs
+ // roomUnitLock -> currentBots taken by RoomUnitManager.addBot/clear.
+ final ArrayList bots;
+ synchronized (currentBots) {
+ bots = new ArrayList<>(currentBots.valueCollection());
+ }
+
+ for (Bot bot : bots) {
try {
- final Bot bot;
- try {
- botIterator.advance();
- bot = botIterator.value();
- } catch (Exception e) {
- break;
+ if (bot == null || bot.getRoomUnit() == null) {
+ continue;
}
if (!this.room.isAllowBotsWalk() && bot.getRoomUnit().isWalking()) {
@@ -322,10 +327,8 @@ public class RoomCycleManager {
if (this.cycleRoomUnit(bot.getRoomUnit(), RoomUnitType.BOT)) {
updatedUnit.add(bot.getRoomUnit());
}
-
- } catch (NoSuchElementException e) {
+ } catch (Exception e) {
LOGGER.error("Caught exception", e);
- break;
}
}
}
@@ -339,31 +342,37 @@ public class RoomCycleManager {
return;
}
- TIntObjectIterator petIterator = currentPets.iterator();
- for (int i = currentPets.size(); i-- > 0; ) {
+ // Snapshot under the monitor, then cycle off-lock (see processBots): avoids
+ // holding currentPets for the whole tick and the roomUnitLock inversion.
+ final ArrayList pets;
+ synchronized (currentPets) {
+ pets = new ArrayList<>(currentPets.valueCollection());
+ }
+
+ for (Pet pet : pets) {
try {
- petIterator.advance();
- } catch (NoSuchElementException e) {
+ if (pet == null || pet.getRoomUnit() == null) {
+ continue;
+ }
+
+ if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
+ updatedUnit.add(pet.getRoomUnit());
+ }
+
+ pet.cycle();
+
+ if (pet.packetUpdate) {
+ updatedUnit.add(pet.getRoomUnit());
+ pet.packetUpdate = false;
+ }
+
+ if (pet.getRoomUnit().isWalking() && pet.getRoomUnit().getPath().size() == 1
+ && pet.getRoomUnit().hasStatus(RoomUnitStatus.GESTURE)) {
+ pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
+ updatedUnit.add(pet.getRoomUnit());
+ }
+ } catch (Exception e) {
LOGGER.error("Caught exception", e);
- break;
- }
-
- Pet pet = petIterator.value();
- if (this.cycleRoomUnit(pet.getRoomUnit(), RoomUnitType.PET)) {
- updatedUnit.add(pet.getRoomUnit());
- }
-
- pet.cycle();
-
- if (pet.packetUpdate) {
- updatedUnit.add(pet.getRoomUnit());
- pet.packetUpdate = false;
- }
-
- if (pet.getRoomUnit().isWalking() && pet.getRoomUnit().getPath().size() == 1
- && pet.getRoomUnit().hasStatus(RoomUnitStatus.GESTURE)) {
- pet.getRoomUnit().removeStatus(RoomUnitStatus.GESTURE);
- updatedUnit.add(pet.getRoomUnit());
}
}
}
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 d823d8ea..b307d989 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
@@ -167,17 +167,22 @@ public class RoomItemManager {
*/
public THashSet getFloorItems() {
THashSet items = new THashSet<>();
- TIntObjectIterator iterator = this.roomItems.iterator();
+ // roomItems is a TCollections.synchronizedMap; its iterator is not safe
+ // against concurrent put/remove (item place/pickup), so hold the map
+ // monitor for the whole traversal, matching the mutation sites.
+ synchronized (this.roomItems) {
+ TIntObjectIterator iterator = this.roomItems.iterator();
- for (int i = this.roomItems.size(); i-- > 0; ) {
- try {
- iterator.advance();
- } catch (Exception e) {
- break;
- }
+ for (int i = this.roomItems.size(); i-- > 0; ) {
+ try {
+ iterator.advance();
+ } catch (Exception e) {
+ break;
+ }
- if (iterator.value().getBaseItem().getType() == FurnitureType.FLOOR) {
- items.add(iterator.value());
+ if (iterator.value().getBaseItem().getType() == FurnitureType.FLOOR) {
+ items.add(iterator.value());
+ }
}
}
@@ -189,17 +194,19 @@ public class RoomItemManager {
*/
public THashSet getWallItems() {
THashSet items = new THashSet<>();
- TIntObjectIterator iterator = this.roomItems.iterator();
+ synchronized (this.roomItems) {
+ TIntObjectIterator iterator = this.roomItems.iterator();
- for (int i = this.roomItems.size(); i-- > 0; ) {
- try {
- iterator.advance();
- } catch (Exception e) {
- break;
- }
+ for (int i = this.roomItems.size(); i-- > 0; ) {
+ try {
+ iterator.advance();
+ } catch (Exception e) {
+ break;
+ }
- if (iterator.value().getBaseItem().getType() == FurnitureType.WALL) {
- items.add(iterator.value());
+ if (iterator.value().getBaseItem().getType() == FurnitureType.WALL) {
+ items.add(iterator.value());
+ }
}
}
@@ -211,18 +218,20 @@ public class RoomItemManager {
*/
public THashSet getPostItNotes() {
THashSet items = new THashSet<>();
- TIntObjectIterator iterator = this.roomItems.iterator();
+ synchronized (this.roomItems) {
+ TIntObjectIterator iterator = this.roomItems.iterator();
- for (int i = this.roomItems.size(); i-- > 0; ) {
- try {
- iterator.advance();
- } catch (Exception e) {
- break;
- }
+ for (int i = this.roomItems.size(); i-- > 0; ) {
+ try {
+ iterator.advance();
+ } catch (Exception e) {
+ break;
+ }
- if (iterator.value().getBaseItem().getInteractionType().getType()
- == InteractionPostIt.class) {
- items.add(iterator.value());
+ if (iterator.value().getBaseItem().getInteractionType().getType()
+ == InteractionPostIt.class) {
+ items.add(iterator.value());
+ }
}
}
@@ -276,44 +285,49 @@ public class RoomItemManager {
}
}
- TIntObjectIterator iterator = this.roomItems.iterator();
+ // Cache miss: iterate roomItems under its monitor so a concurrent
+ // place/pickup can't rehash the map mid-traversal (which the per-advance
+ // try/catch would otherwise silently swallow into an incomplete result).
+ synchronized (this.roomItems) {
+ TIntObjectIterator iterator = this.roomItems.iterator();
- for (int i = this.roomItems.size(); i-- > 0; ) {
- HabboItem item;
- try {
- iterator.advance();
- item = iterator.value();
- } catch (Exception e) {
- break;
- }
+ for (int i = this.roomItems.size(); i-- > 0; ) {
+ HabboItem item;
+ try {
+ iterator.advance();
+ item = iterator.value();
+ } catch (Exception e) {
+ break;
+ }
- if (item == null) {
- continue;
- }
+ if (item == null) {
+ continue;
+ }
- if (item.getBaseItem().getType() != FurnitureType.FLOOR) {
- continue;
- }
+ if (item.getBaseItem().getType() != FurnitureType.FLOOR) {
+ continue;
+ }
- int width, length;
+ int width, length;
- if (item.getRotation() != 2 && item.getRotation() != 6) {
- width = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
- length = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
- } else {
- width = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
- length = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
- }
+ if (item.getRotation() != 2 && item.getRotation() != 6) {
+ width = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
+ length = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
+ } else {
+ width = item.getBaseItem().getLength() > 0 ? item.getBaseItem().getLength() : 1;
+ length = item.getBaseItem().getWidth() > 0 ? item.getBaseItem().getWidth() : 1;
+ }
- if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
- && tile.y <= item.getY() + length - 1)) {
- continue;
- }
+ if (!(tile.x >= item.getX() && tile.x <= item.getX() + width - 1 && tile.y >= item.getY()
+ && tile.y <= item.getY() + length - 1)) {
+ continue;
+ }
- items.add(item);
+ items.add(item);
- if (returnOnFirst) {
- return items;
+ if (returnOnFirst) {
+ return items;
+ }
}
}
@@ -956,9 +970,11 @@ public class RoomItemManager {
public int getUserUniqueFurniCount(int userId) {
THashSet- items = new THashSet<>();
- for (HabboItem item : this.roomItems.valueCollection()) {
- if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
- items.add(item.getBaseItem());
+ synchronized (this.roomItems) {
+ for (HabboItem item : this.roomItems.valueCollection()) {
+ if (!items.contains(item.getBaseItem()) && item.getUserId() == userId) {
+ items.add(item.getBaseItem());
+ }
}
}
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 29156355..489c6345 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
@@ -130,13 +130,16 @@ public class RoomLayout {
this.roomTiles = new RoomTile[this.mapSizeX][this.mapSizeY];
for (short y = 0; y < this.mapSizeY; y++) {
- if (modelTemp[y].isEmpty() || modelTemp[y].equalsIgnoreCase("\r")) {
- continue;
- }
+ // A row shorter/longer than the model width (or empty) cannot be parsed
+ // per-square. Previously such tiles were left null while tileExists()
+ // still reported them present, causing NPEs in the coordinate accessors.
+ // Fill them with INVALID tiles so every in-bounds coordinate is non-null.
+ boolean validRow = !modelTemp[y].isEmpty() && modelTemp[y].length() == this.mapSizeX;
for (short x = 0; x < this.mapSizeX; x++) {
- if (modelTemp[y].length() != this.mapSizeX) {
- break;
+ if (!validRow) {
+ this.roomTiles[x][y] = new RoomTile(x, y, (short) 0, RoomTileState.INVALID, true);
+ continue;
}
String square = modelTemp[y].substring(x, x + 1).trim().toLowerCase();
@@ -159,7 +162,9 @@ public class RoomLayout {
}
}
- this.doorTile = this.roomTiles[this.doorX][this.doorY];
+ this.doorTile = (this.doorX >= 0 && this.doorX < this.mapSizeX && this.doorY >= 0 && this.doorY < this.mapSizeY)
+ ? this.roomTiles[this.doorX][this.doorY]
+ : null;
if (this.doorTile != null) {
this.doorTile.setAllowStack(false);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java
index dd3a29e6..43cddbd9 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java
@@ -731,10 +731,10 @@ public class RoomManager {
habbo.getClient().sendResponse(new RoomModelComposer(room));
- if (!room.getWallPaint().equals("0.0"))
+ if (room.getWallPaint() != null && !room.getWallPaint().equals("0.0"))
habbo.getClient().sendResponse(new RoomPaintComposer("wallpaper", room.getWallPaint()));
- if (!room.getFloorPaint().equals("0.0"))
+ if (room.getFloorPaint() != null && !room.getFloorPaint().equals("0.0"))
habbo.getClient().sendResponse(new RoomPaintComposer("floor", room.getFloorPaint()));
habbo.getClient().sendResponse(new RoomPaintComposer("landscape", room.getBackgroundPaint()));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java
index c9eb266d..9166305b 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomRightsManager.java
@@ -272,10 +272,16 @@ public class RoomRightsManager {
} else if (this.isOwner(habbo)) {
habbo.getClient().sendResponse(new RoomOwnerComposer());
flatCtrl = RoomRightLevels.MODERATOR;
- } else if (this.hasRights(habbo) && !this.room.hasGuild()) {
- flatCtrl = RoomRightLevels.RIGHTS;
} else if (this.room.hasGuild()) {
- flatCtrl = this.getGuildRightLevel(habbo);
+ // Explicit room rights must still be honoured in guild rooms (the old
+ // `&& !hasGuild()` guard stripped them for non-guild members) — take
+ // whichever of the two is stronger.
+ RoomRightLevels guildLevel = this.getGuildRightLevel(habbo);
+ flatCtrl = (this.hasRights(habbo) && RoomRightLevels.RIGHTS.isEqualOrGreaterThan(guildLevel))
+ ? RoomRightLevels.RIGHTS
+ : guildLevel;
+ } else if (this.hasRights(habbo)) {
+ flatCtrl = RoomRightLevels.RIGHTS;
}
habbo.getClient().sendResponse(new RoomRightsComposer(flatCtrl));
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
index 4d22505b..9e775568 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomSpecialTypes.java
@@ -152,15 +152,23 @@ public class RoomSpecialTypes {
public InteractionNest getNest(int itemId) {
- return this.nests.get(itemId);
+ synchronized (this.nests) {
+ return this.nests.get(itemId);
+ }
}
public void addNest(InteractionNest item) {
- this.nests.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
+ synchronized (this.nests) {
+ this.nests.put(item.getId(), item);
+ }
+ this.specialItemsById.put(item.getId(), item);
}
public void removeNest(InteractionNest item) {
- this.nests.remove(item.getId()); this.specialItemsById.remove(item.getId());
+ synchronized (this.nests) {
+ this.nests.remove(item.getId());
+ }
+ this.specialItemsById.remove(item.getId());
}
public THashSet getNests() {
@@ -174,15 +182,23 @@ public class RoomSpecialTypes {
public InteractionPetDrink getPetDrink(int itemId) {
- return this.petDrinks.get(itemId);
+ synchronized (this.petDrinks) {
+ return this.petDrinks.get(itemId);
+ }
}
public void addPetDrink(InteractionPetDrink item) {
- this.petDrinks.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
+ synchronized (this.petDrinks) {
+ this.petDrinks.put(item.getId(), item);
+ }
+ this.specialItemsById.put(item.getId(), item);
}
public void removePetDrink(InteractionPetDrink item) {
- this.petDrinks.remove(item.getId()); this.specialItemsById.remove(item.getId());
+ synchronized (this.petDrinks) {
+ this.petDrinks.remove(item.getId());
+ }
+ this.specialItemsById.remove(item.getId());
}
public THashSet getPetDrinks() {
@@ -196,15 +212,23 @@ public class RoomSpecialTypes {
public InteractionPetFood getPetFood(int itemId) {
- return this.petFoods.get(itemId);
+ synchronized (this.petFoods) {
+ return this.petFoods.get(itemId);
+ }
}
public void addPetFood(InteractionPetFood item) {
- this.petFoods.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
+ synchronized (this.petFoods) {
+ this.petFoods.put(item.getId(), item);
+ }
+ this.specialItemsById.put(item.getId(), item);
}
public void removePetFood(InteractionPetFood petFood) {
- this.petFoods.remove(petFood.getId()); this.specialItemsById.remove(petFood.getId());
+ synchronized (this.petFoods) {
+ this.petFoods.remove(petFood.getId());
+ }
+ this.specialItemsById.remove(petFood.getId());
}
public THashSet getPetFoods() {
@@ -218,15 +242,23 @@ public class RoomSpecialTypes {
public InteractionPetToy getPetToy(int itemId) {
- return this.petToys.get(itemId);
+ synchronized (this.petToys) {
+ return this.petToys.get(itemId);
+ }
}
public void addPetToy(InteractionPetToy item) {
- this.petToys.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
+ synchronized (this.petToys) {
+ this.petToys.put(item.getId(), item);
+ }
+ this.specialItemsById.put(item.getId(), item);
}
public void removePetToy(InteractionPetToy petToy) {
- this.petToys.remove(petToy.getId()); this.specialItemsById.remove(petToy.getId());
+ synchronized (this.petToys) {
+ this.petToys.remove(petToy.getId());
+ }
+ this.specialItemsById.remove(petToy.getId());
}
public THashSet getPetToys() {
@@ -240,15 +272,23 @@ public class RoomSpecialTypes {
public InteractionPetTree getPetTree(int itemId) {
- return this.petTrees.get(itemId);
+ synchronized (this.petTrees) {
+ return this.petTrees.get(itemId);
+ }
}
public void addPetTree(InteractionPetTree item) {
- this.petTrees.put(item.getId(), item); this.specialItemsById.put(item.getId(), item);
+ synchronized (this.petTrees) {
+ this.petTrees.put(item.getId(), item);
+ }
+ this.specialItemsById.put(item.getId(), item);
}
public void removePetTree(InteractionPetTree petTree) {
- this.petTrees.remove(petTree.getId()); this.specialItemsById.remove(petTree.getId());
+ synchronized (this.petTrees) {
+ this.petTrees.remove(petTree.getId());
+ }
+ this.specialItemsById.remove(petTree.getId());
}
public THashSet getPetTrees() {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java
index 46138a3c..38d2b66f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java
@@ -26,6 +26,7 @@ public class RoomTrade {
private final List users;
private final Room room;
+ private boolean completed = false;
public RoomTrade(Habbo userOne, Habbo userTwo, Room room) {
this.users = new ArrayList<>();
@@ -54,7 +55,7 @@ public class RoomTrade {
this.sendMessageToUsers(new TradeStartComposer(this));
}
- public void offerItem(Habbo habbo, HabboItem item) {
+ public synchronized void offerItem(Habbo habbo, HabboItem item) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (user.getItems().contains(item))
@@ -67,7 +68,7 @@ public class RoomTrade {
this.updateWindow();
}
- public void offerMultipleItems(Habbo habbo, THashSet items) {
+ public synchronized void offerMultipleItems(Habbo habbo, THashSet items) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
for (HabboItem item : items) {
@@ -81,7 +82,7 @@ public class RoomTrade {
this.updateWindow();
}
- public void removeItem(Habbo habbo, HabboItem item) {
+ public synchronized void removeItem(Habbo habbo, HabboItem item) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
if (!user.getItems().contains(item))
@@ -94,7 +95,7 @@ public class RoomTrade {
this.updateWindow();
}
- public void accept(Habbo habbo, boolean value) {
+ public synchronized void accept(Habbo habbo, boolean value) {
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
user.setAccepted(value);
@@ -110,7 +111,13 @@ public class RoomTrade {
}
}
- public void confirm(Habbo habbo) {
+ public synchronized void confirm(Habbo habbo) {
+ // Re-entry guard: both participants confirm on their own EventLoop
+ // threads. Without this (and the method-level lock) two concurrent
+ // confirms could each observe "all confirmed" and run tradeItems()
+ // twice → item/credit duplication.
+ if (this.completed) return;
+
RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo);
user.confirm();
@@ -122,6 +129,8 @@ public class RoomTrade {
accepted = false;
}
if (accepted) {
+ this.completed = true;
+
if (this.tradeItems()) {
this.closeWindow();
this.sendMessageToUsers(new TradeCompleteComposer());
@@ -264,6 +273,10 @@ public class RoomTrade {
protected void clearAccepted() {
for (RoomTradeUser user : this.users) {
user.setAccepted(false);
+ // Any change to the offered items invalidates a prior confirmation;
+ // without this a stale confirmed=true lets a user strip their side
+ // and still complete the trade once the partner re-confirms.
+ user.setConfirmed(false);
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTradeUser.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTradeUser.java
index 6b222e32..9ac826c0 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTradeUser.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTradeUser.java
@@ -51,6 +51,10 @@ public class RoomTradeUser {
this.confirmed = true;
}
+ public void setConfirmed(boolean value) {
+ this.confirmed = value;
+ }
+
public void addItem(HabboItem item) {
this.items.add(item);
}
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 7b9326e0..af4987f9 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
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
@@ -71,7 +72,10 @@ public class RoomUnit {
private RoomUserRotation headRotation = RoomUserRotation.NORTH;
private DanceType danceType;
private RoomUnitType roomUnitType;
- private Deque path = new LinkedList<>();
+ // Concurrent + volatile: the room cycle thread polls/clears this path while a
+ // walk packet thread rebuilds it via findPath/setPath. A plain LinkedList would
+ // corrupt under the concurrent structural modification.
+ private volatile Deque path = new ConcurrentLinkedDeque<>();
private int handItem;
private long handItemTimestamp;
private long lastRollerTime;
@@ -587,7 +591,7 @@ public class RoomUnit {
Deque newPath = this.room.getLayout().getPathfinder()
.findPath(this.currentLocation, this.goalLocation, this.goalLocation, this);
if (newPath != null && !newPath.isEmpty()) {
- this.path = newPath;
+ this.path = new ConcurrentLinkedDeque<>(newPath);
}
}
@@ -765,7 +769,7 @@ public class RoomUnit {
}
public void setPath(Deque path) {
- this.path = path;
+ this.path = (path == null) ? new ConcurrentLinkedDeque<>() : new ConcurrentLinkedDeque<>(path);
}
public RoomRightLevels getRightsLevel() {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java
index b6bd7df5..c3105f54 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/pathfinding/impl/PathfinderImpl.java
@@ -24,8 +24,11 @@ public class PathfinderImpl implements Pathfinder {
private static final int CACHED_TIMEOUT_MS = Emulator.getConfig()
.getInt(CONFIG_EXECUTION_TIME, 25);
+ // Default ON: bound A* to CACHED_TIMEOUT_MS (25ms) so a pathological search
+ // can't run unbounded and stall the thread. On timeout findPath returns an
+ // empty path (the unit simply doesn't move there) — graceful degradation.
private static final boolean CACHED_TIMEOUT_ENABLED = Emulator.getConfig()
- .getBoolean(CONFIG_TIMEOUT_ENABLED, false);
+ .getBoolean(CONFIG_TIMEOUT_ENABLED, true);
private static final long CACHED_TIMEOUT_NANOS = CACHED_TIMEOUT_MS * 1_000_000L;
private final Room room;
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java
index 61f8075e..9983c49c 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/Habbo.java
@@ -146,31 +146,23 @@ public class Habbo implements Runnable {
this.habboInfo.setIpLogin(ip);
}
- if (this.client.getMachineId() == null || this.client.getMachineId().length() == 0) {
- return false;
- }
+ // The Nitro client sends the UniqueID (machine fingerprint) packet right
+ // AFTER the SSO ticket, so client.getMachineId() may still be null here.
+ // Do NOT reject the login for a missing machineId — MachineIDEvent sets it
+ // and enforces the MAC ban as soon as the UniqueID packet arrives. Only
+ // MAC-ban check here when the fingerprint is already available.
+ String machineId = this.client.getMachineId();
+ if (machineId != null && !machineId.isEmpty()) {
+ this.habboInfo.setMachineID(machineId);
- this.habboInfo.setMachineID(this.client.getMachineId());
-
- if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
- return false;
+ if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
+ return false;
+ }
}
if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
return false;
}
-
- this.habboInfo.setMachineID(this.client.getMachineId());
-
- if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
- return false;
- }
-
- if (Emulator.getGameEnvironment().getModToolManager().hasIPBan(this.habboInfo.getIpLogin())) {
- return false;
- }
-
- this.habboInfo.setMachineID(this.client.getMachineId());
this.isOnline(true);
this.messenger.connectionChanged(this, true, false);
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java
index 373642be..06bff0a0 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboInfo.java
@@ -55,6 +55,11 @@ public class HabboInfo implements Runnable {
private RideablePet riding;
private Class extends Game> currentGame;
private TIntIntHashMap currencies;
+ // Serializes credits + currencies read-modify-write and the saveCurrencies
+ // snapshot so the credit-roller thread and purchase/trade handler threads
+ // can't lose updates or rehash the Trove map mid-iteration. Never held
+ // across run()'s DB I/O.
+ private final Object currencyLock = new Object();
private GamePlayer gamePlayer;
private int photoRoomId;
private int photoTimestamp;
@@ -123,11 +128,16 @@ public class HabboInfo implements Runnable {
}
private void saveCurrencies() {
- List entries = new ArrayList<>(this.currencies.size());
- this.currencies.forEachEntry((type, amount) -> {
- entries.add(new int[]{type, amount});
- return true;
- });
+ // Snapshot under the lock so a concurrent adjustOrPutValue/put can't
+ // rehash the Trove map while we iterate; do the DB batch off-lock.
+ List entries;
+ synchronized (this.currencyLock) {
+ entries = new ArrayList<>(this.currencies.size());
+ this.currencies.forEachEntry((type, amount) -> {
+ entries.add(new int[]{type, amount});
+ return true;
+ });
+ }
try {
SqlQueries.batchUpdate(
@@ -238,20 +248,30 @@ public class HabboInfo implements Runnable {
}
public int getCurrencyAmount(int type) {
- return this.currencies.get(type);
+ synchronized (this.currencyLock) {
+ return this.currencies.get(type);
+ }
}
public TIntIntHashMap getCurrencies() {
- return this.currencies;
+ // Return a snapshot under the lock: callers iterate this map, which would
+ // otherwise corrupt during a concurrent adjustOrPutValue rehash.
+ synchronized (this.currencyLock) {
+ return new TIntIntHashMap(this.currencies);
+ }
}
public void addCurrencyAmount(int type, int amount) {
- this.currencies.adjustOrPutValue(type, amount, amount);
+ synchronized (this.currencyLock) {
+ this.currencies.adjustOrPutValue(type, amount, amount);
+ }
this.run();
}
public void setCurrencyAmount(int type, int amount) {
- this.currencies.put(type, amount);
+ synchronized (this.currencyLock) {
+ this.currencies.put(type, amount);
+ }
this.run();
}
@@ -380,20 +400,26 @@ public class HabboInfo implements Runnable {
}
public boolean canBuy(CatalogItem item) {
- return this.credits >= item.getCredits() && this.getCurrencies().get(item.getPointsType()) >= item.getPoints();
+ return this.getCredits() >= item.getCredits() && this.getCurrencyAmount(item.getPointsType()) >= item.getPoints();
}
public int getCredits() {
- return this.credits;
+ synchronized (this.currencyLock) {
+ return this.credits;
+ }
}
public void setCredits(int credits) {
- this.credits = credits;
+ synchronized (this.currencyLock) {
+ this.credits = credits;
+ }
this.run();
}
public void addCredits(int credits) {
- this.credits += credits;
+ synchronized (this.currencyLock) {
+ this.credits += credits;
+ }
this.run();
}
@@ -600,6 +626,13 @@ public class HabboInfo implements Runnable {
public void run() {
this.saveCurrencies();
+ // Read credits under the lock so the persisted value is consistent with
+ // concurrent addCredits/setCredits (matches the currencyLock invariant).
+ final int creditsForSave;
+ synchronized (this.currencyLock) {
+ creditsForSave = this.credits;
+ }
+
try {
SqlQueries.update(
"UPDATE users SET motto = ?, online = ?, look = ?, gender = ?, credits = ?, last_login = ?, last_online = ?, home_room = ?, ip_current = ?, `rank` = ?, machine_id = ?, username = ?, background_id = ?, background_stand_id = ?, background_overlay_id = ?, background_card_id = ?, background_border_id = ? WHERE id = ?",
@@ -607,7 +640,7 @@ public class HabboInfo implements Runnable {
this.online ? "1" : "0",
this.look,
this.gender.name(),
- this.credits,
+ creditsForSave,
Emulator.getIntUnixTimestamp(),
this.lastOnline,
this.homeRoom,
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java
index 48261f66..ef602679 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java
@@ -111,7 +111,7 @@ public class HabboManager {
habbo = this.cloneCheck(userId);
if (habbo != null) {
habbo.alert(Emulator.getTexts().getValue("loggedin.elsewhere"));
- Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
habbo = null;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
index 3c7e820d..cbd84b82 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboStats.java
@@ -448,14 +448,26 @@ public class HabboStats implements Runnable {
return 0;
}
- if (this.achievementProgress.containsKey(achievement))
- return this.achievementProgress.get(achievement);
-
- return -1;
+ synchronized (this.achievementProgress) {
+ Integer progress = this.achievementProgress.get(achievement);
+ return progress != null ? progress : -1;
+ }
}
public void setProgress(Achievement achievement, int progress) {
- this.achievementProgress.put(achievement, progress);
+ synchronized (this.achievementProgress) {
+ this.achievementProgress.put(achievement, progress);
+ }
+ }
+
+ /** Atomic read-add-write so concurrent progress sources don't lose updates. Returns the new total. */
+ public int incrementProgress(Achievement achievement, int amount) {
+ synchronized (this.achievementProgress) {
+ Integer current = this.achievementProgress.get(achievement);
+ int next = (current != null ? current : 0) + amount;
+ this.achievementProgress.put(achievement, next);
+ return next;
+ }
}
public int getRentedTimeEnd() {
diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java
index 9ee3631a..119e1745 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredHandler.java
@@ -178,6 +178,15 @@ public class WiredHandler {
private static boolean handle(InteractionWiredTrigger trigger, final RoomUnit roomUnit, final Room room, final Object[] stuff, final LegacyExecutionPlan executionPlan) {
long millis = System.currentTimeMillis();
int roomUnitId = roomUnit != null ? roomUnit.getId() : -1;
+
+ // Only one thread may process a given trigger box at a time, so the
+ // cooldown check (below) and setCooldown (further down) act as one
+ // atomic claim — preventing a concurrent packet/cycle double-fire.
+ if (!trigger.tryBeginProcessing()) {
+ return false;
+ }
+
+ try {
if (Emulator.isReady && ((Emulator.getConfig().getBoolean("wired.custom.enabled", false) && (trigger.canExecute(millis) || roomUnitId > -1) && trigger.userCanExecute(roomUnitId, millis)) || (!Emulator.getConfig().getBoolean("wired.custom.enabled", false) && trigger.canExecute(millis))) && trigger.execute(roomUnit, room, stuff)) {
THashSet conditions = room.getRoomSpecialTypes().getConditions(trigger.getX(), trigger.getY());
THashSet effects = room.getRoomSpecialTypes().getEffects(trigger.getX(), trigger.getY());
@@ -272,6 +281,9 @@ public class WiredHandler {
}
return false;
+ } finally {
+ trigger.endProcessing();
+ }
}
private static boolean evaluateConditions(THashSet conditions, RoomUnit roomUnit, Room room, Object[] stuff, int evaluationMode, int evaluationValue) {
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/ClientMessage.java b/Emulator/src/main/java/com/eu/habbo/messages/ClientMessage.java
index 55fdae44..5817a758 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/ClientMessage.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/ClientMessage.java
@@ -4,6 +4,8 @@ import com.eu.habbo.util.PacketUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
+import java.nio.charset.StandardCharsets;
+
public class ClientMessage {
private final int header;
private final ByteBuf buffer;
@@ -61,10 +63,17 @@ public class ClientMessage {
public String readString() {
try {
- int length = this.readShort();
+ // Length is an unsigned short in the protocol; mask to avoid a
+ // negative array size, and clamp to what's actually buffered so a
+ // bogus length can't throw mid-read and desync the remaining fields.
+ int length = this.readShort() & 0xFFFF;
+ int available = this.buffer.readableBytes();
+ if (length > available) {
+ length = available;
+ }
byte[] data = new byte[length];
this.buffer.readBytes(data);
- return new String(data);
+ return new String(data, StandardCharsets.UTF_8);
} catch (Exception e) {
return "";
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/ServerMessage.java b/Emulator/src/main/java/com/eu/habbo/messages/ServerMessage.java
index 7b03768e..73660a55 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/ServerMessage.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/ServerMessage.java
@@ -7,6 +7,7 @@ import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
public class ServerMessage {
@@ -61,7 +62,7 @@ public class ServerMessage {
}
try {
- byte[] data = obj.getBytes();
+ byte[] data = obj.getBytes(StandardCharsets.UTF_8);
this.stream.writeShort(data.length);
this.stream.write(data);
} catch (IOException e) {
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java
index a80067b7..fdf052bd 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogBuyItemEvent.java
@@ -53,6 +53,15 @@ public class CatalogBuyItemEvent extends MessageHandler {
String extraData = this.packet.readString();
int count = this.packet.readInt();
+ // Clamp the client-supplied quantity. Without this the club-offer
+ // branch accumulates cost in plain ints and a huge count overflows
+ // to a negative total, bypassing the affordability checks and
+ // CREDITING the buyer (free currency/subscription exploit).
+ if (count < 1 || count > 100) {
+ this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
+ return;
+ }
+
try {
if (this.client.getHabbo().getInventory().getItemsComponent().itemCount() > HabboInventory.MAXIMUM_ITEMS) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR).compose());
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/OpenRecycleBoxEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/OpenRecycleBoxEvent.java
index 523d5d0d..bd8cd616 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/OpenRecycleBoxEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/OpenRecycleBoxEvent.java
@@ -29,6 +29,11 @@ public class OpenRecycleBoxEvent extends MessageHandler {
if (item.getUserId() != this.client.getHabbo().getHabboInfo().getId()) return;
if (item instanceof InteractionGift) {
+ // The actual unwrap (OpenGift) runs async/delayed and only then
+ // removes the wrapper, so a second packet would otherwise pass
+ // the room/owner checks and double-process the gift. Claim it once.
+ if (!((InteractionGift) item).tryStartOpening()) return;
+
if (item.getBaseItem().getName().contains("present_wrap")) {
((InteractionGift) item).explode = true;
room.updateItem(item);
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/RecycleEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/RecycleEvent.java
index 884b6f6c..f1b6d8dd 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/RecycleEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/recycler/RecycleEvent.java
@@ -40,23 +40,26 @@ public class RecycleEvent extends MessageHandler {
}
}
- if (items.size() == count) {
- for (HabboItem item : items) {
- this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
- this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
- Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
- }
- } else {
+ if (items.size() != count) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
+ // Compute the reward BEFORE consuming the inputs. Previously the
+ // inputs were deleted first, so a null reward (misconfiguration)
+ // permanently destroyed the 8 furni with nothing in return.
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
if (reward == null) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
+ for (HabboItem item : items) {
+ this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
+ this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
+ Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
+ }
+
this.client.sendResponse(new AddHabboItemComposer(reward));
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/crafting/CraftingCraftItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/crafting/CraftingCraftItemEvent.java
index fe0c4993..6bd4f2e4 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/crafting/CraftingCraftItemEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/crafting/CraftingCraftItemEvent.java
@@ -37,6 +37,8 @@ public class CraftingCraftItemEvent extends MessageHandler {
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
if (habboItem == null) {
+ // Not enough ingredients — give back whatever we already pulled.
+ this.restoreItems(toRemove);
return;
}
@@ -70,8 +72,23 @@ public class CraftingCraftItemEvent extends MessageHandler {
return;
}
+ // Reward creation failed after we already pulled the ingredients —
+ // restore them so the craft isn't a silent item sink.
+ this.restoreItems(toRemove);
}
this.client.sendResponse(new CraftingResultComposer(null));
}
+
+ private void restoreItems(TIntObjectHashMap items) {
+ if (items.isEmpty()) {
+ return;
+ }
+ items.forEachValue(item -> {
+ this.client.getHabbo().getInventory().getItemsComponent().addItem(item);
+ this.client.sendResponse(new AddHabboItemComposer(item));
+ return true;
+ });
+ this.client.sendResponse(new InventoryRefreshComposer());
+ }
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java
index 274a5dbd..bcbd5626 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java
@@ -49,15 +49,17 @@ public class FurniEditorSearchEvent extends MessageHandler {
try {
int numericQuery = Integer.parseInt(query);
isNumeric = true;
+ String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
params.add(numericQuery);
params.add(numericQuery);
- params.add("%" + query + "%");
- params.add("%" + query + "%");
+ params.add(likeQuery);
+ params.add(likeQuery);
} catch (NumberFormatException e) {
+ String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
- params.add("%" + query + "%");
- params.add("%" + query + "%");
+ params.add(likeQuery);
+ params.add(likeQuery);
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java
index 64696bf7..b9c7fd71 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/MachineIDEvent.java
@@ -32,8 +32,15 @@ public class MachineIDEvent extends MessageHandler {
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
Emulator.getThreading().run(this.client.getHabbo());
+
+ // The fingerprint can arrive AFTER login (UniqueID is sent right after the
+ // SSO ticket), so Habbo.connect() may have skipped the MAC-ban check for
+ // lack of a machineId. Enforce it now that the fingerprint is known.
+ if (Emulator.getGameEnvironment().getModToolManager().hasMACBan(this.client)) {
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
+ }
}
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
}
-}
\ No newline at end of file
+}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java
index cdebe27d..f9704bec 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java
@@ -306,7 +306,7 @@ public class SecureLoginEvent extends MessageHandler {
Emulator.getPluginManager().fireEvent(userLoginEvent);
if(userLoginEvent.isCancelled()) {
- Emulator.getGameServer().getGameClientManager().disposeClient(this.client);
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(this.client);
return;
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java
index e0d16e2d..c47bad24 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java
@@ -12,6 +12,7 @@ import java.sql.SQLException;
public class HousekeepingGiveCreditsEvent extends MessageHandler {
private static final String ACTION_KEY = "user.give_credits";
+ private static final int MAX_GRANT = 1_000_000_000;
@Override
public int getRatelimit() {
@@ -27,7 +28,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
int userId = this.packet.readInt();
int amount = this.packet.readInt();
- if (userId <= 0 || amount == 0) {
+ if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -38,6 +39,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
// giveCredits already pushes UserCreditsComposer and persists via the
// standard HabboInfo write path; nothing extra needed for the online branch.
online.giveCredits(amount);
+ this.audit(userId, amount);
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
return;
}
@@ -57,6 +59,15 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler {
return;
}
+ this.audit(userId, amount);
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
+
+ private void audit(int userId, int amount) {
+ com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
+ this.client.getHabbo().getHabboInfo().getId(),
+ this.client.getHabbo().getHabboInfo().getUsername(),
+ ACTION_KEY, userId, "amount=" + amount,
+ this.client.getHabbo().getHabboInfo().getIpLogin());
+ }
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java
index ba347b93..5e5053a1 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java
@@ -18,6 +18,7 @@ import java.sql.SQLException;
*/
public class HousekeepingGiveCurrencyEvent extends MessageHandler {
private static final int CURRENCY_DUCKETS = 0;
+ private static final int MAX_GRANT = 1_000_000_000;
@Override
public int getRatelimit() {
@@ -36,7 +37,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
String actionKey = "user.give_currency_" + currencyType;
- if (userId <= 0 || amount == 0) {
+ if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) {
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input"));
return;
}
@@ -52,6 +53,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
online.givePoints(currencyType, amount);
}
+ this.audit(actionKey, userId, currencyType, amount);
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
return;
}
@@ -69,6 +71,15 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler {
return;
}
+ this.audit(actionKey, userId, currencyType, amount);
this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, ""));
}
+
+ private void audit(String actionKey, int userId, int currencyType, int amount) {
+ com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
+ this.client.getHabbo().getHabboInfo().getId(),
+ this.client.getHabbo().getHabboInfo().getUsername(),
+ actionKey, userId, "type=" + currencyType + " amount=" + amount,
+ this.client.getHabbo().getHabboInfo().getIpLogin());
+ }
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java
index 985250c9..bccf6588 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java
@@ -56,7 +56,7 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
- statement.setString(1, exactMatch ? query : query + "%");
+ statement.setString(1, exactMatch ? query : com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java
index 67f35d8a..db100eb8 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java
@@ -11,6 +11,7 @@ import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
+import java.sql.ResultSet;
import java.sql.SQLException;
public class HousekeepingSetUserRankEvent extends MessageHandler {
@@ -44,6 +45,43 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
Rank rank = permissions.getRank(rankId);
+ // Rank-ceiling guard: an operator must never be able to grant a rank
+ // above their own, nor modify a user who already outranks them. This
+ // mirrors GiveRankCommand and prevents privilege escalation through
+ // the housekeeping path (including self-promotion).
+ int operatorRankId = this.client.getHabbo().getHabboInfo().getRank().getId();
+
+ if (rank.getId() > operatorRankId) {
+ this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
+ return;
+ }
+
+ Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
+
+ int targetRankId;
+ if (online != null) {
+ targetRankId = online.getHabboInfo().getRank().getId();
+ } else {
+ targetRankId = 0;
+ try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
+ PreparedStatement statement = connection.prepareStatement("SELECT rank FROM users WHERE id = ? LIMIT 1")) {
+ statement.setInt(1, userId);
+ try (ResultSet set = statement.executeQuery()) {
+ if (set.next()) {
+ targetRankId = set.getInt("rank");
+ }
+ }
+ } catch (SQLException e) {
+ this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed"));
+ return;
+ }
+ }
+
+ if (targetRankId > operatorRankId) {
+ this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high"));
+ return;
+ }
+
// Persist for the offline path. Online users get their in-memory
// HabboInfo.rank rebound below so server-side hasPermission()
// checks land on the new permission set without a relogin.
@@ -57,8 +95,6 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
return;
}
- Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
-
if (online != null) {
online.getHabboInfo().setRank(rank);
// Ship the refreshed permissions snapshot — same payload the
@@ -66,6 +102,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler {
online.getClient().sendResponse(new UserPermissionsComposer(online));
}
+ com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log(
+ this.client.getHabbo().getHabboInfo().getId(),
+ this.client.getHabbo().getHabboInfo().getUsername(),
+ ACTION_KEY, userId, "rankId=" + rankId,
+ this.client.getHabbo().getHabboInfo().getIpLogin());
+
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, ""));
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java
index ad55465e..a175cdd0 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java
@@ -3,6 +3,7 @@ package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ScripterManager;
import com.eu.habbo.habbohotel.permissions.Permission;
+import com.eu.habbo.habbohotel.users.HabboInfo;
import com.eu.habbo.habbohotel.users.HabboManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.modtool.ModToolUserChatlogComposer;
@@ -12,7 +13,11 @@ public class ModToolRequestUserChatlogEvent extends MessageHandler {
public void handle() throws Exception {
if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
int userId = this.packet.readInt();
- String username = HabboManager.getOfflineHabboInfo(userId).getUsername();
+ HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(userId);
+ if (habboInfo == null) {
+ return;
+ }
+ String username = habboInfo.getUsername();
this.client.sendResponse(new ModToolUserChatlogComposer(Emulator.getGameEnvironment().getModToolManager().getUserRoomVisitsAndChatlogs(userId), userId, username));
} else {
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java
index 78be5765..53cf4e3d 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java
@@ -5,6 +5,13 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer;
public class AddSavedSearchEvent extends MessageHandler {
+ private static final int MAX_SAVED_SEARCHES = 50;
+
+ @Override
+ public int getRatelimit() {
+ return 1000;
+ }
+
@Override
public void handle() throws Exception {
String searchCode = this.packet.readString();
@@ -13,6 +20,11 @@ public class AddSavedSearchEvent extends MessageHandler {
if (searchCode.length() > 255) searchCode = searchCode.substring(0, 255);
if (filter.length() > 255) filter = filter.substring(0, 255);
+ if (this.client.getHabbo().getHabboInfo().getSavedSearches().size() >= MAX_SAVED_SEARCHES) {
+ this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches()));
+ return;
+ }
+
this.client.getHabbo().getHabboInfo().addSavedSearch(new NavigatorSavedSearch(searchCode, filter));
this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches()));
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/DeleteSavedSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/DeleteSavedSearchEvent.java
index 8429ffe3..24ffef06 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/DeleteSavedSearchEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/DeleteSavedSearchEvent.java
@@ -5,6 +5,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.navigator.NewNavigatorSavedSearchesComposer;
public class DeleteSavedSearchEvent extends MessageHandler {
+ @Override
+ public int getRatelimit() {
+ return 500;
+ }
+
@Override
public void handle() throws Exception {
int searchId = this.packet.readInt();
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestNewNavigatorRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestNewNavigatorRoomsEvent.java
index efb78298..95b5661d 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestNewNavigatorRoomsEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/RequestNewNavigatorRoomsEvent.java
@@ -15,12 +15,20 @@ import java.util.*;
public class RequestNewNavigatorRoomsEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestNewNavigatorRoomsEvent.class);
+ private static final int MAX_VIEW_LENGTH = 32;
+ private static final int MAX_QUERY_LENGTH = 64;
+
+ @Override
+ public int getRatelimit() {
+ return 200;
+ }
@Override
public void handle() throws Exception {
String view = this.packet.readString();
String query = this.packet.readString();
-
+ if (view.length() > MAX_VIEW_LENGTH) return;
+ if (query.length() > MAX_QUERY_LENGTH) query = query.substring(0, MAX_QUERY_LENGTH);
if (view.equals("query")) view = "hotel_view";
if (view.equals("groups")) view = "hotel_view";
@@ -43,7 +51,7 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
NavigatorFilterField field = Emulator.getGameEnvironment().getNavigatorManager().filterSettings.get(filterField);
if (filter != null) {
if (query.contains(":")) {
- String[] parts = query.split(":");
+ String[] parts = query.split(":", 2);
if (parts.length > 1) {
filterField = parts[0];
@@ -53,6 +61,7 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
if (!Emulator.getGameEnvironment().getNavigatorManager().filterSettings.containsKey(filterField)) {
filterField = "anything";
}
+ part = "";
}
}
@@ -95,28 +104,19 @@ public class RequestNewNavigatorRoomsEvent extends MessageHandler {
rooms.addAll(searchResultList.rooms);
resultLists.add(new SearchResultList(searchResultList.order, searchResultList.code, searchResultList.query, searchResultList.action, searchResultList.mode, searchResultList.hidden, rooms, searchResultList.filter, searchResultList.showInvisible, searchResultList.displayOrder, searchResultList.categoryOrder));
}
+ if ("group".equals(filterField)) {
+ final String needle = part.toLowerCase();
+ for (SearchResultList list : resultLists) {
+ list.rooms.removeIf(room -> !room.belongsToGuild()
+ || (!needle.isEmpty() && !room.getGuildName().toLowerCase().contains(needle)));
+ }
+ }
filter.filter(field.field, part, resultLists);
resultLists = toQueryResults(resultLists);
this.client.sendResponse(new NewNavigatorSearchResultsComposer(view, query, resultLists));
} catch (Exception e) {
LOGGER.error("Caught exception", e);
}
-
- /*
- try
- {
-
- List resultLists = new ArrayList<>(filter.getResult(this.client.getHabbo(), field, part, category != null ? category.getId() : -1));
- filter.filter(field.field, part, resultLists);
-
- Collections.sort(resultLists);
- this.client.sendResponse(new NewNavigatorSearchResultsComposer(view, query, resultLists));
- }
- catch (Exception e)
- {
- LOGGER.error("Caught exception", e);
- }
- */
}
private ArrayList toQueryResults(List resultLists) {
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomRequestBannedUsersEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomRequestBannedUsersEvent.java
index 321bdb42..e103bd0e 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomRequestBannedUsersEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomRequestBannedUsersEvent.java
@@ -14,7 +14,7 @@ public class RoomRequestBannedUsersEvent extends MessageHandler {
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (room == null) return;
- if (!room.hasRights(this.client.getHabbo()) || !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
+ if (!room.hasRights(this.client.getHabbo()) && !this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) return;
this.client.sendResponse(new RoomBannedUsersComposer(room));
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ConfirmPetBreedingEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ConfirmPetBreedingEvent.java
index 13a1e200..17692282 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ConfirmPetBreedingEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/ConfirmPetBreedingEvent.java
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.rooms.pets;
import com.eu.habbo.habbohotel.items.interactions.pets.InteractionPetBreedingNest;
+import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.HabboItem;
import com.eu.habbo.messages.incoming.MessageHandler;
@@ -13,7 +14,10 @@ public class ConfirmPetBreedingEvent extends MessageHandler {
int petOneId = this.packet.readInt();
int petTwoId = this.packet.readInt();
- HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId);
+ Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
+ if (room == null) return;
+
+ HabboItem item = room.getHabboItem(itemId);
if (item instanceof InteractionPetBreedingNest) {
((InteractionPetBreedingNest) item).breed(this.client.getHabbo(), name, petOneId, petTwoId);
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
index 3cce0a34..7da0c5f4 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/pets/PetPickupEvent.java
@@ -23,7 +23,7 @@ public class PetPickupEvent extends MessageHandler {
Pet pet = room.getPet(petId);
if (pet != null) {
- if (this.client.getHabbo().getHabboInfo().getId() == pet.getId() || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
+ if (this.client.getHabbo().getHabboInfo().getId() == pet.getUserId() || room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
if (!this.client.getHabbo().hasPermission(Permission.ACC_UNLIMITED_PETS) && this.client.getHabbo().getInventory().getPetsComponent().getPets().size() >= PetManager.MAXIMUM_PET_INVENTORY_SIZE) {
this.client.getHabbo().alert(Emulator.getTexts().getValue("error.pets.max.inventory").replace("%amount%", PetManager.MAXIMUM_PET_INVENTORY_SIZE + ""));
return;
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
index 51a38650..63e905af 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserWalkEvent.java
@@ -163,7 +163,13 @@ public class RoomUserWalkEvent extends MessageHandler {
}
if (roomUnit.getMoveBlockingTask() != null) {
- roomUnit.getMoveBlockingTask().get();
+ try {
+ // Bound the wait so a stuck/delayed move-blocking task can't park
+ // the Netty event loop (and thus every client on it) indefinitely.
+ roomUnit.getMoveBlockingTask().get(2, java.util.concurrent.TimeUnit.SECONDS);
+ } catch (java.util.concurrent.TimeoutException | java.util.concurrent.ExecutionException | InterruptedException e) {
+ // proceed with the walk regardless
+ }
}
boolean needsLocationResync =
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolSanctionInfoComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolSanctionInfoComposer.java
index b281fe53..875e1597 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolSanctionInfoComposer.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolSanctionInfoComposer.java
@@ -9,8 +9,8 @@ import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import gnu.trove.map.hash.THashMap;
-import org.joda.time.DateTime;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
@@ -47,7 +47,10 @@ public class ModToolSanctionInfoComposer extends MessageComposer {
if (item.probationTimestamp > 0) {
probationEndTime = new Date((long) item.probationTimestamp * 1000);
- probationStartTime = new DateTime(probationEndTime).minusDays(modToolSanctions.getProbationDays(modToolSanctionLevelItem)).toDate();
+ probationStartTime = Date.from(probationEndTime.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .minusDays(modToolSanctions.getProbationDays(modToolSanctionLevelItem))
+ .toInstant());
Date tradeLockedUntil = null;
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java
index 51e00c4c..64bf2d1f 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rooms/users/RoomUsersComposer.java
@@ -99,7 +99,7 @@ public class RoomUsersComposer extends MessageComposer {
this.response.appendInt(1);
this.response.appendString(habbo.getHabboInfo().getGender().name().toUpperCase());
this.response.appendInt(habbo.getHabboStats().guild != 0 ? habbo.getHabboStats().guild : -1);
- this.response.appendInt(habbo.getHabboStats().guild != 0 ? habbo.getHabboStats().guild : -1);
+ this.response.appendInt(habbo.getHabboStats().guild != 0 ? 1 : -1);
String name = "";
if (habbo.getHabboStats().guild != 0) {
Guild g = Emulator.getGameEnvironment().getGuildManager().getGuild(habbo.getHabboStats().guild);
diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/DisconnectUser.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/DisconnectUser.java
index 8b768ac0..8f8a51ed 100644
--- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/DisconnectUser.java
+++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/DisconnectUser.java
@@ -29,7 +29,7 @@ public class DisconnectUser extends RCONMessage {
private static final int MAX_FRAME_SIZE = 500000;
+ // Runs the game packet handler OFF the Netty I/O event loop, so a blocking
+ // handler (login/friends/catalog/guild JDBC, A* pathfinding, etc.) can no
+ // longer stall socket I/O for every other client sharing that I/O thread.
+ // A DefaultEventExecutorGroup pins each channel to one executor, so a single
+ // client's packets stay strictly ordered (no new intra-client races); the
+ // cross-client concurrency degree is the same the multi-threaded I/O group
+ // already had. Daemon threads so they don't block JVM shutdown.
+ private static final EventExecutorGroup PACKET_HANDLER_GROUP = new DefaultEventExecutorGroup(
+ packetHandlerThreads(),
+ new DefaultThreadFactory("GamePacketHandler", true));
+
+ // Size of the packet-handler pool. Defaults to max(16, 2x CPU cores); set
+ // the optional `io.packet.handler.threads` config key to override.
+ private static int packetHandlerThreads() {
+ int fallback = Math.max(16, Runtime.getRuntime().availableProcessors() * 2);
+ if (Emulator.getConfig() == null) {
+ return fallback;
+ }
+ int configured = Emulator.getConfig().getInt("io.packet.handler.threads", fallback);
+ return configured > 0 ? configured : fallback;
+ }
+
private final SslContext sslContext;
private final boolean sslEnabled;
private final WebSocketServerProtocolConfig wsConfig;
@@ -82,7 +107,7 @@ public class WebSocketChannelInitializer extends ChannelInitializer(512),
+ new java.util.concurrent.ThreadFactory() {
+ private final AtomicInteger counter = new AtomicInteger(1);
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r, "auth-http-worker-" + counter.getAndIncrement());
+ t.setDaemon(true);
+ return t;
+ }
+ });
+
+ // Max threads for the auth pool. Defaults to 16; set the optional
+ // `auth.http.pool.size` config key to override.
+ private static int authPoolMax() {
+ int fallback = 16;
+ if (com.eu.habbo.Emulator.getConfig() == null) {
+ return fallback;
+ }
+ int configured = com.eu.habbo.Emulator.getConfig().getInt("auth.http.pool.size", fallback);
+ return configured > 0 ? configured : fallback;
+ }
+
static final String LOGIN_PATH = "/api/auth/login";
static final String REGISTER_PATH = "/api/auth/register";
static final String FORGOT_PATH = "/api/auth/forgot-password";
@@ -52,10 +90,30 @@ public class AuthHttpHandler extends ChannelInboundHandlerAdapter {
return;
}
+ // Offload the (potentially blocking) auth work off the event loop. Netty
+ // writes are thread-safe, so the endpoints' sendJson/writeAndFlush calls
+ // are fine from the worker; the request is released once the work ends.
try {
- handle(ctx, req, path);
- } finally {
- ReferenceCountUtil.release(req);
+ AUTH_EXECUTOR.execute(() -> {
+ try {
+ handle(ctx, req, path);
+ } catch (Throwable t) {
+ LOGGER.error("Auth handler failed for {}", path, t);
+ try {
+ sendJson(ctx, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorPayload("Internal error."));
+ } catch (Throwable ignored) {
+ // response may already be partially written — nothing else to do
+ }
+ } finally {
+ ReferenceCountUtil.release(req);
+ }
+ });
+ } catch (RejectedExecutionException rejected) {
+ try {
+ sendJson(ctx, req, HttpResponseStatus.SERVICE_UNAVAILABLE, errorPayload("Server busy, try again shortly."));
+ } finally {
+ ReferenceCountUtil.release(req);
+ }
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpUtil.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpUtil.java
index 0d949ec8..644e9e1b 100644
--- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpUtil.java
+++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthHttpUtil.java
@@ -28,7 +28,7 @@ import java.sql.SQLException;
import java.util.Base64;
import java.util.regex.Pattern;
-final class AuthHttpUtil {
+public final class AuthHttpUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
@@ -132,7 +132,10 @@ final class AuthHttpUtil {
String ipHeader = Emulator.getConfig() != null
? Emulator.getConfig().getValue("ws.ip.header", "")
: "";
- if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
+ // Only trust a client-supplied forwarded-IP header when the DIRECT peer
+ // is a trusted reverse proxy; otherwise an attacker hitting the port
+ // directly could spoof it to evade per-IP rate limiting and IP bans.
+ if (!ipHeader.isEmpty() && req.headers().contains(ipHeader) && isTrustedProxy(ctx)) {
String hv = req.headers().get(ipHeader);
if (hv != null && !hv.isEmpty()) {
int comma = hv.indexOf(',');
@@ -148,6 +151,37 @@ final class AuthHttpUtil {
return "";
}
+ /**
+ * Whether the channel's direct peer may set a forwarded-IP header. Loopback
+ * is always trusted; additional proxies can be allow-listed (exact IP or
+ * string prefix, comma-separated) via the {@code ws.ip.header.trusted}
+ * config key. Default-deny so the header can't be spoofed from the open net.
+ */
+ public static boolean isTrustedProxy(ChannelHandlerContext ctx) {
+ String peerIp = (ctx.channel().remoteAddress() instanceof InetSocketAddress a)
+ ? a.getAddress().getHostAddress() : null;
+ if (peerIp == null || peerIp.isEmpty()) return false;
+ if (peerIp.equals("127.0.0.1") || peerIp.equals("::1") || peerIp.equals("0:0:0:0:0:0:0:1")) {
+ return true;
+ }
+ String trusted = Emulator.getConfig() != null
+ ? Emulator.getConfig().getValue("ws.ip.header.trusted", "")
+ : "";
+ if (trusted.isEmpty()) return false;
+ for (String entry : trusted.split(",")) {
+ String t = entry.trim();
+ if (t.isEmpty()) continue;
+ // Exact IP match, or a dotted/colon prefix range (e.g. "10.0.0." or
+ // "2001:db8:") — never a bare-IP prefix, so "10.0.0.1" can't also
+ // trust "10.0.0.12".
+ boolean isRange = t.endsWith(".") || t.endsWith(":");
+ if (peerIp.equals(t) || (isRange && peerIp.startsWith(t))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
static boolean checkPassword(String plain, String stored) {
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
try {
diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthRateLimiter.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthRateLimiter.java
index 67cabf17..1d0186e3 100644
--- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthRateLimiter.java
+++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AuthRateLimiter.java
@@ -11,8 +11,34 @@ public final class AuthRateLimiter {
private static final Map> STATE = new ConcurrentHashMap<>();
private static final Map> PROBE_STATE = new ConcurrentHashMap<>();
+ // Both maps are keyed by client IP and reachable by unauthenticated traffic.
+ // recordSuccess removes STATE on login, but failed-only and probe-only IPs
+ // never get removed otherwise — unbounded growth over the JVM lifetime.
+ // Opportunistically evict window-expired entries once the maps get large.
+ private static final int SWEEP_THRESHOLD = 10_000;
+ private static final long SWEEP_MIN_INTERVAL_MS = 60_000L;
+ private static volatile long lastSweepMillis = 0L;
+
private AuthRateLimiter() {}
+ private static void maybeSweep(long now) {
+ if (STATE.size() < SWEEP_THRESHOLD && PROBE_STATE.size() < SWEEP_THRESHOLD) return;
+ if (now - lastSweepMillis < SWEEP_MIN_INTERVAL_MS) return;
+ lastSweepMillis = now;
+
+ long stateWindowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
+ STATE.entrySet().removeIf(e -> {
+ State s = e.getValue().get();
+ return s == null || (s.lockedUntilMillis <= now && (now - s.windowStartMillis) > stateWindowMs);
+ });
+
+ long probeWindowMs = configInt("login.probe.window_sec", 60) * 1000L;
+ PROBE_STATE.entrySet().removeIf(e -> {
+ ProbeState p = e.getValue().get();
+ return p == null || (now - p.windowStartMillis) > probeWindowMs;
+ });
+ }
+
public static boolean isLocked(String ip) {
if (!isEnabled() || ip == null || ip.isEmpty()) return false;
@@ -38,6 +64,7 @@ public final class AuthRateLimiter {
if (!isEnabled() || ip == null || ip.isEmpty()) return;
long now = System.currentTimeMillis();
+ maybeSweep(now);
long windowMs = configInt("login.ratelimit.window_sec", 60) * 1000L;
int maxAttempts = configInt("login.ratelimit.max_attempts", 5);
long lockoutMs = configInt("login.ratelimit.lockout_sec", 120) * 1000L;
@@ -64,6 +91,7 @@ public final class AuthRateLimiter {
if (isLocked(ip)) return false;
long now = System.currentTimeMillis();
+ maybeSweep(now);
long windowMs = configInt("login.probe.window_sec", 60) * 1000L;
int maxAttempts = configInt("login.probe.max_attempts", 20);
diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/SessionEndpoints.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/SessionEndpoints.java
index 32f355be..74967c50 100644
--- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/SessionEndpoints.java
+++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/SessionEndpoints.java
@@ -69,7 +69,7 @@ final class SessionEndpoints {
com.eu.habbo.habbohotel.users.Habbo habbo =
Emulator.getGameServer().getGameClientManager().getHabbo(userId);
if (habbo != null && habbo.getClient() != null) {
- Emulator.getGameServer().getGameClientManager().disposeClient(habbo.getClient());
+ Emulator.getGameServer().getGameClientManager().forceDisposeClient(habbo.getClient());
}
}
}
diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java
index 126648ed..6845206e 100644
--- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java
+++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameByteDecryption.java
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.decoders;
import com.eu.habbo.networking.gameserver.GameServerAttributes;
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -15,14 +16,17 @@ public class GameByteDecryption extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List