| 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; |
|
|
| |
| |
| |
| |
| public class BotContext : IDisposable |
| { |
| private readonly IProtocolAdapter _adapter; |
| private readonly IEventDispatcher _eventDispatcher; |
| private readonly ILogger<BotContext> _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<ConnectionState>? StateChanged; |
|
|
| public BotContext( |
| string profileId, |
| IProtocolAdapter adapter, |
| IEventDispatcher eventDispatcher, |
| ILogger<BotContext> logger) |
| { |
| ProfileId = profileId; |
| _adapter = adapter; |
| _eventDispatcher = eventDispatcher; |
| _logger = logger; |
| _security = new SilkroadSecurity(); |
| } |
|
|
| |
| |
| |
| 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>()); |
|
|
| _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; |
| } |
| } |
|
|
| |
| |
| |
| 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); |
| 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 }); |
| } |
|
|
| |
| |
| |
| 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 }); |
| } |
|
|
| |
| |
| |
| 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 }); |
| |
| |
| 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(); |
| } |
| } |
|
|