fix(runtime/core): Slice 5.3 review corrections — retirement/transit close, per-unit pricing, guarded auto-close dispatch
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
The adversarial review's three blocking findings, each fixed at root: 1. A vendor session now CLOSES when its entity retires (despawn, death, ObjectDelete) and at teleport BEGIN (HasPendingTeleportStart || IsTeleportActive at the existing per-frame seam — both hosts funnel through RuntimeWorldTransitState.TryQueueTeleportStart, which flips the pending flag strictly before activation). The previous permissive early-return stranded the session forever: panel pinned to a stale guid, ActiveVendorId swallowing Use for the rest of the session. 2. VendorShopItem carries the desc's stack size, and VendorPricing.PerUnitValue ports retail's stack-total division (VendorProfile::VendorSellPrice 0x005D1B00: <= 0 guard, integer division) — a stack of 50 arrows now prices per arrow, not at 50x. 3. VendorState.Close() guards its observer fanout with the dispatcher's catch-and-log semantics — a throwing panel listener can no longer propagate into the unprotected per-frame path. Register honesty rides along: the 0.6 m UseRadius fallback was acdream's invention (ACE's CheckClose has no fallback; retail passes the raw authored radius) — removed, the watcher now uses the raw radius and AP-160's citations are corrected and extended with the accepted-position-snapshot cadence; AD-72 files VendorPricing's double-vs-x87-extended narrowing (AD-33's class, bounded by the ±0.1 margin). Nine tests added. Clean-room complete solution: 11,311 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9796d71522
commit
609a2dfda0
9 changed files with 369 additions and 23 deletions
|
|
@ -3,6 +3,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
|
|
@ -18,22 +19,72 @@ namespace AcDream.Runtime.Gameplay;
|
|||
/// ACE's server-side belt-and-suspenders equivalent, <c>Vendor.CheckClose</c>
|
||||
/// (<c>references/ACE/Source/ACE.Server/WorldObjects/Vendor.cs:322-367</c>),
|
||||
/// polls every 1.5 s and closes when <c>GetCylinderDistance(lastPlayer) >
|
||||
/// UseRadius</c>, falling back to <c>wo.UseRadius ?? 0.6f</c> when the vendor
|
||||
/// carries no explicit radius (<c>WorldObject_Use.cs:50,57</c>).
|
||||
/// UseRadius</c> — a plain nullable-float comparison with NO fallback
|
||||
/// default of its own (<c>UseRadius</c> is <c>float?</c>,
|
||||
/// <c>WorldObject_Properties.cs:1242-1246</c>; a nullable comparison against
|
||||
/// a null right operand is always <c>false</c>, so <c>CheckClose</c> never
|
||||
/// closes when the vendor carries no explicit radius). The <c>?? 0.6f</c>
|
||||
/// fallback this class's earlier revision mis-attributed to
|
||||
/// <c>Vendor.CheckClose</c> actually lives in a DIFFERENT method,
|
||||
/// <c>WorldObject.IsWithinUseRadiusOf</c> (<c>WorldObject_Use.cs:50,57</c>) —
|
||||
/// the APPROACH check ("how close you need to be to open the shop"), never
|
||||
/// the close/distance-watch path. Slice 5.3 review fix 4.
|
||||
/// </summary>
|
||||
public static class RuntimeVendorRangeQuery
|
||||
{
|
||||
/// <summary>ACE <c>WorldObject_Use.cs:50</c>: <c>wo.UseRadius ?? 0.6f</c>.</summary>
|
||||
private const float DefaultUseRadius = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// Close the open vendor session (if any) once the local player has
|
||||
/// moved beyond the vendor's own UseRadius. No-op when no vendor is
|
||||
/// open, or when either side's live position cannot be resolved this
|
||||
/// tick (matches the existing App-layer convention at
|
||||
/// moved beyond the vendor's own UseRadius, once the vendor entity
|
||||
/// itself is no longer resolvable, or once an in-session portal/
|
||||
/// teleport has begun. No-op only when no vendor is open, or when the
|
||||
/// PLAYER's own live position cannot be resolved this tick (matches the
|
||||
/// existing App-layer convention at
|
||||
/// <c>WorldSelectionQuery.IsWithinExternalContainerUseRange</c>: "the
|
||||
/// server remains authoritative while render projection is absent" —
|
||||
/// never force-close on missing data).
|
||||
/// never force-close on missing PLAYER data). A missing/retired VENDOR
|
||||
/// is a different case — see fix 1a below.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Fix 1b — in-session transit closes at BEGIN, not arrival (Slice
|
||||
/// 5.3 review).</b> <see cref="RuntimeWorldTransitState"/> is the
|
||||
/// canonical Runtime owner of the F751/teleport lifecycle (J6.2/J6.3).
|
||||
/// <c>HasPendingTeleportStart</c> flips true the instant
|
||||
/// <c>TryQueueTeleportStart</c> succeeds — for a GRAPHICAL host this is
|
||||
/// strictly BEFORE <c>ActivateQueuedTeleport</c> (and therefore before
|
||||
/// <c>IsTeleportActive</c>), because
|
||||
/// <c>LocalPlayerTeleportController.TryActivatePendingPresentation</c>
|
||||
/// defers activation until the host can enter portal space; for a
|
||||
/// HEADLESS host (<c>RuntimeLiveEntitySessionController.OnTeleportStarted</c>)
|
||||
/// both flip in the same synchronous call. Checking BOTH flags here
|
||||
/// observes "transit begin" through "transit still resolving" for
|
||||
/// either host without a second call site: both hosts
|
||||
/// (<c>LocalPlayerTeleportController.OnTeleportStarted</c> for
|
||||
/// graphical, <c>RuntimeLiveEntitySessionController.OnTeleportStarted</c>
|
||||
/// for headless) reach <c>TryQueueTeleportStart</c> as their sole entry
|
||||
/// point, and this method already runs once per advanced frame for BOTH
|
||||
/// hosts via the SAME post-network-command-phase callback
|
||||
/// (<see cref="GameRuntime.CreateLocalPlayerFrameController"/>) that
|
||||
/// already exists for the range check below — so this reuses that
|
||||
/// existing per-frame seam instead of adding a new polling loop or event
|
||||
/// channel. Unconditional: while a transit is in flight, distance is not
|
||||
/// even evaluated, matching how a portal/teleport makes the player's and
|
||||
/// vendor's positions momentarily incomparable (different landblock
|
||||
/// frames — see register AP-160's extension).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Fix 1a — a retired vendor entity closes the session (Slice 5.3
|
||||
/// review).</b> Retail's own range watcher
|
||||
/// (<c>CPlayerSystem::RegisterObjectRangeHandler</c>) dies with its
|
||||
/// target — it has nothing left to watch once the vendor NPC is
|
||||
/// retired (despawn, death, ObjectDelete during a recall). The prior
|
||||
/// revision's early-return here on a failed <c>TryGetActive</c> was a
|
||||
/// permissive default that stranded the session forever once the
|
||||
/// vendor left the active entity set. A missing POSITION on a still-
|
||||
/// active record gets the same treatment: a positionless vendor cannot
|
||||
/// be range-checked, and retail's watcher likewise has nothing to
|
||||
/// watch.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Distance metric divergence (register AP-160):</b> retail/ACE close
|
||||
|
|
@ -59,6 +110,16 @@ public static class RuntimeVendorRangeQuery
|
|||
if (vendorId == 0u)
|
||||
return;
|
||||
|
||||
// Fix 1b: close unconditionally the instant an in-session transit
|
||||
// has begun — see the class doc above for why HasPendingTeleportStart
|
||||
// is the earliest observable "transit begin" edge for both hosts.
|
||||
RuntimeWorldTransitState transit = runtime.TransitOwner;
|
||||
if (transit.HasPendingTeleportStart || transit.IsTeleportActive)
|
||||
{
|
||||
vendor.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
uint playerGuid = runtime.PlayerIdentity.ServerGuid;
|
||||
if (playerGuid == 0u
|
||||
|| !runtime.EntityObjects.Entities.TryGetActive(
|
||||
|
|
@ -69,15 +130,24 @@ public static class RuntimeVendorRangeQuery
|
|||
return;
|
||||
}
|
||||
|
||||
// Fix 1a: a retired vendor entity, or one left with no resolvable
|
||||
// position, closes the session instead of stranding it open forever.
|
||||
if (!runtime.EntityObjects.Entities.TryGetActive(
|
||||
vendorId,
|
||||
out RuntimeEntityRecord vendorRecord)
|
||||
|| vendorRecord.Snapshot.Position is not { } vendorPosition)
|
||||
{
|
||||
vendor.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
float useRadius = vendorRecord.Snapshot.UseRadius ?? DefaultUseRadius;
|
||||
// Fix 4: retail passes the vendor's raw authored UseRadius with NO
|
||||
// client-side fallback (see the class doc's ACE citation correction)
|
||||
// — an absent/zero radius closes on the very first nonzero-distance
|
||||
// check, since ObjectRangeMath.ObjectsInRange with range=0 requires
|
||||
// an EXACT position match. That is retail's own behavior for a
|
||||
// radius-0 (or unauthored) handler, not a bug to paper over.
|
||||
float useRadius = vendorRecord.Snapshot.UseRadius ?? 0f;
|
||||
bool inRange = ObjectRangeMath.ObjectsInRange(
|
||||
AbsolutePosition(playerPosition),
|
||||
0f,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue