diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index f0d2b65d..35122e79 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -156,7 +156,12 @@ public static class CreateObject // IconComposer.GetIcon already composites these layers in the correct // retail order (underlay / base / overlay+tint / effect). uint IconOverlayId = 0, - uint IconUnderlayId = 0); + uint IconUnderlayId = 0, + // D.5.2 (2026-06-17): UiEffects bitfield (weenieFlags 0x80) — drives the icon's + // effect recolor (Magical=0x1 … Nether=0x1000). The ONLY wire path for the effect + // state (PropertyInt.UiEffects=18 has no [AssessmentProperty] → not in appraise). + // Previously read + discarded at the UiEffects skip. 0 = no effect. + uint UiEffects = 0); /// /// The relevant subset of the server-sent MovementData / @@ -571,7 +576,7 @@ public static class CreateObject // 0x00000010 Usable u32 KEPT // 0x00000020 UseRadius f32 KEPT // 0x00080000 TargetType u32 (skip) - // 0x00000080 UiEffects u32 (skip) + // 0x00000080 UiEffects u32 CAPTURE (D.5.2) // 0x00000200 CombatUse sbyte/1 byte (skip) // 0x00000400 Structure u16 (skip) // 0x00000800 MaxStructure u16 (skip) @@ -605,6 +610,7 @@ public static class CreateObject float? useRadius = null; uint iconOverlayId = 0; uint iconUnderlayId = 0; + uint uiEffects = 0; uint weenieFlags2 = 0; try { @@ -666,10 +672,10 @@ public static class CreateObject if (body.Length - pos < 4) throw new FormatException("trunc TargetType"); pos += 4; } - if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32 + if ((weenieFlags & 0x00000080u) != 0) // UiEffects u32 ← CAPTURE { if (body.Length - pos < 4) throw new FormatException("trunc UiEffects"); - pos += 4; + uiEffects = ReadU32(body, ref pos); } if ((weenieFlags & 0x00000200u) != 0) // CombatUse sbyte (1 byte) { @@ -808,7 +814,8 @@ public static class CreateObject friction, elasticity, IconId: iconId, Useability: useability, UseRadius: useRadius, - IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId); + IconOverlayId: iconOverlayId, IconUnderlayId: iconUnderlayId, + UiEffects: uiEffects); // Local helper: if we ran out of fields past PhysicsData, still // return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion). diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs index ce9ea4a1..f760f98b 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -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