You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LoginServer mediates all server moves. Game servers call one BeginTransferAsync, LoginServer validates capacity (via existing heartbeat data), generates auth, updates _connectedAccounts, and returns target IP/port. Forward trip position stored in LoginServer memory (30s). Origin server/map persisted on Character DB for return-trip routing.
IP + port + capacity come from the existing GameServerHeartbeat pub/sub (published by GameServerStatePublisher, already consumed by ConnectServer). LoginServer.Host subscribes to the same topic — no new registration RPC needed.
sequenceDiagram
participant C as Client
participant GS as Source GameServer
participant LS as LoginServer
participant GS2 as Target GameServer
Note over C,GS2: Forward Trip / Login Redirect / Return Trip
C->>GS: Enter gate / Select character
GS->>GS: Check SharedMapLocator
alt Map is global, hosted elsewhere
GS->>LS: BeginTransferAsync(account, from, to, map)
LS->>LS: Check capacity via heartbeat<br/>Generate auth[4]<br/>Update _connectedAccounts
LS-->>GS: TransferResult (auth, IP, port)
GS->>GS: Save OriginServerId to DB
GS-->>C: 0xB1:0x00 (auth, IP, port)
GS->>GS: Disconnect client
else Normal flow
GS->>GS: Local warp / EnterWorld
end
Note over C,GS2: Auth Validation
C->>GS2: Connect + 0xB1:0x01 (auth)
GS2->>LS: ValidateAuth(auth)
LS->>LS: Match auth, clear PendingMove<br/>Return DestMapNumber
LS-->>GS2: DestMapNumber
GS2->>GS2: Load account from DB<br/>Set Player.Account<br/>Find character, set CurrentMap
GS2->>GS2: Set Player.SelectedCharacter
Note over GS2: OnPlayerEnteredWorldAsync fires<br/>Party/guild/friends wired
Loading
Sprints
Sprint 1 — Data Model
Add config entities for global map assignment and origin columns on Character for return-trip routing.
Extend ILoginServer with BeginTransferAsync. Add a named helper to resolve map→host server (used by gate, warp, and login redirect — single source of truth). LoginServer subscribes to existing GameServerHeartbeat topic for capacity + IP/port data.
Implementation: filters GameConfiguration.GlobalMapHostAssignments, resolves host from HostSubServer.ServerID.
ISharedMapLocator added as a property on IGameServerContext (same pattern as LoginServer, GuildServer, FriendServer). All call sites access it via ((IGameServerContext)player.GameContext).SharedMapLocator.
// src/Interfaces/ILoginServer.cs — add/// <summary>/// Initiates a server-to-server transfer. Called by source game server for/// live teleport, or by any game server for login redirect.////// LoginServer:/// - checks target capacity via heartbeat data/// - generates 4 auth codes, stores (auth[4], destMapNumber) for 30s/// - removes old server from _connectedAccounts, inserts new server/// - returns auth codes + target address on success, error code on failure/// </summary>ValueTask<TransferResult>BeginTransferAsync(stringaccountName,bytefromServerId,bytetoServerId,shortdestMapNumber);// Discriminated result — success has payload, failure has only codepublicreadonlyrecordstructTransferPayload(uintAuth1,uintAuth2,uintAuth3,uintAuth4,stringTargetIp,ushortTargetPort);publicreadonlyrecordstructTransferResult(TransferResultCodeCode,TransferPayload?Payload=null);publicenumTransferResultCode{Success,TargetOffline,TargetFull,InternalError}
LoginServer internal additions:
_pendingMoves: Dictionary<string, PendingMove> — keyed by account name
record PendingMove(byte ServerId, short DestMapNumber, uint[] Auth, uint Tick) — Tick for 30s expiry
BeginTransferAsync: lookup target server from heartbeat data → check CurrentConnections < MaxConnections → store PendingMove → update _connectedAccounts[account] = toServerId → return auth + address from heartbeat
LoginServer.Host subscribes to GameServerHeartbeat topic (same as ConnectServer already does) — populates ConcurrentDictionary<byte, ServerHeartbeatData> with current connections, max connections, public endpoint
No new IGameServer additions needed
Files: 2 new (SharedMapLocation.cs, ISharedMapLocator.cs), 1 modified
Sprint 3 — Packet, View Plugin, and Handler
Server → Client 0xC3:0xB1:0x00 — MapServerChange (38 bytes)
Offset
Type
Field
0-3
C3HeaderWithSubCode
{ Type=0xC3, Code=0xB1, SubCode=0x00, Length=38 }
4-19
char[16]
ServerIpAddress
20-21
ushort
ServerPort
22-23
ushort
ServerCode
24-27
uint
AuthCode1
28-31
uint
AuthCode2
32-35
uint
AuthCode3
36-37
uint
AuthCode4
Client → Server (0xC3:0xB1:0x01, 69 bytes) already exists in codebase.
Deserialize 69-byte packet, Xor3-decode account/character name
Validate auth codes with LoginServer → get DestMapNumber
Load account from DB: player.PersistenceContext.GetAccountByLoginNameAsync(accountName)
Set player.Account = loadedAccount
Find target character in loadedAccount.Characters by decoded character name
Set character's CurrentMap to dest map, position to map spawn gate
Set Player.SelectedCharacter = character — this triggers OnPlayerEnteredWorldAsync (requires player.Account to be set), which fires PlayerEnteredWorld event that wires up party rejoin, guild registration, friend online state. Do NOT manually place character on map.
Files: 4 new
Sprint 4 — Teleport Logic
Wire BeginTransferAsync + ISharedMapLocator into gate/warp flows.
// In a shared helper:asyncValueTask<bool>TryCrossServerMoveAsync(Playerplayer,ExitGatetargetGate){vargsc=(IGameServerContext)player.GameContext;varlocation=gsc.SharedMapLocator.LocateGlobalMap(targetGate.Map.Number);if(!location.IsGlobal||location.HostServerId==gsc.Id)returnfalse;varresult=awaitgsc.LoginServer.BeginTransferAsync(player.Account!.LoginName,gsc.Id,location.HostServerId!.Value,targetGate.Map.Number);if(result.Code!=TransferResultCode.Success){// error feedback to playerreturntrue;}varpayload=result.Payload!;// Save origin for return tripplayer.SelectedCharacter!.OriginServerId=gsc.Id;player.SelectedCharacter!.OriginMapNumber=player.SelectedCharacter!.CurrentMap!.Number;// persist to DBawaitplayer.InvokeViewPlugInAsync<IMapServerChangePlugIn>(
p =>p.MapServerChangeAsync(payload.TargetIp,payload.TargetPort,location.HostServerId.Value,payload.Auth1,payload.Auth2,payload.Auth3,payload.Auth4));player.Client.Disconnect();returntrue;}
Called from WarpGateAction.EnterGateAsync and WarpAction.WarpToAsync.
GameServerMapInitializer creates global map instances for HostedSharedMaps.
vargsc=(IGameServerContext)player.GameContext;varlocation=gsc.SharedMapLocator.LocateGlobalMap(player.SelectedCharacter!.CurrentMap!.Number);if(location.IsGlobal&&location.HostServerId!=gsc.Id){varresult=awaitgsc.LoginServer.BeginTransferAsync(player.Account!.LoginName,gsc.Id,location.HostServerId!.Value,player.SelectedCharacter!.CurrentMap!.Number);// send 0xB1:0x00 via IMapServerChangePlugIn, disconnect}
Return trip — WarpGateAction:
varcharacter=player.SelectedCharacter!;if(character.OriginServerId!=null){varresult=awaitgsc.LoginServer.BeginTransferAsync(player.Account!.LoginName,gsc.Id,character.OriginServerId.Value,character.OriginMapNumber!.Value);character.OriginServerId=null;character.OriginMapNumber=null;// persist to DB// send 0xB1:0x00 via IMapServerChangePlugIn, disconnect}
Cross-Server Shared Event Maps
Depends on: existing map/gate system, existing
GameServerHeartbeatpub/subScope
Intra-group subserver switching via
0xC3:0xB1:0x00/0xC3:0xB1:0x01.Glossary
MapHostingMode,GlobalMapHostAssignment,OriginServerId,OriginMapNumber,HostedSharedMapsSharedMapLocation,ISharedMapLocator,BeginTransferAsync,TransferResult,TransferPayload,PendingMoveMapServerChange(0xC3:0xB1:0x00),IMapServerChangePlugIn,MapServerChangeGroupHandler,ServerChangeAuthenticationHandlerPlugInTryCrossServerMoveAsyncTryRedirectExistingSessionAsync,TryReturnToOriginAsyncArchitecture
LoginServer mediates all server moves. Game servers call one
BeginTransferAsync, LoginServer validates capacity (via existing heartbeat data), generates auth, updates_connectedAccounts, and returns target IP/port. Forward trip position stored in LoginServer memory (30s). Origin server/map persisted on Character DB for return-trip routing.IP + port + capacity come from the existing
GameServerHeartbeatpub/sub (published byGameServerStatePublisher, already consumed byConnectServer).LoginServer.Hostsubscribes to the same topic — no new registration RPC needed.sequenceDiagram participant C as Client participant GS as Source GameServer participant LS as LoginServer participant GS2 as Target GameServer Note over C,GS2: Forward Trip / Login Redirect / Return Trip C->>GS: Enter gate / Select character GS->>GS: Check SharedMapLocator alt Map is global, hosted elsewhere GS->>LS: BeginTransferAsync(account, from, to, map) LS->>LS: Check capacity via heartbeat<br/>Generate auth[4]<br/>Update _connectedAccounts LS-->>GS: TransferResult (auth, IP, port) GS->>GS: Save OriginServerId to DB GS-->>C: 0xB1:0x00 (auth, IP, port) GS->>GS: Disconnect client else Normal flow GS->>GS: Local warp / EnterWorld end Note over C,GS2: Auth Validation C->>GS2: Connect + 0xB1:0x01 (auth) GS2->>LS: ValidateAuth(auth) LS->>LS: Match auth, clear PendingMove<br/>Return DestMapNumber LS-->>GS2: DestMapNumber GS2->>GS2: Load account from DB<br/>Set Player.Account<br/>Find character, set CurrentMap GS2->>GS2: Set Player.SelectedCharacter Note over GS2: OnPlayerEnteredWorldAsync fires<br/>Party/guild/friends wiredSprints
Sprint 1 — Data Model
Add config entities for global map assignment and origin columns on Character for return-trip routing.
Files: 2 new, 4 modified + EF migration
Sprint 2 — LoginServer Contract + Auth State + Map Host Resolver
Extend
ILoginServerwithBeginTransferAsync. Add a named helper to resolve map→host server (used by gate, warp, and login redirect — single source of truth). LoginServer subscribes to existingGameServerHeartbeattopic for capacity + IP/port data.Implementation: filters
GameConfiguration.GlobalMapHostAssignments, resolves host fromHostSubServer.ServerID.ISharedMapLocatoradded as a property onIGameServerContext(same pattern asLoginServer,GuildServer,FriendServer). All call sites access it via((IGameServerContext)player.GameContext).SharedMapLocator.LoginServer internal additions:
_pendingMoves: Dictionary<string, PendingMove>— keyed by account namerecord PendingMove(byte ServerId, short DestMapNumber, uint[] Auth, uint Tick)— Tick for 30s expiryBeginTransferAsync: lookup target server from heartbeat data → checkCurrentConnections < MaxConnections→ storePendingMove→ update_connectedAccounts[account] = toServerId→ return auth + address from heartbeatPendingMove→ returnDestMapNumberLoginServer.Hostsubscribes toGameServerHeartbeattopic (same asConnectServeralready does) — populatesConcurrentDictionary<byte, ServerHeartbeatData>with current connections, max connections, public endpointIGameServeradditions neededFiles: 2 new (
SharedMapLocation.cs,ISharedMapLocator.cs), 1 modifiedSprint 3 — Packet, View Plugin, and Handler
Server → Client
0xC3:0xB1:0x00— MapServerChange (38 bytes)C3HeaderWithSubCode{ Type=0xC3, Code=0xB1, SubCode=0x00, Length=38 }char[16]ServerIpAddressushortServerPortushortServerCodeuintAuthCode1uintAuthCode2uintAuthCode3uintAuthCode4Client → Server (
0xC3:0xB1:0x01, 69 bytes) already exists in codebase.New group handler for
0xB1:MapServerChangeGroupHandler— extendsGroupPacketHandlerPlugIn,GroupKey = 0xB1ServerChangeAuthenticationHandlerPlugIn—[BelongsToGroup(0xB1)],Key = 0x01ServerChangeAuthenticationHandlerPlugInflow:DestMapNumberplayer.PersistenceContext.GetAccountByLoginNameAsync(accountName)player.Account = loadedAccountloadedAccount.Charactersby decoded character nameCurrentMapto dest map, position to map spawn gatePlayer.SelectedCharacter = character— this triggersOnPlayerEnteredWorldAsync(requiresplayer.Accountto be set), which firesPlayerEnteredWorldevent that wires up party rejoin, guild registration, friend online state. Do NOT manually place character on map.Files: 4 new
Sprint 4 — Teleport Logic
Wire
BeginTransferAsync+ISharedMapLocatorinto gate/warp flows.Called from
WarpGateAction.EnterGateAsyncandWarpAction.WarpToAsync.GameServerMapInitializercreates global map instances forHostedSharedMaps.Files: 1 new, 3 modified
Sprint 5 — Login Redirect + Return Trip + Edge Cases
Login redirect —
SetSelectedCharacterAsync:Return trip —
WarpGateAction:Edge cases
TargetOfflineCurrentConnections >= MaxConnections→TargetFullOriginServerId/OriginMapNumber; fallback to host safe zoneSetSelectedCharacterAsyncTargetOffline→ player cannot log in until host recovers._connectedAccountsalready updated prevents it.SelectedCharactersetter triggers party rejoin on arrival. Trade cancelled (can't persist across servers).Notes
GameServerHeartbeatpub/sub. LoginServer subscribes — no new RPC._connectedAccounts[account]atomically updated insideBeginTransferAsync.GlobalMapHostAssignmentsowned byGameConfigurationwith[MemberOfAggregate].Player.SelectedCharacter→OnPlayerEnteredWorldAsyncwires party/guild/friend.