Merge pull request #129 from medievalshell/Dev

feat: rare values + fortune wheel + in-client prize editor + feat: soundboard (room-scoped custom audio pads) + feat: version string tied to project version + "Extended" title
This commit is contained in:
DuckieTM
2026-05-28 13:50:52 +02:00
committed by GitHub
32 changed files with 1160 additions and 2 deletions
+2 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.12</version>
<version>4.2.24</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -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;
}
@@ -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,327 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class WheelManager {
private static final Logger LOGGER = LoggerFactory.getLogger(WheelManager.class);
private static final int RECENT_KEEP = 50;
private static final int SECONDS_PER_DAY = 86400;
private final List<WheelPrize> prizes = new ArrayList<>();
private int totalWeight = 0;
private int freeSpinsPerDay = 1;
private int spinCost = 50;
private int spinCostType = 5;
public WheelManager() {
long millis = System.currentTimeMillis();
this.createTables();
this.reload();
LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
}
public void reload() {
this.loadSettings();
this.loadPrizes();
}
private void createTables() {
final String[] ddl = {
"CREATE TABLE IF NOT EXISTS `wheel_prizes` (" +
"`id` INT(11) NOT NULL AUTO_INCREMENT, `type` VARCHAR(16) NOT NULL DEFAULT 'nothing', " +
"`value` VARCHAR(64) NOT NULL DEFAULT '', `amount` INT(11) NOT NULL DEFAULT 1, " +
"`points_type` INT(11) NOT NULL DEFAULT 5, `weight` INT(11) NOT NULL DEFAULT 1, " +
"`label` VARCHAR(64) NOT NULL DEFAULT '', `enabled` TINYINT(1) NOT NULL DEFAULT 1, " +
"`sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS `wheel_user_state` (" +
"`user_id` INT(11) NOT NULL, `free_spins` INT(11) NOT NULL DEFAULT 0, " +
"`extra_spins` INT(11) NOT NULL DEFAULT 0, `last_reset` INT(11) NOT NULL DEFAULT 0, " +
"PRIMARY KEY (`user_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS `wheel_recent_wins` (" +
"`id` INT(11) NOT NULL AUTO_INCREMENT, `user_id` INT(11) NOT NULL, " +
"`username` VARCHAR(64) NOT NULL DEFAULT '', `look` VARCHAR(255) NOT NULL DEFAULT '', " +
"`prize_label` VARCHAR(64) NOT NULL DEFAULT '', `won_at` INT(11) NOT NULL DEFAULT 0, " +
"PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
};
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
Statement statement = connection.createStatement()) {
for (String query : ddl) {
statement.execute(query);
}
} catch (SQLException e) {
LOGGER.error("Failed to create fortune wheel tables", e);
}
}
private void loadSettings() {
this.freeSpinsPerDay = Emulator.getConfig().getInt("wheel.free_spins_per_day", 1);
this.spinCost = Emulator.getConfig().getInt("wheel.spin_cost", 50);
this.spinCostType = Emulator.getConfig().getInt("wheel.spin_cost_type", 5);
}
private void loadPrizes() {
this.prizes.clear();
this.totalWeight = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM wheel_prizes WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
WheelPrize prize = new WheelPrize(set);
this.prizes.add(prize);
this.totalWeight += prize.weight;
}
} catch (SQLException e) {
LOGGER.error("Failed to load fortune wheel prizes", e);
}
}
public List<WheelPrize> getPrizes() {
return this.prizes;
}
public int getSpinCost() {
return this.spinCost;
}
public int getSpinCostType() {
return this.spinCostType;
}
private int today() {
return Emulator.getIntUnixTimestamp() / SECONDS_PER_DAY;
}
// Reads the user's spin balance, applying the lazy daily reset and creating the row if missing.
public WheelUserState getUserState(int userId) {
WheelUserState state = new WheelUserState();
boolean exists = false;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT free_spins, extra_spins, last_reset FROM wheel_user_state WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
state.freeSpins = set.getInt("free_spins");
state.extraSpins = set.getInt("extra_spins");
state.lastReset = set.getInt("last_reset");
exists = true;
}
}
} catch (SQLException e) {
LOGGER.error("Failed to read wheel state for user {}", userId, e);
}
int today = this.today();
if (!exists) {
state.freeSpins = this.freeSpinsPerDay;
state.extraSpins = 0;
state.lastReset = today;
this.persistUserState(userId, state);
} else if (state.lastReset != today) {
state.freeSpins = this.freeSpinsPerDay;
state.lastReset = today;
this.persistUserState(userId, state);
}
return state;
}
private void persistUserState(int userId, WheelUserState state) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_user_state (user_id, free_spins, extra_spins, last_reset) VALUES (?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE free_spins = VALUES(free_spins), extra_spins = VALUES(extra_spins), last_reset = VALUES(last_reset)")) {
statement.setInt(1, userId);
statement.setInt(2, state.freeSpins);
statement.setInt(3, state.extraSpins);
statement.setInt(4, state.lastReset);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist wheel state for user {}", userId, e);
}
}
// Consumes a spin (free first, then extra), picks a weighted prize, grants it and records the win.
// Returns the prize, or null if the user has no spins or no prizes are configured.
public synchronized WheelPrize spin(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
WheelUserState state = this.getUserState(userId);
boolean usedFree;
if (state.freeSpins > 0) {
state.freeSpins--;
usedFree = true;
} else if (state.extraSpins > 0) {
state.extraSpins--;
usedFree = false;
} else {
return null;
}
WheelPrize prize = this.pickWeighted();
if (prize == null) {
// No prizes configured — refund the spin we just consumed.
if (usedFree) state.freeSpins++; else state.extraSpins++;
return null;
}
this.giveReward(habbo, prize, state);
this.persistUserState(userId, state);
// Record every spin (including "nothing") so the live feed shows all activity.
this.recordWin(habbo, prize);
return prize;
}
private WheelPrize pickWeighted() {
if (this.prizes.isEmpty() || this.totalWeight <= 0) return null;
int roll = ThreadLocalRandom.current().nextInt(this.totalWeight);
int acc = 0;
for (WheelPrize prize : this.prizes) {
acc += prize.weight;
if (roll < acc) return prize;
}
return this.prizes.get(this.prizes.size() - 1);
}
private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) {
switch (prize.type) {
case "credits":
habbo.giveCredits(prize.amount);
break;
case "points":
habbo.givePoints(prize.pointsType, prize.amount);
break;
case "spin":
state.extraSpins += Math.max(0, prize.amount);
break;
case "item":
this.giveItem(habbo, prize);
break;
case "badge":
habbo.addBadge(prize.value, "Fortune Wheel");
break;
case "nothing":
default:
break;
}
}
private void giveItem(Habbo habbo, WheelPrize prize) {
int baseId;
try {
baseId = Integer.parseInt(prize.value.trim());
} catch (NumberFormatException e) {
return;
}
Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId);
if (base == null) return;
int quantity = Math.max(1, prize.amount);
THashSet<HabboItem> items = new THashSet<>();
for (int i = 0; i < quantity; i++) {
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), base, 0, 0, "");
if (item != null) items.add(item);
}
if (!items.isEmpty()) {
habbo.addFurniture(items);
}
}
private void recordWin(Habbo habbo, WheelPrize prize) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_recent_wins (user_id, username, look, prize_label, won_at) VALUES (?, ?, ?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, habbo.getHabboInfo().getUsername());
statement.setString(3, habbo.getHabboInfo().getLook());
statement.setString(4, prize.label);
statement.setInt(5, Emulator.getIntUnixTimestamp());
statement.executeUpdate();
}
// Trim to the most recent RECENT_KEEP rows.
try (PreparedStatement trim = connection.prepareStatement(
"DELETE FROM wheel_recent_wins WHERE id < (SELECT id FROM (SELECT id FROM wheel_recent_wins ORDER BY id DESC LIMIT 1 OFFSET ?) t)")) {
trim.setInt(1, RECENT_KEEP - 1);
trim.executeUpdate();
}
} catch (SQLException e) {
LOGGER.error("Failed to record wheel win", e);
}
}
public List<WheelRecentWin> getRecentWins(int limit) {
List<WheelRecentWin> wins = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT username, look, prize_label FROM wheel_recent_wins ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
wins.add(new WheelRecentWin(set.getString("username"), set.getString("look"), set.getString("prize_label")));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load wheel recent wins", e);
}
return wins;
}
// Buys one extra spin with the configured currency. Returns false if the user can't afford it.
public synchronized boolean buySpin(Habbo habbo) {
if (this.spinCost <= 0) return false;
if (this.spinCostType == -1) {
if (habbo.getHabboInfo().getCredits() < this.spinCost) return false;
habbo.giveCredits(-this.spinCost);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false;
habbo.givePoints(this.spinCostType, -this.spinCost);
}
int userId = habbo.getHabboInfo().getId();
WheelUserState state = this.getUserState(userId);
state.extraSpins++;
this.persistUserState(userId, state);
return true;
}
// Admin: update one prize row. Caller reloads once after a batch.
public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) {
statement.setString(1, type != null ? type : "nothing");
statement.setString(2, value != null ? value : "");
statement.setInt(3, amount);
statement.setInt(4, pointsType);
statement.setInt(5, Math.max(0, weight));
statement.setString(6, label != null ? label : "");
statement.setInt(7, id);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
}
}
}
@@ -0,0 +1,44 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import java.sql.ResultSet;
import java.sql.SQLException;
// One slice of the wheel. type = item | badge | credits | points | spin | nothing.
public class WheelPrize {
public final int id;
public final String type;
public final String value; // item: base item id ; badge: badge code ; others: unused
public final int amount; // item qty / credits / points / extra spins
public final int pointsType; // for type=points
public final int weight;
public final String label;
public final int spriteId; // resolved for item prizes so the client can render the furni icon
public WheelPrize(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.type = set.getString("type");
this.value = set.getString("value");
this.amount = set.getInt("amount");
this.pointsType = set.getInt("points_type");
this.weight = Math.max(0, set.getInt("weight"));
this.label = set.getString("label");
this.spriteId = resolveSpriteId(this.type, this.value);
}
private static int resolveSpriteId(String type, String value) {
if (!"item".equals(type) || value == null) return 0;
try {
Item item = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(value.trim()));
return item != null ? item.getSpriteId() : 0;
} catch (NumberFormatException e) {
return 0;
}
}
public String badgeCode() {
return "badge".equals(this.type) && this.value != null ? this.value : "";
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.wheel;
// A row in the "latest winners" panel. Denormalized (username/look stored at win time).
public class WheelRecentWin {
public final String username;
public final String look;
public final String prizeLabel;
public WheelRecentWin(String username, String look, String prizeLabel) {
this.username = username != null ? username : "";
this.look = look != null ? look : "";
this.prizeLabel = prizeLabel != null ? prizeLabel : "";
}
}
@@ -0,0 +1,12 @@
package com.eu.habbo.habbohotel.wheel;
// Per-user spin balance. freeSpins resets daily (lazy, on access); extraSpins persist.
public class WheelUserState {
public int freeSpins;
public int extraSpins;
public int lastReset; // day index (unix / 86400) of the last daily reset
public int totalSpins() {
return this.freeSpins + this.extraSpins;
}
}
@@ -745,5 +745,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()
));
}
}
@@ -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,23 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
public class WheelAdminGetPrizesEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
return;
}
this.client.sendResponse(new WheelAdminPrizesComposer(
Emulator.getGameEnvironment().getWheelManager().getPrizes()));
}
}
@@ -0,0 +1,45 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
public class WheelAdminSavePrizesEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) {
return;
}
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
int count = this.packet.readInt();
for (int i = 0; i < count; i++) {
int id = this.packet.readInt();
String type = this.packet.readString();
String value = this.packet.readString();
int amount = this.packet.readInt();
int pointsType = this.packet.readInt();
int weight = this.packet.readInt();
String label = this.packet.readString();
wheel.savePrize(id, type, value, amount, pointsType, weight, label);
}
wheel.reload();
// Send the refreshed admin list + the player view so the editor updates live.
this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(this.client.getHabbo().getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
public class WheelBuySpinEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
wheel.buySpin(habbo); // whether or not it succeeds, resend the balance
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
public class WheelOpenEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
}
}
@@ -0,0 +1,39 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.habbohotel.wheel.WheelPrize;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelResultComposer;
public class WheelSpinEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1500;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
WheelPrize prize = wheel.spin(habbo);
if (prize != null) {
this.client.sendResponse(new WheelResultComposer(prize.id));
}
// Refresh the balance either way so the client unlocks the wheel.
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
if (prize != null) {
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
}
}
}
@@ -594,4 +594,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;
}
}