You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
🆙 Stage 2 reconnect
This commit is contained in:
@@ -8,6 +8,16 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
/**
|
||||
* Manages a grace period for disconnected users. Instead of immediately
|
||||
* disposing a Habbo when their WebSocket drops, the Habbo is held in
|
||||
* a "ghost" state for a configurable number of seconds. If the same
|
||||
* user reconnects (via SSO ticket) within the grace window, their
|
||||
* existing Habbo object is resumed on the new connection — keeping
|
||||
* them in their room, preserving inventory state, etc.
|
||||
*
|
||||
* Config key: session.reconnect.grace.seconds (default: 30)
|
||||
*/
|
||||
public class SessionResumeManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SessionResumeManager.class);
|
||||
@@ -27,6 +37,12 @@ public class SessionResumeManager {
|
||||
return Emulator.getConfig().getInt("session.reconnect.grace.seconds", 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* Park a disconnected Habbo in ghost mode. Their room presence is
|
||||
* preserved, but the old GameClient channel is closed.
|
||||
*
|
||||
* @return true if the habbo was parked (grace period > 0), false if immediate dispose should happen
|
||||
*/
|
||||
public boolean parkHabbo(Habbo habbo, String ssoTicket) {
|
||||
int graceSeconds = getGracePeriodSeconds();
|
||||
if (graceSeconds <= 0) {
|
||||
@@ -35,6 +51,7 @@ public class SessionResumeManager {
|
||||
|
||||
int userId = habbo.getHabboInfo().getId();
|
||||
|
||||
// Cancel any existing ghost session for this user
|
||||
GhostSession existing = ghostSessions.remove(userId);
|
||||
if (existing != null && existing.disposeFuture != null) {
|
||||
existing.disposeFuture.cancel(false);
|
||||
@@ -43,10 +60,12 @@ public class SessionResumeManager {
|
||||
LOGGER.info("[SessionResume] Parking {} (id={}) for {}s grace period",
|
||||
habbo.getHabboInfo().getUsername(), userId, graceSeconds);
|
||||
|
||||
// Restore the SSO ticket so the client can reconnect with the same ticket
|
||||
if (ssoTicket != null && !ssoTicket.isEmpty()) {
|
||||
restoreSsoTicket(userId, ssoTicket);
|
||||
}
|
||||
|
||||
// Schedule the final disconnect after the grace period
|
||||
ScheduledFuture<?> future = Emulator.getThreading().run(() -> {
|
||||
GhostSession ghost = ghostSessions.remove(userId);
|
||||
if (ghost != null) {
|
||||
@@ -60,12 +79,18 @@ public class SessionResumeManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resume a ghost session for the given user ID.
|
||||
*
|
||||
* @return the parked Habbo if found within grace period, null otherwise
|
||||
*/
|
||||
public Habbo resumeSession(int userId) {
|
||||
GhostSession ghost = ghostSessions.remove(userId);
|
||||
if (ghost == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cancel the scheduled dispose
|
||||
if (ghost.disposeFuture != null) {
|
||||
ghost.disposeFuture.cancel(false);
|
||||
}
|
||||
@@ -76,10 +101,16 @@ public class SessionResumeManager {
|
||||
return ghost.habbo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a ghost session (is in grace period).
|
||||
*/
|
||||
public boolean hasGhostSession(int userId) {
|
||||
return ghostSessions.containsKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately expire all ghost sessions (e.g. on emulator shutdown).
|
||||
*/
|
||||
public void disposeAll() {
|
||||
for (GhostSession ghost : ghostSessions.values()) {
|
||||
if (ghost.disposeFuture != null) {
|
||||
@@ -90,6 +121,9 @@ public class SessionResumeManager {
|
||||
ghostSessions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual full disconnect that normally happens in Habbo.disconnect().
|
||||
*/
|
||||
private void performFullDisconnect(Habbo habbo) {
|
||||
try {
|
||||
habbo.getHabboInfo().setOnline(false);
|
||||
@@ -97,6 +131,8 @@ public class SessionResumeManager {
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[SessionResume] Error during deferred disconnect", e);
|
||||
}
|
||||
|
||||
// Clear the SSO ticket now that the grace period is truly over
|
||||
clearSsoTicket(habbo.getHabboInfo().getId());
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ public class RoomManager {
|
||||
/**
|
||||
* Loads a room, optionally loading its data.
|
||||
* If the room is already being loaded in the background, this will wait for that to complete.
|
||||
*
|
||||
*
|
||||
* @param id The room ID
|
||||
* @param loadData Whether to load room data (items, bots, pets, etc.)
|
||||
* @return The loaded room, or null if not found
|
||||
@@ -499,14 +499,18 @@ public class RoomManager {
|
||||
}
|
||||
|
||||
public void enterRoom(Habbo habbo, int roomId, String password) {
|
||||
this.enterRoom(habbo, roomId, password, false, null);
|
||||
this.enterRoom(habbo, roomId, password, false, null, false);
|
||||
}
|
||||
|
||||
public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks) {
|
||||
this.enterRoom(habbo, roomId, password, overrideChecks, null);
|
||||
this.enterRoom(habbo, roomId, password, overrideChecks, null, false);
|
||||
}
|
||||
|
||||
public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks, RoomTile doorLocation) {
|
||||
this.enterRoom(habbo, roomId, password, overrideChecks, doorLocation, false);
|
||||
}
|
||||
|
||||
public void enterRoom(Habbo habbo, int roomId, String password, boolean overrideChecks, RoomTile doorLocation, boolean isReconnectSpawn) {
|
||||
Room room = this.loadRoom(roomId, true);
|
||||
|
||||
if (room == null)
|
||||
@@ -547,7 +551,7 @@ public class RoomManager {
|
||||
room.hasRights(habbo) ||
|
||||
(room.getState().equals(RoomState.INVISIBLE) && room.hasRights(habbo)) ||
|
||||
(room.hasGuild() && room.getGuildRightLevel(habbo).isGreaterThan(RoomRightLevels.GUILD_RIGHTS))) {
|
||||
this.openRoom(habbo, room, doorLocation);
|
||||
this.openRoom(habbo, room, doorLocation, isReconnectSpawn);
|
||||
} else if (room.getState() == RoomState.LOCKED) {
|
||||
boolean rightsFound = false;
|
||||
|
||||
@@ -572,7 +576,7 @@ public class RoomManager {
|
||||
room.addToQueue(habbo);
|
||||
} else if (room.getState() == RoomState.PASSWORD) {
|
||||
if (room.getPassword().equalsIgnoreCase(password))
|
||||
this.openRoom(habbo, room, doorLocation);
|
||||
this.openRoom(habbo, room, doorLocation, isReconnectSpawn);
|
||||
else {
|
||||
habbo.getClient().sendResponse(new GenericErrorMessagesComposer(GenericErrorMessagesComposer.WRONG_PASSWORD_USED));
|
||||
habbo.getClient().sendResponse(new HotelViewComposer());
|
||||
@@ -585,6 +589,10 @@ public class RoomManager {
|
||||
}
|
||||
|
||||
void openRoom(Habbo habbo, Room room, RoomTile doorLocation) {
|
||||
this.openRoom(habbo, room, doorLocation, false);
|
||||
}
|
||||
|
||||
void openRoom(Habbo habbo, Room room, RoomTile doorLocation, boolean isReconnectSpawn) {
|
||||
if (room == null || room.getLayout() == null)
|
||||
return;
|
||||
|
||||
@@ -623,7 +631,13 @@ public class RoomManager {
|
||||
if (doorLocation == null) {
|
||||
habbo.getRoomUnit().setBodyRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]);
|
||||
habbo.getRoomUnit().setHeadRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]);
|
||||
} else if (isReconnectSpawn) {
|
||||
// Reconnect spawn: place at tile but keep normal room behavior
|
||||
// (user can still leave by door, no teleport flags)
|
||||
habbo.getRoomUnit().setBodyRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]);
|
||||
habbo.getRoomUnit().setHeadRotation(RoomUserRotation.values()[room.getLayout().getDoorDirection()]);
|
||||
} else {
|
||||
// Furniture teleport spawn
|
||||
habbo.getRoomUnit().setCanLeaveRoomByDoor(false);
|
||||
habbo.getRoomUnit().isTeleporting = true;
|
||||
HabboItem topItem = room.getTopItemAt(doorLocation.x, doorLocation.y);
|
||||
|
||||
+20
-1
@@ -1,9 +1,9 @@
|
||||
package com.eu.habbo.messages.incoming.handshake;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.messenger.Messenger;
|
||||
import com.eu.habbo.habbohotel.gameclients.GameClient;
|
||||
import com.eu.habbo.habbohotel.gameclients.SessionResumeManager;
|
||||
import com.eu.habbo.habbohotel.messenger.Messenger;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
|
||||
import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
|
||||
import com.eu.habbo.habbohotel.navigation.NavigatorSavedSearch;
|
||||
@@ -84,14 +84,21 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (this.client.getHabbo() == null) {
|
||||
// Store SSO ticket on client for grace period tracking
|
||||
this.client.setSsoTicket(sso);
|
||||
|
||||
// Race condition fix: if the old WebSocket connection is still alive on the
|
||||
// server when the client reconnects, the SSO ticket won't be in the DB yet
|
||||
// (it was cleared on first login, and parkHabbo hasn't run because the old
|
||||
// channel hasn't closed). Find the old client by SSO ticket and force-dispose
|
||||
// it, which parks the habbo and restores the ticket to the DB.
|
||||
GameClient existingClient = Emulator.getGameServer().getGameClientManager().findClientBySsoTicket(sso);
|
||||
if (existingClient != null && existingClient != this.client) {
|
||||
LOGGER.info("[SessionResume] Found existing client with same SSO ticket — disposing old connection to trigger parking");
|
||||
Emulator.getGameServer().getGameClientManager().disposeClient(existingClient);
|
||||
}
|
||||
|
||||
// First, look up the user ID to check for ghost sessions
|
||||
int lookupUserId = 0;
|
||||
try (java.sql.Connection conn = Emulator.getDatabase().getDataSource().getConnection();
|
||||
java.sql.PreparedStatement stmt = conn.prepareStatement("SELECT id FROM users WHERE auth_ticket = ? LIMIT 1")) {
|
||||
@@ -105,6 +112,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
LOGGER.error("Caught exception looking up user for session resume", e);
|
||||
}
|
||||
|
||||
// Check if this user has a ghost session (disconnected within grace period)
|
||||
Habbo habbo = null;
|
||||
boolean isSessionResume = false;
|
||||
|
||||
@@ -113,6 +121,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
}
|
||||
|
||||
if (habbo != null) {
|
||||
// Session resume — reattach the existing Habbo to the new client
|
||||
isSessionResume = true;
|
||||
LOGGER.info("[SessionResume] Resuming session for {} (id={})",
|
||||
habbo.getHabboInfo().getUsername(), habbo.getHabboInfo().getId());
|
||||
@@ -121,6 +130,7 @@ 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")) {
|
||||
@@ -132,6 +142,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal login — load from database
|
||||
habbo = Emulator.getGameEnvironment().getHabboManager().loadHabbo(sso);
|
||||
}
|
||||
|
||||
@@ -177,6 +188,11 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
int roomIdToEnter = 0;
|
||||
|
||||
if (isSessionResume) {
|
||||
// On session resume, DON'T set roomIdToEnter. The client keeps its
|
||||
// existing room view alive and the habbo is already in the room on
|
||||
// the server. Setting roomIdToEnter = 0 prevents UserHomeRoomComposer
|
||||
// from triggering a full room re-entry on the client (which would
|
||||
// tear down and rebuild the room view).
|
||||
Room currentRoom = habbo.getHabboInfo().getCurrentRoom();
|
||||
if (currentRoom != null) {
|
||||
LOGGER.info("[SessionResume] {} is still in room {} — client will resume in-place",
|
||||
@@ -213,6 +229,8 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
|
||||
this.client.sendResponses(messages);
|
||||
|
||||
//Hardcoded
|
||||
//this.client.sendResponse(new ForumsTestComposer());
|
||||
this.client.sendResponse(new InventoryAchievementsComposer());
|
||||
|
||||
ModToolSanctions modToolSanctions = Emulator.getGameEnvironment().getModToolSanctions();
|
||||
@@ -248,6 +266,7 @@ public class SecureLoginEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip login-only events on session resume (welcome alerts, login events, etc.)
|
||||
if (!isSessionResume) {
|
||||
UserLoginEvent userLoginEvent = new UserLoginEvent(habbo, this.client.getHabbo().getHabboInfo().getIpLogin());
|
||||
Emulator.getPluginManager().fireEvent(userLoginEvent);
|
||||
|
||||
+56
-3
@@ -2,18 +2,38 @@ package com.eu.habbo.messages.incoming.rooms;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.rooms.Room;
|
||||
import com.eu.habbo.habbohotel.rooms.RoomTile;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class RequestRoomLoadEvent extends MessageHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RequestRoomLoadEvent.class);
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
int roomId = this.packet.readInt();
|
||||
String password = this.packet.readString();
|
||||
|
||||
// Optional spawn coordinates from the client (for future reconnection support).
|
||||
int spawnX = -1;
|
||||
int spawnY = -1;
|
||||
|
||||
try {
|
||||
int remaining = this.packet.getBuffer().readableBytes();
|
||||
if (remaining >= 8) {
|
||||
spawnX = this.packet.readInt();
|
||||
spawnY = this.packet.readInt();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
spawnX = -1;
|
||||
spawnY = -1;
|
||||
}
|
||||
|
||||
// Reset stale loadingRoom if timestamp has expired (indicates failed/stuck load)
|
||||
if (this.client.getHabbo().getHabboInfo().getLoadingRoom() != 0
|
||||
&& this.client.getHabbo().getHabboStats().roomEnterTimestamp + 5000 < System.currentTimeMillis()) {
|
||||
if (this.client.getHabbo().getHabboInfo().getLoadingRoom() != 0
|
||||
&& this.client.getHabbo().getHabboStats().roomEnterTimestamp + 5000 < System.currentTimeMillis()) {
|
||||
this.client.getHabbo().getHabboInfo().setLoadingRoom(0);
|
||||
}
|
||||
|
||||
@@ -30,6 +50,18 @@ public class RequestRoomLoadEvent extends MessageHandler {
|
||||
|
||||
Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom();
|
||||
if (room != null) {
|
||||
// If re-entering the same room (session resume / reconnect), capture
|
||||
// the user's current position before removal so we can respawn there.
|
||||
if (room.getId() == roomId && spawnX < 0 && spawnY < 0
|
||||
&& this.client.getHabbo().getRoomUnit() != null
|
||||
&& this.client.getHabbo().getRoomUnit().getCurrentLocation() != null) {
|
||||
RoomTile currentLoc = this.client.getHabbo().getRoomUnit().getCurrentLocation();
|
||||
spawnX = currentLoc.x;
|
||||
spawnY = currentLoc.y;
|
||||
LOGGER.info("[RequestRoomLoadEvent] Re-entering same room {} — preserving position ({}, {})",
|
||||
roomId, spawnX, spawnY);
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getRoomManager().logExit(this.client.getHabbo());
|
||||
|
||||
room.removeHabbo(this.client.getHabbo(), true);
|
||||
@@ -41,7 +73,28 @@ public class RequestRoomLoadEvent extends MessageHandler {
|
||||
this.client.getHabbo().getRoomUnit().isTeleporting = false;
|
||||
}
|
||||
|
||||
Emulator.getGameEnvironment().getRoomManager().enterRoom(this.client.getHabbo(), roomId, password);
|
||||
// Resolve spawn tile from coordinates (either from client or from saved position above)
|
||||
RoomTile spawnTile = null;
|
||||
|
||||
if (spawnX >= 0 && spawnY >= 0) {
|
||||
Room targetRoom = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
|
||||
if (targetRoom == null) {
|
||||
targetRoom = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId);
|
||||
}
|
||||
if (targetRoom != null && targetRoom.getLayout() != null) {
|
||||
RoomTile tile = targetRoom.getLayout().getTile((short) spawnX, (short) spawnY);
|
||||
if (tile != null && tile.isWalkable()) {
|
||||
spawnTile = tile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isReconnect = spawnTile != null;
|
||||
LOGGER.debug("[RequestRoomLoadEvent] Entering room {} (spawnTile={}, isReconnect={})",
|
||||
roomId,
|
||||
spawnTile != null ? "(" + spawnTile.x + "," + spawnTile.y + ")" : "door",
|
||||
isReconnect);
|
||||
Emulator.getGameEnvironment().getRoomManager().enterRoom(this.client.getHabbo(), roomId, password, false, spawnTile, isReconnect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user