diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index 48b678d3..db3c8a7b 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -127,6 +127,12 @@ public static class CreateObject // defaults (0.05f elasticity, 0.5f friction). float? Friction = null, float? Elasticity = null, + // D.5.1 (2026-06-16): icon dat id (0x06xxxxxx) from the WeenieHeader + // fixed prefix. Previously discarded at cs:516; surfaced so the action + // bar / equipment UI can display the correct icon sprite without a + // separate dat lookup. Zero means "not sent" (packed zero sentinel in + // ReadPackedDwordOfKnownType preserves 0 as-is). + uint IconId = 0, // 2026-05-15: optional WeenieHeader tail. The retail // `ITEM_USEABLE _useability` (acclient.h:6478) — gates whether the // R-key Use action does anything. (Useability & USEABLE_REMOTE @@ -506,6 +512,7 @@ public static class CreateObject string? name = null; uint? itemType = null; uint weenieFlags = 0; + uint iconId = 0; if (body.Length - pos >= 4) { weenieFlags = ReadU32(body, ref pos); @@ -513,7 +520,7 @@ public static class CreateObject { name = ReadString16L(body, ref pos); _ = ReadPackedDword(body, ref pos); // WeenieClassId - _ = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); + iconId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); if (body.Length - pos >= 4) itemType = ReadU32(body, ref pos); if (body.Length - pos >= 4) @@ -611,7 +618,8 @@ public static class CreateObject instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq, physicsState, objectDescriptionFlags, friction, elasticity, - useability, useRadius); + IconId: iconId, + Useability: useability, UseRadius: useRadius); // 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 1e9ce105..8a98d62f 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -156,6 +156,29 @@ public sealed class CreateObjectTests Assert.Equal(2.5f, parsed.Value.UseRadius!.Value, precision: 3); } + // ----------------------------------------------------------------------- + // D.5.1 (2026-06-16): IconId was discarded at cs:516 — surface it so the + // action bar / equipment UI can read icon dat ids from spawn messages. + // ----------------------------------------------------------------------- + + [Fact] + public void TryParse_IconId_Surfaced() + { + // Icon dat id 0x06001234: the wire writer strips the 0x06000000 prefix + // before packing (WritePackedDwordOfKnownType strips it), so we write + // 0x1234 as the packed value and expect 0x06001234 back. + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x50000009u, + name: "SwordIcon", + itemType: (uint)ItemType.MeleeWeapon, + iconId: 0x1234u); + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x06001234u, parsed!.Value.IconId); + } + private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, @@ -163,6 +186,7 @@ public sealed class CreateObjectTests uint physicsState = 0, uint objectDescriptionFlags = 0, uint weenieFlags = 0, + uint iconId = 0, uint? value = null, uint? useability = null, float? useRadius = null) @@ -187,8 +211,8 @@ public sealed class CreateObjectTests // Fixed WeenieHeader prefix per ACE SerializeCreateObject. WriteU32(bytes, weenieFlags); // weenieFlags WriteString16L(bytes, name); - WritePackedDword(bytes, 0x1234); // WeenieClassId - WritePackedDword(bytes, 0); // IconId via known-type writer + WritePackedDword(bytes, 0x1234); // WeenieClassId + WritePackedDword(bytes, iconId); // IconId via known-type writer (prefix stripped by ACE writer) WriteU32(bytes, itemType); WriteU32(bytes, objectDescriptionFlags); Align4(bytes);