diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 0b2e02f9..f2f285b4 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -69,6 +69,149 @@ namespace MinecraftClient.Mapping } } + public static void LoadDefaultDimensions1206Plus() + { + // TODO: Move this to a JSON file. + + var defaultRegistryCodec = new Dictionary + { + { "minecraft:dimension_type", new Dictionary + { + { "value", new object[] + { + new Dictionary + { + { "name", "minecraft:overworld" }, + { "id", 0 }, + { "element", new Dictionary + { + { "piglin_safe", (byte)0 }, + { "natural", 1 }, + { "ambient_light", 0.0 }, + { "monster_spawn_block_light_limit", 0 }, + { "infiniburn", "#minecraft:infiniburn_overworld" }, + { "respawn_anchor_works", 0 }, + { "has_skylight", 1 }, + { "bed_works", 1 }, + { "effects", "minecraft:overworld" }, + { "has_raids", 1 }, + { "logical_height", 384 }, + { "coordinate_scale", 1.0 }, + { "monster_spawn_light_level", new Dictionary + { + { "min_inclusive", 0 }, + { "max_inclusive", 7 }, + { "type", "minecraft:uniform" } + } + }, + { "min_y", -64 }, + { "ultrawarm", 0 }, + { "has_ceiling", 0 }, + { "height", 384 } + } + } + }, + new Dictionary + { + { "name", "minecraft:overworld_caves" }, + { "id", 1 }, + { "element", new Dictionary + { + { "piglin_safe", (byte)0 }, + { "natural", 1 }, + { "ambient_light", 0.0 }, + { "monster_spawn_block_light_limit", 0 }, + { "infiniburn", "#minecraft:infiniburn_overworld" }, + { "respawn_anchor_works", 0 }, + { "has_skylight", 1 }, + { "bed_works", 1 }, + { "effects", "minecraft:overworld" }, + { "has_raids", 1 }, + { "logical_height", 384 }, + { "coordinate_scale", 1.0 }, + { "monster_spawn_light_level", new Dictionary + { + { "min_inclusive", 0 }, + { "max_inclusive", 7 }, + { "type", "minecraft:uniform" } + } + }, + { "min_y", -64 }, + { "ultrawarm", 0 }, + { "has_ceiling", 1 }, + { "height", 384 } + } + } + }, + new Dictionary + { + { "name", "minecraft:the_end" }, + { "id", 2 }, + { "element", new Dictionary + { + { "piglin_safe", (byte)0 }, + { "natural", 0 }, + { "ambient_light", 0.0 }, + { "monster_spawn_block_light_limit", 0 }, + { "infiniburn", "#minecraft:infiniburn_end" }, + { "respawn_anchor_works", 0 }, + { "has_skylight", 0 }, + { "bed_works", 0 }, + { "effects", "minecraft:the_end" }, + { "fixed_time", 6000 }, + { "has_raids", 1 }, + { "logical_height", 256 }, + { "coordinate_scale", 1.0 }, + { "monster_spawn_light_level", new Dictionary + { + { "min_inclusive", 0 }, + { "max_inclusive", 7 }, + { "type", "minecraft:uniform" } + } + }, + { "min_y", 0 }, + { "ultrawarm", 0 }, + { "has_ceiling", 0 }, + { "height", 256 } + } + } + }, + new Dictionary + { + { "name", "minecraft:the_nether" }, + { "id", 3 }, + { "element", new Dictionary + { + { "piglin_safe", (byte)1 }, + { "natural", 0 }, + { "ambient_light", 0.1 }, + { "monster_spawn_block_light_limit", 15 }, + { "infiniburn", "#minecraft:infiniburn_nether" }, + { "respawn_anchor_works", 1 }, + { "has_skylight", 0 }, + { "bed_works", 0 }, + { "effects", "minecraft:the_nether" }, + { "fixed_time", 18000 }, + { "has_raids", 0 }, + { "logical_height", 128 }, + { "coordinate_scale", 8.0 }, + { "monster_spawn_light_level", 7 }, + { "min_y", 0 }, + { "ultrawarm", 1 }, + { "has_ceiling", 1 }, + { "height", 256 } + } + } + } + } + } + } + } + }; + + StoreDimensionList(defaultRegistryCodec); + } + /// /// Store one dimension - Directly used in 1.16.2 to 1.18.2 /// @@ -92,7 +235,6 @@ namespace MinecraftClient.Mapping curDimension = dimensionList[name]; // Should not fail } - /// /// Get current dimension /// diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 8149a06c..35cc97e4 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -148,9 +148,11 @@ namespace MinecraftClient public List GetLoadedChatBots() { return new List(bots); } public void GetCookie(string key, out byte[]? data) => Cookies.TryGetValue(key, out data); public void SetCookie(string key, byte[] data) => Cookies[key] = data; + public void DeleteCookie(string key) => Cookies.Remove(key, out var data); - readonly TcpClient client; - readonly IMinecraftCom handler; + TcpClient client; + IMinecraftCom handler; + SessionToken _sessionToken; CancellationTokenSource? cmdprompt = null; Tuple? timeoutdetector = null; @@ -186,6 +188,7 @@ namespace MinecraftClient this.port = port; this.protocolversion = protocolversion; this.playerKeyPair = playerKeyPair; + _sessionToken = session; Log = Settings.Config.Logging.LogToFile ? new FileLogLogger(Config.AppVar.ExpandVars(Settings.Config.Logging.LogFile), Settings.Config.Logging.PrependTimestamp) @@ -294,6 +297,77 @@ namespace MinecraftClient } } } + + public void Transfer(string newHost, int newPort) + { + try + { + Log.Info($"Initiating a transfer to: {host}:{port}"); + + // Unload bots + UnloadAllBots(); + bots.Clear(); + + // Close existing connection + client.Close(); + + // Establish new connection + client = ProxyHandler.NewTcpClient(newHost, newPort); + client.ReceiveBufferSize = 1024 * 1024; + client.ReceiveTimeout = Config.Main.Advanced.TcpTimeout * 1000; + + // Reinitialize the protocol handler + handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, null, this); + Log.Info($"Connected to {host}:{port}"); + + // Retry login process + if (handler.Login(playerKeyPair, _sessionToken)) + { + foreach (var bot in botsOnHold) + BotLoad(bot, false); + botsOnHold.Clear(); + + Log.Info("Successfully transferred connection and logged in."); + cmdprompt = new CancellationTokenSource(); + ConsoleInteractive.ConsoleReader.BeginReadThread(); + ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; + } + else + { + Log.Error("Failed to login to the new host."); + throw new Exception("Login failed after transfer."); + } + } + catch (Exception ex) + { + Log.Error($"Transfer to {newHost}:{newPort} failed: {ex.Message}"); + + // Handle reconnection attempts + if (timeoutdetector != null) + { + timeoutdetector.Item2.Cancel(); + timeoutdetector = null; + } + + if (ReconnectionAttemptsLeft > 0) + { + Log.Info($"Reconnecting... Attempts left: {ReconnectionAttemptsLeft}"); + Thread.Sleep(5000); + ReconnectionAttemptsLeft--; + Program.Restart(); + } + else if (InternalConfig.InteractiveMode) + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + Program.HandleFailure(); + } + + throw new Exception("Transfer failed and reconnection attempts exhausted."); + } + } /// /// Register bots diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 43a45030..e089e78d 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -449,6 +449,9 @@ namespace MinecraftClient.Protocol.Handlers } else { + // TODO: Implement proper parsing for 1.20.6 when there is a custom data pack on the server + // THis is a temporary workaround to get the client to be useable asap + var registryId = dataTypes.ReadNextString(packetData); var entryCount = dataTypes.ReadNextVarInt(packetData); @@ -466,7 +469,8 @@ namespace MinecraftClient.Protocol.Handlers if (hasData) { - dataTypes.ReadNextNbt(packetData); // Never seem to be sent because hasData is always false + // TODO: Parse in case when the server data packs differ from the client + dataTypes.ReadNextNbt(packetData); } if (registryId == "minecraft:chat_type") @@ -476,9 +480,9 @@ namespace MinecraftClient.Protocol.Handlers if (registryId == "minecraft:chat_type") ChatParser.ReadChatType(avaliableChats); - else + else { - // TODO: 1.20.6 Somehow store this data from dimensionType in the World class + World.LoadDefaultDimensions1206Plus(); } } @@ -502,9 +506,8 @@ namespace MinecraftClient.Protocol.Handlers case ConfigurationPacketTypesIn.Transfer: var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - - // TODO: 1.20.6 Implement Host Chaging in the McClient class - // McClient.Instance?.Transfer(host, port); + + McClient.Instance?.Transfer(host, port); break; case ConfigurationPacketTypesIn.KnownDataPacks: @@ -673,7 +676,6 @@ namespace MinecraftClient.Protocol.Handlers // varInt: [1.9.1 to 1.15.2] // byte: below 1.9.1 string? dimensionTypeName = null; - int? dimensionTypeInt2 = null; Dictionary? dimensionType = null; switch (protocolVersion) { @@ -681,9 +683,6 @@ namespace MinecraftClient.Protocol.Handlers { switch (protocolVersion) { - case >= MC_1_20_6_Version: - dimensionTypeInt2 = dataTypes.ReadNextVarInt(packetData); - break; case >= MC_1_19_Version: dimensionTypeName = dataTypes.ReadNextString(packetData); // Dimension Type: Identifier @@ -731,9 +730,6 @@ namespace MinecraftClient.Protocol.Handlers case < MC_1_20_6_Version: World.SetDimension(dimensionTypeName!); break; - case >= MC_1_20_6_Version: - // TODO: 1.20.6 Set the dimension (use dimensionTypeInt) - break; } } @@ -782,8 +778,19 @@ namespace MinecraftClient.Protocol.Handlers else { dataTypes.ReadNextBool(packetData); // Do limited crafting - var dimensionTypeName = - dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + + // Dimension Type (string bellow 1.20.6, VarInt for 1.20.6+) + var dimensionTypeName = protocolVersion < MC_1_20_6_Version + ? dataTypes.ReadNextString(packetData) // < 1.20.6 + : (dataTypes.ReadNextVarInt(packetData) switch // 1.20.6+ // TODO: Use values from the registry + { + 0 => "minecraft:overworld", + 1 => "minecraft:overworld_caves", + 2 => "minecraft:the_end", + 3 => "minecraft:the_nether", + _ => null + } ?? "minecraft:overworld"); + dataTypes.ReadNextString(packetData); // Dimension Name (World Name) - 1.16 and above if (handler.GetTerrainEnabled()) @@ -1240,14 +1247,20 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.Respawn: string? dimensionTypeNameRespawn = null; Dictionary? dimensionTypeRespawn = null; - int? dimensionTypeInt = null; if (protocolVersion >= MC_1_16_Version) { switch (protocolVersion) { case >= MC_1_20_6_Version: - dimensionTypeInt = dataTypes.ReadNextVarInt(packetData); + dimensionTypeNameRespawn = dataTypes.ReadNextVarInt(packetData) switch // 1.20.6+ // TODO: Use values from the registry + { + 0 => "minecraft:overworld", + 1 => "minecraft:overworld_caves", + 2 => "minecraft:the_end", + 3 => "minecraft:the_nether", + _ => null + } ?? "minecraft:overworld"; break; case >= MC_1_19_Version: dimensionTypeNameRespawn = @@ -1286,12 +1299,9 @@ namespace MinecraftClient.Protocol.Handlers World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); World.SetDimension(dimensionName); break; - case < MC_1_20_6_Version: + case <= MC_1_20_6_Version: World.SetDimension(dimensionTypeNameRespawn!); break; - case >= MC_1_20_6_Version: - // TODO: 1.20.6 Set the dimension (use dimensionTypeInt) - break; } } @@ -2759,8 +2769,7 @@ namespace MinecraftClient.Protocol.Handlers var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - // TODO: 1.20.6 Implement Host Chaging in the McClient class - // McClient.Instance?.Transfer(host, port); + McClient.Instance?.Transfer(host, port); break; default: @@ -3393,14 +3402,14 @@ namespace MinecraftClient.Protocol.Handlers SendMessageAcknowledgment(ConsumeAcknowledgment()); } } - + /// - /// Send a chat command to the server - 1.19 and above + /// Send a chat command to the server, with or without signing based on the online mode and version. /// /// Command - /// PlayerKeyPair + /// PlayerKeyPair (optional) /// True if properly sent - public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair) + public bool SendChatCommand(string command, PlayerKeyPair? playerKeyPair = null) { if (string.IsNullOrEmpty(command)) return true; @@ -3410,85 +3419,78 @@ namespace MinecraftClient.Protocol.Handlers log.Debug($"chat command = {command}"); + if (protocolVersion >= MC_1_20_6_Version && !isOnlineMode) + { + List fields = new(); + fields.AddRange(dataTypes.GetString(command)); + SendPacket(PacketTypesOut.ChatCommand, fields); + return true; + } + try { - List>? needSigned = null; // List< Argument Name, Argument Value > - if (playerKeyPair != null && isOnlineMode && protocolVersion >= MC_1_19_Version - && Config.Signature is { LoginWithSecureProfile: true, SignMessageInCommand: true }) + List>? needSigned = null; + + if (protocolVersion >= MC_1_19_Version && Config.Signature is { LoginWithSecureProfile: true, SignMessageInCommand: true }) needSigned = DeclareCommands.CollectSignArguments(command); lock (MessageSigningLock) { - var acknowledgment1192 = - protocolVersion == MC_1_19_2_Version ? ConsumeAcknowledgment() : null; + var acknowledgment1192 = protocolVersion == MC_1_19_2_Version ? ConsumeAcknowledgment() : null; - var (acknowledgment1193, bitset1193, messageCount1193) = - protocolVersion >= MC_1_19_3_Version - ? lastSeenMessagesCollector.Collect_1_19_3() - : new(Array.Empty(), Array.Empty(), 0); + var (acknowledgment1193, bitset1193, messageCount1193) = protocolVersion >= MC_1_19_3_Version + ? lastSeenMessagesCollector.Collect_1_19_3() + : new(Array.Empty(), Array.Empty(), 0); List fields = new(); - - // Command: String fields.AddRange(dataTypes.GetString(command)); - - // Timestamp: Instant(Long) var timeNow = DateTimeOffset.UtcNow; fields.AddRange(DataTypes.GetLong(timeNow.ToUnixTimeMilliseconds())); - if (needSigned == null || needSigned!.Count == 0) + if (needSigned == null || needSigned.Count == 0) { - fields.AddRange(DataTypes.GetLong(0)); // Salt: Long - fields.AddRange(DataTypes.GetVarInt(0)); // Signature Length: VarInt + fields.AddRange(DataTypes.GetLong(0)); + fields.AddRange(DataTypes.GetVarInt(0)); } else { var uuid = handler.GetUserUuid(); var salt = GenerateSalt(); - fields.AddRange(salt); // Salt: Long - fields.AddRange(DataTypes.GetVarInt(needSigned.Count)); // Signature Length: VarInt + fields.AddRange(salt); + fields.AddRange(DataTypes.GetVarInt(needSigned.Count)); foreach (var (argName, message) in needSigned) { - fields.AddRange(dataTypes.GetString(argName)); // Argument name: String - + fields.AddRange(dataTypes.GetString(argName)); var sign = protocolVersion switch { - MC_1_19_Version => playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, - ref salt), - MC_1_19_2_Version => playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, - ref salt, acknowledgment1192!.lastSeen), - _ => playerKeyPair!.PrivateKey.SignMessage(message, uuid, chatUuid, messageIndex++, - timeNow, ref salt, acknowledgment1193) + MC_1_19_Version => playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt), + MC_1_19_2_Version => playerKeyPair!.PrivateKey.SignMessage(message, uuid, timeNow, ref salt, acknowledgment1192!.lastSeen), + _ => playerKeyPair!.PrivateKey.SignMessage(message, uuid, chatUuid, messageIndex++, timeNow, ref salt, acknowledgment1193) }; if (protocolVersion <= MC_1_19_2_Version) - fields.AddRange(DataTypes.GetVarInt(sign.Length)); // Signature length: VarInt + fields.AddRange(DataTypes.GetVarInt(sign.Length)); - fields.AddRange(sign); // Signature: Byte Array + fields.AddRange(sign); } } if (protocolVersion <= MC_1_19_2_Version) - fields.AddRange(dataTypes.GetBool(false)); // Signed Preview: Boolean + fields.AddRange(dataTypes.GetBool(false)); switch (protocolVersion) { case MC_1_19_2_Version: - // Message Acknowledgment (1.19.2) - fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment1192!, - isOnlineMode && Config.Signature.LoginWithSecureProfile)); + fields.AddRange(dataTypes.GetAcknowledgment(acknowledgment1192!, isOnlineMode && Config.Signature.LoginWithSecureProfile)); break; case >= MC_1_19_3_Version: - // message count fields.AddRange(DataTypes.GetVarInt(messageCount1193)); - - // Acknowledged: BitSet fields.AddRange(bitset1193); break; } - SendPacket(PacketTypesOut.ChatCommand, fields); + SendPacket(protocolVersion < MC_1_20_6_Version ? PacketTypesOut.ChatCommand : PacketTypesOut.SignedChatCommand, fields); } return true; @@ -3506,7 +3508,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + /// /// Send a chat message to the server /// @@ -4687,21 +4689,22 @@ namespace MinecraftClient.Protocol.Handlers if (hasPayload) packet.AddRange(dataTypes.GetArray(data!)); // Payload Data Array Size + Data Array - switch(currentState) + switch (currentState) { - case CurrentState.Login: + case CurrentState.Login: SendPacket(0x04, packet); break; - - case CurrentState.Configuration: + + case CurrentState.Configuration: SendPacket(ConfigurationPacketTypesOut.CookieResponse, packet); break; - + case CurrentState.Play: SendPacket(PacketTypesOut.CookieResponse, packet); break; } + McClient.Instance?.DeleteCookie(name); return true; } catch (SocketException) diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index d6033e9b..a64021b5 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -46,6 +46,9 @@ namespace MinecraftClient.Protocol ILogger GetLogger(); void GetCookie(string key, out byte[]? data); void SetCookie(string key, byte[] data); + void DeleteCookie(string key); + + void Transfer(string newHost, int newPort); /// /// Invoke a task on the main thread, wait for completion and retrieve return value.