Basic support for minecraft 1.19 (#2084)

* merge commit from milutinke
* chat signature & encrypted login
* Bug fix :EncryptionResponse format error below 1.18.2
* Implemented chat command signature
* Chat message parsing and verification for 1.19
* Add signature settings
* Update Simplified Chinese Translation
* Clear up comments
* Fix wrong variable naming
* Bug fix: SignatureV2 Processing
This commit is contained in:
BruceChen 2022-08-15 23:55:44 +08:00 committed by GitHub
parent d9f1a77ac2
commit a8bbb1ac76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 5218 additions and 1174 deletions

View file

@ -13,6 +13,7 @@ using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Mapping;
using MinecraftClient.Inventory;
using MinecraftClient.Logger;
using MinecraftClient.Protocol.Keys;
namespace MinecraftClient
{
@ -23,24 +24,24 @@ namespace MinecraftClient
{
public static int ReconnectionAttemptsLeft = 0;
private static readonly List<string> cmd_names = new List<string>();
private static readonly Dictionary<string, Command> cmds = new Dictionary<string, Command>();
private readonly Dictionary<Guid, string> onlinePlayers = new Dictionary<Guid, string>();
private static readonly List<string> cmd_names = new();
private static readonly Dictionary<string, Command> cmds = new();
private readonly Dictionary<Guid, PlayerInfo> onlinePlayers = new();
private static bool commandsLoaded = false;
private Queue<string> chatQueue = new Queue<string>();
private Queue<string> chatQueue = new();
private static DateTime nextMessageSendTime = DateTime.MinValue;
private Queue<Action> threadTasks = new Queue<Action>();
private object threadTasksLock = new object();
private Queue<Action> threadTasks = new();
private object threadTasksLock = new();
private readonly List<ChatBot> bots = new List<ChatBot>();
private static readonly List<ChatBot> botsOnHold = new List<ChatBot>();
private static Dictionary<int, Container> inventories = new Dictionary<int, Container>();
private readonly List<ChatBot> bots = new();
private static readonly List<ChatBot> botsOnHold = new();
private static Dictionary<int, Container> inventories = new();
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new Dictionary<string, List<ChatBot>>();
private readonly List<string> registeredServerPluginChannels = new List<String>();
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new();
private readonly List<string> registeredServerPluginChannels = new();
private bool terrainAndMovementsEnabled;
private bool terrainAndMovementsRequested = false;
@ -48,9 +49,9 @@ namespace MinecraftClient
private bool inventoryHandlingRequested = false;
private bool entityHandlingEnabled;
private object locationLock = new object();
private object locationLock = new();
private bool locationReceived = false;
private World world = new World();
private World world = new();
private Queue<Location> steps;
private Queue<Location> path;
private Location location;
@ -59,6 +60,7 @@ namespace MinecraftClient
private float playerYaw;
private float playerPitch;
private double motionY;
private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..)
private string host;
private int port;
@ -66,10 +68,12 @@ namespace MinecraftClient
private string username;
private string uuid;
private string sessionid;
private PlayerKeyPair playerKeyPair;
private DateTime lastKeepAlive;
private object lastKeepAliveLock = new object();
private object lastKeepAliveLock = new();
private int respawnTicks = 0;
private int gamemode = 0;
private bool isSupportPreviewsChat;
private int playerEntityID;
@ -81,7 +85,7 @@ namespace MinecraftClient
private byte CurrentSlot = 0;
// Entity handling
private Dictionary<int, Entity> entities = new Dictionary<int, Entity>();
private Dictionary<int, Entity> entities = new();
// server TPS
private long lastAge = 0;
@ -89,12 +93,9 @@ namespace MinecraftClient
private double serverTPS = 0;
private double averageTPS = 20;
private const int maxSamples = 5;
private List<double> tpsSamples = new List<double>(maxSamples);
private List<double> tpsSamples = new(maxSamples);
private double sampleSum = 0;
// players latency
private Dictionary<string, int> playersLatency = new Dictionary<string, int>();
// ChatBot OnNetworkPacket event
private bool networkPacketCaptureEnabled = false;
@ -105,9 +106,11 @@ namespace MinecraftClient
public string GetSessionID() { return sessionid; }
public Location GetCurrentLocation() { return location; }
public float GetYaw() { return playerYaw; }
public int GetSequenceId() { return sequenceId; }
public float GetPitch() { return playerPitch; }
public World GetWorld() { return world; }
public Double GetServerTPS() { return averageTPS; }
public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; }
public float GetHealth() { return playerHealth; }
public int GetSaturation() { return playerFoodSaturation; }
public int GetLevel() { return playerLevel; }
@ -122,7 +125,7 @@ namespace MinecraftClient
TcpClient client;
IMinecraftCom handler;
CancellationTokenSource cmdprompt = null;
CancellationTokenSource? cmdprompt = null;
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
public ILogger Log;
@ -136,9 +139,9 @@ namespace MinecraftClient
/// <param name="server_ip">The server IP</param>
/// <param name="port">The server port to use</param>
/// <param name="protocolversion">Minecraft protocol version to use</param>
public McClient(string username, string uuid, string sessionID, int protocolversion, ForgeInfo forgeInfo, string server_ip, ushort port)
public McClient(string username, string uuid, string sessionID, PlayerKeyPair playerKeyPair, int protocolversion, ForgeInfo forgeInfo, string server_ip, ushort port)
{
StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, false, "");
StartClient(username, uuid, sessionID, playerKeyPair, server_ip, port, protocolversion, forgeInfo, false, "");
}
/// <summary>
@ -151,9 +154,9 @@ namespace MinecraftClient
/// <param name="port">The server port to use</param>
/// <param name="protocolversion">Minecraft protocol version to use</param>
/// <param name="command">The text or command to send.</param>
public McClient(string username, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, string command)
public McClient(string username, string uuid, string sessionID, PlayerKeyPair playerKeyPair, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, string command)
{
StartClient(username, uuid, sessionID, server_ip, port, protocolversion, forgeInfo, true, command);
StartClient(username, uuid, sessionID, playerKeyPair, server_ip, port, protocolversion, forgeInfo, true, command);
}
/// <summary>
@ -167,7 +170,7 @@ namespace MinecraftClient
/// <param name="uuid">The player's UUID for online-mode authentication</param>
/// <param name="singlecommand">If set to true, the client will send a single command and then disconnect from the server</param>
/// <param name="command">The text or command to send. Will only be sent if singlecommand is set to true.</param>
private void StartClient(string user, string uuid, string sessionID, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, bool singlecommand, string command)
private void StartClient(string user, string uuid, string sessionID, PlayerKeyPair playerKeyPair, string server_ip, ushort port, int protocolversion, ForgeInfo forgeInfo, bool singlecommand, string command)
{
terrainAndMovementsEnabled = Settings.TerrainAndMovements;
inventoryHandlingEnabled = Settings.InventoryHandling;
@ -180,6 +183,7 @@ namespace MinecraftClient
this.host = server_ip;
this.port = port;
this.protocolversion = protocolversion;
this.playerKeyPair = playerKeyPair;
this.Log = Settings.LogToFile
? new FileLogLogger(Settings.ExpandVars(Settings.LogFile), Settings.PrependTimestamp)
@ -227,7 +231,7 @@ namespace MinecraftClient
handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, forgeInfo, this);
Log.Info(Translations.Get("mcc.version_supported"));
if (!singlecommand)
if (!singlecommand)
{
timeoutdetector = new(new Thread(new ParameterizedThreadStart(TimeoutDetector)), new CancellationTokenSource());
timeoutdetector.Item1.Name = "MCC Connection timeout detector";
@ -236,11 +240,11 @@ namespace MinecraftClient
try
{
if (handler.Login())
if (handler.Login(this.playerKeyPair))
{
if (singlecommand)
{
handler.SendChatMessage(command);
handler.SendChatMessage(command, playerKeyPair);
Log.Info(Translations.Get("mcc.single_cmd", command));
Thread.Sleep(5000);
handler.Disconnect();
@ -294,7 +298,7 @@ namespace MinecraftClient
ReconnectionAttemptsLeft--;
Program.Restart();
}
else if (!singlecommand && Settings.interactiveMode)
else if (!singlecommand && Settings.interactiveMode)
{
ConsoleInteractive.ConsoleReader.StopReadThread();
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
@ -331,7 +335,7 @@ namespace MinecraftClient
if (chatQueue.Count > 0 && nextMessageSendTime < DateTime.Now)
{
string text = chatQueue.Dequeue();
handler.SendChatMessage(text);
handler.SendChatMessage(text, playerKeyPair);
nextMessageSendTime = DateTime.Now + Settings.messageCooldown;
}
}
@ -402,23 +406,23 @@ namespace MinecraftClient
do
{
Thread.Sleep(TimeSpan.FromSeconds(15));
if (((CancellationToken)o!).IsCancellationRequested)
return;
lock (lastKeepAliveLock)
{
if (lastKeepAlive.AddSeconds(30) < DateTime.Now)
{
if (((CancellationToken)o!).IsCancellationRequested)
return;
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.Get("error.timeout"));
return;
}
}
}
while (! ( (CancellationToken)o! ).IsCancellationRequested);
while (!((CancellationToken)o!).IsCancellationRequested);
}
/// <summary>
@ -448,13 +452,13 @@ namespace MinecraftClient
handler.Dispose();
}
if (cmdprompt != null)
if (cmdprompt != null)
{
cmdprompt.Cancel();
cmdprompt = null;
}
if (timeoutdetector != null)
if (timeoutdetector != null)
{
timeoutdetector.Item2.Cancel();
timeoutdetector = null;
@ -521,7 +525,7 @@ namespace MinecraftClient
}
}
if (!will_restart)
if (!will_restart)
{
ConsoleInteractive.ConsoleReader.StopReadThread();
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
@ -533,23 +537,24 @@ namespace MinecraftClient
#endregion
#region Command prompt and internal MCC commands
private void ConsoleReaderOnMessageReceived(object? sender, string e)
{
if (client.Client == null)
return;
if (client.Client.Connected)
if (client.Client.Connected)
{
new Thread(() => {
new Thread(() =>
{
InvokeOnMainThread(() => HandleCommandPromptText(e));
}).Start();
}
else
return;
}
/// <summary>
/// Allows the user to send chat messages, commands, and leave the server.
/// Process text from the MCC command prompt on the main thread.
@ -640,7 +645,7 @@ namespace MinecraftClient
/// <param name="response_msg">May contain a confirmation or error message after processing the command, or "" otherwise.</param>
/// <param name="localVars">Local variables passed along with the command</param>
/// <returns>TRUE if the command was indeed an internal MCC command</returns>
public bool PerformInternalCommand(string command, ref string response_msg, Dictionary<string, object> localVars = null)
public bool PerformInternalCommand(string command, ref string response_msg, Dictionary<string, object>? localVars = null)
{
/* Process the provided command */
@ -1012,6 +1017,9 @@ namespace MinecraftClient
/// <returns>All players latency</returns>
public Dictionary<string, int> GetPlayersLatency()
{
Dictionary<string, int> playersLatency = new();
foreach (var player in onlinePlayers)
playersLatency.Add(player.Value.Name, player.Value.Ping);
return playersLatency;
}
@ -1047,7 +1055,11 @@ namespace MinecraftClient
{
lock (onlinePlayers)
{
return onlinePlayers.Values.Distinct().ToArray();
string[] playerNames = new string[onlinePlayers.Count];
int idx = 0;
foreach (var player in onlinePlayers)
playerNames[idx++] = player.Value.Name;
return playerNames;
}
}
@ -1062,12 +1074,28 @@ namespace MinecraftClient
{
foreach (Guid key in onlinePlayers.Keys)
{
uuid2Player.Add(key.ToString(), onlinePlayers[key]);
uuid2Player.Add(key.ToString(), onlinePlayers[key].Name);
}
}
return uuid2Player;
}
/// <summary>
/// Get player info from uuid
/// </summary>
/// <param name="uuid">Player's UUID</param>
/// <returns>Player info</returns>
public PlayerInfo? GetPlayerInfo(Guid uuid)
{
lock (onlinePlayers)
{
if (onlinePlayers.ContainsKey(uuid))
return onlinePlayers[uuid];
else
return null;
}
}
#endregion
#region Action methods: Perform an action on the Server
@ -1083,7 +1111,7 @@ namespace MinecraftClient
/// <param name="timeout">How long to wait until the path is evaluated (default: 5 seconds)</param>
/// <remarks>When location is unreachable, computation will reach timeout, then optionally fallback to a close location within maxOffset</remarks>
/// <returns>True if a path has been found</returns>
public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout=null)
public bool MoveTo(Location location, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, TimeSpan? timeout = null)
{
lock (locationLock)
{
@ -1244,7 +1272,7 @@ namespace MinecraftClient
/// <returns>TRUE if the item was successfully used</returns>
public bool UseItemOnHand()
{
return InvokeOnMainThread(() => handler.SendUseItem(0));
return InvokeOnMainThread(() => handler.SendUseItem(0, this.sequenceId));
}
/// <summary>
@ -1548,7 +1576,7 @@ namespace MinecraftClient
/// <param name="count">Item count</param>
/// <param name="nbt">Item NBT</param>
/// <returns>TRUE if item given successfully</returns>
public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary<string, object> nbt = null)
public bool DoCreativeGive(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt = null)
{
return InvokeOnMainThread(() => handler.SendCreativeInventoryAction(slot, itemType, count, nbt));
}
@ -1634,7 +1662,7 @@ namespace MinecraftClient
/// <returns>TRUE if successfully placed</returns>
public bool PlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand)
{
return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace));
return InvokeOnMainThread(() => handler.SendPlayerBlockPlacement((int)hand, location, blockFace, this.sequenceId));
}
/// <summary>
@ -1660,9 +1688,9 @@ namespace MinecraftClient
// Send dig start and dig end, will need to wait for server response to know dig result
// See https://wiki.vg/How_to_Write_a_Client#Digging for more details
return handler.SendPlayerDigging(0, location, blockFace)
return handler.SendPlayerDigging(0, location, blockFace, this.sequenceId)
&& (!swingArms || DoAnimation((int)Hand.MainHand))
&& handler.SendPlayerDigging(2, location, blockFace);
&& handler.SendPlayerDigging(2, location, blockFace, this.sequenceId);
}
/// <summary>
@ -1724,7 +1752,7 @@ namespace MinecraftClient
/// Teleporting to other entityies is NOT implemented yet
public bool Spectate(Entity entity)
{
if(entity.Type == EntityType.Player)
if (entity.Type == EntityType.Player)
{
return SpectateByUUID(entity.UUID);
}
@ -1740,9 +1768,9 @@ namespace MinecraftClient
/// <param name="UUID">UUID of player/entity to teleport to</param>
public bool SpectateByUUID(Guid UUID)
{
if(GetGamemode() == 3)
if (GetGamemode() == 3)
{
if(InvokeRequired)
if (InvokeRequired)
return InvokeOnMainThread(() => SpectateByUUID(UUID));
return handler.SendSpectate(UUID);
}
@ -1764,7 +1792,7 @@ namespace MinecraftClient
/// </example>
/// <param name="action">Action to execute on each bot</param>
/// <param name="botList">Only fire the event for the specified bot list (default: all bots)</param>
private void DispatchBotEvent(Action<ChatBot> action, IEnumerable<ChatBot> botList = null)
private void DispatchBotEvent(Action<ChatBot> action, IEnumerable<ChatBot>? botList = null)
{
ChatBot[] selectedBots;
@ -1872,7 +1900,7 @@ namespace MinecraftClient
/// Check if the client is currently processing a Movement.
/// </summary>
/// <returns>true if a movement is currently handled</returns>
public bool ClientIsMoving()
public bool ClientIsMoving()
{
return terrainAndMovementsEnabled && locationReceived && ((steps != null && steps.Count > 0) || (path != null && path.Count > 0));
}
@ -1969,31 +1997,40 @@ namespace MinecraftClient
}
/// <summary>
/// Received some text from the server
/// Received chat/system message from the server
/// </summary>
/// <param name="text">Text received</param>
/// <param name="isJson">TRUE if the text is JSON-Encoded</param>
public void OnTextReceived(string text, bool isJson)
/// <param name="message">Message received</param>
public void OnTextReceived(ChatMessage message)
{
UpdateKeepAlive();
List<string> links = new List<string>();
string json = null;
List<string> links = new();
string messageText;
if (isJson)
if (message.isJson)
{
json = text;
text = ChatParser.ParseText(json, links);
if (message.isSignedChat)
{
if (!Settings.ShowIllegalSignedChat && !message.isSystemChat && !(bool)message.isSignatureLegal!)
return;
messageText = ChatParser.ParseSignedChat(message, links);
}
else
messageText = ChatParser.ParseText(message.content, links);
}
else
{
messageText = message.content;
}
Log.Chat(text);
Log.Chat(messageText);
if (Settings.DisplayChatLinks)
foreach (string link in links)
Log.Chat(Translations.Get("mcc.link", link));
DispatchBotEvent(bot => bot.GetText(text));
DispatchBotEvent(bot => bot.GetText(text, json));
DispatchBotEvent(bot => bot.GetText(messageText));
DispatchBotEvent(bot => bot.GetText(messageText, message.content));
}
/// <summary>
@ -2106,20 +2143,25 @@ namespace MinecraftClient
/// <summary>
/// Triggered when a new player joins the game
/// </summary>
/// <param name="uuid">UUID of the player</param>
/// <param name="name">Name of the player</param>
public void OnPlayerJoin(Guid uuid, string name)
/// <param name="player">player info</param>
public void OnPlayerJoin(PlayerInfo player)
{
//Ignore placeholders eg 0000tab# from TabListPlus
if (!ChatBot.IsValidName(name))
if (!ChatBot.IsValidName(player.Name))
return;
if (player.Name == username)
{
// 1.19+ offline server is possible to return different uuid
this.uuid = player.UUID.ToString().Replace("-", string.Empty);
}
lock (onlinePlayers)
{
onlinePlayers[uuid] = name;
onlinePlayers[player.UUID] = player;
}
DispatchBotEvent(bot => bot.OnPlayerJoin(uuid, name));
DispatchBotEvent(bot => bot.OnPlayerJoin(player.UUID, player.Name));
}
/// <summary>
@ -2128,13 +2170,13 @@ namespace MinecraftClient
/// <param name="uuid">UUID of the player</param>
public void OnPlayerLeave(Guid uuid)
{
string username = null;
string? username = null;
lock (onlinePlayers)
{
if (onlinePlayers.ContainsKey(uuid))
{
username = onlinePlayers[uuid];
username = onlinePlayers[uuid].Name;
onlinePlayers.Remove(uuid);
}
}
@ -2191,7 +2233,7 @@ namespace MinecraftClient
/// <summary>
/// Called when an entity effects
/// </summary>
public void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags)
public void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary<string, object>? factorCodec)
{
if (entities.ContainsKey(entityid))
DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags));
@ -2202,10 +2244,10 @@ namespace MinecraftClient
/// </summary>
public void OnSpawnPlayer(int entityID, Guid uuid, Location location, byte Yaw, byte Pitch)
{
string playerName = null;
string? playerName = null;
if (onlinePlayers.ContainsKey(uuid))
playerName = onlinePlayers[uuid];
Entity playerEntity = new Entity(entityID, EntityType.Player, location, uuid, playerName);
playerName = onlinePlayers[uuid].Name;
Entity playerEntity = new(entityID, EntityType.Player, location, uuid, playerName);
OnSpawnEntity(playerEntity);
}
@ -2243,7 +2285,7 @@ namespace MinecraftClient
// Further regular gamemode change events
if (onlinePlayers.ContainsKey(uuid))
{
string playerName = onlinePlayers[uuid];
string playerName = onlinePlayers[uuid].Name;
if (playerName == this.username)
this.gamemode = gamemode;
DispatchBotEvent(bot => bot.OnGamemodeUpdate(playerName, uuid, gamemode));
@ -2429,11 +2471,11 @@ namespace MinecraftClient
/// <param name="latency">Latency</param>
public void OnLatencyUpdate(Guid uuid, int latency)
{
string playerName = null;
if (onlinePlayers.ContainsKey(uuid))
{
playerName = onlinePlayers[uuid];
playersLatency[playerName] = latency;
PlayerInfo player = onlinePlayers[uuid];
player.Ping = latency;
string playerName = player.Name;
foreach (KeyValuePair<int, Entity> ent in entities)
{
if (ent.Value.UUID == uuid && ent.Value.Name == playerName)
@ -2599,6 +2641,52 @@ namespace MinecraftClient
DispatchBotEvent(bot => bot.OnEntityAnimation(entity, animation));
}
}
/// <summary>
/// Will be called when a Synchronization sequence is recevied, this sequence need to be sent when breaking or placing blocks
/// </summary>
/// <param name="sequenceId">Sequence ID</param>
public void OnBlockChangeAck(int sequenceId)
{
this.sequenceId = sequenceId;
}
/// <summary>
/// This method is called when the protocol handler receives server data
/// </summary>
/// <param name="hasMotd">Indicates if the server has a motd message</param>
/// <param name="motd">Server MOTD message</param>
/// <param name="hasIcon">Indicates if the server has a an icon</param>
/// <param name="iconBase64">Server icon in Base 64 format</param>
/// <param name="previewsChat">Indicates if the server previews chat</param>
public void OnServerDataRecived(bool hasMotd, string motd, bool hasIcon, string iconBase64, bool previewsChat)
{
this.isSupportPreviewsChat = previewsChat;
}
/// <summary>
/// This method is called when the protocol handler receives "Set Display Chat Preview" packet
/// </summary>
/// <param name="previewsChat">Indicates if the server previews chat</param>
public void OnChatPreviewSettingUpdate(bool previewsChat)
{
this.isSupportPreviewsChat = previewsChat;
}
/// <summary>
/// This method is called when the protocol handler receives "Login Success" packet
/// </summary>
/// <param name="uuid">The player's UUID received from the server</param>
/// <param name="userName">The player's username received from the server</param>
/// <param name="playerProperty">Tuple<Name, Value, Signature(empty if there is no signature)></param>
public void OnLoginSuccess(Guid uuid, string userName, Tuple<string, string, string>[]? playerProperty)
{
//string UUID = uuid.ToString().Replace("-", String.Empty);
//Log.Info("now UUID = " + this.uuid);
//Log.Info("new UUID = " + UUID);
////handler.SetUserUUID(UUID);
}
#endregion
}
}