From 9705b3e42a0d26045f1f5dbc750db39d458391e4 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 28 May 2026 13:00:02 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=86=95=20Added=20the=20option=20turn?= =?UTF-8?q?=20in=20menu=20for=20BOT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/eu/habbo/habbohotel/bots/Bot.java | 32 +++++++++++++------ .../eu/habbo/habbohotel/bots/FrankBot.java | 10 ++++-- .../rooms/bots/BotSaveSettingsEvent.java | 28 ++++++++++++---- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java index 9123c338..d373cfa5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java @@ -189,11 +189,7 @@ public class Bot implements Runnable { int timeOut = Emulator.getRandom().nextInt(20) * 2; this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp()); } - }/* else { - for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) { - WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray()); - } - }*/ + } } if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) { @@ -218,7 +214,7 @@ public class Bot implements Runnable { } else { this.lastChatIndex++; if (this.lastChatIndex >= this.chatLines.size()) { - this.lastChatIndex = 0; // start from scratch :-3 + this.lastChatIndex = 0; } } @@ -310,9 +306,6 @@ public class Bot implements Runnable { public void setName(String name) { this.name = name; this.needsUpdate = true; - - //if(this.room != null) - //this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose()); } public String getMotto() { @@ -539,9 +532,28 @@ public class Bot implements Runnable { } } - private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11}; + + public static final int ACTION_ROTATE = 11; + + private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L; + + private volatile long lastOwnerActionAt; public short[] getOwnerActionIds() { return DEFAULT_OWNER_ACTION_IDS; } + + public synchronized boolean tryAcquireOwnerActionSlot() { + long now = System.currentTimeMillis(); + if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) { + return false; + } + this.lastOwnerActionAt = now; + return true; + } + + public void onPostOwnerAction(int actionId) { + // no-op default + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java index a373f0fc..2a844cb9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java @@ -23,7 +23,6 @@ import java.util.regex.Pattern; public class FrankBot extends ButlerBot { private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class); - public static final String BOT_TYPE = "frank"; public static final String PERMISSION_USE = "acc_bot_frank"; private static final String KEY_DOOR_LINES = "__door_lines"; @@ -75,13 +74,20 @@ public class FrankBot extends ButlerBot { } } - private static final short[] FRANK_OWNER_ACTIONS = new short[0]; + private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE }; @Override public short[] getOwnerActionIds() { return FRANK_OWNER_ACTIONS; } + @Override + public void onPostOwnerAction(int actionId) { + if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) { + this.homeRotation = this.getRoomUnit().getBodyRotation(); + } + } + public static void initialise() { chatResponses.clear(); doorLines = DEFAULT_DOOR_LINES; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java index 6d7c8d21..23a99af3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java @@ -5,11 +5,13 @@ import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.bots.BotManager; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BotErrorComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDanceComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserNameChangedComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUsersComposer; import com.eu.habbo.plugin.events.bots.BotSavedChatEvent; import com.eu.habbo.plugin.events.bots.BotSavedLookEvent; @@ -28,16 +30,20 @@ public class BotSaveSettingsEvent extends MessageHandler { if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) { int botId = this.packet.readInt(); - Bot bot = room.getBot(Math.abs(botId)); - if (bot == null) return; - - if (bot.getOwnerActionIds().length == 0) - return; - int settingId = this.packet.readInt(); + boolean allowed = false; + for (short a : bot.getOwnerActionIds()) { + if (a == settingId) { + allowed = true; + break; + } + } + if (!allowed) return; + + if (!bot.tryAcquireOwnerActionSlot()) return; switch (settingId) { case 1: @@ -163,8 +169,18 @@ public class BotSaveSettingsEvent extends MessageHandler { bot.needsUpdate(true); room.sendComposer(new RoomUsersComposer(bot).compose()); break; + case Bot.ACTION_ROTATE: + if (bot.getRoomUnit() == null) break; + int next = (bot.getRoomUnit().getBodyRotation().getValue() + 2) % 8; + RoomUserRotation rotation = RoomUserRotation.fromValue(next); + bot.getRoomUnit().setRotation(rotation); + bot.needsUpdate(true); + room.sendComposer(new RoomUserStatusComposer(bot.getRoomUnit()).compose()); + break; } + bot.onPostOwnerAction(settingId); + if (bot.needsUpdate()) { Emulator.getThreading().run(bot); } From 1ba2e43d4d185fee589c089bb691af7f0dafa3da Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 28 May 2026 16:36:22 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=86=99=20Wheel=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...l.sql => 008_soundboard_fortune_wheel.sql} | 42 +++-- .../Own_Database_RunFirst/021_soundboard.sql | 15 -- .../habbo/habbohotel/wheel/WheelManager.java | 159 +++++++++++------- .../wheel/WheelAdminGetPrizesEvent.java | 5 +- .../wheel/WheelAdminSavePrizesEvent.java | 9 +- 5 files changed, 138 insertions(+), 92 deletions(-) rename Database Updates/{Own_Database_RunFirst/020_fortune_wheel.sql => 008_soundboard_fortune_wheel.sql} (67%) delete mode 100644 Database Updates/Own_Database_RunFirst/021_soundboard.sql diff --git a/Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql b/Database Updates/008_soundboard_fortune_wheel.sql similarity index 67% rename from Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql rename to Database Updates/008_soundboard_fortune_wheel.sql index e158425c..b5dba5ba 100644 --- a/Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql +++ b/Database Updates/008_soundboard_fortune_wheel.sql @@ -1,3 +1,18 @@ +-- Soundboard +-- The room flag column + sounds table are also created at boot by + +ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0; + +CREATE TABLE IF NOT EXISTS `soundboard_sounds` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client + `url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges) + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + -- Fortune Wheel -- Tables are also created at boot by WheelManager (CREATE TABLE IF NOT EXISTS), -- so applying this file is only needed to seed prizes + settings. @@ -44,15 +59,20 @@ INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES ('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).') ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); --- Example prizes (currency / spin / nothing don't reference furniture ids). --- Add `item`/`badge` rows with your own ids: e.g. --- INSERT INTO wheel_prizes (type, value, amount, weight, label, sort_order) VALUES ('item','',1,5,'Raro',1); --- INSERT INTO wheel_prizes (type, value, amount, weight, label, sort_order) VALUES ('badge','',1,5,'Distintivo',2); + INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`) VALUES - ('points', 25, 5, 20, '25 diamanti', 10), - ('points', 50, 5, 12, '50 diamanti', 11), - ('points', 200, 5, 3, '200 diamanti', 12), - ('credits', 100, 0, 15, '100 crediti', 13), - ('spin', 1, 0, 15, '1 Giro Extra', 14), - ('spin', 2, 0, 6, '2 Giri Extra', 15), - ('nothing', 0, 0, 29, 'Nulla', 16); + ('points',25, 5, 20, '25 diamonds',1), + ('points',50, 5, 12, '50 diamonds',2), + ('points',200, 5, 3, '200 diamonds',3), + ('credits',100, 0, 15, '100 credits',4), + ('spin',1, 0, 15, '1 Extra spin', 5), + ('spin',2, 0, 6, '2 Extra spins',6), + ('nothing',0, 0, 29, 'Oh to bad!',7); + +INSERT INTO `permission_definitions` + (`permission_key`, `max_value`, `comment`, + `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) +VALUES + ('acc_wheeladmin', 1, 'Required to open the Fortune Wheel settings popup and edit prize rows.', + 0, 0, 0, 0, 0, 0, 1) +ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); diff --git a/Database Updates/Own_Database_RunFirst/021_soundboard.sql b/Database Updates/Own_Database_RunFirst/021_soundboard.sql deleted file mode 100644 index 0cb9e167..00000000 --- a/Database Updates/Own_Database_RunFirst/021_soundboard.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Soundboard --- The room flag column + sounds table are also created at boot by --- SoundboardManager (ALTER ... ADD COLUMN IF NOT EXISTS / CREATE TABLE IF NOT --- EXISTS), so applying this file is only needed to seed sounds up-front. - -ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0; - -CREATE TABLE IF NOT EXISTS `soundboard_sounds` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client - `url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges) - `enabled` TINYINT(1) NOT NULL DEFAULT 1, - `sort_order` INT(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java index e5ccc316..ba577dc4 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java @@ -12,9 +12,10 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; public class WheelManager { @@ -22,15 +23,28 @@ public class WheelManager { private static final int RECENT_KEEP = 50; private static final int SECONDS_PER_DAY = 86400; + public static final Set VALID_PRIZE_TYPES = Set.of( + "credits", "points", "spin", "item", "badge", "nothing"); + public static final int MAX_PRIZES_PER_SAVE = 64; + public static final int MAX_STRING_LEN = 64; + public static final int MAX_PRIZE_AMOUNT = 1_000_000; + public static final int MAX_ITEM_QUANTITY = 100; + public static final int MAX_WEIGHT = 1_000_000; + public static final int MAX_EXTRA_SPINS = 10_000; + private static final long MIN_SPIN_INTERVAL_MS = 1500L; + private final List prizes = new ArrayList<>(); private int totalWeight = 0; private int freeSpinsPerDay = 1; private int spinCost = 50; private int spinCostType = 5; + private final ConcurrentHashMap lastSpinAt = new ConcurrentHashMap<>(); + private final ConcurrentHashMap userStateCache = new ConcurrentHashMap<>(); + private final java.util.concurrent.CopyOnWriteArrayList recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>(); + public WheelManager() { long millis = System.currentTimeMillis(); - this.createTables(); this.reload(); LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis); } @@ -38,35 +52,7 @@ public class WheelManager { public void reload() { this.loadSettings(); this.loadPrizes(); - } - - private void createTables() { - final String[] ddl = { - "CREATE TABLE IF NOT EXISTS `wheel_prizes` (" + - "`id` INT(11) NOT NULL AUTO_INCREMENT, `type` VARCHAR(16) NOT NULL DEFAULT 'nothing', " + - "`value` VARCHAR(64) NOT NULL DEFAULT '', `amount` INT(11) NOT NULL DEFAULT 1, " + - "`points_type` INT(11) NOT NULL DEFAULT 5, `weight` INT(11) NOT NULL DEFAULT 1, " + - "`label` VARCHAR(64) NOT NULL DEFAULT '', `enabled` TINYINT(1) NOT NULL DEFAULT 1, " + - "`sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "CREATE TABLE IF NOT EXISTS `wheel_user_state` (" + - "`user_id` INT(11) NOT NULL, `free_spins` INT(11) NOT NULL DEFAULT 0, " + - "`extra_spins` INT(11) NOT NULL DEFAULT 0, `last_reset` INT(11) NOT NULL DEFAULT 0, " + - "PRIMARY KEY (`user_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", - "CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (" + - "`id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, " + - "`username` VARCHAR(64) NOT NULL DEFAULT '', `look` VARCHAR(255) NOT NULL DEFAULT '', " + - "`prize_label` VARCHAR(64) NOT NULL DEFAULT '', `won_at` INT(11) NOT NULL DEFAULT 0, " + - "PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - }; - - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); - Statement statement = connection.createStatement()) { - for (String query : ddl) { - statement.execute(query); - } - } catch (SQLException e) { - LOGGER.error("Failed to create fortune wheel tables", e); - } + this.loadRecentWins(); } private void loadSettings() { @@ -108,8 +94,19 @@ public class WheelManager { return Emulator.getIntUnixTimestamp() / SECONDS_PER_DAY; } - // Reads the user's spin balance, applying the lazy daily reset and creating the row if missing. - public WheelUserState getUserState(int userId) { + public synchronized WheelUserState getUserState(int userId) { + int today = this.today(); + WheelUserState cached = this.userStateCache.get(userId); + + if (cached != null) { + if (cached.lastReset != today) { + cached.freeSpins = this.freeSpinsPerDay; + cached.lastReset = today; + this.persistUserState(userId, cached); + } + return cached; + } + WheelUserState state = new WheelUserState(); boolean exists = false; @@ -128,7 +125,6 @@ public class WheelManager { LOGGER.error("Failed to read wheel state for user {}", userId, e); } - int today = this.today(); if (!exists) { state.freeSpins = this.freeSpinsPerDay; state.extraSpins = 0; @@ -140,6 +136,7 @@ public class WheelManager { this.persistUserState(userId, state); } + this.userStateCache.put(userId, state); return state; } @@ -158,10 +155,13 @@ public class WheelManager { } } - // Consumes a spin (free first, then extra), picks a weighted prize, grants it and records the win. - // Returns the prize, or null if the user has no spins or no prizes are configured. public synchronized WheelPrize spin(Habbo habbo) { int userId = habbo.getHabboInfo().getId(); + long now = System.currentTimeMillis(); + Long last = this.lastSpinAt.get(userId); + if (last != null && (now - last) < MIN_SPIN_INTERVAL_MS) return null; + this.lastSpinAt.put(userId, now); + WheelUserState state = this.getUserState(userId); boolean usedFree; @@ -177,15 +177,12 @@ public class WheelManager { WheelPrize prize = this.pickWeighted(); if (prize == null) { - // No prizes configured — refund the spin we just consumed. if (usedFree) state.freeSpins++; else state.extraSpins++; return null; } this.giveReward(habbo, prize, state); this.persistUserState(userId, state); - - // Record every spin (including "nothing") so the live feed shows all activity. this.recordWin(habbo, prize); return prize; @@ -204,21 +201,26 @@ public class WheelManager { } private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) { + int amount = Math.max(0, Math.min(prize.amount, MAX_PRIZE_AMOUNT)); + switch (prize.type) { case "credits": - habbo.giveCredits(prize.amount); + if (amount > 0) habbo.giveCredits(amount); break; case "points": - habbo.givePoints(prize.pointsType, prize.amount); + if (amount > 0) habbo.givePoints(prize.pointsType, amount); break; case "spin": - state.extraSpins += Math.max(0, prize.amount); + int room = Math.max(0, MAX_EXTRA_SPINS - state.extraSpins); + state.extraSpins += Math.min(amount, room); break; case "item": - this.giveItem(habbo, prize); + this.giveItem(habbo, prize, Math.min(amount, MAX_ITEM_QUANTITY)); break; case "badge": - habbo.addBadge(prize.value, "Fortune Wheel"); + if (prize.value != null && !prize.value.isEmpty()) { + habbo.addBadge(prize.value, "Fortune Wheel"); + } break; case "nothing": default: @@ -226,7 +228,9 @@ public class WheelManager { } } - private void giveItem(Habbo habbo, WheelPrize prize) { + private void giveItem(Habbo habbo, WheelPrize prize, int quantity) { + if (quantity <= 0 || prize.value == null) return; + int baseId; try { baseId = Integer.parseInt(prize.value.trim()); @@ -237,7 +241,6 @@ public class WheelManager { Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId); if (base == null) return; - int quantity = Math.max(1, prize.amount); THashSet items = new THashSet<>(); for (int i = 0; i < quantity; i++) { HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), base, 0, 0, ""); @@ -250,6 +253,16 @@ public class WheelManager { } private void recordWin(Habbo habbo, WheelPrize prize) { + WheelRecentWin win = new WheelRecentWin( + habbo.getHabboInfo().getUsername(), + habbo.getHabboInfo().getLook(), + prize.label); + + this.recentWinsCache.add(0, win); + while (this.recentWinsCache.size() > RECENT_KEEP) { + this.recentWinsCache.remove(this.recentWinsCache.size() - 1); + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { try (PreparedStatement statement = connection.prepareStatement( "INSERT INTO wheel_recent_wins (user_id, username, look, prize_label, won_at) VALUES (?, ?, ?, ?, ?)")) { @@ -261,7 +274,6 @@ public class WheelManager { statement.executeUpdate(); } - // Trim to the most recent RECENT_KEEP rows. try (PreparedStatement trim = connection.prepareStatement( "DELETE FROM wheel_recent_wins WHERE id < (SELECT id FROM (SELECT id FROM wheel_recent_wins ORDER BY id DESC LIMIT 1 OFFSET ?) t)")) { trim.setInt(1, RECENT_KEEP - 1); @@ -273,25 +285,38 @@ public class WheelManager { } public List getRecentWins(int limit) { - List wins = new ArrayList<>(); + if (limit <= 0) return new ArrayList<>(); + int size = this.recentWinsCache.size(); + if (size == 0) return new ArrayList<>(); + int take = Math.min(limit, size); + return new ArrayList<>(this.recentWinsCache.subList(0, take)); + } + + private void loadRecentWins() { + this.recentWinsCache.clear(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT username, look, prize_label FROM wheel_recent_wins ORDER BY id DESC LIMIT ?")) { - statement.setInt(1, limit); + statement.setInt(1, RECENT_KEEP); try (ResultSet set = statement.executeQuery()) { while (set.next()) { - wins.add(new WheelRecentWin(set.getString("username"), set.getString("look"), set.getString("prize_label"))); + this.recentWinsCache.add(new WheelRecentWin( + set.getString("username"), + set.getString("look"), + set.getString("prize_label"))); } } } catch (SQLException e) { LOGGER.error("Failed to load wheel recent wins", e); } - return wins; } - // Buys one extra spin with the configured currency. Returns false if the user can't afford it. public synchronized boolean buySpin(Habbo habbo) { if (this.spinCost <= 0) return false; + int userId = habbo.getHabboInfo().getId(); + WheelUserState state = this.getUserState(userId); + if (state.extraSpins >= MAX_EXTRA_SPINS) return false; + if (this.spinCostType == -1) { if (habbo.getHabboInfo().getCredits() < this.spinCost) return false; habbo.giveCredits(-this.spinCost); @@ -300,28 +325,42 @@ public class WheelManager { habbo.givePoints(this.spinCostType, -this.spinCost); } - int userId = habbo.getHabboInfo().getId(); - WheelUserState state = this.getUserState(userId); state.extraSpins++; this.persistUserState(userId, state); return true; } - // Admin: update one prize row. Caller reloads once after a batch. public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) { + String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing"; + String safeValue = truncate(value, MAX_STRING_LEN); + String safeLabel = truncate(label, MAX_STRING_LEN); + int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT); + int safeWeight = clamp(weight, 0, MAX_WEIGHT); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( "UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) { - statement.setString(1, type != null ? type : "nothing"); - statement.setString(2, value != null ? value : ""); - statement.setInt(3, amount); + statement.setString(1, safeType); + statement.setString(2, safeValue); + statement.setInt(3, safeAmount); statement.setInt(4, pointsType); - statement.setInt(5, Math.max(0, weight)); - statement.setString(6, label != null ? label : ""); + statement.setInt(5, safeWeight); + statement.setString(6, safeLabel); statement.setInt(7, id); statement.executeUpdate(); } catch (SQLException e) { LOGGER.error("Failed to save wheel prize {}", id, e); } } + + private static String truncate(String s, int max) { + if (s == null) return ""; + return s.length() <= max ? s : s.substring(0, max); + } + + private static int clamp(int value, int min, int max) { + if (value < min) return min; + if (value > max) return max; + return value; + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java index a8b4e23a..e6aee93c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java @@ -1,11 +1,12 @@ package com.eu.habbo.messages.incoming.wheel; import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer; public class WheelAdminGetPrizesEvent extends MessageHandler { + public static final String PERMISSION_KEY = "acc_wheeladmin"; + @Override public int getRatelimit() { return 500; @@ -13,7 +14,7 @@ public class WheelAdminGetPrizesEvent extends MessageHandler { @Override public void handle() throws Exception { - if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { + if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java index d63ab6cb..3dbf9235 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java @@ -1,13 +1,14 @@ package com.eu.habbo.messages.incoming.wheel; import com.eu.habbo.Emulator; -import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.wheel.WheelManager; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer; import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer; public class WheelAdminSavePrizesEvent extends MessageHandler { + public static final String PERMISSION_KEY = "acc_wheeladmin"; + @Override public int getRatelimit() { return 1000; @@ -15,13 +16,15 @@ public class WheelAdminSavePrizesEvent extends MessageHandler { @Override public void handle() throws Exception { - if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { + if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) { return; } WheelManager wheel = Emulator.getGameEnvironment().getWheelManager(); int count = this.packet.readInt(); + if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return; + for (int i = 0; i < count; i++) { int id = this.packet.readInt(); String type = this.packet.readString(); @@ -30,13 +33,11 @@ public class WheelAdminSavePrizesEvent extends MessageHandler { int pointsType = this.packet.readInt(); int weight = this.packet.readInt(); String label = this.packet.readString(); - wheel.savePrize(id, type, value, amount, pointsType, weight, label); } wheel.reload(); - // Send the refreshed admin list + the player view so the editor updates live. this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes())); this.client.sendResponse(new WheelDataComposer( wheel.getUserState(this.client.getHabbo().getHabboInfo().getId()), From f8fe1e3e22cfc9bd8c68d4de8ec4cdd66ab2ef45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 14:37:58 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=86=99=20Bump=20version=20to=204.2.25?= =?UTF-8?q?=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emulator/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emulator/pom.xml b/Emulator/pom.xml index ff8e57c4..88b0e6a3 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.2.24 + 4.2.25 UTF-8