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