C4 route 4b-3 collapse (docs/research/2026-08-04-onposition-collapse-contract.md). Behaviour-preserving: the ~640-line duplicated player-guid and NPC-guid copies of the remote routing tail in LiveEntityNetworkUpdateController.OnPosition become one guid-blind tail, reached by every remote guid through the single ApplyRemoteContactRouting/RunRemoteArmTail seam. Two guid-conditionals survive, both named and justified: - Row 8 (TS-44 sticky suppression, creature-only): retail's sticky is independent of this acdream-only steady-state gate; the register row already describes it as NPC-only and this collapse does not widen it. - The AirborneSnap arm's interp-clear + shadow-publish (rows 2a/2b, player-only preserve): unifying either way would be an unauthorized behaviour change. #316 (shadow publish) is a real, unmeasured pre-existing defect, deliberately preserved not fixed. The interp-clear's equivalence could not be proven for the steep-non-walkable-landing edge case (AdjustOffset's CONTACT-keyed gate vs. AP-139's WALKABLE-keyed per-tick clear) — preserved per contract stop condition 2 rather than shipped on an incomplete proof. Category-(c) resolutions (contract §2.1-2.5), each with its evidence: - Row 2a (interp clear): PRESERVED — AdjustOffset's `if (!inContact) return` proves inertness on flat landings, but not on the steep-contact edge case. - Row 2b (shadow publish / #316): PRESERVED — no design note ever sanctioned the player-guid skip; the file's own #184 Slice 2b comments contradict it. - Row 2c (EnsureRemoteMotionBindings): UNIFIED — the method is idempotent (`if (rm.Host is not null) return rm.Sink;`), so "always ensure" is safe. - Row 3 (wire-cell adopt ordering): UNIFIED — RebucketLiveEntity already commits the wire cell before either guid branch runs, so the deleted player-guid pre-write was a proven no-op. - Row 4 (LastServerPos/Time sample timing): UNIFIED — on a genuine first UP, InterpolationManager.Enqueue's already-close branch and the Snapped branch both converge on the same body pose/orientation for a zero-distance target. - Row 12 (wall-clock capture): UNIFIED — one shared `nowSec`, a microsecond-scale skew in acdream-only bookkeeping/diagnostics. Sabotage check (contract §5, performed and reverted, not committed): deleting the one remaining TryArmConstraintAfterOperation call failed 10/16 dual-guid matrix tests, spanning BOTH guid halves of every arming-dependent scenario (teleport, landing, near, far, sticky) — proof the matrix discriminates a defect regardless of which guid range exercises it, closing the class of bug that let 4b-3's A1/A2/R3 findings survive review when only one copy's tests were green. New tests/AcDream.App.Tests/Physics/LiveEntityNetworkOnPositionCollapseMatrixTests.cs drives 8 scenarios x 2 guid ranges (0x50xxxxxx player, 0x8xxxxxxx creature) through the complete production OnPosition entry point. Doc comments on ApplyRemoteContactRouting, RunRemoteArmTail, ApplyWireAirborneLeftoverBookkeeping, TryAdoptWireCellAfterRouting, and the AirborneNoOperation throw guard updated to describe the collapsed one-path world (the "two callers stay one decision" claim was true before this commit and false after — fixed in the same commit that makes it false). One branch-routing source-text pin (LiveEntityNetworkBranchRoutingTests.cs) updated to follow the AP-140 CONTACT gate to its new address inside ApplyRemoteContactRouting. #316 stays OPEN, deliberately not fixed here — see its updated ISSUES.md entry. dotnet build AcDream.slnx -c Release: 0 errors. Verified independently bisectable at this exact commit: AcDream.App.Tests 4104/4107 (3 pre-existing skips), AcDream.Runtime.Tests 1125/1125. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
8.4 KiB
C#
195 lines
8.4 KiB
C#
using System.Text.RegularExpressions;
|
|
using AcDream.App.Physics;
|
|
|
|
namespace AcDream.App.Tests.Physics;
|
|
|
|
public sealed class LiveEntityNetworkBranchRoutingTests
|
|
{
|
|
[Fact]
|
|
public void Vector_ProjectileStopsCanonicalAndOrdinaryRoutes()
|
|
{
|
|
var calls = new List<string>();
|
|
|
|
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
|
|
() => { calls.Add("projectile"); return true; },
|
|
() => { calls.Add("canonical"); return true; },
|
|
() => calls.Add("ordinary"));
|
|
|
|
Assert.Equal(LiveEntityVectorRoute.Projectile, route);
|
|
Assert.Equal(["projectile"], calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Vector_CanonicalBodyRunsOnlyAfterProjectileDeclines()
|
|
{
|
|
var calls = new List<string>();
|
|
|
|
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
|
|
() => { calls.Add("projectile"); return false; },
|
|
() => { calls.Add("canonical"); return true; },
|
|
() => calls.Add("ordinary"));
|
|
|
|
Assert.Equal(LiveEntityVectorRoute.CanonicalBody, route);
|
|
Assert.Equal(["projectile", "canonical"], calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Vector_OrdinaryRemoteIsTheLastFallback()
|
|
{
|
|
var calls = new List<string>();
|
|
|
|
LiveEntityVectorRoute route = LiveEntityVectorRouter.Route(
|
|
() => { calls.Add("projectile"); return false; },
|
|
() => { calls.Add("canonical"); return false; },
|
|
() => calls.Add("ordinary"));
|
|
|
|
Assert.Equal(LiveEntityVectorRoute.OrdinaryRemote, route);
|
|
Assert.Equal(["projectile", "canonical", "ordinary"], calls);
|
|
}
|
|
|
|
// C4 route 2 (2026-08-03): LocalForcePositionTransaction and its
|
|
// ForcePosition_* coverage here are RETIRED, not adapted — the class is
|
|
// deleted outright (docs/research/2026-08-03-c4-route-2-contract.md
|
|
// §"The contract" item 2). Its three jobs (ownership validation, the
|
|
// blip/commit, and the exactly-once ack including the displaced-
|
|
// authority case its trailing isCurrent() covered) are now properties of
|
|
// the Runtime-owned RuntimeAcceptedPositionDriveController and are tested
|
|
// there: tests/AcDream.Runtime.Tests/Session/RuntimeAcceptedPositionDriveControllerTests.cs.
|
|
|
|
/// <summary>
|
|
/// R8 review fix (2026-08-03): source pins for the generic-tail
|
|
/// double-write guard in <c>LiveEntityNetworkUpdateController.OnPosition</c>.
|
|
/// A full behavioral fixture is impractical here for the SAME reason
|
|
/// <c>C3cF1ProductionWiringTests</c> gives — the controller's dependency
|
|
/// set is composition-only (67+ collaborators wired only by
|
|
/// <c>SessionPlayerComposition</c>) — so this follows that file's exact
|
|
/// established pattern: assert the STRUCTURE of the production source
|
|
/// rather than construct the class. The Runtime-level behavioral
|
|
/// coverage for the seam itself lives in
|
|
/// RuntimeAcceptedPositionDriveControllerTests; these pins are what stop
|
|
/// this App-layer call site from silently reintroducing the retired
|
|
/// duplicate-write authority (the deleted LocalForcePositionTransaction
|
|
/// pair + the generic render-tail writing the SAME accepted Position a
|
|
/// second time — 670f307c's divergence class).
|
|
/// </summary>
|
|
public sealed class LiveEntityNetworkUpdateControllerForcePositionWiringTests
|
|
{
|
|
[Fact]
|
|
public void LocalForcePositionTransactionIsNeverCalledFromThisFile()
|
|
{
|
|
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
|
|
// The name may still appear in a comment explaining what
|
|
// replaced it (contract §"the deleted LocalForcePositionTransaction");
|
|
// what must be gone is any actual call into it.
|
|
Assert.DoesNotContain(
|
|
"LocalForcePositionTransaction.Apply(",
|
|
source,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenericTailWriteIsNeverDuplicatedForTheLocalForcePositionPath()
|
|
{
|
|
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
|
|
// The generic render-tail's WorldEntity write is the ONE
|
|
// remaining writer of an accepted Position — it must serve
|
|
// remotes only, never a second local-player write alongside the
|
|
// Runtime-committed one.
|
|
Assert.Single(
|
|
Regex.Matches(source, @"entity\.SetPosition\(worldPos\);")
|
|
.Cast<Match>());
|
|
}
|
|
|
|
[Fact]
|
|
public void CommittedOrDeferredCellReturnsBeforeReachingTheGenericTail()
|
|
{
|
|
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
|
|
// The Committed/DeferredCell branch must still return
|
|
// immediately after its two preserved side effects — a missing
|
|
// `return` here would fall through into the generic tail below
|
|
// and resurrect the double-write.
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"ObserveAcceptedLocalPosition\(\s*"
|
|
+ @"update\.Position\.LandblockId\);\s*return;",
|
|
RegexOptions.Singleline),
|
|
source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// AP-140 (retired 2026-08-04): the accepted-Position routing gate
|
|
/// for the free-flight/landing decision must select the hard snap on
|
|
/// retail's CONTACT predicate
|
|
/// (<c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its
|
|
/// whole body on <c>transient_state & 1</c> @0x00555D52, and bit 0
|
|
/// is <c>CONTACT_TS</c>), not on the client <c>Airborne</c> flag,
|
|
/// which is <c>!Body.OnWalkable</c> — WALKABILITY, a strictly wider
|
|
/// set that also captures a remote sliding on a steep face.
|
|
///
|
|
/// <para>
|
|
/// C4 route 4b-3's OnPosition collapse (2026-08-04) dissolved
|
|
/// <c>OnPosition</c>'s former standalone player-remote LANDING
|
|
/// TRANSITION block — which used to carry its own
|
|
/// <c>if (!rmState.Body.InContact)</c> copy of this gate — into
|
|
/// <c>ApplyRemoteContactRouting</c>'s free-flight carve-out, now the
|
|
/// ONE site (for every guid) that decides this. The gate itself did
|
|
/// not move in spirit, only in address: this pin follows it there.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// A source pin rather than a behavioural fixture for the reason this
|
|
/// class already documents: the controller's dependency set is
|
|
/// composition-only. The gate inside <c>ApplyRemoteContactRouting</c>
|
|
/// — a static method, so reachable — IS covered behaviourally, in
|
|
/// <c>LiveEntityNetworkRemoteSteadyStateIntegrationTests</c>. Restore
|
|
/// <c>if (rmState.Airborne)</c> or <c>if (remote.Airborne)</c> here
|
|
/// and this test fails.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void PlayerRemoteLandingSnapSelectsOnContactNotOnWalkability()
|
|
{
|
|
string source = ReadSource("LiveEntityNetworkUpdateController.cs");
|
|
|
|
Assert.Contains(
|
|
"if (!remote.Body.InContact)",
|
|
source,
|
|
StringComparison.Ordinal);
|
|
// `rmState.Airborne` survives as a WRITE target and in prose (the
|
|
// block's own comment explains why it is deliberately not cleared
|
|
// there); what must never come back is reading it as the gate.
|
|
Assert.DoesNotContain(
|
|
"if (rmState.Airborne)",
|
|
source,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"if (remote.Airborne)",
|
|
source,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
private static string ReadSource(string fileName)
|
|
{
|
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
|
while (directory is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
{
|
|
return File.ReadAllText(Path.Combine(
|
|
directory.FullName,
|
|
"src",
|
|
"AcDream.App",
|
|
"Physics",
|
|
fileName));
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
}
|
|
}
|
|
}
|