From 8dd5155562093730bfa4bcb775b766ee36f90988 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 28 May 2026 15:30:33 +0200 Subject: [PATCH 1/6] 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. --- .../habbohotel/items/interactions/InteractionRoomAds.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRoomAds.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRoomAds.java index d37dea17..198df0d5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRoomAds.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionRoomAds.java @@ -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 { From 9c831a9da4f46a38164a3eec199735d50165940c Mon Sep 17 00:00:00 2001 From: medievalshell Date: Thu, 28 May 2026 22:41:10 +0200 Subject: [PATCH 2/6] feat: grant acc_wheeladmin to staff ranks for the wheel prize editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../022_wheel_admin_permission.sql | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql diff --git a/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql b/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql new file mode 100644 index 00000000..a33b544d --- /dev/null +++ b/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql @@ -0,0 +1,51 @@ +-- Fortune Wheel — admin permission grant (`acc_wheeladmin`) +-- +-- Both the client (FortuneWheelView "Settings" button) and the server handlers +-- (WheelAdminGetPrizesEvent / WheelAdminSavePrizesEvent, PERMISSION_KEY = +-- "acc_wheeladmin") gate the prize editor on `acc_wheeladmin`. +-- +-- The key itself is registered upstream in +-- `Database Updates/008_soundboard_fortune_wheel.sql`, but that file only grants +-- it to `rank_7` (its author's 7-rank hotel) and its ON DUPLICATE clause touches +-- the comment only. So on a hotel with a different rank layout the key would end +-- up granted to nobody useful. This file supplies the portable grant. +-- +-- Idempotent + portable: registers the key as a no-op safety (in case 008 hasn't +-- run yet) and grants it to the same ranks that hold acc_ads_background — the +-- adjacent "manage room-ad furni" permission — with dynamic SQL over the per-rank +-- columns, so no rank ids are hardcoded. If acc_ads_background is absent the JOIN +-- matches nothing and the key stays ungranted (safe; the button just stays +-- hidden until granted by hand). +-- +-- After applying, reload at runtime with `:update_permissions` (rebinds online +-- users to the fresh rank and rebroadcasts the permission map — no relog) or +-- restart the emulator. + +-- 1) Safety no-op registration (008 is the canonical registrar). rank_* default 0. +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.' +); + +-- 2) Grant to the same ranks as acc_ads_background, portably. +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; From c255f1e1b4a2a490e4127bfa91f20fa2d3df426e Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 00:45:02 +0200 Subject: [PATCH 3/6] 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. --- .../habbohotel/catalog/layouts/RoomBundleLayout.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/RoomBundleLayout.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/RoomBundleLayout.java index d4f6147c..1b0c475a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/RoomBundleLayout.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/layouts/RoomBundleLayout.java @@ -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) { From 478f7bdba092df9e5d843f7c28ca44a54df9d9bc Mon Sep 17 00:00:00 2001 From: medievalshell Date: Fri, 29 May 2026 04:45:34 +0200 Subject: [PATCH 4/6] 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). --- .../habbohotel/gameclients/GameClient.java | 8 ++++++- .../gameclients/SessionResumeManager.java | 24 +++++++++++++++---- .../habbo/habbohotel/users/HabboManager.java | 15 +++++------- .../incoming/handshake/SecureLoginEvent.java | 15 ++++-------- .../habbo/messages/rcon/UpdateSoundboard.java | 21 ++++++++++++++++ .../eu/habbo/messages/rcon/UpdateWheel.java | 21 ++++++++++++++++ .../networking/rconserver/RCONServer.java | 2 ++ 7 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateSoundboard.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateWheel.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java index f526c15b..7ffe5228 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java @@ -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); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java index 1f654d35..e8099bbc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/SessionResumeManager.java @@ -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); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java index 4413163d..48261f66 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/users/HabboManager.java @@ -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) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index 076a6795..7c7dcf3c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -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); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateSoundboard.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateSoundboard.java new file mode 100644 index 00000000..59b796c8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateSoundboard.java @@ -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 { + + public UpdateSoundboard() { + super(SoundboardJSON.class); + } + + @Override + public void handle(Gson gson, SoundboardJSON object) { + Emulator.getGameEnvironment().getSoundboardManager().reload(); + } + + static class SoundboardJSON { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateWheel.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateWheel.java new file mode 100644 index 00000000..aa9b2be4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateWheel.java @@ -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 { + + public UpdateWheel() { + super(WheelJSON.class); + } + + @Override + public void handle(Gson gson, WheelJSON object) { + Emulator.getGameEnvironment().getWheelManager().reload(); + } + + static class WheelJSON { + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java index c5c1ad8a..9eaeeef4 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java @@ -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); From b7915884b665bf0c96ffa70d99fc3c6e15a77f77 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 29 May 2026 08:28:01 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=86=99=20Update=20Rare-Value=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../habbohotel/catalog/CatalogManager.java | 37 ++++++++++++++----- .../rarevalues/RequestRareValuesEvent.java | 16 +++++--- .../rarevalues/RareValuesComposer.java | 15 +++++++- .../decoders/GameMessageHandler.java | 14 +++++++ 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index c89bf959..f0dbccec 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -202,8 +202,8 @@ public class CatalogManager { public final Item ecotronItem; public final THashMap limitedNumbers; private final List vouchers; - // spriteId -> [credits, points, pointsType], derived from catalog_items (see loadFurnitureValues) public final TIntObjectMap furnitureValues; + private volatile byte[] rareValuesPayloadCache; public CatalogManager() { long millis = System.currentTimeMillis(); @@ -249,10 +249,6 @@ public class CatalogManager { 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); @@ -266,8 +262,6 @@ public class CatalogManager { 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; @@ -291,13 +285,39 @@ public class CatalogManager { } } + 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 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 getFurnitureValues() { return this.furnitureValues; } + public byte[] getRareValuesPayloadSnapshot() { + return this.rareValuesPayloadCache; + } + private synchronized void loadLimitedNumbers() { this.limitedNumbers.clear(); @@ -1104,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)) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java index 117043a4..0ca00ad0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rarevalues/RequestRareValuesEvent.java @@ -1,11 +1,10 @@ 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; -// 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() { @@ -14,8 +13,15 @@ public class RequestRareValuesEvent extends MessageHandler { @Override public void handle() throws Exception { - this.client.sendResponse(new RareValuesComposer( - Emulator.getGameEnvironment().getCatalogManager().getFurnitureValues() - )); + 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())); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java index f713c8c9..85778e77 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/rarevalues/RareValuesComposer.java @@ -6,18 +6,29 @@ import com.eu.habbo.messages.outgoing.Outgoing; import gnu.trove.iterator.TIntObjectIterator; import gnu.trove.map.TIntObjectMap; -// Sends the full spriteId -> value map to the client. Consumed by the toolbar -// price guide and the furni infostand "value" line. See CatalogManager#loadFurnitureValues. public class RareValuesComposer extends MessageComposer { private final TIntObjectMap values; + private final byte[] snapshot; + + public RareValuesComposer(byte[] snapshot) { + this.values = null; + this.snapshot = snapshot; + } public RareValuesComposer(TIntObjectMap 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 iterator = this.values.iterator(); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageHandler.java index bbe9540f..a80be5a5 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/decoders/GameMessageHandler.java @@ -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 { From dfea6bcf8360e5ebdb4a40f9af46ed2b59581164 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 30 May 2026 07:52:02 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=86=99=20Updated=20SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../008_soundboard_fortune_wheel.sql | 131 ++++++++++-------- .../022_wheel_admin_permission.sql | 51 ------- 2 files changed, 71 insertions(+), 111 deletions(-) delete mode 100644 Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql diff --git a/Database Updates/008_soundboard_fortune_wheel.sql b/Database Updates/008_soundboard_fortune_wheel.sql index b5dba5ba..49e2c59b 100644 --- a/Database Updates/008_soundboard_fortune_wheel.sql +++ b/Database Updates/008_soundboard_fortune_wheel.sql @@ -1,78 +1,89 @@ --- Soundboard --- The room flag column + sounds table are also created at boot by - -ALTER TABLE `rooms` ADD COLUMN IF NOT EXISTS `soundboard_enabled` TINYINT(1) NOT NULL DEFAULT 0; +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, + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(64) NOT NULL DEFAULT '', -- pad label shown in the client + `url` VARCHAR(255) NOT NULL DEFAULT '', -- audio url (uploaded via CMS, like custom badges) + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `sort_order` INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- Fortune Wheel --- Tables are also created at boot by WheelManager (CREATE TABLE IF NOT EXISTS), --- so applying this file is only needed to seed prizes + settings. - +-- ---------------------------------------------------------------------------- +-- 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, + `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) + `user_id` INT(11) NOT NULL, + `free_spins` INT(11) NOT NULL DEFAULT 0, -- remaining free spins for the current day + `extra_spins` INT(11) NOT NULL DEFAULT 0, -- bought / won spins + `last_reset` INT(11) NOT NULL DEFAULT 0, -- day index of last daily reset (unix / 86400) PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `wheel_recent_wins` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `user_id` INT(11) NOT NULL, - `username` VARCHAR(64) NOT NULL DEFAULT '', - `look` VARCHAR(255) NOT NULL DEFAULT '', - `prize_label` VARCHAR(64) NOT NULL DEFAULT '', - `won_at` INT(11) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - KEY `idx_wheel_recent_wins_id` (`id`) + `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.') - ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); -INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES - ('wheel.spin_cost', '50', 'Fortune wheel: cost of one extra spin.') - ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); -INSERT INTO `emulator_settings` (`key`, `value`, `comment`) VALUES - ('wheel.spin_cost_type', '5', 'Fortune wheel: currency type for the spin cost (5 = diamonds; -1 = credits).') - ON DUPLICATE KEY UPDATE `comment` = VALUES(`comment`); - - -INSERT INTO `wheel_prizes` (`type`, `amount`, `points_type`, `weight`, `label`, `sort_order`) VALUES - ('points',25, 5, 20, '25 diamonds',1), - ('points',50, 5, 12, '50 diamonds',2), - ('points',200, 5, 3, '200 diamonds',3), - ('credits',100, 0, 15, '100 credits',4), - ('spin',1, 0, 15, '1 Extra spin', 5), - ('spin',2, 0, 6, '2 Extra spins',6), - ('nothing',0, 0, 29, 'Oh to bad!',7); - -INSERT INTO `permission_definitions` - (`permission_key`, `max_value`, `comment`, - `rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`) -VALUES - ('acc_wheeladmin', 1, 'Required to open the Fortune Wheel settings popup and edit prize rows.', - 0, 0, 0, 0, 0, 0, 1) + ('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; \ No newline at end of file diff --git a/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql b/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql deleted file mode 100644 index a33b544d..00000000 --- a/Database Updates/Own_Database_RunFirst/022_wheel_admin_permission.sql +++ /dev/null @@ -1,51 +0,0 @@ --- Fortune Wheel — admin permission grant (`acc_wheeladmin`) --- --- Both the client (FortuneWheelView "Settings" button) and the server handlers --- (WheelAdminGetPrizesEvent / WheelAdminSavePrizesEvent, PERMISSION_KEY = --- "acc_wheeladmin") gate the prize editor on `acc_wheeladmin`. --- --- The key itself is registered upstream in --- `Database Updates/008_soundboard_fortune_wheel.sql`, but that file only grants --- it to `rank_7` (its author's 7-rank hotel) and its ON DUPLICATE clause touches --- the comment only. So on a hotel with a different rank layout the key would end --- up granted to nobody useful. This file supplies the portable grant. --- --- Idempotent + portable: registers the key as a no-op safety (in case 008 hasn't --- run yet) and grants it to the same ranks that hold acc_ads_background — the --- adjacent "manage room-ad furni" permission — with dynamic SQL over the per-rank --- columns, so no rank ids are hardcoded. If acc_ads_background is absent the JOIN --- matches nothing and the key stays ungranted (safe; the button just stays --- hidden until granted by hand). --- --- After applying, reload at runtime with `:update_permissions` (rebinds online --- users to the fresh rank and rebroadcasts the permission map — no relog) or --- restart the emulator. - --- 1) Safety no-op registration (008 is the canonical registrar). rank_* default 0. -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.' -); - --- 2) Grant to the same ranks as acc_ads_background, portably. -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;