feat: add packet palette, data components, and protocol fixes for MC 1.21.2

Packet Palette (Phase 2.1):
- Create PacketPalette1212.cs with complete ID mapping for protocol 768
  (131 clientbound + 60 serverbound play packets, plus config packets)
- Add new PacketTypesIn enum values: EntityPositionSync, MoveMinecartAlongTrack,
  PlayerRotation, RecipeBookAdd/Remove/Settings, SetCursorItem, SetHeldSlot,
  SetPlayerInventory
- Add new PacketTypesOut enum values: BundleItemSelected, ClientTickEnd
- Update PacketType18Handler routing for 1.21.2

Data Components (Phase 1.4):
- Create StructuredComponentsRegistry1212 with 67 components (was 57 in 1.21)
  reflecting the new 1.21.2 DataComponents ordering
- Implement 11 new component parsers: ConsumableComponent, UseRemainderComponent,
  UseCooldownComponent, DamageResistantComponent, EnchantableComponent,
  EquippableComponent, RepairableComponent, GliderComponent, TooltipStyleComponent,
  DeathProtectionComponent, ItemModelComponent
- Create FoodComponent1212 (simplified: nutrition/saturation/canAlwaysEat only;
  eatSeconds/effects/usingConvertsTo moved to consumable/use_remainder)
- Create SubComponentRegistry1212 and route in StructuredComponentsHandler

Protocol Fixes:
- Fix PlayerPositionAndLook packet for 1.21.2 (new format: teleportId first,
  added deltaMovement Vec3, flags as Int instead of Byte)
- Fix login success packet (remove strictErrorHandling read for >= 1.21.2)
- Fix ClientSettings/ClientInformation packet (add particleStatus VarInt)
- Add SetHeldSlot as alias for HeldItemChange in packet handler

Verified: MCC connects to 1.21.2 vanilla server, stays connected, chat works.
Made-with: Cursor
This commit is contained in:
BruceChen 2026-03-20 03:25:20 +08:00
parent df833ae2a8
commit 57a0dedb33
20 changed files with 953 additions and 33 deletions

View file

@ -0,0 +1,129 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_21;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class ConsumableComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public float ConsumeSeconds { get; set; }
public int Animation { get; set; }
public SoundEventSubComponent? Sound { get; set; }
public bool HasConsumeParticles { get; set; }
public List<ConsumeEffectData> Effects { get; set; } = new();
public override void Parse(Queue<byte> data)
{
ConsumeSeconds = dataTypes.ReadNextFloat(data);
Animation = dataTypes.ReadNextVarInt(data);
Sound = (SoundEventSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.SoundEvent, data);
HasConsumeParticles = dataTypes.ReadNextBool(data);
var effectCount = dataTypes.ReadNextVarInt(data);
for (var i = 0; i < effectCount; i++)
{
var effectTypeId = dataTypes.ReadNextVarInt(data);
var effectData = ReadConsumeEffectPayload(effectTypeId, data);
Effects.Add(new ConsumeEffectData(effectTypeId, effectData));
}
}
private byte[] ReadConsumeEffectPayload(int effectTypeId, Queue<byte> data)
{
var payload = new List<byte>();
switch (effectTypeId)
{
case 0: // apply_effects: List<MobEffectInstance> + probability(float)
var effectCount = dataTypes.ReadNextVarInt(data);
payload.AddRange(DataTypes.GetVarInt(effectCount));
for (var i = 0; i < effectCount; i++)
payload.AddRange(ReadMobEffectInstance(data));
payload.AddRange(DataTypes.GetFloat(dataTypes.ReadNextFloat(data)));
break;
case 1: // remove_effects: HolderSet<MobEffect>
payload.AddRange(ReadHolderSet(data));
break;
case 2: // clear_all_effects: empty
break;
case 3: // teleport_randomly: float diameter
payload.AddRange(DataTypes.GetFloat(dataTypes.ReadNextFloat(data)));
break;
case 4: // play_sound: Holder<SoundEvent>
var sound = (SoundEventSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.SoundEvent, data);
payload.AddRange(sound.Serialize());
break;
}
return payload.ToArray();
}
private byte[] ReadMobEffectInstance(Queue<byte> data)
{
var result = new List<byte>();
var effectId = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(effectId));
result.AddRange(ReadMobEffectDetails(data));
return result.ToArray();
}
private byte[] ReadMobEffectDetails(Queue<byte> data)
{
var result = new List<byte>();
var amplifier = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(amplifier));
var duration = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(duration));
var ambient = dataTypes.ReadNextBool(data);
result.AddRange(DataTypes.GetBool(ambient));
var showParticles = dataTypes.ReadNextBool(data);
result.AddRange(DataTypes.GetBool(showParticles));
var showIcon = dataTypes.ReadNextBool(data);
result.AddRange(DataTypes.GetBool(showIcon));
var hasHiddenEffect = dataTypes.ReadNextBool(data);
result.AddRange(DataTypes.GetBool(hasHiddenEffect));
if (hasHiddenEffect)
result.AddRange(ReadMobEffectDetails(data));
return result.ToArray();
}
private byte[] ReadHolderSet(Queue<byte> data)
{
var result = new List<byte>();
var type = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(type));
if (type == 0)
{
var tagName = dataTypes.ReadNextString(data);
result.AddRange(DataTypes.GetString(tagName));
}
else
{
for (var i = 0; i < type - 1; i++)
{
var id = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(id));
}
}
return result.ToArray();
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetFloat(ConsumeSeconds));
data.AddRange(DataTypes.GetVarInt(Animation));
if (Sound != null) data.AddRange(Sound.Serialize());
data.AddRange(DataTypes.GetBool(HasConsumeParticles));
data.AddRange(DataTypes.GetVarInt(Effects.Count));
foreach (var effect in Effects)
{
data.AddRange(DataTypes.GetVarInt(effect.EffectTypeId));
data.AddRange(effect.Payload);
}
return new Queue<byte>(data);
}
public record ConsumeEffectData(int EffectTypeId, byte[] Payload);
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class DamageResistantComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public string Types { get; set; } = null!;
public override void Parse(Queue<byte> data)
{
Types = dataTypes.ReadNextString(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetString(Types));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,106 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_21;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class DeathProtectionComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public List<ConsumeEffectData> DeathEffects { get; set; } = new();
public override void Parse(Queue<byte> data)
{
var effectCount = dataTypes.ReadNextVarInt(data);
for (var i = 0; i < effectCount; i++)
{
var effectTypeId = dataTypes.ReadNextVarInt(data);
var effectData = ReadConsumeEffectPayload(effectTypeId, data);
DeathEffects.Add(new ConsumeEffectData(effectTypeId, effectData));
}
}
private byte[] ReadConsumeEffectPayload(int effectTypeId, Queue<byte> data)
{
var payload = new List<byte>();
switch (effectTypeId)
{
case 0: // apply_effects
var effectCount = dataTypes.ReadNextVarInt(data);
payload.AddRange(DataTypes.GetVarInt(effectCount));
for (var i = 0; i < effectCount; i++)
payload.AddRange(ReadMobEffectInstance(data));
payload.AddRange(DataTypes.GetFloat(dataTypes.ReadNextFloat(data)));
break;
case 1: // remove_effects
payload.AddRange(ReadHolderSet(data));
break;
case 2: // clear_all_effects
break;
case 3: // teleport_randomly
payload.AddRange(DataTypes.GetFloat(dataTypes.ReadNextFloat(data)));
break;
case 4: // play_sound
var sound = (SoundEventSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.SoundEvent, data);
payload.AddRange(sound.Serialize());
break;
}
return payload.ToArray();
}
private byte[] ReadMobEffectInstance(Queue<byte> data)
{
var result = new List<byte>();
result.AddRange(DataTypes.GetVarInt(dataTypes.ReadNextVarInt(data)));
result.AddRange(ReadMobEffectDetails(data));
return result.ToArray();
}
private byte[] ReadMobEffectDetails(Queue<byte> data)
{
var result = new List<byte>();
result.AddRange(DataTypes.GetVarInt(dataTypes.ReadNextVarInt(data)));
result.AddRange(DataTypes.GetVarInt(dataTypes.ReadNextVarInt(data)));
result.AddRange(DataTypes.GetBool(dataTypes.ReadNextBool(data)));
result.AddRange(DataTypes.GetBool(dataTypes.ReadNextBool(data)));
result.AddRange(DataTypes.GetBool(dataTypes.ReadNextBool(data)));
var hasHidden = dataTypes.ReadNextBool(data);
result.AddRange(DataTypes.GetBool(hasHidden));
if (hasHidden)
result.AddRange(ReadMobEffectDetails(data));
return result.ToArray();
}
private byte[] ReadHolderSet(Queue<byte> data)
{
var result = new List<byte>();
var type = dataTypes.ReadNextVarInt(data);
result.AddRange(DataTypes.GetVarInt(type));
if (type == 0)
{
result.AddRange(DataTypes.GetString(dataTypes.ReadNextString(data)));
}
else
{
for (var i = 0; i < type - 1; i++)
result.AddRange(DataTypes.GetVarInt(dataTypes.ReadNextVarInt(data)));
}
return result.ToArray();
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(DeathEffects.Count));
foreach (var effect in DeathEffects)
{
data.AddRange(DataTypes.GetVarInt(effect.EffectTypeId));
data.AddRange(effect.Payload);
}
return new Queue<byte>(data);
}
public record ConsumeEffectData(int EffectTypeId, byte[] Payload);
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class EnchantableComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Value { get; set; }
public override void Parse(Queue<byte> data)
{
Value = dataTypes.ReadNextVarInt(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(Value));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,94 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_21;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class EquippableComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Slot { get; set; }
public SoundEventSubComponent? EquipSound { get; set; }
public bool HasModel { get; set; }
public string? Model { get; set; }
public bool HasCameraOverlay { get; set; }
public string? CameraOverlay { get; set; }
public bool HasAllowedEntities { get; set; }
public int AllowedEntitiesType { get; set; }
public string? AllowedEntitiesTag { get; set; }
public List<int>? AllowedEntitiesIds { get; set; }
public bool Dispensable { get; set; }
public bool Swappable { get; set; }
public bool DamageOnHurt { get; set; }
public override void Parse(Queue<byte> data)
{
Slot = dataTypes.ReadNextVarInt(data);
EquipSound = (SoundEventSubComponent)subComponentRegistry.ParseSubComponent(SubComponents.SoundEvent, data);
HasModel = dataTypes.ReadNextBool(data);
if (HasModel)
Model = dataTypes.ReadNextString(data);
HasCameraOverlay = dataTypes.ReadNextBool(data);
if (HasCameraOverlay)
CameraOverlay = dataTypes.ReadNextString(data);
HasAllowedEntities = dataTypes.ReadNextBool(data);
if (HasAllowedEntities)
{
AllowedEntitiesType = dataTypes.ReadNextVarInt(data);
if (AllowedEntitiesType == 0)
{
AllowedEntitiesTag = dataTypes.ReadNextString(data);
}
else
{
AllowedEntitiesIds = new List<int>();
for (var i = 0; i < AllowedEntitiesType - 1; i++)
AllowedEntitiesIds.Add(dataTypes.ReadNextVarInt(data));
}
}
Dispensable = dataTypes.ReadNextBool(data);
Swappable = dataTypes.ReadNextBool(data);
DamageOnHurt = dataTypes.ReadNextBool(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(Slot));
if (EquipSound != null) data.AddRange(EquipSound.Serialize());
data.AddRange(DataTypes.GetBool(HasModel));
if (HasModel && Model != null)
data.AddRange(DataTypes.GetString(Model));
data.AddRange(DataTypes.GetBool(HasCameraOverlay));
if (HasCameraOverlay && CameraOverlay != null)
data.AddRange(DataTypes.GetString(CameraOverlay));
data.AddRange(DataTypes.GetBool(HasAllowedEntities));
if (HasAllowedEntities)
{
data.AddRange(DataTypes.GetVarInt(AllowedEntitiesType));
if (AllowedEntitiesType == 0 && AllowedEntitiesTag != null)
{
data.AddRange(DataTypes.GetString(AllowedEntitiesTag));
}
else if (AllowedEntitiesIds != null)
{
foreach (var id in AllowedEntitiesIds)
data.AddRange(DataTypes.GetVarInt(id));
}
}
data.AddRange(DataTypes.GetBool(Dispensable));
data.AddRange(DataTypes.GetBool(Swappable));
data.AddRange(DataTypes.GetBool(DamageOnHurt));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class FoodComponent1212(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Nutrition { get; set; }
public float Saturation { get; set; }
public bool CanAlwaysEat { get; set; }
public override void Parse(Queue<byte> data)
{
Nutrition = dataTypes.ReadNextVarInt(data);
Saturation = dataTypes.ReadNextFloat(data);
CanAlwaysEat = dataTypes.ReadNextBool(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(Nutrition));
data.AddRange(DataTypes.GetFloat(Saturation));
data.AddRange(DataTypes.GetBool(CanAlwaysEat));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,8 @@
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class GliderComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: EmptyComponent(dataTypes, itemPalette, subComponentRegistry);

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class ItemModelComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public string Identifier { get; set; } = null!;
public override void Parse(Queue<byte> data)
{
Identifier = dataTypes.ReadNextString(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetString(Identifier));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class RepairableComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public int Type { get; set; }
public string? TagName { get; set; }
public List<int>? ItemIds { get; set; }
public override void Parse(Queue<byte> data)
{
Type = dataTypes.ReadNextVarInt(data);
if (Type == 0)
{
TagName = dataTypes.ReadNextString(data);
}
else
{
ItemIds = new List<int>();
for (var i = 0; i < Type - 1; i++)
ItemIds.Add(dataTypes.ReadNextVarInt(data));
}
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetVarInt(Type));
if (Type == 0 && TagName != null)
{
data.AddRange(DataTypes.GetString(TagName));
}
else if (ItemIds != null)
{
foreach (var id in ItemIds)
data.AddRange(DataTypes.GetVarInt(id));
}
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class TooltipStyleComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public string Identifier { get; set; } = null!;
public override void Parse(Queue<byte> data)
{
Identifier = dataTypes.ReadNextString(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetString(Identifier));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class UseCooldownComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public float Seconds { get; set; }
public bool HasCooldownGroup { get; set; }
public string? CooldownGroup { get; set; }
public override void Parse(Queue<byte> data)
{
Seconds = dataTypes.ReadNextFloat(data);
HasCooldownGroup = dataTypes.ReadNextBool(data);
if (HasCooldownGroup)
CooldownGroup = dataTypes.ReadNextString(data);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(DataTypes.GetFloat(Seconds));
data.AddRange(DataTypes.GetBool(HasCooldownGroup));
if (HasCooldownGroup && CooldownGroup != null)
data.AddRange(DataTypes.GetString(CooldownGroup));
return new Queue<byte>(data);
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using MinecraftClient.Inventory;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Protocol.Handlers.StructuredComponents.Core;
namespace MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_2;
public class UseRemainderComponent(DataTypes dataTypes, ItemPalette itemPalette, SubComponentRegistry subComponentRegistry)
: StructuredComponent(dataTypes, itemPalette, subComponentRegistry)
{
public Item? ConvertInto { get; set; }
public override void Parse(Queue<byte> data)
{
ConvertInto = dataTypes.ReadNextItemSlot(data, ItemPalette);
}
public override Queue<byte> Serialize()
{
var data = new List<byte>();
data.AddRange(dataTypes.GetItemSlot(ConvertInto, ItemPalette));
return new Queue<byte>(data);
}
}