WeenieClassId + Value/StackSize/MaxStackSize/Burden/capacities/Container/Wielder/ ValidLocations/CurrentWieldedLocation/Priority/Structure/Workmanship. Nullable = flag absent (don't clobber on merge). Cursor walk unchanged; +cursor-integrity test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
602 lines
25 KiB
C#
602 lines
25 KiB
C#
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);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// 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);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// D.5.1 (2026-06-17): extended WeenieHeader optional-tail walk — the parser
|
||
// now continues past UseRadius through ALL intervening fields to reach
|
||
// IconOverlay (weenieFlags bit 0x40000000) and IconUnderlay (weenieFlags2
|
||
// bit 0x01, present when objDescFlags bit 0x04000000 is set).
|
||
//
|
||
// Two tests:
|
||
// 1. WithIconOverlay — sets only the IconOverlay bit + the minimum
|
||
// intervening fields (none in this minimal body, so weenieFlags only has
|
||
// 0x40000000). Verifies the parse walks to IconOverlay and captures it.
|
||
// 2. WithIconOverlayAndUnderlay — sets IconOverlay + the IncludesSecondHeader
|
||
// objDescFlag + weenieFlags2 bit 0x01, writes both ids, asserts both are
|
||
// captured.
|
||
// 3. NoOverlayBits_CommonCase — weenieFlags=0, verifies the extended walk
|
||
// produces no overlay (regression guard for the common spawn path).
|
||
// -----------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void TryParse_IconOverlay_CapturedFromExtendedTail()
|
||
{
|
||
// Only IconOverlay (0x40000000) bit set in weenieFlags. No intervening
|
||
// optional fields, so the extended tail immediately reads the overlay id.
|
||
// ACE WritePackedDwordOfKnownType strips the 0x06000000 prefix before
|
||
// packing; the reader ORs it back in.
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x5000000Au,
|
||
name: "EnchantedSword",
|
||
itemType: (uint)ItemType.MeleeWeapon,
|
||
weenieFlags: 0x40000000u, // IconOverlay
|
||
iconOverlayId: 0x1ABCu); // will be read back as 0x06001ABC
|
||
|
||
var parsed = CreateObject.TryParse(body);
|
||
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0x06001ABCu, parsed!.Value.IconOverlayId);
|
||
Assert.Equal(0u, parsed.Value.IconUnderlayId);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_IconOverlayAndUnderlay_BothCaptured()
|
||
{
|
||
// IncludesSecondHeader in objDescFlags (0x04000000) makes the parser read
|
||
// weenieFlags2. weenieFlags2 bit 0x01 (IconUnderlay) triggers the underlay
|
||
// read. Both overlay + underlay are captured.
|
||
// objectDescriptionFlags: 0x04000000 = IncludesSecondHeader
|
||
// weenieFlags: 0x40000000 = IconOverlay
|
||
// weenieFlags2: 0x00000001 = IconUnderlay
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x5000000Bu,
|
||
name: "MagicRing",
|
||
itemType: (uint)ItemType.Jewelry,
|
||
objectDescriptionFlags: 0x04000000u,
|
||
weenieFlags: 0x40000000u,
|
||
weenieFlags2: 0x00000001u,
|
||
iconOverlayId: 0x5678u, // → 0x06005678
|
||
iconUnderlayId: 0x9ABCu); // → 0x06009ABC
|
||
|
||
var parsed = CreateObject.TryParse(body);
|
||
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0x06005678u, parsed!.Value.IconOverlayId);
|
||
Assert.Equal(0x06009ABCu, parsed.Value.IconUnderlayId);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_NoOverlayBits_CommonCase_OverlaysStayZero()
|
||
{
|
||
// Regression guard: most spawned entities (creatures, scenery, players)
|
||
// have weenieFlags=0 and no second-header. The extended walk must not
|
||
// corrupt existing parsed fields and must leave overlay ids at zero.
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x5000000Cu,
|
||
name: "CommonDrudge",
|
||
itemType: (uint)ItemType.Creature,
|
||
weenieFlags: 0u);
|
||
|
||
var parsed = CreateObject.TryParse(body);
|
||
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal("CommonDrudge", parsed!.Value.Name);
|
||
Assert.Equal(0u, parsed.Value.IconOverlayId);
|
||
Assert.Equal(0u, parsed.Value.IconUnderlayId);
|
||
Assert.Null(parsed.Value.Useability);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_IntermediateFieldsBeforeIconOverlay_SkippedCorrectly()
|
||
{
|
||
// Verifies the cursor arithmetic for fields between UseRadius and
|
||
// IconOverlay. This body sets several intermediate bits (Structure u16,
|
||
// MaxStructure u16, StackSize u16, Burden u16) plus IconOverlay.
|
||
// If any skip is wrong, the parser reads the wrong bytes as the
|
||
// overlay id or throws, both of which the assert would catch.
|
||
// 0x00000400 = Structure (u16)
|
||
// 0x00000800 = MaxStructure (u16)
|
||
// 0x00001000 = StackSize (u16)
|
||
// 0x00200000 = Burden (u16)
|
||
// 0x40000000 = IconOverlay
|
||
const uint flags = 0x40000000u | 0x00200000u | 0x00001000u | 0x00000800u | 0x00000400u;
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x5000000Du,
|
||
name: "FancySword",
|
||
itemType: (uint)ItemType.MeleeWeapon,
|
||
weenieFlags: flags,
|
||
structure: 50,
|
||
maxStructure: 100,
|
||
stackSize: 1,
|
||
burden: 300,
|
||
iconOverlayId: 0x2222u); // → 0x06002222
|
||
|
||
var parsed = CreateObject.TryParse(body);
|
||
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0x06002222u, parsed!.Value.IconOverlayId);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_HouseRestrictionsSkipped_ThenIconOverlayCaptured()
|
||
{
|
||
// Verifies that the variable-length RestrictionDB skip (weenieFlags bit
|
||
// 0x04000000) lands the cursor at the correct position so that
|
||
// IconOverlay (bit 0x40000000) immediately after it is still captured.
|
||
//
|
||
// Wire layout per ACE RestrictionDB (16 bytes, zero entries):
|
||
// Version(u32) + OpenStatus(u32) + MonarchId(u32) = 12 bytes
|
||
// count(u16) + numBuckets(u16) = 4 bytes
|
||
// entries: count(0) × 8 = 0 bytes
|
||
// total = 16 bytes
|
||
//
|
||
// Also exercises the IncludesSecondHeader / IconUnderlay path so that
|
||
// all three optional-tail branches that follow HouseOwner are covered
|
||
// in a single cursor sweep.
|
||
//
|
||
// weenieFlags: 0x04000000 (HouseRestrictions) | 0x40000000 (IconOverlay)
|
||
// objectDescriptionFlags: 0x04000000 (IncludesSecondHeader → weenieFlags2 present)
|
||
// weenieFlags2: 0x00000001 (IconUnderlay)
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x5000000Eu,
|
||
name: "HousePortal",
|
||
itemType: (uint)ItemType.Portal,
|
||
objectDescriptionFlags: 0x04000000u, // IncludesSecondHeader
|
||
weenieFlags: 0x04000000u | 0x40000000u, // HouseRestrictions + IconOverlay
|
||
weenieFlags2: 0x00000001u, // IconUnderlay
|
||
iconOverlayId: 0x3333u, // → 0x06003333
|
||
iconUnderlayId: 0x4444u); // → 0x06004444
|
||
|
||
var parsed = CreateObject.TryParse(body);
|
||
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0x06003333u, parsed!.Value.IconOverlayId);
|
||
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);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_WeenieClassId_Surfaced()
|
||
{
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x50000020u, name: "Sword", itemType: (uint)ItemType.MeleeWeapon,
|
||
weenieClassId: 0xABCDu);
|
||
var parsed = CreateObject.TryParse(body);
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0xABCDu, parsed!.Value.WeenieClassId);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_FullItemFields_Captured()
|
||
{
|
||
uint flags =
|
||
0x00000008u | 0x00001000u | 0x00002000u | 0x00200000u |
|
||
0x00000002u | 0x00000004u | 0x00004000u | 0x00008000u |
|
||
0x00010000u | 0x00020000u | 0x00040000u | 0x00000400u |
|
||
0x00000800u | 0x01000000u;
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x50000021u, name: "Pack", itemType: (uint)ItemType.Container,
|
||
weenieFlags: flags,
|
||
value: 250u, stackSize: 7, maxStackSize: 100u, burden: 42,
|
||
itemsCapacity: 24, containersCapacity: 7,
|
||
container: 0x50000099u, wielder: 0x5000009Au,
|
||
validLocations: 0x02000000u, currentWieldedLocation: 0x02000000u,
|
||
priority: 8u, structure: 5, maxStructure: 10, workmanship: 7.5f);
|
||
var parsed = CreateObject.TryParse(body);
|
||
Assert.NotNull(parsed);
|
||
var p = parsed!.Value;
|
||
Assert.Equal(250, p.Value);
|
||
Assert.Equal(7, p.StackSize);
|
||
Assert.Equal(100, p.StackSizeMax);
|
||
Assert.Equal(42, p.Burden);
|
||
Assert.Equal(24, p.ItemsCapacity);
|
||
Assert.Equal(7, p.ContainersCapacity);
|
||
Assert.Equal(0x50000099u, p.ContainerId);
|
||
Assert.Equal(0x5000009Au, p.WielderId);
|
||
Assert.Equal(0x02000000u, p.ValidLocations);
|
||
Assert.Equal(0x02000000u, p.CurrentWieldedLocation);
|
||
Assert.Equal(8u, p.Priority);
|
||
Assert.Equal(5, p.Structure);
|
||
Assert.Equal(10, p.MaxStructure);
|
||
Assert.Equal(7.5f, p.Workmanship);
|
||
}
|
||
|
||
[Fact]
|
||
public void TryParse_MidTailFieldsSet_StillReachesIconOverlay()
|
||
{
|
||
uint flags = 0x00001000u | 0x00004000u | 0x40000000u;
|
||
byte[] body = BuildMinimalCreateObjectWithWeenieHeader(
|
||
guid: 0x50000022u, name: "Ring", itemType: (uint)ItemType.Jewelry,
|
||
weenieFlags: flags, stackSize: 1, container: 0x500000F0u,
|
||
iconOverlayId: 0x4321u);
|
||
var parsed = CreateObject.TryParse(body);
|
||
Assert.NotNull(parsed);
|
||
Assert.Equal(0x06004321u, parsed!.Value.IconOverlayId);
|
||
Assert.Equal(0x500000F0u, parsed.Value.ContainerId);
|
||
}
|
||
|
||
private static byte[] BuildMinimalCreateObjectWithWeenieHeader(
|
||
uint guid,
|
||
string name,
|
||
uint itemType,
|
||
uint physicsState = 0,
|
||
uint objectDescriptionFlags = 0,
|
||
uint weenieFlags = 0,
|
||
uint weenieFlags2 = 0,
|
||
uint iconId = 0,
|
||
uint uiEffects = 0,
|
||
uint? value = null,
|
||
uint? useability = null,
|
||
float? useRadius = null,
|
||
uint iconOverlayId = 0,
|
||
uint iconUnderlayId = 0,
|
||
// intermediate fields for cursor-arithmetic test
|
||
ushort? structure = null,
|
||
ushort? maxStructure = null,
|
||
ushort? stackSize = null,
|
||
ushort? burden = null,
|
||
uint weenieClassId = 0x1234,
|
||
uint? maxStackSize = null,
|
||
byte? itemsCapacity = null,
|
||
byte? containersCapacity = null,
|
||
uint? container = null,
|
||
uint? wielder = null,
|
||
uint? validLocations = null,
|
||
uint? currentWieldedLocation = null,
|
||
uint? priority = null,
|
||
float? workmanship = null)
|
||
{
|
||
var bytes = new List<byte>();
|
||
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, weenieClassId); // WeenieClassId
|
||
WritePackedDword(bytes, iconId); // IconId via known-type writer (prefix stripped by ACE writer)
|
||
WriteU32(bytes, itemType);
|
||
WriteU32(bytes, objectDescriptionFlags);
|
||
Align4(bytes);
|
||
|
||
// IncludesSecondHeader → weenieFlags2 written immediately after the align,
|
||
// before any other optional tail field (ACE WorldObject_Networking.cs:84-85).
|
||
if ((objectDescriptionFlags & 0x04000000u) != 0)
|
||
WriteU32(bytes, weenieFlags2);
|
||
|
||
// Optional WeenieHeader tail — same order as ACE
|
||
// WorldObject_Networking.cs:87-206. Each field is written only when
|
||
// its weenieFlags bit is set, matching the parser's walker exactly.
|
||
// Fields not parameterized above default to 0.
|
||
if ((weenieFlags & 0x00000001u) != 0) { /* PluralName — not parameterized */ }
|
||
if ((weenieFlags & 0x00000002u) != 0) bytes.Add(itemsCapacity ?? 0); // ItemsCapacity u8
|
||
if ((weenieFlags & 0x00000004u) != 0) bytes.Add(containersCapacity ?? 0); // ContainersCapacity u8
|
||
if ((weenieFlags & 0x00000100u) != 0) WriteU16(bytes, 0); // AmmoType u16
|
||
if ((weenieFlags & 0x00000008u) != 0) WriteU32(bytes, value ?? 0u); // Value u32
|
||
if ((weenieFlags & 0x00000010u) != 0) WriteU32(bytes, useability ?? 0u); // Usable u32
|
||
if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32
|
||
{
|
||
Span<byte> tmp = stackalloc byte[4];
|
||
BinaryPrimitives.WriteSingleLittleEndian(tmp, useRadius ?? 0f);
|
||
bytes.AddRange(tmp.ToArray());
|
||
}
|
||
if ((weenieFlags & 0x00080000u) != 0) WriteU32(bytes, 0); // TargetType 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
|
||
if ((weenieFlags & 0x00001000u) != 0) WriteU16(bytes, stackSize ?? 0); // StackSize u16
|
||
if ((weenieFlags & 0x00002000u) != 0) WriteU16(bytes, (ushort)(maxStackSize ?? 0)); // MaxStackSize u16
|
||
if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, container ?? 0); // Container u32
|
||
if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, wielder ?? 0); // Wielder u32
|
||
if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, validLocations ?? 0); // ValidLocations u32
|
||
if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, currentWieldedLocation ?? 0); // CurrentlyWieldedLocation u32
|
||
if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, priority ?? 0); // Priority u32
|
||
if ((weenieFlags & 0x00100000u) != 0) bytes.Add(0); // RadarBlipColor u8
|
||
if ((weenieFlags & 0x00800000u) != 0) bytes.Add(0); // RadarBehavior u8
|
||
if ((weenieFlags & 0x08000000u) != 0) WriteU16(bytes, 0); // PScript u16
|
||
if ((weenieFlags & 0x01000000u) != 0) // Workmanship f32
|
||
{
|
||
Span<byte> tmp = stackalloc byte[4];
|
||
BinaryPrimitives.WriteSingleLittleEndian(tmp, workmanship ?? 0f);
|
||
bytes.AddRange(tmp.ToArray());
|
||
}
|
||
if ((weenieFlags & 0x00200000u) != 0) WriteU16(bytes, burden ?? 0); // Burden u16
|
||
if ((weenieFlags & 0x00400000u) != 0) WriteU16(bytes, 0); // Spell u16
|
||
if ((weenieFlags & 0x02000000u) != 0) WriteU32(bytes, 0); // HouseOwner u32
|
||
// HouseRestrictions (0x04000000): not parameterized (zero entries).
|
||
// Wire: Version(u32) + OpenStatus(u32) + MonarchId(u32) + count(u16) + numBuckets(u16) + entries.
|
||
// Zero entries → 16 bytes total.
|
||
if ((weenieFlags & 0x04000000u) != 0)
|
||
{
|
||
WriteU32(bytes, 0x10000002u); // Version
|
||
WriteU32(bytes, 0u); // OpenStatus
|
||
WriteU32(bytes, 0u); // MonarchId
|
||
WriteU16(bytes, 0); // count
|
||
WriteU16(bytes, 768); // numBuckets (retail constant)
|
||
}
|
||
if ((weenieFlags & 0x20000000u) != 0) WriteU32(bytes, 0); // HookItemTypes u32
|
||
if ((weenieFlags & 0x00000040u) != 0) WriteU32(bytes, 0); // Monarch u32
|
||
if ((weenieFlags & 0x10000000u) != 0) WriteU16(bytes, 0); // HookType u16
|
||
if ((weenieFlags & 0x40000000u) != 0) WritePackedDword(bytes, iconOverlayId); // IconOverlay
|
||
if ((weenieFlags2 & 0x00000001u) != 0) WritePackedDword(bytes, iconUnderlayId); // IconUnderlay
|
||
|
||
return bytes.ToArray();
|
||
}
|
||
|
||
private static void WriteU32(List<byte> bytes, uint value)
|
||
{
|
||
Span<byte> tmp = stackalloc byte[4];
|
||
BinaryPrimitives.WriteUInt32LittleEndian(tmp, value);
|
||
bytes.AddRange(tmp.ToArray());
|
||
}
|
||
|
||
private static void WriteU16(List<byte> bytes, ushort value)
|
||
{
|
||
Span<byte> tmp = stackalloc byte[2];
|
||
BinaryPrimitives.WriteUInt16LittleEndian(tmp, value);
|
||
bytes.AddRange(tmp.ToArray());
|
||
}
|
||
|
||
private static void WritePackedDword(List<byte> 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<byte> 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<byte> bytes)
|
||
{
|
||
while ((bytes.Count & 3) != 0)
|
||
bytes.Add(0);
|
||
}
|
||
}
|