feat(D.5.2): capture UiEffects from CreateObject weenie header

Previously, weenieFlags bit 0x80 (UiEffects) was read + discarded with
`pos += 4`. Now it is captured into `uiEffects` and surfaced as
`Parsed.UiEffects` — the sole wire path for the effect bitfield since
PropertyInt.UiEffects (18) has no [AssessmentProperty] and never appears
in appraise responses.

Test builder gains `uint uiEffects = 0` param; write line updated to use
it. Three new parse tests: UiEffects_Captured, UiEffectsThenIconOverlay
(cursor-arithmetic regression), and NoUiEffectsBit_LeavesUiEffectsZero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-17 18:28:31 +02:00
parent 5a2af61508
commit 8df0b64676
2 changed files with 60 additions and 6 deletions

View file

@ -332,6 +332,52 @@ public sealed class CreateObjectTests
Assert.Equal(0x06004444u, parsed.Value.IconUnderlayId);
}
// -----------------------------------------------------------------------
// D.5.2 (2026-06-17): UiEffects bitfield (weenieFlags bit 0x80) — captured
// instead of skipped. Drives the icon's effect-overlay recolor.
// -----------------------------------------------------------------------
[Fact]
public void TryParse_UiEffects_Captured()
{
// weenieFlags 0x80 = UiEffects; value 0x1 = Magical.
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000010u, name: "MagicWand", itemType: (uint)ItemType.Caster,
weenieFlags: 0x80u, uiEffects: 0x1u);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal(0x1u, parsed!.Value.UiEffects);
}
[Fact]
public void TryParse_UiEffectsThenIconOverlay_BothCaptured()
{
// Verifies the cursor still reaches IconOverlay after reading (not skipping) UiEffects.
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000011u, name: "GlowSword", itemType: (uint)ItemType.MeleeWeapon,
weenieFlags: 0x80u | 0x40000000u, uiEffects: 0x4u, iconOverlayId: 0x1ABCu);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal(0x4u, parsed!.Value.UiEffects);
Assert.Equal(0x06001ABCu, parsed.Value.IconOverlayId);
}
[Fact]
public void TryParse_NoUiEffectsBit_LeavesUiEffectsZero()
{
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
guid: 0x50000012u, name: "PlainRock", itemType: (uint)ItemType.Misc, weenieFlags: 0u);
var parsed = CreateObject.TryParse(body);
Assert.NotNull(parsed);
Assert.Equal(0u, parsed!.Value.UiEffects);
}
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
uint guid,
string name,
@ -341,6 +387,7 @@ public sealed class CreateObjectTests
uint weenieFlags = 0,
uint weenieFlags2 = 0,
uint iconId = 0,
uint uiEffects = 0,
uint? value = null,
uint? useability = null,
float? useRadius = null,
@ -400,7 +447,7 @@ public sealed class CreateObjectTests
bytes.AddRange(tmp.ToArray());
}
if ((weenieFlags & 0x00080000u) != 0) WriteU32(bytes, 0); // TargetType u32
if ((weenieFlags & 0x00000080u) != 0) WriteU32(bytes, 0); // UiEffects u32
if ((weenieFlags & 0x00000080u) != 0) WriteU32(bytes, uiEffects); // UiEffects u32
if ((weenieFlags & 0x00000200u) != 0) bytes.Add(0); // CombatUse sbyte/1 byte
if ((weenieFlags & 0x00000400u) != 0) WriteU16(bytes, structure ?? 0); // Structure u16
if ((weenieFlags & 0x00000800u) != 0) WriteU16(bytes, maxStructure ?? 0); // MaxStructure u16