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; diff --git a/Emulator/pom.xml b/Emulator/pom.xml index ff8e57c4..88b0e6a3 100644 --- a/Emulator/pom.xml +++ b/Emulator/pom.xml @@ -6,7 +6,7 @@ com.eu.habbo Habbo - 4.2.24 + 4.2.25 UTF-8 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) { 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/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 { 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);