feat(retail): Commit B — retail-faithful AP cadence + screen-rect picker
Retires divergences flagged in the 2026-05-16 faithfulness audit: 1. AP cadence. Replaces the 1 Hz idle / 10 Hz active flat heartbeat with a diff-driven model gated on `Contact && OnWalkable` (acclient_2013_pseudo_c.txt:700327 SendPositionEvent). Sends on position or cell change while grounded on walkable, plus a 1 sec heartbeat; suppressed entirely airborne. PlayerMovementController exposes `NotePositionSent(pos, cellId, now)` which GameWindow stamps after each AutonomousPosition / MoveToState send — mirrors retail's shared `last_sent_position_time` between SendPositionEvent (0x006b4770) and SendMovementEvent (0x006b4680). Known divergence from retail: ours is per-frame-while-moving, retail's effective rate is ~1 Hz during smooth motion (cell/plane checks). Filed as #74, blocked by #63 — when #63 lands we revert to retail's narrower gate. 2. Workaround retirement. Removes TinyMargin (0.05 m inside arrival) and the AP-flush before re-send (`SendAutonomousPositionNow`). The diff-driven cadence makes both obsolete. Close-range turn-first deferred Use is kept (it IS retail — ACE Player_Move.cs:66-87 mirrors retail's CreateMoveToChain pre-callback rotation), renamed `OnAutoWalkArrivedSendDeferredAction` to clarify it's a FIRST send. `isRetryAfterArrival` parameter dropped. 3. Far-range Use/PickUp retry. Restored — was load-bearing, not the "redundant cleanup" the Group 2 audit thought. Issue #63 means ACE drops the first Use as too-far without re-polling on subsequent APs; the arrival re-send is what makes far-range Use complete. Logs include `(queued for arrival re-send pending #63)` to make this explicit. Removes when #63 closes. 4. Screen-rect picker. New `AcDream.Core.Selection.ScreenProjection` helper shared by `WorldPicker` and `TargetIndicatorPanel`. The `Setup.SelectionSphere` projects to a screen-space square (retail anchor `SmartBox::GetObjectBoundingBox` 0x00452e20); picker hit-tests the mouse pixel against the same rect the indicator draws, inflated by 8 px (`TriangleSize`). Guarantees what-you-see is what-you-click — including rect corners that were dead zones under the old ray-sphere picker. Per-type radius (1.0/1.6/2.0 m) and vertical-offset (0.2/0.9/1.0/1.5 m) heuristic lambdas retired; `IsTallSceneryGuid` deleted; `EntityHeightFor` trimmed to 1.5 m × scale defensive default. No defensive sphere synth — entities without a baked `SelectionSphere` are skipped, matching retail's `GfxObjUnderSelectionRay` (0x0054c740). 5. Rotation rate run multiplier (Commit A precursor). `TurnRateFor(running)` helper applies retail's `run_turn_factor = 1.5f` (PDB-named 0x007c8914) under HoldKey.Run, matching `apply_run_to_command` at 0x00527be0 (line 305098). Effective: walking ≈ 90°/s, running ≈ 135°/s. Keyboard A/D + ApplyAutoWalkOverlay both use it. 6. Useability gate (Commit A precursor). `IsUseableTarget` corrected to `useability != 0` per `ItemUses::IsUseable` at 256455 — ANY non-zero passes (USEABLE_NO=1, USEABLE_CONTAINED=8, etc.), not just the USEABLE_REMOTE bit. Cross-checked against 4 call sites in retail (ItemHolder::UseObject 0x00588a80, DetermineUseResult 0x402697, UsingItem 0x367638, disable-button-state 0x198826). Added `ProbeUseabilityFallbackEnabled` diagnostic (`ACDREAM_PROBE_USEABILITY_FALLBACK=1`) to measure how often the creature/BF_DOOR fallback fires for ACE-seed-DB entities with null useability. CLAUDE.md updated with the graceful-shutdown rule for relaunch: Stop-Process bypasses the logout packet, leaving ACE's session marked logged-in for ~3+ min. CloseMainWindow() sends WM_CLOSE so the shutdown hook runs and the logout packet reaches ACE. Tests: +3 ScreenProjectionTests + 6 WorldPickerRectOverloadTests = +9. Core.Net 294/294 pass; Core 1073/1081 (8 pre-existing Physics failures unchanged). Visual-verified 2026-05-16: rotation rate, useability, screen-rect click area, double-click + R-key + F-key Use/PickUp at short and long range — dialogue/door/pickup fire on arrival. Filed follow-ups #70 (triangle apex/size DAT sprite), #71 (picker Stage B polygon refine), #72 (cdb omega.z probe), #73 (retail-message sweep pattern), #74 (per-frame AP chattier than retail — blocked by #63). Old ray-sphere `WorldPicker.Pick(origin, direction, ...)` overload kept for back-compat; no callers in acdream proper. Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2bc3a9e99
commit
b5da17db76
10 changed files with 1348 additions and 573 deletions
|
|
@ -84,89 +84,18 @@ public sealed class TargetIndicatorPanel
|
|||
public float EntityHeight { get; set; } = 1.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the world-space height to use for a given entity's
|
||||
/// indicator box. The base height per type is multiplied by the
|
||||
/// entity's <paramref name="scale"/> so an upscaled sign or NPC
|
||||
/// gets a proportionally bigger box.
|
||||
///
|
||||
/// <para>Per-type base height:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Creature (NPC / monster / player): 1.8 m (humanoid)</item>
|
||||
/// <item>Door / Lifestone / Portal: 2.4 m (door-frame tall)</item>
|
||||
/// <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 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.)</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.
|
||||
/// <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>
|
||||
/// Defensive fallback height when the entity has no usable
|
||||
/// SelectionSphere (Radius ≤ 1e-4f). With B.7's sphere-projection
|
||||
/// path active (since commit f4f4143), this fallback only fires
|
||||
/// for entities whose Setup didn't bake a selection sphere —
|
||||
/// rare in practice. The single 1.5 m × scale default is a sane
|
||||
/// midpoint; per-type branches were retired in the 2026-05-16
|
||||
/// Commit B because the sphere path is authoritative.
|
||||
/// </summary>
|
||||
public float EntityHeightFor(uint itemType, uint pwdBitfield, float scale, uint? useability = null)
|
||||
{
|
||||
if (scale <= 0f) scale = 1f; // defensive
|
||||
bool isCreature = (itemType & (uint)AcDream.Core.Items.ItemType.Creature) != 0;
|
||||
if (isCreature) return 1.8f * scale;
|
||||
|
||||
// BF_DOOR = 0x1000, BF_LIFESTONE = 0x4000, BF_PORTAL = 0x40000,
|
||||
// BF_CORPSE = 0x2000 (acclient.h:6431-6463).
|
||||
const uint TallStructureMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
|
||||
if ((pwdBitfield & TallStructureMask) != 0) return 2.4f * scale;
|
||||
|
||||
// 2026-05-15 — KEY DISCRIMINATOR. Misc-class ItemTypes are
|
||||
// ambiguous in retail: dropped jewellery / coins / food / tapers
|
||||
// are Misc, but so are signs, banners, and decorative scenery.
|
||||
// ACE distinguishes the two via ITEM_USEABLE (acclient.h:6478):
|
||||
// a real pickup item has USEABLE_REMOTE (0x20) set; a sign has
|
||||
// USEABLE_UNDEF (0). If we know useability and it lacks
|
||||
// USEABLE_REMOTE, treat the entity as tall scenery regardless
|
||||
// of ItemType. This is what fixes the Holtburg town sign
|
||||
// showing a tiny pole-base box.
|
||||
const uint USEABLE_REMOTE_BIT = 0x20u;
|
||||
bool useableFromWorld = useability is uint u
|
||||
&& (u & USEABLE_REMOTE_BIT) != 0;
|
||||
|
||||
// Small carry items: weapons / armour / clothing / jewellery /
|
||||
// money / food / misc / weapons / containers / gems / spell
|
||||
// components / writable / keys / casters / lockables.
|
||||
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);
|
||||
|
||||
// Real pickup item: ItemType is small-item-class AND the server
|
||||
// marked it useable from the world. 0.8 m × scale box.
|
||||
if ((itemType & SmallItemMask) != 0 && useableFromWorld) return 0.8f * scale;
|
||||
|
||||
// Tall scenery: anything else (signs, banners, untyped
|
||||
// post-mounted objects, AND Misc-typed-but-non-useable entities
|
||||
// like the Holtburg sign). 3 m × scale covers a typical
|
||||
// post-mounted sign from ground to top.
|
||||
return 3.0f * scale;
|
||||
if (scale <= 0f) scale = 1f;
|
||||
return 1.5f * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -214,8 +143,10 @@ public sealed class TargetIndicatorPanel
|
|||
|
||||
if (info.WorldSphereCenter is Vector3 sphereCenter
|
||||
&& info.WorldSphereRadius is float sphereRadius
|
||||
&& TryComputeScreenRectFromSphere(sphereCenter, sphereRadius, view, projection, viewport,
|
||||
out var rMin, out var rMax))
|
||||
&& AcDream.Core.Selection.ScreenProjection.TryProjectSphereToScreenRect(
|
||||
sphereCenter, sphereRadius, view, projection, viewport,
|
||||
out var rMin, out var rMax, out _,
|
||||
minSidePixels: 12f))
|
||||
{
|
||||
// 2026-05-16 — retail-faithful path per
|
||||
// SmartBox::GetObjectBoundingBox (decomp 0x00452e20).
|
||||
|
|
@ -294,67 +225,6 @@ public sealed class TargetIndicatorPanel
|
|||
drawList.AddTriangleFilled(bl + new Vector2( t, -t), bl + new Vector2( t, 0), bl + new Vector2(0, -t), col);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-05-16. Project a world-space sphere (center + radius) as a
|
||||
/// screen-space square and return its bounding rectangle. Matches
|
||||
/// retail <c>SmartBox::GetObjectBoundingBox</c> (decomp
|
||||
/// <c>0x00452e20</c>) which uses
|
||||
/// <c>Render::GetViewerBBox(sphere, &corner1, &corner2)</c>
|
||||
/// to compute a camera-aligned BBox of the sphere then projects
|
||||
/// the 2 corner points.
|
||||
///
|
||||
/// <para>
|
||||
/// Mathematical equivalent (faster, no per-corner reprojection):
|
||||
/// project the sphere center to screen, then compute the
|
||||
/// screen-space radius as
|
||||
/// <c>worldRadius * projection.M22 * viewport.Y / (2 * clip.W)</c>
|
||||
/// where <c>M22 = 1/tan(fovY/2)</c> for a standard right-handed
|
||||
/// perspective. The rect is centered at the projected sphere
|
||||
/// center with side length <c>2 * screenRadius</c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Returns <c>false</c> if the sphere center is behind the camera
|
||||
/// (<c>clip.W <= 0</c>).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static bool TryComputeScreenRectFromSphere(
|
||||
Vector3 worldCenter, float worldRadius,
|
||||
Matrix4x4 view, Matrix4x4 projection, Vector2 viewport,
|
||||
out Vector2 rectMin, out Vector2 rectMax)
|
||||
{
|
||||
rectMin = default;
|
||||
rectMax = default;
|
||||
|
||||
var viewProj = view * projection;
|
||||
var clip = Vector4.Transform(new Vector4(worldCenter, 1f), viewProj);
|
||||
if (clip.W <= 0.001f) return false;
|
||||
|
||||
float ndcX = clip.X / clip.W;
|
||||
float ndcY = clip.Y / clip.W;
|
||||
float screenX = (ndcX * 0.5f + 0.5f) * viewport.X;
|
||||
float screenY = (1f - (ndcY * 0.5f + 0.5f)) * viewport.Y;
|
||||
|
||||
// Screen-space radius. projection.M22 = cot(fovY/2). clip.W is
|
||||
// the camera-space distance (positive in front of camera for
|
||||
// standard right-handed perspective).
|
||||
float scaleY = projection.M22;
|
||||
if (scaleY <= 0f) return false;
|
||||
float screenRadius = worldRadius * scaleY * viewport.Y / (2f * clip.W);
|
||||
|
||||
// Cull obviously-off-screen entities (more than a screen away).
|
||||
if (screenX + screenRadius < -viewport.X || screenX - screenRadius > 2f * viewport.X) return false;
|
||||
if (screenY + screenRadius < -viewport.Y || screenY - screenRadius > 2f * viewport.Y) return false;
|
||||
|
||||
// Floor at MinSide so distant entities still get a visible indicator.
|
||||
const float MinSide = 12f;
|
||||
if (screenRadius < MinSide * 0.5f) screenRadius = MinSide * 0.5f;
|
||||
|
||||
rectMin = new Vector2(screenX - screenRadius, screenY - screenRadius);
|
||||
rectMax = new Vector2(screenX + screenRadius, screenY + screenRadius);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Project a world-space point to screen-space pixels. Returns
|
||||
/// <c>false</c> if the point is behind the camera or outside the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue