From 7fca2acd240bb82a77f8fb57fbe0591cacc08a74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:39:30 +0000 Subject: [PATCH] Add WebSocketBot as external MCCScript bot with string enum serialization Port the WebSocket bot functionality to an external standalone script that: - Uses //MCCScript 1.0 format instead of being a built-in bot - Uses System.Text.Json instead of Newtonsoft.Json (no external DLL) - Serializes ItemType, EntityType, and other enums as string names instead of numeric IDs (addresses #2805) - Adds GetItemTypeMappings and GetEntityTypeMappings commands - Accepts both string names and numeric IDs for enum parameters - Uses 4096 byte WebSocket buffer for better message handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MinecraftClient/MinecraftClient.csproj | 1 + .../config/ChatBots/WebSocketBot.cs | 1181 +++++++++++++++++ 2 files changed, 1182 insertions(+) create mode 100644 MinecraftClient/config/ChatBots/WebSocketBot.cs diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 768b8b36..59bde3af 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -62,6 +62,7 @@ + diff --git a/MinecraftClient/config/ChatBots/WebSocketBot.cs b/MinecraftClient/config/ChatBots/WebSocketBot.cs new file mode 100644 index 00000000..5b8617c4 --- /dev/null +++ b/MinecraftClient/config/ChatBots/WebSocketBot.cs @@ -0,0 +1,1181 @@ +//MCCScript 1.0 +//using System.Collections.Concurrent +//using System.Collections.Generic +//using System.IO +//using System.Linq +//using System.Net +//using System.Net.Sockets +//using System.Net.WebSockets +//using System.Text +//using System.Text.Json +//using System.Text.Json.Serialization +//using System.Text.RegularExpressions +//using System.Threading +//using System.Threading.Tasks +//using MinecraftClient.CommandHandler +//using MinecraftClient.Inventory +//using MinecraftClient.Mapping +//using MinecraftClient.Scripting +//using MinecraftClient + +MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "wspass12345")); + +//MCCScript Extensions + +public class WebSocketSession +{ + public string SessionId { get; set; } + public WebSocket WebSocket { get; } + public bool IsAuthenticated { get; set; } + + public WebSocketSession(string sessionId, WebSocket webSocket) + { + SessionId = sessionId; + WebSocket = webSocket; + IsAuthenticated = false; + } +} + +public class WebSocketServer +{ + private HttpListener? _listener; + private CancellationTokenSource? _cts; + private readonly ConcurrentDictionary _sessions = new(); + + public event Action? NewSession; + public event Action? SessionDropped; + public event Action? MessageReceived; + + public IReadOnlyDictionary Sessions => _sessions; + + public async Task Start(string ip, int port) + { + _cts = new CancellationTokenSource(); + _listener = new HttpListener(); + _listener.Prefixes.Add($"http://{ip}:{port}/"); + _listener.Start(); + + while (!_cts.Token.IsCancellationRequested) + { + try + { + var context = await _listener.GetContextAsync().ConfigureAwait(false); + + if (context.Request.IsWebSocketRequest) + _ = Task.Run(() => ProcessWebSocketSession(context, _cts.Token)); + else + { + context.Response.StatusCode = 400; + context.Response.Close(); + } + } + catch (ObjectDisposedException) { break; } + catch (HttpListenerException) { break; } + catch { /* ignore transient errors */ } + } + } + + private async Task ProcessWebSocketSession(HttpListenerContext context, CancellationToken ct) + { + WebSocketContext wsContext; + try + { + wsContext = await context.AcceptWebSocketAsync(null).ConfigureAwait(false); + } + catch { return; } + + var ws = wsContext.WebSocket; + var sessionId = Guid.NewGuid().ToString("D"); + var session = new WebSocketSession(sessionId, ws); + + _sessions.TryAdd(sessionId, session); + NewSession?.Invoke(sessionId, session); + + var buffer = new byte[4096]; + var messageBuffer = new List(); + + try + { + while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested) + { + var result = await ws.ReceiveAsync(new ArraySegment(buffer), ct).ConfigureAwait(false); + + if (result.MessageType == WebSocketMessageType.Close) + break; + + messageBuffer.AddRange(new ArraySegment(buffer, 0, result.Count)); + + if (result.EndOfMessage) + { + var message = Encoding.UTF8.GetString(messageBuffer.ToArray()); + messageBuffer.Clear(); + MessageReceived?.Invoke(sessionId, message); + } + } + } + catch { /* connection dropped */ } + finally + { + _sessions.TryRemove(sessionId, out _); + SessionDropped?.Invoke(sessionId); + + if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived) + { + try + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session ended", CancellationToken.None) + .ConfigureAwait(false); + } + catch { /* best effort */ } + } + + ws.Dispose(); + } + } + + public bool RenameSession(string oldId, string newId) + { + if (!_sessions.TryRemove(oldId, out var session)) + return false; + + if (_sessions.ContainsKey(newId)) + { + _sessions.TryAdd(oldId, session); + return false; + } + + session.SessionId = newId; + _sessions.TryAdd(newId, session); + return true; + } + + public async Task SendToSession(string sessionId, string message) + { + if (!_sessions.TryGetValue(sessionId, out var session)) + return; + + if (session.WebSocket.State != WebSocketState.Open) + return; + + var bytes = Encoding.UTF8.GetBytes(message); + + try + { + await session.WebSocket.SendAsync( + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + CancellationToken.None).ConfigureAwait(false); + } + catch { /* send failed, session will be cleaned up */ } + } + + public void Stop() + { + _cts?.Cancel(); + + foreach (var kvp in _sessions) + { + try + { + var ws = kvp.Value.WebSocket; + if (ws.State == WebSocketState.Open) + ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server stopping", CancellationToken.None) + .GetAwaiter().GetResult(); + ws.Dispose(); + } + catch { /* best effort */ } + } + + _sessions.Clear(); + + try { _listener?.Stop(); } catch { } + try { _listener?.Close(); } catch { } + } +} + +public class WebSocketBot : ChatBot +{ + private readonly string _ip; + private readonly int _port; + private readonly string _password; + private readonly bool _debugMode; + + private WebSocketServer? _server; + private JsonSerializerOptions _jsonOptions = null!; + private readonly List _waitingEvents = new(); + private bool _gameJoined; + + private static readonly Regex Ipv4Regex = new( + @"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$", + RegexOptions.Compiled); + + public WebSocketBot(string ip, int port, string password, bool debugMode = false) + { + if (!Ipv4Regex.IsMatch(ip) && ip != "+" && ip != "*") + throw new ArgumentException($"Invalid IP address: {ip}"); + + if (port is < 1 or > 65535) + throw new ArgumentException($"Invalid port: {port}. Must be between 1 and 65535."); + + _ip = ip; + _port = port; + _password = password; + _debugMode = debugMode; + } + + public override void Initialize() + { + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IncludeFields = true, + Converters = { new JsonStringEnumConverter() } + }; + + _server = new WebSocketServer(); + _server.NewSession += OnNewSession; + _server.SessionDropped += OnSessionDropped; + _server.MessageReceived += OnMessageReceived; + + _ = Task.Run(async () => + { + try + { + await _server.Start(_ip, _port); + } + catch (Exception ex) + { + LogToConsole($"[WebSocketBot] Server failed to start: {ex.Message}"); + } + }); + + LogToConsole($"[WebSocketBot] Starting on {_ip}:{_port}"); + } + + public override void AfterGameJoined() + { + _gameJoined = true; + _waitingEvents.Add("OnGameJoined"); + } + + public override void Update() + { + if (_waitingEvents.Count > 0) + { + var events = _waitingEvents.ToList(); + _waitingEvents.Clear(); + + foreach (var evt in events) + BroadcastEvent(evt, "N/A"); + } + } + + public override void OnUnload() + { + BroadcastEvent("OnWsConnectionClose", "N/A"); + _server?.Stop(); + } + + // --- Event Overrides --- + + public override void GetText(string text) + { + text = GetVerbatim(text); + string message = "", username = ""; + + BroadcastEvent("OnChatRaw", SerializeData(new { text })); + + if (IsPrivateMessage(text, ref message, ref username)) + BroadcastEvent("OnChatPrivate", SerializeData(new { sender = username, message, rawText = text })); + else if (IsChatMessage(text, ref message, ref username)) + BroadcastEvent("OnChatPublic", SerializeData(new { sender = username, message, rawText = text })); + + string tpSender = ""; + if (IsTeleportRequest(text, ref tpSender)) + BroadcastEvent("OnTeleportRequest", SerializeData(new { sender = tpSender, rawText = text })); + } + + public override bool OnDisconnect(DisconnectReason reason, string message) + { + BroadcastEvent("OnDisconnect", SerializeData(new { reason = reason.ToString(), message })); + return false; + } + + public override void OnBlockBreakAnimation(Entity entity, Location location, byte stage) + { + BroadcastEvent("OnBlockBreakAnimation", SerializeData(new { entity, location, stage })); + } + + public override void OnEntityAnimation(Entity entity, byte animation) + { + BroadcastEvent("OnEntityAnimation", SerializeData(new { entity, animation })); + } + + public override void OnPlayerProperty(Dictionary prop) + { + BroadcastEvent("OnPlayerProperty", SerializeData(prop)); + } + + public override void OnServerTpsUpdate(double tps) + { + BroadcastEvent("OnServerTpsUpdate", SerializeData(new { tps })); + } + + public override void OnTimeUpdate(long worldAge, long timeOfDay) + { + BroadcastEvent("OnTimeUpdate", SerializeData(new { worldAge, timeOfDay })); + } + + public override void OnEntityMove(Entity entity) + { + BroadcastEvent("OnEntityMove", SerializeData(entity)); + } + + public override void OnInternalCommand(string commandName, string commandParams, CmdResult result) + { + BroadcastEvent("OnInternalCommand", SerializeData(new + { + commandName, + commandParams, + result = new { status = result.status.ToString(), result = result.result } + })); + } + + public override void OnEntitySpawn(Entity entity) + { + BroadcastEvent("OnEntitySpawn", SerializeData(entity)); + } + + public override void OnEntityDespawn(Entity entity) + { + BroadcastEvent("OnEntityDespawn", SerializeData(entity)); + } + + public override void OnHeldItemChange(byte slot) + { + BroadcastEvent("OnHeldItemChange", SerializeData(new { slot })); + } + + public override void OnHealthUpdate(float health, int food) + { + BroadcastEvent("OnHealthUpdate", SerializeData(new { health, food })); + } + + public override void OnExplosion(Location explode, float strength, int recordcount) + { + BroadcastEvent("OnExplosion", SerializeData(new { location = explode, strength, recordcount })); + } + + public override void OnSetExperience(float experienceBar, int level, int totalExperience) + { + BroadcastEvent("OnSetExperience", SerializeData(new { experienceBar, level, totalExperience })); + } + + public override void OnGamemodeUpdate(string playerName, Guid uuid, int gamemode) + { + BroadcastEvent("OnGamemodeUpdate", SerializeData(new { playerName, uuid, gamemode })); + } + + public override void OnLatencyUpdate(string playerName, Guid uuid, int latency) + { + BroadcastEvent("OnLatencyUpdate", SerializeData(new { playerName, uuid, latency })); + } + + public override void OnMapData(int mapId, byte scale, bool trackingPosition, bool locked, + List icons, byte columnsUpdated, byte rowsUpdated, byte mapColumnX, + byte mapRowZ, byte[]? colors) + { + BroadcastEvent("OnMapData", SerializeData(new + { + mapId, scale, trackingPosition, locked, icons, + columnsUpdated, rowsUpdated, mapColumnX, mapRowZ, + colors = colors != null ? Convert.ToBase64String(colors) : null + })); + } + + public override void OnTradeList(int windowId, List trades, VillagerInfo villagerInfo) + { + BroadcastEvent("OnTradeList", SerializeData(new { windowId, trades, villagerInfo })); + } + + public override void OnTitle(int action, string titleText, string subtitleText, + string actionBarText, int fadeIn, int stay, int fadeOut, string json) + { + BroadcastEvent("OnTitle", SerializeData(new + { + action, titleText, subtitleText, actionBarText, + fadeIn, stay, fadeOut, json + })); + } + + public override void OnEntityEquipment(Entity entity, int slot, Item? item) + { + BroadcastEvent("OnEntityEquipment", SerializeData(new { entity, slot, item })); + } + + public override void OnEntityEffect(Entity entity, Effects effect, int amplifier, int duration, byte flags) + { + BroadcastEvent("OnEntityEffect", SerializeData(new + { + entity, effect = effect.ToString(), amplifier, duration, flags + })); + } + + public override void OnScoreboardObjective(string objectiveName, byte mode, + string objectiveValue, int type, string json, int numberFormat) + { + BroadcastEvent("OnScoreboardObjective", SerializeData(new + { + objectiveName, mode, objectiveValue, type, json, numberFormat + })); + } + + public override void OnUpdateScore(string entityName, int action, string objectiveName, + string objectiveDisplayName, int value, int numberFormat) + { + BroadcastEvent("OnUpdateScore", SerializeData(new + { + entityName, action, objectiveName, objectiveDisplayName, value, numberFormat + })); + } + + public override void OnInventoryUpdate(int inventoryId) + { + BroadcastEvent("OnInventoryUpdate", SerializeData(new { inventoryId })); + } + + public override void OnInventoryOpen(int inventoryId) + { + BroadcastEvent("OnInventoryOpen", SerializeData(new { inventoryId })); + } + + public override void OnInventoryClose(int inventoryId) + { + BroadcastEvent("OnInventoryClose", SerializeData(new { inventoryId })); + } + + public override void OnPlayerJoin(Guid uuid, string name) + { + BroadcastEvent("OnPlayerJoin", SerializeData(new { uuid, name })); + } + + public override void OnPlayerLeave(Guid uuid, string? name) + { + BroadcastEvent("OnPlayerLeave", SerializeData(new { uuid, name })); + } + + public override void OnDeath() + { + BroadcastEvent("OnDeath", "N/A"); + } + + public override void OnRespawn() + { + BroadcastEvent("OnRespawn", "N/A"); + } + + public override void OnEntityHealth(Entity entity, float health) + { + BroadcastEvent("OnEntityHealth", SerializeData(new { entity, health })); + } + + public override void OnEntityMetadata(Entity entity, Dictionary metadata) + { + BroadcastEvent("OnEntityMetadata", SerializeData(new { entity, metadata })); + } + + public override void OnPlayerStatus(byte statusId) + { + BroadcastEvent("OnPlayerStatus", SerializeData(new { statusId })); + } + + public override void OnNetworkPacket(int packetID, List packetData, bool isLogin, bool isInbound) + { + BroadcastEvent("OnNetworkPacket", SerializeData(new + { + packetID, + data = Convert.ToBase64String(packetData.ToArray()), + isLogin, + isInbound + })); + } + + // --- Serialization helpers --- + + private string SerializeData(object data) + { + return JsonSerializer.Serialize(data, _jsonOptions); + } + + private void BroadcastEvent(string eventName, string data) + { + if (_server == null) return; + + var envelope = new Dictionary { ["event"] = eventName, ["data"] = data }; + var json = JsonSerializer.Serialize(envelope); + + foreach (var kvp in _server.Sessions) + { + if (!kvp.Value.IsAuthenticated) continue; + _ = _server.SendToSession(kvp.Key, json); + } + } + + private void SendSessionEvent(string sessionId, string eventName, string data) + { + if (_server == null) return; + + var envelope = new Dictionary { ["event"] = eventName, ["data"] = data }; + var json = JsonSerializer.Serialize(envelope); + + _ = _server.SendToSession(sessionId, json); + } + + // --- Session event handlers --- + + private void OnNewSession(string sessionId, WebSocketSession session) + { + if (_debugMode) + LogToConsole($"[WebSocketBot] New session: {sessionId}"); + } + + private void OnSessionDropped(string sessionId) + { + if (_debugMode) + LogToConsole($"[WebSocketBot] Session dropped: {sessionId}"); + } + + private void OnMessageReceived(string sessionId, string message) + { + if (_debugMode) + LogToConsole($"[WebSocketBot] [{sessionId}] Received: {message}"); + + if (_server == null || !_server.Sessions.TryGetValue(sessionId, out var session)) + return; + + try + { + using var doc = JsonDocument.Parse(message); + var root = doc.RootElement; + + if (root.TryGetProperty("command", out var commandElement)) + { + var command = commandElement.GetString() ?? ""; + var requestId = root.TryGetProperty("requestId", out var rid) ? rid.GetString() ?? "" : ""; + + var parameters = new List(); + if (root.TryGetProperty("parameters", out var paramsElement) && + paramsElement.ValueKind == JsonValueKind.Array) + { + foreach (var p in paramsElement.EnumerateArray()) + { + parameters.Add(p.ValueKind switch + { + JsonValueKind.String => p.GetString(), + JsonValueKind.Number => p.TryGetInt64(out var l) ? (object)l : p.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => p.GetRawText() + }); + } + } + + HandleCommand(sessionId, session, command, requestId, parameters); + return; + } + } + catch + { + // Not valid JSON, treat as plain text + } + + HandlePlainText(sessionId, session, message); + } + + private void HandlePlainText(string sessionId, WebSocketSession session, string text) + { + if (!session.IsAuthenticated) + { + SendSessionEvent(sessionId, "OnWsCommandResponse", + SerializeData(new { success = false, message = "Not authenticated", requestId = "" })); + return; + } + + if (text.StartsWith('/')) + { + var cmd = text[1..]; + var result = new CmdResult(); + PerformInternalCommand("send " + cmd, ref result); + SendSessionEvent(sessionId, "OnMccCommandResponse", + SerializeData(new { command = cmd, status = result.status.ToString(), result = result.result ?? "" })); + } + else + { + SendText(text); + } + } + + // --- Command processing --- + + private void HandleCommand(string sessionId, WebSocketSession session, string command, + string requestId, List parameters) + { + // Protocol commands available without auth + switch (command) + { + case "Authenticate": + HandleAuthenticate(sessionId, session, requestId, parameters); + return; + case "ChangeSessionId": + HandleChangeSessionId(sessionId, session, requestId, parameters); + return; + } + + if (!session.IsAuthenticated) + { + SendCommandResponse(sessionId, requestId, false, "Not authenticated"); + return; + } + + try + { + switch (command) + { + case "LogToConsole": + LogToConsole(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "LogDebugToConsole": + LogDebugToConsole(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "LogToConsoleTranslated": + LogToConsoleTranslated(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "LogDebugToConsoleTranslated": + LogDebugToConsoleTranslated(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "ReconnectToTheServer": + { + var extra = parameters.Count > 0 ? Convert.ToInt32(parameters[0]) : 3; + var delay = parameters.Count > 1 ? Convert.ToInt32(parameters[1]) : 0; + ReconnectToTheServer(extra, delay); + SendCommandResponse(sessionId, requestId, true); + break; + } + + case "DisconnectAndExit": + SendCommandResponse(sessionId, requestId, true); + DisconnectAndExit(); + break; + + case "SendPrivateMessage": + SendPrivateMessage(GetParam(parameters, 0), GetParam(parameters, 1)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "RunScript": + RunScript(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "GetTerrainEnabled": + SendCommandResponse(sessionId, requestId, true, SerializeData(new { enabled = GetTerrainEnabled() })); + break; + + case "SetTerrainEnabled": + SetTerrainEnabled(GetParam(parameters, 0)); + SendCommandResponse(sessionId, requestId, true); + break; + + case "GetEntityHandlingEnabled": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { enabled = GetEntityHandlingEnabled() })); + break; + + case "Sneak": + { + var on = GetParam(parameters, 0); + var result = Sneak(on); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "SendEntityAction": + { + var actionType = ParseEnum(parameters[0]); + var result = SendEntityAction(actionType); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "DigBlock": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + var direction = parameters.Count > 3 ? ParseEnum(parameters[3]) : Direction.Down; + var loc = new Location(x, y, z); + + if (!GetTerrainEnabled()) + { + SendCommandResponse(sessionId, requestId, false, "Terrain not enabled"); + break; + } + + var current = GetCurrentLocation(); + if (current.Distance(loc) > 6.0) + { + SendCommandResponse(sessionId, requestId, false, "Block too far away (max 6 blocks)"); + break; + } + + var world = GetWorld(); + var block = world.GetBlock(loc); + if (block.Type == Material.Air) + { + SendCommandResponse(sessionId, requestId, false, "Block is air"); + break; + } + + var digResult = DigBlock(loc, direction); + SendCommandResponse(sessionId, requestId, digResult); + break; + } + + case "SetSlot": + { + var slot = Convert.ToInt32(parameters[0]); + SetSlot(slot); + SendCommandResponse(sessionId, requestId, true); + break; + } + + case "GetWorld": + { + if (!GetTerrainEnabled()) + { + SendCommandResponse(sessionId, requestId, false, "Terrain not enabled"); + break; + } + // Return basic world info rather than full world data + SendCommandResponse(sessionId, requestId, true, SerializeData(new { available = true })); + break; + } + + case "GetEntities": + { + if (!GetEntityHandlingEnabled()) + { + SendCommandResponse(sessionId, requestId, false, "Entity handling not enabled"); + break; + } + var entities = GetEntities(); + SendCommandResponse(sessionId, requestId, true, SerializeData(entities)); + break; + } + + case "GetPlayersLatency": + { + var latency = GetPlayersLatency(); + SendCommandResponse(sessionId, requestId, true, SerializeData(latency)); + break; + } + + case "GetCurrentLocation": + SendCommandResponse(sessionId, requestId, true, SerializeData(GetCurrentLocation())); + break; + + case "MoveToLocation": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + var allowUnsafe = parameters.Count > 3 && GetParam(parameters, 3); + var allowDirectTp = parameters.Count > 4 && GetParam(parameters, 4); + var maxOffset = parameters.Count > 5 ? Convert.ToInt32(parameters[5]) : 0; + var minOffset = parameters.Count > 6 ? Convert.ToInt32(parameters[6]) : 0; + var result = MoveToLocation(new Location(x, y, z), allowUnsafe, allowDirectTp, maxOffset, minOffset); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "ClientIsMoving": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { moving = ClientIsMoving() })); + break; + + case "LookAtLocation": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + LookAtLocation(new Location(x, y, z)); + SendCommandResponse(sessionId, requestId, true); + break; + } + + case "GetTimestamp": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { timestamp = GetTimestamp() })); + break; + + case "GetServerPort": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { port = GetServerPort() })); + break; + + case "GetServerHost": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { host = GetServerHost() })); + break; + + case "GetUsername": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { username = GetUsername() })); + break; + + case "GetGamemode": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { gamemode = GetGamemode() })); + break; + + case "GetYaw": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { yaw = GetYaw() })); + break; + + case "GetPitch": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { pitch = GetPitch() })); + break; + + case "GetUserUUID": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { uuid = GetUserUUID() })); + break; + + case "GetOnlinePlayers": + SendCommandResponse(sessionId, requestId, true, + SerializeData(GetOnlinePlayers())); + break; + + case "GetOnlinePlayersWithUUID": + SendCommandResponse(sessionId, requestId, true, + SerializeData(GetOnlinePlayersWithUUID())); + break; + + case "GetServerTPS": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { tps = GetServerTPS() })); + break; + + case "InteractEntity": + { + var entityId = Convert.ToInt32(parameters[0]); + var interactType = ParseEnum(parameters[1]); + var hand = parameters.Count > 2 ? ParseEnum(parameters[2]) : Hand.MainHand; + var result = InteractEntity(entityId, interactType, hand); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "CreativeGive": + { + var slot = Convert.ToInt32(parameters[0]); + var itemType = ParseEnum(parameters[1]); + var count = Convert.ToInt32(parameters[2]); + var result = CreativeGive(slot, itemType, count); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "CreativeDelete": + { + var slot = Convert.ToInt32(parameters[0]); + var result = CreativeDelete(slot); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "SendAnimation": + { + var hand = parameters.Count > 0 ? ParseEnum(parameters[0]) : Hand.MainHand; + var result = SendAnimation(hand); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "SendPlaceBlock": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + var direction = ParseEnum(parameters[3]); + var hand = parameters.Count > 4 ? ParseEnum(parameters[4]) : Hand.MainHand; + var result = SendPlaceBlock(new Location(x, y, z), direction, hand); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "UseItemInHand": + { + var result = UseItemInHand(); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "GetInventoryEnabled": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { enabled = GetInventoryEnabled() })); + break; + + case "GetPlayerInventory": + { + if (!GetInventoryEnabled()) + { + SendCommandResponse(sessionId, requestId, false, "Inventory not enabled"); + break; + } + var inv = GetPlayerInventory(); + SendCommandResponse(sessionId, requestId, true, SerializeData(inv)); + break; + } + + case "GetInventories": + { + if (!GetInventoryEnabled()) + { + SendCommandResponse(sessionId, requestId, false, "Inventory not enabled"); + break; + } + var inventories = GetInventories(); + SendCommandResponse(sessionId, requestId, true, SerializeData(inventories)); + break; + } + + case "WindowAction": + { + var inventoryId = Convert.ToInt32(parameters[0]); + var slot = Convert.ToInt32(parameters[1]); + var actionType = ParseEnum(parameters[2]); + var result = WindowAction(inventoryId, slot, actionType); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "ChangeSlot": + { + var slot = Convert.ToInt16(parameters[0]); + var result = ChangeSlot(slot); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "GetCurrentSlot": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { slot = GetCurrentSlot() })); + break; + + case "ClearInventories": + { + var result = ClearInventories(); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "UpdateSign": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + var line1 = GetParam(parameters, 3); + var line2 = GetParam(parameters, 4); + var line3 = GetParam(parameters, 5); + var line4 = GetParam(parameters, 6); + var result = UpdateSign(new Location(x, y, z), line1, line2, line3, line4); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "SelectTrade": + { + var selectedSlot = Convert.ToInt32(parameters[0]); + var result = SelectTrade(selectedSlot); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "UpdateCommandBlock": + { + var x = Convert.ToDouble(parameters[0]); + var y = Convert.ToDouble(parameters[1]); + var z = Convert.ToDouble(parameters[2]); + var cmd = GetParam(parameters, 3); + var mode = ParseEnum(parameters[4]); + var flags = ParseEnum(parameters[5]); + var result = UpdateCommandBlock(new Location(x, y, z), cmd, mode, flags); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "CloseInventory": + { + var inventoryId = Convert.ToInt32(parameters[0]); + var result = CloseInventory(inventoryId); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "GetMaxChatMessageLength": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { length = GetMaxChatMessageLength() })); + break; + + case "Respawn": + { + var result = Respawn(); + SendCommandResponse(sessionId, requestId, result); + break; + } + + case "GetProtocolVersion": + SendCommandResponse(sessionId, requestId, true, + SerializeData(new { protocolVersion = GetProtocolVersion() })); + break; + + case "GetItemTypeMappings": + { + var mappings = new Dictionary(); + foreach (ItemType value in Enum.GetValues(typeof(ItemType))) + mappings[value.ToString()] = (int)value; + SendCommandResponse(sessionId, requestId, true, SerializeData(mappings)); + break; + } + + case "GetEntityTypeMappings": + { + var mappings = new Dictionary(); + foreach (EntityType value in Enum.GetValues(typeof(EntityType))) + mappings[value.ToString()] = (int)value; + SendCommandResponse(sessionId, requestId, true, SerializeData(mappings)); + break; + } + + default: + SendCommandResponse(sessionId, requestId, false, $"Unknown command: {command}"); + break; + } + } + catch (Exception ex) + { + SendCommandResponse(sessionId, requestId, false, $"Error: {ex.Message}"); + } + } + + private void HandleAuthenticate(string sessionId, WebSocketSession session, string requestId, + List parameters) + { + if (parameters.Count == 0 || GetParam(parameters, 0) != _password) + { + SendCommandResponse(sessionId, requestId, false, "Invalid password"); + return; + } + + session.IsAuthenticated = true; + SendCommandResponse(sessionId, requestId, true, "Authenticated"); + } + + private void HandleChangeSessionId(string sessionId, WebSocketSession session, string requestId, + List parameters) + { + if (parameters.Count == 0) + { + SendCommandResponse(sessionId, requestId, false, "New session ID required"); + return; + } + + var newId = GetParam(parameters, 0); + if (string.IsNullOrWhiteSpace(newId)) + { + SendCommandResponse(sessionId, requestId, false, "New session ID cannot be empty"); + return; + } + + if (_server == null) + { + SendCommandResponse(sessionId, requestId, false, "Server not initialized"); + return; + } + + if (_server.RenameSession(sessionId, newId)) + SendCommandResponse(newId, requestId, true, $"Session renamed to {newId}"); + else + SendCommandResponse(sessionId, requestId, false, $"Failed to rename session to {newId}"); + } + + // --- Response helpers --- + + private void SendCommandResponse(string sessionId, string requestId, bool success, string? message = null) + { + var response = new Dictionary + { + ["success"] = success, + ["requestId"] = requestId + }; + + if (message != null) + response["message"] = message; + + SendSessionEvent(sessionId, "OnWsCommandResponse", SerializeData(response)); + } + + // --- Parsing helpers --- + + private static T ParseEnum(object? param) where T : struct, Enum + { + if (param is string s) + { + if (Enum.TryParse(s, ignoreCase: true, out var parsed)) + return parsed; + } + + try + { + var numeric = Convert.ToInt32(param); + return (T)Enum.ToObject(typeof(T), numeric); + } + catch + { + throw new ArgumentException($"Cannot parse '{param}' as {typeof(T).Name}"); + } + } + + private static T GetParam(List parameters, int index) + { + if (index >= parameters.Count) + throw new ArgumentException($"Missing parameter at index {index}"); + + var val = parameters[index]; + + if (val is T typed) + return typed; + + if (typeof(T) == typeof(bool) && val != null) + return (T)(object)Convert.ToBoolean(val); + + if (typeof(T) == typeof(string)) + return (T)(object)(val?.ToString() ?? ""); + + return (T)Convert.ChangeType(val!, typeof(T)); + } +}