diff --git a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs index be31e33..982afff 100644 --- a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs +++ b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs @@ -401,6 +401,12 @@ public static class PlayerDescriptionParser if (optionFlags.HasFlag(CharacterOptionDataFlag.CharacterOptions2)) options2 = ReadU32(payload, ref pos); + + if (!optionFlags.HasFlag(CharacterOptionDataFlag.GameplayOptions)) + { + // Strict path: inventory + equipped follow directly. + TryUnpackInventoryStrict(payload, ref pos, inventory, equipped); + } } } catch (FormatException ex) @@ -711,6 +717,49 @@ public static class PlayerDescriptionParser bucket); } + /// Strict inventory + equipped block reader. Returns true if + /// the bytes from parse cleanly per holtburger + /// events.rs:143-193 (unpack_inventory_and_equipped_strict). + /// Counts capped at 10,000; inventory ContainerType must be 0..2 + /// (NonContainer / Container / Foci). + private static bool TryUnpackInventoryStrict( + ReadOnlySpan src, ref int pos, + List inventory, List equipped) + { + inventory.Clear(); + equipped.Clear(); + if (pos + 4 > src.Length) return false; + uint invCount = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos)); + pos += 4; + if (invCount > 10_000) return false; + + for (uint i = 0; i < invCount; i++) + { + if (pos + 8 > src.Length) return false; + uint guid = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos)); + uint wtype = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos + 4)); + pos += 8; + if (wtype > 2) return false; + inventory.Add(new InventoryEntry(guid, wtype)); + } + + if (pos + 4 > src.Length) return false; + uint eqCount = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos)); + pos += 4; + if (eqCount > 10_000) return false; + + for (uint i = 0; i < eqCount; i++) + { + if (pos + 12 > src.Length) return false; + uint guid = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos)); + uint loc = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos + 4)); + uint prio = BinaryPrimitives.ReadUInt32LittleEndian(src.Slice(pos + 8)); + pos += 12; + equipped.Add(new EquippedEntry(guid, loc, prio)); + } + return true; + } + private static ushort ReadU16(ReadOnlySpan src, ref int pos) { if (src.Length - pos < 2) throw new FormatException("truncated u16"); diff --git a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs index 91454a7..458169e 100644 --- a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs +++ b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs @@ -624,4 +624,43 @@ public sealed class PlayerDescriptionParserTests Assert.NotNull(parsed); Assert.Equal(0xC0FFEE01u, parsed!.Value.Options2); } + + [Fact] + public void TryParse_TrailerInventoryEquippedStrict_NoGameplayOptionsBit() + { + var sb = new MemoryStream(); + using var writer = new BinaryWriter(sb); + writer.Write(0u); // propertyFlags + writer.Write(0x52u); // weenieType + writer.Write(0x201u); // ATTRIBUTE | ENCHANTMENT + writer.Write(1u); // has_health + writer.Write(0u); // empty attribute_flags + writer.Write(0u); // empty enchantment mask + + writer.Write(0u); // option_flags = None — no GAMEPLAY_OPTIONS + writer.Write(0u); // options1 + writer.Write(0u); // legacy hotbar list count=0 + writer.Write(0u); // spellbook_filters + + // Inventory: 2 entries + writer.Write(2u); + writer.Write(0x500000A0u); writer.Write(0u); // NonContainer + writer.Write(0x500000A1u); writer.Write(1u); // Container + + // Equipped: 1 entry + writer.Write(1u); + writer.Write(0x500000B0u); writer.Write(0x00000200u); writer.Write(1u); // ChestArmor, prio=1 + + var parsed = PlayerDescriptionParser.TryParse(sb.ToArray()); + + Assert.NotNull(parsed); + Assert.Equal(2, parsed!.Value.Inventory.Count); + Assert.Equal(0x500000A0u, parsed.Value.Inventory[0].Guid); + Assert.Equal(0u, parsed.Value.Inventory[0].ContainerType); + Assert.Equal(1u, parsed.Value.Inventory[1].ContainerType); + Assert.Single(parsed.Value.Equipped); + Assert.Equal(0x500000B0u, parsed.Value.Equipped[0].Guid); + Assert.Equal(0x00000200u, parsed.Value.Equipped[0].EquipLocation); + Assert.Equal(1u, parsed.Value.Equipped[0].Priority); + } }