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:
Erik 2026-05-15 20:07:32 +02:00
parent 520badd566
commit 58e155615d
5 changed files with 369 additions and 16 deletions

View file

@ -79,15 +79,23 @@ public sealed class TargetIndicatorPanel
/// <item>Small carry items (Money, Food, Gem, SpellComponents,
/// Misc, Weapons, Armour, Clothing, Jewelry, Container):
/// 0.8 m (item dropped on the ground)</item>
/// <item>Everything else (signs, generic objects, untyped
/// scenery interactables): 1.5 m (mid-sized object
/// default; without mesh AABB this is a best guess)</item>
/// <item>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 &gt; 1 grows
/// the box proportionally.)</item>
/// </list>
///
/// <para>
/// 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.
/// <see cref="AcDream.Core.Physics.PhysicsDataCache.GetVisualBounds"/>
/// already caches per-GfxObj AABBs; combining them across a
/// multi-part Setup gives the entity-level bounds we'd want.
/// </para>
/// </summary>
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;
}
/// <summary>