Compare commits

..

22 Commits

Author SHA1 Message Date
github-actions[bot] bf1a29a6e8 🆙 Bump version to 4.2.26 [skip ci] 2026-05-30 05:53:48 +00:00
DuckieTM 6391d721ff Merge pull request #136 from duckietm/dev
Dev
2026-05-30 07:52:43 +02:00
DuckieTM dfea6bcf83 🆙 Updated SQL 2026-05-30 07:52:02 +02:00
DuckieTM a7f207bb76 Merge pull request #134 from medievalshell/Dev
feat: persist `scale` for room ads / branding furni
2026-05-30 07:13:59 +02:00
duckietm b7915884b6 🆙 Update Rare-Value page 2026-05-29 08:28:01 +02:00
medievalshell 478f7bdba0 feat/fix: RCON wheel+soundboard reload, robust SSO reconnect behind Cloudflare
- RCON: add updatewheel/updatesoundboard (reload WheelManager/SoundboardManager live) so the CMS admin pages apply changes without an emulator restart.

- SSO ticket is no longer single-use: loadHabbo, session-resume and performFullDisconnect no longer clear auth_ticket. Behind Cloudflare the WS is dropped and the client retries with the same ticket; clearing it caused 'non-existing SSO token' and the 'refresh twice' / kicked-on-reconnect symptoms. The ticket now lives until its TTL (auth_ticket_expires_at), is overwritten by the CMS on the next /client load, or cleared on logout.

- SessionResume: restoreSsoTicket only restores when auth_ticket is empty (don't clobber a fresh CMS ticket); GameClient.dispose only parks/disconnects when the habbo is still attached to this client (a fast reconnect may have re-attached it to the new connection).
2026-05-29 04:45:34 +02:00
medievalshell c255f1e1b4 fix: guard RoomBundleLayout against null RoomManager during catalog init
CatalogManager.loadFurnitureValues() (rare-values feature) iterates every catalog page during GameEnvironment.load(); for a RoomBundleLayout this calls getRoomManager().loadRoom(), but RoomManager is constructed after CatalogManager so getRoomManager() returns null -> NullPointerException -> boot aborts. Null-guard the room load so the bundle resolves lazily at runtime instead.
2026-05-29 00:45:02 +02:00
medievalshell 9c831a9da4 feat: grant acc_wheeladmin to staff ranks for the wheel prize editor
The wheel prize editor is gated on acc_wheeladmin (client Settings button +
server WheelAdmin{Get,Save}PrizesEvent). Upstream 008_soundboard_fortune_wheel
registers the key but only grants rank_7 (its 7-rank hotel). This portable,
idempotent migration grants it to the same ranks as acc_ads_background via
dynamic SQL over the per-rank columns — no hardcoded rank ids. Apply then
:update_permissions or restart.
2026-05-28 22:47:15 +02:00
Medievalshell 08d1ae97a7 Merge branch 'duckietm:main' into Dev 2026-05-28 22:16:17 +02:00
github-actions[bot] f8fe1e3e22 🆙 Bump version to 4.2.25 [skip ci] 2026-05-28 14:37:58 +00:00
DuckieTM be77cdf4aa Merge pull request #135 from duckietm/dev
Dev
2026-05-28 16:36:43 +02:00
duckietm 1ba2e43d4d 🆙 Wheel updates 2026-05-28 16:36:22 +02:00
medievalshell 8dd5155562 feat: persist scale for room ads / branding furni
InteractionRoomAds now carries a `scale` default value (100) alongside
imageUrl/clickUrl/offsetX/Y/Z, so the image zoom set in the client's
position editor is stored and broadcast like the other branding fields.
2026-05-28 15:30:33 +02:00
DuckieTM 4f4f581371 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
2026-05-28 13:50:52 +02:00
duckietm 9705b3e42a 🆕 Added the option turn in menu for BOT 2026-05-28 13:00:02 +02:00
medievalshell e626a7fc50 feat: version string tied to project version + "Extended" title
The :about / :info hotel-info title was hardcoded ("Arcturus Morningstar
4.1.0") and drifted from the real build. Now Emulator.version reads the
jar manifest's Implementation-Version (= ${project.version}, added via the
assembly plugin) and falls back to MAJOR.MINOR.BUILD only outside a jar.
Title becomes "Arcturus Morningstar Extended <version>" (e.g. 4.2.24).
2026-05-28 12:33:50 +02:00
Medievalshell d6ebb632e6 Merge branch 'duckietm:main' into Dev 2026-05-28 12:11:27 +02:00
github-actions[bot] 014ca9ca48 🆙 Bump version to 4.2.24 [skip ci] 2026-05-28 09:50:45 +00:00
DuckieTM d189d66f9e Merge pull request #133 from duckietm/dev
🆙 Update effects
2026-05-28 11:49:38 +02:00
duckietm c272a36cc5 🆙 Update effects 2026-05-28 11:49:20 +02:00
medievalshell 10a2b2b872 feat: soundboard (room-scoped custom audio pads)
Server side of the soundboard feature:
- rooms.soundboard_enabled flag + soundboard_sounds table (self-bootstraps
  at boot via SoundboardManager; migration 021 seeds up-front)
- SoundboardManager loads enabled sounds and persists the per-room flag
- SoundboardPlayEvent broadcasts the pressed pad to everyone in the room
- SoundboardSetEnabledEvent (owner/staff) toggles the room flag and
  pushes refreshed settings
- settings (flag + sound list) sent on room enter, alongside YouTube
2026-05-28 09:03:27 +02:00
medievalshell 458b37dbed feat: rare values + fortune wheel + in-client prize editor
Catalog-derived rare value map (diamond-priced), fortune wheel (WheelManager,
weighted RNG, lazy daily reset, rewards, recent wins) + admin prize editor
gated on acc_supporttool. Packets 9300-9305 / 9400-9404. Migration 020.
2026-05-28 02:39:01 +02:00
45 changed files with 1420 additions and 59 deletions
+8 -8
View File
@@ -49,15 +49,15 @@ INSERT INTO `bot_chat_responses` (`bot_type`, `keys`, `responses`) VALUES
('frank', 'pills;drugs', 'Drugs are bad, mmkay?');
INSERT IGNORE INTO `bot_serves` (`keys`, `item`) VALUES
('sunflower', 21),
('cola;habbo cola', 32),
('sunflower', 1002),
('cola;habbo cola', 19),
('rose', 1000),
('book', 20),
('tea', 6),
('coffee', 1),
('migraine;headache;pills', 34),
('radioactive liquid;radioactive', 36),
('turkey;can of turkey', 38);
('book', 1003),
('tea', 27),
('coffee', 8),
('migraine;headache;pills', 1015),
('radioactive liquid;radioactive', 30),
('turkey;can of turkey', 70);
-- VERY IMPORTANT !!!!
-- First check if the items_base ID and catalog_items ID is not in use !
@@ -0,0 +1,89 @@
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
-- ----------------------------------------------------------------------------
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`)
) 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.'),
('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.'),
('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`)
SELECT `type`, `amount`, `points_type`, `weight`, `label`, `sort_order`
FROM (
SELECT 'points' AS `type`, 25 AS `amount`, 5 AS `points_type`, 20 AS `weight`, '25 diamonds' AS `label`, 1 AS `sort_order`
UNION ALL SELECT 'points', 50, 5, 12, '50 diamonds', 2
UNION ALL SELECT 'points', 200, 5, 3, '200 diamonds', 3
UNION ALL SELECT 'credits', 100, 0, 15, '100 credits', 4
UNION ALL SELECT 'spin', 1, 0, 15, '1 Extra spin', 5
UNION ALL SELECT 'spin', 2, 0, 6, '2 Extra spins', 6
UNION ALL SELECT 'nothing', 0, 0, 29, 'Oh to bad!', 7
) AS seed
WHERE NOT EXISTS (SELECT 1 FROM `wheel_prizes`);
INSERT IGNORE INTO `permission_definitions` (`permission_key`, `max_value`, `comment`)
VALUES (
'acc_wheeladmin',
1,
'Allows opening the Fortune Wheel prize editor (FortuneWheelSettingsView) to add/edit prize slices. Gated server-side by the same key.'
);
SET @cols := NULL;
SELECT GROUP_CONCAT(CONCAT('dst.`', `column_name`, '` = src.`', `column_name`, '`') SEPARATOR ', ')
INTO @cols
FROM `information_schema`.`columns`
WHERE `table_schema` = DATABASE()
AND `table_name` = 'permission_definitions'
AND `column_name` REGEXP '^rank_[0-9]+$';
SET @sql := CONCAT(
'UPDATE `permission_definitions` dst ',
'JOIN `permission_definitions` src ON src.`permission_key` = ''acc_ads_background'' ',
'SET ', @cols, ' ',
'WHERE dst.`permission_key` = ''acc_wheeladmin'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+2 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.23</version>
<version>4.2.26</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;
}
@@ -189,11 +189,7 @@ public class Bot implements Runnable {
int timeOut = Emulator.getRandom().nextInt(20) * 2;
this.roomUnit.setWalkTimeOut((timeOut < 10 ? 5 : timeOut) + Emulator.getIntUnixTimestamp());
}
}/* else {
for (RoomTile t : this.room.getLayout().getTilesAround(this.room.getLayout().getTile(this.getRoomUnit().getX(), this.getRoomUnit().getY()))) {
WiredManager.handle(WiredTriggerType.BOT_REACHED_STF, this.roomUnit, this.room, this.room.getItemsAt(t).toArray());
}
}*/
}
}
if (!this.chatLines.isEmpty() && this.chatTimeOut <= Emulator.getIntUnixTimestamp() && this.chatAuto) {
@@ -218,7 +214,7 @@ public class Bot implements Runnable {
} else {
this.lastChatIndex++;
if (this.lastChatIndex >= this.chatLines.size()) {
this.lastChatIndex = 0; // start from scratch :-3
this.lastChatIndex = 0;
}
}
@@ -310,9 +306,6 @@ public class Bot implements Runnable {
public void setName(String name) {
this.name = name;
this.needsUpdate = true;
//if(this.room != null)
//this.room.sendComposer(new ChangeNameUpdatedComposer(this.getRoomUnit(), this.getName()).compose());
}
public String getMotto() {
@@ -539,9 +532,28 @@ public class Bot implements Runnable {
}
}
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
private static final short[] DEFAULT_OWNER_ACTION_IDS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11};
public static final int ACTION_ROTATE = 11;
private static final long MIN_OWNER_ACTION_INTERVAL_MS = 200L;
private volatile long lastOwnerActionAt;
public short[] getOwnerActionIds() {
return DEFAULT_OWNER_ACTION_IDS;
}
public synchronized boolean tryAcquireOwnerActionSlot() {
long now = System.currentTimeMillis();
if (now - this.lastOwnerActionAt < MIN_OWNER_ACTION_INTERVAL_MS) {
return false;
}
this.lastOwnerActionAt = now;
return true;
}
public void onPostOwnerAction(int actionId) {
// no-op default
}
}
@@ -23,7 +23,6 @@ import java.util.regex.Pattern;
public class FrankBot extends ButlerBot {
private static final Logger LOGGER = LoggerFactory.getLogger(FrankBot.class);
public static final String BOT_TYPE = "frank";
public static final String PERMISSION_USE = "acc_bot_frank";
private static final String KEY_DOOR_LINES = "__door_lines";
@@ -75,13 +74,20 @@ public class FrankBot extends ButlerBot {
}
}
private static final short[] FRANK_OWNER_ACTIONS = new short[0];
private static final short[] FRANK_OWNER_ACTIONS = { (short) Bot.ACTION_ROTATE };
@Override
public short[] getOwnerActionIds() {
return FRANK_OWNER_ACTIONS;
}
@Override
public void onPostOwnerAction(int actionId) {
if (actionId == ACTION_ROTATE && this.getRoomUnit() != null) {
this.homeRotation = this.getRoomUnit().getBodyRotation();
}
}
public static void initialise() {
chatResponses.clear();
doorLines = DEFAULT_DOOR_LINES;
@@ -202,6 +202,8 @@ public class CatalogManager {
public final Item ecotronItem;
public final THashMap<Integer, CatalogLimitedConfiguration> limitedNumbers;
private final List<Voucher> vouchers;
public final TIntObjectMap<int[]> furnitureValues;
private volatile byte[] rareValuesPayloadCache;
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,76 @@ public class CatalogManager {
this.loadClothing();
this.loadRecycler();
this.loadGiftWrappers();
this.loadFurnitureValues();
}
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();
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});
}
}
}
}
this.rebuildRareValuesPayloadCache();
LOGGER.info("Furniture Values -> Loaded! ({} entries)", this.furnitureValues.size());
}
private void rebuildRareValuesPayloadCache() {
try (java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(this.furnitureValues.size() * 16 + 8);
java.io.DataOutputStream out = new java.io.DataOutputStream(baos)) {
out.writeInt(this.furnitureValues.size());
TIntObjectIterator<int[]> iterator = this.furnitureValues.iterator();
while (iterator.hasNext()) {
iterator.advance();
int[] value = iterator.value();
out.writeInt(iterator.key()); // spriteId
out.writeInt(value[0]); // credits
out.writeInt(value[1]); // points
out.writeInt(value[2]); // pointsType
}
this.rareValuesPayloadCache = baos.toByteArray();
} catch (java.io.IOException e) {
LOGGER.error("Failed to build rare values payload cache", e);
this.rareValuesPayloadCache = null;
}
}
public TIntObjectMap<int[]> getFurnitureValues() {
return this.furnitureValues;
}
public byte[] getRareValuesPayloadSnapshot() {
return this.rareValuesPayloadCache;
}
private synchronized void loadLimitedNumbers() {
@@ -1051,9 +1124,6 @@ public class CatalogManager {
type = type.replace("bot_", "");
type = type.replace("visitor_logger", "visitor_log");
// Permission gate keyed on the canonical base-item name
// (admin-controlled but stable), not the catalog page name
// which can be renamed and bypass the check.
if (("bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)
|| ("rentable_bot_" + com.eu.habbo.habbohotel.bots.FrankBot.BOT_TYPE).equals(baseName)) {
if (!habbo.getClient().getHabbo().hasPermission(com.eu.habbo.habbohotel.bots.FrankBot.PERMISSION_USE)) {
@@ -42,14 +42,18 @@ public class RoomBundleLayout extends SingleBundle {
}
if (this.room == null) {
if (this.roomId > 0) {
this.room = Emulator.getGameEnvironment().getRoomManager().loadRoom(this.roomId);
RoomManager roomManager = Emulator.getGameEnvironment().getRoomManager();
if (this.roomId > 0 && roomManager != null) {
this.room = roomManager.loadRoom(this.roomId);
if (this.room != null)
this.room.preventUnloading = true;
} else {
} else if (this.roomId <= 0) {
LOGGER.error("No room id specified for room bundle {}({})", this.getPageName(), this.getId());
}
// roomManager can be null when CatalogManager.loadFurnitureValues() runs
// during GameEnvironment.load() before RoomManager is constructed; in that
// case skip eager room loading — the bundle resolves lazily at runtime.
}
if (this.room == null) {
@@ -153,7 +153,13 @@ public class GameClient {
this.channel.close();
if (this.habbo != null) {
if (this.habbo.isOnline()) {
// Agisci sull'Habbo SOLO se è ancora attaccato a QUESTO client. Su un
// reconnect veloce (drop Cloudflare → il client riconnette) l'Habbo può
// essere già stato riassegnato alla NUOVA connessione (session resume):
// in quel caso questo dispose della vecchia connessione NON deve
// parcheggiarlo né disconnetterlo, altrimenti ucciderebbe la sessione
// appena ripristinata (era la causa del "Bye"/kick al 2° reconnect).
if (this.habbo.getClient() == this && this.habbo.isOnline()) {
// Try to park the habbo in the grace period instead of immediate disconnect
boolean parked = SessionResumeManager.getInstance().parkHabbo(this.habbo, this.ssoTicket);
@@ -118,16 +118,32 @@ public class SessionResumeManager {
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
}
clearSsoTicket(habbo.getHabboInfo().getId());
// NON svuotare il ticket SSO qui. Dietro Cloudflare la pagina si ricarica
// lentamente (~15s) e la grace (5s) scade prima che la nuova connessione
// arrivi: svuotando il ticket si cancellava quello NUOVO appena scritto dal
// CMS per il refresh → "non-existing SSO token" → bisognava refreshare 2 volte.
// Il ticket vive col suo TTL (auth_ticket_expires_at) e viene sovrascritto dal
// CMS al prossimo /client o azzerato al logout.
}
private void restoreSsoTicket(int userId, String ssoTicket) {
// Restore the old ticket ONLY if no fresh ticket has been written in the
// meantime. On a hard-refresh the CMS writes a NEW auth_ticket for the same
// user before this parking restore runs; without the guard we'd clobber it
// with the old ticket, so the new connection's SSO wouldn't be found and the
// client would get "session expired" on the first attempt. The guard means:
// normal reconnect (ticket cleared to '' after login) -> restore; hard-refresh
// (CMS already wrote a new ticket) -> leave the new ticket untouched.
try (var connection = Emulator.getDatabase().getDataSource().getConnection();
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
var statement = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? AND (auth_ticket = '' OR auth_ticket IS NULL) LIMIT 1")) {
statement.setString(1, ssoTicket);
statement.setInt(2, userId);
statement.execute();
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
int updated = statement.executeUpdate();
if (updated > 0) {
LOGGER.info("[SessionResume] Restored SSO ticket for user {} during grace period", userId);
} else {
LOGGER.info("[SessionResume] Skipped SSO restore for user {} — a newer ticket is already present (likely a fresh login/hard-refresh)", userId);
}
} catch (Exception e) {
LOGGER.error("[SessionResume] Failed to restore SSO ticket for user " + userId, e);
}
@@ -29,6 +29,10 @@ public class InteractionRoomAds extends InteractionCustomValues {
{
this.put("offsetZ", "0");
}
{
this.put("scale", "100");
}
};
public InteractionRoomAds(ResultSet set, Item baseItem) throws SQLException {
@@ -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");
}
}
@@ -132,15 +132,12 @@ public class HabboManager {
Emulator.getPluginManager().fireEvent(new UserRegisteredEvent(habbo));
}
if (!Emulator.debugging) {
try (PreparedStatement stmt = connection.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
stmt.setString(1, "");
stmt.setInt(2, habbo.getHabboInfo().getId());
stmt.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
// NB: il ticket SSO NON viene svuotato qui di proposito. Dietro
// Cloudflare il WebSocket viene droppato e il client ritenta più
// volte con lo STESSO ticket: se lo consumassimo al primo uso, i
// retry (e l'hard-refresh) fallirebbero con "non-existing SSO token".
// Il ticket resta valido fino alla scadenza (auth_ticket_expires_at,
// TTL gestito dal CMS) o finché il CMS non ne scrive uno nuovo / logout.
}
}
} catch (SQLException e) {
@@ -0,0 +1,366 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboItem;
import gnu.trove.set.hash.THashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class WheelManager {
private static final Logger LOGGER = LoggerFactory.getLogger(WheelManager.class);
private static final int RECENT_KEEP = 50;
private static final int SECONDS_PER_DAY = 86400;
public static final Set<String> VALID_PRIZE_TYPES = Set.of(
"credits", "points", "spin", "item", "badge", "nothing");
public static final int MAX_PRIZES_PER_SAVE = 64;
public static final int MAX_STRING_LEN = 64;
public static final int MAX_PRIZE_AMOUNT = 1_000_000;
public static final int MAX_ITEM_QUANTITY = 100;
public static final int MAX_WEIGHT = 1_000_000;
public static final int MAX_EXTRA_SPINS = 10_000;
private static final long MIN_SPIN_INTERVAL_MS = 1500L;
private final List<WheelPrize> prizes = new ArrayList<>();
private int totalWeight = 0;
private int freeSpinsPerDay = 1;
private int spinCost = 50;
private int spinCostType = 5;
private final ConcurrentHashMap<Integer, Long> lastSpinAt = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, WheelUserState> userStateCache = new ConcurrentHashMap<>();
private final java.util.concurrent.CopyOnWriteArrayList<WheelRecentWin> recentWinsCache = new java.util.concurrent.CopyOnWriteArrayList<>();
public WheelManager() {
long millis = System.currentTimeMillis();
this.reload();
LOGGER.info("Wheel Manager -> Loaded! ({} MS)", System.currentTimeMillis() - millis);
}
public void reload() {
this.loadSettings();
this.loadPrizes();
this.loadRecentWins();
}
private void loadSettings() {
this.freeSpinsPerDay = Emulator.getConfig().getInt("wheel.free_spins_per_day", 1);
this.spinCost = Emulator.getConfig().getInt("wheel.spin_cost", 50);
this.spinCostType = Emulator.getConfig().getInt("wheel.spin_cost_type", 5);
}
private void loadPrizes() {
this.prizes.clear();
this.totalWeight = 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT * FROM wheel_prizes WHERE enabled = 1 ORDER BY sort_order ASC, id ASC");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
WheelPrize prize = new WheelPrize(set);
this.prizes.add(prize);
this.totalWeight += prize.weight;
}
} catch (SQLException e) {
LOGGER.error("Failed to load fortune wheel prizes", e);
}
}
public List<WheelPrize> getPrizes() {
return this.prizes;
}
public int getSpinCost() {
return this.spinCost;
}
public int getSpinCostType() {
return this.spinCostType;
}
private int today() {
return Emulator.getIntUnixTimestamp() / SECONDS_PER_DAY;
}
public synchronized WheelUserState getUserState(int userId) {
int today = this.today();
WheelUserState cached = this.userStateCache.get(userId);
if (cached != null) {
if (cached.lastReset != today) {
cached.freeSpins = this.freeSpinsPerDay;
cached.lastReset = today;
this.persistUserState(userId, cached);
}
return cached;
}
WheelUserState state = new WheelUserState();
boolean exists = false;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT free_spins, extra_spins, last_reset FROM wheel_user_state WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
state.freeSpins = set.getInt("free_spins");
state.extraSpins = set.getInt("extra_spins");
state.lastReset = set.getInt("last_reset");
exists = true;
}
}
} catch (SQLException e) {
LOGGER.error("Failed to read wheel state for user {}", userId, e);
}
if (!exists) {
state.freeSpins = this.freeSpinsPerDay;
state.extraSpins = 0;
state.lastReset = today;
this.persistUserState(userId, state);
} else if (state.lastReset != today) {
state.freeSpins = this.freeSpinsPerDay;
state.lastReset = today;
this.persistUserState(userId, state);
}
this.userStateCache.put(userId, state);
return state;
}
private void persistUserState(int userId, WheelUserState state) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_user_state (user_id, free_spins, extra_spins, last_reset) VALUES (?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE free_spins = VALUES(free_spins), extra_spins = VALUES(extra_spins), last_reset = VALUES(last_reset)")) {
statement.setInt(1, userId);
statement.setInt(2, state.freeSpins);
statement.setInt(3, state.extraSpins);
statement.setInt(4, state.lastReset);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist wheel state for user {}", userId, e);
}
}
public synchronized WheelPrize spin(Habbo habbo) {
int userId = habbo.getHabboInfo().getId();
long now = System.currentTimeMillis();
Long last = this.lastSpinAt.get(userId);
if (last != null && (now - last) < MIN_SPIN_INTERVAL_MS) return null;
this.lastSpinAt.put(userId, now);
WheelUserState state = this.getUserState(userId);
boolean usedFree;
if (state.freeSpins > 0) {
state.freeSpins--;
usedFree = true;
} else if (state.extraSpins > 0) {
state.extraSpins--;
usedFree = false;
} else {
return null;
}
WheelPrize prize = this.pickWeighted();
if (prize == null) {
if (usedFree) state.freeSpins++; else state.extraSpins++;
return null;
}
this.giveReward(habbo, prize, state);
this.persistUserState(userId, state);
this.recordWin(habbo, prize);
return prize;
}
private WheelPrize pickWeighted() {
if (this.prizes.isEmpty() || this.totalWeight <= 0) return null;
int roll = ThreadLocalRandom.current().nextInt(this.totalWeight);
int acc = 0;
for (WheelPrize prize : this.prizes) {
acc += prize.weight;
if (roll < acc) return prize;
}
return this.prizes.get(this.prizes.size() - 1);
}
private void giveReward(Habbo habbo, WheelPrize prize, WheelUserState state) {
int amount = Math.max(0, Math.min(prize.amount, MAX_PRIZE_AMOUNT));
switch (prize.type) {
case "credits":
if (amount > 0) habbo.giveCredits(amount);
break;
case "points":
if (amount > 0) habbo.givePoints(prize.pointsType, amount);
break;
case "spin":
int room = Math.max(0, MAX_EXTRA_SPINS - state.extraSpins);
state.extraSpins += Math.min(amount, room);
break;
case "item":
this.giveItem(habbo, prize, Math.min(amount, MAX_ITEM_QUANTITY));
break;
case "badge":
if (prize.value != null && !prize.value.isEmpty()) {
habbo.addBadge(prize.value, "Fortune Wheel");
}
break;
case "nothing":
default:
break;
}
}
private void giveItem(Habbo habbo, WheelPrize prize, int quantity) {
if (quantity <= 0 || prize.value == null) return;
int baseId;
try {
baseId = Integer.parseInt(prize.value.trim());
} catch (NumberFormatException e) {
return;
}
Item base = Emulator.getGameEnvironment().getItemManager().getItem(baseId);
if (base == null) return;
THashSet<HabboItem> items = new THashSet<>();
for (int i = 0; i < quantity; i++) {
HabboItem item = Emulator.getGameEnvironment().getItemManager().createItem(habbo.getHabboInfo().getId(), base, 0, 0, "");
if (item != null) items.add(item);
}
if (!items.isEmpty()) {
habbo.addFurniture(items);
}
}
private void recordWin(Habbo habbo, WheelPrize prize) {
WheelRecentWin win = new WheelRecentWin(
habbo.getHabboInfo().getUsername(),
habbo.getHabboInfo().getLook(),
prize.label);
this.recentWinsCache.add(0, win);
while (this.recentWinsCache.size() > RECENT_KEEP) {
this.recentWinsCache.remove(this.recentWinsCache.size() - 1);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO wheel_recent_wins (user_id, username, look, prize_label, won_at) VALUES (?, ?, ?, ?, ?)")) {
statement.setInt(1, habbo.getHabboInfo().getId());
statement.setString(2, habbo.getHabboInfo().getUsername());
statement.setString(3, habbo.getHabboInfo().getLook());
statement.setString(4, prize.label);
statement.setInt(5, Emulator.getIntUnixTimestamp());
statement.executeUpdate();
}
try (PreparedStatement trim = connection.prepareStatement(
"DELETE FROM wheel_recent_wins WHERE id < (SELECT id FROM (SELECT id FROM wheel_recent_wins ORDER BY id DESC LIMIT 1 OFFSET ?) t)")) {
trim.setInt(1, RECENT_KEEP - 1);
trim.executeUpdate();
}
} catch (SQLException e) {
LOGGER.error("Failed to record wheel win", e);
}
}
public List<WheelRecentWin> getRecentWins(int limit) {
if (limit <= 0) return new ArrayList<>();
int size = this.recentWinsCache.size();
if (size == 0) return new ArrayList<>();
int take = Math.min(limit, size);
return new ArrayList<>(this.recentWinsCache.subList(0, take));
}
private void loadRecentWins() {
this.recentWinsCache.clear();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT username, look, prize_label FROM wheel_recent_wins ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, RECENT_KEEP);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
this.recentWinsCache.add(new WheelRecentWin(
set.getString("username"),
set.getString("look"),
set.getString("prize_label")));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load wheel recent wins", e);
}
}
public synchronized boolean buySpin(Habbo habbo) {
if (this.spinCost <= 0) return false;
int userId = habbo.getHabboInfo().getId();
WheelUserState state = this.getUserState(userId);
if (state.extraSpins >= MAX_EXTRA_SPINS) return false;
if (this.spinCostType == -1) {
if (habbo.getHabboInfo().getCredits() < this.spinCost) return false;
habbo.giveCredits(-this.spinCost);
} else {
if (habbo.getHabboInfo().getCurrencyAmount(this.spinCostType) < this.spinCost) return false;
habbo.givePoints(this.spinCostType, -this.spinCost);
}
state.extraSpins++;
this.persistUserState(userId, state);
return true;
}
public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) {
String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing";
String safeValue = truncate(value, MAX_STRING_LEN);
String safeLabel = truncate(label, MAX_STRING_LEN);
int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT);
int safeWeight = clamp(weight, 0, MAX_WEIGHT);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, id);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
}
}
private static String truncate(String s, int max) {
if (s == null) return "";
return s.length() <= max ? s : s.substring(0, max);
}
private static int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
}
@@ -0,0 +1,44 @@
package com.eu.habbo.habbohotel.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.items.Item;
import java.sql.ResultSet;
import java.sql.SQLException;
// One slice of the wheel. type = item | badge | credits | points | spin | nothing.
public class WheelPrize {
public final int id;
public final String type;
public final String value; // item: base item id ; badge: badge code ; others: unused
public final int amount; // item qty / credits / points / extra spins
public final int pointsType; // for type=points
public final int weight;
public final String label;
public final int spriteId; // resolved for item prizes so the client can render the furni icon
public WheelPrize(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.type = set.getString("type");
this.value = set.getString("value");
this.amount = set.getInt("amount");
this.pointsType = set.getInt("points_type");
this.weight = Math.max(0, set.getInt("weight"));
this.label = set.getString("label");
this.spriteId = resolveSpriteId(this.type, this.value);
}
private static int resolveSpriteId(String type, String value) {
if (!"item".equals(type) || value == null) return 0;
try {
Item item = Emulator.getGameEnvironment().getItemManager().getItem(Integer.parseInt(value.trim()));
return item != null ? item.getSpriteId() : 0;
} catch (NumberFormatException e) {
return 0;
}
}
public String badgeCode() {
return "badge".equals(this.type) && this.value != null ? this.value : "";
}
}
@@ -0,0 +1,14 @@
package com.eu.habbo.habbohotel.wheel;
// A row in the "latest winners" panel. Denormalized (username/look stored at win time).
public class WheelRecentWin {
public final String username;
public final String look;
public final String prizeLabel;
public WheelRecentWin(String username, String look, String prizeLabel) {
this.username = username != null ? username : "";
this.look = look != null ? look : "";
this.prizeLabel = prizeLabel != null ? prizeLabel : "";
}
}
@@ -0,0 +1,12 @@
package com.eu.habbo.habbohotel.wheel;
// Per-user spin balance. freeSpins resets daily (lazy, on access); extraSpins persist.
public class WheelUserState {
public int freeSpins;
public int extraSpins;
public int lastReset; // day index (unix / 86400) of the last daily reset
public int totalSpins() {
return this.freeSpins + this.extraSpins;
}
}
@@ -745,5 +745,16 @@ public class PacketManager {
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
this.registerHandler(Incoming.RequestRareValuesEvent, com.eu.habbo.messages.incoming.rarevalues.RequestRareValuesEvent.class);
this.registerHandler(Incoming.WheelOpenEvent, com.eu.habbo.messages.incoming.wheel.WheelOpenEvent.class);
this.registerHandler(Incoming.WheelSpinEvent, com.eu.habbo.messages.incoming.wheel.WheelSpinEvent.class);
this.registerHandler(Incoming.WheelBuySpinEvent, com.eu.habbo.messages.incoming.wheel.WheelBuySpinEvent.class);
this.registerHandler(Incoming.WheelAdminGetPrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminGetPrizesEvent.class);
this.registerHandler(Incoming.WheelAdminSavePrizesEvent, com.eu.habbo.messages.incoming.wheel.WheelAdminSavePrizesEvent.class);
this.registerHandler(Incoming.SoundboardPlayEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardPlayEvent.class);
this.registerHandler(Incoming.SoundboardSetEnabledEvent, com.eu.habbo.messages.incoming.soundboard.SoundboardSetEnabledEvent.class);
}
}
@@ -486,4 +486,14 @@ public class Incoming {
public static final int HousekeepingSendHotelAlertEvent = 9121;
public static final int HousekeepingGetDashboardEvent = 9122;
public static final int HousekeepingListActionLogEvent = 9123;
// Custom features IDs 9300+ reserved
public static final int RequestRareValuesEvent = 9300;
public static final int WheelOpenEvent = 9301;
public static final int WheelSpinEvent = 9302;
public static final int WheelBuySpinEvent = 9303;
public static final int WheelAdminGetPrizesEvent = 9304;
public static final int WheelAdminSavePrizesEvent = 9305;
public static final int SoundboardPlayEvent = 9306;
public static final int SoundboardSetEnabledEvent = 9307;
}
@@ -133,17 +133,10 @@ public class SecureLoginEvent extends MessageHandler {
this.client.setHabbo(habbo);
this.client.setMachineId(habbo.getHabboInfo().getMachineID());
// Clear the SSO ticket now that session is resumed (prevent reuse)
if (!Emulator.debugging) {
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
java.sql.PreparedStatement stmt = conn.prepareStatement("UPDATE users SET auth_ticket = ? WHERE id = ? LIMIT 1")) {
stmt.setString(1, "");
stmt.setInt(2, habbo.getHabboInfo().getId());
stmt.execute();
} catch (Exception e) {
LOGGER.error("Failed to clear SSO ticket after session resume", e);
}
}
// NB: NON svuotiamo il ticket SSO qui (vedi HabboManager.loadHabbo):
// dietro Cloudflare il client ritenta la connessione con lo stesso
// ticket, quindi deve restare valido fino alla scadenza TTL. Consumarlo
// farebbe fallire i retry / l'hard-refresh con "non-existing SSO token".
} else {
// Normal login load from database
habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso);
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.rarevalues;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.rarevalues.RareValuesComposer;
public class RequestRareValuesEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 5000;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null) return;
CatalogManager catalog = Emulator.getGameEnvironment().getCatalogManager();
byte[] snapshot = catalog.getRareValuesPayloadSnapshot();
if (snapshot != null) {
this.client.sendResponse(new RareValuesComposer(snapshot));
return;
}
this.client.sendResponse(new RareValuesComposer(catalog.getFurnitureValues()));
}
}
@@ -5,11 +5,13 @@ import com.eu.habbo.habbohotel.bots.Bot;
import com.eu.habbo.habbohotel.bots.BotManager;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomUserRotation;
import com.eu.habbo.habbohotel.users.DanceType;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BotErrorComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDanceComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserNameChangedComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserStatusComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUsersComposer;
import com.eu.habbo.plugin.events.bots.BotSavedChatEvent;
import com.eu.habbo.plugin.events.bots.BotSavedLookEvent;
@@ -28,16 +30,20 @@ public class BotSaveSettingsEvent extends MessageHandler {
if (room.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER)) {
int botId = this.packet.readInt();
Bot bot = room.getBot(Math.abs(botId));
if (bot == null)
return;
if (bot.getOwnerActionIds().length == 0)
return;
int settingId = this.packet.readInt();
boolean allowed = false;
for (short a : bot.getOwnerActionIds()) {
if (a == settingId) {
allowed = true;
break;
}
}
if (!allowed) return;
if (!bot.tryAcquireOwnerActionSlot()) return;
switch (settingId) {
case 1:
@@ -163,8 +169,18 @@ public class BotSaveSettingsEvent extends MessageHandler {
bot.needsUpdate(true);
room.sendComposer(new RoomUsersComposer(bot).compose());
break;
case Bot.ACTION_ROTATE:
if (bot.getRoomUnit() == null) break;
int next = (bot.getRoomUnit().getBodyRotation().getValue() + 2) % 8;
RoomUserRotation rotation = RoomUserRotation.fromValue(next);
bot.getRoomUnit().setRotation(rotation);
bot.needsUpdate(true);
room.sendComposer(new RoomUserStatusComposer(bot.getRoomUnit()).compose());
break;
}
bot.onPostOwnerAction(settingId);
if (bot.needsUpdate()) {
Emulator.getThreading().run(bot);
}
@@ -0,0 +1,31 @@
package com.eu.habbo.messages.incoming.soundboard;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.soundboard.SoundboardSound;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.soundboard.SoundboardPlayComposer;
public class SoundboardPlayEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 250;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
Room room = this.currentRoom();
if (room == null || !room.isSoundboardEnabled()) return;
int soundId = this.packet.readInt();
SoundboardSound sound = Emulator.getGameEnvironment().getSoundboardManager().getSound(soundId);
if (sound == null) return;
// Broadcast to everyone in the room.
room.sendComposer(new SoundboardPlayComposer(sound.id, sound.url, habbo.getHabboInfo().getUsername()).compose());
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.incoming.soundboard;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.soundboard.SoundboardSettingsComposer;
public class SoundboardSetEnabledEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
Room room = this.currentRoom();
if (room == null) return;
// Only the room owner (or staff) may toggle the soundboard for the room.
boolean isOwner = room.getOwnerId() == habbo.getHabboInfo().getId();
if (!isOwner && !habbo.hasPermission(Permission.ACC_SUPPORTTOOL)) return;
boolean enabled = this.packet.readInt() == 1;
room.setSoundboardEnabled(enabled);
Emulator.getGameEnvironment().getSoundboardManager().setRoomEnabled(room.getId(), enabled);
// Push the refreshed settings (flag + sound list) to everyone in the room
// so the toolbar icon appears/disappears live.
room.sendComposer(new SoundboardSettingsComposer(enabled, Emulator.getGameEnvironment().getSoundboardManager().getSounds()).compose());
}
}
@@ -0,0 +1,24 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
public class WheelAdminGetPrizesEvent extends MessageHandler {
public static final String PERMISSION_KEY = "acc_wheeladmin";
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) {
return;
}
this.client.sendResponse(new WheelAdminPrizesComposer(
Emulator.getGameEnvironment().getWheelManager().getPrizes()));
}
}
@@ -0,0 +1,46 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
public class WheelAdminSavePrizesEvent extends MessageHandler {
public static final String PERMISSION_KEY = "acc_wheeladmin";
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
if (this.client.getHabbo() == null || !this.client.getHabbo().hasPermission(PERMISSION_KEY)) {
return;
}
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
int count = this.packet.readInt();
if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return;
for (int i = 0; i < count; i++) {
int id = this.packet.readInt();
String type = this.packet.readString();
String value = this.packet.readString();
int amount = this.packet.readInt();
int pointsType = this.packet.readInt();
int weight = this.packet.readInt();
String label = this.packet.readString();
wheel.savePrize(id, type, value, amount, pointsType, weight, label);
}
wheel.reload();
this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(this.client.getHabbo().getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
public class WheelBuySpinEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
wheel.buySpin(habbo); // whether or not it succeeds, resend the balance
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
public class WheelOpenEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
}
}
@@ -0,0 +1,39 @@
package com.eu.habbo.messages.incoming.wheel;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.habbohotel.wheel.WheelPrize;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelRecentWinsComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelResultComposer;
public class WheelSpinEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1500;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
if (habbo == null) return;
WheelManager wheel = Emulator.getGameEnvironment().getWheelManager();
WheelPrize prize = wheel.spin(habbo);
if (prize != null) {
this.client.sendResponse(new WheelResultComposer(prize.id));
}
// Refresh the balance either way so the client unlocks the wheel.
this.client.sendResponse(new WheelDataComposer(
wheel.getUserState(habbo.getHabboInfo().getId()),
wheel.getSpinCost(), wheel.getSpinCostType(), wheel.getPrizes()));
if (prize != null) {
this.client.sendResponse(new WheelRecentWinsComposer(wheel.getRecentWins(50)));
}
}
}
@@ -594,4 +594,13 @@ public class Outgoing {
public static final int HousekeepingDashboardComposer = 9204;
public static final int HousekeepingActionLogComposer = 9205;
// Custom features IDs 9400+ reserved
public static final int RareValuesComposer = 9400;
public static final int WheelDataComposer = 9401;
public static final int WheelResultComposer = 9402;
public static final int WheelRecentWinsComposer = 9403;
public static final int WheelAdminPrizesComposer = 9404;
public static final int SoundboardSettingsComposer = 9405;
public static final int SoundboardPlayComposer = 9406;
}
@@ -0,0 +1,46 @@
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;
public class RareValuesComposer extends MessageComposer {
private final TIntObjectMap<int[]> values;
private final byte[] snapshot;
public RareValuesComposer(byte[] snapshot) {
this.values = null;
this.snapshot = snapshot;
}
public RareValuesComposer(TIntObjectMap<int[]> values) {
this.values = values;
this.snapshot = null;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.RareValuesComposer);
if (this.snapshot != null) {
this.response.appendRawBytes(this.snapshot);
return this.response;
}
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;
}
}
@@ -0,0 +1,21 @@
package com.eu.habbo.messages.rcon;
import com.eu.habbo.Emulator;
import com.google.gson.Gson;
// Ricarica i suoni della Soundboard dal DB (live), così i suoni aggiunti/caricati
// dal CMS (/admin/soundboard) si applicano senza riavviare l'emulatore.
public class UpdateSoundboard extends RCONMessage<UpdateSoundboard.SoundboardJSON> {
public UpdateSoundboard() {
super(SoundboardJSON.class);
}
@Override
public void handle(Gson gson, SoundboardJSON object) {
Emulator.getGameEnvironment().getSoundboardManager().reload();
}
static class SoundboardJSON {
}
}
@@ -0,0 +1,21 @@
package com.eu.habbo.messages.rcon;
import com.eu.habbo.Emulator;
import com.google.gson.Gson;
// Ricarica i premi/settings della Ruota della Fortuna dal DB (live), così le
// modifiche fatte dal CMS (/admin/wheel) si applicano senza riavviare l'emulatore.
public class UpdateWheel extends RCONMessage<UpdateWheel.WheelJSON> {
public UpdateWheel() {
super(WheelJSON.class);
}
@Override
public void handle(Gson gson, WheelJSON object) {
Emulator.getGameEnvironment().getWheelManager().reload();
}
static class WheelJSON {
}
}
@@ -11,6 +11,7 @@ import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.UnsupportedMessageTypeException;
import io.netty.handler.ssl.NotSslRecordException;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,6 +40,19 @@ public class GameMessageHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof ClientMessage)) {
try {
if (Emulator.getConfig().getBoolean("debug.mode")) {
LOGGER.debug("Discarding non-game message {} from {}",
msg.getClass().getSimpleName(), ctx.channel().remoteAddress());
}
} finally {
ReferenceCountUtil.release(msg);
ctx.channel().close();
}
return;
}
ClientMessage message = (ClientMessage) msg;
try {
@@ -45,6 +45,8 @@ public class RCONServer extends Server {
this.addRCONMessage("sendroombundle", SendRoomBundle.class);
this.addRCONMessage("setrank", SetRank.class);
this.addRCONMessage("updatewordfilter", UpdateWordfilter.class);
this.addRCONMessage("updatewheel", UpdateWheel.class);
this.addRCONMessage("updatesoundboard", UpdateSoundboard.class);
this.addRCONMessage("updatecatalog", UpdateCatalog.class);
this.addRCONMessage("executecommand", ExecuteCommand.class);
this.addRCONMessage("progressachievement", ProgressAchievement.class);