A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class 670f307c fixed on
the remote path. The outbound AutonomousPosition ack also fired BEFORE any
canonical commit existed: we told ACE "got it, I'm here" before deciding where
"here" was, and the trailing isCurrent() could only suppress the continuation,
never recall the packet.
RuntimeAcceptedPositionDriveController is now the one Runtime-owned seam. Both
hosts call the identical TryExecuteAcceptedLocalPosition; App and headless
project the committed result through the existing placement projection sink
(LiveEntityRuntime.TryApplyRuntimePlacementPlace already performed the same
four writes, from committed state rather than a wire guess).
Retail: SmartBox::HandleReceivedPosition @0x00453FD0's FORCE_POSITION branch is
get_heading -> Frame::set_heading -> SmartBox::BlipPlayer @0x00453940 -> stamp
POSITION_TS -> SendPositionEvent @0x00454091 -> return @0x0045409D. BlipPlayer
is CPhysicsObj::SetPositionSimple @0x005162B0 with flags 0x1012
(Teleport|Slide|SendPositionEvent) — a real collision-resolving SetPosition,
not a snap. The pinned classifier already encoded this exactly.
Named behaviour changes:
* The ack is now an OUTPUT of the committed route, fired strictly after the
canonical commit and exactly once per accepted force packet.
* The ForcePosition route no longer re-arms the constraint leash. The force
branch returns at 0x0045409D, ahead of all three ConstrainTo sites
(0x00454272, 0x0045418A, 0x004541EC); the old re-arm cited retail's "Player,
normal" branch, which BlipPlayer is not on. The teleport, CommitPreparedPosition
and first-entry callers legitimately still constrain and are untouched.
* A force correction that terminates WITHOUT committing still sends its
position event and is not retried — retail's BlipPlayer discards
SetPositionSimple's SetPositionError return and acks unconditionally.
A single _pending funnel owns the in-flight placement, deciding on the token's
PositionAuthorityVersion against the record's: equal -> clear; advanced with the
newest accepted event still a force -> re-issue, re-classified; advanced to an
ordinary Apply -> clear, since newer server truth owns that pose. This closes a
double-apply/double-ack and a silently-dropped correction that two earlier
iterations of this slice each introduced.
AD-62 records the residual: a ForcePosition our async collision publication
cannot carry to a committed placement is not re-applied. Retail has no park —
its world is fully resident and its placement synchronous — so the state is
unreachable there. AP-131 is NOT retired; its legacy Position caller is route 4.
Deleted: LocalForcePositionTransaction, PlayerMovementController.BlipPosition,
HeadlessSessionWorldProjection.BlipLocalPlayer.
Gates: complete Release solution 10,858 passed / 4 skipped / 0 failed (baseline
10,844/4/0). Two independent Opus reviews (retail-conformance and
architecture/adversarial) PASS on the final diff after three FAIL rounds; every
intermediate state was fully green, so the suite caught none of the four real
defects. Connected acceptance is NOT run: nothing a user can do makes ACE emit
a ForcePosition without retail's @pklite, which acdream does not implement — see
docs/research/2026-08-03-c4-route-2-visual-gate.md.
Known gap, recorded not claimed: the plan's acceptance item 2 is unmet. The App
double-write check is a source pin, and "the committed projection moves the
render entity" is uncovered at any layer (#292). Filed alongside: #286-#291,
#293-#296.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
5.8 KiB
C#
143 lines
5.8 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);
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|
|
}
|