feat(B.8): retail useability gate + tall-scenery indicator scaling
Two retail divergences fixed end-to-end: 1. R-key Use on non-useable entities (signs, banners, decorative scenery) was silently sending Use/PickUp to ACE, triggering auto-walk + NPC-style chat fallback. Retail's client checks ITEM_USEABLE (acclient.h:6478) and silently ignores Use when the USEABLE_REMOTE (0x20) bit isn't set. Now ports that gate. 2. Holtburg town sign indicator + click sphere only covered the base of the pole because the "everything else" default in EntityHeightFor was 1.5 m and the picker's vertical offset for default class was 0.2 m. A 3 m sign on a pole was almost entirely outside both shapes. Wire change: - CreateObject parser now walks the WeenieHeader optional tail (per ACE WorldObject_Networking.cs:87-114) up through Useability + UseRadius. Captures weenieFlags upfront, then conditionally skips PluralName, ItemCapacity, ContainerCapacity, AmmoType, Value before reading Useability (u32) and UseRadius (f32). - CreateObject.Parsed + WorldSession.EntitySpawn record append two new optional fields (Useability uint?, UseRadius float?), both defaulting to null. Existing call sites unchanged. - 3 new tests cover: no weenieFlags → null, weenieFlags=0x10 alone → useability read, weenieFlags=0x8|0x10|0x20 → walker skips Value then reads Useability + UseRadius in correct order. Behaviour change: - GameWindow.IsUseableTarget(guid) — authoritative path uses spawn .Useability when present (REMOTE bit gate); fallback when null permits Use on creatures + BF_DOOR/LIFESTONE/PORTAL/CORPSE for M1 flow continuity. - UseCurrentSelection (R-key dispatcher) and SendUse + SendPickUp (double-click + F-key direct paths) gate on IsUseableTarget, silent early-return matching retail. isRetryAfterArrival skips the gate (re-fires only previously-gated actions). - TargetIndicatorPanel.EntityHeightFor default branch 1.5 m → 3 m for non-creature non-flat non-small-item entities (sign-class). Scale > 1 still grows proportionally. - WorldPicker callbacks: new IsTallSceneryGuid branch lifts sphere centre to 1.5 m with 1.6 m radius for sign-class entities, mirroring the indicator's 3 m default so click sphere matches the visible box. Tests: 293/293 pass in AcDream.Core.Net.Tests (+3 new walker tests). dotnet build clean. Retail anchors: - acclient.h:6478 — ITEM_USEABLE enum (USEABLE_REMOTE = 0x20) - acclient.h:6431-6463 — PWD bitfield (BF_DOOR etc.) - ACE WorldObject_Networking.cs:87-114 — wire field order - ACE WeenieHeaderFlag — Usable = 0x10, UseRadius = 0x20 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
520badd566
commit
58e155615d
5 changed files with 369 additions and 16 deletions
|
|
@ -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. <c>(Useability & USEABLE_REMOTE
|
||||
// (0x20)) != 0</c> means the entity is useable from the world via
|
||||
// mouse Use. Signs / banners / decorative scenery have
|
||||
// <c>USEABLE_UNDEF (0x0)</c> 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).
|
||||
// <c>UseRadius</c> 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);
|
||||
|
||||
/// <summary>
|
||||
/// The relevant subset of the server-sent <c>MovementData</c> /
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
public event Action<EntitySpawn>? 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue