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/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 fa31b151..1a69f83f 100644
--- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
+++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java
@@ -158,10 +158,12 @@ public class Room implements Comparable, ISerialize, Runnable {
private String tags;
private boolean publicRoom;
private boolean staffPromotedRoom;
- private boolean allowPets;
- private boolean allowPetsEat;
+ // Read every room cycle (processBots/processPets) but written from settings/
+ // admin packet handlers on another thread — volatile for cross-thread visibility.
+ private volatile boolean allowPets;
+ private volatile boolean allowPetsEat;
private boolean allowWalkthrough;
- private boolean allowBotsWalk;
+ private volatile boolean allowBotsWalk;
private boolean allowEffects;
private boolean hideWall;
private int chatMode;
@@ -1026,6 +1028,10 @@ public class Room implements Comparable, ISerialize, Runnable {
com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id);
}
+ // Drop this room's shared wired-variable assignment caches (otherwise
+ // they accrue per (room, item, user) for the JVM lifetime).
+ com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredVariableReferenceSupport.invalidateRoom(this.id);
+
this.itemManager.clear();
this.unitManager.clearQueue();
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/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/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