From f7a5eea8e844f388b494726bff3cdf4bf4bb896c Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 08:28:25 +0200 Subject: [PATCH] feat(net): #13 read shortcuts list (SHORTCUT bit) in PD trailer Co-Authored-By: Claude Sonnet 4.6 --- .../Messages/PlayerDescriptionParser.cs | 14 +++++ .../PlayerDescriptionParserTests.cs | 63 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs index 065d1d2..110e12b 100644 --- a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs +++ b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs @@ -341,6 +341,20 @@ public static class PlayerDescriptionParser { optionFlags = (CharacterOptionDataFlag)ReadU32(payload, ref pos); options1 = ReadU32(payload, ref pos); + + if (optionFlags.HasFlag(CharacterOptionDataFlag.Shortcut)) + { + uint count = ReadU32(payload, ref pos); + if (count > 10_000) throw new FormatException("unreasonable shortcut count"); + for (uint i = 0; i < count; i++) + { + uint idx = ReadU32(payload, ref pos); + uint guid = ReadU32(payload, ref pos); + ushort spellId = ReadU16(payload, ref pos); + ushort layer = ReadU16(payload, ref pos); + shortcuts.Add(new ShortcutEntry(idx, guid, spellId, layer)); + } + } } } catch (FormatException ex) diff --git a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs index 1c25333..3d1d157 100644 --- a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs +++ b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs @@ -375,6 +375,69 @@ public sealed class PlayerDescriptionParserTests Assert.False(parsed.Value.TrailerTruncated); } + [Fact] + public void TryParse_TrailerShortcuts_PopulatesList() + { + 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(0x01u); // option_flags = SHORTCUT + writer.Write(0xCAFEu); // options1 sentinel + + // Shortcut count + 2 entries (16 B each). + writer.Write(2u); + writer.Write(0u); writer.Write(0xAABBCCDDu); writer.Write((ushort)0); writer.Write((ushort)0); + writer.Write(7u); writer.Write(0u); writer.Write((ushort)1234); writer.Write((ushort)5); + + var parsed = PlayerDescriptionParser.TryParse(sb.ToArray()); + + Assert.NotNull(parsed); + Assert.Equal(2, parsed!.Value.Shortcuts.Count); + Assert.Equal(0u, parsed.Value.Shortcuts[0].Index); + Assert.Equal(0xAABBCCDDu, parsed.Value.Shortcuts[0].ObjectGuid); + Assert.Equal((ushort)0, parsed.Value.Shortcuts[0].SpellId); + Assert.Equal(7u, parsed.Value.Shortcuts[1].Index); + Assert.Equal((ushort)1234, parsed.Value.Shortcuts[1].SpellId); + Assert.Equal((ushort)5, parsed.Value.Shortcuts[1].Layer); + } + + [Fact] + public void TryParse_TrailerShortcuts_TruncatedMidList_FlagsTrailerTruncatedAndPreservesPriorEntries() + { + 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(0x01u); // option_flags = SHORTCUT + writer.Write(0u); // options1 + writer.Write(3u); // claimed shortcut count = 3 + // First entry complete (16 B). + writer.Write(1u); writer.Write(0xAAAAu); writer.Write((ushort)10); writer.Write((ushort)1); + // Second entry truncated to 8 bytes — ReadU16 will throw FormatException. + writer.Write(2u); writer.Write(0xBBBBu); + // (no SpellId/Layer — payload ends here) + + var parsed = PlayerDescriptionParser.TryParse(sb.ToArray()); + + Assert.NotNull(parsed); + // Inner catch fired — flag set. + Assert.True(parsed!.Value.TrailerTruncated); + // First entry survives in the partial list. + Assert.Single(parsed.Value.Shortcuts); + Assert.Equal(1u, parsed.Value.Shortcuts[0].Index); + } + [Fact] public void TryParse_TrailerAbsent_LessThan8BytesAfterEnchantments_PreservesUpstreamAndDoesNotFlagTruncation() {