Ahmedramadan24's picture
Add src/SilkroadBot.Core/State/BotContext.cs
1459920 verified
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;
/// <summary>
/// Central bot state machine. Manages the lifecycle of a single bot instance
/// including connection, authentication, and game session management.
/// </summary>
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();
}
/// <summary>
/// Connect to the gateway server and begin the authentication flow.
/// </summary>
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;
}
}
/// <summary>
/// Login with credentials.
/// </summary>
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 });
}
/// <summary>
/// Send a packet through the connection.
/// </summary>
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 });
}
/// <summary>
/// Disconnect and cleanup.
/// </summary>
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();
}
}