using System.Buffers.Binary; using System.Text; using AcDream.Core.Items; using AcDream.Core.Net.Messages; namespace AcDream.Core.Net.Tests.Messages; public sealed class CreateObjectTests { [Fact] public void TryParse_WeenieHeaderPrefix_ReturnsNameAndItemType() { byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000002u, name: "Drudge", itemType: (uint)ItemType.Creature); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x50000002u, parsed.Value.Guid); Assert.Equal("Drudge", parsed.Value.Name); Assert.Equal((uint)ItemType.Creature, parsed.Value.ItemType); } // ----------------------------------------------------------------------- // Commit A of 2026-04-29 live-entity collision port: // PhysicsState (post-flags u32) + ObjectDescriptionFlags (PWD bitfield) // must be surfaced for downstream collision registration. // ----------------------------------------------------------------------- [Fact] public void TryParse_PhysicsState_Parsed() { // ETHEREAL_PS = 0x4 + IGNORE_COLLISIONS_PS = 0x10 → 0x14 byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000003u, name: "GhostNpc", itemType: (uint)ItemType.Creature, physicsState: 0x14u); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x14u, parsed!.Value.PhysicsState); } [Fact] public void TryParse_ObjectDescriptionFlags_PlayerKillerBitsSurface() { // BF_PLAYER (0x8) | BF_PLAYER_KILLER (0x20) → a PK player. byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000004u, name: "+PkPlayer", itemType: (uint)ItemType.Creature, objectDescriptionFlags: 0x8u | 0x20u); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x28u, parsed!.Value.ObjectDescriptionFlags); } [Fact] public void TryParse_ObjectDescriptionFlags_PkLiteBit() { // BF_PLAYER (0x8) | BF_PKLITE_PKSTATUS (0x2000000) → a PK-lite player. byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000005u, name: "+PklPlayer", itemType: (uint)ItemType.Creature, objectDescriptionFlags: 0x8u | 0x2000000u); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x2000008u, parsed!.Value.ObjectDescriptionFlags); } // ----------------------------------------------------------------------- // 2026-05-15: WeenieHeader optional-tail walker landed for Useability + // UseRadius (acclient.h ITEM_USEABLE enum at line 6478). The R-key Use // gate consumes Useability; signs without USEABLE_REMOTE (0x20) silently // ignore Use. // ----------------------------------------------------------------------- [Fact] public void TryParse_NoWeenieFlags_LeavesUseabilityNull() { // Sign-like entity: weenieFlags=0 (no optional fields). // Useability stays null (parser walked past nothing). byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000006u, name: "Holtburg Sign", itemType: 0x8000u); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Null(parsed!.Value.Useability); Assert.Null(parsed.Value.UseRadius); } [Fact] public void TryParse_WeenieFlagsUsable_ReadsUseability() { // Useable NPC: weenieFlags has bit 0x10 set, body carries // ITEM_USEABLE = USEABLE_REMOTE (0x20). byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000007u, name: "Tirenia", itemType: (uint)ItemType.Creature, weenieFlags: 0x10u, useability: 0x20u); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x20u, parsed!.Value.Useability); } [Fact] public void TryParse_WeenieFlagsUsable_ReadsUseableNoValue() { // Holtburg sign case (observed 2026-05-16): ACE sends // weenieFlags=0x10 + Useability=USEABLE_NO (0x01) for signs. // The parser must read this verbatim — downstream code // distinguishes USEABLE_NO from USEABLE_REMOTE for the // pickup vs use gate. byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x7A9B3001u, name: "Holtburg", itemType: 0x80u, // Misc weenieFlags: 0x10u, useability: 0x01u); // USEABLE_NO var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x01u, parsed!.Value.Useability); } [Fact] public void TryParse_WeenieFlagsValueAndUsableAndUseRadius_AllReadInOrder() { // Verify the walker skips Value (bit 0x8, 4 bytes) BEFORE reading // Useability (bit 0x10) and UseRadius (bit 0x20). Wire order in // ACE WorldObject_Networking.cs:99-106 is Value, Useable, UseRadius. byte[] body = BuildMinimalCreateObjectWithWeenieHeader( guid: 0x50000008u, name: "PriceyDoor", itemType: (uint)ItemType.Misc, weenieFlags: 0x8u | 0x10u | 0x20u, value: 0x12345678u, useability: 0x20u, useRadius: 2.5f); var parsed = CreateObject.TryParse(body); Assert.NotNull(parsed); Assert.Equal(0x20u, parsed!.Value.Useability); Assert.NotNull(parsed.Value.UseRadius); Assert.Equal(2.5f, parsed.Value.UseRadius!.Value, precision: 3); } private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, uint itemType, uint physicsState = 0, uint objectDescriptionFlags = 0, uint weenieFlags = 0, uint? value = null, uint? useability = null, float? useRadius = null) { var bytes = new List(); WriteU32(bytes, CreateObject.Opcode); WriteU32(bytes, guid); // ModelData header: marker, subpalette count, texture count, animpart count. bytes.Add(0x11); bytes.Add(0); bytes.Add(0); bytes.Add(0); // PhysicsData: physics flags = 0, then PhysicsState u32, then 9 seq stamps. WriteU32(bytes, 0); WriteU32(bytes, physicsState); for (int i = 0; i < 9; i++) WriteU16(bytes, 0); Align4(bytes); // 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 WriteU32(bytes, itemType); WriteU32(bytes, objectDescriptionFlags); Align4(bytes); // Optional WeenieHeader tail (2026-05-15) — same order as ACE // WorldObject_Networking.cs:87-114. Each field is written only when // its weenieFlags bit is set, matching the parser's walker exactly. if ((weenieFlags & 0x00000008u) != 0) // Value u32 WriteU32(bytes, value ?? 0u); if ((weenieFlags & 0x00000010u) != 0) // Useability u32 WriteU32(bytes, useability ?? 0u); if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32 { Span tmp = stackalloc byte[4]; BinaryPrimitives.WriteSingleLittleEndian(tmp, useRadius ?? 0f); bytes.AddRange(tmp.ToArray()); } return bytes.ToArray(); } private static void WriteU32(List bytes, uint value) { Span tmp = stackalloc byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(tmp, value); bytes.AddRange(tmp.ToArray()); } private static void WriteU16(List bytes, ushort value) { Span tmp = stackalloc byte[2]; BinaryPrimitives.WriteUInt16LittleEndian(tmp, value); bytes.AddRange(tmp.ToArray()); } private static void WritePackedDword(List bytes, uint value) { if (value <= 0x7FFF) { WriteU16(bytes, (ushort)value); return; } WriteU16(bytes, (ushort)(((value >> 16) & 0x7FFF) | 0x8000)); WriteU16(bytes, (ushort)(value & 0xFFFF)); } private static void WriteString16L(List bytes, string value) { byte[] encoded = Encoding.GetEncoding(1252).GetBytes(value); WriteU16(bytes, checked((ushort)encoded.Length)); bytes.AddRange(encoded); Align4(bytes); } private static void Align4(List bytes) { while ((bytes.Count & 3) != 0) bytes.Add(0); } }