using Microsoft.Extensions.Logging; using SilkroadBot.Core.Events; using SilkroadBot.Core.Networking; using SilkroadBot.Core.Cryptography; using SilkroadBot.Domain.Enums; using SilkroadBot.Domain.Interfaces; using SilkroadBot.Domain.Models; namespace SilkroadBot.Core.State; /// /// Central bot state machine. Manages the lifecycle of a single bot instance /// including connection, authentication, and game session management. /// public class BotContext : IDisposable { private readonly IProtocolAdapter _adapter; private readonly IEventDispatcher _eventDispatcher; private readonly ILogger _logger; private readonly SilkroadSecurity _security; private NetworkClient? _networkClient; private CancellationTokenSource? _cts; public string ProfileId { get; } public GameState GameState { get; } = new(); public ConnectionState ConnectionState { get; private set; } = ConnectionState.Disconnected; public ExecutionMode ExecutionMode { get; set; } = ExecutionMode.Clientless; public IProtocolAdapter Adapter => _adapter; public IEventDispatcher Events => _eventDispatcher; public event Action? StateChanged; public BotContext( string profileId, IProtocolAdapter adapter, IEventDispatcher eventDispatcher, ILogger logger) { ProfileId = profileId; _adapter = adapter; _eventDispatcher = eventDispatcher; _logger = logger; _security = new SilkroadSecurity(); } /// /// Connect to the gateway server and begin the authentication flow. /// public async Task ConnectAsync(string host, int port, CancellationToken ct = default) { if (ConnectionState != ConnectionState.Disconnected) throw new InvalidOperationException($"Cannot connect in state {ConnectionState}"); _cts = CancellationTokenSource.CreateLinkedTokenSource(ct); _networkClient = new NetworkClient(_adapter, LoggerFactory.Create(b => b.AddConsole()).CreateLogger()); _networkClient.PacketReceived += OnPacketReceivedAsync; _networkClient.Disconnected += OnDisconnectedAsync; SetState(ConnectionState.Connecting); try { await _networkClient.ConnectAsync(host, port, _cts.Token); SetState(ConnectionState.Handshaking); } catch (Exception ex) { _logger.LogError(ex, "Failed to connect to {Host}:{Port}", host, port); SetState(ConnectionState.Disconnected); throw; } } /// /// Login with credentials. /// public async Task LoginAsync(string username, string password, CancellationToken ct = default) { if (_networkClient == null || !_networkClient.IsConnected) throw new InvalidOperationException("Not connected"); SetState(ConnectionState.Authenticating); var writer = new PacketWriter(); writer.WriteByte(0x01); // Login type writer.WriteAscii(username); writer.WriteAscii(password); var opcode = _adapter.GetOpcodeMap().GetOpcode("LOGIN_REQUEST"); var packet = Packet.ToServer(opcode, writer.ToArray()); await _networkClient.SendAsync(packet, ct); await _eventDispatcher.PublishAsync(new PacketSentEvent { Packet = packet }); } /// /// Send a packet through the connection. /// public async Task SendPacketAsync(Packet packet, CancellationToken ct = default) { if (_networkClient == null || !_networkClient.IsConnected) throw new InvalidOperationException("Not connected"); await _networkClient.SendAsync(packet, ct); await _eventDispatcher.PublishAsync(new PacketSentEvent { Packet = packet }); } /// /// Disconnect and cleanup. /// public async Task DisconnectAsync() { if (_networkClient != null) { await _networkClient.DisconnectAsync(); } SetState(ConnectionState.Disconnected); } private async Task OnPacketReceivedAsync(Packet packet) { await _eventDispatcher.PublishAsync(new PacketReceivedEvent { Packet = packet }); // Handle handshake packets var opcodeMap = _adapter.GetOpcodeMap(); var logicalName = opcodeMap.GetLogicalName(packet.Opcode); switch (logicalName) { case "HANDSHAKE": await HandleHandshakeAsync(packet); break; case "LOGIN_RESPONSE": HandleLoginResponse(packet); break; case "HP_MP_UPDATE": HandleHealthUpdate(packet); break; case "CHAT_RECEIVE": await HandleChatAsync(packet); break; case "ENTITY_SPAWN": await HandleEntitySpawnAsync(packet); break; case "ENTITY_DESPAWN": await HandleEntityDespawnAsync(packet); break; } } private async Task HandleHandshakeAsync(Packet packet) { _logger.LogDebug("Processing handshake..."); var response = _security.ProcessHandshake(packet.Payload); var opcode = _adapter.GetOpcodeMap().GetOpcode("HANDSHAKE_RESPONSE"); await _networkClient!.SendAsync(Packet.ToServer(opcode, response)); SetState(ConnectionState.Connected); } private void HandleLoginResponse(Packet packet) { var reader = new PacketReader(packet.Payload); byte result = reader.ReadByte(); if (result == 0x01) { _logger.LogInformation("Login successful"); SetState(ConnectionState.InGame); } else { _logger.LogWarning("Login failed with code: 0x{Code:X2}", result); SetState(ConnectionState.Connected); } } private void HandleHealthUpdate(Packet packet) { if (packet.Payload.Length < 8) return; var reader = new PacketReader(packet.Payload); int hp = (int)reader.ReadUInt32(); int mp = (int)reader.ReadUInt32(); GameState.Update(s => { s.Character.HP = hp; s.Character.MP = mp; }); _ = _eventDispatcher.PublishAsync(new HealthChangedEvent { HP = hp, MaxHP = GameState.Character.MaxHP, MP = mp, MaxMP = GameState.Character.MaxMP }); } private async Task HandleChatAsync(Packet packet) { var reader = new PacketReader(packet.Payload); var chatType = (ChatType)reader.ReadByte(); var sender = reader.ReadAscii(); var message = reader.ReadUnicode(); await _eventDispatcher.PublishAsync(new ChatMessageEvent { ChatType = chatType, Sender = sender, Message = message }); } private async Task HandleEntitySpawnAsync(Packet packet) { var reader = new PacketReader(packet.Payload); var entity = new GameEntity { UniqueId = reader.ReadUInt32(), ModelId = reader.ReadUInt32() }; GameState.Update(s => s.NearbyEntities.Add(entity)); await _eventDispatcher.PublishAsync(new EntitySpawnEvent { Entity = entity }); } private async Task HandleEntityDespawnAsync(Packet packet) { var reader = new PacketReader(packet.Payload); uint uid = reader.ReadUInt32(); GameState.Update(s => s.NearbyEntities.RemoveAll(e => e.UniqueId == uid)); await _eventDispatcher.PublishAsync(new EntityDespawnEvent { UniqueId = uid }); } private Task OnDisconnectedAsync(Exception? ex) { if (ex != null) _logger.LogError(ex, "Disconnected with error"); else _logger.LogInformation("Disconnected"); SetState(ConnectionState.Disconnected); return Task.CompletedTask; } private void SetState(ConnectionState newState) { var oldState = ConnectionState; ConnectionState = newState; GameState.ConnectionState = newState; StateChanged?.Invoke(newState); _ = _eventDispatcher.PublishAsync(new ConnectionStateChangedEvent { OldState = oldState, NewState = newState }); } public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); _networkClient?.Dispose(); } }