feat(net): #13 heuristic inventory locator after gameplay_options blob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d9a5e40203
commit
91693ea44c
2 changed files with 90 additions and 1 deletions
|
|
@ -402,7 +402,17 @@ public static class PlayerDescriptionParser
|
|||
if (optionFlags.HasFlag(CharacterOptionDataFlag.CharacterOptions2))
|
||||
options2 = ReadU32(payload, ref pos);
|
||||
|
||||
if (!optionFlags.HasFlag(CharacterOptionDataFlag.GameplayOptions))
|
||||
if (optionFlags.HasFlag(CharacterOptionDataFlag.GameplayOptions))
|
||||
{
|
||||
int gameplayStart = pos;
|
||||
if (TryHeuristicInventoryStart(payload, gameplayStart, out int invStart, out int end,
|
||||
inventory, equipped))
|
||||
{
|
||||
gameplayOptions = payload.Slice(gameplayStart, invStart - gameplayStart).ToArray();
|
||||
pos = end;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Strict path: inventory + equipped follow directly.
|
||||
TryUnpackInventoryStrict(payload, ref pos, inventory, equipped);
|
||||
|
|
@ -760,6 +770,43 @@ public static class PlayerDescriptionParser
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>4-byte-aligned forward scan from <paramref name="start"/>
|
||||
/// looking for the first offset where <c>TryUnpackInventoryStrict</c>
|
||||
/// consumes exactly to end-of-buffer. Mirrors holtburger
|
||||
/// <c>find_inventory_start_after_gameplay_options</c> in events.rs:195-218.</summary>
|
||||
private static bool TryHeuristicInventoryStart(
|
||||
ReadOnlySpan<byte> src, int start,
|
||||
out int invStart, out int end,
|
||||
List<InventoryEntry> inventory, List<EquippedEntry> equipped)
|
||||
{
|
||||
invStart = end = 0;
|
||||
inventory.Clear();
|
||||
equipped.Clear();
|
||||
if (start + 8 > src.Length) return false;
|
||||
|
||||
int candidate = start;
|
||||
int misalign = candidate & 3;
|
||||
if (misalign != 0) candidate += 4 - misalign;
|
||||
|
||||
int last = src.Length - 8;
|
||||
while (candidate <= last)
|
||||
{
|
||||
int tmp = candidate;
|
||||
var tmpInv = new List<InventoryEntry>();
|
||||
var tmpEq = new List<EquippedEntry>();
|
||||
if (TryUnpackInventoryStrict(src, ref tmp, tmpInv, tmpEq) && tmp == src.Length)
|
||||
{
|
||||
invStart = candidate;
|
||||
end = tmp;
|
||||
inventory.AddRange(tmpInv);
|
||||
equipped.AddRange(tmpEq);
|
||||
return true;
|
||||
}
|
||||
candidate += 4;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ushort ReadU16(ReadOnlySpan<byte> src, ref int pos)
|
||||
{
|
||||
if (src.Length - pos < 2) throw new FormatException("truncated u16");
|
||||
|
|
|
|||
|
|
@ -663,4 +663,46 @@ public sealed class PlayerDescriptionParserTests
|
|||
Assert.Equal(0x00000200u, parsed.Value.Equipped[0].EquipLocation);
|
||||
Assert.Equal(1u, parsed.Value.Equipped[0].Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_TrailerGameplayOptions_HeuristicLocatesInventoryStart()
|
||||
{
|
||||
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
|
||||
|
||||
// option_flags = GAMEPLAY_OPTIONS (0x200)
|
||||
writer.Write(0x200u);
|
||||
writer.Write(0u); // options1
|
||||
writer.Write(0u); // legacy hotbar count=0
|
||||
writer.Write(0u); // spellbook_filters
|
||||
|
||||
// 16 bytes of opaque gameplay_options blob — values that *almost* look
|
||||
// like an inventory header but fail validation (wtype > 2 or count too
|
||||
// big), forcing the heuristic to walk past them.
|
||||
writer.Write(0xDEADBEEFu); // looks like inv_count = 0xDEADBEEF (> 10_000) — rejected
|
||||
writer.Write(0xCAFEBABEu);
|
||||
writer.Write(0x12345678u);
|
||||
writer.Write(0x87654321u);
|
||||
|
||||
// Real inventory: 1 entry, then equipped: 1 entry — must consume to EOF.
|
||||
writer.Write(1u);
|
||||
writer.Write(0x50000200u); writer.Write(0u);
|
||||
writer.Write(1u);
|
||||
writer.Write(0x50000300u); writer.Write(0x00000200u); writer.Write(1u);
|
||||
|
||||
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Single(parsed!.Value.Inventory);
|
||||
Assert.Equal(0x50000200u, parsed.Value.Inventory[0].Guid);
|
||||
Assert.Single(parsed.Value.Equipped);
|
||||
Assert.Equal(0x50000300u, parsed.Value.Equipped[0].Guid);
|
||||
Assert.Equal(16, parsed.Value.GameplayOptions.Length);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue