diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 2efad8c..7cdf527 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -9007,6 +9007,14 @@ public sealed class GameWindow : IDisposable
// origin is a single point. 0.7 m default is fine for
// humanoids and most items; doors / portals need ~2 m
// to cover the doorframe.
+ //
+ // 2026-05-15 sign-class extension: post-mounted scenery
+ // (Holtburg town sign etc.) needs the sphere TALLER than
+ // wider. We classify "non-creature, non-flat, non-small-item"
+ // as tall scenery and grow the sphere to 1.6 m radius lifted
+ // to 1.5 m vertical offset — covers a 3 m post from
+ // ground to top. Mirrors TargetIndicatorPanel.EntityHeightFor's
+ // 3 m default so the click sphere matches the visible box.
radiusForGuid: g =>
{
if (_lastSpawnByGuid.TryGetValue(g, out var s)
@@ -9018,6 +9026,7 @@ public sealed class GameWindow : IDisposable
const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
if ((odf & LargeFlatMask) != 0) return 2.0f;
}
+ if (IsTallSceneryGuid(g)) return 1.6f;
// 1.0 m sphere centred at chest height (see
// verticalOffsetForGuid) covers a 1.8 m humanoid from
// shin to crown without overlapping neighbours.
@@ -9039,6 +9048,7 @@ public sealed class GameWindow : IDisposable
const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
if ((odf & LargeFlatMask) != 0) return 1.0f; // mid-door
}
+ if (IsTallSceneryGuid(g)) return 1.5f; // mid-pole height
return 0.2f; // small ground item — sphere just above feet
});
@@ -9079,6 +9089,19 @@ public sealed class GameWindow : IDisposable
return;
}
+ // 2026-05-15: retail-faithful useability gate. Signs / banners /
+ // decorative scenery have ITEM_USEABLE = USEABLE_UNDEF (acclient.h:6478)
+ // and the retail client silently ignores R-key on them — no walk,
+ // no packet, no toast. We honor that here. The user can still
+ // left-click to select and see the entity's name; only the
+ // interact action is suppressed.
+ if (!IsUseableTarget(sel))
+ {
+ if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
+ Console.WriteLine($"[B.4b] use ignored — not useable guid=0x{sel:X8}");
+ return;
+ }
+
// B.7 (2026-05-15): the user requested R behave as a universal
// interact key — pickup for items, use for NPCs / doors /
// lifestones / portals / corpses. Matches retail's "use"
@@ -9113,6 +9136,18 @@ public sealed class GameWindow : IDisposable
_debugVm?.AddToast("Not in world");
return;
}
+ // 2026-05-15: defense-in-depth useability gate. Double-click flows
+ // directly through SendUse without passing UseCurrentSelection's
+ // dispatcher gate, so re-check here. Silent ignore matches retail
+ // (acclient.h:6478 ITEM_USEABLE — USEABLE_REMOTE bit required).
+ // isRetryAfterArrival bypasses the gate because we only retry an
+ // action we previously gated through.
+ if (!isRetryAfterArrival && !IsUseableTarget(guid))
+ {
+ if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
+ Console.WriteLine($"[B.4b] SendUse ignored — not useable guid=0x{guid:X8}");
+ return;
+ }
// B.6 (2026-05-15): install a speculative auto-walk on the local
// player toward the target. For far targets ACE will overwrite
// this with its own MovementType=6 wire payload (and a better
@@ -9165,6 +9200,18 @@ public sealed class GameWindow : IDisposable
_debugVm?.AddToast("Not in world");
return;
}
+ // 2026-05-15: useability gate (acclient.h:6478 ITEM_USEABLE).
+ // F-key on a non-useable entity (sign, banner, decorative
+ // scenery) is silently ignored — without this we'd send
+ // PutItemInContainer for a sign and ACE would reply with a
+ // noisy InventoryServerSaveFailed. Retail's client doesn't
+ // attempt the wire send at all.
+ if (!isRetryAfterArrival && !IsUseableTarget(itemGuid))
+ {
+ if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
+ Console.WriteLine($"[B.5] SendPickUp ignored — not useable item=0x{itemGuid:X8}");
+ return;
+ }
// B.6 (2026-05-15): same speculative turn-to-target + deferral as
// SendUse — close-range pickup rotates locally to face the
// item first, then the wire packet fires when the local
@@ -9429,6 +9476,130 @@ public sealed class GameWindow : IDisposable
return (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0;
}
+ ///
+ /// 2026-05-15. True when the entity is "tall scenery" — has a known
+ /// non-zero ItemType that is NOT in the small-carry-item mask AND
+ /// has no door/lifestone/portal/corpse PWD bits AND is not a
+ /// creature. The Holtburg town sign is the canonical example: a
+ /// 3 m post-mounted entity that needs the pick sphere lifted to
+ /// mid-pole with a wider radius so the user can click any part of
+ /// the visible mesh, not just the pole base.
+ ///
+ ///
+ /// Mirrors 's
+ /// classification — both fall into the "everything else: 3 m default"
+ /// branch — so the visible indicator box and the click sphere
+ /// match.
+ ///
+ ///
+ private bool IsTallSceneryGuid(uint guid)
+ {
+ if (_liveEntityInfoByGuid.TryGetValue(guid, out var info)
+ && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0)
+ return false;
+
+ if (!_lastSpawnByGuid.TryGetValue(guid, out var spawn))
+ return false;
+
+ if (spawn.ObjectDescriptionFlags is { } odf)
+ {
+ // Excludes door/lifestone/portal/corpse — handled by
+ // the LargeFlatMask branch in the picker callbacks.
+ const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
+ if ((odf & LargeFlatMask) != 0) return false;
+ }
+
+ uint it = spawn.ItemType ?? 0u;
+ if (it == 0u) return false; // no ItemType info — keep default behaviour
+
+ // Same SmallItemMask as TargetIndicatorPanel.EntityHeightFor.
+ const uint SmallItemMask =
+ (uint)(AcDream.Core.Items.ItemType.MeleeWeapon
+ | AcDream.Core.Items.ItemType.Armor
+ | AcDream.Core.Items.ItemType.Clothing
+ | AcDream.Core.Items.ItemType.Jewelry
+ | AcDream.Core.Items.ItemType.Food
+ | AcDream.Core.Items.ItemType.Money
+ | AcDream.Core.Items.ItemType.Misc
+ | AcDream.Core.Items.ItemType.MissileWeapon
+ | AcDream.Core.Items.ItemType.Container
+ | AcDream.Core.Items.ItemType.Gem
+ | AcDream.Core.Items.ItemType.SpellComponents
+ | AcDream.Core.Items.ItemType.Writable
+ | AcDream.Core.Items.ItemType.Key
+ | AcDream.Core.Items.ItemType.Caster);
+ if ((it & SmallItemMask) != 0) return false;
+
+ // Has an ItemType, but not creature / flat-class / small-item:
+ // tall scenery (sign / banner / generic post-mounted object).
+ return true;
+ }
+
+ ///
+ /// 2026-05-15. Retail-faithful gate for R-key Use / F-key PickUp.
+ /// Returns true when the entity is interactable from the world per
+ /// the server-supplied ITEM_USEABLE _useability field
+ /// (acclient.h:6478) — specifically when the USEABLE_REMOTE
+ /// (0x20) bit is set OR a composite form containing it
+ /// (USEABLE_REMOTE_NEVER_WALK = 0x60, the
+ /// SOURCE_X_TARGET_REMOTE variants in the 0x200000+ range).
+ ///
+ ///
+ /// Retail behaviour for non-useable entities (signs, banners,
+ /// decorative scenery): the R-key does nothing — no walk, no Use
+ /// packet, no toast. The retail client checks useability before
+ /// any action and silently ignores the press. We honor that with
+ /// a silent early return at the call site.
+ ///
+ ///
+ ///
+ /// Fallback when useability is unknown. The wire's
+ /// weenieFlags & 0x10 bit gates whether ACE serializes
+ /// useability at all. If absent,
+ /// is null. Conservatively we permit Use for entities we've
+ /// historically been able to interact with — creatures, doors,
+ /// lifestones, portals, corpses — to avoid regressing the existing
+ /// M1 flows. Pure-scenery untyped entities (the sign case) fall
+ /// through to "blocked".
+ ///
+ ///
+ private bool IsUseableTarget(uint guid)
+ {
+ if (_lastSpawnByGuid.TryGetValue(guid, out var spawn))
+ {
+ // Authoritative path: server published Useability.
+ if (spawn.Useability is uint useability)
+ {
+ // USEABLE_REMOTE (0x20) — bit set in every from-world
+ // useable variant per acclient.h:6478 ITEM_USEABLE enum.
+ const uint USEABLE_REMOTE_BIT = 0x20u;
+ return (useability & USEABLE_REMOTE_BIT) != 0;
+ }
+
+ // Useability NOT in PWD — fall back to known-useable types.
+ // ObjectDescriptionFlags BF_DOOR|BF_LIFESTONE|BF_PORTAL|BF_CORPSE
+ // historically work with Use; allow them through.
+ if (spawn.ObjectDescriptionFlags is { } odf)
+ {
+ const uint UseableFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
+ if ((odf & UseableFlatMask) != 0) return true;
+ }
+ }
+
+ // Creatures (NPCs / players) are always Use targets (dialogue,
+ // PvP target). Keeps the fallback permissive for the M1 flows.
+ if (_liveEntityInfoByGuid.TryGetValue(guid, out var info)
+ && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0)
+ {
+ return true;
+ }
+
+ // Default: not useable. Signs, banners, untyped scenery with no
+ // server-supplied useability and no creature/door PWD bits land
+ // here — exactly the retail "nothing happens" case.
+ return false;
+ }
+
private string DescribeLiveEntity(uint guid)
{
if (_liveEntityInfoByGuid.TryGetValue(guid, out var info)
diff --git a/src/AcDream.App/UI/TargetIndicatorPanel.cs b/src/AcDream.App/UI/TargetIndicatorPanel.cs
index e657f40..283386e 100644
--- a/src/AcDream.App/UI/TargetIndicatorPanel.cs
+++ b/src/AcDream.App/UI/TargetIndicatorPanel.cs
@@ -79,15 +79,23 @@ public sealed class TargetIndicatorPanel
/// - Small carry items (Money, Food, Gem, SpellComponents,
/// Misc, Weapons, Armour, Clothing, Jewelry, Container):
/// 0.8 m (item dropped on the ground)
- /// - Everything else (signs, generic objects, untyped
- /// scenery interactables): 1.5 m (mid-sized object
- /// default; without mesh AABB this is a best guess)
+ /// - Everything else (signs on a pole, generic tall scenery,
+ /// untyped scenery interactables): 3.0 m (post-on-ground
+ /// tall — bumped from 1.5 m on 2026-05-15 because the
+ /// Holtburg sign was getting a tiny pole-only box. Most
+ /// non-typed non-flat AC scenery is either small-item-on-
+ /// ground (handled above) or post-mounted; 3 m is the
+ /// right midpoint for the post case. Scale > 1 grows
+ /// the box proportionally.)
///
///
///
/// Future refinement (deferred): read the entity's actual mesh
/// AABB at registration time and use the projected silhouette
- /// for an exact-fit box. Issue #66-ish.
+ /// for an exact-fit box.
+ ///
+ /// already caches per-GfxObj AABBs; combining them across a
+ /// multi-part Setup gives the entity-level bounds we'd want.
///
///
public float EntityHeightFor(uint itemType, uint pwdBitfield, float scale)
@@ -125,11 +133,9 @@ public sealed class TargetIndicatorPanel
| AcDream.Core.Items.ItemType.Caster);
if ((itemType & SmallItemMask) != 0) return 0.8f * scale;
- // Everything else (signs, scenery interactables, untyped objects):
- // 1.5 m default — bigger than a small item but smaller than a
- // humanoid, splitting the difference until we have real mesh
- // bounds to project.
- return 1.5f * scale;
+ // Tall scenery (signs / banners / untyped post-mounted objects):
+ // 3.0 m. See class doc above for the bump rationale.
+ return 3.0f * scale;
}
///
diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs
index a5fcd7b..580de7d 100644
--- a/src/AcDream.Core.Net/Messages/CreateObject.cs
+++ b/src/AcDream.Core.Net/Messages/CreateObject.cs
@@ -126,7 +126,20 @@ public static class CreateObject
// weren't set; subscribers fall back to PhysicsBody constructor
// defaults (0.05f elasticity, 0.5f friction).
float? Friction = null,
- float? Elasticity = null);
+ float? Elasticity = null,
+ // 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
+ // (0x20)) != 0 means the entity is useable from the world via
+ // mouse Use. Signs / banners / decorative scenery have
+ // USEABLE_UNDEF (0x0) here — selecting them via left-click is
+ // fine, but R-key Use should be a no-op (retail-faithful: the
+ // character does not walk toward; nothing happens).
+ // UseRadius is the use-action's reach in meters; doubles as
+ // a sizing hint for selection indicators on entities that
+ // publish it.
+ uint? Useability = null,
+ float? UseRadius = null);
///
/// The relevant subset of the server-sent MovementData /
@@ -470,9 +483,10 @@ public static class CreateObject
// ObjectDescriptionFlags, align.
string? name = null;
uint? itemType = null;
+ uint weenieFlags = 0;
if (body.Length - pos >= 4)
{
- pos += 4; // skip weenieFlags u32
+ weenieFlags = ReadU32(body, ref pos);
try
{
name = ReadString16L(body, ref pos);
@@ -496,11 +510,80 @@ public static class CreateObject
catch { /* truncated name — partial result is still useful */ }
}
+ // --- WeenieHeader optional tail (2026-05-15): walk the
+ // conditional fields up through Useability + UseRadius.
+ //
+ // Wire order is fixed by ACE WorldObject_Networking.cs:87-114
+ // and matches retail PWD::Pack order. We MUST skip every
+ // preceding optional field (even those we don't care about)
+ // because each one moves the parse cursor.
+ //
+ // Field bit width decoded?
+ // ------- ------ -------- --------
+ // weenieFlags2 conditional on objDescFlags & 0x80000000 (BF_INCLUDES_SECOND_HEADER)
+ // u32 skipped
+ // PluralName 0x1 String16L (variable, padded to 4) skipped
+ // ItemCapacity 0x2 1 byte skipped
+ // ContainerCap 0x4 1 byte skipped
+ // AmmoType 0x100 u16 skipped
+ // Value 0x8 u32 skipped
+ // Useability 0x10 u32 KEPT
+ // UseRadius 0x20 f32 KEPT
+ //
+ // Wrapped in try/catch — if a malformed entity truncates the
+ // tail we still return the prefix fields. Most spawned entities
+ // either have all of these or none of them.
+ uint? useability = null;
+ float? useRadius = null;
+ try
+ {
+ bool hasSecondHeader = objectDescriptionFlags.HasValue
+ && (objectDescriptionFlags.Value & 0x80000000u) != 0;
+ if (hasSecondHeader && body.Length - pos >= 4) pos += 4; // weenieFlags2
+
+ if ((weenieFlags & 0x00000001u) != 0) // PluralName
+ _ = ReadString16L(body, ref pos);
+
+ if ((weenieFlags & 0x00000002u) != 0) // ItemCapacity
+ {
+ if (body.Length - pos < 1) throw new FormatException("trunc ItemCap");
+ pos += 1;
+ }
+ if ((weenieFlags & 0x00000004u) != 0) // ContainerCapacity
+ {
+ if (body.Length - pos < 1) throw new FormatException("trunc ContCap");
+ pos += 1;
+ }
+ if ((weenieFlags & 0x00000100u) != 0) // AmmoType u16
+ {
+ if (body.Length - pos < 2) throw new FormatException("trunc AmmoType");
+ pos += 2;
+ }
+ if ((weenieFlags & 0x00000008u) != 0) // Value u32
+ {
+ if (body.Length - pos < 4) throw new FormatException("trunc Value");
+ pos += 4;
+ }
+ if ((weenieFlags & 0x00000010u) != 0) // Useability u32 ← KEEP
+ {
+ if (body.Length - pos < 4) throw new FormatException("trunc Useability");
+ useability = ReadU32(body, ref pos);
+ }
+ if ((weenieFlags & 0x00000020u) != 0) // UseRadius f32 ← KEEP
+ {
+ if (body.Length - pos < 4) throw new FormatException("trunc UseRadius");
+ useRadius = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos));
+ pos += 4;
+ }
+ }
+ catch { /* truncated weenie tail — keep whatever we got. */ }
+
return new Parsed(guid, position, setupTableId, animParts,
textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId,
instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq,
physicsState, objectDescriptionFlags,
- friction, elasticity);
+ friction, elasticity,
+ useability, 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/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index 2e644c6..8b4e0f7 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -69,7 +69,18 @@ public sealed class WorldSession : IDisposable
// Elasticity defaults to 0.05f. When set, drives the velocity-
// reflection bounce magnitude (clamped to [0, 0.1] retail-side).
float? Friction = null,
- float? Elasticity = null);
+ float? Elasticity = null,
+ // 2026-05-15: from the WeenieHeader optional tail.
+ // Useability: retail ITEM_USEABLE enum (acclient.h:6478). Bit
+ // USEABLE_REMOTE (0x20) means the entity accepts R-key Use from
+ // the world; signs/banners have USEABLE_UNDEF (0x0) and should
+ // silently ignore Use attempts. null = weenieFlags didn't include
+ // the field (treat conservatively as not-useable).
+ // UseRadius: server's use-action reach in meters. Doubles as a
+ // sizing hint for tall-scenery selection indicators when the
+ // server publishes it for non-useable display entities.
+ uint? Useability = null,
+ float? UseRadius = null);
/// Fires when the session finishes parsing a CreateObject.
public event Action? EntitySpawned;
@@ -703,7 +714,9 @@ public sealed class WorldSession : IDisposable
parsed.Value.PhysicsState,
parsed.Value.ObjectDescriptionFlags,
parsed.Value.Friction,
- parsed.Value.Elasticity));
+ parsed.Value.Elasticity,
+ parsed.Value.Useability,
+ parsed.Value.UseRadius));
}
}
else if (op == DeleteObject.Opcode)
diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs
index 71bf72d..64f9566 100644
--- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs
+++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs
@@ -74,12 +74,78 @@ public sealed class CreateObjectTests
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_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 objectDescriptionFlags = 0,
+ uint weenieFlags = 0,
+ uint? value = null,
+ uint? useability = null,
+ float? useRadius = null)
{
var bytes = new List();
WriteU32(bytes, CreateObject.Opcode);
@@ -99,7 +165,7 @@ public sealed class CreateObjectTests
Align4(bytes);
// Fixed WeenieHeader prefix per ACE SerializeCreateObject.
- WriteU32(bytes, 0); // weenieFlags
+ WriteU32(bytes, weenieFlags); // weenieFlags
WriteString16L(bytes, name);
WritePackedDword(bytes, 0x1234); // WeenieClassId
WritePackedDword(bytes, 0); // IconId via known-type writer
@@ -107,6 +173,20 @@ public sealed class CreateObjectTests
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();
}