Merge remote-tracking branch 'origin/master' into feat/optimization

# Conflicts:
#	tools/run-creative-e2e.sh
This commit is contained in:
Anon 2026-04-03 15:44:59 +02:00
commit 22e987070a
106 changed files with 20031 additions and 3930 deletions

View file

@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers
return ReadNextNbt(cache, true);
}
/// <summary>
/// Read an ItemStackTemplate (26.1+) from a cache of bytes.
/// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch.
/// ItemStackTemplate is always non-empty (no count=0 sentinel).
/// </summary>
public Item ReadNextItemStackTemplate(Queue<byte> cache, ItemPalette itemPalette)
{
var itemId = ReadNextVarInt(cache);
var itemCount = ReadNextVarInt(cache);
var item = new Item(itemPalette.FromId(itemId), itemCount, null);
var numberOfComponentsToAdd = ReadNextVarInt(cache);
var numberofComponentsToRemove = ReadNextVarInt(cache);
var structuredComponentHandler = new StructuredComponentsHandler(protocolversion, this, itemPalette);
var strcturedComponentsToAdd = new List<StructuredComponent>(numberOfComponentsToAdd);
for (var i = 0; i < numberOfComponentsToAdd; i++)
{
var componentTypeId = ReadNextVarInt(cache);
strcturedComponentsToAdd.Add(structuredComponentHandler.Parse(componentTypeId, cache));
}
for (var i = 0; i < numberofComponentsToRemove; i++)
ReadNextVarInt(cache);
if (strcturedComponentsToAdd.Count > 0)
item.Components = strcturedComponentsToAdd;
return item;
}
/// <summary>
/// Read a single item slot from a cache of bytes and remove it from the cache
/// </summary>
@ -664,8 +695,10 @@ namespace MinecraftClient.Protocol.Handlers
}
}
return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
data);
entity.UUID = entityUUID;
return entity;
}
/// <summary>
@ -1021,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers
}
}
private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4;
private static double UnpackLpVec3(long packedAxis)
{
return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0;
}
/// <summary>
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+).
/// Variable-length encoding: first byte 0 = zero vector; otherwise
/// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation.
/// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+).
/// Returned vector is expressed in blocks per tick.
/// </summary>
public void ReadNextLpVec3(Queue<byte> cache)
public (double X, double Y, double Z) ReadNextLpVec3Values(Queue<byte> cache)
{
int first = ReadNextByte(cache);
if (first == 0)
return;
ReadNextByte(cache); // second byte
ReadData(4, cache); // uint32
if ((first & 4) == 4) // continuation bit set
ReadNextVarInt(cache);
return (0.0, 0.0, 0.0);
int second = ReadNextByte(cache);
uint high = (uint)ReadNextInt(cache);
long packed = ((long)high << 16) | (long)(second << 8) | (uint)first;
long scale = first & 3;
if (HasLpVec3Continuation(first))
scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2;
return (
UnpackLpVec3(packed >> 3) * scale,
UnpackLpVec3(packed >> 18) * scale,
UnpackLpVec3(packed >> 33) * scale
);
}
/// <summary>
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it.
/// </summary>
public void ReadNextLpVec3(Queue<byte> cache)
{
ReadNextLpVec3Values(cache);
}
/// <summary>
@ -1715,16 +1772,15 @@ namespace MinecraftClient.Protocol.Handlers
public byte[] GetLocation(Location location)
{
byte[] locationBytes;
ulong x = (ulong)(int)Math.Floor(location.X) & 0x3FFFFFF;
ulong y = (ulong)(int)Math.Floor(location.Y) & 0xFFF;
ulong z = (ulong)(int)Math.Floor(location.Z) & 0x3FFFFFF;
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
{
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
((((ulong)location.Z) & 0x3FFFFFF) << 12) |
(((ulong)location.Y) & 0xFFF));
locationBytes = BitConverter.GetBytes((x << 38) | (z << 12) | y);
}
else
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
((((ulong)location.Y) & 0xFFF) << 26) |
(((ulong)location.Z) & 0x3FFFFFF));
locationBytes = BitConverter.GetBytes((x << 38) | (y << 26) | z);
Array.Reverse(locationBytes); //Endianness
return locationBytes;

View file

@ -811,6 +811,11 @@ namespace MinecraftClient.Protocol.Handlers
return false; //Currently not implemented
}
public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll)
{
return false; //MC 1.8-1.12.1 recipe book not supported
}
public bool SendCloseWindow(int windowId)
{
return false; //Currently not implemented

File diff suppressed because it is too large Load diff

View file

@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol
bool ClickContainerButton(int windowId, int buttonId);
/// <summary>
/// Send a place recipe packet to the server for the active recipe book container.
/// </summary>
/// <param name="windowId">Id of the window being clicked</param>
/// <param name="recipeId">Recipe identifier to craft</param>
/// <param name="makeAll">True to craft as many items as possible</param>
/// <returns>True if packet was successfully sent</returns>
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
/// <summary>
/// Plays animation
/// </summary>

View file

@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol
/// <param name="onGround">TRUE if on ground</param>
void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround);
/// <summary>
/// Called when an entity velocity update packet is received.
/// Velocity values are in blocks per tick.
/// </summary>
/// <param name="entityID">Entity ID</param>
/// <param name="velocityX">Velocity X</param>
/// <param name="velocityY">Velocity Y</param>
/// <param name="velocityZ">Velocity Z</param>
void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ);
/// <summary>
/// Called when additional properties have been received for an entity
/// </summary>
@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol
/// <param name="affectedBlocks">Amount of affected blocks</param>
void OnExplosion(Location location, float strength, int affectedBlocks);
/// <summary>
/// Called when a sound packet is received.
/// </summary>
/// <param name="soundName">Sound key if available, otherwise null</param>
/// <param name="location">Sound location for world sounds, or null if unavailable</param>
/// <param name="category">Sound category id</param>
/// <param name="volume">Sound volume</param>
/// <param name="pitch">Sound pitch</param>
/// <param name="entityID">Source entity id for entity-sound packets, if any</param>
void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID);
/// <summary>
/// Called when a player's game mode has changed
/// </summary>
@ -434,6 +455,19 @@ namespace MinecraftClient.Protocol
/// <param name="factorCodec">factorCodec</param>
void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary<String, object>? factorCodec);
/// <summary>
/// Called when an entity has an effect removed
/// </summary>
/// <param name="entityid">Entity ID</param>
/// <param name="effect">Effect that was removed</param>
void OnRemoveEntityEffect(int entityid, Effects effect);
/// <summary>
/// Get the player's active effects
/// </summary>
/// <returns>Dictionary of active effects</returns>
Dictionary<Effects, EffectData> GetPlayerEffects();
/// <summary>
/// Called when Soreboard Objective
/// </summary>
@ -455,6 +489,23 @@ namespace MinecraftClient.Protocol
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat);
/// <summary>
/// Called when a Teams packet is received from the server.
/// </summary>
/// <param name="teamName">Internal team name (up to 16 chars)</param>
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
/// <param name="nameTagVisibility">Nametag visibility rule string. Present when method is 0 or 2.</param>
/// <param name="collisionRule">Collision rule string. Present when method is 0 or 2.</param>
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
string nameTagVisibility, string collisionRule, int color,
string prefix, string suffix, List<string> players);
/// <summary>
/// Called when the client received the Tab Header and Footer
/// </summary>
@ -504,6 +555,33 @@ namespace MinecraftClient.Protocol
public void SetCanSendMessage(bool canSendMessage);
/// <summary>
/// Called when recipe book recipes are added or replaced.
/// </summary>
/// <param name="recipes">Recipe entries to add</param>
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
/// <summary>
/// Called when recipe book recipes are removed.
/// </summary>
/// <param name="recipeIds">Recipe identifiers to remove</param>
public void OnRecipeBookRemove(string[] recipeIds);
/// <summary>
/// Called when achievement/advancement data is received from the server.
/// </summary>
/// <param name="added">Achievements that were added or updated</param>
/// <param name="removedIds">IDs of achievements that were removed</param>
/// <param name="reset">True if all existing state should be cleared before applying</param>
public void OnAchievementsUpdate(IReadOnlyList<Achievement> added, IReadOnlyList<string> removedIds, bool reset);
/// <summary>
/// Called when the server selects an advancement tab.
/// </summary>
/// <param name="tabId">The tab identifier, or null if no tab is selected</param>
public void OnSelectAdvancementTab(string? tabId);
/// <summary>
/// Send a click container button packet to the server.
/// Used for Enchanting table, Lectern, stone cutter and loom

View file

@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using DnsClient;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.Forge;
@ -388,6 +389,61 @@ namespace MinecraftClient.Protocol
}
}
private static readonly Regex VersionTokenRegex = new(@"\d+\.\d+(?:\.\d+)?", RegexOptions.Compiled);
private static readonly int[] SupportedProtocols18 =
[
4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404,
477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756,
757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
772, 773, 774, 775
];
/// <summary>
/// For multi-version servers (e.g. "Requires MC 1.8 / 1.21"), try to find the
/// highest protocol version that both the server and MCC support.
/// Returns true if the protocol was upgraded, with the new value in
/// <paramref name="protocolVersion"/>.
/// </summary>
public static bool TryUpgradeProtocolVersion(string versionName, ref int protocolVersion)
{
if (string.IsNullOrEmpty(versionName))
return false;
var matches = VersionTokenRegex.Matches(versionName);
if (matches.Count < 2)
return false;
int bestProtocol = protocolVersion;
string bestVersion = "";
foreach (Match m in matches)
{
int proto = MCVer2ProtocolVersion(m.Value);
if (proto <= 0)
continue;
if (Array.IndexOf(SupportedProtocols18, proto) < 0)
continue;
if (proto > bestProtocol)
{
bestProtocol = proto;
bestVersion = m.Value;
}
}
if (bestProtocol > protocolVersion && bestVersion.Length > 0)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(
Translations.mcc_server_info_version_upgrade,
ProtocolVersion2MCVer(protocolVersion), protocolVersion,
"§a" + bestVersion + "§8", bestProtocol));
protocolVersion = bestProtocol;
return true;
}
return false;
}
/// <summary>
/// Convert a network protocol version number to human-readable Minecraft version number
/// </summary>

View file

@ -0,0 +1,119 @@
using System;
using System.Text;
using MinecraftClient.Protocol.Message;
using MinecraftClient.Scripting;
namespace MinecraftClient.Protocol
{
internal static class ServerStatusDisplay
{
private const int MaxSamplePlayers = 10;
internal static void Show(ServerStatusInfo info)
{
if (ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBackend)
ShowTui(info, tuiBackend);
else
ShowClassic(info);
}
private static void ShowClassic(ServerStatusInfo info)
{
var sb = new StringBuilder();
sb.AppendLine();
sb.Append("§8§m");
sb.Append(new string('-', 50));
sb.AppendLine("§r");
if (!string.IsNullOrEmpty(info.MotdRaw))
{
try
{
sb.AppendLine(ChatParser.ParseText(info.MotdRaw));
}
catch
{
sb.AppendLine(info.MotdRaw);
}
}
sb.Append("§f");
sb.Append(Translations.mcc_server_info_label_server);
sb.Append(" §b");
sb.Append(info.Host);
sb.Append("§7:§b");
sb.AppendLine(info.Port.ToString());
sb.Append("§f");
sb.Append(Translations.mcc_server_info_label_version);
sb.Append(" §b");
sb.Append(ChatBot.GetVerbatim(info.VersionName));
sb.Append(" §7(");
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7"));
sb.AppendLine(")");
if (info.ResolvedProtocol != 0)
{
string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol);
sb.Append("§f");
sb.Append(Translations.mcc_server_info_label_connecting_as);
sb.Append(" §a");
sb.Append(resolvedMcVer);
sb.Append(" §7(");
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§a" + info.ResolvedProtocol + "§7"));
sb.AppendLine(")");
}
if (info.PingMs >= 0)
{
sb.Append("§f");
sb.Append(Translations.mcc_server_info_label_ping);
sb.Append(" §a");
sb.AppendLine(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs));
}
sb.Append("§f");
sb.Append(Translations.mcc_server_info_label_players);
sb.Append(" §a");
sb.Append(info.OnlinePlayers);
sb.Append("§7/§c");
sb.AppendLine(info.MaxPlayers.ToString());
if (info.SamplePlayers.Count > 0)
{
sb.Append("§f");
sb.AppendLine(Translations.mcc_server_info_label_online);
int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers);
for (int i = 0; i < shown; i++)
sb.AppendLine($" §a{info.SamplePlayers[i].Name}");
if (info.SamplePlayers.Count > shown)
sb.AppendLine($" §7{string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}");
}
sb.Append("§8§m");
sb.Append(new string('-', 50));
sb.Append("§r");
ConsoleIO.WriteLineFormatted(sb.ToString(), acceptnewlines: true);
}
private static void ShowTui(ServerStatusInfo info, Tui.TuiConsoleBackend backend)
{
var view = backend.GetView();
if (view is null)
{
ShowClassic(info);
return;
}
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
var panel = Tui.ServerStatusPanelBuilder.Build(info);
view.AppendControlToLog(panel);
});
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace MinecraftClient.Protocol
{
/// <summary>
/// Holds the structured result of a Minecraft server status (SLP) ping,
/// including MOTD, player counts, sample player list, version, and favicon.
/// </summary>
public sealed class ServerStatusInfo
{
public string Host { get; init; } = string.Empty;
public int Port { get; init; }
public string VersionName { get; init; } = string.Empty;
public int ProtocolVersion { get; init; }
public int ResolvedProtocol { get; set; }
public int OnlinePlayers { get; init; }
public int MaxPlayers { get; init; }
public List<SamplePlayer> SamplePlayers { get; init; } = [];
public string MotdRaw { get; init; } = string.Empty;
public string? FaviconBase64 { get; init; }
public long PingMs { get; init; }
public sealed class SamplePlayer
{
public string Name { get; init; } = string.Empty;
public string Id { get; init; } = string.Empty;
}
}
}