Merge pull request #135 from duckietm/dev

Dev
This commit is contained in:
DuckieTM
2026-05-28 16:36:43 +02:00
committed by GitHub
34 changed files with 1257 additions and 19 deletions
@@ -0,0 +1,78 @@
-- 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.
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`);
INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`) VALUES
('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`);
+1
View File
@@ -38,6 +38,7 @@
<archive>
<manifest>
<mainClass>com.eu.habbo.Emulator</mainClass>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
@@ -39,12 +39,23 @@ public final class Emulator {
private static final String OS_NAME = (System.getProperty("os.name") != null ? System.getProperty("os.name") : "Unknown");
private static final String CLASS_PATH = (System.getProperty("java.class.path") != null ? System.getProperty("java.class.path") : "Unknown");
// Fallback version, only used when running outside a packaged jar (e.g. from
// the IDE). In production the version comes from the jar manifest below.
public final static int MAJOR = 4;
public final static int MINOR = 1;
public final static int BUILD = 0;
public final static String PREVIEW = "";
public static final String version = "Arcturus Morningstar" + " " + MAJOR + "." + MINOR + "." + BUILD + " " + PREVIEW;
// Tied to the Maven project version: read from the jar manifest
// (Implementation-Version = ${project.version}, see pom assembly plugin).
private static String resolveVersionNumber() {
String implementation = Emulator.class.getPackage().getImplementationVersion();
if (implementation != null && !implementation.isEmpty()) return implementation;
String fallback = MAJOR + "." + MINOR + "." + BUILD;
return PREVIEW.isEmpty() ? fallback : fallback + " " + PREVIEW;
}
public static final String version = "Arcturus Morningstar Extended " + resolveVersionNumber();
private static final String logo =
"\n" +
"███╗ ███╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ███████╗████████╗ █████╗ ██████╗ \n" +
@@ -6,6 +6,8 @@ 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.soundboard.SoundboardManager;
import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.crafting.CraftingManager;
import com.eu.habbo.habbohotel.guides.GuideManager;
@@ -64,6 +66,8 @@ public class GameEnvironment {
private GoogleTranslateManager googleTranslateManager;
private CustomBadgeManager customBadgeManager;
private InfostandBackgroundManager infostandBackgroundManager;
private WheelManager wheelManager;
private SoundboardManager soundboardManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -93,6 +97,8 @@ public class GameEnvironment {
this.googleTranslateManager = new GoogleTranslateManager();
this.customBadgeManager = new CustomBadgeManager();
this.infostandBackgroundManager = new InfostandBackgroundManager();
this.wheelManager = new WheelManager();
this.soundboardManager = new SoundboardManager();
this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -156,6 +162,14 @@ public class GameEnvironment {
return this.catalogManager;
}
public WheelManager getWheelManager() {
return this.wheelManager;
}
public SoundboardManager getSoundboardManager() {
return this.soundboardManager;
}
public HotelViewManager getHotelViewManager() {
return this.hotelViewManager;
}
@@ -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
}
}
@@ -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;
@@ -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() {
@@ -197,6 +197,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
private int wiredInspectMask = WIRED_ACCESS_DEFAULT_INSPECT_MASK;
private int wiredModifyMask = WIRED_ACCESS_DEFAULT_MODIFY_MASK;
private boolean youtubeEnabled = false;
private boolean soundboardEnabled = false;
private String youtubeCurrentVideo = "";
private String youtubeSenderName = "";
private final java.util.List<String> youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -204,6 +205,8 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
public boolean isYoutubeEnabled() { return this.youtubeEnabled; }
public void setYoutubeEnabled(boolean enabled) { this.youtubeEnabled = enabled; }
public boolean isSoundboardEnabled() { return this.soundboardEnabled; }
public void setSoundboardEnabled(boolean enabled) { this.soundboardEnabled = enabled; }
public String getYoutubeCurrentVideo() { return this.youtubeCurrentVideo; }
public String getYoutubeSenderName() { return this.youtubeSenderName; }
public java.util.List<String> getYoutubePlaylist() { return this.youtubePlaylist; }
@@ -250,6 +253,7 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
this.allowWalkthrough = set.getBoolean("allow_walkthrough");
this.hideWall = set.getBoolean("allow_hidewall");
try { this.youtubeEnabled = set.getBoolean("youtube_enabled"); } catch (Exception e) { this.youtubeEnabled = false; }
try { this.soundboardEnabled = set.getBoolean("soundboard_enabled"); } catch (Exception e) { this.soundboardEnabled = false; }
this.chatMode = set.getInt("chat_mode");
this.chatWeight = set.getInt("chat_weight");
this.chatSpeed = set.getInt("chat_speed");
@@ -1020,6 +1020,10 @@ public class RoomManager {
room.getYoutubeWatchers()).compose());
}
habbo.getClient().sendResponse(new com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer(
room.isSoundboardEnabled(),
Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
WiredManager.triggerUserEntersRoom(room, habbo.getRoomUnit());
room.habboEntered(habbo);
@@ -0,0 +1,78 @@
package com.eu.habbo.habbohotel.soundboard;
import com.eu.habbo.Emulator;
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;
public class SoundboardManager {
private static final Logger LOGGER = LoggerFactory.getLogger(SoundboardManager.class);
private final List<SoundboardSound> sounds = new ArrayList<>();
public SoundboardManager() {
long millis = System.currentTimeMillis();
this.bootstrap();
this.reload();
LOGGER.info("Soundboard Manager -> Loaded! ({} MS, {} sounds)", System.currentTimeMillis() - millis, this.sounds.size());
}
// Self-bootstrap: room flag column + sounds table, so the feature works even
// before the manual migration is applied.
private void bootstrap() {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement()) {
statement.execute("ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0");
statement.execute("CREATE TABLE IF NOT EXISTS `soundboard_sounds` (" +
"`id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '', " +
"`url` VARCHAR(255) 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");
} catch (SQLException e) {
LOGGER.error("Failed to bootstrap soundboard schema", e);
}
}
public void reload() {
this.sounds.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id, name, url FROM soundboard_sounds WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.sounds.add(new SoundboardSound(set));
}
} catch (SQLException e) {
LOGGER.error("Failed to load soundboard sounds", e);
}
}
public List<SoundboardSound> getSounds() {
return this.sounds;
}
public SoundboardSound getSound(int id) {
for (SoundboardSound sound : this.sounds) {
if (sound.id == id) return sound;
}
return null;
}
// Owner toggle — persists the room flag with a dedicated UPDATE (kept out of
// the big room-settings save to avoid touching that statement).
public void setRoomEnabled(int roomId, boolean enabled) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET soundboard_enabled = ? WHERE id = ? LIMIT 1")) {
statement.setString(1, enabled ? "1" : "0");
statement.setInt(2, roomId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to set soundboard_enabled for room {}", roomId, e);
}
}
}
@@ -0,0 +1,17 @@
package com.eu.habbo.habbohotel.soundboard;
import java.sql.ResultSet;
import java.sql.SQLException;
// One soundboard pad: a named audio clip served from a URL (uploaded via the CMS).
public class SoundboardSound {
public final int id;
public final String name;
public final String url;
public SoundboardSound(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.name = set.getString("name");
this.url = set.getString("url");
}
}
@@ -0,0 +1,366 @@
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.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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;
public static final Set<String> 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<WheelPrize> prizes = new ArrayList<>();
private int totalWeight = 0;
private int freeSpinsPerDay = 1;
private int spinCost = 50;
private int spinCostType = 5;
private final ConcurrentHashMap<Integer, Long> lastSpinAt = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, WheelUserState> userStateCache = new ConcurrentHashMap<>();
private final java.util.concurrent.CopyOnWriteArrayList<WheelRecentWin> recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>();
public WheelManager() {
long millis = System.currentTimeMillis();
this.reload();
LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
}
public void reload() {
this.loadSettings();
this.loadPrizes();
this.loadRecentWins();
}
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;
}
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;
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);
}
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);
}
this.userStateCache.put(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);
}
}
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;
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) {
if (usedFree) state.freeSpins++; else state.extraSpins++;
return null;
}
this.giveReward(habbo, prize, state);
this.persistUserState(userId, state);
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) {
int amount = Math.max(0, Math.min(prize.amount, MAX_PRIZE_AMOUNT));
switch (prize.type) {
case "credits":
if (amount > 0) habbo.giveCredits(amount);
break;
case "points":
if (amount > 0) habbo.givePoints(prize.pointsType, amount);
break;
case "spin":
int room = Math.max(0, MAX_EXTRA_SPINS - state.extraSpins);
state.extraSpins += Math.min(amount, room);
break;
case "item":
this.giveItem(habbo, prize, Math.min(amount, MAX_ITEM_QUANTITY));
break;
case "badge":
if (prize.value != null && !prize.value.isEmpty()) {
habbo.addBadge(prize.value, "Fortune Wheel");
}
break;
case "nothing":
default:
break;
}
}
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());
} catch (NumberFormatException e) {
return;
}
Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId);
if (base == null) return;
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) {
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 (?, ?, ?, ?, ?)")) {
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();
}
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) {
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, RECENT_KEEP);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
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);
}
}
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);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false;
habbo.givePoints(this.spinCostType, -this.spinCost);
}
state.extraSpins++;
this.persistUserState(userId, state);
return true;
}
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, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
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;
}
}
@@ -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,16 @@ 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);
this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class);
}
}
@@ -486,4 +486,14 @@ 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;
public static final int SoundboardPlayEvent = 9306;
public static final int SoundboardSetEnabledEvent = 9307;
}
@@ -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()
));
}
}
@@ -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);
}
@@ -0,0 +1,31 @@
package com.eu.habbo.messages.incoming.soundboard;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.soundboard.SoundboardSound;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.soundboard.SoundboardPlayComposer;
public class SoundboardPlayEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 250;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
Room room = this.currentRoom();
if (room == null || !room.isSoundboardEnabled()) return;
int soundId = this.packet.readInt();
SoundboardSound sound = Emulator.getGameEnvironment().getSoundboardManager().getSound(soundId);
if (sound == null) return;
// Broadcast to everyone in the room.
room.sendComposer(new SoundboardPlayComposer(sound.id, sound.url, habbo.getHabboInfo().getUsername()).compose());
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.soundboard;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer;
public class SoundboardSetEnabledEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
Room room = this.currentRoom();
if (room == null) return;
// Only the room owner (or staff) may toggle the soundboard for the room.
boolean isOwner = room.getOwnerId() == habbo.getHabboInfo().getId();
if (!isOwner && !habbo.hasPermission(Permission.ACC_SUPPORTTOOL)) return;
boolean enabled = this.packet.readInt() == 1;
room.setSoundboardEnabled(enabled);
Emulator.getGameEnvironment().getSoundboardManager().setRoomEnabled(room.getId(), enabled);
// Push the refreshed settings (flag + sound list) to everyone in the room
// so the toolbar icon appears/disappears live.
room.sendComposer(new SoundboardSettingsComposer(enabled, Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
}
}
@@ -0,0 +1,24 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
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;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) {
return;
}
this.client.sendResponse(new WheelAdminPrizesComposer(
Emulator.getGameEnvironment().getWheelManager().getPrizes()));
}
}
@@ -0,0 +1,46 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
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;
}
@Override
public void handle() throws Exception {
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();
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();
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,13 @@ 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;
public static final int SoundboardSettingsComposer = 9405;
public static final int SoundboardPlayComposer = 9406;
}
@@ -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,27 @@
package com.eu.habbo.messages.outgoing.soundboard;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
// Broadcast to everyone in the room when a pad is pressed they all play the clip.
public class SoundboardPlayComposer extends MessageComposer {
private final int soundId;
private final String url;
private final String username;
public SoundboardPlayComposer(int soundId, String url, String username) {
this.soundId = soundId;
this.url = url != null ? url : "";
this.username = username != null ? username : "";
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.SoundboardPlayComposer);
this.response.appendInt(this.soundId);
this.response.appendString(this.url);
this.response.appendString(this.username);
return this.response;
}
}
@@ -0,0 +1,33 @@
package com.eu.habbo.messages.outgoing.soundboard;
import com.eu.habbo.habbohotel.soundboard.SoundboardSound;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
import java.util.List;
// Sent on room enter (and on toggle): whether the soundboard is active in this
// room + the available pads. The client shows the toolbar icon only if enabled.
public class SoundboardSettingsComposer extends MessageComposer {
private final boolean enabled;
private final List<SoundboardSound> sounds;
public SoundboardSettingsComposer(boolean enabled, List<SoundboardSound> sounds) {
this.enabled = enabled;
this.sounds = sounds;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.SoundboardSettingsComposer);
this.response.appendBoolean(this.enabled);
this.response.appendInt(this.sounds.size());
for (SoundboardSound sound : this.sounds) {
this.response.appendInt(sound.id);
this.response.appendString(sound.name);
this.response.appendString(sound.url);
}
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;
}
}