Minecraft-Console-Client/MinecraftClient/ChatBots/WebSocketBot.cs
2023-11-19 12:57:09 +00:00

1341 lines
No EOL
52 KiB
C#

using System;
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.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.CommandHandler;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
using MinecraftClient.Scripting;
using Newtonsoft.Json;
using Tomlet.Attributes;
namespace MinecraftClient.ChatBots;
internal class SessionEventArgs : EventArgs
{
public string SessionId { get; }
public SessionEventArgs(string sessionId)
{
SessionId = sessionId;
}
}
internal class MessageReceivedEventArgs : EventArgs
{
public string SessionId { get; }
public string Message { get; }
public MessageReceivedEventArgs(string sessionId, string message)
{
SessionId = sessionId;
Message = message;
}
}
internal class WebSocketSession
{
public string SessionId { get; set; }
public WebSocket WebSocket { get; set; }
public WebSocketSession(string sessionId, WebSocket webSocket)
{
SessionId = sessionId;
WebSocket = webSocket;
}
}
internal class WebSocketServer
{
public readonly ConcurrentDictionary<string, WebSocketSession> Sessions;
public event EventHandler<SessionEventArgs>? NewSession;
public event EventHandler<SessionEventArgs>? SessionDropped;
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
private HttpListener? listener;
public WebSocketServer()
{
Sessions = new ConcurrentDictionary<string, WebSocketSession>();
}
public async Task Start(string ipAddress, int port)
{
listener = new HttpListener();
listener.Prefixes.Add($"http://{ipAddress}:{port}/");
listener.Start();
while (listener.IsListening)
{
var context = await listener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
var sessionGuid = Guid.NewGuid().ToString();
var webSocketContext = await context.AcceptWebSocketAsync(null);
var webSocket = webSocketContext.WebSocket;
var webSocketSession = new WebSocketSession(sessionGuid, webSocket);
NewSession?.Invoke(this, new SessionEventArgs(sessionGuid));
Sessions.TryAdd(sessionGuid, webSocketSession);
_ = ProcessWebSocketSession(webSocketSession);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
}
public async Task Stop()
{
foreach (var session in Sessions)
{
await session.Value.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server shutting down",
CancellationToken.None);
}
Sessions.Clear();
listener?.Stop();
}
private async Task ProcessWebSocketSession(WebSocketSession webSocketSession)
{
var buffer = new byte[1024];
try
{
while (webSocketSession.WebSocket.State == WebSocketState.Open)
{
var receiveResult =
await webSocketSession.WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer),
CancellationToken.None);
if (receiveResult.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, receiveResult.Count);
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(webSocketSession.SessionId, message));
}
else if (receiveResult.MessageType == WebSocketMessageType.Close)
{
await webSocketSession.WebSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Connection closed by the client",
CancellationToken.None);
break;
}
}
}
finally
{
Sessions.TryRemove(webSocketSession.SessionId, out _);
SessionDropped?.Invoke(this, new SessionEventArgs(webSocketSession.SessionId));
}
}
public bool RenameSession(string oldSessionId, string newSessionId)
{
if (!Sessions.ContainsKey(oldSessionId) || Sessions.ContainsKey(newSessionId))
return false;
if (!Sessions.TryRemove(oldSessionId, out var webSocketSession))
return false;
webSocketSession.SessionId = newSessionId;
if (Sessions.TryAdd(newSessionId, webSocketSession))
return true;
webSocketSession.SessionId = oldSessionId;
if (!Sessions.TryAdd(oldSessionId, webSocketSession))
throw new Exception("Failed to add back the old session after failed rename");
return false;
}
public async Task SendToSession(string sessionId, string message)
{
try
{
if (Sessions.TryGetValue(sessionId, out var webSocketSession))
{
var buffer = Encoding.UTF8.GetBytes(message);
await webSocketSession.WebSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text,
true,
CancellationToken.None);
}
}
catch (WebSocketException ex)
{
if (ex.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionReset })
{
if (Sessions.ContainsKey(sessionId))
Sessions.Remove(sessionId, out _);
}
}
}
}
internal class WsChatBotCommand
{
[JsonProperty("command")] public string Command { get; set; } = "";
[JsonProperty("requestId")] public string RequestId { get; set; } = "";
[JsonProperty("parameters")] public object[]? Parameters { get; set; }
}
internal class WsCommandResponder
{
private WebSocketBot _bot;
private string _sessionId;
private string _command;
private string _requestId;
public WsCommandResponder(WebSocketBot bot, string sessionId, string command, string requestId)
{
_bot = bot;
_sessionId = sessionId;
_command = command;
_requestId = requestId;
}
private void SendCommandResponse(bool success, string result, bool overrideAuth = false)
{
_bot.SendCommandResponse(_sessionId, success, _requestId, _command, result, overrideAuth);
}
public void SendErrorResponse(string error, bool overrideAuth = false)
{
SendCommandResponse(false, error, overrideAuth);
}
public void SendSuccessResponse(string result, bool overrideAuth = false)
{
SendCommandResponse(true, result, overrideAuth);
}
public void SendSuccessResponse(bool overrideAuth = false)
{
SendSuccessResponse(JsonConvert.SerializeObject(true), overrideAuth);
}
public string Quote(string text)
{
return $"\"{text}\"";
}
}
internal class NbtData
{
public NBT? nbt { get; set; }
}
internal class NBT
{
public Dictionary<string, object>? nbt { get; set; }
}
internal class NbtDictionaryConverter : JsonConverter<Dictionary<string, object>>
{
public override void WriteJson(JsonWriter writer, Dictionary<string, object>? value, JsonSerializer serializer)
=> throw new NotImplementedException();
public override Dictionary<string, object>? ReadJson(JsonReader reader, Type objectType,
Dictionary<string, object>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var keyValuePairs = serializer.Deserialize<IEnumerable<KeyValuePair<string, object>>>(reader);
return new(keyValuePairs!);
}
}
public class WebSocketBot : ChatBot
{
private string? _ip;
private int _port;
private string? _password;
private WebSocketServer? _server;
private List<string> _authenticatedSessions;
private List<(string, string)> _waitingEvents;
public static Configs Config = new();
[TomlDoNotInlineObject]
public class Configs
{
[NonSerialized] private const string BotName = "Websocket";
public bool Enabled = false;
[TomlInlineComment("$ChatBot.WebSocketBot.Ip$")]
public string? Ip = "127.0.0.1";
[TomlInlineComment("$ChatBot.WebSocketBot.Port$")]
public int Port = 8043;
[TomlInlineComment("$ChatBot.WebSocketBot.Password$")]
public string? Password = Guid.NewGuid().ToString().Replace("-", "").Trim().ToLower();
[TomlInlineComment("$ChatBot.WebSocketBot.DebugMode$")]
public bool DebugMode = false;
}
public WebSocketBot()
{
var match = Regex.Match(Config.Ip!, @"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}");
if (!match.Success)
{
LogToConsole(Translations.bot_WebSocketBot_failed_to_start_ip);
return;
}
if (Config.Port > 65535)
{
LogToConsole(string.Format(Translations.bot_WebSocketBot_failed_to_start_port, _port.ToString()));
return;
}
_ip = Config.Ip;
_port = Config.Port;
_password = Config.Password;
_authenticatedSessions = new();
_waitingEvents = new();
}
public override void Initialize()
{
Task.Run(() =>
{
_authenticatedSessions.Clear();
if (_server != null)
{
SendEvent("OnWsRestarting", "");
_server.Stop(); // If you await, this will freeze the task and the websocket won't work
_server = null;
}
try
{
LogToConsole(Translations.bot_WebSocketBot_starting);
_server = new();
_server.Start(_ip!, _port); // If you await, this will freeze the task and the websocket won't work
LogToConsole(string.Format(Translations.bot_WebSocketBot_started, _ip, _port.ToString()));
foreach (var (eventName, data) in _waitingEvents)
SendEvent(eventName, data);
}
catch (Exception e)
{
LogToConsole(string.Format(Translations.bot_WebSocketBot_failed_to_start_custom, e));
return;
}
_server.NewSession += (_, session) =>
LogToConsole(string.Format(Translations.bot_WebSocketBot_new_session, session.SessionId));
_server.SessionDropped += (_, session) =>
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_disconnected, session.SessionId));
_server.MessageReceived += (_, messageObject) =>
{
if (!ProcessWebsocketCommand(messageObject.SessionId, _password!, messageObject.Message))
return;
var command = messageObject.Message;
command = command.StartsWith('/') ? command[1..] : $"send {command}";
CmdResult response = new();
PerformInternalCommand(command, ref response);
SendSessionEvent(messageObject.SessionId, "OnMccCommandResponse", $"{{\"response\": \"{response}\"}}");
};
});
}
private bool ProcessWebsocketCommand(string sessionId, string password, string message)
{
message = message.Trim();
if (string.IsNullOrEmpty(message))
return false;
if (message.StartsWith('{'))
{
try
{
if (Config.DebugMode)
LogDebugToConsole($"\n\n\tGot command\n\n\t{message}\n\n");
var cmd = JsonConvert.DeserializeObject<WsChatBotCommand>(message)!;
var responder = new WsCommandResponder(this, sessionId, cmd.Command, cmd.RequestId);
// Allow session name changing without authenticating for easier identification
if (cmd.Command.Equals("ChangeSessionId", StringComparison.OrdinalIgnoreCase))
{
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expected 1 (newSessionid)!"), true);
return false;
}
var newId = (cmd.Parameters[0] as string)!;
switch (newId.Length)
{
case 0:
responder.SendErrorResponse(responder.Quote("Please provide a valid session ID!"),
true);
return false;
case > 32:
responder.SendErrorResponse(
responder.Quote("The session ID can't be longer than 32 characters!"), true);
return false;
}
if (!_server!.RenameSession(sessionId, newId))
{
responder.SendErrorResponse(
responder.Quote("Failed to change the session id to: '" + newId + "'"),
true);
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_id_failed_to_change, sessionId,
newId));
return false;
}
// If the session is authenticated, remove the old session id and add the new one
if (_authenticatedSessions.Contains(sessionId))
{
_authenticatedSessions.Remove(sessionId);
_authenticatedSessions.Add(newId);
}
responder.SendSuccessResponse(
responder.Quote("The session ID was successfully changed to: '" + newId + "'"), true);
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_id_changed, sessionId, newId));
return false;
}
// Authentication and session commands
if (password.Length != 0)
{
if (!_authenticatedSessions.Contains(sessionId))
{
// Special case for authentication
if (cmd.Command.Equals("Authenticate", StringComparison.OrdinalIgnoreCase))
{
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expected 1 (password)!"), true);
return false;
}
var pass = (cmd.Parameters[0] as string)!;
if (pass.Length == 0)
{
responder.SendErrorResponse(
responder.Quote(
"Please provide a valid password! (Example: 'Authenticate password123')"),
true);
return false;
}
if (!pass.Equals(password))
{
responder.SendErrorResponse(responder.Quote("Incorrect password provided!"), true);
return false;
}
_authenticatedSessions.Add(sessionId);
responder.SendSuccessResponse(responder.Quote("Successfully authenticated!"), true);
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_authenticated, sessionId));
return false;
}
responder.SendErrorResponse(
responder.Quote("You must authenticate in order to send and receive data!"), true);
return false;
}
}
else
{
if (!_authenticatedSessions.Contains(sessionId))
{
responder.SendSuccessResponse(responder.Quote("Successfully authenticated!"));
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_authenticated, sessionId));
_authenticatedSessions.Add(sessionId);
return false;
}
}
// Process other commands
switch (cmd.Command)
{
case "LogToConsole":
if (cmd.Parameters == null || cmd.Parameters.Length > 1 || cmd.Parameters.Length < 1)
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting a single parameter!"));
return false;
}
LogToConsole((cmd.Parameters[0] as string)!);
responder.SendSuccessResponse();
break;
case "LogDebugToConsole":
if (cmd.Parameters == null || cmd.Parameters.Length > 1 || cmd.Parameters.Length < 1)
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting a single parameter!"));
return false;
}
LogDebugToConsole((cmd.Parameters[0] as string)!);
responder.SendSuccessResponse();
break;
case "LogToConsoleTranslated":
if (cmd.Parameters == null || cmd.Parameters.Length > 1 || cmd.Parameters.Length < 1)
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting a single parameter!"));
return false;
}
LogToConsoleTranslated((cmd.Parameters[0] as string)!);
responder.SendSuccessResponse();
break;
case "LogDebugToConsoleTranslated":
if (cmd.Parameters!.Length > 1 || cmd.Parameters.Length < 1)
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting a single parameter!"));
return false;
}
LogDebugToConsoleTranslated((cmd.Parameters[0] as string)!);
responder.SendSuccessResponse();
break;
case "ReconnectToTheServer":
if (cmd.Parameters is not { Length: 2 })
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 2 parameters (extraAttempts, delaySeconds)!"));
return false;
}
ReconnectToTheServer(Convert.ToInt32(cmd.Parameters[0]), Convert.ToInt32(cmd.Parameters[1]));
responder.SendSuccessResponse();
break;
case "DisconnectAndExit":
responder.SendSuccessResponse();
DisconnectAndExit();
break;
case "SendPrivateMessage":
if (cmd.Parameters is not { Length: 2 })
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 2 parameters (player, message)!"));
return false;
}
SendPrivateMessage((cmd.Parameters[0] as string)!, (cmd.Parameters[1] as string)!);
responder.SendSuccessResponse();
break;
case "RunScript":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (filename)!"));
return false;
}
RunScript((cmd.Parameters[0] as string)!);
responder.SendSuccessResponse();
break;
case "GetTerrainEnabled":
responder.SendSuccessResponse(GetTerrainEnabled().ToString().ToLower());
break;
case "SetTerrainEnabled":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (enabled)!"));
return false;
}
SetTerrainEnabled((bool)cmd.Parameters[0]);
responder.SendSuccessResponse();
break;
case "GetEntityHandlingEnabled":
responder.SendSuccessResponse(GetEntityHandlingEnabled().ToString().ToLower());
break;
case "Sneak":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (on)!"));
return false;
}
Sneak((bool)cmd.Parameters[0]);
responder.SendSuccessResponse();
break;
case "SendEntityAction":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (actionType)!"));
return false;
}
SendEntityAction(((Protocol.EntityActionType)(Convert.ToInt32(cmd.Parameters[0]))));
responder.SendSuccessResponse();
break;
case "DigBlock":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 3 ||
cmd.Parameters.Length > 5)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 1 or 3 parameter(s) (location, swingArms?, lookAtBlock?)!"));
return false;
}
var location = new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]), Convert.ToInt32(cmd.Parameters[2]));
if (location.DistanceSquared(GetCurrentLocation().EyesLocation()) > 25)
{
responder.SendErrorResponse(
responder.Quote("The block you're trying to dig is too far away!"));
return false;
}
if (GetWorld().GetBlock(location).Type == Material.Air)
{
responder.SendErrorResponse(responder.Quote("The block you're trying to dig is is air!"));
return false;
}
var result = cmd.Parameters.Length switch
{
3 => DigBlock(location),
4 => DigBlock(location, (bool)cmd.Parameters[3]),
5 => DigBlock(location, (bool)cmd.Parameters[3], (bool)cmd.Parameters[4]),
_ => false
};
responder.SendSuccessResponse(JsonConvert.SerializeObject(result));
break;
case "SetSlot":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (slotNumber)!"));
return false;
}
SetSlot(Convert.ToInt32(cmd.Parameters[0]));
responder.SendSuccessResponse();
break;
case "GetWorld":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetWorld()));
break;
case "GetEntities":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetEntities()));
break;
case "GetPlayersLatency":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetPlayersLatency()));
break;
case "GetCurrentLocation":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetCurrentLocation()));
break;
case "MoveToLocation":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 3 ||
cmd.Parameters.Length > 8)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 1 or 7 parameter(s) (x, y, z, allowUnsafe?, allowDirectTeleport?, maxOffset?, minoffset?, timeout?)!"));
return false;
}
var allowUnsafe = false;
var allowDirectTeleport = false;
var maxOffset = 0;
var minOffset = 0;
TimeSpan? timeout = null;
if (cmd.Parameters.Length >= 4)
allowUnsafe = (bool)cmd.Parameters[3];
if (cmd.Parameters.Length >= 5)
allowDirectTeleport = (bool)cmd.Parameters[4];
if (cmd.Parameters.Length >= 6)
maxOffset = Convert.ToInt32(cmd.Parameters[5]);
if (cmd.Parameters.Length >= 7)
minOffset = Convert.ToInt32(cmd.Parameters[6]);
if (cmd.Parameters.Length == 8)
timeout = TimeSpan.FromSeconds(Convert.ToInt32(cmd.Parameters[7]));
var canMove = MoveToLocation(
new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]),
Convert.ToInt32(cmd.Parameters[2])),
allowUnsafe,
allowDirectTeleport,
maxOffset,
minOffset,
timeout);
responder.SendSuccessResponse(JsonConvert.SerializeObject(canMove));
break;
case "ClientIsMoving":
responder.SendSuccessResponse(JsonConvert.SerializeObject(ClientIsMoving()));
break;
case "LookAtLocation":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 3 ||
cmd.Parameters.Length > 3)
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 3 parameter(s) (x, y, z)!"));
return false;
}
LookAtLocation(new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]), Convert.ToInt32(cmd.Parameters[2])));
responder.SendSuccessResponse();
break;
case "GetTimestamp":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetTimestamp()));
break;
case "GetServerPort":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetServerPort()));
break;
case "GetServerHost":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetServerHost()));
break;
case "GetUsername":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetUsername()));
break;
case "GetGamemode":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GameModeString(GetGamemode())));
break;
case "GetYaw":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetYaw()));
break;
case "GetPitch":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetPitch()));
break;
case "GetUserUUID":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetUserUUID()));
break;
case "GetOnlinePlayers":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetOnlinePlayers()));
break;
case "GetOnlinePlayersWithUUID":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetOnlinePlayersWithUUID()));
break;
case "GetServerTPS":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetServerTPS()));
break;
case "InteractEntity":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 2 ||
cmd.Parameters.Length > 3)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting at least 2 and at most 3 parameter(s) (entityId, interactionType, hand?)!"));
return false;
}
var interactionType = (InteractType)Convert.ToInt32(cmd.Parameters[1]);
var interactionHand = Hand.MainHand;
if (cmd.Parameters.Length == 3)
interactionHand = (Hand)Convert.ToInt32(cmd.Parameters[2]);
responder.SendSuccessResponse(JsonConvert.SerializeObject(
InteractEntity(Convert.ToInt32(cmd.Parameters[0]), interactionType, interactionHand)));
break;
case "CreativeGive":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 3 ||
cmd.Parameters.Length > 4)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting at least 3 and at most 4 parameter(s) (slotId, itemType, count, nbt?)!"));
return false;
}
NBT? nbt = null;
if (cmd.Parameters.Length == 4)
nbt = JsonConvert.DeserializeObject<NBT>(cmd.Parameters[3].ToString()!,
new NbtDictionaryConverter())!;
responder.SendSuccessResponse(
JsonConvert.SerializeObject(CreativeGive(
Convert.ToInt32(cmd.Parameters[0]),
(ItemType)Convert.ToInt32(cmd.Parameters[1]),
Convert.ToInt32(cmd.Parameters[2]),
nbt == null ? new Dictionary<string, object>() : nbt!.nbt!)
));
break;
case "CreativeDelete":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting at 1 parameter (slotId)!"));
return false;
}
responder.SendSuccessResponse(
JsonConvert.SerializeObject(CreativeDelete(Convert.ToInt32(cmd.Parameters[0]))));
break;
case "SendAnimation":
var hand = Hand.MainHand;
if (cmd.Parameters is { Length: 1 })
hand = (Hand)Convert.ToInt32(cmd.Parameters[0]);
responder.SendSuccessResponse(JsonConvert.SerializeObject(SendAnimation(hand)));
break;
case "SendPlaceBlock":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length < 4 ||
cmd.Parameters.Length > 4)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting at least 4 and at most 5 parameters (x, y, z, blockFace, hand?)!"));
return false;
}
var blockLocation = new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]), Convert.ToInt32(cmd.Parameters[2]));
var blockFacingDirection = (Direction)Convert.ToInt32(cmd.Parameters[3]);
var handToUse = Hand.MainHand;
if (cmd.Parameters.Length == 4)
handToUse = (Hand)Convert.ToInt32(cmd.Parameters[4]);
responder.SendSuccessResponse(
JsonConvert.SerializeObject(SendPlaceBlock(blockLocation, blockFacingDirection,
handToUse)));
break;
case "UseItemInHand":
responder.SendSuccessResponse(JsonConvert.SerializeObject(UseItemInHand()));
break;
case "GetInventoryEnabled":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetInventoryEnabled()));
break;
case "GetPlayerInventory":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetPlayerInventory()));
break;
case "GetInventories":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetInventories()));
break;
case "WindowAction":
if (cmd.Parameters == null || cmd.Parameters.Length == 0 || cmd.Parameters.Length != 3)
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 3 parameters (inventoryId, slotId, windowActionType)!"));
return false;
}
responder.SendSuccessResponse(
JsonConvert.SerializeObject(WindowAction(
Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]),
(WindowActionType)Convert.ToInt32(cmd.Parameters[2])
)));
break;
case "ChangeSlot":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (slotId)!"));
return false;
}
responder.SendSuccessResponse(
JsonConvert.SerializeObject(ChangeSlot((short)Convert.ToInt32(cmd.Parameters[0]))));
break;
case "GetCurrentSlot":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetCurrentSlot()));
break;
case "ClearInventories":
responder.SendSuccessResponse(JsonConvert.SerializeObject(ClearInventories()));
break;
case "UpdateSign":
if (cmd.Parameters is not { Length: 7 })
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 1 parameter (x, y, z, line1, line2, line3, line4)!"));
return false;
}
var signLocation = new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]), Convert.ToInt32(cmd.Parameters[2]));
responder.SendSuccessResponse(
JsonConvert.SerializeObject(UpdateSign(signLocation,
(string)cmd.Parameters[3],
(string)cmd.Parameters[4],
(string)cmd.Parameters[5],
(string)cmd.Parameters[6]
)));
break;
case "SelectTrade":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (selectedSlot)!"));
return false;
}
responder.SendSuccessResponse(
JsonConvert.SerializeObject(SelectTrade(Convert.ToInt32(cmd.Parameters[0]))));
break;
case "UpdateCommandBlock":
if (cmd.Parameters is not { Length: 6 })
{
responder.SendErrorResponse(responder.Quote(
"Invalid number of parameters, expecting 1 parameter (x, y, z, command, commandBlockMode, commandBlockFlags)!"));
return false;
}
var commandBlockLocation = new Location(Convert.ToInt32(cmd.Parameters[0]),
Convert.ToInt32(cmd.Parameters[1]), Convert.ToInt32(cmd.Parameters[2]));
responder.SendSuccessResponse(
UpdateCommandBlock(commandBlockLocation,
(string)cmd.Parameters[3],
(CommandBlockMode)Convert.ToInt32(cmd.Parameters[4]),
(CommandBlockFlags)Convert.ToInt32(cmd.Parameters[5])
).ToString().ToLower());
break;
case "CloseInventory":
if (cmd.Parameters is not { Length: 1 })
{
responder.SendErrorResponse(
responder.Quote("Invalid number of parameters, expecting 1 parameter (inventoryId)!"));
return false;
}
responder.SendSuccessResponse(CloseInventory(Convert.ToInt32(cmd.Parameters[0])).ToString()
.ToLower());
break;
case "GetMaxChatMessageLength":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetMaxChatMessageLength()));
break;
case "Respawn":
responder.SendSuccessResponse(JsonConvert.SerializeObject(Respawn()));
break;
case "GetProtocolVersion":
responder.SendSuccessResponse(JsonConvert.SerializeObject(GetProtocolVersion()));
break;
default:
responder.SendErrorResponse(
responder.Quote($"Unknown command {cmd.Command} received!"));
break;
}
}
catch (Exception e)
{
LogDebugToConsole(e.Message);
SendSessionEvent(sessionId, "OnWsCommandResponse",
"{\"success\": false, \"message\": \"An error occured, possible reasons: mail-formed json, type conversion, internal error\", \"stackTrace\": \"" +
Json.EscapeString(e.ToString()) + "\"}", true);
return false;
}
return false;
}
if (password.Length != 0)
{
if (!_authenticatedSessions.Contains(sessionId))
{
SendSessionEvent(sessionId, "OnWsCommandResponse",
"{\"error\": true, \"message\": \"You must authenticate in order to send and receive data!\"}",
true);
return false;
}
}
else
{
if (!_authenticatedSessions.Contains(sessionId))
{
SendSessionEvent(sessionId, "OnWsCommandResponse",
"{\"success\": true, \"message\": \"Successfully authenticated!\"}", true);
LogToConsole(string.Format(Translations.bot_WebSocketBot_session_authenticated, sessionId));
_authenticatedSessions.Add(sessionId);
}
}
return true;
}
public override void OnUnload()
{
if (_server != null)
{
SendEvent("OnWsConnectionClose", "");
_server.Stop();
_server = null;
}
_authenticatedSessions.Clear();
}
// ==========================================================================================
// Bot Events
// ==========================================================================================
public override void AfterGameJoined()
{
// Workaround to wait until the WebSocket server has been started
// This would fire before the WS server is started, this causing a null exception.
_waitingEvents.Add(("OnGameJoined", ""));
}
public override void OnBlockBreakAnimation(Entity entity, Location location, byte stage)
{
SendEvent("OnBlockBreakAnimation", new { entity, location, stage });
}
public override void OnEntityAnimation(Entity entity, byte animation)
{
SendEvent("OnEntityAnimation", new { entity, animation });
}
public override void GetText(string text)
{
text = GetVerbatim(text).Trim();
var message = "";
var username = "";
if (IsPrivateMessage(text, ref message, ref username))
SendEvent("OnChatPrivate", new { sender = username, message, rawText = text });
else if (IsChatMessage(text, ref message, ref username))
SendEvent("OnChatPublic", new { username, message, rawText = text });
else if (IsTeleportRequest(text, ref username))
SendEvent("OnTeleportRequest", new { sender = username, rawText = text });
}
public override void GetText(string text, string? json)
{
SendEvent("OnChatRaw", new { text, json });
}
public override bool OnDisconnect(DisconnectReason reason, string message)
{
var reasonString = reason switch
{
DisconnectReason.ConnectionLost => "Connection Lost",
DisconnectReason.UserLogout => "User Logout",
DisconnectReason.InGameKick => "In-Game Kick",
DisconnectReason.LoginRejected => "Login Rejected",
_ => "Unknown"
};
SendEvent("OnDisconnect", new { reason = reasonString, message });
return false;
}
public override void OnPlayerProperty(Dictionary<string, double> prop)
{
SendEvent("OnPlayerProperty", prop);
}
public override void OnServerTpsUpdate(double tps)
{
SendEvent("OnServerTpsUpdate", new { tps });
}
public override void OnTimeUpdate(long worldAge, long timeOfDay)
{
SendEvent("OnTimeUpdate", new { worldAge, timeOfDay });
}
public override void OnEntityMove(Entity entity)
{
SendEvent("OnEntityMove", entity);
}
public override void OnInternalCommand(string commandName, string commandParams, CmdResult result)
{
SendEvent("OnInternalCommand",
new { command = commandName, parameters = commandParams, result = result.ToString().Replace("\"", "'") });
}
public override void OnEntitySpawn(Entity entity)
{
SendEvent("OnEntitySpawn", entity);
}
public override void OnEntityDespawn(Entity entity)
{
SendEvent("OnEntityDespawn", entity);
}
public override void OnHeldItemChange(byte slot)
{
SendEvent("OnHeldItemChange", new { itemSlot = slot });
}
public override void OnHealthUpdate(float health, int food)
{
SendEvent("OnHealthUpdate", new { health, food });
}
public override void OnExplosion(Location explode, float strength, int recordCount)
{
SendEvent("OnExplosion", new { location = explode, strength, recordCount });
}
public override void OnSetExperience(float experienceBar, int level, int totalExperience)
{
SendEvent("OnSetExperience",
new { experienceBar, level, totalExperience });
}
public override void OnGamemodeUpdate(string playerName, Guid uuid, int gameMode)
{
SendEvent("OnGamemodeUpdate", new { playerName, uuid, gameMode = GameModeString(gameMode) });
}
public override void OnLatencyUpdate(string playerName, Guid uuid, int latency)
{
SendEvent("OnLatencyUpdate", new { playerName, uuid, latency });
}
public override void OnMapData(int mapId, byte scale, bool trackingPosition, bool locked, List<MapIcon> icons,
byte columnsUpdated, byte rowsUpdated, byte mapColumnX, byte mapRowZ, byte[]? colors)
{
SendEvent("OnMapData",
new
{
mapId, scale, trackingPosition, locked, icons, columnsUpdated, rowsUpdated, mapColumnX, mapRowZ,
colors
});
}
public override void OnTradeList(int windowId, List<VillagerTrade> trades, VillagerInfo villagerInfo)
{
SendEvent("OnTradeList", new { windowId, trades, villagerInfo });
}
public override void OnTitle(int action, string titleText, string subtitleText, string actionBarText, int fadein,
int stay, int fadeout, string json_)
{
SendEvent("OnTitle",
new
{
action, titleText, subtitleText, actionBarText,
fadeIn = fadein, stay, rawJson = json_
});
}
public override void OnEntityEquipment(Entity entity, int slot, Item? item)
{
SendEvent("OnEntityEquipment", new { entity, slot, item });
}
public override void OnEntityEffect(Entity entity, Effects effect, int amplifier, int duration, byte flags)
{
SendEvent("OnEntityEffect", new { entity, effect, amplifier, duration, flags });
}
public override void OnScoreboardObjective(string objectiveName, byte mode, string objectiveValue, int type,
string json_)
{
SendEvent("OnScoreboardObjective",
new { objectiveName, mode, objectiveValue, type, rawJson = json_ });
}
public override void OnUpdateScore(string entityName, int action, string objectiveName, int value)
{
SendEvent("OnUpdateScore",
new { entityName, action, objectiveName, type = value });
}
public override void OnInventoryUpdate(int inventoryId)
{
SendEvent("OnInventoryUpdate", new { inventoryId });
}
public override void OnInventoryOpen(int inventoryId)
{
SendEvent("OnInventoryOpen", new { inventoryId });
}
public override void OnInventoryClose(int inventoryId)
{
SendEvent("OnInventoryClose", new { inventoryId });
}
public override void OnPlayerJoin(Guid uuid, string name)
{
SendEvent("OnPlayerJoin", new { uuid, name });
}
public override void OnPlayerLeave(Guid uuid, string? name)
{
SendEvent("OnPlayerLeave", new { uuid, name = name ?? "null" });
}
public override void OnDeath()
{
SendEvent("OnDeath", "");
}
public override void OnRespawn()
{
SendEvent("OnRespawn", "");
}
public override void OnEntityHealth(Entity entity, float health)
{
SendEvent("OnEntityHealth", new { entity, health });
}
public override void OnEntityMetadata(Entity entity, Dictionary<int, object?>? metadata)
{
SendEvent("OnEntityMetadata", new { entity, metadata });
}
public override void OnPlayerStatus(byte statusId)
{
SendEvent("OnPlayerStatus", new { statusId });
}
public override void OnNetworkPacket(int packetID, List<byte> packetData, bool isLogin, bool isInbound)
{
SendEvent("OnNetworkPacket", new { packetId = packetID, isLogin, isInbound, packetData });
}
// ==========================================================================================
// Helper methods
// ==========================================================================================
private void SendEvent(string type, object data, bool overrideAuth = false)
{
if (_server == null)
return;
foreach (var (sessionId, _) in _server!.Sessions)
SendSessionEvent(sessionId, type, JsonConvert.SerializeObject(data), overrideAuth);
}
private void SendEvent(string type, string data, bool overrideAuth = false)
{
if (_server == null)
return;
foreach (var (sessionId, _) in _server.Sessions)
SendSessionEvent(sessionId, type, data, overrideAuth);
}
private void SendSessionEvent(string sessionId, string type, string data, bool overrideAuth = false)
{
if (sessionId.Length > 0 && (overrideAuth || _authenticatedSessions.Contains(sessionId)))
{
_server?.SendToSession(sessionId,
$"{{\"event\": \"{type}\", \"data\": {(string.IsNullOrEmpty(data) ? "null" : $"\"{Json.EscapeString(data)}\"")}}}")
.Wait();
if (!(type.Contains("Entity") || type.Equals("OnTimeUpdate") || type.Equals("OnServerTpsUpdate")) &&
Config.DebugMode)
LogDebugToConsole(
$"\n\n\tSending:\n\n\t{{\"event\": \"{type}\", \"data\": {(string.IsNullOrEmpty(data)
? "null"
: $"\"{Json.EscapeString(data)}\"")}}}\n\n");
}
}
public void SendCommandResponse(string sessionId, bool success, string requestId, string command,
string result, bool overrideAuth = false)
{
SendSessionEvent(sessionId, "OnWsCommandResponse",
$"{{\"success\": {success.ToString().ToLower()}, \"requestId\": \"{requestId}\", \"command\": \"{command}\", \"result\": {(string.IsNullOrEmpty(result) ? "null" : result)}}}",
overrideAuth);
}
private static string GameModeString(int gameMode)
{
return gameMode switch
{
0 => "survival",
1 => "creative",
2 => "adventure",
3 => "spectator",
_ => "unknown"
};
}
}