From ba45f71f6ebe944e0f01d0fc29f59e4279f3f957 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:22:53 +0000 Subject: [PATCH 1/7] Initial plan 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 2/7] 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)); + } +} From c8c9f9ada52c57c43b3adb190783a5074297e466 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:47:33 +0000 Subject: [PATCH 3/7] Add WebSocketBot as external MCCScript bot with string enum serialization (#2805) Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/c762752f-46be-44f6-a05d-8c5effc8ef43 --- MinecraftClient/config/ChatBots/WebSocketBot.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/config/ChatBots/WebSocketBot.cs b/MinecraftClient/config/ChatBots/WebSocketBot.cs index 5b8617c4..a1111b77 100644 --- a/MinecraftClient/config/ChatBots/WebSocketBot.cs +++ b/MinecraftClient/config/ChatBots/WebSocketBot.cs @@ -285,8 +285,6 @@ public class WebSocketBot : ChatBot 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)) @@ -297,6 +295,11 @@ public class WebSocketBot : ChatBot BroadcastEvent("OnTeleportRequest", SerializeData(new { sender = tpSender, rawText = text })); } + public override void GetText(string text, string? json) + { + BroadcastEvent("OnChatRaw", SerializeData(new { text, json })); + } + public override bool OnDisconnect(DisconnectReason reason, string message) { BroadcastEvent("OnDisconnect", SerializeData(new { reason = reason.ToString(), message })); From 6d6d1105ebc68585d8148d8b7a0125dc03213eee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:51:10 +0000 Subject: [PATCH 4/7] Restore websocket docs and add sidebar navigation Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/c762752f-46be-44f6-a05d-8c5effc8ef43 --- docs/.vuepress/configs/l10n_configs/en.ts | 9 + docs/guide/websocket/Commands.md | 527 +++++++++++++++++++++ docs/guide/websocket/Events.md | 544 ++++++++++++++++++++++ docs/guide/websocket/README.md | 118 +++++ 4 files changed, 1198 insertions(+) create mode 100644 docs/guide/websocket/Commands.md create mode 100644 docs/guide/websocket/Events.md create mode 100644 docs/guide/websocket/README.md diff --git a/docs/.vuepress/configs/l10n_configs/en.ts b/docs/.vuepress/configs/l10n_configs/en.ts index 795a765b..90cac2cc 100644 --- a/docs/.vuepress/configs/l10n_configs/en.ts +++ b/docs/.vuepress/configs/l10n_configs/en.ts @@ -62,6 +62,15 @@ export const defaultThemeConfig_en: DefaultThemeLocaleData = { "/guide/creating-text-script.md", "/guide/chat-bots.md", "/guide/creating-bots.md", + { + text: "WebSocket Bot", + collapsible: true, + children: [ + "/guide/websocket/README.md", + "/guide/websocket/Commands.md", + "/guide/websocket/Events.md", + ], + }, "/guide/ai-assisted-development.md", "/guide/contibuting.md" ], diff --git a/docs/guide/websocket/Commands.md b/docs/guide/websocket/Commands.md new file mode 100644 index 00000000..8fe8232b --- /dev/null +++ b/docs/guide/websocket/Commands.md @@ -0,0 +1,527 @@ +# WebSocket Commands + +Commands are JSON objects sent over the WebSocket connection. +Each command produces a response through the [`OnWsCommandResponse`](Events.md#onwscommandresponse) event. + +```json +{ + "command": "CommandName", + "requestId": "unique-id", + "parameters": [] +} +``` + +## Protocol Commands + +These commands manage the WebSocket session itself. + +### `Authenticate` + +Authenticate with the configured password. +Must be called before any other command (except `ChangeSessionId`). + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------| +| 0 | string | Password | + +**Example:** +```json +{ + "command": "Authenticate", + "requestId": "auth-001", + "parameters": ["wspass12345"] +} +``` + +### `ChangeSessionId` + +Rename the current session. Can be called without authentication. +The new ID must be 1-32 characters and not already taken. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|----------------| +| 0 | string | New session ID | + +**Example:** +```json +{ + "command": "ChangeSessionId", + "requestId": "rename-001", + "parameters": ["my-bot"] +} +``` + +## Logging Commands + +### `LogToConsole` + +Log a message to the MCC console. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------| +| 0 | string | Message | + +### `LogDebugToConsole` + +Log a debug message to the MCC console (only visible in debug mode). + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------| +| 0 | string | Message | + +### `LogToConsoleTranslated` + +Log a translated message using an MCC translation key. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|------------------| +| 0 | string | Translation key | + +### `LogDebugToConsoleTranslated` + +Log a translated debug message. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|------------------| +| 0 | string | Translation key | + +## Session Commands + +### `ReconnectToTheServer` + +Reconnect to the Minecraft server. + +**Parameters:** + +| Index | Type | Description | +|-------|------|-------------------------------------| +| 0 | int | Extra reconnect attempts (default 3)| +| 1 | int | Delay in seconds (default 0) | + +### `DisconnectAndExit` + +Disconnect from the server and shut down MCC. +No parameters. + +## Chat Commands + +### `SendPrivateMessage` + +Send a private message to a player. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|---------------| +| 0 | string | Player name | +| 1 | string | Message | + +## Script Commands + +### `RunScript` + +Run an MCC script file. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------| +| 0 | string | File name | + +## World and Terrain Commands + +### `GetTerrainEnabled` + +Check if terrain handling is enabled. +No parameters. Returns `{ "enabled": true/false }`. + +### `SetTerrainEnabled` + +Enable or disable terrain handling. + +**Parameters:** + +| Index | Type | Description | +|-------|------|-------------| +| 0 | bool | Enabled | + +### `GetWorld` + +Check if world data is available. +No parameters. Returns `{ "available": true }` if terrain is enabled. + +### `DigBlock` + +Break a block at the given coordinates. +Validates the block is within 6 blocks and is not air. + +**Parameters:** + +| Index | Type | Description | +|-------|---------|--------------------------------| +| 0 | double | X coordinate | +| 1 | double | Y coordinate | +| 2 | double | Z coordinate | +| 3 | string | Direction (optional, e.g. "Down") | + +The `Direction` parameter accepts string names: `Down`, `Up`, `North`, `South`, `West`, `East`. + +## Entity Commands + +### `GetEntityHandlingEnabled` + +Check if entity handling is enabled. +No parameters. Returns `{ "enabled": true/false }`. + +### `GetEntities` + +Get all tracked entities. +No parameters. Returns a dictionary of entity ID to entity object. + +Entity types are serialized as string names (e.g., `"Zombie"`, `"Player"`). + +### `InteractEntity` + +Interact with an entity. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|--------------------------------------| +| 0 | int | Entity ID | +| 1 | string | Interaction type (`Interact`, `Attack`, `InteractAt`) | +| 2 | string | Hand (optional, `MainHand` or `OffHand`) | + +### `SendEntityAction` + +Send an entity action. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|------------------------------| +| 0 | string | Action type (e.g. `StartSneaking`, `StopSneaking`) | + +### `Sneak` + +Toggle sneaking. + +**Parameters:** + +| Index | Type | Description | +|-------|------|------------------| +| 0 | bool | true to sneak, false to stop | + +## Movement Commands + +### `GetCurrentLocation` + +Get the player's current location. +No parameters. Returns a location object with `x`, `y`, `z`. + +### `MoveToLocation` + +Move the player to a location using pathfinding. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|----------------------------------| +| 0 | double | X coordinate | +| 1 | double | Y coordinate | +| 2 | double | Z coordinate | +| 3 | bool | Allow unsafe (optional, false) | +| 4 | bool | Allow direct teleport (optional) | +| 5 | int | Max offset (optional, 0) | +| 6 | int | Min offset (optional, 0) | + +### `ClientIsMoving` + +Check if the client is currently moving. +No parameters. Returns `{ "moving": true/false }`. + +### `LookAtLocation` + +Make the player look at coordinates. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------| +| 0 | double | X | +| 1 | double | Y | +| 2 | double | Z | + +## Player Info Commands + +### `GetUsername` + +Get the player's username. +No parameters. Returns `{ "username": "..." }`. + +### `GetUserUUID` + +Get the player's UUID. +No parameters. Returns `{ "uuid": "..." }`. + +### `GetGamemode` + +Get the current gamemode. +No parameters. Returns `{ "gamemode": 0 }`. + +### `GetYaw` + +Get the player's yaw rotation. +No parameters. Returns `{ "yaw": 0.0 }`. + +### `GetPitch` + +Get the player's pitch rotation. +No parameters. Returns `{ "pitch": 0.0 }`. + +### `GetOnlinePlayers` + +Get a list of online player names. +No parameters. Returns a string array. + +### `GetOnlinePlayersWithUUID` + +Get online players with their UUIDs. +No parameters. Returns a dictionary of UUID to player name. + +### `GetPlayersLatency` + +Get latency information for online players. +No parameters. + +## Server Info Commands + +### `GetServerHost` + +Get the server hostname. +No parameters. Returns `{ "host": "..." }`. + +### `GetServerPort` + +Get the server port. +No parameters. Returns `{ "port": 25565 }`. + +### `GetServerTPS` + +Get the server TPS (ticks per second). +No parameters. Returns `{ "tps": 20.0 }`. + +### `GetTimestamp` + +Get the current timestamp. +No parameters. Returns `{ "timestamp": "..." }`. + +### `GetProtocolVersion` + +Get the Minecraft protocol version. +No parameters. Returns `{ "protocolVersion": 769 }`. + +### `GetMaxChatMessageLength` + +Get the maximum chat message length. +No parameters. Returns `{ "length": 256 }`. + +## Inventory Commands + +### `GetInventoryEnabled` + +Check if inventory handling is enabled. +No parameters. Returns `{ "enabled": true/false }`. + +### `GetPlayerInventory` + +Get the player's inventory. +No parameters. Returns the full inventory container with items. + +Item types are serialized as string names (e.g., `"DiamondSword"`, `"Stone"`). + +### `GetInventories` + +Get all open inventories. +No parameters. + +### `WindowAction` + +Perform a window/inventory action. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|---------------------------------------------------| +| 0 | int | Inventory ID | +| 1 | int | Slot ID | +| 2 | string | Action type (e.g. `LeftClick`, `RightClick`, `DropItemStack`) | + +### `ChangeSlot` + +Change the active hotbar slot. + +**Parameters:** + +| Index | Type | Description | +|-------|-------|-------------------| +| 0 | short | Slot number (0-8) | + +### `GetCurrentSlot` + +Get the currently selected hotbar slot. +No parameters. Returns `{ "slot": 0 }`. + +### `SetSlot` + +Set the active slot (legacy command). + +**Parameters:** + +| Index | Type | Description | +|-------|------|-------------| +| 0 | int | Slot number | + +### `ClearInventories` + +Clear tracked inventory state. +No parameters. + +### `CloseInventory` + +Close an inventory window. + +**Parameters:** + +| Index | Type | Description | +|-------|------|--------------| +| 0 | int | Inventory ID | + +## Creative Mode Commands + +### `CreativeGive` + +Give an item in creative mode. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|--------------------------------------------| +| 0 | int | Slot ID | +| 1 | string | Item type (e.g. `"DiamondSword"` or `798`) | +| 2 | int | Count | + +### `CreativeDelete` + +Delete an item from a slot in creative mode. + +**Parameters:** + +| Index | Type | Description | +|-------|------|-------------| +| 0 | int | Slot ID | + +## Block Interaction Commands + +### `SendPlaceBlock` + +Place a block. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|--------------------------| +| 0 | double | X coordinate | +| 1 | double | Y coordinate | +| 2 | double | Z coordinate | +| 3 | string | Direction (e.g. `"Up"`) | +| 4 | string | Hand (optional, `"MainHand"` or `"OffHand"`) | + +### `SendAnimation` + +Play arm swing animation. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|------------------------------------| +| 0 | string | Hand (optional, default `"MainHand"`) | + +### `UseItemInHand` + +Use the item currently held. +No parameters. + +### `UpdateSign` + +Update text on a sign. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|--------------| +| 0 | double | X coordinate | +| 1 | double | Y coordinate | +| 2 | double | Z coordinate | +| 3 | string | Line 1 | +| 4 | string | Line 2 | +| 5 | string | Line 3 | +| 6 | string | Line 4 | + +### `UpdateCommandBlock` + +Update a command block. + +**Parameters:** + +| Index | Type | Description | +|-------|--------|-------------------------| +| 0 | double | X coordinate | +| 1 | double | Y coordinate | +| 2 | double | Z coordinate | +| 3 | string | Command | +| 4 | string | Mode (e.g. `"Sequence"`, `"Auto"`, `"Redstone"`) | +| 5 | string | Flags | + +## Trading Commands + +### `SelectTrade` + +Select a villager trade. + +**Parameters:** + +| Index | Type | Description | +|-------|------|-------------| +| 0 | int | Trade index | + +### `Respawn` + +Respawn after death. +No parameters. + +## Mapping Commands (New) + +These commands address [issue #2805](https://github.com/MCCTeam/Minecraft-Console-Client/issues/2805), allowing clients to query enum mappings dynamically instead of maintaining hardcoded ID tables. + +### `GetItemTypeMappings` + +Get a dictionary of all ItemType names to their numeric IDs. +No parameters. Returns `{ "DiamondSword": 798, "Stone": 1, ... }`. + +### `GetEntityTypeMappings` + +Get a dictionary of all EntityType names to their numeric IDs. +No parameters. Returns `{ "Player": 128, "Zombie": 119, ... }`. diff --git a/docs/guide/websocket/Events.md b/docs/guide/websocket/Events.md new file mode 100644 index 00000000..310172fc --- /dev/null +++ b/docs/guide/websocket/Events.md @@ -0,0 +1,544 @@ +# WebSocket Events + +Events are JSON messages pushed to all authenticated WebSocket clients. +Each event has this structure: + +```json +{ + "event": "EventName", + "data": "{ ... serialized payload ... }" +} +``` + +The `data` field is a JSON string. Parse it to access the event payload. + +All enum values are serialized as **string names** (e.g., `"Zombie"` instead of `119`). + +## Protocol Events + +### `OnWsCommandResponse` + +Sent after every command execution. + +**Payload:** +```json +{ + "success": true, + "requestId": "your-request-id", + "message": "optional result or error message" +} +``` + +Match the `requestId` to track which command produced this response. + +### `OnMccCommandResponse` + +Sent when a plain-text MCC command (starting with `/`) is executed. + +**Payload:** +```json +{ + "command": "move north", + "status": "Done", + "result": "" +} +``` + +### `OnGameJoined` + +Sent after the client joins the server and the game session starts. +Payload: `"N/A"` + +### `OnWsRestarting` + +Sent when the WebSocket server is restarting (e.g., on reconnect). +Payload: `"N/A"` + +### `OnWsConnectionClose` + +Sent when the WebSocket server is shutting down. +Payload: `"N/A"` + +## Chat Events + +### `OnChatRaw` + +Sent for every incoming chat message, including the raw JSON. + +**Payload:** +```json +{ + "text": "Formatted text content", + "json": "{ raw JSON from server }" +} +``` + +### `OnChatPublic` + +Sent when a public chat message is detected. + +**Payload:** +```json +{ + "sender": "PlayerName", + "message": "Hello world", + "rawText": " Hello world" +} +``` + +### `OnChatPrivate` + +Sent when a private message is detected. + +**Payload:** +```json +{ + "sender": "PlayerName", + "message": "Secret message", + "rawText": "PlayerName whispers to you: Secret message" +} +``` + +### `OnTeleportRequest` + +Sent when a teleport request is detected. + +**Payload:** +```json +{ + "sender": "PlayerName", + "rawText": "PlayerName has requested to teleport to you" +} +``` + +## Connection Events + +### `OnDisconnect` + +Sent when MCC disconnects from the server. + +**Payload:** +```json +{ + "reason": "ConnectionLost", + "message": "Connection has been lost." +} +``` + +Reason values: `ConnectionLost`, `UserLogout`, `InGameKick`, `LoginRejected`. + +## Entity Events + +Entity objects include their `type` as a string name (e.g., `"Zombie"`, `"Player"`). + +### `OnEntitySpawn` + +Sent when an entity spawns. + +**Payload:** Full entity object. + +### `OnEntityDespawn` + +Sent when an entity despawns. + +**Payload:** Full entity object. + +### `OnEntityMove` + +Sent when an entity moves. + +**Payload:** Full entity object with updated location. + +### `OnEntityAnimation` + +Sent when an entity plays an animation. + +**Payload:** +```json +{ + "entity": { ... }, + "animation": 0 +} +``` + +### `OnEntityHealth` + +Sent when an entity's health changes. + +**Payload:** +```json +{ + "entity": { ... }, + "health": 20.0 +} +``` + +### `OnEntityMetadata` + +Sent when entity metadata updates. + +**Payload:** +```json +{ + "entity": { ... }, + "metadata": { "0": ..., "1": ... } +} +``` + +### `OnEntityEquipment` + +Sent when an entity's equipment changes. + +**Payload:** +```json +{ + "entity": { ... }, + "slot": 0, + "item": { "type": "DiamondSword", "count": 1, ... } +} +``` + +Item types are string names (e.g., `"DiamondSword"`). + +### `OnEntityEffect` + +Sent when an entity gets an effect. + +**Payload:** +```json +{ + "entity": { ... }, + "effect": "Speed", + "amplifier": 1, + "duration": 600, + "flags": 0 +} +``` + +### `OnBlockBreakAnimation` + +Sent when a block break animation plays. + +**Payload:** +```json +{ + "entity": { ... }, + "location": { "x": 10, "y": 64, "z": -20 }, + "stage": 5 +} +``` + +## Player Events + +### `OnPlayerJoin` + +Sent when a player joins the server. + +**Payload:** +```json +{ + "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "name": "PlayerName" +} +``` + +### `OnPlayerLeave` + +Sent when a player leaves the server. + +**Payload:** +```json +{ + "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "name": "PlayerName" +} +``` + +### `OnPlayerProperty` + +Sent when player properties update (e.g., speed, attack damage). + +**Payload:** Dictionary of property name to value. + +### `OnPlayerStatus` + +Sent when the player's status changes. + +**Payload:** +```json +{ + "statusId": 0 +} +``` + +### `OnDeath` + +Sent when the player dies. +Payload: `"N/A"` + +### `OnRespawn` + +Sent when the player respawns. +Payload: `"N/A"` + +## Health and Experience Events + +### `OnHealthUpdate` + +Sent when the player's health or food level changes. + +**Payload:** +```json +{ + "health": 20.0, + "food": 20 +} +``` + +### `OnSetExperience` + +Sent when experience updates. + +**Payload:** +```json +{ + "experienceBar": 0.5, + "level": 10, + "totalExperience": 200 +} +``` + +## Game Events + +### `OnGamemodeUpdate` + +Sent when a player's gamemode changes. + +**Payload:** +```json +{ + "playerName": "Steve", + "uuid": "...", + "gamemode": 1 +} +``` + +### `OnLatencyUpdate` + +Sent when a player's latency changes. + +**Payload:** +```json +{ + "playerName": "Steve", + "uuid": "...", + "latency": 42 +} +``` + +### `OnHeldItemChange` + +Sent when the held item slot changes. + +**Payload:** +```json +{ + "slot": 0 +} +``` + +### `OnExplosion` + +Sent when an explosion occurs. + +**Payload:** +```json +{ + "location": { "x": 10, "y": 64, "z": -20 }, + "strength": 4.0, + "recordcount": 12 +} +``` + +### `OnTitle` + +Sent when a title, subtitle, or action bar message is displayed. + +**Payload:** +```json +{ + "action": 0, + "titleText": "Welcome", + "subtitleText": "", + "actionBarText": "", + "fadeIn": 10, + "stay": 70, + "fadeOut": 20, + "json": "..." +} +``` + +## Server Events + +### `OnServerTpsUpdate` + +Sent when the server TPS updates. + +**Payload:** +```json +{ + "tps": 20.0 +} +``` + +### `OnTimeUpdate` + +Sent when the world time updates. + +**Payload:** +```json +{ + "worldAge": 1000000, + "timeOfDay": 6000 +} +``` + +### `OnInternalCommand` + +Sent when an MCC internal command is executed. + +**Payload:** +```json +{ + "commandName": "move", + "commandParams": "north", + "result": { + "status": "Done", + "result": "" + } +} +``` + +## Inventory Events + +### `OnInventoryUpdate` + +Sent when an inventory's contents change. + +**Payload:** +```json +{ + "inventoryId": 0 +} +``` + +### `OnInventoryOpen` + +Sent when an inventory window opens. + +**Payload:** +```json +{ + "inventoryId": 1 +} +``` + +### `OnInventoryClose` + +Sent when an inventory window closes. + +**Payload:** +```json +{ + "inventoryId": 1 +} +``` + +## Scoreboard Events + +### `OnScoreboardObjective` + +Sent when a scoreboard objective updates. + +**Payload:** +```json +{ + "objectiveName": "health", + "mode": 0, + "objectiveValue": "Health", + "type": 0, + "json": "...", + "numberFormat": 0 +} +``` + +### `OnUpdateScore` + +Sent when a scoreboard score updates. + +**Payload:** +```json +{ + "entityName": "Steve", + "action": 0, + "objectiveName": "health", + "objectiveDisplayName": "Health", + "value": 20, + "numberFormat": 0 +} +``` + +## Map and Trade Events + +### `OnMapData` + +Sent when map data updates. + +**Payload:** +```json +{ + "mapId": 0, + "scale": 1, + "trackingPosition": true, + "locked": false, + "icons": [], + "columnsUpdated": 128, + "rowsUpdated": 128, + "mapColumnX": 0, + "mapRowZ": 0, + "colors": "base64-encoded-string" +} +``` + +Note: `colors` is base64-encoded when present, `null` otherwise. + +### `OnTradeList` + +Sent when a villager trade list is received. + +**Payload:** +```json +{ + "windowId": 1, + "trades": [...], + "villagerInfo": { ... } +} +``` + +## Network Events + +### `OnNetworkPacket` + +Sent for every network packet (when subscribed). + +**Payload:** +```json +{ + "packetID": 42, + "data": "base64-encoded-packet-data", + "isLogin": false, + "isInbound": true +} +``` + +Note: `data` is base64-encoded. This event generates heavy traffic and is mainly useful for debugging. diff --git a/docs/guide/websocket/README.md b/docs/guide/websocket/README.md new file mode 100644 index 00000000..45c45f1b --- /dev/null +++ b/docs/guide/websocket/README.md @@ -0,0 +1,118 @@ +# WebSocket Bot + +The WebSocket Bot is an **external example bot** that lets you remotely control MCC over WebSocket. +It runs a local WebSocket server inside your MCC session, accepts commands as JSON messages, and pushes game events back to connected clients in real time. + +::: warning External Bot +This bot is **not** built into MCC. +You load it as a standalone script with `/script ChatBots/WebSocketBot.cs`. +::: + +## Quick Start + +1. Copy `config/ChatBots/WebSocketBot.cs` into your MCC `config/ChatBots/` folder (it ships in the repo under that path). +2. Open the file and edit the line near the top: + ```csharp + MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "wspass12345")); + ``` + - Replace `127.0.0.1` with the IP to bind (use `+` or `*` for all interfaces). + - Replace `8043` with your preferred port. + - Replace `wspass12345` with a strong password. +3. Optionally enable debug logging: + ```csharp + MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "mypassword", debugMode: true)); + ``` +4. In MCC, run: `/script ChatBots/WebSocketBot.cs` + +The bot starts a WebSocket server. Connect to `ws://127.0.0.1:8043/` with any WebSocket client. + +## Protocol Overview + +All communication uses JSON over WebSocket text frames. + +### Authentication Flow + +``` +Connect via WebSocket + | + v +(Optional) Send "ChangeSessionId" to set a friendly session name + | + v +Send "Authenticate" with the configured password + | + v +Send commands and receive events +``` + +### Sending Commands + +Commands are JSON objects with this shape: + +```json +{ + "command": "CommandName", + "requestId": "any-unique-string", + "parameters": [1, "text", true] +} +``` + +- `command` - the procedure name (case-sensitive) +- `requestId` - a client-generated ID so you can match responses to requests +- `parameters` - an ordered array of arguments (types depend on the command) + +Every command produces an `OnWsCommandResponse` event with `success`, `requestId`, and optionally `message`. + +### Sending Plain Text + +You can also send plain text directly: +- Text starting with `/` is forwarded to MCC as an internal command (e.g., `/move north`). +- Other text is sent as chat. + +### Receiving Events + +Events arrive as JSON: + +```json +{ + "event": "EventName", + "data": "{ ... serialized payload ... }" +} +``` + +The `data` field is a JSON string that you parse separately to get the event payload. + +## Enum Serialization (String Names) + +All enum values (ItemType, EntityType, Direction, Hand, etc.) are serialized as **string names**, not numeric IDs. + +For example, an entity of type `Zombie` appears as: +```json +{ "type": "Zombie", "location": { "x": 10, "y": 64, "z": -20 } } +``` + +When sending commands that accept enum parameters, you can pass **either** a string name or a numeric value: +```json +{ "command": "InteractEntity", "requestId": "abc", "parameters": [42, "Interact", "MainHand"] } +``` +or: +```json +{ "command": "InteractEntity", "requestId": "abc", "parameters": [42, 0, 0] } +``` + +Two dedicated commands let you query the full mapping tables: +- `GetItemTypeMappings` returns `{ "DiamondSword": 798, "Stone": 1, ... }` +- `GetEntityTypeMappings` returns `{ "Player": 128, "Zombie": 119, ... }` + +These are useful if your client needs a name-to-ID lookup for the current MCC version. + +## Reference + +- [Commands](Commands.md) - full list of available commands +- [Events](Events.md) - full list of emitted events + +## Compatibility + +- Requires any MCC version that supports `/script` (standalone MCCScript 1.0 bots). +- Uses only `System.Text.Json` (built into .NET), so no extra DLLs are needed. +- Compatible with [MCC.js](https://github.com/milutinke/MCC.js) and any WebSocket client library. From d5a9ae3deb6d1569f2ef7bb07ca82b4fd4b77eb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:54:04 +0000 Subject: [PATCH 5/7] Address code review feedback: fix password placeholder, IPv4 regex, and doc references Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/c762752f-46be-44f6-a05d-8c5effc8ef43 --- MinecraftClient/config/ChatBots/WebSocketBot.cs | 5 +++-- docs/guide/websocket/Commands.md | 4 ++-- docs/guide/websocket/README.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MinecraftClient/config/ChatBots/WebSocketBot.cs b/MinecraftClient/config/ChatBots/WebSocketBot.cs index a1111b77..875daa58 100644 --- a/MinecraftClient/config/ChatBots/WebSocketBot.cs +++ b/MinecraftClient/config/ChatBots/WebSocketBot.cs @@ -18,7 +18,8 @@ //using MinecraftClient.Scripting //using MinecraftClient -MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "wspass12345")); +// IMPORTANT: Change the password below before use! +MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "CHANGE_THIS_PASSWORD")); //MCCScript Extensions @@ -207,7 +208,7 @@ public class WebSocketBot : ChatBot private bool _gameJoined; private static readonly Regex Ipv4Regex = new( - @"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$", + @"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.){3}(25[0-5]|(2[0-4]|1\d|[1-9]|)\d)$", RegexOptions.Compiled); public WebSocketBot(string ip, int port, string password, bool debugMode = false) diff --git a/docs/guide/websocket/Commands.md b/docs/guide/websocket/Commands.md index 8fe8232b..d3ca44f7 100644 --- a/docs/guide/websocket/Commands.md +++ b/docs/guide/websocket/Commands.md @@ -31,7 +31,7 @@ Must be called before any other command (except `ChangeSessionId`). { "command": "Authenticate", "requestId": "auth-001", - "parameters": ["wspass12345"] + "parameters": ["your-password-here"] } ``` @@ -514,7 +514,7 @@ No parameters. ## Mapping Commands (New) -These commands address [issue #2805](https://github.com/MCCTeam/Minecraft-Console-Client/issues/2805), allowing clients to query enum mappings dynamically instead of maintaining hardcoded ID tables. +These commands let clients query enum mappings dynamically at runtime, so they do not need to maintain hardcoded numeric ID tables that break across MCC versions. For background, see [issue #2805](https://github.com/MCCTeam/Minecraft-Console-Client/issues/2805). ### `GetItemTypeMappings` diff --git a/docs/guide/websocket/README.md b/docs/guide/websocket/README.md index 45c45f1b..ee5000f3 100644 --- a/docs/guide/websocket/README.md +++ b/docs/guide/websocket/README.md @@ -13,11 +13,11 @@ You load it as a standalone script with `/script ChatBots/WebSocketBot.cs`. 1. Copy `config/ChatBots/WebSocketBot.cs` into your MCC `config/ChatBots/` folder (it ships in the repo under that path). 2. Open the file and edit the line near the top: ```csharp - MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "wspass12345")); + MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "CHANGE_THIS_PASSWORD")); ``` - Replace `127.0.0.1` with the IP to bind (use `+` or `*` for all interfaces). - Replace `8043` with your preferred port. - - Replace `wspass12345` with a strong password. + - Replace `CHANGE_THIS_PASSWORD` with a strong, unique password. 3. Optionally enable debug logging: ```csharp MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "mypassword", debugMode: true)); From ed0a69185f2266a8794f0d2df5a624708c1980c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:57:34 +0000 Subject: [PATCH 6/7] Fix timing attack in password comparison using constant-time XOR Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/c762752f-46be-44f6-a05d-8c5effc8ef43 --- MinecraftClient/config/ChatBots/WebSocketBot.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/config/ChatBots/WebSocketBot.cs b/MinecraftClient/config/ChatBots/WebSocketBot.cs index 875daa58..7cfa0a1a 100644 --- a/MinecraftClient/config/ChatBots/WebSocketBot.cs +++ b/MinecraftClient/config/ChatBots/WebSocketBot.cs @@ -1089,7 +1089,21 @@ public class WebSocketBot : ChatBot private void HandleAuthenticate(string sessionId, WebSocketSession session, string requestId, List parameters) { - if (parameters.Count == 0 || GetParam(parameters, 0) != _password) + if (parameters.Count == 0) + { + SendCommandResponse(sessionId, requestId, false, "Invalid password"); + return; + } + + var provided = GetParam(parameters, 0); + var expected = _password; + + // Fixed-time comparison to prevent timing attacks + var diff = provided.Length ^ expected.Length; + for (int i = 0; i < expected.Length; i++) + diff |= expected[i] ^ (i < provided.Length ? provided[i] : 0xFF); + + if (diff != 0) { SendCommandResponse(sessionId, requestId, false, "Invalid password"); return; From eeb4383099a50af0c865c7d6d4df58099187733e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:32:39 +0000 Subject: [PATCH 7/7] Fix Roslyn compiler references for WebSocket/HttpListener and fix MCCScript using directive semicolons Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/6fa57d65-60be-4a4b-b15a-ad3541ac6de3 --- .../Scripting/DynamicRun/Builder/Compiler.cs | 10 +++++++- .../config/ChatBots/WebSocketBot.cs | 25 ++++++------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs index 2b201d0b..c7524acd 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs @@ -134,6 +134,12 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder assemblyrefs.Add(new("System.Runtime")); assemblyrefs.Add(new("System.Private.Uri")); assemblyrefs.Add(new("System.Net.Requests")); + assemblyrefs.Add(new("System.Net.WebSockets")); + assemblyrefs.Add(new("System.Net.HttpListener")); + assemblyrefs.Add(new("System.Net.Primitives")); + assemblyrefs.Add(new("System.Net.Sockets")); + assemblyrefs.Add(new("Microsoft.Win32.Primitives")); + assemblyrefs.Add(new("System.Collections.Concurrent")); foreach (var refs in assemblyrefs) { Assembly? loadedAssembly; @@ -182,7 +188,9 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder // Add facade assemblies needed for Roslyn compilation when referencing // libraries that target netstandard (e.g. Brigadier.NET). var runtimeDir = Path.GetDirectoryName(SystemPrivateCoreLib)!; - foreach (var facadeName in new[] { "netstandard.dll", "System.Runtime.dll", "System.Private.Uri.dll", "System.Net.Requests.dll" }) + foreach (var facadeName in new[] { "netstandard.dll", "System.Runtime.dll", "System.Private.Uri.dll", "System.Net.Requests.dll", + "System.Net.WebSockets.dll", "System.Net.HttpListener.dll", "System.Net.Primitives.dll", "System.Net.Sockets.dll", + "Microsoft.Win32.Primitives.dll", "System.Collections.Concurrent.dll" }) { var facadePath = Path.Combine(runtimeDir, facadeName); if (File.Exists(facadePath)) diff --git a/MinecraftClient/config/ChatBots/WebSocketBot.cs b/MinecraftClient/config/ChatBots/WebSocketBot.cs index 7cfa0a1a..4437c567 100644 --- a/MinecraftClient/config/ChatBots/WebSocketBot.cs +++ b/MinecraftClient/config/ChatBots/WebSocketBot.cs @@ -1,22 +1,11 @@ //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 +//using System.Collections.Concurrent; +//using System.Net.Sockets; +//using System.Net.WebSockets; +//using System.Text.Json; +//using System.Text.Json.Serialization; +//using System.Threading.Tasks; +//using MinecraftClient.CommandHandler; // IMPORTANT: Change the password below before use! MCC.LoadBot(new WebSocketBot("127.0.0.1", 8043, "CHANGE_THIS_PASSWORD"));