fix(vendor): evidence-based pass — max-first stack ceiling; the local player resolves never-animated MoveTo targets
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

Both chains pinned by the live [vendor-diag] run (vendor-diag.log)
after three code-reading rounds each failed:

The split bar: ACE serializes descStackSize=1 for EVERY browse row
(live wire, log 343-348) — the R1-era "ACE never populates desc"
claim is retracted with the line quoted. Retail's vendor sites read
pwd._maxStackSize directly (four sites, incl. UpdateItemsList
@0x004c1ea0 stamping min(remaining, _maxStackSize));
ResolveAuthoredStackSize flips to max-first for its vendor-only
consumers. Taper ceiling 1000, scarab 100, seed 1 for exempt.
Pricing still reads the desc (per-1 values on ACE).

Walk-to-use: the local player's getObjectA seam was bound to
TryGetPhysicsHost, which resolves only INSTALLED physics hosts — a
never-animated vendor has none, so TargetManager.SetTarget got null,
the MoveToObject armed with zero nodes, and UseTime never dispatched.
The log's natural=False completions were the user's own movement keys
(retail-correct input-edge cancels); attempt 4 worked because the
greeting animation had installed a host. RuntimePhysicsState gains
the retail CObjectMaint::GetObjectA seam (bound canonical resolver
with installed-host fallback); the graphical host binds the SAME
lazy-minimal-host resolver every remote already uses — whose own doc
comment names this exact never-animated hazard. The reservation
release was already correct (2b premise refuted with evidence); the
production-wiring invariants are now pinned by four new tests
including the pre-fix pathology as a permanent sabotage control.

AP-169 rewritten a second time, honestly. The [vendor-diag] probe
family (ACDREAM_DUMP_VENDOR) lands env-gated for future live triage.

Clean-room complete solution: 11,536 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 17:17:04 +02:00
parent d003449bb4
commit 02b735ba4a
21 changed files with 1139 additions and 102 deletions

View file

@ -2442,6 +2442,105 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
return result.ToImmutable();
}
/// <summary>
/// 2026-08-08 vendor-approach root fix (the armed-but-inert far Use):
/// the published local player's host seam <c>getObjectA</c> is retail's
/// <c>CObjectMaint::GetObjectA</c> — it must resolve ANY in-world
/// object so <c>TargetManager.SetTarget</c>'s <c>add_voyeur</c> can
/// deliver the immediate initial target snapshot a deferred
/// <c>MoveToObject</c> needs before it queues a single node. The old
/// binding went straight to
/// <c>RuntimePhysicsState.TryGetPhysicsHost</c> (installed hosts only),
/// so a moveto against a never-animated NPC/static target — no
/// remote-motion binding, no installed host — armed but never
/// initialized: no nodes, no movement, no natural completion, until
/// user input or the 10 s staleness timeout cancelled it (the live
/// vendor-diag evidence: three consecutive approaches at 7.37/6.01/
/// 5.49 m sat inert; the fourth worked only because the vendor had
/// animated by then and gained a host).
///
/// Phase 1 pins the unbound fallback (exact installed hosts only — the
/// no-window shape): the moveto arms and stays UNinitialized with an
/// empty node plan. Phase 2 binds the canonical object-table resolver
/// (the graphical host's <c>ResolvePhysicsHost</c> stand-in, installed
/// by <c>SessionPlayerComposition</c> through
/// <c>RuntimePhysicsState.BindObjectTableHostResolver</c>) and proves
/// the SAME published moveto now receives the AddVoyeur snapshot
/// synchronously and builds its node plan. Sabotage-verified: with the
/// pre-fix <c>getObjectA: _physics.TryGetPhysicsHost</c> binding,
/// Phase 2 fails (the bound resolver is never consulted).
/// </summary>
[Fact]
public void PublishedLocalPlayerMoveToResolvesUninstalledTargetsThroughTheBoundObjectTableResolver()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.NotNull(fixture.Record.PhysicsHost);
MovementManager movement = fixture.Movement.Controller!.Movement;
MoveToManager moveTo = movement.MoveTo!;
const uint vendorGuid = 0x7C95B01Cu;
Vector3 vendorPos =
fixture.Record.PhysicsBody!.Position + new Vector3(10f, 0f, 0f);
MovementStruct Approach() => new()
{
ObjectId = vendorGuid,
TopLevelId = vendorGuid,
Pos = new AcDream.Core.Physics.Position(
Cell, vendorPos, Quaternion.Identity),
Params = new MovementParameters
{
DistanceToObject = 3f,
CanCharge = true,
},
Type = MovementType.MoveToObject,
Radius = 0.5f,
Height = 2f,
};
// Phase 1 — no resolver bound (the no-window fallback): the target
// has no installed host, so the deferred object move arms but never
// receives its first target update — exactly the pre-fix pathology.
Assert.Equal(WeenieError.None, movement.PerformMovement(Approach()));
Assert.True(moveTo.IsMovingTo());
Assert.False(moveTo.Initialized);
Assert.Empty(moveTo.PendingActions);
moveTo.CancelMoveTo(WeenieError.ActionCancelled);
// Phase 2 — the graphical host's bind: the SAME published moveto
// resolves the never-animated target through the object-table
// resolver, receives AddVoyeur's immediate Ok snapshot
// synchronously, and builds its node plan.
var vendorHost = new EntityPhysicsHost(
vendorGuid,
getPosition: () => new AcDream.Core.Physics.Position(
Cell, vendorPos, Quaternion.Identity),
getVelocity: static () => Vector3.Zero,
getRadius: static () => 0.5f,
inContact: static () => true,
minterpMaxSpeed: static () => null,
curTime: static () => 0d,
physicsTimerTime: static () => 0d,
getObjectA: static _ => null,
handleUpdateTarget: static _ => { },
interruptCurrentMovement: static () => { });
fixture.Lifetime.Physics.BindObjectTableHostResolver(
guid => guid == vendorGuid ? vendorHost : null);
Assert.Equal(WeenieError.None, movement.PerformMovement(Approach()));
Assert.True(moveTo.IsMovingTo());
Assert.True(moveTo.Initialized);
Assert.NotEmpty(moveTo.PendingActions);
moveTo.CancelMoveTo(WeenieError.ActionCancelled);
}
private readonly record struct EvaluationPuritySnapshot(
RuntimePhysicsOwnershipSnapshot PhysicsOwnership,
RuntimeSetPositionOwnershipSnapshot SetPositionOwnership,