From 458b37dbed0901696420a6741d4c748684ab0e08 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 28 May 2026 02:39:01 +0200 Subject: [PATCH] feat: rare values + fortune wheel + in-client prize editor Catalog-derived rare value map (diamond-priced), fortune wheel (WheelManager, weighted RNG, lazy daily reset, rewards, recent wins) + admin prize editor gated on acc_supporttool. Packets 9300-9305 / 9400-9404. Migration 020. --- .../020_fortune_wheel.sql | 58 ++++ .../eu/habbo/habbohotel/GameEnvironment.java | 7 + .../habbohotel/catalog/CatalogManager.java | 53 +++ .../habbo/habbohotel/wheel/WheelManager.java | 327 ++++++++++++++++++ .../eu/habbo/habbohotel/wheel/WheelPrize.java | 44 +++ .../habbohotel/wheel/WheelRecentWin.java | 14 + .../habbohotel/wheel/WheelUserState.java | 12 + .../com/eu/habbo/messages/PacketManager.java | 8 + .../eu/habbo/messages/incoming/Incoming.java | 8 + .../rarevalues/RequestRareValuesEvent.java | 21 ++ .../wheel/WheelAdminGetPrizesEvent.java | 23 ++ .../wheel/WheelAdminSavePrizesEvent.java | 45 +++ .../incoming/wheel/WheelBuySpinEvent.java | 27 ++ .../incoming/wheel/WheelOpenEvent.java | 27 ++ .../incoming/wheel/WheelSpinEvent.java | 39 +++ .../eu/habbo/messages/outgoing/Outgoing.java | 7 + .../rarevalues/RareValuesComposer.java | 35 ++ .../wheel/WheelAdminPrizesComposer.java | 34 ++ .../outgoing/wheel/WheelDataComposer.java | 46 +++ .../wheel/WheelRecentWinsComposer.java | 29 ++ .../outgoing/wheel/WheelResultComposer.java | 22 ++ 21 files changed, 886 insertions(+) create mode 100644 Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelPrize.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelRecentWin.java create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelUserState.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelBuySpinEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelOpenEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelSpinEvent.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelAdminPrizesComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelDataComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelRecentWinsComposer.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelResultComposer.java diff --git a/Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql b/Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql new file mode 100644 index 00000000..e158425c --- /dev/null +++ b/Database Updates/Own_Database_RunFirst/020_fortune_wheel.sql @@ -0,0 +1,58 @@ +-- 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. + +CREATE TABLE IF NOT EXISTS `wheel_prizes` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `type` VARCHAR(16) NOT NULL DEFAULT 'nothing', -- item | badge | credits | points | spin | nothing + `value` VARCHAR(64) NOT NULL DEFAULT '', -- item: base item id ; badge: badge code ; others: unused + `amount` INT(11) NOT NULL DEFAULT 1, -- item qty / credits / points / extra spins + `points_type` INT(11) NOT NULL DEFAULT 5, -- for type=points (diamond default 5) + `weight` INT(11) NOT NULL DEFAULT 1, -- relative probability + `label` VARCHAR(64) NOT NULL DEFAULT '', -- slice label override (optional) + `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, -- remaining free spins for the current day + `extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins + `last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400) + 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`), + KEY `idx_wheel_recent_wins_id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES + ('wheel.free_spins_per_day', '1', 'Fortune wheel: free spins granted each day.') + ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); +INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES + ('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.') + ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); +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); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index d8b8fe96..0a09636f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.achievements.AchievementManager; import com.eu.habbo.habbohotel.bots.BotManager; import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager; import com.eu.habbo.habbohotel.catalog.CatalogManager; +import com.eu.habbo.habbohotel.wheel.WheelManager; import com.eu.habbo.habbohotel.commands.CommandHandler; import com.eu.habbo.habbohotel.crafting.CraftingManager; import com.eu.habbo.habbohotel.guides.GuideManager; @@ -64,6 +65,7 @@ public class GameEnvironment { private GoogleTranslateManager googleTranslateManager; private CustomBadgeManager customBadgeManager; private InfostandBackgroundManager infostandBackgroundManager; + private WheelManager wheelManager; public void load() throws Exception { LOGGER.info("GameEnvironment -> Loading..."); @@ -93,6 +95,7 @@ public class GameEnvironment { this.googleTranslateManager = new GoogleTranslateManager(); this.customBadgeManager = new CustomBadgeManager(); this.infostandBackgroundManager = new InfostandBackgroundManager(); + this.wheelManager = new WheelManager(); this.roomManager.loadPublicRooms(); this.navigatorManager.loadNavigator(); @@ -156,6 +159,10 @@ public class GameEnvironment { return this.catalogManager; } + public WheelManager getWheelManager() { + return this.wheelManager; + } + public HotelViewManager getHotelViewManager() { return this.hotelViewManager; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 8ac07faf..76452042 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -202,6 +202,8 @@ public class CatalogManager { public final Item ecotronItem; public final THashMap limitedNumbers; private final List vouchers; + // spriteId -> [credits, points, pointsType], derived from catalog_items (see loadFurnitureValues) + public final TIntObjectMap furnitureValues; public CatalogManager() { long millis = System.currentTimeMillis(); @@ -219,6 +221,7 @@ public class CatalogManager { this.buildersClubOfferDefs = new TIntIntHashMap(); this.vouchers = new ArrayList<>(); this.limitedNumbers = new THashMap<>(); + this.furnitureValues = new TIntObjectHashMap<>(); this.initialize(); @@ -243,6 +246,56 @@ public class CatalogManager { this.loadClothing(); this.loadRecycler(); this.loadGiftWrappers(); + this.loadFurnitureValues(); + } + + // Builds spriteId -> [credits, points, pointsType] from catalog_items so the + // client can show a furni's "value" (toolbar price guide + infostand line). + // Only single-item, single-amount FLOOR/WALL sales are considered, so bundles + // and multi-packs don't pollute the per-rare price. First clean entry wins. + private synchronized void loadFurnitureValues() { + this.furnitureValues.clear(); + final int diamondType = Emulator.getConfig().getInt("seasonal.currency.diamond", 5); + + for (CatalogPage page : this.catalogPages.valueCollection()) { + for (CatalogItem catalogItem : page.getCatalogItems().valueCollection()) { + if (catalogItem.getAmount() != 1) + continue; + + int credits = catalogItem.getCredits(); + int points = catalogItem.getPoints(); + int pointsType = catalogItem.getPointsType(); + + // Only diamond-priced items — both the "Valore Rari" panel and the + // infostand value line show diamonds only. + if (points <= 0 || pointsType != diamondType) + continue; + + THashSet baseItems = catalogItem.getBaseItems(); + + if (baseItems.size() != 1) + continue; + + for (Item item : baseItems) { + FurnitureType type = item.getType(); + + if (type != FurnitureType.FLOOR && type != FurnitureType.WALL) + continue; + + int spriteId = item.getSpriteId(); + + if (spriteId > 0 && !this.furnitureValues.containsKey(spriteId)) { + this.furnitureValues.put(spriteId, new int[]{credits, points, pointsType}); + } + } + } + } + + LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size()); + } + + public TIntObjectMap getFurnitureValues() { + return this.furnitureValues; } private synchronized void loadLimitedNumbers() { 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 new file mode 100644 index 00000000..e5ccc316 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java @@ -0,0 +1,327 @@ +package com.eu.habbo.habbohotel.wheel; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboItem; +import gnu.trove.set.hash.THashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.concurrent.ThreadLocalRandom; + +public class WheelManager { + private static final Logger LOGGER = LoggerFactory.getLogger(WheelManager.class); + private static final int RECENT_KEEP = 50; + private static final int SECONDS_PER_DAY = 86400; + + private final List prizes = new ArrayList<>(); + private int totalWeight = 0; + private int freeSpinsPerDay = 1; + private int spinCost = 50; + private int spinCostType = 5; + + public WheelManager() { + long millis = System.currentTimeMillis(); + this.createTables(); + this.reload(); + LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis); + } + + 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); + } + } + + private void loadSettings() { + this.freeSpinsPerDay = Emulator.getConfig().getInt("wheel.free_spins_per_day", 1); + this.spinCost = Emulator.getConfig().getInt("wheel.spin_cost", 50); + this.spinCostType = Emulator.getConfig().getInt("wheel.spin_cost_type", 5); + } + + private void loadPrizes() { + this.prizes.clear(); + this.totalWeight = 0; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM wheel_prizes WHERE enabled = 1 ORDER BY sort_order ASC, id ASC"); + ResultSet set = statement.executeQuery()) { + while (set.next()) { + WheelPrize prize = new WheelPrize(set); + this.prizes.add(prize); + this.totalWeight += prize.weight; + } + } catch (SQLException e) { + LOGGER.error("Failed to load fortune wheel prizes", e); + } + } + + public List getPrizes() { + return this.prizes; + } + + public int getSpinCost() { + return this.spinCost; + } + + public int getSpinCostType() { + return this.spinCostType; + } + + private int today() { + 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) { + WheelUserState state = new WheelUserState(); + boolean exists = false; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT free_spins, extra_spins, last_reset FROM wheel_user_state WHERE user_id = ?")) { + statement.setInt(1, userId); + try (ResultSet set = statement.executeQuery()) { + if (set.next()) { + state.freeSpins = set.getInt("free_spins"); + state.extraSpins = set.getInt("extra_spins"); + state.lastReset = set.getInt("last_reset"); + exists = true; + } + } + } catch (SQLException e) { + LOGGER.error("Failed to read wheel state for user {}", userId, e); + } + + int today = this.today(); + if (!exists) { + state.freeSpins = this.freeSpinsPerDay; + state.extraSpins = 0; + state.lastReset = today; + this.persistUserState(userId, state); + } else if (state.lastReset != today) { + state.freeSpins = this.freeSpinsPerDay; + state.lastReset = today; + this.persistUserState(userId, state); + } + + return state; + } + + private void persistUserState(int userId, WheelUserState state) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO wheel_user_state (user_id, free_spins, extra_spins, last_reset) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE free_spins = VALUES(free_spins), extra_spins = VALUES(extra_spins), last_reset = VALUES(last_reset)")) { + statement.setInt(1, userId); + statement.setInt(2, state.freeSpins); + statement.setInt(3, state.extraSpins); + statement.setInt(4, state.lastReset); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to persist wheel state for user {}", userId, e); + } + } + + // 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(); + WheelUserState state = this.getUserState(userId); + + boolean usedFree; + if (state.freeSpins > 0) { + state.freeSpins--; + usedFree = true; + } else if (state.extraSpins > 0) { + state.extraSpins--; + usedFree = false; + } else { + return null; + } + + 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; + } + + private WheelPrize pickWeighted() { + if (this.prizes.isEmpty() || this.totalWeight <= 0) return null; + + int roll = ThreadLocalRandom.current().nextInt(this.totalWeight); + int acc = 0; + for (WheelPrize prize : this.prizes) { + acc += prize.weight; + if (roll < acc) return prize; + } + return this.prizes.get(this.prizes.size() - 1); + } + + private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) { + switch (prize.type) { + case "credits": + habbo.giveCredits(prize.amount); + break; + case "points": + habbo.givePoints(prize.pointsType, prize.amount); + break; + case "spin": + state.extraSpins += Math.max(0, prize.amount); + break; + case "item": + this.giveItem(habbo, prize); + break; + case "badge": + habbo.addBadge(prize.value, "Fortune Wheel"); + break; + case "nothing": + default: + break; + } + } + + private void giveItem(Habbo habbo, WheelPrize prize) { + int baseId; + try { + baseId = Integer.parseInt(prize.value.trim()); + } catch (NumberFormatException e) { + return; + } + + 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, ""); + if (item != null) items.add(item); + } + + if (!items.isEmpty()) { + habbo.addFurniture(items); + } + } + + private void recordWin(Habbo habbo, WheelPrize prize) { + 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 (?, ?, ?, ?, ?)")) { + statement.setInt(1, habbo.getHabboInfo().getId()); + statement.setString(2, habbo.getHabboInfo().getUsername()); + statement.setString(3, habbo.getHabboInfo().getLook()); + statement.setString(4, prize.label); + statement.setInt(5, Emulator.getIntUnixTimestamp()); + 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); + trim.executeUpdate(); + } + } catch (SQLException e) { + LOGGER.error("Failed to record wheel win", e); + } + } + + public List getRecentWins(int limit) { + List wins = new ArrayList<>(); + 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); + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + wins.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; + + if (this.spinCostType == -1) { + if (habbo.getHabboInfo().getCredits() < this.spinCost) return false; + habbo.giveCredits(-this.spinCost); + } else { + if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false; + 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) { + 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.setInt(4, pointsType); + statement.setInt(5, Math.max(0, weight)); + statement.setString(6, label != null ? label : ""); + statement.setInt(7, id); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Failed to save wheel prize {}", id, e); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelPrize.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelPrize.java new file mode 100644 index 00000000..7ed54cb6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelPrize.java @@ -0,0 +1,44 @@ +package com.eu.habbo.habbohotel.wheel; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.items.Item; + +import java.sql.ResultSet; +import java.sql.SQLException; + +// One slice of the wheel. type = item | badge | credits | points | spin | nothing. +public class WheelPrize { + public final int id; + public final String type; + public final String value; // item: base item id ; badge: badge code ; others: unused + public final int amount; // item qty / credits / points / extra spins + public final int pointsType; // for type=points + public final int weight; + public final String label; + public final int spriteId; // resolved for item prizes so the client can render the furni icon + + public WheelPrize(ResultSet set) throws SQLException { + this.id = set.getInt("id"); + this.type = set.getString("type"); + this.value = set.getString("value"); + this.amount = set.getInt("amount"); + this.pointsType = set.getInt("points_type"); + this.weight = Math.max(0, set.getInt("weight")); + this.label = set.getString("label"); + this.spriteId = resolveSpriteId(this.type, this.value); + } + + private static int resolveSpriteId(String type, String value) { + if (!"item".equals(type) || value == null) return 0; + try { + Item item = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(value.trim())); + return item != null ? item.getSpriteId() : 0; + } catch (NumberFormatException e) { + return 0; + } + } + + public String badgeCode() { + return "badge".equals(this.type) && this.value != null ? this.value : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelRecentWin.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelRecentWin.java new file mode 100644 index 00000000..03d43816 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelRecentWin.java @@ -0,0 +1,14 @@ +package com.eu.habbo.habbohotel.wheel; + +// A row in the "latest winners" panel. Denormalized (username/look stored at win time). +public class WheelRecentWin { + public final String username; + public final String look; + public final String prizeLabel; + + public WheelRecentWin(String username, String look, String prizeLabel) { + this.username = username != null ? username : ""; + this.look = look != null ? look : ""; + this.prizeLabel = prizeLabel != null ? prizeLabel : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelUserState.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelUserState.java new file mode 100644 index 00000000..f6522a52 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelUserState.java @@ -0,0 +1,12 @@ +package com.eu.habbo.habbohotel.wheel; + +// Per-user spin balance. freeSpins resets daily (lazy, on access); extraSpins persist. +public class WheelUserState { + public int freeSpins; + public int extraSpins; + public int lastReset; // day index (unix / 86400) of the last daily reset + + public int totalSpins() { + return this.freeSpins + this.extraSpins; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index a5e9b84a..7fbfec63 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -745,5 +745,13 @@ public class PacketManager { this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class); this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class); this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class); + + this.registerHandler(Incoming.RequestRareValuesEvent, com.eu.habbo.messages.incoming.rarevalues.RequestRareValuesEvent.class); + + this.registerHandler(Incoming.WheelOpenEvent, com.eu.habbo.messages.incoming.wheel.WheelOpenEvent.class); + this.registerHandler(Incoming.WheelSpinEvent, com.eu.habbo.messages.incoming.wheel.WheelSpinEvent.class); + this.registerHandler(Incoming.WheelBuySpinEvent, com.eu.habbo.messages.incoming.wheel.WheelBuySpinEvent.class); + this.registerHandler(Incoming.WheelAdminGetPrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminGetPrizesEvent.class); + this.registerHandler(Incoming.WheelAdminSavePrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminSavePrizesEvent.class); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 4222bdfa..52b32bb2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -486,4 +486,12 @@ public class Incoming { public static final int HousekeepingSendHotelAlertEvent = 9121; public static final int HousekeepingGetDashboardEvent = 9122; public static final int HousekeepingListActionLogEvent = 9123; + + // Custom features — IDs 9300+ reserved + public static final int RequestRareValuesEvent = 9300; + public static final int WheelOpenEvent = 9301; + public static final int WheelSpinEvent = 9302; + public static final int WheelBuySpinEvent = 9303; + public static final int WheelAdminGetPrizesEvent = 9304; + public static final int WheelAdminSavePrizesEvent = 9305; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java new file mode 100644 index 00000000..117043a4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java @@ -0,0 +1,21 @@ +package com.eu.habbo.messages.incoming.rarevalues; + +import com.eu.habbo.Emulator; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.rarevalues.RareValuesComposer; + +// Client requests the furni value map once on load. Public info (catalog prices), +// no permission gate. Rate limited since the payload is large. +public class RequestRareValuesEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 5000; + } + + @Override + public void handle() throws Exception { + this.client.sendResponse(new RareValuesComposer( + Emulator.getGameEnvironment().getCatalogManager().getFurnitureValues() + )); + } +} 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 new file mode 100644 index 00000000..a8b4e23a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java @@ -0,0 +1,23 @@ +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 { + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { + return; + } + + this.client.sendResponse(new WheelAdminPrizesComposer( + Emulator.getGameEnvironment().getWheelManager().getPrizes())); + } +} 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 new file mode 100644 index 00000000..d63ab6cb --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java @@ -0,0 +1,45 @@ +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 { + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { + return; + } + + WheelManager wheel = Emulator.getGameEnvironment().getWheelManager(); + + int count = this.packet.readInt(); + for (int i = 0; i < count; i++) { + int id = this.packet.readInt(); + String type = this.packet.readString(); + String value = this.packet.readString(); + int amount = this.packet.readInt(); + 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()), + wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelBuySpinEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelBuySpinEvent.java new file mode 100644 index 00000000..c0e585e6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelBuySpinEvent.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.incoming.wheel; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wheel.WheelManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer; + +public class WheelBuySpinEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + WheelManager wheel = Emulator.getGameEnvironment().getWheelManager(); + wheel.buySpin(habbo); // whether or not it succeeds, resend the balance + + this.client.sendResponse(new WheelDataComposer( + wheel.getUserState(habbo.getHabboInfo().getId()), + wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes())); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelOpenEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelOpenEvent.java new file mode 100644 index 00000000..876ab96f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelOpenEvent.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.incoming.wheel; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wheel.WheelManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer; +import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer; + +public class WheelOpenEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + WheelManager wheel = Emulator.getGameEnvironment().getWheelManager(); + this.client.sendResponse(new WheelDataComposer( + wheel.getUserState(habbo.getHabboInfo().getId()), + wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes())); + this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50))); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelSpinEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelSpinEvent.java new file mode 100644 index 00000000..286de1f3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelSpinEvent.java @@ -0,0 +1,39 @@ +package com.eu.habbo.messages.incoming.wheel; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.wheel.WheelManager; +import com.eu.habbo.habbohotel.wheel.WheelPrize; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer; +import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer; +import com.eu.habbo.messages.outgoing.wheel.WheelResultComposer; + +public class WheelSpinEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1500; + } + + @Override + public void handle() throws Exception { + Habbo habbo = this.client.getHabbo(); + if (habbo == null) return; + + WheelManager wheel = Emulator.getGameEnvironment().getWheelManager(); + WheelPrize prize = wheel.spin(habbo); + + if (prize != null) { + this.client.sendResponse(new WheelResultComposer(prize.id)); + } + + // Refresh the balance either way so the client unlocks the wheel. + this.client.sendResponse(new WheelDataComposer( + wheel.getUserState(habbo.getHabboInfo().getId()), + wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes())); + + if (prize != null) { + this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50))); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 3e6bf2ec..6eb8c3c6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -594,4 +594,11 @@ public class Outgoing { public static final int HousekeepingDashboardComposer = 9204; public static final int HousekeepingActionLogComposer = 9205; + // Custom features — IDs 9400+ reserved + public static final int RareValuesComposer = 9400; + public static final int WheelDataComposer = 9401; + public static final int WheelResultComposer = 9402; + public static final int WheelRecentWinsComposer = 9403; + public static final int WheelAdminPrizesComposer = 9404; + } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java new file mode 100644 index 00000000..f713c8c9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java @@ -0,0 +1,35 @@ +package com.eu.habbo.messages.outgoing.rarevalues; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; +import gnu.trove.iterator.TIntObjectIterator; +import gnu.trove.map.TIntObjectMap; + +// Sends the full spriteId -> value map to the client. Consumed by the toolbar +// price guide and the furni infostand "value" line. See CatalogManager#loadFurnitureValues. +public class RareValuesComposer extends MessageComposer { + private final TIntObjectMap values; + + public RareValuesComposer(TIntObjectMap values) { + this.values = values; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.RareValuesComposer); + this.response.appendInt(this.values.size()); + + TIntObjectIterator iterator = this.values.iterator(); + while (iterator.hasNext()) { + iterator.advance(); + int[] value = iterator.value(); + this.response.appendInt(iterator.key()); // spriteId + this.response.appendInt(value[0]); // credits + this.response.appendInt(value[1]); // points + this.response.appendInt(value[2]); // pointsType + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelAdminPrizesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelAdminPrizesComposer.java new file mode 100644 index 00000000..22196ac3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelAdminPrizesComposer.java @@ -0,0 +1,34 @@ +package com.eu.habbo.messages.outgoing.wheel; + +import com.eu.habbo.habbohotel.wheel.WheelPrize; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +// Raw editable prize list for the in-client admin editor (sends value/amount/ +// pointsType as stored, unlike WheelDataComposer which resolves icons for players). +public class WheelAdminPrizesComposer extends MessageComposer { + private final List prizes; + + public WheelAdminPrizesComposer(List prizes) { + this.prizes = prizes; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WheelAdminPrizesComposer); + this.response.appendInt(this.prizes.size()); + for (WheelPrize prize : this.prizes) { + this.response.appendInt(prize.id); + this.response.appendString(prize.type); + this.response.appendString(prize.value == null ? "" : prize.value); + this.response.appendInt(prize.amount); + this.response.appendInt(prize.pointsType); + this.response.appendInt(prize.weight); + this.response.appendString(prize.label == null ? "" : prize.label); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelDataComposer.java new file mode 100644 index 00000000..486c4a5c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelDataComposer.java @@ -0,0 +1,46 @@ +package com.eu.habbo.messages.outgoing.wheel; + +import com.eu.habbo.habbohotel.wheel.WheelPrize; +import com.eu.habbo.habbohotel.wheel.WheelUserState; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +// User spin balance + cost + the full prize list (one entry per slice). +public class WheelDataComposer extends MessageComposer { + private final WheelUserState state; + private final int spinCost; + private final int spinCostType; + private final List prizes; + + public WheelDataComposer(WheelUserState state, int spinCost, int spinCostType, List prizes) { + this.state = state; + this.spinCost = spinCost; + this.spinCostType = spinCostType; + this.prizes = prizes; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WheelDataComposer); + this.response.appendInt(this.state.freeSpins); + this.response.appendInt(this.state.extraSpins); + this.response.appendInt(this.spinCost); + this.response.appendInt(this.spinCostType); + + this.response.appendInt(this.prizes.size()); + for (WheelPrize prize : this.prizes) { + this.response.appendInt(prize.id); + this.response.appendString(prize.type); + this.response.appendInt(prize.spriteId); // item only, else 0 + this.response.appendString(prize.badgeCode()); // badge only, else "" + this.response.appendInt(prize.amount); + this.response.appendInt(prize.pointsType); + this.response.appendString(prize.label == null ? "" : prize.label); + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelRecentWinsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelRecentWinsComposer.java new file mode 100644 index 00000000..b4eac3e7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelRecentWinsComposer.java @@ -0,0 +1,29 @@ +package com.eu.habbo.messages.outgoing.wheel; + +import com.eu.habbo.habbohotel.wheel.WheelRecentWin; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +import java.util.List; + +// "Latest winners" list: username + look (for the headshot) + prize label. +public class WheelRecentWinsComposer extends MessageComposer { + private final List wins; + + public WheelRecentWinsComposer(List wins) { + this.wins = wins; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WheelRecentWinsComposer); + this.response.appendInt(this.wins.size()); + for (WheelRecentWin win : this.wins) { + this.response.appendString(win.username); + this.response.appendString(win.look); + this.response.appendString(win.prizeLabel); + } + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelResultComposer.java new file mode 100644 index 00000000..76aeeed2 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/wheel/WheelResultComposer.java @@ -0,0 +1,22 @@ +package com.eu.habbo.messages.outgoing.wheel; + +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +// The winning prize id. The client animates the wheel to that slice; the reward +// was already granted server-side. +public class WheelResultComposer extends MessageComposer { + private final int prizeId; + + public WheelResultComposer(int prizeId) { + this.prizeId = prizeId; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.WheelResultComposer); + this.response.appendInt(this.prizeId); + return this.response; + } +}