refactor(net): #13 Parsed.TrailerTruncated + diag logging

Code-quality review followup on Task 2 (becbde6) — addresses I1 (the
forward-looking concern that Tasks 3-9's inner-catch will leave partial
lists visible to callers with no signal) and M1 (silent inner catch).

Changes:
  - Parsed gains a trailing `bool TrailerTruncated` field. Both
    construction sites pass `false` by default; the trailer try/catch
    flips a local `trailerTruncated` to `true` on FormatException and
    feeds it into the final return.
  - Inner catch logs `pos`/`payload.Length`/exception message under
    ACDREAM_DUMP_VITALS=1, mirroring the outer catch's diagnostic
    pattern.
  - Task 2 test strengthened to assert defaults on Options2 /
    SpellbookFilters / HotbarSpells / DesiredComps / GameplayOptions /
    Equipped + TrailerTruncated=false (M2 followup — gives Tasks 3-9
    a regression guard if they consume into the wrong field).
  - New test `TryParse_TrailerAbsent_LessThan8BytesAfterEnchantments_*`
    documents the contract that <8 bytes after enchantments means the
    trailer is absent (not truncated): TrailerTruncated stays false,
    upstream attribute data survives.
  - Plan updated in lockstep so Tasks 3-11 implementers see the
    `trailerTruncated` local and the new return-arg position.

271/271 AcDream.Core.Net.Tests pass.
This commit is contained in:
Erik 2026-05-10 08:26:08 +02:00
parent becbde60a4
commit 9a0dfe03da
3 changed files with 96 additions and 11 deletions

View file

@ -362,5 +362,52 @@ public sealed class PlayerDescriptionParserTests
Assert.Equal(0xDEADBEEFu, parsed.Value.Options1);
Assert.Empty(parsed.Value.Shortcuts);
Assert.Empty(parsed.Value.Inventory);
// Defaults for the trailer fields not yet read (Tasks 3-9 will
// populate them). Asserting them here gives those tasks a
// pre-existing regression guard if they accidentally consume into
// the wrong field's wire bytes.
Assert.Equal(0u, parsed.Value.Options2);
Assert.Equal(0u, parsed.Value.SpellbookFilters);
Assert.Empty(parsed.Value.HotbarSpells);
Assert.Empty(parsed.Value.DesiredComps);
Assert.True(parsed.Value.GameplayOptions.IsEmpty);
Assert.Empty(parsed.Value.Equipped);
Assert.False(parsed.Value.TrailerTruncated);
}
[Fact]
public void TryParse_TrailerAbsent_LessThan8BytesAfterEnchantments_PreservesUpstreamAndDoesNotFlagTruncation()
{
// Fewer than 8 bytes remain after the enchantment block, so the
// trailer header is treated as absent (no read attempted). Upstream
// attribute data must survive; TrailerTruncated stays false because
// the parser never *started* the trailer — it correctly skipped it.
// (Tasks 3-9 will introduce truncation-mid-section cases that flip
// TrailerTruncated to true.)
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
// Attribute block: only Strength (bit 0).
writer.Write(0x01u);
writer.Write(50u); writer.Write(10u); writer.Write(0u);
// Empty enchantment mask.
writer.Write(0u);
// Truncated trailer: only 4 bytes (would-be option_flags) instead of 8.
writer.Write(0xCAFEu);
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
Assert.NotNull(parsed);
// Upstream attribute survived.
Assert.Single(parsed!.Value.Attributes);
Assert.Equal(1u, parsed.Value.Attributes[0].AtType);
// Trailer was absent (< 8 bytes), so no truncation flag and all
// trailer fields stay at their initial defaults.
Assert.False(parsed.Value.TrailerTruncated);
Assert.Equal(PlayerDescriptionParser.CharacterOptionDataFlag.None, parsed.Value.OptionFlags);
Assert.Equal(0u, parsed.Value.Options1);
}
}