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

@ -52,6 +52,77 @@ public sealed class RuntimePhysicsStateTests
Assert.True(physics.CaptureOwnership().IsDisposed);
}
/// <summary>
/// 2026-08-08 vendor-approach root fix: <c>ResolveObjectTableHost</c> is
/// the retail <c>CObjectMaint::GetObjectA</c> seam consumed by the local
/// player's publication-chain host. Unbound (no-window hosts) it answers
/// exact installed hosts only; bound (the graphical host's canonical
/// lazy resolver, installed by <c>SessionPlayerComposition</c>) it
/// delegates every lookup — including entities that have no installed
/// host yet, the case whose null answer left a far-use MoveToObject
/// armed but inert. Dispose clears the binding so a retired session
/// route cannot leak its App closure.
/// </summary>
[Fact]
public void ResolveObjectTableHostDelegatesToTheBoundResolverAndFallsBackToInstalledHosts()
{
var lifetime = new RuntimeEntityObjectLifetime();
RuntimeEntityRecord record =
lifetime.Entities.AddActive(Spawn(0x70000001u, 1));
EntityPhysicsHost installed = MinimalHost(record.ServerGuid);
lifetime.Physics.InstallPhysicsHost(record, installed);
const uint uninstalledGuid = 0x7C95B01Cu;
// Unbound fallback: installed hosts resolve, anything else is null.
Assert.Same(
installed,
lifetime.Physics.ResolveObjectTableHost(record.ServerGuid));
Assert.Null(lifetime.Physics.ResolveObjectTableHost(uninstalledGuid));
// Bound: the canonical resolver owns EVERY lookup.
EntityPhysicsHost lazyMinimal = MinimalHost(uninstalledGuid);
var resolvedGuids = new List<uint>();
lifetime.Physics.BindObjectTableHostResolver(guid =>
{
resolvedGuids.Add(guid);
return guid == uninstalledGuid ? lazyMinimal : null;
});
Assert.Same(
lazyMinimal,
lifetime.Physics.ResolveObjectTableHost(uninstalledGuid));
Assert.Null(lifetime.Physics.ResolveObjectTableHost(0x70000002u));
Assert.Equal(
new[] { uninstalledGuid, 0x70000002u },
resolvedGuids);
// Rebinding replaces (last bind wins); null clears back to the
// exact-installed-host fallback.
lifetime.Physics.BindObjectTableHostResolver(null);
Assert.Null(lifetime.Physics.ResolveObjectTableHost(uninstalledGuid));
Assert.Same(
installed,
lifetime.Physics.ResolveObjectTableHost(record.ServerGuid));
lifetime.Physics.BindObjectTableHostResolver(_ => lazyMinimal);
lifetime.Dispose();
Assert.Throws<ObjectDisposedException>(
() => lifetime.Physics.ResolveObjectTableHost(uninstalledGuid));
}
private static EntityPhysicsHost MinimalHost(uint guid) => new(
guid,
getPosition: static () => new AcDream.Core.Physics.Position(
0u, Vector3.Zero, Quaternion.Identity),
getVelocity: static () => Vector3.Zero,
getRadius: static () => 0f,
inContact: static () => true,
minterpMaxSpeed: static () => null,
curTime: static () => 0d,
physicsTimerTime: static () => 0d,
getObjectA: static _ => null,
handleUpdateTarget: static _ => { },
interruptCurrentMovement: static () => { });
[Fact]
public void CanonicalRecordAndPhysicsOwnerOwnRemoteComponentAndWorksets()
{