From 67d2f52f64e38358ba6fcf657eb99694d13a904a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 20 May 2026 20:34:37 +0200 Subject: [PATCH 1/3] fix(permissions): acc_supporttool incorrectly granted to VIP, denied to Super Mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default permission_definitions seed for acc_supporttool used the pattern (0, 1, 1, 1, 1, 0, 1) across rank_1..rank_7 — apparently shifted by two columns: * rank_2 (VIP) and rank_3 (X) had ALLOWED. With acc_supporttool=1 the SecureLoginEvent path sends ModeratorInitMessageEvent on login, which makes the React client surface the ModTools toolbar button and let the user open room/user info windows. The actual sanction endpoints (ModToolSanctionBanEvent, ModToolWarnEvent, …) still gate on ACC_SUPPORTTOOL so a VIP cannot actually take moderator action — but they can request user info, room info and chatlogs they have no business reading. * rank_6 (Super Mod) was DISALLOWED, which is obviously not what the name says. Corrected pattern: (0, 0, 0, 1, 1, 1, 1) — Support (4), Moderator (5), Super Mod (6), Administrator (7). Matches the convention used by the other staff-only acc_modtool_* keys. Two changes: - Default Database/FullDatabase.sql: fix the seed for fresh installs. - Database Updates/004_fix_acc_supporttool_rank.sql: idempotent UPDATE to realign existing deployments. Found by user report: a rank-2 (VIP) account on the live retro had the ModTools button visible in the toolbar after login. --- .../004_fix_acc_supporttool_rank.sql | 31 +++++++++++++++++++ Default Database/FullDatabase.sql | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Database Updates/004_fix_acc_supporttool_rank.sql diff --git a/Database Updates/004_fix_acc_supporttool_rank.sql b/Database Updates/004_fix_acc_supporttool_rank.sql new file mode 100644 index 00000000..f0dfd371 --- /dev/null +++ b/Database Updates/004_fix_acc_supporttool_rank.sql @@ -0,0 +1,31 @@ +-- ============================================================ +-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod +-- ============================================================ +-- The default permission_definitions seed shipped acc_supporttool +-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and +-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod) +-- did NOT. That's two bugs: +-- +-- * VIP users see the ModTools button on the toolbar and can +-- open Room/User info windows. The actual sanction endpoints +-- still gate on ACC_SUPPORTTOOL server-side so they can't +-- actually moderate, but the UI exposure is wrong and lets a +-- VIP request user info / room info / chatlogs they have no +-- business reading. +-- * Super Mod is denied the tool entirely, which is obviously +-- unintended given the rank name. +-- +-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1). +-- +-- Run on existing deployments to align with the corrected default +-- seed in `Default Database/FullDatabase.sql`. Idempotent. + +UPDATE `permission_definitions` + SET `rank_1` = 0, + `rank_2` = 0, + `rank_3` = 0, + `rank_4` = 1, + `rank_5` = 1, + `rank_6` = 1, + `rank_7` = 1 + WHERE `permission_key` = 'acc_supporttool'; diff --git a/Default Database/FullDatabase.sql b/Default Database/FullDatabase.sql index 75a3b75b..c7e85d40 100644 --- a/Default Database/FullDatabase.sql +++ b/Default Database/FullDatabase.sql @@ -28598,7 +28598,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`, ('acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.', 0, 0, 0, 0, 0, 0, 1), ('acc_staff_pick', 1, 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.', 0, 0, 0, 0, 0, 0, 1), ('acc_superwired', 1, 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.', 0, 0, 0, 0, 0, 0, 1), - ('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 1, 1, 1, 1, 0, 1), + ('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 0, 0, 1, 1, 1, 1), ('acc_trade_anywhere', 1, 'Allows starting trades outside the normal trade-enabled areas.', 0, 0, 0, 0, 0, 0, 1), ('acc_unignorable', 1, 'Prevents the account from being ignored by other users through the ignore system.', 0, 0, 0, 0, 0, 0, 0), ('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1), From e7ba4d09264e18a1bc3277b2d30a56bdf0930aff Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 20 May 2026 21:30:49 +0200 Subject: [PATCH 2/3] feat(modtool): populate lastPurchase / tradeLockExpiry / identityBans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModToolUserInfoComposer used to send three trailing fields hardcoded to empty/zero — the client rendered placeholders for every user, on every panel open: appendString(""); // Trading lock expiry timestamp appendString(""); // Last Purchase Timestamp appendInt(0); // Number of account bans These are useful moderation signals and the data already exists in the live tables. Wire them up. Last Purchase Query MAX(timestamp) FROM logs_shop_purchases WHERE user_id = ?. Returns the most recent purchase epoch. Rendered as yyyy-MM-dd HH:mm. Empty when the user has never bought anything (the query returns NULL → getInt returns 0 → formatUnixTimestamp emits ""). Trading lock expiry Query MAX(trade_locked_until) FROM sanctions WHERE habbo_id = ? AND trade_locked_until > . Latest ACTIVE lock only — past entries don't count. Same yyyy-MM-dd HH:mm format. Empty when no active lock. Identity related bans Count of DISTINCT other user accounts that have a ban entry against the same machine_id as the target. Self is excluded since the target's own bans already show up in banCount. An empty machine_id (default '') short-circuits to 0 so we never match accounts whose machine fingerprint was never recorded. The existing totalBans counter is extracted into a helper alongside the three new ones — cleaner than the inline try-catch tower it used to live in, same behaviour. Format choice yyyy-MM-dd HH:mm matches the timestamp shown elsewhere in moderation UI; both string fields go through the same formatter so the empty case stays consistent (empty string, not "1970-01-01..."). No client-side changes needed — ModeratorUserInfoData already parses both strings and the int, and the React ModToolsUserView already renders them. They were just always empty before. --- .../modtool/ModToolUserInfoComposer.java | 121 +++++++++++++++--- 1 file changed, 100 insertions(+), 21 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java index 4772d455..3e2b938f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/modtool/ModToolUserInfoComposer.java @@ -12,10 +12,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; public class ModToolUserInfoComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private final ResultSet set; private final boolean hideMail; @@ -29,37 +32,30 @@ public class ModToolUserInfoComposer extends MessageComposer { protected ServerMessage composeInternal() { this.response.init(Outgoing.ModToolUserInfoComposer); try { - int totalBans = 0; + int userId = this.set.getInt("user_id"); + String machineId = this.set.getString("machine_id"); + int now = Emulator.getIntUnixTimestamp(); - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) { - statement.setInt(1, this.set.getInt("user_id")); - try (ResultSet set = statement.executeQuery()) { - if (set.next()) { - totalBans = set.getInt("amount"); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + int totalBans = countBansForUser(userId); + int lastPurchaseTimestamp = fetchLastPurchaseTimestamp(userId); + int tradeLockExpiryTimestamp = fetchActiveTradeLockExpiry(userId, now); + int identityRelatedBanCount = countIdentityRelatedBans(userId, machineId); - this.response.appendInt(this.set.getInt("user_id")); + this.response.appendInt(userId); this.response.appendString(this.set.getString("username")); this.response.appendString(this.set.getString("look")); - this.response.appendInt((Emulator.getIntUnixTimestamp() - this.set.getInt("account_created")) / 60); - this.response.appendInt((this.set.getInt("online") == 1 ? 0 : Emulator.getIntUnixTimestamp() - this.set.getInt("last_online")) / 60); + this.response.appendInt((now - this.set.getInt("account_created")) / 60); + this.response.appendInt((this.set.getInt("online") == 1 ? 0 : now - this.set.getInt("last_online")) / 60); this.response.appendBoolean(this.set.getInt("online") == 1); this.response.appendInt(this.set.getInt("cfh_send")); this.response.appendInt(this.set.getInt("cfh_abusive")); this.response.appendInt(this.set.getInt("cfh_warnings")); this.response.appendInt(totalBans); // Number of bans this.response.appendInt(this.set.getInt("tradelock_amount")); - this.response.appendString(""); //Trading lock expiry timestamp - this.response.appendString(""); //Last Purchase Timestamp - this.response.appendInt(this.set.getInt("user_id")); //Personal Identification # - this.response.appendInt(0); // Number of account bans + this.response.appendString(formatUnixTimestamp(tradeLockExpiryTimestamp)); // Trading lock expiry timestamp + this.response.appendString(formatUnixTimestamp(lastPurchaseTimestamp)); // Last Purchase Timestamp + this.response.appendInt(userId); //Personal Identification # + this.response.appendInt(identityRelatedBanCount); // Number of account bans on the same machine_id this.response.appendString(this.hideMail ? "" : this.set.getString("mail")); this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt @@ -90,4 +86,87 @@ public class ModToolUserInfoComposer extends MessageComposer { public ResultSet getSet() { return set; } + + private static int countBansForUser(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) { + statement.setInt(1, userId); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) return set.getInt("amount"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + return 0; + } + + /** + * Most recent purchase timestamp from logs_shop_purchases for this + * user. Returns 0 when the user has never bought anything (in which + * case the wire field stays empty and the client shows the empty + * placeholder). + */ + private static int fetchLastPurchaseTimestamp(int userId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT MAX(`timestamp`) AS ts FROM logs_shop_purchases WHERE user_id = ?")) { + statement.setInt(1, userId); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) return set.getInt("ts"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + return 0; + } + + /** + * Latest active trade-lock expiry from the sanctions table. Only + * locks expiring in the future are considered — past entries don't + * count. Returns 0 when no active lock exists. + */ + private static int fetchActiveTradeLockExpiry(int userId, int now) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT MAX(trade_locked_until) AS expiry FROM sanctions WHERE habbo_id = ? AND trade_locked_until > ?")) { + statement.setInt(1, userId); + statement.setInt(2, now); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) return set.getInt("expiry"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + return 0; + } + + /** + * Count of OTHER user accounts that have been banned from the same + * machine_id as this user. An empty machine_id (default '') is + * ignored — never matches anything by definition. Self is excluded + * because the user's own bans are already counted under banCount. + */ + private static int countIdentityRelatedBans(int userId, String machineId) { + if (machineId == null || machineId.isEmpty()) return 0; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT user_id) AS amount FROM bans WHERE machine_id = ? AND user_id != ?")) { + statement.setString(1, machineId); + statement.setInt(2, userId); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) return set.getInt("amount"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + return 0; + } + + /** + * Wire format for date fields is `yyyy-MM-dd HH:mm`. A 0 timestamp + * is rendered as an empty string so the client falls back to its + * empty-state placeholder. + */ + private static String formatUnixTimestamp(int timestamp) { + if (timestamp <= 0) return ""; + return DATE_FORMAT.format(new Date(timestamp * 1000L)); + } } From 4e47dbee1676ee5e9741326f5e09486524e79794 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 20 May 2026 21:54:07 +0200 Subject: [PATCH 3/3] fix(modtool): bump users_settings counters on every sanction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The User Info panel reads its CFH / Cautions / Bans / Trade locks counters from `users_settings.cfh_send` / `cfh_warnings` / `cfh_bans` (via totalBans) / `tradelock_amount`. Historically only `cfh_send` was ever incremented (by `InsertModToolIssue` on CFH submit), so a user could accumulate any number of Alert / Mute / Ban / TradeLock sanctions without the stats reflecting it — every panel showed all zeros even on accounts with a long sanction history visible in the modern `sanctions` table. The two systems aren't going away — `ModToolSanctions` (the modern one) tracks individual sanction events with probation timestamps, while the legacy `users_settings.cfh_*` columns are flat counters the ModTool UI displays. Both need to stay in sync. Wire them up: `ModToolManager.bumpUserSettingCounter(userId, column)` Static helper, column-whitelisted (`cfh_warnings` / `cfh_bans` / `cfh_abusive` / `tradelock_amount`) to keep the dynamic SQL safe. Single UPDATE per call; SQL exceptions logged, never thrown. `ModToolSanctionAlertEvent`, `ModToolSanctionMuteEvent` → bump `cfh_warnings`. Mute is a punitive but non-banning action; both it and Alert are recorded as a warning on the legacy counter, matching what the Cautions stat card represents in the new UI. `ModToolSanctionBanEvent` → bump `cfh_bans`. The `totalBans` field the composer sends ALREADY counts entries in the `bans` table, so the wire field reflects reality immediately — this column bump is a defensive duplicate so any code that reads `users_settings.cfh_bans` directly (e.g. plugin scripts, CMS dashboards) stays in sync. `ModToolSanctionTradeLockEvent` → bump `tradelock_amount`. Mirrors what `AllowTradingCommand` already does for the command-line path. `ModToolManager.closeTicketAsAbusive` → bump `cfh_abusive` for the REPORTER (issue.senderId), not the reported user. The Abusive counter measures false reports filed by the user, so it belongs on whoever opened the CFH that got closed as abusive. No client-side changes — counter columns are unchanged, only the write paths are. --- .../habbohotel/modtool/ModToolManager.java | 38 +++++++++++++++++++ .../modtool/ModToolSanctionAlertEvent.java | 3 ++ .../modtool/ModToolSanctionBanEvent.java | 2 + .../modtool/ModToolSanctionMuteEvent.java | 3 ++ .../ModToolSanctionTradeLockEvent.java | 3 ++ 5 files changed, 49 insertions(+) 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 1984ad28..03546504 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 @@ -651,6 +651,10 @@ public class ModToolManager { sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE)); } + // Reporter (the user who opened the CFH) gets their abusive + // counter bumped — the legacy stat shown in the User Info table. + bumpUserSettingCounter(issue.senderId, "cfh_abusive"); + this.updateTicketToMods(issue); this.removeTicket(issue); @@ -737,4 +741,38 @@ public class ModToolManager { return issues; } + + /** + * Increments a single integer counter on `users_settings` for the + * given user. Used by the moderation sanction handlers to bump the + * legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings, + * cfh_bans, cfh_abusive, tradelock_amount) — historically these were + * only ever incremented by the CFH submission path, so a user could + * accumulate any number of bans/mutes without the User Info table + * reflecting it. + * + * Restricted to a whitelisted column name to keep the dynamic SQL + * safe; the caller passes a Permission-style constant. + */ + public static void bumpUserSettingCounter(int userId, String column) { + switch (column) { + case "cfh_warnings": + case "cfh_bans": + case "cfh_abusive": + case "tradelock_amount": + break; + default: + LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) { + statement.setInt(1, userId); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e); + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java index 4c901c46..21bd1393 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.modtool; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.modtool.ModToolManager; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.permissions.Permission; @@ -47,6 +48,8 @@ public class ModToolSanctionAlertEvent extends MessageHandler { } else { habbo.alert(message); } + + ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings"); } else { this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java index 1814041b..5a2c8afa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.modtool; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.modtool.ModToolBanType; +import com.eu.habbo.habbohotel.modtool.ModToolManager; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.modtool.ScripterManager; @@ -73,6 +74,7 @@ public class ModToolSanctionBanEvent extends MessageHandler { Emulator.getGameEnvironment().getModToolManager().ban(userId, this.client.getHabbo(), message, duration, ModToolBanType.ACCOUNT, cfhTopic); } + ModToolManager.bumpUserSettingCounter(userId, "cfh_bans"); } else { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java index e9356fdf..3be70cf6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.modtool; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.modtool.ModToolManager; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionLevelItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctions; @@ -59,6 +60,8 @@ public class ModToolSanctionMuteEvent extends MessageHandler { habbo.alert(message); this.client.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_mute.muted").replace("%user%", habbo.getHabboInfo().getUsername())); } + + ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings"); } else { this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java index 0cece3ce..d501eba5 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.modtool; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.modtool.ModToolManager; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.permissions.Permission; @@ -49,6 +50,8 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler { habbo.getHabboStats().setAllowTrade(false); habbo.alert(message); } + + ModToolManager.bumpUserSettingCounter(userId, "tradelock_amount"); } else { this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); }