diff --git a/Database Updates/008_soundboard_fortune_wheel.sql b/Database Updates/008_soundboard_fortune_wheel.sql new file mode 100644 index 00000000..b5dba5ba --- /dev/null +++ b/Database Updates/008_soundboard_fortune_wheel.sql @@ -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`); diff --git a/Emulator/pom.xml b/Emulator/pom.xml index 1f4ed186..ff8e57c4 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -38,6 +38,7 @@ com.eu.habbo.Emulator + true diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 1a9fe716..de1a4e86 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -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" + 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..8fdb7842 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,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; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java index 9123c338..d373cfa5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/Bot.java @@ -189,11 +189,7 @@ public class Bot implements Runnable { int timeOut = Emulator.getRandom().nextInt(20) * 2; this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp()); } - }/* else { - for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) { - WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray()); - } - }*/ + } } if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) { @@ -218,7 +214,7 @@ public class Bot implements Runnable { } else { this.lastChatIndex++; if (this.lastChatIndex >= this.chatLines.size()) { - this.lastChatIndex = 0; // start from scratch :-3 + this.lastChatIndex = 0; } } @@ -310,9 +306,6 @@ public class Bot implements Runnable { public void setName(String name) { this.name = name; this.needsUpdate = true; - - //if(this.room != null) - //this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose()); } public String getMotto() { @@ -539,9 +532,28 @@ public class Bot implements Runnable { } } - private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11}; + + public static final int ACTION_ROTATE = 11; + + private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L; + + private volatile long lastOwnerActionAt; public short[] getOwnerActionIds() { return DEFAULT_OWNER_ACTION_IDS; } + + public synchronized boolean tryAcquireOwnerActionSlot() { + long now = System.currentTimeMillis(); + if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) { + return false; + } + this.lastOwnerActionAt = now; + return true; + } + + public void onPostOwnerAction(int actionId) { + // no-op default + } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java index a373f0fc..2a844cb9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/bots/FrankBot.java @@ -23,7 +23,6 @@ import java.util.regex.Pattern; public class FrankBot extends ButlerBot { private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class); - public static final String BOT_TYPE = "frank"; public static final String PERMISSION_USE = "acc_bot_frank"; private static final String KEY_DOOR_LINES = "__door_lines"; @@ -75,13 +74,20 @@ public class FrankBot extends ButlerBot { } } - private static final short[] FRANK_OWNER_ACTIONS = new short[0]; + private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE }; @Override public short[] getOwnerActionIds() { return FRANK_OWNER_ACTIONS; } + @Override + public void onPostOwnerAction(int actionId) { + if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) { + this.homeRotation = this.getRoomUnit().getBodyRotation(); + } + } + public static void initialise() { chatResponses.clear(); doorLines = DEFAULT_DOOR_LINES; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 3ea4b00b..c89bf959 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/rooms/Room.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java index 8d693678..0e4f2d87 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java @@ -197,6 +197,7 @@ public class Room implements Comparable, 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 youtubePlaylist = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -204,6 +205,8 @@ public class Room implements Comparable, 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 getYoutubePlaylist() { return this.youtubePlaylist; } @@ -250,6 +253,7 @@ public class Room implements Comparable, 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"); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java index a9699d0f..dd3a29e6 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomManager.java @@ -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); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java new file mode 100644 index 00000000..d263af39 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardManager.java @@ -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 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 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); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java new file mode 100644 index 00000000..71a388ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/soundboard/SoundboardSound.java @@ -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"); + } +} 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..ba577dc4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wheel/WheelManager.java @@ -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 VALID_PRIZE_TYPES = Set.of( + "credits", "points", "spin", "item", "badge", "nothing"); + public static final int MAX_PRIZES_PER_SAVE = 64; + public static final int MAX_STRING_LEN = 64; + public static final int MAX_PRIZE_AMOUNT = 1_000_000; + public static final int MAX_ITEM_QUANTITY = 100; + public static final int MAX_WEIGHT = 1_000_000; + public static final int MAX_EXTRA_SPINS = 10_000; + private static final long MIN_SPIN_INTERVAL_MS = 1500L; + + private final List prizes = new ArrayList<>(); + private int totalWeight = 0; + private int freeSpinsPerDay = 1; + private int spinCost = 50; + private int spinCostType = 5; + + private final ConcurrentHashMap lastSpinAt = new ConcurrentHashMap<>(); + private final ConcurrentHashMap userStateCache = new ConcurrentHashMap<>(); + private final java.util.concurrent.CopyOnWriteArrayList recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>(); + + public WheelManager() { + long millis = System.currentTimeMillis(); + this.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 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 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 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; + } +} 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..0b54d15e 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,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); } } 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..9d372215 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,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; } 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/rooms/bots/BotSaveSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java index 6d7c8d21..23a99af3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/bots/BotSaveSettingsEvent.java @@ -5,11 +5,13 @@ import com.eu.habbo.habbohotel.bots.Bot; import com.eu.habbo.habbohotel.bots.BotManager; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomUserRotation; import com.eu.habbo.habbohotel.users.DanceType; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BotErrorComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDanceComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUserNameChangedComposer; +import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer; import com.eu.habbo.messages.outgoing.rooms.users.RoomUsersComposer; import com.eu.habbo.plugin.events.bots.BotSavedChatEvent; import com.eu.habbo.plugin.events.bots.BotSavedLookEvent; @@ -28,16 +30,20 @@ public class BotSaveSettingsEvent extends MessageHandler { if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) { int botId = this.packet.readInt(); - Bot bot = room.getBot(Math.abs(botId)); - if (bot == null) return; - - if (bot.getOwnerActionIds().length == 0) - return; - int settingId = this.packet.readInt(); + boolean allowed = false; + for (short a : bot.getOwnerActionIds()) { + if (a == settingId) { + allowed = true; + break; + } + } + if (!allowed) return; + + if (!bot.tryAcquireOwnerActionSlot()) return; switch (settingId) { case 1: @@ -163,8 +169,18 @@ public class BotSaveSettingsEvent extends MessageHandler { bot.needsUpdate(true); room.sendComposer(new RoomUsersComposer(bot).compose()); break; + case Bot.ACTION_ROTATE: + if (bot.getRoomUnit() == null) break; + int next = (bot.getRoomUnit().getBodyRotation().getValue() + 2) % 8; + RoomUserRotation rotation = RoomUserRotation.fromValue(next); + bot.getRoomUnit().setRotation(rotation); + bot.needsUpdate(true); + room.sendComposer(new RoomUserStatusComposer(bot.getRoomUnit()).compose()); + break; } + bot.onPostOwnerAction(settingId); + if (bot.needsUpdate()) { Emulator.getThreading().run(bot); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java new file mode 100644 index 00000000..eb227160 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardPlayEvent.java @@ -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()); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java new file mode 100644 index 00000000..ab507007 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/soundboard/SoundboardSetEnabledEvent.java @@ -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()); + } +} 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..e6aee93c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminGetPrizesEvent.java @@ -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())); + } +} 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..3dbf9235 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wheel/WheelAdminSavePrizesEvent.java @@ -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())); + } +} 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..882365a4 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,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; + } 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/soundboard/SoundboardPlayComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardPlayComposer.java new file mode 100644 index 00000000..745205ad --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardPlayComposer.java @@ -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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java new file mode 100644 index 00000000..7293e01f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/soundboard/SoundboardSettingsComposer.java @@ -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 sounds; + + public SoundboardSettingsComposer(boolean enabled, List 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; + } +} 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; + } +}