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.
This commit is contained in:
medievalshell
2026-05-28 02:39:01 +02:00
parent 55b38e7b85
commit 458b37dbed
21 changed files with 886 additions and 0 deletions
@@ -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','<base_item_id>',1,5,'Raro',1);
-- INSERT INTO wheel_prizes (type, value, amount, weight, label, sort_order) VALUES ('badge','<BADGE_CODE>',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);
@@ -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;
}
@@ -202,6 +202,8 @@ public class CatalogManager {
public final Item ecotronItem;
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
private final List<Voucher> vouchers;
// spriteId -> [credits, points, pointsType], derived from catalog_items (see loadFurnitureValues)
public final TIntObjectMap<int[]> 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<Item> 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<int[]> getFurnitureValues() {
return this.furnitureValues;
}
private synchronized void loadLimitedNumbers() {
@@ -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<WheelPrize> 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<WheelPrize> 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<HabboItem> 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<WheelRecentWin> getRecentWins(int limit) {
List<WheelRecentWin> 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);
}
}
}
@@ -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 : "";
}
}
@@ -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 : "";
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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()
));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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)));
}
}
@@ -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)));
}
}
}
@@ -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;
}
@@ -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<int[]> values;
public RareValuesComposer(TIntObjectMap<int[]> values) {
this.values = values;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.RareValuesComposer);
this.response.appendInt(this.values.size());
TIntObjectIterator<int[]> 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;
}
}
@@ -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<WheelPrize> prizes;
public WheelAdminPrizesComposer(List<WheelPrize> 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;
}
}
@@ -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<WheelPrize> prizes;
public WheelDataComposer(WheelUserState state, int spinCost, int spinCostType, List<WheelPrize> 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;
}
}
@@ -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<WheelRecentWin> wins;
public WheelRecentWinsComposer(List<WheelRecentWin> 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;
}
}
@@ -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;
}
}