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
|
|
@ -49,6 +49,45 @@ namespace AcDream.Core.Items;
|
|||
/// </summary>
|
||||
public static class VendorPricing
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>VendorProfile::VendorSellPrice</c>/<c>VendorBuyPrice</c>'s shared
|
||||
/// per-unit division (<c>0x005D1B00</c>/<c>0x005D1B70</c>, both bodies
|
||||
/// identical apart from which rate/branch they go on to feed
|
||||
/// <see cref="SellPrice"/>/<see cref="BuyPrice"/> — read directly from
|
||||
/// the decompiled body, <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:484801-484813</c>).
|
||||
/// The wire item's <c>Value</c> field is the STACK's TOTAL value, not a
|
||||
/// per-unit price — retail divides it by the item's own authored
|
||||
/// <c>PublicWeenieDesc::_stackSize</c> (<paramref name="descStackSize"/>
|
||||
/// — NOT <see cref="VendorShopItem.StackSize"/>, which is
|
||||
/// <c>ItemProfile</c>'s separately-packed SUPPLY count, a different wire
|
||||
/// field entirely) before either price formula ever sees it. Slice 5.3
|
||||
/// review fix 2.
|
||||
/// </summary>
|
||||
/// <param name="stackTotalValue">
|
||||
/// The item's raw wire <c>Value</c> (retail <c>_value</c> — the whole
|
||||
/// stack's total value, not one unit's).
|
||||
/// </param>
|
||||
/// <param name="descStackSize">
|
||||
/// The item's own <c>PublicWeenieDesc::_stackSize</c> (retail
|
||||
/// <c>_stackSize</c>). <see langword="null"/> (the field absent on the
|
||||
/// wire) is treated exactly like retail's zeroed-struct default when the
|
||||
/// field was never sent — same as an explicit 0 or negative: no
|
||||
/// division.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <paramref name="stackTotalValue"/> unchanged when
|
||||
/// <paramref name="descStackSize"/> is absent or <c><= 0</c> (retail
|
||||
/// <c>pc:484808-484810</c>: <c>if (_stackSize <= 0) return
|
||||
/// SellPrice(_value, ...)</c>); otherwise the INTEGER-divided per-unit
|
||||
/// value (retail <c>pc:484812</c>: <c>COMBINE(0, _value) / _stackSize</c>
|
||||
/// — a plain non-negative integer divide, .NET's <c>int</c> division
|
||||
/// truncates toward zero the same way).
|
||||
/// </returns>
|
||||
public static int PerUnitValue(int stackTotalValue, int? descStackSize) =>
|
||||
descStackSize is { } size && size > 0
|
||||
? stackTotalValue / size
|
||||
: stackTotalValue;
|
||||
|
||||
/// <summary>
|
||||
/// <c>ShopSystem::BuyPrice</c> (<c>0x006B6120</c>): the price the vendor
|
||||
/// PAYS the player for <paramref name="quantity"/> units of an item
|
||||
|
|
|
|||
|
|
@ -44,7 +44,20 @@ public readonly record struct VendorShopItem(
|
|||
string? Name,
|
||||
uint? ItemType,
|
||||
uint IconId,
|
||||
int? Value);
|
||||
int? Value,
|
||||
// Slice 5.3 review fix 2: the ITEM'S OWN authored stack depth (retail
|
||||
// PublicWeenieDesc::_stackSize, wire AcDream.Core.Net.Messages.
|
||||
// PublicWeenieDescBody.StackSize) -- the divisor VendorPricing.PerUnitValue
|
||||
// needs to turn Value's STACK-TOTAL wire number into a per-unit display
|
||||
// price (VendorProfile::VendorSellPrice/VendorBuyPrice, 0x005D1B00/
|
||||
// 0x005D1B70). This is a DIFFERENT wire field from StackSize above:
|
||||
// that one is ItemProfile's packed SUPPLY count (how many the vendor has
|
||||
// in stock), this one is how many units make up one priced stack (e.g.
|
||||
// 50 for a stack of arrows). Nullable because the wire field is
|
||||
// conditionally present (weenieFlags-gated) -- absent maps to null here,
|
||||
// matching retail's own zeroed-struct default of 0 for the same case
|
||||
// (see VendorPricing.PerUnitValue's <= 0 guard).
|
||||
int? DescStackSize = null);
|
||||
|
||||
public enum VendorStateTransitionKind
|
||||
{
|
||||
|
|
@ -134,6 +147,31 @@ public sealed class VendorState
|
|||
/// different-vendor open superseding this one; see research doc §A.3).
|
||||
/// Returns <c>false</c> if no vendor was open.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Slice 5.3 review fix 3.</b> Unlike <see cref="Reset"/>, a failing
|
||||
/// <see cref="Changed"/> observer here is never rethrown. <c>Close()</c>'s
|
||||
/// production caller (<c>RuntimeVendorRangeQuery.EnforceRange</c>) runs
|
||||
/// inside the per-frame post-network-command-phase callback
|
||||
/// (<c>GameRuntime.CreateLocalPlayerFrameController</c>'s post-network
|
||||
/// phase) with no try/catch anywhere up the frame-loop chain — an
|
||||
/// <see cref="AggregateException"/> propagating out of here, <see cref="Reset"/>'s
|
||||
/// shape, would kill the frame. <see cref="Reset"/> keeps that
|
||||
/// collect-and-rethrow shape because ITS callers (session
|
||||
/// reset/portal-out/logout — a rare, explicit teardown boundary) already
|
||||
/// tolerate/handle it (e.g. <c>RuntimeInventoryState.Dispose</c>'s own
|
||||
/// <c>Try(...)</c> wrapper collects <see cref="Reset"/>'s failures
|
||||
/// alongside every other child's). This still fans out to every listener
|
||||
/// via <c>GetInvocationList()</c> (one broken observer must not starve
|
||||
/// another — same resilience as <see cref="Reset"/>), but LOGS each
|
||||
/// failure instead of collecting it into an exception, matching
|
||||
/// <c>GameEventDispatcher.Dispatch</c>'s own boundary contract
|
||||
/// (<c>src/AcDream.Core.Net/Messages/GameEventDispatcher.cs:95-117</c> —
|
||||
/// catch, <c>Console.Error.WriteLine</c>, never rethrow, "the decode
|
||||
/// thread must survive handler failures"): a per-frame boundary must
|
||||
/// survive its own observers' failures the same way. Not silent
|
||||
/// swallowing — the failure surfaces on <see cref="Console.Error"/>
|
||||
/// exactly the way the dispatcher's do.
|
||||
/// </remarks>
|
||||
public bool Close()
|
||||
{
|
||||
if (VendorId == 0u) return false;
|
||||
|
|
@ -141,7 +179,20 @@ public sealed class VendorState
|
|||
uint previous = VendorId;
|
||||
ClearFields();
|
||||
|
||||
Changed?.Invoke(new VendorTransition(VendorStateTransitionKind.Closed, previous, 0u));
|
||||
var transition = new VendorTransition(VendorStateTransitionKind.Closed, previous, 0u);
|
||||
Action<VendorTransition>? listeners = Changed;
|
||||
if (listeners is not null)
|
||||
{
|
||||
foreach (Action<VendorTransition> listener in listeners.GetInvocationList())
|
||||
{
|
||||
try { listener(transition); }
|
||||
catch (Exception error)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[VendorState] Close() observer threw: {error.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue