feat(net): #13 read OptionFlags + Options1 after enchantments
First step of the PD trailer walk. Wraps trailer reads in their own try/catch so a malformed trailer does not null out the upstream attribute/skill/spell/enchantment data we already extracted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
65870349a8
commit
becbde60a4
2 changed files with 59 additions and 14 deletions
|
|
@ -309,26 +309,42 @@ public static class PlayerDescriptionParser
|
||||||
ReadSpellTable(payload, ref pos, spells);
|
ReadSpellTable(payload, ref pos, spells);
|
||||||
|
|
||||||
// ── Enchantments (Issue #7 / #12) ───────────────────────────────
|
// ── Enchantments (Issue #7 / #12) ───────────────────────────────
|
||||||
// Outer EnchantmentMask + per-bucket count + N×Enchantment(60-64 B).
|
|
||||||
// Holtburger events.rs:462-501. After this block come options /
|
|
||||||
// shortcuts / hotbars / inventory / equipped — those need a
|
|
||||||
// heuristic walker for the variable-length gameplay_options blob.
|
|
||||||
// Filed as ISSUES.md #13 for follow-up; stop here cleanly so
|
|
||||||
// partial parses still populate enchantments.
|
|
||||||
if (vectorFlags.HasFlag(DescriptionVectorFlag.Enchantment))
|
if (vectorFlags.HasFlag(DescriptionVectorFlag.Enchantment))
|
||||||
ReadEnchantmentBlock(payload, ref pos, enchantments);
|
ReadEnchantmentBlock(payload, ref pos, enchantments);
|
||||||
|
|
||||||
|
// ── Trailer (Issue #13): options + shortcuts + hotbars + inventory ──
|
||||||
|
// Wrapped in its own try/catch — a malformed trailer must not destroy
|
||||||
|
// the attribute / skill / spell / enchantment data we already extracted.
|
||||||
|
CharacterOptionDataFlag optionFlags = CharacterOptionDataFlag.None;
|
||||||
|
uint options1 = 0;
|
||||||
|
uint options2 = 0;
|
||||||
|
uint spellbookFilters = 0;
|
||||||
|
List<ShortcutEntry> shortcuts = new();
|
||||||
|
List<IReadOnlyList<uint>> hotbarSpells = new();
|
||||||
|
List<(uint, uint)> desiredComps = new();
|
||||||
|
ReadOnlyMemory<byte> gameplayOptions = ReadOnlyMemory<byte>.Empty;
|
||||||
|
List<InventoryEntry> inventory = new();
|
||||||
|
List<EquippedEntry> equipped = new();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (payload.Length - pos >= 8)
|
||||||
|
{
|
||||||
|
optionFlags = (CharacterOptionDataFlag)ReadU32(payload, ref pos);
|
||||||
|
options1 = ReadU32(payload, ref pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
// Trailer corrupted — keep what we have and return.
|
||||||
|
}
|
||||||
|
|
||||||
return new Parsed(
|
return new Parsed(
|
||||||
weenieType, propertyFlags, vectorFlags, hasHealth,
|
weenieType, propertyFlags, vectorFlags, hasHealth,
|
||||||
bundle, positions, attributes, skills, spells, enchantments,
|
bundle, positions, attributes, skills, spells, enchantments,
|
||||||
CharacterOptionDataFlag.None, 0u, 0u,
|
optionFlags, options1, options2,
|
||||||
System.Array.Empty<ShortcutEntry>(),
|
shortcuts, hotbarSpells, desiredComps, spellbookFilters,
|
||||||
System.Array.Empty<IReadOnlyList<uint>>(),
|
gameplayOptions, inventory, equipped);
|
||||||
System.Array.Empty<(uint, uint)>(),
|
|
||||||
0u,
|
|
||||||
ReadOnlyMemory<byte>.Empty,
|
|
||||||
System.Array.Empty<InventoryEntry>(),
|
|
||||||
System.Array.Empty<EquippedEntry>());
|
|
||||||
}
|
}
|
||||||
catch (FormatException ex)
|
catch (FormatException ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -334,4 +334,33 @@ public sealed class PlayerDescriptionParserTests
|
||||||
Assert.Equal(2.0f, parsed.Value.Spells[1234u]);
|
Assert.Equal(2.0f, parsed.Value.Spells[1234u]);
|
||||||
Assert.Equal(2.0f, parsed.Value.Spells[5678u]);
|
Assert.Equal(2.0f, parsed.Value.Spells[5678u]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_TrailerOptionFlagsAndOptions1_AreReadAfterEnchantments()
|
||||||
|
{
|
||||||
|
// ATTRIBUTE | ENCHANTMENT vector flag; empty enchantment mask (0).
|
||||||
|
// After mask, trailer adds u32 option_flags + u32 options1.
|
||||||
|
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); // EnchantmentMask = empty
|
||||||
|
|
||||||
|
// Trailer header: option_flags + options1
|
||||||
|
writer.Write(0u); // option_flags = None — no further sections
|
||||||
|
writer.Write(0xDEADBEEFu); // options1 sentinel
|
||||||
|
|
||||||
|
// No more bytes — spellbook_filters is optional (defaults to 0).
|
||||||
|
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
|
||||||
|
|
||||||
|
Assert.NotNull(parsed);
|
||||||
|
Assert.Equal(PlayerDescriptionParser.CharacterOptionDataFlag.None, parsed!.Value.OptionFlags);
|
||||||
|
Assert.Equal(0xDEADBEEFu, parsed.Value.Options1);
|
||||||
|
Assert.Empty(parsed.Value.Shortcuts);
|
||||||
|
Assert.Empty(parsed.Value.Inventory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue