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);