using System; using System.Threading; using System.Threading.Tasks; using PhoenixLib.Logging; using WingsAPI.Communication; using WingsAPI.Communication.Sessions; using WingsAPI.Communication.Sessions.Model; using WingsAPI.Communication.Sessions.Request; using WingsAPI.Communication.Sessions.Response; namespace WingsEmu.Master.Sessions { public class SessionService : ISessionService { private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1); private readonly EncryptionKeyFactory _encryptionKeyFactory; private readonly ISessionManager _sessionManager; public SessionService(ISessionManager sessionManager, EncryptionKeyFactory encryptionKeyFactory) { _sessionManager = sessionManager; _encryptionKeyFactory = encryptionKeyFactory; } public async ValueTask CreateSession(CreateSessionRequest request) { await _semaphoreSlim.WaitAsync(); try { Session existingSession = await _sessionManager.GetSessionByAccountId(request.AccountId); if (existingSession is not null && existingSession.State != SessionState.Disconnected && existingSession.State != SessionState.ServerSelection) { Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] A Session for account {request.AccountId} already exists"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } var session = new Session { Id = Guid.NewGuid().ToString().Replace("-", ""), IpAddress = request.IpAddress, AccountId = request.AccountId, AccountName = request.AccountName, Authority = request.AuthorityType, State = SessionState.Disconnected, EncryptionKey = _encryptionKeyFactory.CreateEncryptionKey() }; bool created = await _sessionManager.Create(session); if (!created) { Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] Failed to save session of account {session.AccountId} with session id {session.Id} into redis"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][CREATE_SESSION] Successfully created session for account {session.AccountId} with session id {session.Id}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask GetSessionByAccountName(GetSessionByAccountNameRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountName(request.AccountName); if (session is null) { return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask GetSessionByAccountId(GetSessionByAccountIdRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null) { Log.Debug($"[SESSION_SERVICE][GET_SESSION_BY_ACCOUNT_ID] Couldn't find session with account id: {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][GET_SESSION_BY_ACCOUNT_ID] Successfully found a session with account id: {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask ConnectToLoginServer(ConnectToLoginServerRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Can't connect account with ID {request.AccountId} to login server (No session found)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } if (session.State != SessionState.Disconnected && session.State != SessionState.CharacterSelection) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Can't session with ID {request.AccountId} to login server (Already connected)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } session.State = SessionState.ServerSelection; session.HardwareId = request.HardwareId; session.ClientVersion = request.ClientVersion; bool updated = await _sessionManager.Update(session); if (!updated) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Failed to update session of {session.AccountId} into redis"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][CONNECT_TO_LOGIN_SERVER] Successfully connected session of {session.AccountId} to login server"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask Disconnect(DisconnectSessionRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null) { Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (Couldn't find session)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } if (session.State == SessionState.CrossChannelAuthentication) { Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (CrossChannel or CharacterSelection)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } if (request.ForceDisconnect == false) { if (session.EncryptionKey != request.EncryptionKey) { Log.Debug($"[SESSION_SERVICE][DISCONNECT] Can't disconnect session of account {request.AccountId} (CrossChannel or CharacterSelection)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } } session.State = SessionState.Disconnected; session.CharacterId = 0; session.ChannelId = 0; session.ServerGroup = null; bool saved = await _sessionManager.Update(session); if (!saved) { Log.Debug($"[SESSION_SERVICE][DISCONNECT] Failed to update session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][DISCONNECT] Successfully updated session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS }; } finally { _semaphoreSlim.Release(); } } public async ValueTask ConnectCharacter(ConnectCharacterRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null || session.State != SessionState.CharacterSelection || session.ChannelId != request.ChannelId) { Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Can't connect character from session of account {request.AccountId} (Couldn't find session)"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } session.CharacterId = request.CharacterId; session.State = SessionState.InGame; bool updated = await _sessionManager.Update(session); if (!updated) { Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Failed to update session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][CONNECT_CHARACTER] Successfully update session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask ActivateCrossChannelAuthentication(ActivateCrossChannelAuthenticationRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null || session.State != SessionState.InGame) { Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Couldn't find session for account {request.AccountId} or account doesn't have correct state ({session?.State})"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } session.State = SessionState.CrossChannelAuthentication; session.AllowedCrossChannelId = request.ChannelId; bool updated = await _sessionManager.Update(session); if (!updated) { Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Failed to update session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][ACTIVE_CROSS_CHANNEL_AUTHENTICATION] Successfully updated session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask Pulse(PulseRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null || session.State != SessionState.InGame) { Log.Debug($"[SESSION_SERVICE][PULSE] Couldn't find session of account or incorrect state {request.AccountId} ({session?.State})"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } bool pulsed = await _sessionManager.Pulse(session); if (!pulsed) { Log.Debug($"[SESSION_SERVICE][PULSE] Failed to pulse session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } session.LastPulse = DateTime.UtcNow; bool updated = await _sessionManager.Update(session); if (!updated) { Log.Debug($"[SESSION_SERVICE][PULSE] Failed to update session of account {request.AccountId} after pulse"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][PULSE] Successfully pulsed session of account {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } public async ValueTask ConnectToWorldServer(ConnectToWorldServerRequest request) { await _semaphoreSlim.WaitAsync(); try { Session session = await _sessionManager.GetSessionByAccountId(request.AccountId); if (session is null) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Couldn't find session of account or incorrect state {request.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } // Can't connect to world if not in server selection screen or cross channel if (session.State != SessionState.ServerSelection && session.State != SessionState.CrossChannelAuthentication) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Incorrect session state {session.State}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } if (session.State == SessionState.CrossChannelAuthentication && session.AllowedCrossChannelId != request.ChannelId) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Incorrect cross channel ID {request.ChannelId} instead of {session.AllowedCrossChannelId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } session.LastChannelId = session.ChannelId; session.ServerGroup = request.ServerGroup; session.ChannelId = request.ChannelId; session.State = SessionState.CharacterSelection; session.AllowedCrossChannelId = 0; bool updated = await _sessionManager.Update(session); if (!updated) { Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Failed to update session of account {session.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.GENERIC_SERVER_ERROR }; } Log.Debug($"[SESSION_SERVICE][CONNECT_TO_WORLD_SERVER] Successfully updated session of account {session.AccountId}"); return new SessionResponse { ResponseType = RpcResponseType.SUCCESS, Session = session }; } finally { _semaphoreSlim.Release(); } } } }