The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0, so IconComposer's underlay/overlay layers were never drawn on toolbar icons. Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the serializer is the authority; acdream connects to a local ACE server): UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) → Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) → Container(u32) → Wielder(u32) → ValidLocations(u32) → CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) → RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) → Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) → HookItemTypes(u32) → Monarch(u32) → HookType(u16) → IconOverlay(PackedDwordKnownType) ← CAPTURE → IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32) + count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a fixed constant. weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader (objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested. The entire extended walk is inside try/catch: truncated packets degrade to IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting. Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params (defaulted 0, backward-compatible). No change to IconComposer or ToolbarController (they already consume the ids). Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay regression, intermediate-fields cursor arithmetic). Full suite: 0 failures, 2636 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
448 lines
19 KiB
C#
448 lines
19 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);
|
|
}
|
|
|
|
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? 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)
|
|
{
|
|
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, 0x1234); // 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(0); // ItemsCapacity u8
|
|
if ((weenieFlags & 0x00000004u) != 0) bytes.Add(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, 0); // 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, 0); // MaxStackSize u16
|
|
if ((weenieFlags & 0x00004000u) != 0) WriteU32(bytes, 0); // Container u32
|
|
if ((weenieFlags & 0x00008000u) != 0) WriteU32(bytes, 0); // Wielder u32
|
|
if ((weenieFlags & 0x00010000u) != 0) WriteU32(bytes, 0); // ValidLocations u32
|
|
if ((weenieFlags & 0x00020000u) != 0) WriteU32(bytes, 0); // CurrentlyWieldedLocation u32
|
|
if ((weenieFlags & 0x00040000u) != 0) WriteU32(bytes, 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, 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);
|
|
}
|
|
}
|