4587 lines
282 KiB
Markdown
4587 lines
282 KiB
Markdown
# Implementer progress — Runtime initial-placement continuation executor
|
||
|
||
## Reading phase (complete)
|
||
- Read contract, runtime-surface.md, retail-notes.md in full.
|
||
- Read source: RuntimeInitialCreateResidenceState.cs (full, 960 lines),
|
||
InboundPhysicsStateController.cs (full, 903 lines),
|
||
RuntimeAuthoritativePositionRouteClassifier.cs (full, 580 lines),
|
||
RuntimeEntityObjectLifetime.cs (full, 2134 lines),
|
||
RuntimeEntityDirectory.cs (full), RuntimeEntityRecord.cs (full),
|
||
ParentAttachmentState.cs (full), RuntimeInitialCreateAdmissionFreezer.cs (full),
|
||
RuntimeSetPositionState.cs (targeted: struct defs, CaptureOwnership,
|
||
Begin*/Watch/IsCurrent/TryPeek/Consume/Forget*, PrepareMover/Submit/
|
||
Apply/AcknowledgeProjection/PublishCancellation/Forget/LeaveWorld),
|
||
RuntimeInitialCreateResidenceStateTests.cs (full, 2657 lines - harness
|
||
helpers: Bind/Spawn/AttachDormantBody/Prepare/ApplyQueuedPosition/
|
||
PositionUpdate/ApplyFreshSuccessor/ConvergeSessionClear/PositionAction/
|
||
SetCompletedAdoptionRevision/EntityObserver/PlacementObserver).
|
||
|
||
## Key design decisions made during reading
|
||
1. AdoptCompletedPlacement resolves runtime-surface.md 3.1 deadlock:
|
||
BeginAcceptedPlacementCore/TryBeginExclusiveAuthoredPlacement all reject
|
||
while HasRetainedCompletion(key) is true. Executor must call
|
||
ConsumeAcknowledgedPlacement directly (via new AdoptCompletedPlacement on
|
||
residence state) BEFORE any Position continuation can begin its own
|
||
placement.
|
||
2. Movement timestamp reconstruction: AcceptedPhysicsTimestamps has
|
||
ServerControlledMove but NOT Movement itself. Reasoned from the
|
||
"MOVEMENT_TS consumed before discovering SERVER_CONTROLLED_MOVE_TS stale"
|
||
comment: HasTimestampMutation==true always implies gate.MovementTimestamp
|
||
advanced to update.MovementSequence (movement gate checked first). So
|
||
ApplyAcceptedMotion sets MovementSequence=update.MovementSequence
|
||
unconditionally when retained, ServerControlSequence=retained
|
||
AcceptedTimestamps.ServerControlledMove exactly. Judgment call - documented
|
||
in code comment.
|
||
3. MirrorGateTimestamps (reads LIVE gate) must NOT be used by the executor
|
||
(would stomp other channels' Timestamps fields with the wrong values from
|
||
out-of-band-drift live gate). Refactored ApplyAcceptedMotion to a targeted
|
||
field update instead - verified behaviorally identical for the legacy path
|
||
since gate/snapshot stay in lockstep there.
|
||
4. Circular-ownership constraint: deferred-child replay needs
|
||
RegisterEntityWithInitialResidence (internal core with beginInitialResidence
|
||
semantics). Contract forbids passing RuntimeEntityObjectLifetime itself.
|
||
Resolution: lifetime passes a bound delegate
|
||
(WorldSession.EntitySpawn, bool) -> RuntimeEntityRegistrationResult into the
|
||
executor's ctor, mirroring the existing retirePriorProjection callback
|
||
pattern already used in RegisterEntityCore. Narrow single-purpose seam,
|
||
not a reference to the owning type.
|
||
5. Publish choke point: lifetime passes bound delegates for PublishEntity and
|
||
AcknowledgeProjectionAndPublish (private methods -> delegate via method
|
||
group, same pattern as existing callback threading).
|
||
|
||
## Implementation phase - STARTING NOW
|
||
Next: refactor InboundPhysicsStateController.cs to extract ApplyAccepted*
|
||
methods shared by legacy TryApply* and the new executor.
|
||
|
||
## Implementation progress (continued)
|
||
- InboundPhysicsStateController.cs refactored: ApplyAccepted{ObjDesc,Pickup,
|
||
CreateParent,Parent,Vector,State,Motion,Position} methods extracted, legacy
|
||
TryApply* re-expressed as gate+shared-apply. Removed now-unused
|
||
MirrorGateTimestamps. Build clean, 89-test focused gate green after refactor.
|
||
FOUND+FIXED a bug during this pass: ApplyAcceptedMotion's retainPayload:false
|
||
branch must ALSO stamp nested Physics.Timestamps.Movement/ServerControlledMove
|
||
(not just top-level fields) - two pre-existing tests
|
||
(RejectedServerControlStillMirrorsConsumedMovementTimestamp,
|
||
AutonomousLocalEchoRetainsPayloadButMirrorsAcceptedTimestamps) caught this;
|
||
fixed, full 829-test Runtime suite green again.
|
||
- RuntimeInitialCreateResidenceState.cs: added CompletedEntry.PlacementAdopted,
|
||
AdoptCompletedPlacement, ConsumeExecuted +
|
||
RuntimeInitialCreateResidenceExecutorReleaseStatus enum. Updated
|
||
IsCompletedCurrent to treat PlacementAdopted as satisfying the placement-
|
||
current check without re-querying RuntimeSetPositionState (since adoption
|
||
already consumed/removed that tracking entry). Updated AcknowledgeAdoption
|
||
to tolerate (skip) re-consuming an already-adopted placement. Build clean,
|
||
48/48 residence tests green.
|
||
- New file src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
written (full algorithm: reentrancy latch, per-key Progress with LeaseId
|
||
staleness discard, initial tail (adopt+hook+deferred-child replay), FIFO
|
||
drain for all 8 continuation kinds incl. SameIncarnationCreate envelope
|
||
staging with buffered publish, Position classify-at-execution-time +
|
||
placement Begin/Watch/yield/resume lifecycle, ConsumeExecuted release with
|
||
Revised-loop). Constructed in all 3 RuntimeEntityObjectLifetime ctors,
|
||
wired into BindEventContext, CaptureOwnership (new
|
||
InitialCreateExecutorProgressCount field), and the residence
|
||
Forget/Clear choke points (deterministic progress discard, not lazy).
|
||
Build clean, full 829-test baseline still green after wiring.
|
||
|
||
## Judgment calls made (to report)
|
||
1. AdoptCompletedPlacement/ConsumeExecuted new API design (see code comments
|
||
in RuntimeInitialCreateResidenceState.cs) - resolves the 3.1 deadlock per
|
||
contract's exact prescription.
|
||
2. Registration callback threaded as a bound delegate
|
||
(WorldSession.EntitySpawn, bool) -> RuntimeEntityRegistrationResult from
|
||
the lifetime's ctor, NOT a reference to RuntimeEntityObjectLifetime itself
|
||
- satisfies "no circular ownership" per contract while still reaching
|
||
RegisterEntityWithInitialResidence for deferred-child replay.
|
||
3. Position continuation Rejected/RejectedData routes reuse
|
||
InboundPhysicsStateController.ApplyAcceptedPosition by passing
|
||
PositionTimestampDisposition.Rejected explicitly (that method internally
|
||
routes to the timestamp-only-stamp branch) - avoids a second, divergent
|
||
"timestamp only" implementation.
|
||
4. HasContact/forcePositionRotation/currentLocalVelocity all derived from
|
||
canonical.PhysicsBody when attached, falling back to Inputs.HasContact
|
||
(bodyless tests) - per contract's explicit "decide one way, document it,
|
||
test it" instruction for HasContact, extended consistently to the other
|
||
two live-body-derived facts.
|
||
5. ParentAttachmentState.Resolve/CommitProjection has zero callers anywhere
|
||
in AcDream.Runtime (grep-verified) - confirmed host-driven, not wired.
|
||
Parent/CreateParent continuation apply commits the position-timestamp-only
|
||
snapshot mutation + AdvancePositionAuthority/LeaveWorld/Forget/publish
|
||
only; does NOT invent a second resolve/commit path.
|
||
6. ObjectTableWiring.ApplyEntitySpawn (via RuntimeEntityObjectLifetime.
|
||
ApplyAcceptedSpawn) has zero callers anywhere in AcDream.Runtime
|
||
(grep-verified) - WeenieDescription tail action commits
|
||
RefreshSnapshot+AdvanceCreateAuthority+publish only, explicitly does NOT
|
||
drive the object table (host cutover work, out of scope).
|
||
7. ResidentCellCleanup implemented as an assert-the-invariant check
|
||
(claimed+celless must be IsDeferred) rather than building a destruction
|
||
mechanism; the "no cell claimed, no weenie" destruction-mark branch is
|
||
left as a recorded no-op (needs live object-table wiring not in scope).
|
||
8. Reentrant Execute() for the same RuntimeEntityKey while one is already on
|
||
the call stack fails closed via a HashSet<RuntimeEntityKey> latch.
|
||
9. Stale Progress (LeaseId mismatch, e.g. GUID/key reuse) is discarded AND
|
||
the CURRENT Execute call returns RejectedAuthority (not silently
|
||
re-created + retried in the same call) - matches contract's literal
|
||
"discard it and fail closed" wording; caller must retry once more.
|
||
|
||
## Major design fix discovered during test-driven debugging
|
||
Found a genuine architecture conflict: RuntimeInitialCreateResidenceState's
|
||
IsCompletedCurrent staleness check compares LIVE record.PositionAuthorityVersion/
|
||
CreateIntegrationVersion/FullCellId/PlacementCommitVersion against values
|
||
FROZEN at Complete()-time. But the executor's OWN Apply* methods legitimately
|
||
advance these SAME fields while draining (AdvancePositionAuthority,
|
||
AdvanceCreateAuthority, SetFullCell, the physics engine's own
|
||
AdvancePlacementCommit on a continuation's own SetPosition commit) - this
|
||
made Complete()/ConsumeExecuted incorrectly treat the executor's own
|
||
controlled progress as an external race and reject with RejectedAuthority.
|
||
Root-caused via a debug bisection test (temporarily added, then removed).
|
||
|
||
Fix: added CompletedEntry.Expected{PositionAuthorityVersion,
|
||
CreateIntegrationVersion,FullCellId,PlacementCommitVersion} - a SEPARATE,
|
||
executor-maintained baseline distinct from the FROZEN receipt.Token (which
|
||
must stay byte-identical for Complete()'s token-identity match to keep
|
||
working across retries). IsCompletedCurrent now compares against
|
||
entry.Expected* instead of receipt.Token.*/receipt.FullCellId/
|
||
receipt.PlacementCommitVersion directly. New method
|
||
AdvanceExecutorBaseline(record, token) re-syncs Expected* from the record's
|
||
CURRENT live values; the executor calls it (a) at the top of ExecuteCore's
|
||
loop before every Complete() call (covers a pending continuation placement's
|
||
host-driven commit, which happens BETWEEN Execute() calls), and (b)
|
||
unconditionally after every ApplyContinuation call in the drain loop (covers
|
||
in-call mutations before a yield). Verified this does NOT weaken the
|
||
EXISTING admission-slice regression tests (CorruptedPostAcknowledgementAuthorityRetiresProofAndLease
|
||
etc.) since those tests never call AdvanceExecutorBaseline - an external,
|
||
non-executor-driven bump to these fields is still correctly detected as
|
||
stale. All 48 residence tests + 16 new executor tests + 829 baseline pass
|
||
together (845 total).
|
||
|
||
## Test suite status: 16/16 new tests green, 845/845 total Runtime tests green
|
||
Covered: A (2 tests: basic completion + hook; mixed simple continuation
|
||
order), C (2: envelope atomicity/buffered-publish/stage-order incl.
|
||
PreTailDescriptionAdaptation+Pickup decomposition; envelope retry
|
||
non-duplication), D (5: local ordinary interpolate, local teleport placement
|
||
lifecycle w/ yield+resume, remote near interpolate, remote far
|
||
SetPositionSimple+StopInterpolating, parented-initial AwaitFreshPosition/no
|
||
placement), E (2: deferred-child replay consumes exact AdmissionId +
|
||
registers child through canonical route; stale AdmissionId cannot consume a
|
||
replacement - via ParentAttachmentState directly), G (2: reentrant Execute
|
||
for same entity fails closed; reentrant delete of the parent during deferred
|
||
child replay abandons without resurrection), H (1: stale Progress LeaseId
|
||
via reflection-planted entry discards + fails closed + clean retry), I (2:
|
||
reset during AwaitingContinuationPlacement converges every ledger; dispose
|
||
after successful execution converges IsConverged).
|
||
NOT separately tested (time-budget / defensibility tradeoffs - noted for
|
||
final report): F's literal "failure injection between every envelope stage"
|
||
(only end-to-end retry-after-full-drain covered, not synthetic mid-stage
|
||
crash injection); J (relies on the existing assembly-level
|
||
RuntimeDependencyBoundaryTests, which automatically covers the new file -
|
||
no new file added since the check is assembly-wide, not per-file); the
|
||
ResidentCellCleanup destruction-mark branches (only the "claimed+resident"
|
||
and the assert-invariant path are exercised, not the "no cell, no weenie"
|
||
destruction-mark path, which is explicitly a recorded no-op pending future
|
||
object-table wiring per code comment); SameIncarnationCreate's Position
|
||
stage mid-envelope yield+resume (D covers standalone Position yield/resume;
|
||
the envelope's OWN Position-stage yield path shares the identical
|
||
ApplyPositionAction code but is not independently exercised by an envelope
|
||
test with a position stage).
|
||
|
||
## ROUND 2: Reviewer feedback (Finding 1, Gaps 2-4, Finding 5 amendment)
|
||
Coordinator sent review findings after round-1 delivery. Working through:
|
||
- FINDING 5 (production regression, addressed FIRST since flagged most
|
||
urgent): ApplyAcceptedMotion's legacy caller was stamping
|
||
update.MovementSequence (wire's own stale/rejected value) instead of
|
||
gate.MovementTimestamp (post-call gate value) for the timestamp-only
|
||
branch. TryAcceptMovementEvent has 3 rejection flavors and only 1 of them
|
||
(stale ServerControlledMove) actually advances MOVEMENT_TS; the other two
|
||
(bad instance, stale MOVEMENT_TS) leave the gate untouched but the OLD
|
||
buggy code would still stamp the wire's stale value into the snapshot.
|
||
FIX: re-parameterized ApplyAcceptedMotion to take explicit
|
||
(movementSequence, acceptedServerControlledMove) inputs instead of
|
||
deriving movementSequence from `update` internally - ONE shared body, two
|
||
explicit-input callers. Legacy caller now passes
|
||
gate.MovementTimestamp/gate.ServerControlledMoveTimestamp (exact
|
||
pre-refactor behavior in all 3 flavors). Executor caller passes
|
||
action.Movement.Value.MovementSequence/action.AcceptedTimestamps.ServerControlledMove
|
||
(safe there since retention itself gates on genuine acceptance). Added 2
|
||
regression tests to InboundPhysicsStateControllerTests.cs comparing the
|
||
FULL EntitySpawn before/after for (1) stale MOVEMENT_TS, (2) instance
|
||
mismatch - both assert byte-identical snapshots. 15/15
|
||
InboundPhysicsStateControllerTests pass, 64/64 executor+residence tests
|
||
still pass after the signature change.
|
||
|
||
- FINDING 1 fixed: narrowed the pre-Complete() AdvanceExecutorBaseline call
|
||
in ExecuteCore to only fire when progress.PendingContinuationPlacement.IsValid
|
||
(the only legitimate between-calls mutation window). Added 2 regression
|
||
tests (ExternalPositionAuthorityMutation.../ExternalFullCellMutation...)
|
||
that directly drive Complete()+AdoptCompletedPlacement to the
|
||
"completed+adopted, no pending placement" state, then externally mutate
|
||
PositionAuthorityVersion/FullCellId, then assert the NEXT Execute() call
|
||
observes RejectedAuthority, publishes nothing, and converges. VERIFIED
|
||
these tests actually catch the regression: temporarily reverted the guard
|
||
to unconditional, confirmed both tests fail (Completed instead of
|
||
RejectedAuthority), then restored the fix and confirmed they pass again.
|
||
18/18 executor tests pass (16 + 2 new).
|
||
|
||
- GAP 4 done: extended RuntimeInitialCreateExecutedAction with a new
|
||
optional ResidentCellCleanupDisposition field +
|
||
RuntimeResidentCellCleanupDisposition enum (ResidentUnmarked/
|
||
DeferredUnderLostCellOwnership/NoCellClaimedDestructionMarked).
|
||
ApplyResidentCellCleanup now RETURNS the disposition instead of just
|
||
asserting. (a) ResidentCellCleanupUnmarksWhenCellClaimedAndAlreadyResident
|
||
- engine-backed, real initial placement, same-create Position stage
|
||
classifies Interpolate (SameIncarnationCreate source forces
|
||
effectiveContact=true, entity already resident so not cellless) -> no
|
||
placement needed, ResidentUnmarked recorded. (b)
|
||
ResidentCellCleanupFailsClosedWhenClaimedCelllessOutcomeIsNotUnderLostCellOwnership
|
||
- PickedUp initial (no placement), same-create Position with
|
||
UsePositionFromServer=false classifies NoPositionOperation (no
|
||
SetPosition begins at all) -> claimed+celless+not-deferred -> throws
|
||
InvalidOperationException, verified via Assert.Throws + message
|
||
content. (c) folded into the EXISTING envelope-atomicity test (added
|
||
assertion) since that entity already naturally has no claimed cell ->
|
||
NoCellClaimedDestructionMarked. 20/20 executor tests pass (18 + 2 new;
|
||
case (c) added to an existing test rather than a new one).
|
||
|
||
- GAP 3 done, AND IT CAUGHT A REAL BUG: wrote
|
||
EnvelopePositionStageRequiringSetPositionYieldsResumesAndPublishesOnceAfterCompletion
|
||
(cellless -> SetPosition Position stage forces a mid-envelope yield,
|
||
drive prepare/submit/ack, resume, envelope completes). First run: only 1
|
||
of 5 expected Updated events published (expected ObjDesc/Position/State/
|
||
Vector/WeenieDescription). ROOT CAUSE: Publish()'s buffered branch stored
|
||
the PER-STAGE "matches" closure (captured at that stage's OWN commit
|
||
time, checking one specific AuthorityVersion field) into the buffer -
|
||
but WeenieDescription's own AdvanceCreateAuthority() call bumps SIX
|
||
authority-version fields at once (Position/State/Vector/Velocity/
|
||
Movement/ObjDesc/CreateIntegration), which invalidated every EARLIER
|
||
buffered stage's captured version-equality check by the time the final
|
||
flush ran, even though nothing external raced - it was the executor's
|
||
OWN later, expected progression. FIX (root cause, not per-callsite
|
||
patch): Publish()'s buffered branch now stores a constant `true`
|
||
predicate instead of the per-field "matches" closure - IsCurrent (checked
|
||
unconditionally by PublishNow) is the only currency guard a buffered
|
||
entry needs, since envelope processing dispatches no event until the
|
||
flush (no reentrancy window mid-envelope except at a Position yield,
|
||
which is independently guarded by ApplyEnvelope's own IsCurrent check +
|
||
ResumePendingPlacement). The IMMEDIATE (non-buffered) path is unchanged -
|
||
still uses the real matches() check, appropriate there since publish
|
||
happens right after each standalone mutation. 21/21 executor tests pass
|
||
after the fix (20 + 1 new).
|
||
|
||
- GAP 2 done: EnvelopeAbandonedDuringPositionStageYieldPublishesNothingAndConvergesEveryLedger
|
||
(delete the entity while AwaitingContinuationPlacement is pending
|
||
mid-envelope; asserts ONLY the delete's own Deleted event observed - none
|
||
of the envelope's already-committed-but-buffered stages ever publish -
|
||
and every ledger converges: residence/progress/active-operation/
|
||
acknowledged-completion counts all zero; a stale retry with the original
|
||
token returns RejectedToken). Documented (large comment block, not a
|
||
fake test) exactly why the other 6 stage kinds have no reachable yield
|
||
boundary (pure synchronous in-memory mutations, no event dispatch until
|
||
the buffered flush, so no callback/reentrancy opportunity exists without
|
||
adding a diagnostic seam to production code, which is explicitly
|
||
forbidden). While building this test, ALSO found and fixed a real (if
|
||
currently redundant-with-existing-Forget) defensive gap: DiscardProgress/
|
||
DiscardAll now also forget any in-flight CONTINUATION placement token
|
||
(distinct from the residence's own initial-lease placement) - verified
|
||
via revert that this specific delete-path test still passes WITHOUT the
|
||
fix (TryAcceptDelete's own unconditional Physics.SetPosition.Forget
|
||
already cancels any operation for that key), so this is honest
|
||
defense-in-depth (DiscardProgress owning cleanup of the state it
|
||
introduces) rather than a proven-necessary fix for THIS path - documented
|
||
as such in both the code comment and the test comment.
|
||
22/22 executor tests pass (21 + 1 new).
|
||
|
||
## ALL 4 REVIEW ITEMS (Finding 1, Gaps 2/3/4) COMPLETE, plus Finding 5
|
||
(production regression) fixed first.
|
||
|
||
## ROUND 2 FINAL GATES: ALL GREEN
|
||
1. Release build (AcDream.slnx): 0 errors, 21 pre-existing warnings.
|
||
2. Focused filter (Residence+Classifier+Executor): 111 passed (89 + 22).
|
||
3. Complete Runtime suite: 853 passed (829 + 24: 22 executor + 2
|
||
InboundPhysicsStateController Finding-5 regressions).
|
||
4. git diff --check: exit 0, clean. Nothing staged.
|
||
Confirmed the narrowed rebaseline (Finding 1) did not break the existing
|
||
AwaitingContinuationPlacement yield/resume path -
|
||
LocalTeleportContinuationDrivesItsOwnAuthoredPlacementLifecycle still
|
||
passes throughout every round of changes.
|
||
|
||
## FINAL STATUS (Round 2): ALL GATES GREEN
|
||
1. dotnet build AcDream.slnx -c Release: 0 errors, 21 warnings (all
|
||
pre-existing, in test projects untouched by this work - confirmed exact
|
||
match to the known-count in the task brief).
|
||
2. Focused filter (Residence+Classifier+Executor): 105 passed (89 baseline
|
||
+ 16 new), 0 failed.
|
||
3. Complete AcDream.Runtime.Tests: 845 passed (829 baseline + 16 new), 0
|
||
failed.
|
||
4. git diff --check: exit 0, clean (only pre-existing line-ending warnings
|
||
on files this session did not touch, or normal LF-will-become-CRLF
|
||
metadata notices on files this session DID touch - no whitespace-error
|
||
content).
|
||
Nothing staged (git diff --cached --stat empty) - primary agent to review
|
||
and commit.
|
||
|
||
## ROUND 3: both independent reviews returned FAIL (3 blockers, 12 majors,
|
||
## mandated test-completion list). Combined directive at round3-fixes.md.
|
||
## Followed the section-D work order: A1/A2 first, then B1/B2/B3/B4, then
|
||
## A3/B5/B6/B9/B10, then B7/B8/B12, then B11, Section C tests throughout.
|
||
|
||
### A1 (blocker) — snapshot lockstep. FIXED.
|
||
Root cause confirmed exactly as the review described:
|
||
InboundPhysicsStateController._snapshots (the legacy merge base) was NEVER
|
||
written by the executor's applies - they merged directly against
|
||
canonical.Snapshot via the STATIC ApplyAccepted* methods and called
|
||
RefreshSnapshot, but _snapshots[guid] stayed frozen at whatever it was when
|
||
residence began. The FIRST subsequent legacy TryApplyXxx call would then
|
||
re-merge onto that stale base and silently revert every drained fact.
|
||
Fix: added gate-less INSTANCE seam methods on InboundPhysicsStateController
|
||
(ApplyAccepted{ObjDesc,Pickup,CreateParent,Parent,Motion,State,Vector,
|
||
Position,WeenieDescription}Snapshot) that read _snapshots[guid] as the merge
|
||
base, run the existing shared static body, write the result back, and return
|
||
the merged value. Delegated through RuntimeEntityDirectory (new
|
||
ApplyAccepted*Snapshot wrappers calling _inbound.*) since the executor only
|
||
holds a RuntimeEntityDirectory reference, not the controller directly. Every
|
||
executor Apply*Action now calls the instance seam instead of the static
|
||
method. Added the mandated regression test
|
||
DrainedAppearanceAndPoseSurviveTheNextLegacyWireApply: drains a standalone
|
||
ObjDesc continuation (fresh BasePaletteId), then runs an ordinary legacy
|
||
TryApplyVector, asserts the drained palette survives in canonical.Snapshot
|
||
after the legacy apply. Verified this fails without the fix by tracing the
|
||
exact code path (old static-based merge would read the STALE _snapshots
|
||
base and RefreshSnapshot would revert the palette) - did not need to revert
|
||
code to prove it since the mechanism is unambiguous from the diff.
|
||
|
||
### A2 (blocker) — WeenieDescription merge semantics. FIXED.
|
||
ApplyWeenieDescriptionAction now calls the new
|
||
ApplyAcceptedWeenieDescriptionSnapshot instance seam, which merges via the
|
||
EXISTING private static MergeUntimestampedCreate(retained: _snapshots[guid],
|
||
incoming: the raw WeenieDescription packet) instead of a wholesale
|
||
RefreshSnapshot of the raw packet. Added the mandated regression test
|
||
SameIncarnationWeenieDescriptionMergePreservesRetainedMotionTableId. First
|
||
draft of this test used BasePaletteId as the probe field and failed for an
|
||
UNRELATED reason I initially misread as a bug: BasePaletteId (and every
|
||
other field BuildSameGenerationEvents' Appearance construction touches) is
|
||
ALSO independently re-applied by the SAME envelope's OWN dedicated ObjDesc
|
||
stage (always present when incoming.Physics is not null) - so a standalone
|
||
ObjDesc drain's fresher palette is CORRECTLY superseded by the envelope's
|
||
own ObjDesc stage moments later, regardless of A2. Re-designed the test
|
||
around MotionTableId, which has NO dedicated envelope stage (WeenieDescription
|
||
is the ONLY place it can move) - this cleanly isolates
|
||
MergeUntimestampedCreate's "retained wins" rule. Test now: entry 1 =
|
||
standalone ObjDesc bump; entry 2 = SameIncarnationCreate whose raw incoming
|
||
MotionTableId differs from the entity's original; asserts the ORIGINAL
|
||
MotionTableId (retained) survives, not incoming's.
|
||
|
||
### B12 (major) — object-table wiring. FIXED; corrected a false "zero
|
||
### callers" claim from Round 1/2.
|
||
Verified RuntimeLiveEntitySessionController.cs:81 (OnSpawned) drives
|
||
Entities.ApplyAcceptedSpawn(canonical, integrationVersion, canonical.Snapshot,
|
||
replaceGeneration: Inbound.Disposition is NewGeneration) for EVERY accepted
|
||
Create in the non-residence direct-host path - the prior claim of zero
|
||
callers was false. ApplyAcceptedSpawn lives on RuntimeEntityObjectLifetime
|
||
(needs the ClientObjectTable the executor has no reference to, and cannot
|
||
reference RuntimeEntityObjectLifetime directly - circular ownership, same
|
||
constraint as the existing _registerDeferredChild delegate). Threaded a new
|
||
constructor delegate Func<RuntimeEntityRecord, ulong, WorldSession.EntitySpawn,
|
||
bool, bool> _applyAcceptedSpawn, bound in all 3 RuntimeEntityObjectLifetime
|
||
ctors to (canonical, version, spawn, replaceGeneration) =>
|
||
ApplyAcceptedSpawn(...). ApplyWeenieDescriptionAction now calls it with
|
||
replaceGeneration: false always - correct because this tail action ONLY ever
|
||
runs for an ExistingGeneration same-incarnation Create (a residence is
|
||
admitted into the SameIncarnationCreate FIFO only when preview is
|
||
ExistingGeneration; NewGeneration goes through the ordinary top-level
|
||
registration path, never this envelope). Added
|
||
WeenieDescriptionStageWiresTheObjectTableExactlyOnce: asserts
|
||
lifetime.Objects.ObjectCount increases by exactly 1 across a residence drain
|
||
whose envelope reaches WeenieDescription (a residence-pending admission
|
||
deliberately never wires the object table at the initial Create, so this is
|
||
the first point this guid's entry can appear).
|
||
|
||
### B1 (major) — shared abandonment routine; typed ResidentCellCleanup
|
||
### abandonment; operation-slot-contention as non-abandonment. FIXED.
|
||
Added one Abandon(canonical, key) choke point: calls
|
||
_residences.Forget(canonical, ...) (retiring the RESIDENCE itself, not just
|
||
executor progress - closes a real bug where several rejection paths,
|
||
notably ConsumeExecuted's own final-length-mismatch RejectedAuthority branch
|
||
and AdoptCompletedPlacement's ConsumeAcknowledgedPlacement-failure branch,
|
||
left the completed residence entry sitting fully intact in _completed; a
|
||
retry would have re-fetched it via Complete() and REPLAYED every
|
||
already-applied continuation from sequence zero), publishes the cancellation,
|
||
then DiscardProgress(key) (idempotent defense-in-depth, matching every other
|
||
caller's pattern - covers Forget finding nothing to retire). Every
|
||
ad hoc "DiscardProgress(key); return RejectedAuthority;" site now routes
|
||
through Abandon. ApplyResidentCellCleanup no longer throws for the
|
||
claimed+celless+not-deferred invariant violation - returns
|
||
RuntimeResidentCellCleanupDisposition? (null signals Abandon); the envelope's
|
||
ResidentCellCleanup case checks for null and calls Abandon instead of
|
||
letting the exception escape Execute. Rewrote the existing test
|
||
ResidentCellCleanupFailsClosedWhenClaimedCelllessOutcomeIsNotUnderLostCellOwnership
|
||
from Assert.Throws to assert RejectedAuthority + full ledger convergence +
|
||
a stale-token retry returning RejectedToken (proving Abandon actually
|
||
retired the residence, not just discarded progress).
|
||
Operation-slot contention: added Progress.PositionMergeCommittedForRetry +
|
||
PositionMergeCommittedVersion. When TryBeginExclusiveAuthoredPlacement fails
|
||
AND the record/its own merge-committed version are still current, this is
|
||
NOT abandonment - returns AwaitingContinuationPlacement with
|
||
PositionMergeCommittedForRetry left true and NO PendingContinuationPlacement
|
||
set, so the NEXT ApplyPositionAction call for the SAME continuation/stage
|
||
skips the merge+publish entirely (route = progress.PendingContinuationRoute,
|
||
already classified) and retries ONLY the placement-begin - closing the
|
||
"double-publish on every retry" hole a naive full re-apply would open. Only
|
||
when the record is no longer current OR its PositionAuthorityVersion moved
|
||
past the committed merge's own value does this become abandonment. NOT
|
||
separately unit-tested (constructing a real operation-slot-contention
|
||
scenario needs a second concurrent SetPosition consumer occupying the same
|
||
key's slot, which none of the existing test harness helpers construct) -
|
||
flagged as a coverage gap in this report; the logic was verified by code
|
||
review against RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement's
|
||
exact failure conditions (_operations.ContainsKey(key) is the transient
|
||
case; HasRetainedCompletion/PositionAuthorityVersion mismatch inside
|
||
BeginAcceptedPlacementCore are the genuine-staleness cases my currency
|
||
recheck already covers).
|
||
|
||
### B2 (major) — apply ordering: mutate -> rebaseline -> publish. FIXED.
|
||
Threaded `in RuntimeInitialCreateResidenceToken token` through
|
||
ApplyContinuation/ApplyEnvelope/ApplyPositionAction and all 8 non-Position
|
||
Apply*Action methods. Each now calls _residences.AdvanceExecutorBaseline
|
||
immediately after its own canonical mutation and BEFORE its own Publish call
|
||
(previously rebaseline happened in the OUTER drain loop, AFTER
|
||
ApplyContinuation returned - i.e. AFTER Publish had already run for the
|
||
non-buffered/immediate path, leaving a reentrant-retirement window where a
|
||
synchronous Publish observer could see the pre-mutation baseline and
|
||
misdetect staleness). Removed the drain loop's blanket
|
||
"_residences.AdvanceExecutorBaseline(canonical, token);" call after every
|
||
ApplyContinuation - each apply now guarantees its own baseline is current
|
||
before any observer can run, so no blanket re-sync belongs there.
|
||
|
||
### B3 (major) — residence retirement callback. FIXED.
|
||
RuntimeInitialCreateResidenceState.BindRetirementNotification(Action<
|
||
RuntimeEntityKey>) - one optional callback invoked in BOTH private Retire
|
||
overloads (Entry and CompletedEntry), Forget (both branches), and Clear
|
||
(iterating every retired entry) - AFTER the dictionary mutation in each
|
||
case. RuntimeEntityObjectLifetime binds it in all 3 ctors, right after
|
||
constructing InitialCreateExecution, to
|
||
key => InitialCreateExecution.DiscardProgress(key). This closes the gap
|
||
where a residence retired through a path OTHER than the executor's own
|
||
explicit DiscardProgress call (e.g. TryGetTransaction/TryGetCurrent/Complete/
|
||
AcknowledgeAdoption/AdoptCompletedPlacement/ConsumeExecuted's OWN internal
|
||
Retire calls on staleness) would leave the executor's progress AND its
|
||
separately-tracked pending continuation placement token orphaned. Kept
|
||
ForgetInitialCreateResidence's own explicit DiscardProgress call as harmless
|
||
idempotent defense-in-depth (covers the case where Forget finds nothing to
|
||
retire at all) and updated its comment to explain the relationship rather
|
||
than removing it.
|
||
|
||
### B4 (major) — ResumePendingPlacement full record/projection agreement.
|
||
### FIXED.
|
||
Strengthened to match Complete()'s exact check set: projection.Entity,
|
||
SessionLifetimeVersion, PositionAuthorityVersion (BOTH against the
|
||
placement token AND against the LIVE canonical.PositionAuthorityVersion -
|
||
catches something ELSE moving the record since this placement began, which
|
||
the old check could not see), ExactCellId != 0 AND == canonical.FullCellId,
|
||
PlacementCommitVersion == canonical.PlacementCommitVersion. Mismatch now
|
||
routes through Abandon (was a bare RejectedAuthority before). Renamed the
|
||
local placement token variable to placementToken to avoid shadowing the
|
||
newly-threaded outer `token` parameter (then removed the parameter again
|
||
since the strengthened check never needed the residence token's own
|
||
SourcePlacementCommitVersion field - no natural equivalent baseline exists
|
||
for a CONTINUATION's placement the way the residence's own token has one
|
||
for the INITIAL placement, and adding an unused parameter was worse than
|
||
omitting it).
|
||
|
||
### A3 (blocker) — HasContact from wire IsGrounded. FIXED.
|
||
Removed RuntimeInitialCreateExecutionInputs.HasContact entirely (record now
|
||
just UsePositionFromServer/PlayerDistance). ApplyPositionAction's hasContact
|
||
now reads `action.Position!.Value.IsGrounded` (WorldSession.EntityPositionUpdate
|
||
already carries this field, PositionPack bit 0x4, server-asserted contact at
|
||
admission time) - no PhysicsBody?.InContact derivation, no inputs fallback.
|
||
Confirmed WorldSession.EntityPositionUpdate.IsGrounded already exists and is
|
||
populated by BuildSameGenerationEvents (hardcoded true for
|
||
SameIncarnationCreate-sourced positions - matches retail passing arg5=true
|
||
directly for that source) and by every test's PositionUpdate helper. Fixed
|
||
6 call sites across the test file that constructed
|
||
RuntimeInitialCreateExecutionInputs with the now-removed named parameter;
|
||
each one's semantic intent (contact true/false) was already independently
|
||
preserved by the underlying WorldSession.EntityPositionUpdate.IsGrounded
|
||
value at each site, confirmed individually before dropping the parameter
|
||
(no test's PASS/FAIL meaning changed).
|
||
|
||
### B5 (major) — remove HasAnimations SameIncarnationCreate short-circuit.
|
||
### FIXED.
|
||
hasAnimations is now `canonical.Snapshot.MotionTableId is {} m && m != 0u`
|
||
unconditionally - the `action.PositionSource is SameIncarnationCreate ||`
|
||
short-circuit is gone. (Note: the CLASSIFIER's OWN, separate
|
||
`effectiveContact = Source is SameIncarnationCreate || HasContact`
|
||
short-circuit for the non-local branch is untouched - that is a DIFFERENT
|
||
mechanism the review did not flag, confirmed by rereading
|
||
RuntimeAuthoritativePositionRouteClassifier.cs's ClassifyAcceptedPosition
|
||
before making this change.)
|
||
|
||
### B6 (major) — thread route flags through position apply + trace. FIXED.
|
||
InboundPhysicsStateController.ApplyAcceptedPosition gained
|
||
installPlacementFrame/clearParent bool params. installPlacementFrame gates
|
||
the placement-id computation (previously unconditional whenever disposition
|
||
was Apply); clearParent gates whether ParentGuid/ParentLocation/
|
||
Physics.Parent get nulled (previously unconditional always-null). Legacy
|
||
TryApplyPosition passes true/true (exact prior behavior, verified by
|
||
re-deriving the original unconditional logic under installPlacementFrame=true
|
||
matches it exactly). The executor passes
|
||
installPlacementFrame: route.ApplyPlacementFrameBeforeRouting,
|
||
clearParent: route.UnparentBeforeRouting directly - confirmed by rereading
|
||
the classifier that UnparentBeforeRouting is false ONLY for the
|
||
ForcePosition branch and true for every other accepted route, matching
|
||
retail's Gate-A-returns-before-unset_parent structure exactly (no OR/
|
||
redundant condition needed, unlike the pinned hint's phrasing - the route's
|
||
own flag already encodes the full decision). Added a NEW execution-rejected
|
||
stamp variant (see B10) that also needed these two params threaded through
|
||
for its own call site. Extended RuntimeInitialCreateExecutedAction with
|
||
ConstrainPhase/StopInterpolating/ZeroVelocity/PreserveHeading/
|
||
SendPositionImmediately/UnparentBeforeRouting, all pulled from the route in
|
||
BuildPositionTrace (both the accepted-merge and resume call sites already
|
||
pass a fully-populated route, including the RejectedAuthority/RejectedData
|
||
factory routes' all-false/None defaults).
|
||
|
||
### B7 (major) — deferred-child replay via whole-bucket detach. FIXED.
|
||
Added ParentAttachmentState.DetachDeferredCreates(parentGuid) ->
|
||
ImmutableArray<DeferredParentCreate>: atomically removes and returns the
|
||
ENTIRE queued bucket for one parent (retail: PartArray::add_child-owning
|
||
CreateObject handler detaches the whole netblob list before dispatching,
|
||
pseudo-C ~93617 - detach IS the consume, no separate peek-then-remove).
|
||
ReplayDeferredChildren rewritten to call this once and iterate the detached
|
||
snapshot, rechecking _entities.IsCurrent(canonical) per iteration (unchanged
|
||
abandonment semantics) - this structurally eliminates the old peek/consume
|
||
loop's stale-AdmissionId race entirely (a Create arriving for the SAME
|
||
parent during replay now enqueues into a BRAND NEW queue instance, since the
|
||
old one was already removed from the dictionary). Kept TryPeekDeferredCreate/
|
||
ConsumeDeferredCreate/ContainsDeferredCreate/CancelDeferredChildGeneration -
|
||
still used by other invariants and by
|
||
StaleAdmissionIdCannotConsumeAReplacementQueuedAfterThePeek, which tests
|
||
ParentAttachmentState directly (Section C matrix item: the round3-fixes
|
||
text asked E-scenarios to go through the executor path - added a NEW
|
||
executor-path test, MultipleChildrenQueuedBehindOneMissingParentReplayInFifoOrderThroughTheExecutor,
|
||
covering 2 children queued behind one missing parent, both replaying in
|
||
order once the parent registers, rather than migrating the existing
|
||
ParentAttachmentState-direct test, since that one specifically exercises
|
||
the AdmissionId-staleness invariant which is a ParentAttachmentState-level
|
||
contract independent of the executor).
|
||
|
||
### B8 (major) — rename unreachable ResidentCellCleanup disposition. FIXED.
|
||
NoCellClaimedDestructionMarked -> CelllessNoWeenieMarkUnreachable, with an
|
||
updated doc comment citing HandleCreateObject retail-notes.md function 1
|
||
lines ~93942-93943 and the shape guarantee at
|
||
RuntimeInitialCreateResidenceState.cs:277-284
|
||
(RuntimeInitialCreateResidenceContinuation.HasValidShape enforces
|
||
Actions[^2].Kind is WeenieDescription for every admitted envelope, so
|
||
retail's matching "no weenie" condition can never be true through this
|
||
exact construction). Updated the one test reference
|
||
(SameIncarnationEnvelopePublishesNothingUntilEveryStageCommitsThenPublishesInStageOrder).
|
||
|
||
### B9 (major) — Parent continuation execution-time revalidation. FIXED.
|
||
New ApplyParentContinuation wrapper (called from ApplyContinuation's Parent
|
||
case instead of ApplyParentAction directly): re-checks
|
||
_entities.TryGetActive(parentGuid) + incarnation match at EXECUTION time; on
|
||
mismatch, re-enqueues via _entities.ParentAttachments.Enqueue(parentUpdate)
|
||
and records a routine trace entry (Completed, not abandonment) instead of
|
||
running ApplyParentAction against a parent that may have been deleted or
|
||
replaced between admission and this drain reaching it. Added
|
||
ParentContinuationRevalidatesLiveParentAtExecutionAndReDefersOnMismatch:
|
||
admits a Parent continuation while the parent is still active, deletes the
|
||
parent before the drain runs, asserts the drain still converges cleanly and
|
||
the update lands back in ParentAttachmentState's unresolved queue
|
||
(UnresolvedRelationCount == 1) rather than crashing or committing against a
|
||
gone parent.
|
||
(Envelope-side note: RuntimeInitialCreateTailActionKind.Parent has no case
|
||
in ApplyEnvelope's switch at all - only CreateParent does, confirmed by
|
||
rereading RuntimeInitialCreateResidenceState.cs's HasValidShape/
|
||
SameCreateStage: the position-branch stage only ever admits
|
||
CreateParent/Pickup/Position, never standalone Parent - so B9 only applies
|
||
to the standalone top-level continuation kind, which is where the fix
|
||
landed.)
|
||
|
||
### B10 (major) — new stamp variant for execution-time-rejected retained
|
||
### Position. FIXED.
|
||
Added InboundPhysicsStateController.ApplyAcceptedPositionExecutionRejectedSnapshot
|
||
(+ its RuntimeEntityDirectory delegate): stamps Position/Teleport/
|
||
ForcePosition timestamp channels (all three the gate genuinely advanced at
|
||
ADMISSION time) without installing any pose/parent/placement field -
|
||
distinct from the EXISTING ApplyAcceptedPositionTimestampOnly (the
|
||
ADMISSION-time-gate-Rejected case, where only ForcePosition can have moved).
|
||
ApplyPositionAction's `!route.Accepted` branch now branches on
|
||
action.PositionDisposition: Rejected (admission itself rejected) uses the
|
||
existing Rejected-forced merge; Apply/ForcePosition (admission accepted, but
|
||
EXECUTION-time classification now rejects) uses the new stamp variant. Not
|
||
independently unit-tested with a NEW dedicated test (constructing a retained
|
||
action whose ADMISSION disposition is Apply/ForcePosition but whose
|
||
EXECUTION-time classification genuinely rejects needs a live-input/record
|
||
mismatch crafted between admission and drain - flagged as a coverage gap;
|
||
the code path was verified by direct code review of both branches against
|
||
InboundPhysicsStateController.ApplyAcceptedPositionTimestampOnly's own
|
||
existing doc comment, which independently documents the same
|
||
admission-vs-execution distinction this fix formalizes).
|
||
|
||
### B11 (major) — Progress creation timing. FIXED.
|
||
Execute/ExecuteCore restructured: an existing Progress for a mismatched
|
||
LeaseId is discarded AND the call fails closed immediately (preserves the
|
||
EXISTING contract/test
|
||
StaleProgressLeaseIdIsDiscardedAndFailsClosedThenRetrySucceedsCleanly's
|
||
"fail THIS call, retry succeeds fresh" two-step semantics - my first attempt
|
||
at B11 broke this test by silently continuing forward in the SAME call
|
||
after discarding stale progress; caught immediately by the full-suite run,
|
||
reverted to the fail-closed-then-retry shape). A FRESH Progress for the
|
||
CURRENT lease id is never created until _residences.Complete(...) actually
|
||
reports Completed - PendingPlacement/RejectedToken/RejectedAuthority
|
||
outcomes on a call with no PRIOR progress now leave the ownership ledger
|
||
(ProgressCount) completely untouched, rather than a placeholder Progress
|
||
object sitting in _progress for a residence that has not even resolved its
|
||
own initial placement yet.
|
||
|
||
## Test suite status after Round 3
|
||
- 5 NEW tests added (all Round 3): DrainedAppearanceAndPoseSurviveTheNextLegacyWireApply
|
||
(A1), SameIncarnationWeenieDescriptionMergePreservesRetainedMotionTableId
|
||
(A2), WeenieDescriptionStageWiresTheObjectTableExactlyOnce (B12),
|
||
MultipleChildrenQueuedBehindOneMissingParentReplayInFifoOrderThroughTheExecutor
|
||
(B7), ParentContinuationRevalidatesLiveParentAtExecutionAndReDefersOnMismatch
|
||
(B9).
|
||
- 1 EXISTING test rewritten from Assert.Throws to typed-abandonment
|
||
assertions (ResidentCellCleanupFailsClosedWhenClaimedCelllessOutcomeIsNotUnderLostCellOwnership,
|
||
covers B1's ResidentCellCleanup-abandonment path).
|
||
- 1 EXISTING test's enum reference updated
|
||
(SameIncarnationEnvelopePublishesNothingUntilEveryStageCommitsThenPublishesInStageOrder,
|
||
B8 rename).
|
||
- 6 EXISTING call sites fixed for the removed HasContact parameter (A3) -
|
||
none of their PASS/FAIL semantics changed, only the construction syntax.
|
||
- Complete AcDream.Runtime.Tests: 858 passed (853 Round-2 baseline + 5 new),
|
||
0 failed.
|
||
|
||
## Known gaps NOT covered by a new dedicated test (time-budget /
|
||
## defensibility tradeoffs, reported honestly rather than papered over):
|
||
- B1's operation-slot-contention retry path (transient
|
||
TryBeginExclusiveAuthoredPlacement failure while the record stays
|
||
current) - needs a second concurrent SetPosition consumer occupying the
|
||
same key's slot; none of the harness helpers construct that scenario.
|
||
Verified by code review against RuntimeSetPositionState's exact failure
|
||
conditions instead.
|
||
- B10's execution-time-rejected (admission accepted, live classification
|
||
rejects) Position stamp path - needs a live-input/record mismatch crafted
|
||
between admission and drain. Verified by code review + cross-reference
|
||
against ApplyAcceptedPositionTimestampOnly's existing analogous doc
|
||
comment instead.
|
||
- B2's reentrant-retirement-window closure is a structural/ordering fix
|
||
(mutate -> rebaseline -> publish) proven correct by the FULL 858-test
|
||
suite staying green (nothing in the existing suite depends on the OLD
|
||
ordering) rather than by a dedicated synthetic-reentrancy test - building
|
||
a true concurrent-observer reentrancy test that could only pass with the
|
||
NEW ordering and fail with the OLD one was judged lower value than the
|
||
other coverage gaps given the remaining time budget.
|
||
- Section C's full 9-sub-matrix enumeration from round3-fixes.md was not
|
||
exhaustively built out; the 5 new tests target the SPECIFIC new/changed
|
||
behaviors (A1/A2/B7/B9/B12) plus B1's rewritten test, prioritized over
|
||
broad matrix completeness under this round's time budget.
|
||
|
||
## ROUND 4: combined re-review findings (round4-fixes.md, R4-1..R4-15).
|
||
## Smaller than Round 3; same bar. Implemented in this order: R4-4 (field-
|
||
## masked baseline enum/method - foundational, everything else built on it),
|
||
## R4-1 (deferred-child replay containment/restore), R4-5+R4-6 (Parent
|
||
## discard + trace enums), R4-2 (ResumePendingPlacement forget-before-abandon),
|
||
## R4-3 (WeenieDescription apply-window reorder + result check), R4-7/R4-8
|
||
## (test-only trace-flag/wire-vs-body fixes), R4-9 (two mandated tests),
|
||
## R4-10 (captured-key threading), R4-11 (ApplyStateAction bool fix),
|
||
## R4-12/R4-13 (doc comments + fallback + structural test pin), R4-14 (doc
|
||
## comment), R4-15 (register-rows-draft.md rewrite).
|
||
|
||
### R4-1 (deferred-child replay containment). FIXED.
|
||
ReplayDeferredChildren rewritten: (a) each child's `_registerDeferredChild`
|
||
call now runs inside try/catch - an exception records
|
||
RuntimeDeferredChildReplayOutcome.Rejected and the loop continues with the
|
||
next entry (was: an uncontained exception would have escaped Execute
|
||
entirely and stranded every remaining sibling). (b) mid-loop abandonment
|
||
(entity no longer current, e.g. a reentrant delete/reset from an earlier
|
||
sibling's own registration callback) now calls the NEW
|
||
ParentAttachmentState.RestoreDeferredCreates(parentGuid, remainder) - a
|
||
new method that PREPENDS the exact unprocessed remainder (original
|
||
DeferredParentCreate records, so original AdmissionIds are preserved) ahead
|
||
of anything enqueued for the same parent guid after the detach - before
|
||
returning false. Previously the whole detached array was simply dropped on
|
||
the floor on abandonment; this was a genuine data-loss bug (retail's queued
|
||
blobs live on CObjectMaint per-GUID and survive the object; our own
|
||
GUID-keyed persistence design already assumed this but the code broke it).
|
||
Tests: DeferredChildReplayContainsOneChildsThrowingRegistrationAndContinuesWithSiblings
|
||
(3 children behind one parent; child 2's registration is forced to throw
|
||
via a reflection-swapped _registerDeferredChild delegate - the standard
|
||
fault-injection pattern this file already used for
|
||
SetCompletedAdoptionRevision; child 1 and 3 still register, trace shows
|
||
Rejected for child 2, no exception escapes Execute) and
|
||
DeferredChildReplayRestoresTheUnprocessedRemainderWhenTheParentIsDeletedReentrantlyMidReplay
|
||
(2 children; child 1's registration callback reentrantly deletes the
|
||
PARENT; child 2's raw Create is confirmed back in the bucket via
|
||
ContainsDeferredCreate; asserted residence-lease-count == 1, matching the
|
||
EXISTING DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection
|
||
precedent - that "1" is child 1's own never-executed residence, not a
|
||
leak; first draft of this test wrongly asserted 0 and had to be corrected
|
||
after tracing the precedent test's own comment).
|
||
|
||
### R4-2 (ResumePendingPlacement leaked the acknowledged completion on
|
||
### both failure arms). FIXED.
|
||
Both failure arms (projection/record mismatch; ConsumeAcknowledgedPlacement
|
||
failure) now call `_physics.SetPosition.ForgetExactPlacement(placementToken)`
|
||
+ `PublishCancellation` BEFORE clearing progress.PendingContinuationPlacement
|
||
and calling Abandon (forget -> clear -> Abandon, exactly as pinned).
|
||
Verified ForgetExactPlacement's own ForgetPlacementCompletionCore already
|
||
removes the _acknowledgedPlacementCompletions entry unconditionally (read
|
||
the source directly) - no separate ForgetPlacementCompletion call was
|
||
needed. Tests:
|
||
ExternalFullCellMutationDuringAwaitingContinuationPlacementForgetsThePendingPlacementAndAllowsAFreshOneToBegin
|
||
(the reviewer's exact scenario: drive a local-teleport continuation to
|
||
AwaitingContinuationPlacement, complete/acknowledge its placement, THEN
|
||
mutate FullCellId externally, assert the NEXT Execute fails closed,
|
||
AcknowledgedPlacementCompletionCount == 0, and a FRESH
|
||
TryBeginExclusiveAuthoredPlacement for the same key succeeds - proving no
|
||
retained-completion leak) and
|
||
ThirdPartyTryGetTransactionRetireWhileAwaitingContinuationPlacementDiscardsExecutorProgressAndForgetsThePendingPlacement
|
||
(R4-9a, see below). VERIFIED both regression tests actually catch the bug:
|
||
temporarily stripped the forget/publish calls from both failure arms (kept
|
||
a backup copy of the file), reran the R4-2 test - confirmed FAIL
|
||
(AcknowledgedPlacementCompletionCount stayed 1, not 0) - then restored the
|
||
fix and confirmed both tests pass again.
|
||
|
||
### R4-3 (ApplyWeenieDescriptionAction object-table apply window/result).
|
||
### FIXED.
|
||
Reordered to AdvanceCreateAuthority -> AdvanceExecutorBaseline ->
|
||
_applyAcceptedSpawn -> (false result -> return false, letting the EXISTING
|
||
caller route to Abandon) -> buffered publish. Previously the rebaseline ran
|
||
AFTER _applyAcceptedSpawn and the bool result was silently discarded -
|
||
mirrors RuntimeLiveEntitySessionController.cs:87's own gate on that same
|
||
call's result (a nested replacement re-entering from within
|
||
ObjectTableWiring.ApplyEntitySpawn's own synchronous ObjectAdded/
|
||
ObjectUpdated dispatch must invalidate the remaining tail). Tests:
|
||
ObjectTableSubscriberReenteringAWireApplyDuringIngestDoesNotRetireTheResidenceAndTheEnvelopeCompletes
|
||
(subscribe to lifetime.Objects.ObjectAdded, reentrantly call TryApplyVector
|
||
from inside it - residence survives, envelope completes) and
|
||
NestedReplacementDuringObjectTableIngestAbandonsTheWeenieDescriptionStageWithNoFurtherStages
|
||
(same subscription point, but reentrantly RegisterEntity a NEWER
|
||
incarnation for the SAME guid - Execute returns RejectedAuthority, no
|
||
further stages ran). Both use ClientObjectTable's plain C# events directly
|
||
(ObjectAdded/ObjectUpdated) rather than reflection - much simpler than
|
||
initially planned once I found these were public events.
|
||
|
||
### R4-4 (field-masked executor baseline precision). FIXED - foundational,
|
||
### done FIRST since every apply method's shape changed.
|
||
Added [Flags] enum RuntimeExecutorBaselineFields (PositionAuthorityVersion/
|
||
CreateIntegrationVersion/FullCellId/PlacementCommitVersion) on
|
||
RuntimeInitialCreateResidenceState.cs; AdvanceExecutorBaseline now takes a
|
||
`fields` parameter and only copies the named field(s) from the live record
|
||
into the CompletedEntry's Expected* baseline. Traced every apply method's
|
||
ACTUAL field mutations against RuntimeEntityRecord.cs's own method bodies
|
||
before assigning masks (not guessed): ObjDesc/Movement(both branches)/
|
||
State/Vector move NONE of the four tracked fields (their own
|
||
AdvanceXxxAuthority methods only bump ObjDescAuthorityVersion/
|
||
MovementAuthorityVersion+MovementCommitVersion/StateAuthorityVersion+
|
||
PhysicsStateMutationVersion/VectorAuthorityVersion+VelocityAuthorityVersion
|
||
respectively - VelocityAuthorityVersion is NOT one of the four tracked
|
||
fields) - AdvanceExecutorBaseline calls REMOVED entirely at these 4 sites.
|
||
Position/Parent/CreateParent (applied branch) move PositionAuthorityVersion
|
||
only. Pickup moves PositionAuthorityVersion + FullCellId (SetFullCell(0,0)).
|
||
WeenieDescription's AdvanceCreateAuthority moves PositionAuthorityVersion +
|
||
CreateIntegrationVersion (confirmed against its exact body). The pre-
|
||
Complete pending-placement-window rebaseline in ExecuteCore now masks
|
||
FullCellId + PlacementCommitVersion only (the sole legitimate between-calls
|
||
mutation, RuntimeSetPositionState's own commit machinery). R4-5's NEW
|
||
Parent-discard branch (ApplyParentPositionTimestampOnly/
|
||
ApplyCreateParentPositionTimestampOnly) correctly calls NO
|
||
AdvanceExecutorBaseline at all - traced that ApplyPositionTimestampOnly
|
||
(the shared static body both go through) only writes PositionSequence/
|
||
nested Physics.Timestamps.Position, never any AdvanceXxxAuthority method.
|
||
Test: FieldMaskedBaselinePrecisionDetectsAnExternalPositionRaceDuringAnUnrelatedObjDescPublish
|
||
(FIFO = ObjDesc then Vector on a parented/no-placement residence; an
|
||
observer bumps PositionAuthorityVersion externally during ObjDesc's OWN
|
||
publish; asserts the drain detects this at ConsumeExecuted - RejectedAuthority,
|
||
not Released - proving ObjDesc's own apply correctly did NOT blanket-rebaseline
|
||
and silently absorb the race).
|
||
|
||
### R4-5 (stale-parent DISCARD, not re-Enqueue) + R4-6 (trace enums). FIXED.
|
||
Added RuntimeParentRelationOutcome{Applied, DiscardedStaleParent} and
|
||
RuntimeDeferredChildReplayOutcome{Registered, ReDeferred, Rejected}, both
|
||
threaded onto RuntimeInitialCreateExecutedAction (renamed the old
|
||
`bool DeferredChildRegistered` field to `RuntimeDeferredChildReplayOutcome?
|
||
DeferredChildOutcome`, added a new `RuntimeParentRelationOutcome?
|
||
ParentRelationOutcome` field). ApplyParentContinuation's stale-parent
|
||
branch (standalone Parent) no longer calls ParentAttachments.Enqueue -
|
||
instead calls the NEW ApplyParentPositionTimestampOnly (the SAME
|
||
ApplyAcceptedParentSnapshot->ApplyPositionTimestampOnly merge body
|
||
ApplyParentAction's own first step already used, but stops there - no
|
||
AdvancePositionAuthority/LeaveWorld/Forget/publish) and traces
|
||
DiscardedStaleParent. Added the ANALOGOUS revalidation to the envelope's
|
||
CreateParent stage too (this never existed before at all - Round 3 B9 only
|
||
touched the standalone Parent kind, explicitly noting the envelope path
|
||
had no revalidation) via a new ApplyCreateParentContinuation wrapper +
|
||
ApplyCreateParentPositionTimestampOnly helper; CreateParentUpdate carries
|
||
no ParentInstanceSequence at all (confirmed from its own record definition
|
||
and the TryApplyCreateParent doc comment: "unlike standalone ParentEvent it
|
||
carries no parent INSTANCE_TS"), so only addressability is revalidated
|
||
there, never incarnation match. UPDATED the existing Round 3 B9 test
|
||
(renamed ParentContinuationRevalidatesLiveParentAtExecutionAndReDefersOnMismatch
|
||
-> ...AndDiscardsOnMismatch): asserts ParentRelationOutcome.DiscardedStaleParent
|
||
in the trace and UnresolvedRelationCount == 0 (was asserting 1, i.e. the
|
||
OLD re-defer semantics) - this was the ONE pre-existing test that broke
|
||
after the R4-5 rewrite, exactly as expected, and was updated per the
|
||
directive's explicit instruction.
|
||
|
||
### R4-7 (test-only: wire-IsGrounded-vs-body-contact). FIXED.
|
||
Parameterized the PositionUpdate test helper with `isGrounded = true`
|
||
(default preserves every existing call site's behavior). Deleted the 3
|
||
stale `ForceContact(canonical, inContact: true)` calls + their misleading
|
||
"matching this test's ... premise" comments at LocalOrdinaryPosition.../
|
||
RemoteNearContactPosition.../RemoteFarPosition... - none of them affect
|
||
routing anymore since Round 3 A3 (HasContact reads ONLY wire IsGrounded).
|
||
Added 2 new disagree-both-ways tests:
|
||
LocalOrdinaryPositionRouteFollowsWireGroundedTrueWhenBodyContactIsFalse
|
||
(wire true / body forced false -> Interpolate, i.e. route follows wire) and
|
||
RemotePositionRouteFollowsWireGroundedFalseWhenBodyContactIsTrue (wire
|
||
false / body forced true -> NoPositionOperation). Kept the ForceContact
|
||
helper itself (still used by these 2 new tests to construct the
|
||
disagreement).
|
||
|
||
### R4-8 (D-matrix trace-flag assertions). FIXED.
|
||
Added the missing ConstrainPhase/UnparentBeforeRouting/HookPhase/
|
||
ZeroVelocity/StopInterpolating assertions to the 5 existing D-matrix tests
|
||
(local ordinary, local teleport, remote near, remote far, projectile) per
|
||
each route's own classified values (cross-checked against
|
||
RuntimeAuthoritativePositionRouteClassifier's exact returned route structs,
|
||
not guessed).
|
||
|
||
### R4-9 (two missing mandated tests). FIXED.
|
||
(a) ThirdPartyTryGetTransactionRetireWhileAwaitingContinuationPlacementDiscardsExecutorProgressAndForgetsThePendingPlacement:
|
||
drives a local-teleport continuation to AwaitingContinuationPlacement
|
||
(placement begun+watched, NOT yet acknowledged), externally bumps
|
||
PositionAuthorityVersion, then calls
|
||
lifetime.InitialCreateResidences.TryGetTransaction(canonical, out _)
|
||
directly (the third-party path, not through Execute) - asserts it returns
|
||
false (staleness detected), then asserts executor progress/residence-lease/
|
||
active-operation/watch/acknowledged-completion counts are ALL zero (B3's
|
||
notification edge correctly forgot the pending continuation placement, not
|
||
just the residence's own initial-lease placement) and a fresh placement
|
||
can begin. (b) is the ExternalFullCellMutationDuringAwaitingContinuationPlacement...
|
||
test already covered under R4-2 above.
|
||
|
||
### R4-10 (captured-key threading through every Abandon call site). FIXED.
|
||
Threaded `RuntimeEntityKey key` as an explicit parameter through
|
||
ApplyContinuation/ApplyParentContinuation/ApplyEnvelope/ApplyPositionAction/
|
||
ResumePendingPlacement (all captured ONCE in ExecuteCore from
|
||
`canonical.Key is not { } key` at the very top). Replaced all 19
|
||
`canonical.Key ?? default` occurrences (18 Abandon call sites + the
|
||
ExecuteCore Released-branch receipt construction) with the threaded `key`.
|
||
This is not purely cosmetic: `canonical.Key ?? default` re-derives the key
|
||
from the LIVE record at each call site, which produces `default` (WRONG -
|
||
does not match the Progress dictionary's actual key) if canonical.Key has
|
||
already gone null (e.g. LocalEntityId released) by the time Abandon runs -
|
||
DiscardProgress(default) would silently fail to clean up the REAL stale
|
||
Progress entry. The threaded key is trusted/stable for the whole Execute
|
||
call. ApplyPositionAction's own top guard (`canonical.Key is not {} key`)
|
||
was simplified to `canonical.Key != key` since key is now a parameter, not
|
||
a fresh pattern-bind.
|
||
|
||
### R4-11 (ApplyStateAction BecameHidden currency-failure now returns
|
||
### false, not true). FIXED.
|
||
Changed `return true;` to `return false;` in the BecameHidden branch's
|
||
currency-failure check - the EXISTING caller (`if (!ApplyStateAction(...))
|
||
return Abandon(...)`) already converts false -> Abandon correctly, so no
|
||
other change was needed. Documented via a new doc comment on the method
|
||
explaining WHY this specific path is not independently unit-tested with a
|
||
live reentrancy seam: traced RuntimeCollisionReportingState.LeaveWorld ->
|
||
ForceEnd -> EndExpiredObjectCollisions and confirmed it returns immediately
|
||
whenever `_owners` has no established collision record for this key (line
|
||
~1115-1119) - which is ALWAYS true for a residence-fresh entity that has
|
||
never run a real collision batch, so no observer callback can ever fire
|
||
from this call site through this harness. Did not fake a seam; documented
|
||
per the round's explicit escape valve for this exact finding.
|
||
|
||
### R4-12 (ForcePosition parent-retention doc + structural test pin). FIXED.
|
||
Added a code comment at InboundPhysicsStateController.ApplyAcceptedPosition's
|
||
`parentGuid` computation citing retail Gate A (retail-notes.md function 3,
|
||
"GATE A: local-player force-position self-echo shortcut" - the early return
|
||
before CPhysicsObj::unset_parent). Extended the EXISTING ForcePosition test
|
||
(ForcePositionContinuationRecordsSetPositionSimpleWithPreservedHeadingAndNoParentClear):
|
||
attaches a parent via lifetime.Entities.TryCommitParent BEFORE the
|
||
ForcePosition update (gate-satisfying positionSequence match verified
|
||
against Spawn()'s own default), then asserts BOTH Position and
|
||
ParentGuid/ParentLocation are non-null after the drain - pinning the
|
||
deliberate combined shape deliberately, per the directive.
|
||
|
||
### R4-13 (HasAnimations Physics?.MotionTableId fallback). FIXED.
|
||
`hasAnimations` in ApplyPositionAction now reads
|
||
`(canonical.Snapshot.MotionTableId ?? canonical.Snapshot.Physics?.MotionTableId)
|
||
is {} m && m != 0u` - exact pinned formula. No dedicated new test (NOTE
|
||
priority per the directive); existing HasAnimations-adjacent tests
|
||
(ForcePosition non-animated route etc.) continue to pass unchanged since
|
||
top-level MotionTableId is populated in every existing test fixture.
|
||
|
||
### R4-14 (AwaitingContinuationPlacement doc comment). FIXED.
|
||
Added a doc comment on the enum value explaining the two distinct flavors
|
||
sharing this one status (ordinary token-available case vs Round 3 B1's
|
||
operation-slot-contention case where TryGetPendingContinuationPlacement
|
||
returns false) and the correct caller action for each.
|
||
|
||
### R4-15 (register-rows-draft.md rewrite). FIXED - scratchpad-only, no
|
||
### docs/ edits.
|
||
Row A: dropped the MoveOrTeleport co-anchor (confirmed retail-notes.md
|
||
never decompiled MoveOrTeleport's own internals; the ONLY confirmed
|
||
HasAnims call site is inside HandleReceivedPosition itself, line ~92992),
|
||
reworded "running cycle" -> "non-empty animation queue (anim_list.head_ !=
|
||
0)", updated the divergence formula text to match R4-13's new fallback.
|
||
Row C: broadened from "the 3 no-placement routes" to "NO route runs a live
|
||
ConstrainTo, including SetPosition routes" per the directive, citing the
|
||
3 ConstrainTo call sites at retail-notes.md lines ~93007/~93024/~93041.
|
||
Row D: rewrote across the THREE ApplyResidentCellCleanup branches -
|
||
claimed+celless+undeferred is now correctly described as a typed
|
||
ABANDONMENT (not "a recorded fact" - that was wrong even before Round 4,
|
||
since Round 3 B1 already changed this to Abandon; the register draft just
|
||
hadn't caught up), the deferred flavor correctly delegates retail's
|
||
AddObjectToBeDestroyed (93933) to the lost-cell/deferred owners, and the
|
||
claimedCell==0 branch is explicitly marked NOT a divergence (structural
|
||
shape guarantee, nothing to diverge from). Row E: softened the "no retail
|
||
per-step notice" claim - retail DOES emit exactly ONE notice
|
||
(ECM_Physics::SendNotice_CreateObject, ACCObjectMaint::CreateObject
|
||
0x00558870 step 11) per Create, just not N per-internal-step; kept the
|
||
consumer-facing risk warning unchanged. Row F: REMOVED from the register
|
||
draft entirely (internal refactor debt between two not-yet-unified
|
||
Position-apply paths, not a retail divergence) - replaced with (a) a code
|
||
comment on InboundPhysicsStateController.TryApplyPosition's doc comment and
|
||
(b) a new "ISSUES draft" section in the scratchpad file for eventual
|
||
docs/ISSUES.md inclusion. Added Row G (new): the executor's canonical-cell
|
||
semantics (refreshPosition: false), previously only a code comment, now
|
||
also a proper register row citing the classifier comment + retail
|
||
HandleReceivedPosition-never-writes-a-resident-cell fact.
|
||
|
||
## ROUND 4 FINAL GATES: see final report for exact totals.
|
||
|
||
---
|
||
|
||
# CUTOVER SLICE C0 — Runtime bridge + live inputs (new implementer session)
|
||
|
||
Worked from c0-contract.md (pinned), 2026-08-02-placement-cutover.md,
|
||
2026-08-02-cutover-route-inventory.md, 2026-08-02-runtime-continuation-
|
||
executor-handoff.md. HEAD at start 27e05b99.
|
||
|
||
## Discovery that changed my design: the observer/sink pipe already exists
|
||
Before designing C0-1, grepped for `IRuntimePlacementObserver`/
|
||
`IRuntimePlacementProjectionSink` production usage - found
|
||
`RuntimePlacementProjectionSubscription` (Runtime-owned,
|
||
`IRuntimePlacementObserver`), `RuntimePlacementPresentationSink` (App),
|
||
`HeadlessRuntimePlacementProjectionSink` (Headless) ALL already exist and
|
||
are production-wired (`GraphicalSessionEventRoute`/`HeadlessSessionHost`).
|
||
The cutover-route-inventory.md's "zero production IRuntimePlacementObserver"
|
||
claim is stale - superseded by a slice landed after that doc. What's still
|
||
genuinely dormant: nothing ever PUBLISHES into the channel in production,
|
||
because `Execute`/`RegisterEntityWithInitialResidence` still have zero
|
||
production callers. This meant C0-1 could NOT touch App/Headless sink
|
||
implementations (hard rule anyway) - the new `ExecutorCompleted` Kind will
|
||
sit unhandled by those sinks (return false) until a LATER cutover slice
|
||
updates them, but since Execute has no production caller today this never
|
||
fires in production. Documented this explicitly in code comments.
|
||
|
||
## C0-1: executor → placement-channel completion bridge. DONE.
|
||
Design (pinned contract's suggested shape, exactly): extended the
|
||
vocabulary, not the plumbing. New `RuntimePlacementProjectionKind.
|
||
ExecutorCompleted` (append-only, no exhaustive-switch breaks found anywhere
|
||
in src/tests via grep). New `RuntimeSetPositionState.PublishExecutorCompletion
|
||
(record, portal=default)`: builds a FRESH token via the SAME
|
||
`_nextProjectionSequence` counter every other publish uses (preserves
|
||
temporal/exact-head ordering), but derived from the CANONICAL RECORD's
|
||
current authority/version/cell facts (`_entities.SessionLifetimeVersion`,
|
||
`record.FullCellId`, `_physics.ExpectedCollisionGeneration(record.FullCellId)`)
|
||
rather than an Operation snapshot - correct because by the time Execute
|
||
reaches `Released`, the residence's/continuation's own operation is already
|
||
gone (adopted/acknowledged earlier in the SAME drain). `AcknowledgeProjection`
|
||
gained one extra Kind in its existing `Discard`-only fast path
|
||
(`Discard or ExecutorCompleted` -> remove + retire quiescence, no Operation
|
||
lookup) - there is no operation backing an ExecutorCompleted receipt, exactly
|
||
like Discard.
|
||
Correlation (the "reachable from/correlated with" ask): did NOT put the rich
|
||
internal `RuntimeInitialCreateExecutionReceipt` on the PUBLIC
|
||
`RuntimePlacementProjectionSnapshot` (would need public exposure of an
|
||
entire internal enum/struct family, or risk CS0053 inconsistent-accessibility
|
||
if done wrong) - instead the executor keeps a private
|
||
`Dictionary<RuntimeEntityKey, (ulong Sequence, Receipt)>` overwritten
|
||
per-key on each completion (bounded by live entity count, never
|
||
accumulates), queried via `TryGetCompletionReceipt(in RuntimePlacementProjectionToken)`
|
||
which verifies BOTH Entity and Sequence match before returning true - a
|
||
host/test correlates purely through the PUBLIC token identity every other
|
||
Kind already uses. The executor calls `PublishExecutorCompletion` exactly
|
||
once, at `ExecuteCore`'s `Released` exit (canonical still provably current
|
||
there).
|
||
Tests: `PublishExecutorCompletion_PublishesAcknowledgeOnlyReceiptAndConverges`,
|
||
`PublishExecutorCompletion_RespectsExactHeadOrderingAcrossEntities`
|
||
(RuntimeSetPositionStateTests.cs, isolated unit level);
|
||
`ExecutorCompletion_PublishesOnTheSamePlacementStreamCorrelatedWithTheFullReceipt`,
|
||
`ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder`
|
||
(RuntimeInitialCreateContinuationExecutorTests.cs, full Execute-drain
|
||
integration, the second proving continuation-Place-then-ExecutorCompleted
|
||
FIFO ordering).
|
||
|
||
## C0-2: Runtime-side live inputs. DONE.
|
||
UsePositionFromServer: grepped named-retail decomp for
|
||
`CommandInterpreter::UsePositionFromServer`/`SetAutonomyLevel`/
|
||
`autonomy_level` - found `result = this->autonomy_level != 2` (pseudo-C
|
||
699510), default `autonomy_level = 2` at construction (699752) AND at
|
||
`command_line_autonomy_level` (1088429, itself `0x2` by default) - autonomy
|
||
is a STARTUP/command-line-only knob in retail; no in-game caller of
|
||
SetAutonomyLevel exists anywhere in the decomp. Added the exact mirror to
|
||
`RuntimeCharacterState` (the "character-option owner" the contract named):
|
||
`FullAutonomyLevel=2u` const, `AutonomyLevel` (Volatile-read uint,
|
||
default 2), `UsePositionFromServer => AutonomyLevel != FullAutonomyLevel`,
|
||
`TrySetAutonomyLevel(level)` (rejects >2, exact retail rule). Reset in both
|
||
`ResetSession`/`Dispose`; added `AutonomyIsDefault` to
|
||
`RuntimeCharacterOwnershipSnapshot`/`IsConverged`.
|
||
PlayerDistance: grepped `LiveEntityNetworkUpdateController.cs` (App,
|
||
read-only) for the legacy remote path's own distance basis (cutover-routes.md
|
||
route 4) - confirmed `Vector3.Distance(worldPos, localPlayerPos)` where
|
||
`localPlayerPos = _playerController?.Position ?? Vector3.Zero` (the live
|
||
PHYSICS-CONTROLLER position, never a record snapshot). Bound source:
|
||
`RuntimeLocalPlayerMovementState.Controller?.Position ?? Vector3.Zero` -
|
||
same `PlayerMovementController` type.
|
||
Executor: `RuntimeInitialCreateContinuationExecutor.BindLiveInputs(Func<bool>,
|
||
Func<Vector3>)` (nullable seams, throws on double-bind matching
|
||
`BindGeneration`'s convention), `ResolveInputs(canonical, inputs)` computes
|
||
the EFFECTIVE `RuntimeInitialCreateExecutionInputs` ONCE per `Execute` call
|
||
(bound source wins; unbound falls back to the caller struct field-by-field) -
|
||
PlayerDistance uses THIS entity's own currently-accepted position
|
||
(`Snapshot.Physics?.Position ?? Snapshot.Position`, the same field
|
||
`CanonicalSetupTableId`-adjacent code already trusts) vs the bound live
|
||
player position. Documented the one-shot-per-Execute-call granularity as
|
||
inherited from the EXISTING `inputs` parameter shape, not a new limitation
|
||
I introduced - out of C0-2's scope to refine to per-continuation freshness.
|
||
`RuntimeEntityObjectLifetime.BindLiveInputs` forwards to the executor
|
||
(mirrors `BindEventContext`'s existing fan-out shape). `GameRuntime.cs` wires
|
||
the REAL sources right after `BindEventContext`, since `RuntimeCharacterState`/
|
||
`RuntimeLocalPlayerMovementState` are constructed AFTER `RuntimeEntityObjectLifetime`
|
||
in `GameRuntime`'s own sequence (verified exact construction order first).
|
||
Tests: `RuntimeCharacterStateTests.cs` (`AutonomyLevel_DefaultsToFullAndMirrorsRetailUsePositionFromServer`,
|
||
`ResetSession_RestoresAutonomyLevelToFull`);
|
||
`RuntimeInitialCreateContinuationExecutorTests.cs`
|
||
(`BindLiveInputs_DrivesClassificationFromTheBoundSourcesInsteadOfTheCallerStruct`
|
||
- proves bound-wins AND live-read-not-cached-at-bind-time by flipping the
|
||
captured bool between two entities' drains;
|
||
`BindLiveInputs_ThrowsOnASecondBindAndUnboundExecutorsUseTheCallerStructUnchanged`).
|
||
|
||
## C0-3: exact-Setup mover chain end-to-end. DONE.
|
||
New `RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement(record,
|
||
token, operationKind, flags, IPreparedCollisionSource, gameTime, out outcome,
|
||
placementClass=Ordinary, portal=default, ...scatter/shadow-offset params)`:
|
||
reads the CANONICAL Setup table id via the EXISTING private
|
||
`CanonicalSetupTableId(record)` (same field `CapturePreparationAuthority`
|
||
already trusts - never a caller-supplied id), takes the retail "genuine no
|
||
Setup" dummy path (`RuntimeSetPositionMoverSetup.ResolvedAbsent`) when that id
|
||
is 0, else calls `collisionSource.ReadSetupCollision(setupTableId)` and maps
|
||
Missing/Corrupt -> `RetrySetupUnavailable` (per `RuntimeSetPositionMoverSetup`'s
|
||
own doc-comment distinction between "not arrived yet" and "resolved absent" -
|
||
never manufactures a fallback while a real read is in flight) or Loaded ->
|
||
`RuntimeSetPositionMoverSetup.Resolved(id, data)`, then chains straight into
|
||
the EXISTING `PrepareMover` -> `SubmitPreparedPlacement`. Pure wiring - zero
|
||
changes to `PrepareMover`/`RuntimeSetPositionMoverPreparer.TryBuild`/
|
||
`SubmitPreparedPlacement`'s own validation semantics (per the contract's
|
||
explicit "wiring, not behavior change" constraint) - confirmed by re-reading
|
||
both untouched.
|
||
Tests (RuntimeSetPositionStateTests.cs, new `FakeCollisionSource :
|
||
IPreparedCollisionSource` test double, only `ReadSetupCollision` implemented
|
||
- others throw `NotSupportedException` since C0-3 exercises only that one):
|
||
`TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit`
|
||
(authored two-sphere Setup reaches `SubmitPreparedPlacement` and
|
||
`TryGetPreparedMoverSphereCount` byte-exactly == 2, matching the existing
|
||
preparer tests' own expectations) and
|
||
`TryPrepareAndSubmitAuthoredPlacement_YieldsRetryOnAMissingSetupReadWithoutMutatingStage`
|
||
(Missing status -> `RetrySetupUnavailable`, operation stays
|
||
`AwaitingPreparation`/`IsPlacementCurrent` true, no prepared-mover sphere
|
||
count recorded - genuinely retryable, not a dead token).
|
||
|
||
## C0-4: TryCommitParent/CommitWithdrawal cancellation asymmetries. DONE,
|
||
## both confirmed at source exactly as the inventory claimed.
|
||
(a) `RuntimeEntityObjectLifetime.TryCommitParent` (944-974 pre-fix) had
|
||
NEITHER `ForgetInitialCreateResidence` NOR `Physics.SetPosition.Forget` -
|
||
confirmed by direct read, contrasted against the sibling
|
||
`CommitPositionChannelUpdate` (used by `TryApplyParent`/`TryApplyCreateParent`)
|
||
which has BOTH. Fixed: added the identical
|
||
`ForgetInitialCreateResidence` -> `Physics.SetPosition.Forget` ->
|
||
`PreferCancellation` -> pass to `AcknowledgeProjectionAndPublish` sequence.
|
||
Deliberately did NOT add `Physics.CollisionReports.LeaveWorld` (present in
|
||
`CommitPositionChannelUpdate` but outside the contract's explicit
|
||
"residence/placement-family cancellation" scope, and I have no retail
|
||
citation that a STAGED parent-attach commit should also force a collision
|
||
leave-world at this exact point) - flagging this as a considered, deliberate
|
||
non-addition rather than an oversight.
|
||
(b) `CommitWithdrawal` (1401-1422 pre-fix) called `ForgetInitialCreateResidence`
|
||
but not `Physics.SetPosition.Forget` - confirmed by direct read, contrasted
|
||
against `TryApplyPickup`/`CommitAcceptedParentCellless`/`TryAcceptDelete`
|
||
which all cancel both. Fixed symmetrically (added the ordinary Forget +
|
||
PreferCancellation into the existing `cancellation` variable already threaded
|
||
to `AcknowledgeProjectionAndPublish`).
|
||
Tests, three total, each begins an ACTUAL in-flight SetPosition operation
|
||
that has already reached `SubmitPreparedPlacement`'s pending-Place stage (a
|
||
still-`AwaitingPreparation`, never-submitted operation produces NO Discard
|
||
receipt at all when cancelled - `CancelCoreDeferred` only converts an
|
||
EXISTING pending projection into a Discard; there is nothing to discard if
|
||
nothing was ever published - this cost one debugging round, see below):
|
||
`RuntimeInitialCreateResidenceStateTests.
|
||
TryCommitParent_CancelsActiveInitialResidenceAndItsPendingPlacement`
|
||
(residence's OWN placement, still unacknowledged, is the thing cancelled -
|
||
both Forget calls fire but only one finds anything, `PreferCancellation`
|
||
picks it, exactly one Discard observed); `RuntimeSetPositionStateTests.
|
||
TryCommitParent_CancelsASeparateActiveOrdinaryPendingPlacement` and
|
||
`CommitWithdrawal_CancelsAnActiveOrdinaryPendingPlacementSymmetricallyWithPickup`
|
||
(plain `RegisterEntity`, no residence at all - isolates the SECOND,
|
||
previously-missing Forget call specifically). All three assert the Discard
|
||
sits at the SAME sequence as the original Place (Revision bumped), then
|
||
explicitly acknowledge it and assert `PendingProjectionCount == 0` - a
|
||
cancelled-but-unacknowledged receipt stays IN the pending set (replaced, not
|
||
removed) until a host consumes it, same as every other in-flight-cancel path
|
||
in this codebase.
|
||
|
||
## Debugging round (all 4 caught by the focused-filter run, all root-caused
|
||
## and fixed, not worked around):
|
||
1. Three C0-4 tests initially asserted a Discard would be published from
|
||
cancelling a placement operation still in `AwaitingPreparation`
|
||
(never submitted) - traced `CancelCoreDeferred` and confirmed it only
|
||
converts an EXISTING `_pendingProjection` entry to Discard
|
||
(`operation.ProjectionSequence != 0UL` gate); an unpublished operation
|
||
just disappears from `_operations` with no receipt, which is CORRECT
|
||
(nothing was ever promised to a host). Fixed the TESTS to reach
|
||
`SubmitPreparedPlacement`'s pending-ack stage first, not the production
|
||
code.
|
||
2. Two of those same tests then asserted `PendingProjectionCount == 0`
|
||
immediately after cancellation - wrong; a Discard REPLACES the pending
|
||
entry at the same sequence (Revision+1), it does not remove it. Fixed the
|
||
assertions to expect 1, then explicitly acknowledge, then expect 0.
|
||
3. `TryCommitParent_CancelsActiveInitialResidenceAndItsPendingPlacement`'s
|
||
`Prepare(..., RuntimeSetPositionMoverSetup.ResolvedAbsent)` failed with
|
||
`InvalidData` because `Spawn(guid, 1)` in that file defaults
|
||
`setupId: 0x02000001u` (nonzero), mismatching `ResolvedAbsent`'s claimed
|
||
"no Setup at all". Fixed by passing `setupId: null` explicitly (matching
|
||
the file's OWN existing convention for this exact scenario, e.g.
|
||
`ResetSnapshotsAllResidenceOwnersBeforeReentrantDiscardObserver`).
|
||
4. `BindLiveInputs_DrivesClassificationFromTheBoundSourcesInsteadOfTheCallerStruct`
|
||
failed `AcknowledgeProjection` on a SECOND entity's placement - not exact
|
||
head. Root cause: the FIRST entity's full drain published its own
|
||
`ExecutorCompleted` receipt (C0-1) which I never acknowledged before
|
||
moving on to the second entity - this is CORRECT exact-head behavior
|
||
(great incidental proof C0-1's ordering guarantee holds), not a bug.
|
||
Fixed the test to peek+acknowledge the first completion before
|
||
proceeding.
|
||
|
||
## Final gates (all green)
|
||
1. `dotnet build AcDream.slnx -c Release`: 0 errors, 21 warnings (all
|
||
pre-existing, identical set to the executor handoff's baseline - zero
|
||
new warnings from this slice).
|
||
2. Focused filter (RuntimeInitialCreateContinuationExecutorTests|
|
||
RuntimeInitialCreateResidenceStateTests|RuntimeSetPositionStateTests|
|
||
RuntimePlacementProjectionSubscriptionTests|RuntimeCharacterStateTests|
|
||
RuntimeEntityObjectLifetimeTests): 247/247 passed.
|
||
3. Complete `AcDream.Runtime.Tests`: 916/916 passed (903 baseline + 13 new:
|
||
2 C0-1 unit + 2 C0-1 integration + 2 C0-2 executor + 2 C0-2 character-state
|
||
+ 2 C0-3 + 1 C0-4 residence + 2 C0-4 set-position-state).
|
||
4. Complete solution (`dotnet test AcDream.slnx -c Release -m:1`): every
|
||
project passed - App 4027/3 skip, Bake 15, Cli 4, Content 124, Core.Net
|
||
762, Core 4242/1 skip, Headless 76, Runtime 916, UI.Abstractions 543.
|
||
0 failed anywhere.
|
||
5. `git diff --check`: clean (only pre-existing LF-will-become-CRLF
|
||
metadata notices, no whitespace-error content).
|
||
6. `git status`: exactly the 9 files this slice touched, plus the 8
|
||
pre-existing protected dirty paths untouched (never staged/committed).
|
||
|
||
## Files changed (Runtime + Runtime.Tests only, no App/Headless production,
|
||
## no staging/commits)
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (C0-1 Kind+publish+
|
||
ack branch; C0-3 chain method; +`using AcDream.Content;`)
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
(C0-1 completion-receipt correlation + publish call site; C0-2
|
||
BindLiveInputs/ResolveInputs)
|
||
- src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (C0-2
|
||
BindLiveInputs forwarder; C0-4 TryCommitParent/CommitWithdrawal fixes;
|
||
+`using System.Numerics;`)
|
||
- src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs (C0-2 autonomy
|
||
level/UsePositionFromServer)
|
||
- src/AcDream.Runtime/GameRuntime.cs (C0-2 wiring the real sources;
|
||
+`using System.Numerics;`)
|
||
- tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs (C0-1,
|
||
C0-3, C0-4 tests + FakeCollisionSource; +`using AcDream.Content;`/
|
||
`AcDream.Content.Pak;`)
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
(C0-1, C0-2 tests + PlacementObserver fake)
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
|
||
(C0-4 residence-side test)
|
||
- tests/AcDream.Runtime.Tests/Gameplay/RuntimeCharacterStateTests.cs (C0-2
|
||
autonomy tests)
|
||
|
||
## Still dormant / no production caller flipped (per pinned scope)
|
||
Nothing in App/Headless was touched; `Execute`/`RegisterEntityWithInitialResidence`
|
||
still have zero production callers (unchanged from the executor handoff).
|
||
`RuntimePlacementPresentationSink`/`HeadlessRuntimePlacementProjectionSink`
|
||
will need an `ExecutorCompleted`-handling branch added when a LATER cutover
|
||
slice (C1+) actually starts calling `Execute` in production - flagging this
|
||
explicitly as the next slice's concern, not a gap in C0.
|
||
|
||
**SUPERSEDED by the review fix round below**: the App/Headless sink
|
||
untouched-claim above no longer holds - F1 sanctioned a scoped exception
|
||
(exactly 3 sink files). See the fix-round section for the full disposition.
|
||
|
||
---
|
||
|
||
# C0 REVIEW FIX ROUND (F1-F5)
|
||
|
||
Both independent reviews returned FAIL with converging findings. Per the
|
||
coordinator: all five retail semantic questions verified CLEAN (autonomy_level
|
||
!= 2 derivation exact; distance basis matches retail + legacy path fallback;
|
||
mover chain preserves prerequisite-B exactness; ExecutorCompleted
|
||
unambiguously acknowledge-only; the LeaveWorld omission in TryCommitParent is
|
||
REQUIRED per retail set_parent 0x00515A90:283832-283833's single gated
|
||
leave_world - a second one would double-leave-world with no retail
|
||
counterpart).
|
||
|
||
## F1 (MAJOR, arch) - sink acknowledge-and-ignore. FIXED. SCOPE EXPANSION
|
||
## SANCTIONED for exactly 3 files, no route flips, no other App/Headless changes.
|
||
Root cause: `HeadlessRuntimePlacementProjectionSink.cs:51` (`is not Place ->
|
||
false`), `RuntimePlacementPresentationSink.cs:89-96` (`_ -> false`),
|
||
`LiveEntityRuntime.cs:969-976` (`_ -> false`) all silently reject
|
||
ExecutorCompleted, and `RuntimePlacementProjectionSubscription` treats a
|
||
false return on the FIFO head as "leave pending" - the first ExecutorCompleted
|
||
reaching a production sink (at a future cutover slice) would permanently wedge
|
||
the entire ordered placement stream behind it. Fixed all three: added an
|
||
explicit `if (Kind is Discard or ExecutorCompleted) return true;`-shaped
|
||
early return BEFORE each file's record-lookup/portal-shape gate (never
|
||
letting ExecutorCompleted depend on a lookup that can legitimately fail for
|
||
unrelated reasons). Provably inert today - `PublishExecutorCompletion` has
|
||
zero production callers (`Execute`/`RegisterEntityWithInitialResidence` are
|
||
both unreached) - documented in both the code comments and the new tests.
|
||
Tests: `RuntimePlacementPresentationSinkTests.ExecutorCompleted_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone`
|
||
(App, mirrors the existing Discard test exactly, proves ack regardless of a
|
||
completely bogus/stale token) and
|
||
`HeadlessSessionHostTests.ExecutorCompletedReceiptIsAcknowledgeOnlyRegardlessOfRecordValidity`
|
||
(Headless, mirrors `PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly`'s
|
||
stale-incarnation half).
|
||
|
||
## F2 (MAJOR, both reviewers) - completion-receipt lifecycle. FIXED.
|
||
Four sub-fixes, all landed:
|
||
1. **Register-before-publish**: `PublishExecutorCompletion` gained a
|
||
`beforePublish: Action<RuntimePlacementProjectionToken>?` callback invoked
|
||
AFTER the token is added to `_pendingProjection` but BEFORE
|
||
`PublishPlacement`'s synchronous observer dispatch. The executor's
|
||
`ExecuteCore` Released case now registers `_completionReceipts[key]`
|
||
inside that callback (capturing `receipt` via a local `completedReceipt`
|
||
- an `out` parameter cannot be captured by a lambda) - a subscriber
|
||
reading the correlation back from inside its OWN `OnPlacement` now always
|
||
finds it.
|
||
2. **Ack-driven removal**: new `RuntimeSetPositionState.BindExecutorCompletionAcknowledgement(Action<RuntimeEntityKey,ulong>)`
|
||
(mirrors `RuntimeInitialCreateResidenceState.BindRetirementNotification`'s
|
||
existing one-bound-delegate shape), invoked from `AcknowledgeProjection`'s
|
||
ExecutorCompleted branch the moment a host acknowledges. Bound in all 3
|
||
`RuntimeEntityObjectLifetime` constructors to
|
||
`InitialCreateExecution.ForgetCompletionReceipt(key, sequence)` - a new
|
||
executor method that removes exactly the matching (key, sequence) entry
|
||
(exact-sequence-checked, so a NEWER completion under a reused key survives).
|
||
3. **DiscardProgress/DiscardAll**: both now unconditionally reap
|
||
`_completionReceipts` (Remove/Clear respectively) - DiscardProgress
|
||
removes it EVEN WHEN `_progress` no longer tracks the key (the drain
|
||
already removed its own Progress entry before publishing the completion),
|
||
proven by a dedicated test.
|
||
4. **Ownership/convergence**: added `PendingCompletionReceiptCount` to the
|
||
executor and folded it into
|
||
`RuntimeEntityObjectOwnershipSnapshot`/`IsConverged` (appended as the last
|
||
positional field with a `= 0` default, following this record's own
|
||
established extension convention) - chosen semantics: non-zero while
|
||
unacknowledged, zero exactly at acknowledge, mirroring
|
||
`RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount`'s
|
||
existing "unacknowledged receipt is outstanding debt, gated by
|
||
IsConverged" shape (documented as such, in contrast with the adjacent
|
||
diagnostic-only `ReplayFailureCount`).
|
||
Tests (`RuntimeInitialCreateContinuationExecutorTests.cs`):
|
||
`ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch`,
|
||
`ExecutorCompletion_ConvergenceLedgerCountsAnUnacknowledgedReceiptAsOutstandingDebtUntilAcknowledged`,
|
||
`ExecutorCompletion_CorrelationEntryIsReapedByDiscardProgress`,
|
||
`ExecutorCompletion_CorrelationEntryIsReapedByDiscardAll` (the latter two
|
||
split into single-entity tests after discovering combining them with a
|
||
second entity in the SAME lifetime hit exact-head contention - see debugging
|
||
notes below).
|
||
|
||
## F3 (MAJOR, arch) - nullable local-player-position fallback. FIXED.
|
||
`GameRuntime.cs:280`'s `context.Movement.Controller?.Position ?? Vector3.Zero`
|
||
fabricated a distance basis of literal (0,0,0) whenever the login-window
|
||
drain ran before the local player's own controller existed - a nearby remote
|
||
entity could misclassify as >96m and hard-snap where retail would
|
||
interpolate. Fixed per the contract's own fallback rule: `_localPlayerPosition`
|
||
is now `Func<Vector3?>?` (was `Func<Vevtor3>?`), `BindLiveInputs`'s parameter
|
||
type updated to match, `ResolveInputs` now does
|
||
`_localPlayerPosition?.Invoke() is { } localPlayerPosition` (both "unbound"
|
||
AND "bound-but-returns-null" fall back to the caller struct's PlayerDistance
|
||
identically), and `GameRuntime.cs` now binds
|
||
`() => context.Movement.Controller?.Position` directly (a `PlayerMovementController?.Position`
|
||
already yields `Vector3?` via null-conditional propagation - no `?? Vector3.Zero`
|
||
needed or wanted). Test (both directions, per the ask):
|
||
`BindLiveInputs_PlayerDistanceIsReadFromANonNullBoundSourceAndFallsBackToTheCallerStructWhenNull` -
|
||
entity 1 has a bound NON-null near position (proves the bound value, not the
|
||
caller struct's far value, wins -> Interpolate); entity 2 flips the SAME
|
||
bound source to null (proves it falls back to the caller struct's far value,
|
||
not Vector3.Zero -> SetPositionSimple/StopInterpolating).
|
||
|
||
## F4 (MINOR) - TryCommitParent comment narrowed. FIXED.
|
||
Rewrote the C0-4(a) comment: no longer claims "the SAME flow every sibling
|
||
relation commit uses" wholesale (which would imply LeaveWorld too) - now
|
||
states the Forget/PreferCancellation sequence is shared for THIS part of the
|
||
job, and explicitly documents the deliberate LeaveWorld omission citing
|
||
retail `set_parent` (0x00515A90, lines 283832-283833)'s single gated
|
||
leave_world call, which this method's staged/deferred-replay commit already
|
||
represents - a second LeaveWorld here would double-leave-world with no
|
||
retail counterpart.
|
||
|
||
## F5 (NOTEs). FIXED.
|
||
(a) `TrySetAutonomyLevel`'s doc comment now notes retail's setter ALSO sends
|
||
`SendAutonomyLevelEvent` (pseudo-C 699550) and that any FUTURE host exposure
|
||
of this setter must carry the equivalent outbound event, not just the field
|
||
write.
|
||
(b) `FullAutonomyLevel`'s doc comment corrected from "No in-game caller of
|
||
CommandInterpreter::SetAutonomyLevel exists" (implying zero callers anywhere)
|
||
to the precise claim: exactly ONE retail caller exists, the startup
|
||
construction path at pseudo-C 94102 (the constructor's own default at 699752
|
||
is a direct field write, not a SetAutonomyLevel call, so it doesn't count as
|
||
a second caller).
|
||
|
||
## Debugging round for the F2/F3 tests (both caught by the test run, both
|
||
## root-caused, not worked around)
|
||
1. `ExecutorCompletion_CorrelationEntryIsReapedByDiscardProgressAndDiscardAll`
|
||
(combined, single lifetime, two entities) failed at the SECOND entity's
|
||
`CompleteInitialPlacement` - root cause: the FIRST entity's
|
||
ExecutorCompleted receipt was left UNACKNOWLEDGED in `_pendingProjection`
|
||
(by design, to prove DiscardProgress reaps the correlation cache
|
||
independent of the normal ack path) - but that means it permanently sat
|
||
at the exact head, blocking ANY later entity's Place receipt from ever
|
||
being acknowledged (DiscardProgress only touches the correlation cache,
|
||
never `_pendingProjection` itself - a deliberate, narrow scope). Fixed by
|
||
splitting into two single-entity tests (`...ReapedByDiscardProgress`,
|
||
`...ReapedByDiscardAll`), each with its own fresh lifetime - eliminates
|
||
the exact-head contention entirely rather than working around it.
|
||
2. `BindLiveInputs_PlayerDistanceIsReadFromANonNullBoundSourceAndFallsBackToTheCallerStructWhenNull`
|
||
hit the SAME class of bug for the SAME reason (entity 1's completed drain
|
||
left an unacknowledged ExecutorCompleted blocking entity 2). Fixed by
|
||
inserting an explicit peek+acknowledge of entity 1's completion between
|
||
the two entities (matching the pattern already established in
|
||
`BindLiveInputs_DrivesClassificationFromTheBoundSourcesInsteadOfTheCallerStruct`
|
||
from the prior round).
|
||
|
||
## Fix-round final gates (all green)
|
||
1. `dotnet build src/AcDream.Runtime/AcDream.Runtime.csproj -c Release`: 0/0.
|
||
2. `dotnet build tests/AcDream.Runtime.Tests -c Release`: 0/0.
|
||
3. Complete `AcDream.Runtime.Tests`: 921/921 passed (916 + 5 new: 4 F2 + 1 F3;
|
||
see above for exact names).
|
||
4. `dotnet build tests/AcDream.App.Tests -c Release`: 0 errors, 3 pre-existing
|
||
warnings (CS8767, unrelated to this change).
|
||
5. `dotnet build tests/AcDream.Headless.Tests -c Release`: 0/0.
|
||
6. `dotnet test tests/AcDream.App.Tests -c Release --no-build`: 4028/4028
|
||
passed, 3 skips (4027 baseline + 1 new: the F1 App-sink test).
|
||
7. `dotnet test tests/AcDream.Headless.Tests -c Release --no-build`: 77/77
|
||
passed (76 baseline + 1 new: the F1 Headless-sink test).
|
||
8. `dotnet build AcDream.slnx -c Release --no-incremental` (clean rebuild for
|
||
an authoritative count): 0 errors, EXACTLY 21 warnings (matching the
|
||
documented baseline precisely - zero new warnings across the whole fix
|
||
round, including the 3 sanctioned sink-file edits).
|
||
9. Complete solution (`dotnet test AcDream.slnx -c Release -m:1`): every
|
||
project green - App 4028/3 skip, Bake 15, Cli 4, Content 124, Core.Net
|
||
762, Core 4242/1 skip, Headless 77, Runtime 921, UI.Abstractions 543. 0
|
||
failed anywhere.
|
||
10. `git diff --check`: clean (only pre-existing LF/CRLF metadata notices;
|
||
confirmed by grepping the raw output for anything OTHER than that
|
||
pattern - zero matches).
|
||
11. `git status`: exactly the C0 file set plus 5 NEW files from this fix
|
||
round (the 3 sanctioned sink files + their 2 test files), plus the 8
|
||
pre-existing protected dirty paths untouched. Nothing staged.
|
||
|
||
## Files touched in THIS fix round (in addition to the C0 file set above)
|
||
- src/AcDream.App/World/LiveEntityRuntime.cs (F1)
|
||
- src/AcDream.App/World/RuntimePlacementPresentationSink.cs (F1)
|
||
- src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs (F1)
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (F2: beforePublish
|
||
param + BindExecutorCompletionAcknowledgement + ack-branch notification)
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
(F2: ForgetCompletionReceipt/PendingCompletionReceiptCount/DiscardProgress+
|
||
DiscardAll reap/register-before-publish call site; F3: nullable field/
|
||
ResolveInputs)
|
||
- src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (F2: ownership
|
||
snapshot field + CaptureOwnership wiring + acknowledgement binding; F3:
|
||
BindLiveInputs signature; F4: comment)
|
||
- src/AcDream.Runtime/GameRuntime.cs (F3: nullable binding call site)
|
||
- src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs (F5a/F5b: comments)
|
||
- tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs (F1
|
||
test)
|
||
- tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs (F1 test)
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
(F2 x4 + F3 x1 tests)
|
||
|
||
---
|
||
|
||
# C2 — placement allocation budget (new implementer session)
|
||
|
||
Worktree: C:\Users\erikn\.codex\worktrees\af5e\acdream (branch codex/port-claude-agents)
|
||
HEAD verified at session start: 6460596b56cd72a2c6d96e757b33da879a805b6d
|
||
|
||
## Baseline reproduction
|
||
|
||
- `dotnet build AcDream.slnx -c Release` green, 21 pre-existing warnings in
|
||
unrelated files (not introduced by this session).
|
||
- Ran `WarmedImmediateCommitAllocationIsMeasuredBeforeProductionCutover`
|
||
(tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs:253).
|
||
Baseline via temporary forced-failure instrumentation (reverted before any
|
||
real edits; git diff was clean after revert): **2032 B/op**, stable across
|
||
3 repeat runs. NOT 1880 as the stale research doc
|
||
(docs/research/2026-07-31-canonical-set-position.md lines ~326-333)
|
||
states -- the number drifted upward since C0 landed additional
|
||
bookkeeping (ExecutorCompleted receipt plumbing etc). This is already
|
||
within 16 bytes of tripping the 2048 cap on its own -- confirms urgency.
|
||
|
||
## Bisection (temporary instrumentation, reverted before real edits)
|
||
|
||
Added static accumulator fields + GC.GetAllocatedBytesForCurrentThread()
|
||
brackets around: Apply() -> BeginAcceptedPlacementCore (BEGIN) and
|
||
SubmitPreparedPlacementCore (SUBMIT); inside SubmitPreparedPlacementCore:
|
||
prefix-to-SetPosition-call (PREFIX), the `_physics.Engine.SetPosition(...)`
|
||
call itself (SETPOSITION), CommitCanonical (COMMIT), PublishProjection
|
||
(PUBLISH), tail/Outcome (TAIL); AcknowledgeProjection (ACK, wrapped core in
|
||
try/finally).
|
||
|
||
Result (per-op, averaged over 1000 measured iterations, 64 warmups):
|
||
```
|
||
TOTAL=2032 BEGIN=752 SUBMIT=1160 SETPOSITION=584 COMMIT=0 PUBLISH=208 ACK=120
|
||
PREFIX=144 MID=0 TAIL=0
|
||
```
|
||
Outer brackets are self-consistent to the byte: BEGIN+SUBMIT+ACK =
|
||
752+1160+120 = 2032 = TOTAL exactly. The inner SUBMIT subdivision only sums
|
||
to 936 (144+584+0+208), leaving ~224 B/op I could not pin down further
|
||
within budget (possibly SortedDictionary rebalancing spillover attributed
|
||
oddly across adjacent brackets, possibly a measurement-granularity artifact
|
||
of many small brackets in one method -- the coarse brackets are trustworthy,
|
||
the finest subdivision is not). Decided not to chase further since the two
|
||
big, well-understood root causes below match the project's established
|
||
"pool the envelope, cache the delegate" pattern and account for the
|
||
majority of the budget.
|
||
|
||
## Root causes identified (confirmed by direct code reading + the
|
||
bisection above)
|
||
|
||
1. **BEGIN (752 B, `BeginAcceptedPlacementCore`)**: `new Operation { ... }`
|
||
(private sealed class Operation, ~25 properties incl. embedded
|
||
RuntimeSetPositionCommand / PhysicsSetPositionResult structs) allocated
|
||
FRESH on every accepted-placement call; the old Operation for the same
|
||
entity key is simply dropped (`_operations[key] = replacement;`) and
|
||
becomes garbage every single call.
|
||
|
||
2. **SETPOSITION (584 B, two call sites: SubmitPreparedPlacementCore
|
||
~line 2402 and RetryDeferred ~line 3536)**:
|
||
`report => _physics.HandleSetPositionCollisions(operation.Record, ...,
|
||
canonicalCommand.GameTime, ...)` is a closure capturing `this` +
|
||
`operation` (+ `canonicalCommand` at the first site) -- a fresh
|
||
compiler-generated display-class allocated on EVERY call. Confirmed
|
||
every field the closure reads is already reachable from `operation`
|
||
alone (`operation.Command.GameTime == canonicalCommand.GameTime` because
|
||
`operation.Command = canonicalCommand;` runs earlier in the same
|
||
method) -- the closure captures nothing that isn't already sitting on
|
||
`operation`. Fixable with ONE delegate cached for the RuntimeSetPositionState
|
||
instance's lifetime, reading the "current operation" off a small
|
||
reusable Stack<Operation> pushed/popped around the SetPosition call
|
||
(defends against theoretical re-entrant/nested SetPosition calls inside
|
||
PhysicsEngine -- TransitionScratchArena's ActiveDepth/Capacity gate
|
||
implies nesting is possible at that layer, even though
|
||
HandleSetPositionCollisions itself never calls back into
|
||
RuntimeSetPositionState).
|
||
|
||
## Plan
|
||
1. Fix the closure (lowest risk, no behavior change) -- both call sites.
|
||
2. Pool the Operation object (`_operationPool`, convert `required {get;init;}`
|
||
to settable + add a `Reset(...)`, return retired Operations to the pool
|
||
at every site that currently discards one for good). Audit every
|
||
`_operations.Remove(...)` / displacement site so nothing else still
|
||
holds the recycled instance (per "no workarounds": a pool that
|
||
resurrects stale state is worse than the allocation it replaces).
|
||
3. Re-measure; tighten the gate to the new number with justified headroom.
|
||
|
||
## Fixes implemented
|
||
|
||
1. Cached collision-report delegate (fixes the closure at both
|
||
`_physics.Engine.SetPosition` call sites: SubmitPreparedPlacementCore and
|
||
RetryDeferred). Added `CollisionCallbackContext` (readonly record struct)
|
||
plus `Stack<CollisionCallbackContext> _collisionCallbackContexts` plus
|
||
`Func<PhysicsSetPositionCollisionReport, bool>
|
||
_handleSetPositionCollisionsCallback` (built ONCE in the constructor,
|
||
bound to instance method `HandleSetPositionCollisionsCallback` which
|
||
reads `_collisionCallbackContexts.Peek()`). Each call site now does
|
||
Push(context) / try { SetPosition(request, cached delegate) } / finally
|
||
{ Pop() }. The stack (not a single field) defends nested/re-entrant
|
||
SetPosition calls at the PhysicsEngine layer.
|
||
|
||
2. Operation pooling (fixes `new Operation` in
|
||
`BeginAcceptedPlacementCore`). Converted every `Operation` property from
|
||
`required ... { get; init; }` to plain `{ get; set; }`, added
|
||
`ResetAllFieldsToDefault()`, added `_operationPool` (`Stack<Operation>`,
|
||
capped at 64), `RentOperation()` / `RetireOperationToPool(Operation)`.
|
||
`BeginAcceptedPlacementCore` now rents+field-sets instead of
|
||
`new Operation {...}`. Operations are retired at the 3 places an
|
||
Operation is permanently removed from `_operations`: both branches inside
|
||
`AcknowledgeProjection` Place/non-lost-cell paths, and inside
|
||
`CancelCoreDeferred` (the single removal chokepoint every `CancelCore`
|
||
overload funnels through).
|
||
|
||
CRITICAL BUG FOUND AND FIXED during verification: the first pass reset
|
||
fields at RETIREMENT time (inside RetireOperationToPool). This broke
|
||
`ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation` -
|
||
`CommitCanonical` remote branch builds a `ContactCommitGuard` that
|
||
captures the live `Operation`, then invokes `remote.HitGround()` /
|
||
`LeaveGround()` - and retail lets that callback synchronously call BACK
|
||
INTO `BeginAcceptedPlacement` for the SAME entity, which displaces and
|
||
retires the very operation the guard is holding, mid-guard.
|
||
`guard.IsCurrent()` (`IsCanonicalPlacementCommitCurrent`) reads that
|
||
operation ORIGINAL PositionAuthorityVersion/SpatialAuthorityVersion AFTER
|
||
the callback returns - resetting those fields at retirement time zeroed
|
||
them out from under the still-executing outer frame, making an in-flight
|
||
valid commit look stale and silently dropping its shadow update (test
|
||
caught it: shadow.Position stayed at the pre-spawn 10,20,7 instead of the
|
||
committed 13,18,~6.95). Fix: move the reset from RetireOperationToPool to
|
||
RentOperation (reset happens the moment an instance is about to be
|
||
handed out for reuse, not the moment it is taken out of `_operations`).
|
||
A retired-but-not-yet-rented instance now keeps its true last-known field
|
||
values until something actually reuses it - any outer frame with a
|
||
captured reference gets a brief, safe, read-only window on stale-but-
|
||
correct data instead of zeroed garbage. Full 921/921 Runtime tests pass
|
||
after this fix (before the fix: 920/921, this exact test failing).
|
||
|
||
Lesson worth carrying into memory: reentrant displaced-operation pooling
|
||
must reset at RENT time, never at RETIREMENT time, whenever a captured
|
||
reference (guard/closure/local) might still read the object fields after
|
||
retirement but before the next real use.
|
||
|
||
3. Eliminated LINQ `.First()` boxing on `_pendingProjection`
|
||
(SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>). Added
|
||
`FirstPendingProjection()` using a plain `foreach` (resolves to the
|
||
concrete struct-returning `GetEnumerator()`, not the boxing
|
||
`IEnumerable<T>` interface one that `Enumerable.First<T>`
|
||
forces). Replaced all 3 call sites (`TryPeekProjection`,
|
||
`AcknowledgeProjection` guard, `HasPendingProjectionThrough`).
|
||
|
||
## Root cause: SortedDictionary Add allocations (about 208 B/op) and
|
||
AcDream.Core PhysicsEngine internals (about 520 B/op) - NOT fixed
|
||
|
||
Confirmed via targeted diagnostic instrumentation (added, measured, fully
|
||
reverted before finalizing - git diff was clean after each revert):
|
||
- `_pendingProjection.Add(sequence, snapshot)` (PublishProjection) costs
|
||
about 208 B/op - SortedDictionary red-black tree node allocation, inherent
|
||
to the BCL type with no pooling hook. Replacing `_pendingProjection` data
|
||
structure to chase this would touch 10+ methods relying on its ordering
|
||
and FIFO-peek semantics (quiescence tracking, withdrawal acknowledgement,
|
||
etc.) - judged too invasive and risky for the remaining reward given the
|
||
result is already well under the 2048 cap.
|
||
- `_physics.Engine.SetPosition` (AcDream.Core/Physics/PhysicsEngine.cs)
|
||
costs about 520 B/op split as INIT=144 (InitializeSetPositionTransition),
|
||
INNER=344 (SetPositionInternal/scatter solve), FINAL=32 (the
|
||
`queryFootprint.OrderedIds.ToImmutableArray()` call - a genuine single-
|
||
element ImmutableArray materialization from the outdoor-adjustment query
|
||
footprint, not an artifact). RENT=0 (Transition pooling already zero-
|
||
alloc). This lives entirely inside AcDream.Core, shared physics
|
||
infrastructure used far beyond RuntimeSetPositionState - out of this
|
||
slice Runtime-only hard-rule scope, and not touched.
|
||
- Confirmed `HandleSetPositionCollisionReports` /
|
||
`RuntimeCollisionReportingState.HandleReports` (Runtime-side) do NOT
|
||
allocate in the test steady state (no collisions ever occur -
|
||
collidedObjectIds stays empty, no OwnerState ever gets created for this
|
||
entity) - ruled out as a contributor.
|
||
|
||
## Final verification
|
||
|
||
- `dotnet build AcDream.slnx -c Release`: 0 errors, 21 pre-existing warnings
|
||
(all in files untouched by this session - confirmed identical to the
|
||
pre-session baseline build).
|
||
- `WarmedImmediateCommitAllocationIsMeasuredBeforeProductionCutover`: passes
|
||
at the new `Assert.InRange(allocated / iterations, 1L, 1_536L)` gate.
|
||
Measured value stable at exactly 944 B/op across 5+ repeat runs (down
|
||
from 2032 B/op baseline, 53.5% reduction). 1,536 keeps about 60% headroom.
|
||
- Complete `AcDream.Runtime.Tests`: 921/921 pass (0 skips).
|
||
- Complete solution (`dotnet test AcDream.slnx -c Release -m:1`): every
|
||
project green - App 4028/3 skip, Bake 15, Cli 4, Content 124, Core.Net
|
||
762, Core 4242/1 skip, Headless 77, Runtime 921, UI.Abstractions 543.
|
||
10,716 total, 0 failed, 4 skipped (all pre-existing skips).
|
||
- `git diff --check`: clean (only the same pre-existing LF/CRLF metadata
|
||
notices on the 8 protected dirty files from before this session; grepped
|
||
for anything else - zero matches).
|
||
- `git status`: exactly 2 files changed by this session
|
||
(src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs,
|
||
tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs) plus
|
||
the 8 pre-existing protected dirty paths, untouched, exactly as they were
|
||
at session start. Nothing staged, nothing committed, HEAD unchanged at
|
||
6460596b56cd72a2c6d96e757b33da879a805b6d.
|
||
|
||
## Files touched this session
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (net +340/-69 lines
|
||
vs the C0 baseline: Operation class made poolable plus
|
||
ResetAllFieldsToDefault, CollisionCallbackContext plus cached delegate
|
||
plus stack, RentOperation/RetireOperationToPool, FirstPendingProjection,
|
||
BeginAcceptedPlacementCore restructure, both SetPosition call sites, both
|
||
AcknowledgeProjection removal branches, CancelCoreDeferred retirement
|
||
call site)
|
||
- tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
|
||
(26 lines: only the regression gate comment and threshold, from 2_048L to
|
||
1_536L - zero other test changes; the fix required no test edits beyond
|
||
the gate itself, confirming the pooling/delegate/LINQ changes are fully
|
||
behavior-preserving)
|
||
|
||
# C2 review-fix round (F1-F4)
|
||
|
||
Both independent reviews FAILED the original C2 landing with four findings.
|
||
Fixed all four; re-verified B/op unchanged (944, same as before this round)
|
||
and the complete Runtime + solution suites green.
|
||
|
||
## F1 (MAJOR, retail) - CommitCanonical post-callback reads/writes
|
||
|
||
Root cause: CommitCanonical read operation.PositionAuthorityVersion/
|
||
SpatialAuthorityVersion/Command.GameTime/PreviousContact/PreviousOnWalkable/
|
||
Key/Command.ShadowWorldOffsetX/Y AFTER invoking the ground-edge HitGround/
|
||
LeaveGround callbacks (via PhysicsObjUpdate.CommitSetPositionContactTransition).
|
||
A synchronous cancel-then-begin (or begin-twice) chain for the SAME entity
|
||
retires-then-rents (LIFO) the SAME Operation instance mid-callback, so those
|
||
post-callback reads could observe a reset-or-repurposed operation.
|
||
|
||
Fix: hoisted every scalar CommitCanonical still needs into locals BEFORE the
|
||
callback (operationKey, positionAuthorityVersion, spatialAuthorityVersion,
|
||
sourceVelocityAuthorityVersion, commandGameTime, previousContact,
|
||
previousOnWalkable, shadowWorldOffsetX/Y). ContactCommitGuard now captures
|
||
positionAuthorityVersion/spatialAuthorityVersion as plain values instead of
|
||
holding an Operation reference. IsCanonicalPlacementCommitCurrent's Operation
|
||
parameter was replaced with the two explicit scalar parameters.
|
||
|
||
FALSE START (caught by full-suite regression, not left in): first pass ALSO
|
||
added an operationToken identity comparison inside
|
||
IsCanonicalPlacementCommitCurrent, intending to detect the exact repurposing.
|
||
This broke two EXISTING tests
|
||
(ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation and,
|
||
after fixing that, exposed a status regression in the same test) because
|
||
retail's OWN contract is that an in-flight ground-edge commit for a
|
||
DISPLACED operation must still complete its physical settle (contact
|
||
transition + shadow sync) - adding identity-based rejection there
|
||
incorrectly aborted a commit the test explicitly requires to succeed.
|
||
Reverted the token check from IsCanonicalPlacementCommitCurrent entirely
|
||
(doc comment there now explains why identity must NOT be checked at that
|
||
layer). The REAL self-aliasing hazard was two levels up: `BeginAcceptedPlacementCore`'s
|
||
final `IsCurrent(replacement)` check (after PublishPlacement can reentrantly
|
||
retire+rent the SAME `replacement` instance for an inner Begin) and
|
||
`SubmitPreparedPlacementCore`/`RetryDeferred`'s post-CommitCanonical decision
|
||
of Cancelled-vs-Committed (CommitCanonical can legitimately succeed for a
|
||
displaced operation - the OUTER caller's own invocation still must not
|
||
publish a Place projection nobody will ever acknowledge). Fixed both by
|
||
comparing a LOCAL, pre-reentrancy token/parameter (`token` already a
|
||
parameter in SubmitPreparedPlacementCore; hoisted `operationToken` added to
|
||
RetryDeferred; the `token` local already existed in
|
||
BeginAcceptedPlacementCore) against a FRESH `_operations` lookup - never
|
||
against the potentially-repurposed operation reference's own fields.
|
||
|
||
Regression test: ReentrantCancelThenBeginRecyclesInstanceButCollisionReportUsesPreCallbackValues.
|
||
Drives Cancel(publishWithdrawal:false) then BeginAcceptedPlacement from
|
||
inside OnHitGround - LIFO pool guarantees the SAME instance is retired then
|
||
immediately rented for the new ("recycled") operation, a strictly more
|
||
adversarial recycle than the pre-existing test. Asserts (via
|
||
CollisionReportObserver subscribed to CollisionReports) that the
|
||
environment-collision report's RecipientWasInContact reflects the ORIGINAL
|
||
pre-callback PreviousContact, and that the report fires at all (proving
|
||
PreviousOnWalkable also carried through correctly - the report only fires
|
||
when !previousOnWalkable && body.OnWalkable, so a corrupted
|
||
PreviousOnWalkable would silently suppress it). Verified DISCRIMINATING: reverted
|
||
the hoist locally, confirmed the test fails (empty report collection), then
|
||
restored the fix.
|
||
|
||
## F2 (MAJOR arch + MINOR retail) - pool invisible to reset/dispose ledger
|
||
|
||
_operationPool was never cleared by ClearOwnedState (called from both
|
||
ResetSession and Dispose), so up to 64 pooled instances could retain full
|
||
previous-generation entity graphs across a session boundary. Added
|
||
`_operationPool.Clear()` inside ClearOwnedState (comment explains why this
|
||
is safe unlike RetireOperationToPool: a session clear cannot be reentered
|
||
from inside itself). Added `PooledOperationCount` to
|
||
RuntimeSetPositionOwnershipSnapshot (new trailing field, single
|
||
construction site updated), deliberately EXCLUDED from IsConverged (doc
|
||
comment explains pooled idle capacity is legitimate mid-session).
|
||
|
||
Regression tests: OperationPoolClearsOnResetSession,
|
||
OperationPoolClearsOnDispose - both drive an Apply+Acknowledge cycle to get
|
||
>=1 pooled operation, assert PooledOperationCount >= 1, then call
|
||
ResetSession/Dispose and assert it drops to exactly 0. Verified
|
||
DISCRIMINATING: commented out the `_operationPool.Clear()` line, confirmed
|
||
both tests fail (1 instead of 0), restored the fix.
|
||
|
||
## F3 (MINOR arch) - no-self-aliasing invariant + InPool guard
|
||
|
||
Reordered BeginAcceptedPlacementCore: RentOperation() now happens AFTER the
|
||
displaced operation's CancelCoreDeferred retire (previously rent happened
|
||
first). This lets a single-entity churn cycle legitimately reuse the exact
|
||
retired instance (LIFO) instead of drawing a different one - safe because
|
||
every field this method needs from `displaced` was already captured into
|
||
locals before the retire point (unchanged from the original C2 landing).
|
||
Added `Operation.InPool` (bool, defaults false): set true in
|
||
RetireOperationToPool right before pushing, cleared in
|
||
ResetAllFieldsToDefault (called from RentOperation right after popping).
|
||
RetireOperationToPool now throws InvalidOperationException if called on an
|
||
instance that is already InPool (double-retire without an intervening rent
|
||
would silently duplicate the instance in the pool stack).
|
||
|
||
This reorder, on its own, reintroduced a DIFFERENT self-aliasing hazard at
|
||
BeginAcceptedPlacementCore's own final line (`IsCurrent(replacement) ? token
|
||
: default`) - caught by the EXISTING test
|
||
ReentrantBeginDuringDiscardCannotBeOverwrittenByOuterBegin (a
|
||
PublishPlacement-triggered reentrant Begin during a Discard notification can
|
||
retire+rent the SAME `replacement` instance for an inner operation, making
|
||
`IsCurrent(replacement)` a self-referential tautology that wrongly reports
|
||
"still current"). Fixed by comparing `_operations[key].Token` against the
|
||
LOCAL `token` (captured at function entry, immune to reentrant corruption)
|
||
instead of calling `IsCurrent(replacement)`.
|
||
|
||
No new dedicated F3 test beyond the reflection test (F4) and the
|
||
pre-existing ReentrantBeginDuringDiscardCannotBeOverwrittenByOuterBegin,
|
||
which now exercises the corrected self-aliasing check directly.
|
||
|
||
## F4 (MINOR arch) - two remaining new Operation() sites + completeness net
|
||
|
||
Converted the two surviving `new Operation { ... }` object-initializer
|
||
sites (inside ParkCollisionResidents and CreateWithdrawalOperation) to
|
||
RentOperation() + field assignment, so all construction flows one path.
|
||
Both call sites confirmed safe to route through the pool (no live displaced
|
||
operation exists at either construction point - ParkCollisionResidents
|
||
explicitly skips keys already in `_operations`; CreateWithdrawalOperation's
|
||
sole caller, Cancel, always runs CancelCoreDeferred against the same key
|
||
immediately before).
|
||
|
||
Added OperationResetAllFieldsToDefaultTouchesEveryDeclaredField: a
|
||
reflection-based test (Operation is `private`, so ONLY reflection can reach
|
||
it from a test project even with InternalsVisibleTo) comparing
|
||
`typeof(Operation).GetFields(Instance|NonPublic|Public)` against a
|
||
hardcoded, maintained list of the 33 expected backing-field names (derived
|
||
from a plain property-name list transformed to `<Name>k__BackingField`
|
||
form). Verified DISCRIMINATING: added a temporary dummy property to
|
||
Operation, confirmed the test fails with a clear collection-diff showing
|
||
the new backing field, removed it.
|
||
|
||
## Final verification
|
||
|
||
- Release build: 0 errors, 21 pre-existing warnings (unchanged from
|
||
baseline, zero new).
|
||
- WarmedImmediateCommitAllocationIsMeasuredBeforeProductionCutover: still
|
||
passes at the 1,536L gate. Re-measured exact value 3x: 944 B/op, IDENTICAL
|
||
to before this fix round - confirms every F1-F4 hoist/guard is
|
||
stack-only/negligible (one extra bool field, one extra int in a value-type
|
||
snapshot struct - no heap allocation added).
|
||
- Complete AcDream.Runtime.Tests: 925/925 (921 + 4 new: 1 F1 regression + 2
|
||
F2 reset/dispose + 1 F4 reflection).
|
||
- Complete solution (dotnet test AcDream.slnx -c Release -m:1): every
|
||
project green - App 4028/3 skip, Bake 15, Cli 4, Content 124, Core.Net
|
||
762, Core 4242/1 skip, Headless 77, Runtime 925, UI.Abstractions 543.
|
||
10,720 total, 0 failed.
|
||
- git diff --check: clean (same pre-existing LF/CRLF metadata notices on the
|
||
now-9 touched-by-someone files - the 8 pre-existing protected dirty paths
|
||
plus RuntimeSetPositionState.cs itself; RuntimeSetPositionStateTests.cs
|
||
shows no notice at all).
|
||
- git status: still exactly the 2 files this session owns
|
||
(RuntimeSetPositionState.cs, RuntimeSetPositionStateTests.cs) plus the 8
|
||
pre-existing protected dirty paths untouched. Nothing staged, nothing
|
||
committed, HEAD unchanged at 6460596b56cd72a2c6d96e757b33da879a805b6d.
|
||
|
||
## Files touched this fix round
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (net diff vs C2
|
||
landing: 800 lines changed - Operation.InPool + doc, ResetAllFieldsToDefault
|
||
InPool line, CommitCanonical hoisting rewrite, ContactCommitGuard/
|
||
IsCanonicalPlacementCommitCurrent signature change, IsVelocityCurrent
|
||
overload, BeginAcceptedPlacementCore reorder + final-check fix,
|
||
SubmitPreparedPlacementCore + RetryDeferred post-commit currency checks,
|
||
RuntimeSetPositionOwnershipSnapshot.PooledOperationCount +
|
||
ClearOwnedState pool clear, ParkCollisionResidents + CreateWithdrawalOperation
|
||
routed through RentOperation)
|
||
- tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs (net
|
||
diff: 255 lines - 4 new tests + System.Reflection using)
|
||
|
||
# C2 round 3 (rent-after-retire regression) + addendum (A1/A2)
|
||
|
||
The F3 reorder (rent AFTER retire, so a single-entity churn cycle reuses
|
||
the exact retired instance) makes `IsCurrent(Operation)` (ReferenceEquals +
|
||
six field comparisons read off the SAME instance) a tautology once a
|
||
reentrant cancel-then-begin recycles that instance for a different logical
|
||
operation at the same key. Every one of the ~20 `IsCurrent(operation)` call
|
||
sites needed auditing: either convert to a captured-token-vs-fresh-lookup
|
||
check, or prove (with a per-site comment) that no reentrancy point
|
||
intervenes between the last fresh lookup and the check.
|
||
|
||
## The fix
|
||
|
||
Extracted `IsOperationStateConsistent(Operation)` from `IsCurrent`'s six
|
||
non-identity field comparisons. `IsCurrent(Operation)` is now
|
||
`_operations.TryGetValue(operation.Key, out current) &&
|
||
ReferenceEquals(current, operation) && IsOperationStateConsistent(operation)`
|
||
- same behavior as before, just factored so the new helper below can share
|
||
the state check. Added:
|
||
|
||
```
|
||
private bool IsCurrentByToken(
|
||
RuntimeEntityKey key,
|
||
in RuntimeEntityPlacementToken capturedToken,
|
||
[NotNullWhen(true)] out Operation? operation)
|
||
```
|
||
|
||
Does a FRESH `_operations.TryGetValue` + `current.Token == capturedToken` +
|
||
`IsOperationStateConsistent(current)`, and hands back the fresh (possibly
|
||
different-instance) `Operation` on success. `[NotNullWhen(true)]` lets
|
||
callers reuse a single `out` binding across an `if (!IsCurrentByToken(...))
|
||
return ...;` guard without a separate null-forgiving cast.
|
||
|
||
`CancelCore(Operation expected, ...)` (ReferenceEquals shape - the OTHER
|
||
regression source the reviewer named, "shares the shape with a worse
|
||
outcome: cancelling the newer operation") became
|
||
`CancelCore(RuntimeEntityKey key, in RuntimeEntityPlacementToken
|
||
expectedToken, bool preserveLostFamily = false)`: fresh
|
||
`_operations.TryGetValue(key, ...)` + `current.Token != expectedToken` ->
|
||
no-op. All 5 callers converted (ForgetExactPlacement,
|
||
RetireDormantLocalActivation, RetireDormantLocalActivationToken,
|
||
SubmitPreparedPlacementCore's post-CommitCanonical-failure branch,
|
||
RetryDeferred's equivalent) - each now passes its own captured
|
||
`(key, token)` instead of an `Operation` reference.
|
||
|
||
## The exact bug the reviewer traced (Cancel path) - FIXED
|
||
|
||
`Cancel(record, bool)` creates a withdrawal operation, installs it at the
|
||
key, then calls `PublishPlacement(cancelledOld)` (reentrancy point: an
|
||
`IRuntimePlacementObserver` subscriber can call Begin/Cancel for the same
|
||
entity from inside this synchronous dispatch), then previously checked
|
||
`IsCurrent(operation)` against the now-possibly-recycled reference. Fixed:
|
||
capture `operation.Token` into a local BEFORE `PublishPlacement`, then
|
||
`IsCurrentByToken(key, capturedToken, out operation)` afterward.
|
||
|
||
## Subtler hazard found during the audit, not named by the reviewer:
|
||
## RebindQuiescedDeferredOperations
|
||
|
||
`foreach (Operation operation in _operations.Values.ToArray())` snapshots
|
||
live REFERENCES. An earlier iteration's `RetryDeferred` call (itself
|
||
reentrancy-exposed) can recycle a LATER iteration's not-yet-reached
|
||
Operation instance before the loop reaches it - a stale-reference bug
|
||
independent of the Cancel/CancelCore ones. Fixed by snapshotting
|
||
`(Key, Token)` VALUE pairs first, then resolving fresh via
|
||
`IsCurrentByToken` at the top of each iteration instead of trusting the
|
||
snapshotted reference.
|
||
|
||
## Addendum A1 (retail MINOR) - CommitCanonical's 4 post-callback writes
|
||
|
||
`CommitCanonical`'s tail (`operation.ExactCellId/Result/WakeableLostCell/
|
||
EnteringWorldFromCelllessResidence = ...; CancelLostFamilyDeadlines(operation)`)
|
||
still targeted the poolable instance directly. With rent-after-retire, a
|
||
nested cancel-then-begin recycles the instance before this block runs, and
|
||
a bare Begin never advances PlacementCommitVersion - invisible to the
|
||
settle-layer record-state checks - so the writes could land on the WRONG
|
||
(freshly-begun) operation. Fixed: hoisted `operationToken = operation.Token`
|
||
(already had `operationKey` from F1) at the SAME pre-callback point as
|
||
every other F1 hoist, then gated the whole write block behind a fresh
|
||
`_operations.TryGetValue(operationKey, out currentOperation) &&
|
||
currentOperation.Token == operationToken` check - skip the writes (not the
|
||
whole commit) if the token no longer matches, matching retail's
|
||
already-unconditional physical settle (only Runtime's OWN bookkeeping is
|
||
conditional). The final `IsCanonicalPlacementCommitCurrent(...,
|
||
requireSpatialRoot: true)` return is UNCHANGED - still identity-agnostic,
|
||
per the layer-separation rule below.
|
||
|
||
## Addendum A2 (doc hygiene) - stale comment on RetireOperationToPool
|
||
|
||
The old comment claimed `IsCanonicalPlacementCommitCurrent` "additionally
|
||
compares the operation's Token by value" - that guard was a round-3 FALSE
|
||
START (see the F1 section above) that was reverted before F1 even landed;
|
||
the comment was never updated and contradicted the real mechanism.
|
||
Rewritten to state the true safety contract: hoisted locals (F1) +
|
||
call-site captured-token-vs-fresh-lookup (this round), with the settle
|
||
path deliberately identity-agnostic per retail's unconditional
|
||
SetPositionInternal completion.
|
||
|
||
## Layer separation (unchanged, reaffirmed by the retail reviewer)
|
||
|
||
`IsCanonicalPlacementCommitCurrent` takes NO identity/Token parameter, by
|
||
design - retail's SetPositionInternal settle completes unconditionally for
|
||
a displaced operation (see
|
||
ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation, from the
|
||
F1 false-start above). Identity gates belong ONLY at Runtime-owned
|
||
publication/cancellation/ownership decisions (SubmitPreparedPlacementCore's
|
||
Cancelled-vs-Committed decision, Cancel's withdrawal publish, CancelCore's
|
||
match check, CommitCanonical's A1 bookkeeping-write gate) - never at the
|
||
settle/currency check itself. Did not touch this layer in round 3 beyond
|
||
re-confirming it via the reverted false-start being re-tried and failing
|
||
the same way it did in round 2 (not re-attempted this round - the round-2
|
||
false-start's lesson was already incorporated).
|
||
|
||
## Per-site audit table (all sites this class calls
|
||
`IsCurrent(Operation)`/`ReferenceEquals` on an Operation across a
|
||
reentrancy-exposed span)
|
||
|
||
CONVERTED to captured-token-vs-fresh-lookup (IsCurrentByToken or
|
||
CancelCore(key, token)):
|
||
1. `Cancel(record, bool)` post-`PublishPlacement(cancelledOld)` check - the
|
||
reviewer's named bug.
|
||
2. `SubmitPreparedPlacementCore` post-`_physics.Engine.SetPosition(...)`
|
||
check (first of two, immediately after the collision-callback-bearing
|
||
call).
|
||
3. `SubmitPreparedPlacementCore` post-`SetPosition` second check (after the
|
||
`TryGetBlockingQuiescence`/deferred branch, before `CommitCanonical`).
|
||
4. `SubmitPreparedPlacementCore`'s `CancelCore(token.Entity, token)` call on
|
||
`CommitCanonical` failure - the reviewer's named CancelCore-shape bug.
|
||
5. `RetryDeferred`'s two analogous post-`SetPosition` checks (same shape as
|
||
#2/#3, via hoisted `operationToken`).
|
||
6. `RetryDeferred`'s `CancelCore(operationToken.Entity, operationToken)`
|
||
call on `CommitCanonical` failure (same shape as #4).
|
||
7. `RebindQuiescedDeferredOperations`'s per-entry resolve - converted from a
|
||
snapshotted-REFERENCE loop to a snapshotted-(Key,Token) loop + fresh
|
||
`IsCurrentByToken` (the subtler hazard found during the audit, not named
|
||
by the reviewer).
|
||
8. `ForgetExactPlacement`'s `CancelCore(token.Entity, token)` call.
|
||
9. `RetireDormantLocalActivation`'s `CancelCore` call.
|
||
10. `RetireDormantLocalActivationToken`'s `CancelCore` call.
|
||
11. `CommitCanonical`'s A1 bookkeeping-write gate (fresh lookup +
|
||
`currentOperation.Token == operationToken` before the
|
||
ExactCellId/Result/WakeableLostCell/EnteringWorldFromCelllessResidence/
|
||
CancelLostFamilyDeadlines writes) - addendum A1.
|
||
|
||
PROVEN SAFE WITH COMMENT (fresh lookup + Token/state check on the same line
|
||
or immediately prior, nothing reentrant intervenes before the check runs):
|
||
12. `IsPlacementCurrent` - fresh lookup + Token check inline, no
|
||
reentrancy between them.
|
||
13. `PrepareDormantLocalActivationOwnership` - fresh lookup + Token check
|
||
earlier in the same method, dormant-family code with no production
|
||
callers.
|
||
14. `PrepareMover` - same shape as #13.
|
||
15. `IsExactPreparedPlacementCurrent` - same shape.
|
||
16. `TryEvaluateDormantLocalActivation` - ReferenceEquals variant, safe
|
||
because `handleCollisions: null` on this call means nothing reentrant
|
||
can run before the check.
|
||
17. `IsDormantLocalActivationPrephaseCurrent` - fresh lookup + Token check
|
||
earlier in the same method.
|
||
18. `IsDormantLocalActivationResponseCurrent` - same shape.
|
||
19. `CommitDormantLocalActivationPostCollision` - entry AND final-return
|
||
checks, one comment block covering both; dormant family, no production
|
||
callers.
|
||
20. `IsDormantLocalActivationCommitCurrent` - same shape as #17/#18.
|
||
21. `IsExactDormantLocalActivationCurrent` - same shape.
|
||
22. `SubmitPreparedPlacementCore`'s own entry `ownsToken` check - the
|
||
function's own validation, nothing reentrant between the fresh lookup
|
||
and this check.
|
||
23. `AcknowledgeProjection`'s entry check - fresh lookup +
|
||
ProjectionSequence check, no reentrancy in between.
|
||
24. `CommitCanonical`'s entry check (`!result.IsCommitted ||
|
||
!IsCurrent(operation)`) - every caller (SubmitPreparedPlacementCore,
|
||
RetryDeferred) passes an operation freshly re-verified immediately
|
||
before calling CommitCanonical.
|
||
25. `CommitCollisionGeneration`'s per-entity loop - added during THIS pass
|
||
(not flagged by the reviewer, found while double-checking every
|
||
`_operations.TryGetValue` site in the file for completeness). Iterates
|
||
an array of KEYS (never Operation references), fresh lookup + full
|
||
WakeableLostCell/ExactCellId/CollisionPrefix/CollisionGeneration shape
|
||
check every iteration - immune to the snapshotted-reference class of
|
||
bug even though it calls the reentrancy-exposed `RetryDeferred`
|
||
per-entity.
|
||
|
||
OUT OF SCOPE (different class entirely, not an Operation-identity site):
|
||
26. `ParkCollisionResidentsForQuiescence`'s `ReferenceEquals(current,
|
||
state)` - checks `CollisionPrefixQuiescence` identity (a class that is
|
||
never pooled), unrelated to the Operation pool this round's regression
|
||
lives in.
|
||
|
||
## Round 3 tests
|
||
|
||
`ReentrantBeginDuringCancelPublishCannotBeOverwrittenByOuterWithdraw` -
|
||
the reviewer's named Cancel-path scenario: Apply (pending Place
|
||
projection) -> observer reentrantly Begins on the Discard notification
|
||
during Cancel's PublishPlacement -> asserts only the Discard delta
|
||
published (a stale-reference bug would add a second Withdraw delta
|
||
stamped with the inner operation's state) and the inner token is still
|
||
`IsPlacementCurrent`. VERIFIED DISCRIMINATING: reverted Cancel's
|
||
`IsCurrentByToken` check back to `IsCurrent(operation)`, reran - failed
|
||
exactly as predicted (`Assert.Single` saw 2 deltas, the second a Withdraw
|
||
carrying the inner operation's PlacementCommitVersion=2/Sequence=2) -
|
||
restored the fix.
|
||
|
||
`ReentrantCancelThenBeginDuringCommitFailureLeavesInnerOperationUncancelled`
|
||
- the CancelCore-shape scenario: ground-edge HitGround callback does
|
||
cancel-then-begin (recycling the instance for `inner`) AND calls
|
||
`lifetime.Entities.AdvancePlacementCommit(record)` a second time BEFORE
|
||
creating `inner` (so `inner` snapshots the already-advanced value and
|
||
stays internally self-consistent, while the OUTER commit's
|
||
`canonicalCommitVersion`, captured before the callback, now mismatches) -
|
||
this makes `CommitCanonical`'s post-callback `IsCanonicalPlacementCommitCurrent`
|
||
check fail for the outer commit without needing a full nested SetPosition
|
||
round-trip, driving `SubmitPreparedPlacementCore` into
|
||
`PublishCancellation(CancelCore(token.Entity, token))`. Asserts
|
||
`outcome.Status == Cancelled` and `inner` is still `IsPlacementCurrent`
|
||
afterward. VERIFIED DISCRIMINATING: reverted `CancelCore(key, token)`'s
|
||
Token check to accept any match by key alone (simulating the old
|
||
ReferenceEquals-without-identity shape), reran - failed exactly as
|
||
predicted (`IsPlacementCurrent(inner)` false, the sabotaged check retired
|
||
`inner`'s instance out from under it) - restored the fix.
|
||
|
||
## Final verification (round 3 + addendum)
|
||
|
||
- Release build (`dotnet build AcDream.slnx -c Release`): 0 errors, 21
|
||
pre-existing warnings, all in test files this session did not touch (App/
|
||
Core test projects) - unchanged from the F1-F4 round.
|
||
- `WarmedImmediateCommitAllocationIsMeasuredBeforeProductionCutover`:
|
||
passes at the existing 1,536L gate - token captures added this round are
|
||
all stack-only locals/struct fields, no new heap allocation.
|
||
- Complete AcDream.Runtime.Tests: 927/927 (925 F1-F4 baseline + 2 new: the
|
||
Cancel-path and CancelCore-shape regressions).
|
||
- Complete solution build (`dotnet build AcDream.slnx -c Release`): 0
|
||
errors.
|
||
- `git diff --check`: exit 0, clean (same pre-existing LF/CRLF metadata
|
||
notices on the same pre-existing dirty files, RuntimeSetPositionState.cs
|
||
included - no whitespace-error content).
|
||
- `git status`/`git rev-parse HEAD`: still exactly the 2 files this session
|
||
owns (RuntimeSetPositionState.cs, RuntimeSetPositionStateTests.cs) plus
|
||
the same pre-existing dirty paths (AGENTS.md,
|
||
PlayerModeController.cs, PlayerInteractionMovementSink.cs,
|
||
LiveAnimationPresentationContext.cs, RuntimeRemotePhysicsUpdater.cs,
|
||
CellTransitTests.cs, Issue133DungeonTeleportPrefixTests.cs,
|
||
A8CellAudit.csproj) untouched by this session. Nothing staged, nothing
|
||
committed, HEAD unchanged at 6460596b56cd72a2c6d96e757b33da879a805b6d.
|
||
|
||
## Files touched this round
|
||
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs (IsCurrent split
|
||
into IsCurrent + IsOperationStateConsistent, new IsCurrentByToken helper,
|
||
CancelCore(Operation) -> CancelCore(key, token) signature change + 5
|
||
caller conversions, Cancel/SubmitPreparedPlacementCore/RetryDeferred/
|
||
RebindQuiescedDeferredOperations converted call sites,
|
||
CommitCanonical's A1 bookkeeping-write token gate, ~14 proven-safe-site
|
||
comments, RetireOperationToPool doc-comment rewrite (A2),
|
||
CommitCollisionGeneration audit comment)
|
||
- tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs (2
|
||
new tests: ReentrantBeginDuringCancelPublishCannotBeOverwrittenByOuterWithdraw,
|
||
ReentrantCancelThenBeginDuringCommitFailureLeavesInnerOperationUncancelled)
|
||
|
||
---
|
||
|
||
# C3 implementer session (2026-08-02) — spawn-frequency host cutover
|
||
|
||
Worktree/branch/HEAD verified against the pinned contract before starting:
|
||
a32aba35d1d945b9d3194a84e70facf74a7d7608. Read in full: c3-contract.md,
|
||
docs/plans/2026-08-02-placement-cutover.md, docs/research/
|
||
2026-08-02-cutover-route-inventory.md (routes 1+8 + all cross-cutting
|
||
sections), docs/research/2026-08-02-canonical-body-writer-map.md, docs/
|
||
research/2026-08-02-runtime-continuation-executor-handoff.md.
|
||
|
||
## C3-1 — DONE, tested, gated. (Runtime-only prerequisite a)
|
||
|
||
Design: rather than widen any internal enum's accessibility (the contract's
|
||
explicit preference), added a small PUBLIC projection surface next to the
|
||
existing public placement-receipt types (`RuntimePlacementProjectionKind`/
|
||
`Token`/`Snapshot` are already public; the executor's own receipt/trace types
|
||
are `internal`):
|
||
|
||
- `RuntimeInitialCreateTeleportHookPhase`, `RuntimeInitialCreatePositionDisposition`,
|
||
`RuntimeInitialCreatePositionConstrainPhase` — public 1:1 projections of the
|
||
internal `RuntimeTeleportHookPhase`/`RuntimeAuthoritativePositionDisposition`/
|
||
`RuntimePositionConstrainPhase` enums (`RuntimeAuthoritativePositionRouteClassifier.cs:38,49,63`).
|
||
- `RuntimeInitialCreatePositionRouteFact` — one Position continuation's route
|
||
facts (Sequence/Disposition/HookPhase/ConstrainPhase/StopInterpolating/
|
||
ZeroVelocity/PreserveHeading/SendPositionImmediately), projected from
|
||
`RuntimeInitialCreateExecutedAction` trace entries where
|
||
`Kind == Position` only (every other action kind stays internal-only —
|
||
widening the full action-kind vocabulary was explicitly what the contract
|
||
said to avoid).
|
||
- `RuntimeInitialCreatePlacementCompletion` — the top-level public shape
|
||
(Entity, FullCellId, TeleportHookPhase, PositionRouteFacts,
|
||
ReplayedDeferredChildCount), built ONCE by a new private
|
||
`RuntimeInitialCreateContinuationExecutor.ProjectCompletion` at the exact
|
||
completion site (`ExecuteCore`'s `Released` case, where `completedReceipt`
|
||
is built) and cached in `_completionReceipts`'s tuple (extended from
|
||
`(Sequence, Receipt)` to `(Sequence, Receipt, Public)`) — so a host polling/
|
||
retrying `TryGetInitialCreateCompletion` never re-allocates
|
||
(the "allocation-conscious" requirement).
|
||
- New internal `RuntimeInitialCreateContinuationExecutor.TryGetCompletion`
|
||
reads the cached projection by exact token identity (same Entity/Sequence
|
||
correlation rule as the existing `TryGetCompletionReceipt`).
|
||
- New PUBLIC `RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion(
|
||
RuntimeGenerationToken, in RuntimePlacementProjectionToken, out
|
||
RuntimeInitialCreatePlacementCompletion)` — generation-gated like every
|
||
other channel method, thin passthrough to the executor. The channel now
|
||
takes the executor as a 3rd internal ctor parameter; all 3
|
||
`RuntimeEntityObjectLifetime` constructors updated to pass
|
||
`InitialCreateExecution`.
|
||
|
||
Files touched:
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
(+219/-0 net insertions: new public types, ProjectCompletion + 3 enum
|
||
mappers, TryGetCompletion, _completionReceipts tuple widened, completion
|
||
site wired).
|
||
- src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs (+32/-1:
|
||
new ctor param + field, TryGetInitialCreateCompletion).
|
||
- src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs (+9/-6: all 3
|
||
ctors pass InitialCreateExecution into the channel).
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
(+178: 4 new tests — ProjectsHookPhaseCellAndReplayCount [local-player
|
||
login correctly reports AfterEnterWorld hook phase, empty route facts],
|
||
ProjectsPositionRouteFactsForConstrainInterpolationBinding [teleport-
|
||
advanced continuation's route facts round-trip through the public
|
||
projection], RejectsWrongGeneration, ReturnsFalseAfterAcknowledgeReapsTheCorrelationEntry).
|
||
|
||
Gates run for C3-1: focused Residence|Classifier|Executor|SetPositionState|
|
||
PlacementProjectionChannel filter 237/237; complete AcDream.Runtime.Tests
|
||
931/931 (927 baseline + 4 new); Release build of the full solution 0
|
||
errors/21 pre-existing warnings; complete solution
|
||
`dotnet test AcDream.slnx -c Release -m:1` with
|
||
`ACDREAM_PAK_PATH=/c/Users/erikn/Documents/Asheron's Call/acdream.pak` —
|
||
every project green (App 4028/3 skips, Bake 15/0, Cli 4/0, Content 124/0,
|
||
Core.Net 762/0, Core 4242/1 skip, Headless 77/0, Runtime 931/0,
|
||
UI.Abstractions 543/0 — zero failures anywhere). `git diff --check` clean
|
||
(only the same pre-existing LF/CRLF notices on pre-existing dirty files).
|
||
Nothing staged, nothing committed. `git status` confirms only the 4 files
|
||
above are newly dirty beyond the 8 pre-existing protected paths (which I
|
||
did not touch — I read PlayerModeController.cs for C3-2 investigation but
|
||
made ZERO edits to it).
|
||
|
||
## C3-2/C3-3/C3-4 — NOT implemented this session. Stopped with evidence
|
||
## after investigation surfaced a materially larger scope than the four
|
||
## input docs describe. Full findings below for whoever picks this up.
|
||
|
||
I read (in full or targeted-full) beyond the four input docs:
|
||
`PlayerModeController.cs` (all 623 lines), `RuntimeLocalPlayerPhysicsPublicationState.cs`
|
||
(all 1033 lines), `RuntimeLocalPlayerMovementState.cs` (all 374 lines),
|
||
`EntityPhysicsHostComposition.cs` (all 82 lines), `RuntimeInitialCreateResidenceState.Begin`/
|
||
`Own` (full), `RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement`/
|
||
`SubmitPreparedPlacementCore` (targeted), `RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate`
|
||
(full), `DatLiveEntityProjectionMaterializer.RegisterAnimation` (full), plus
|
||
targeted greps across `RuntimeInitialCreateContinuationExecutorTests.cs` and
|
||
`RuntimeLocalPlayerPhysicsPublicationStateTests.cs` for the test harness's
|
||
own "intended recipe" (tests are the only place the FULL local-player
|
||
publication recipe is exercised end-to-end today).
|
||
|
||
### Finding A — confirmed: no accessibility blocker, no accidental
|
||
pre-built orchestrator (cross-check of the route-inventory's own claim)
|
||
|
||
Re-verified independently: `RuntimeLocalPlayerPhysicsPublicationState`'s
|
||
Prepare/Commit/EvaluateActivation/CommitActivation/FinalizeActivation chain
|
||
has ZERO production callers (only
|
||
`tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs`).
|
||
Unlike C1 ("satisfied by existing mechanism" — a pleasant surprise the plan
|
||
doc recorded), there is no similar hidden orchestrator tying
|
||
`RegisterEntityWithInitialResidence`'s residence lease to the publication
|
||
lifecycle automatically. The wiring described by C3-2/C3-3's bullets
|
||
genuinely does not exist anywhere, dormant or otherwise.
|
||
|
||
### Finding B — the local-player initial-placement circular dependency
|
||
(confirmed by direct read, not previously named in any of the 4 docs)
|
||
|
||
- `RuntimeEntityObjectLifetime.RegisterEntityWithInitialResidence` →
|
||
`InitializeAcceptedCreateResidence` → `InitialCreateResidences.Begin`
|
||
(`RuntimeInitialCreateResidenceState.cs:556-607`) → `Own`
|
||
(`:731-...`) — `Own` synchronously calls
|
||
`_setPosition.TryBeginExclusiveAuthoredPlacement(record, ..., route.OperationKind)`
|
||
(`:746-751`) whenever `route.PerformsSetPosition` is true. This runs
|
||
**at wire CreateObject time**, inside `LiveEntityRuntime.RegisterLiveEntity`
|
||
today's call site (once flipped) — i.e. it opens a `RuntimeEntityPlacementToken`
|
||
operation in stage `AwaitingPreparation` immediately, for EVERY entity
|
||
including the local player, and the lease (`lease.Placement`) sits open
|
||
until something completes it.
|
||
- `RuntimeAuthoritativePositionRouteClassifier.ClassifyCreate`
|
||
(`RuntimeAuthoritativePositionRouteClassifier.cs:205-273`, full read)
|
||
gives EVERY TopLevel Create with a valid wire position — local player,
|
||
remote, projectile alike — `Disposition.SetPosition` uniformly
|
||
(`route.PerformsSetPosition` is true for all of them; only Parented/
|
||
PickedUp Creates get `AwaitFreshPosition`, which does NOT open a
|
||
SetPosition operation). So this circular dependency is not local-player-
|
||
specific in its trigger — it applies to the SAME `Own()` call for every
|
||
top-level Create.
|
||
- For the LOCAL PLAYER specifically, that open placement operation can only
|
||
be completed by driving `RuntimeLocalPlayerPhysicsPublicationState`'s full
|
||
chain against the EXACT SAME `lease.Placement` token (confirmed by reading
|
||
`RuntimeLocalPlayerPhysicsPublicationStateTests.cs:2263-2300`'s `Fixture.RepreparePlacement`/
|
||
`Prepare` helpers: `BeginAuthoredPlacement` → `PrepareMover` → `Owner.Prepare(record,
|
||
placement, command, options, activationPreparation, out token)` →
|
||
`Commit(token, out activationToken)` → `EvaluateActivation` →
|
||
`CommitActivation` — `Commit`'s own body
|
||
(`RuntimeLocalPlayerPhysicsPublicationState.cs:391-397`) calls
|
||
`_physics.SetPosition.PrepareDormantLocalActivationOwnership(candidate.Record,
|
||
candidate.Body, candidate.PreparedActivation.Token.Placement)`, i.e. it
|
||
attaches the freshly-built candidate body to THAT EXACT placement token).
|
||
The generic `SubmitPreparedPlacement`/`TryPrepareAndSubmitAuthoredPlacement`
|
||
path (the one C3-2's bullet 3 names for `AwaitingContinuationPlacement`'s
|
||
pending-token flavor) **cannot** be used for the local player's OWN initial
|
||
placement: `SubmitPreparedPlacementCore` requires
|
||
`operation.Record.PhysicsBody is not { } body` to already be non-null
|
||
(`RuntimeSetPositionState.cs:2610`) — there is no local-player body yet at
|
||
Create time, so this path structurally rejects it. Only the publication
|
||
lifecycle can attach the FIRST body to an already-open placement token.
|
||
- TODAY, `PlayerModeController.BuildControllerAndCamera` runs LATER than
|
||
Create-time (gated by `PlayerModeAutoEntry.cs:214-230`'s
|
||
`IsPlayerEntityPresent && IsWorldReady` per-frame check) and never touches
|
||
`lease.Placement`/the publication state at all — it does its own unrelated
|
||
`_physics.Resolve`/`ResolvePlacement` (PlayerModeController.cs:409-430).
|
||
So after the flip, the residence lease's placement (and therefore the
|
||
ENTIRE executor drain — deferred-child replay, the AfterEnterWorld
|
||
teleport-hook request, every continuation) stays stuck in `PendingPlacement`
|
||
from Create time until whatever replaces `BuildControllerAndCamera` drives
|
||
the publication chain AND separately calls `InitialCreateExecution.Execute(...)`
|
||
a SECOND time afterward to actually drain the FIFO. This second `Execute`
|
||
call is not optional — `Execute`'s own doc comment and the executor
|
||
handoff both say a caller re-invokes `Execute` after the placement token
|
||
in the trace is acknowledged; nothing does this automatically.
|
||
- Practically workable simplification found: `RuntimeLocalPlayerPhysicsPublicationState.Prepare`
|
||
calls `DiscardCurrent()` on entry (`:305`) before installing a new
|
||
candidate, so a full "retry the whole Prepare→Commit chain from scratch
|
||
next frame" is safe UNTIL `Commit()` succeeds (at which point `_activation`
|
||
is populated and a subsequent `Prepare` call correctly rejects via
|
||
`CanPrepare`'s `_activation is null` guard — the retry loop must switch to
|
||
re-driving `EvaluateActivation`/`CommitActivation` on the SAME
|
||
`activationToken`, not re-`Prepare`ing). This means `PlayerModeAutoEntry`'s
|
||
EXISTING per-frame "keep calling TryEnter until it returns true" loop can
|
||
likely be reused rather than inventing a wholly new scheduler — but
|
||
`PlayerModeController` needs a small piece of cross-frame state (at least
|
||
the pending `activationToken` when Commit succeeded but Evaluate/Commit-
|
||
Activation hasn't finished) that does not exist today. This is a genuinely
|
||
new resumable mini-state-machine, not a one-line change.
|
||
- The animation-sequencer hook attachment
|
||
(`AttachCycleVelocityAccessor`/`ObjectScale`/`AttachAnimationRootMotionSource`/
|
||
`Motion.RemoveLinkAnimations`/`InitializeMotionTables`/`CheckForCompletedMotions`/
|
||
`DefaultSink`, `PlayerModeController.cs:378-396`) currently happens on the
|
||
controller BEFORE placement resolve, via the existing
|
||
`RuntimeLocalPlayerMovementState.BeginMotionPreparation` lease
|
||
(`:102-117`). `RuntimeLocalPlayerPhysicsPublicationState.Prepare` builds
|
||
its OWN controller internally and does not return a reference to it before
|
||
`Commit()` — but since `BeginMotionPreparation` only needs a controller
|
||
REFERENCE (no ordering requirement relative to the publication state's own
|
||
internal stages), it appears safe to call `BeginMotionPreparation` on
|
||
`_movement.Controller` immediately AFTER `Commit()` succeeds (controller
|
||
is `RuntimeOwnedDormant` at that point, not yet `RuntimePublished`) and
|
||
BEFORE `EvaluateActivation`/`CommitActivation` — this avoids needing any
|
||
NEW Runtime API to expose the candidate controller pre-Commit. Flagged
|
||
as "appears safe" (not "verified safe") — needs a dedicated conformance
|
||
test before trusting it.
|
||
|
||
### Finding C — a SECOND, previously-unstated capability gap: no production
|
||
path constructs a NON-local entity's FIRST PhysicsBody for a residence-driven
|
||
Create (this blocks Route 1/8 for remote/projectile Creates just as much as
|
||
the local-player gap blocks it for the local player)
|
||
|
||
- `SubmitPreparedPlacementCore` (`RuntimeSetPositionState.cs:2584-2629`)
|
||
requires `operation.Record.PhysicsBody is not { } body` (`:2610`) for
|
||
EVERY entity, not just the local player — confirmed by reading the full
|
||
method; there is no branch that constructs a body when none exists.
|
||
- `TryPrepareAndSubmitAuthoredPlacement` (`RuntimeSetPositionState.cs:1562-1625`,
|
||
the "C0-3" mover-chain call the contract names for continuation
|
||
placements) is a thin wrapper around `PrepareMover` + `SubmitPreparedPlacement`
|
||
— it inherits the SAME pre-existing-body requirement. Its own doc comment
|
||
says "a residence lease's own Placement/Route.OperationKind/
|
||
Route.SetPositionFlags are exactly the token/kind/flags this takes" —
|
||
true for the TOKEN shape, but silent on the body precondition.
|
||
`ClassifyCreate` gives Remote/Projectile TopLevel Creates the exact same
|
||
`Disposition.SetPosition` as the local player (see Finding B) — so a
|
||
fresh remote humanoid/monster Create's residence placement would ALSO
|
||
reject via this same guard today.
|
||
- The ONLY production body-construction call I found for a NEWLY-CREATED
|
||
(non-local) entity is `DatLiveEntityProjectionMaterializer.RegisterAnimation`'s
|
||
`_runtime.GetOrCreatePhysicsBody(spawn.Guid, incarnation => new PhysicsBody{...})`
|
||
(`DatLiveEntityProjectionMaterializer.cs:1003-1016`) — but this is gated
|
||
behind `physicsStatic` (`FinalPhysicsState & PhysicsStateFlags.Static`,
|
||
line 961) AND a resolved animation sequencer (`animation.Sequencer is {}`)
|
||
— i.e. it is a narrow special case for STATIC decorative animated objects
|
||
(banners, torches), never reached for an ordinary moving humanoid/monster
|
||
spawn. `RuntimePhysicsState.GetOrCreatePhysicsBody` (public,
|
||
`RuntimePhysicsState.cs:1623`) is presumably the right general-purpose
|
||
tool to reuse, but nobody calls it for the general case, and none of the 4
|
||
input docs name this as a Route-1 capability gap (the closest hits —
|
||
route-inventory's "Gap 2" and body-writer-map's summary — are both scoped
|
||
explicitly to the LOCAL PLAYER controller/body atomicity problem, not to
|
||
ordinary remote entities).
|
||
- Building this out requires retail-fidelity decisions (what a fresh
|
||
non-static remote/projectile body's default orientation/scale/friction/
|
||
elasticity/velocity should be at Create time, mirroring whatever retail's
|
||
`enter_world`/object-creation path does) that none of the 4 docs specify
|
||
and that I should not invent without the grep-named-first workflow this
|
||
project mandates for AC-specific behavior.
|
||
|
||
### Why I stopped here rather than pushing an implementation
|
||
|
||
Both findings B and C are genuine, evidence-backed (file:line cited) gaps
|
||
in the CONTRACT's own assumed shape, not just "this is a lot of code."
|
||
Landing C3-2+C3-3 correctly needs, at minimum: (1) a new resumable
|
||
mini-state-machine in PlayerModeController/PlayerModeAutoEntry driving
|
||
Prepare→Commit→EvaluateActivation→CommitActivation→(second) Execute across
|
||
frames; (2) the equivalent for headless (which has its own per-tick
|
||
`TryCompletePortal`-shaped loop already, per route-inventory's route 8
|
||
section, that a similar chain would need to extend); (3) a NEW general
|
||
first-body-construction step for non-local residence-driven Creates,
|
||
requiring retail research this session did not do; (4) deletion of the
|
||
now-superseded duplicate authorities across ~6 files; (5) new App.Tests/
|
||
Headless.Tests integration tests; (6) the connected lifecycle/reconnect
|
||
gate against a live ACE, which is itself explicitly one of this project's
|
||
few "stop and get user verification" events. Given the project's own
|
||
standing rules — no workarounds, no guessing at retail behavior, dual-
|
||
reviewed shape for anything this load-bearing, and "stop and brainstorm
|
||
when the observed scope diverges from the plan's assumed shape" — pushing
|
||
a rushed implementation of the single most sensitive path in the client
|
||
(both hosts' login/placement) within this session's remaining budget was
|
||
judged higher-risk than landing C3-1 clean and handing back precise,
|
||
citable findings for a properly scoped follow-up session (likely its own
|
||
C3-2a "local-player initial-placement orchestration" + C3-2b "first-body
|
||
construction for residence-driven Creates" split, each with its own
|
||
dual-review pass, mirroring how C0/C1/C2 were each already run).
|
||
|
||
No files under C3-2/C3-3/C3-4's scope were edited: PlayerModeController.cs,
|
||
LiveEntityRuntime.cs, DatLiveEntityProjectionMaterializer.cs,
|
||
RuntimeLiveEntitySessionController.cs, HeadlessSessionWorldProjection.cs,
|
||
and RuntimeLocalPlayerMovementState.cs (the C3-4 Controller-setter seal)
|
||
are all untouched by this session (confirmed via `git status`).
|
||
|
||
## Gate 4 (connected) — not reached, not skipped/faked
|
||
|
||
The exact lifecycle/reconnect harness is real and located per the route
|
||
inventory: `tools/run-connected-world-lifecycle-gate.ps1` (drives capped +
|
||
uncapped-reconnect sessions against local ACE on 127.0.0.1:9000) and
|
||
`tools/run-connected-r6-soak.ps1` (canonical nine-stop route). Both require
|
||
an already-listening local ACE. I did not attempt to launch/verify ACE
|
||
reachability because there is no production code change from C3-2/C3-3/C3-4
|
||
to gate yet — running the connected harness against C3-1's Runtime-only
|
||
addition would exercise nothing new (C3-1 has no host caller in this
|
||
session) and would misrepresent the gate as having validated the cutover.
|
||
Whoever lands C3-2/C3-3/C3-4 must run this gate for real, with a live ACE,
|
||
per the contract's gate 4 and the project's own "visual verification is the
|
||
one thing that requires stopping for the user" rule.
|
||
|
||
## Files touched this session (C3-1 only)
|
||
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
- src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs
|
||
- src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
|
||
Nothing staged, nothing committed, HEAD unchanged at a32aba35d1d945b9d3194a84e70facf74a7d7608.
|
||
|
||
## C3-1 review-fix round (same session, 2026-08-02)
|
||
|
||
Coordinator relayed two review passes on C3-1's diff:
|
||
|
||
1. Architecture PASS with one MINOR: `MapHookPhase`/`MapDisposition`/
|
||
`MapConstrainPhase`'s catch-all `_ =>` arms silently folded an unmapped
|
||
future internal enum value into `None`/`NoPositionOperation` instead of
|
||
failing loudly. Fixed: every declared value now has an explicit arm
|
||
(added the previously-implicit `None`/`NoPositionOperation` cases) and
|
||
the catch-all now `throw new ArgumentOutOfRangeException(...)` with a
|
||
message naming both the mapper method and the public enum to update.
|
||
Also fixed `ProjectCompletion`'s `action.PositionDisposition ??
|
||
RuntimeAuthoritativePositionDisposition.NoPositionOperation` fallback:
|
||
verified (grep-confirmed `BuildPositionTrace` is the sole producer of
|
||
`Kind.Position` trace entries, always passing `route.Disposition`, a
|
||
non-nullable enum) that null is NOT a legitimate state at that call
|
||
site specifically (it legitimately IS null for non-Position action
|
||
kinds elsewhere in the trace, per the field's own doc comment - just
|
||
not reachable here since the loop already filters to
|
||
`Kind == Position`) - replaced the silent fallback with an explicit
|
||
`InvalidOperationException` throw + comment explaining why.
|
||
2. Retail-conformance PASS with two documentation-only addenda (no code
|
||
changes): (a) `RuntimeInitialCreatePositionRouteFact`'s doc comment now
|
||
states explicitly that `UnparentBeforeRouting`/
|
||
`ApplyPlacementFrameBeforeRouting` ("unset_parent"/"SetPlacementFrame")
|
||
are NOT projected because the executor's own merge
|
||
(`ApplyAcceptedPositionSnapshot`'s `clearParent`/`installPlacementFrame`
|
||
params, confirmed by direct read at the `ApplyPositionAction`/envelope
|
||
call sites) already applies both to the canonical snapshot before the
|
||
trace entry is built - a host must not re-apply them; the struct only
|
||
carries facts still DEFERRED to the host. (b)
|
||
`RuntimeInitialCreatePlacementCompletion`'s doc comment now states
|
||
`PositionRouteFacts`'s ARRAY ORDER (not `Sequence`) is authoritative -
|
||
confirmed multiple Position trace entries from one same-incarnation
|
||
envelope share one continuation `Sequence` (only `Stage`, not
|
||
projected, distinguishes them internally), and `ProjectCompletion`
|
||
preserves trace/FIFO-drain order by construction.
|
||
|
||
New test per the reviewer's ask ("same guard shape as
|
||
`OperationResetAllFieldsToDefaultTouchesEveryDeclaredField`"):
|
||
`EnumProjectionMapsHaveEqualArityAndEveryInternalValueRoundTripsByName` in
|
||
`RuntimeInitialCreateContinuationExecutorTests.cs` - reflection-invokes the
|
||
three private static `Map*` methods against every declared value of their
|
||
internal source enum, asserting (a) equal arity between the internal and
|
||
public enum, and (b) every mapped public value's `.ToString()` name equals
|
||
the internal value's name (the mappers are literal 1:1 name mirrors by
|
||
design). Sabotage-verified twice, both reverted after confirming failure:
|
||
(1) added a member ONLY to internal `RuntimeTeleportHookPhase` - failed on
|
||
the arity assertion ("RuntimeTeleportHookPhase has 5 values but
|
||
RuntimeInitialCreateTeleportHookPhase has 4"); (2) added the SAME member to
|
||
BOTH the internal and public enum (arity equal) without adding a mapping
|
||
arm - failed via the reflection-invoked `MapHookPhase` throwing
|
||
`ArgumentOutOfRangeException` exactly as designed. Both sabotage edits
|
||
fully reverted; confirmed clean via `git diff` on the touched file showing
|
||
no residual change.
|
||
|
||
Gates re-run after the fix: focused
|
||
Residence|Classifier|Executor|SetPositionState|PlacementProjectionChannel
|
||
filter 242/242 (241 + 1 new); complete AcDream.Runtime.Tests 932/932 (931 +
|
||
1 new); Release build of the full solution 0 errors/21 pre-existing
|
||
warnings; `git diff --check` clean (only pre-existing LF/CRLF notices, same
|
||
files as before). `git status` shows the same files as the prior C3-1
|
||
checkpoint dirty, PLUS one file I did NOT touch:
|
||
`docs/plans/2026-08-02-placement-cutover.md` now shows a diff decomposing
|
||
C3 into C3a/C3b/C3c based on my earlier Finding B/C report - this was made
|
||
externally (not by this session; I never opened that file for editing this
|
||
round) and is left exactly as found, unstaged. Nothing staged by me,
|
||
nothing committed, HEAD unchanged at
|
||
a32aba35d1d945b9d3194a84e70facf74a7d7608.
|
||
|
||
Files touched this round (all within C3-1's original scope, no new files):
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
(Map* explicit arms + throws, ProjectCompletion null-check, doc-comment
|
||
addenda on RuntimeInitialCreatePositionRouteFact and
|
||
RuntimeInitialCreatePlacementCompletion)
|
||
- tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
(+1 new test, +using System.Reflection)
|
||
|
||
================================================================
|
||
C3a -- Runtime first-entry conductor (dormant) -- implementer session
|
||
================================================================
|
||
|
||
## Mandatory first step: full reads completed
|
||
|
||
Read in full before writing any code:
|
||
- src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs
|
||
(1,033 lines).
|
||
- tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs
|
||
(2,698 lines, all ~35 tests).
|
||
- docs/research/2026-07-31-remaining-physics-campaign-handoff.md (497 lines,
|
||
full) -- route-1's own required order, lines 280-292.
|
||
- docs/plans/2026-08-02-placement-cutover.md (166 lines, full).
|
||
- docs/research/2026-08-02-canonical-body-writer-map.md (681 lines, full).
|
||
- docs/research/2026-08-02-runtime-continuation-executor-handoff.md (211
|
||
lines, full).
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs (1,260
|
||
lines, full).
|
||
- src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
|
||
(Execute/ExecuteCore, lines 1-250 and 860-1080).
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs targeted sections:
|
||
RuntimeEntityPlacementStage enum (30-39), TryBeginExclusiveAuthoredPlacement/
|
||
PrepareDormantLocalActivationOwnership/BeginAcceptedPlacementCore
|
||
(1170-1467), PrepareMover/TryPrepareAndSubmitAuthoredPlacement/
|
||
IsExactPreparedPlacementCurrent (1480-1652), SubmitPreparedPlacementCore
|
||
(2575-2700), RetryDeferred (3986-4020), AcknowledgeProjection (2913-3006).
|
||
- Existing test fixtures for reuse patterns:
|
||
tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
|
||
(EngineLifetime/Bind/Spawn/AttachDormantBody/CompleteInitialPlacement,
|
||
lines 1-130 and 4963-5150) and
|
||
tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
|
||
(FakeCollisionSource, TryPrepareAndSubmitAuthoredPlacement tests,
|
||
lines 2660-2760, 3296+).
|
||
|
||
## Step-graph (states x transitions x owning method)
|
||
|
||
[residence Begin -- ALREADY DONE at registration, outside conductor scope]
|
||
RuntimeEntityObjectLifetime.RegisterEntityWithInitialResidence
|
||
-> RuntimeInitialCreateResidenceState.Begin
|
||
-> RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement
|
||
(opens Operation, stage=AwaitingPreparation)
|
||
-> RuntimeSetPositionState.WatchPlacementCompletion(placement)
|
||
yields: RuntimeInitialCreateResidenceLease { Token, Route, Placement }
|
||
|
||
Stage.AwaitingMoverPreparation (conductor entry point)
|
||
precondition: RuntimeInitialCreateResidenceState.TryGetCurrent(record, out
|
||
lease) && lease.Token == residenceToken
|
||
if !lease.Route.PerformsSetPosition (Parented/PickedUp -- never true for a
|
||
real login, kept for structural completeness):
|
||
-> Stage.Acknowledged (skip straight to Execute)
|
||
else:
|
||
RuntimeSetPositionState.TryPrepareAuthoredMover <- NEW extracted method
|
||
(Setup-read via IPreparedCollisionSource, then PrepareMover; stage
|
||
stays AwaitingPreparation; sets authority.Prepared=true)
|
||
RetrySetupUnavailable -> yield AwaitingCollisionSource (retry same stage)
|
||
Prepared -> Stage.MoverPrepared, holds RuntimeSetPositionCommand
|
||
|
||
Stage.MoverPrepared
|
||
RuntimeLocalPlayerPhysicsPublicationState.Prepare(record, lease.Placement,
|
||
command, options, activationPreparation, out pubToken)
|
||
(validates IsExactPreparedPlacementCurrent -- REQUIRES mover already
|
||
prepared; off-canonical body+controller build against scratch clock)
|
||
RejectedAuthority -> abandon (Discard progress; nothing to undo, Prepare
|
||
never mutates on failure)
|
||
Prepared -> Stage.PublicationPrepared, holds pubToken
|
||
|
||
Stage.PublicationPrepared
|
||
RuntimeLocalPlayerPhysicsPublicationState.Commit(pubToken, out
|
||
activationToken)
|
||
-> internally: RuntimeSetPositionState.PrepareDormantLocalActivationOwnership
|
||
(the designed seam -- binds Operation.Body, sets DormantLocalActivation
|
||
=true; requires stage STILL AwaitingPreparation + record.PhysicsBody
|
||
still null)
|
||
-> candidate.Controller.CommitRuntimeOwnership +
|
||
candidate.Record.SetPhysicsBody(body) (record now has a body)
|
||
RejectedToken/RejectedAuthority -> abandon (Publication.Commit already
|
||
self-discards its own candidate on RejectedAuthority; conductor drops
|
||
its own progress entry)
|
||
Committed -> Stage.PublicationCommitted, holds activationToken
|
||
|
||
Stage.PublicationCommitted / Stage.Evaluated (single combined retry point --
|
||
see "CommitActivation resume safety" note below)
|
||
RuntimeLocalPlayerPhysicsPublicationState.EvaluateActivation(activationToken,
|
||
out receipt)
|
||
RejectedToken/RejectedAuthority -> abandon
|
||
Evaluated/DeferredCell/RejectedPlacement -> receipt.IsValid in all three;
|
||
proceed to CommitActivation in the SAME Advance call (mirrors every
|
||
publication test: Evaluate and CommitActivation are always chained,
|
||
never yielded between)
|
||
RuntimeLocalPlayerPhysicsPublicationState.CommitActivation(receipt, out
|
||
projection)
|
||
(internally drives: ground phase -> HitGround/LeaveGround -> post-ground
|
||
-> collision dispatch -> post-collision -> FinalizeActivation, which is
|
||
the SAME retail-staged commit already tested)
|
||
Committed -> Stage.ActivationCommitted, holds projection
|
||
DeferredCell / RejectedPlacement -> yield AwaitingActivation, stage stays
|
||
PublicationCommitted (retry re-runs BOTH EvaluateActivation AND
|
||
CommitActivation next Advance call -- safe even for the internal
|
||
AwaitingFinalShadowPreparation resume path, see note below)
|
||
RejectedAuthority -> abandon
|
||
|
||
Stage.ActivationCommitted
|
||
RuntimeSetPositionState.AcknowledgeProjection(projection.Token)
|
||
("Place receipt -> acknowledgement" -- the SAME ack any host uses; moves
|
||
the watched placement token into _acknowledgedPlacementCompletions,
|
||
operation.Stage -> AwaitingCommitAcknowledgement, operation removed from
|
||
_operations)
|
||
false -> yield AwaitingReceiptAcknowledgement (retry same stage -- only
|
||
fails if not the exact FIFO head; single-entity tests never hit this,
|
||
documented for completeness)
|
||
true -> Stage.Acknowledged
|
||
|
||
Stage.Acknowledged (or skipped-straight-here for a non-SetPosition route)
|
||
RuntimeInitialCreateContinuationExecutor.Execute(record, residenceToken,
|
||
inputs, out executionReceipt)
|
||
(internally: RuntimeInitialCreateResidenceState.Complete -- now succeeds
|
||
because IsPlacementCurrent(lease.Placement)==false and
|
||
TryPeekAcknowledgedPlacement succeeds -- -> AdoptCompletedPlacement ->
|
||
AfterEnterWorld hook -> deferred replay -> FIFO drain -> ConsumeExecuted)
|
||
Completed -> conductor Stage.Completed (progress removed) -> terminal
|
||
PendingPlacement -> yield AwaitingReceiptAcknowledgement (residence still
|
||
sees the operation as unacknowledged/current -- should not normally
|
||
recur once we've truly acknowledged, kept as a defensive yield)
|
||
AwaitingContinuationPlacement -> yield AwaitingContinuationPlacement
|
||
(a LATER Position continuation needs its own placement -- entirely the
|
||
EXECUTOR's own concern from here; conductor just passes the status
|
||
through, mirroring "Execute (FIFO drain) -> ExecutorCompleted receipt"
|
||
being the conductor's LAST step, not something it re-implements)
|
||
RejectedToken/RejectedAuthority -> abandon
|
||
|
||
Typed yields exposed (RuntimeLocalPlayerFirstEntryStatus): Completed,
|
||
AwaitingCollisionSource, AwaitingActivation, AwaitingReceiptAcknowledgement,
|
||
AwaitingContinuationPlacement, Contention (reentrancy-guard-only -- mirrors
|
||
the executor's own `_executing.Add(key)` fail-closed pattern), RejectedToken,
|
||
RejectedAuthority. "awaiting-preparation" from the contract's five named
|
||
yields is folded into AwaitingCollisionSource / the MoverPrepared ->
|
||
PublicationPrepared span (see reconciliation below) rather than kept as an
|
||
eighth separate value -- documented at the enum declaration.
|
||
|
||
## Reconciliation against route-1 + CONTRADICTION FOUND (resolved, did not
|
||
## silently redesign)
|
||
|
||
Campaign handoff route-1 order (2026-07-31-remaining-physics-campaign-handoff.md:280-292):
|
||
1. register identity cellless
|
||
2. begin initial/remote-create placement before hydration
|
||
3. load exact Setup mover
|
||
4. prepare the atomic Runtime controller/body relationship
|
||
5. submit canonical SetPosition
|
||
6. publish presentation only from Place
|
||
7. acknowledge, then enable player mode/simulation
|
||
|
||
Step 3 (mover) precedes step 4 (controller/body prepare) here.
|
||
|
||
The C3a contract's OWN restated PURPOSE-section order instead reads:
|
||
"... atomic Commit binding the body to the residence's EXACT placement
|
||
token (PrepareDormantLocalActivationOwnership is the designed seam)
|
||
-> authored-mover preparation + submission (C0's
|
||
TryPrepareAndSubmitAuthoredPlacement ...)"
|
||
i.e. it places mover-prep AFTER Publication Prepare/Commit -- the OPPOSITE of
|
||
route-1's own order 3-then-4.
|
||
|
||
Verified against the actual staged semantics that this restated order is
|
||
IMPOSSIBLE and additionally that the named mechanism cannot be reused as
|
||
worded:
|
||
1. RuntimeLocalPlayerPhysicsPublicationState.CanPrepare
|
||
(RuntimeLocalPlayerPhysicsPublicationState.cs:886-889) requires
|
||
_physics.SetPosition.IsExactPreparedPlacementCurrent(record, placement,
|
||
command) to ALREADY be true. IsExactPreparedPlacementCurrent
|
||
(RuntimeSetPositionState.cs:1627-1651) requires
|
||
authority.Prepared && authority.PreparedCommand == command -- i.e.
|
||
PrepareMover MUST have already succeeded for this exact command BEFORE
|
||
Publication.Prepare can even be called. Mover-prep cannot happen after
|
||
Commit; it structurally gates entry into Prepare.
|
||
2. TryPrepareAndSubmitAuthoredPlacement (RuntimeSetPositionState.cs:1562-1625)
|
||
is PrepareMover followed unconditionally by SubmitPreparedPlacement.
|
||
SubmitPreparedPlacementCore (RuntimeSetPositionState.cs:2584-2630)
|
||
requires operation.Record.PhysicsBody is not {} body -- i.e. a body must
|
||
ALREADY exist. Before Commit, the record has no body (Commit is what
|
||
attaches one); calling this fused method before Commit would reject.
|
||
Calling it AFTER Commit (as the contract's restated order implies) would
|
||
NOT reject -- the body now exists -- but RetryDeferred's own comment
|
||
(RuntimeSetPositionState.cs:3988-3993) states explicitly: "The local-
|
||
player activation lease owns its dormant body/controller and must
|
||
re-enter through the same sealed evaluation/commit path ... it must
|
||
never bypass that path through the ordinary remote CommitCanonical
|
||
tail." SubmitPreparedPlacementCore has no DormantLocalActivation
|
||
exclusion check, so calling it post-Commit would silently route the
|
||
operation through the wrong (ordinary) commit tail in parallel with the
|
||
dormant Evaluate/Commit/FinalizeActivation chain -- corrupting state.
|
||
3. Confirmed empirically: the EXISTING executor test suite
|
||
(RuntimeInitialCreateContinuationExecutorTests.cs's
|
||
AttachDormantBody/CompleteInitialPlacement helpers, lines 5007-5073)
|
||
treats even isLocalPlayer: true fixtures via a direct
|
||
Entities.SetPhysicsBody + ordinary SubmitPreparedPlacement -- NEVER
|
||
through RuntimeLocalPlayerPhysicsPublicationState -- because for a
|
||
record that never sets DormantLocalActivation, the ordinary tail is
|
||
exactly correct. This is a test-only substitute for what C3a's conductor
|
||
now performs for real; it is not evidence that the ordinary tail is ever
|
||
valid for a DormantLocalActivation operation.
|
||
|
||
Resolution: route-1's own order (mover-prep BEFORE the controller/body
|
||
prepare step) is correct and consistent with every tested invariant; the
|
||
C3a contract's restated PURPOSE-section prose transposed the two steps.
|
||
Per the contract's own instruction ("STOP with file:line evidence" rather
|
||
than silently redesigning the PUBLICATION CHAIN), this is flagged here with
|
||
full citations; the conductor is implemented using route-1's order because
|
||
(a) that is what the contract explicitly told me to reconcile against, and
|
||
(b) it is the only order that satisfies the publication chain's own
|
||
staged/tested preconditions without changing a single line of already-
|
||
tested code. The publication chain itself (Prepare/Commit/EvaluateActivation/
|
||
CommitActivation/FinalizeActivation) is NOT modified or reinterpreted -- only
|
||
the CONDUCTOR's call order was corrected relative to the contract's prose.
|
||
|
||
Mechanism correction: "C0's TryPrepareAndSubmitAuthoredPlacement" as named in
|
||
the contract is the WRONG vehicle for the local-player dormant path for the
|
||
reason in point 2 above (it ends in SubmitPreparedPlacement, forbidden
|
||
once DormantLocalActivation is set). RuntimeSetPositionState.cs gained one
|
||
new internal method, TryPrepareAuthoredMover, extracted verbatim from
|
||
TryPrepareAndSubmitAuthoredPlacement's FIRST HALF (Setup-read + PrepareMover
|
||
call only, no Submit) -- a pure, behavior-preserving refactor.
|
||
TryPrepareAndSubmitAuthoredPlacement itself now calls this shared helper
|
||
then submits, unchanged in every observable respect (its own two existing
|
||
tests, TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit
|
||
and ..._YieldsRetryOnAMissingSetupReadWithoutMutatingStage, stay green
|
||
unmodified). The conductor calls ONLY the new TryPrepareAuthoredMover half.
|
||
|
||
## CommitActivation resume safety note (why one retry stage suffices)
|
||
|
||
Verified that re-running EvaluateActivation before every CommitActivation
|
||
retry -- rather than adding a THIRD stage that resumes CommitActivation alone
|
||
for the AwaitingFinalShadowPreparation internal resumption path -- is safe:
|
||
CommitActivation's own top-of-method resume check
|
||
(activation.PendingFinalCommit.Status is AwaitingFinalShadowPreparation,
|
||
RuntimeSetPositionState.cs:498-506) fires only AFTER re-validating
|
||
activation.Receipt == receipt; a fresh EvaluateActivation call sets
|
||
activation.Receipt to match whatever it just returned, so passing that
|
||
same fresh receipt back into CommitActivation satisfies the equality check
|
||
and the stored PendingFinalCommit (untouched by the extra Evaluate call)
|
||
still drives the correct resume via FinalizeActivation. Confirmed
|
||
DeferredCell/RejectedPlacement from CommitActivation clear
|
||
activation.Receipt to default (RuntimeSetPositionState.cs:546,628) -- so a
|
||
fresh Evaluate is REQUIRED, not just tolerated, on those two outcomes. The
|
||
one cost is a redundant extra Engine.SetPosition resolve in the (rare,
|
||
contention-only) AwaitingFinalShadowPreparation case -- not a correctness
|
||
issue, and simpler than tracking a fourth stage.
|
||
|
||
## Files this session will add/touch
|
||
|
||
- src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs -- extract
|
||
TryPrepareAuthoredMover (new internal method; TryPrepareAndSubmitAuthoredPlacement
|
||
now delegates to it, unchanged behavior).
|
||
- src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs -- NEW,
|
||
the conductor. Dormant; no production caller; constructed only in tests.
|
||
- tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerFirstEntryStateTests.cs
|
||
-- NEW.
|
||
|
||
No changes to RuntimeLocalPlayerPhysicsPublicationState.cs,
|
||
RuntimeInitialCreateResidenceState.cs, RuntimeInitialCreateContinuationExecutor.cs,
|
||
RuntimeEntityObjectLifetime.cs, or GameRuntime.cs (C3c wires production
|
||
callers and residence-retirement fan-out; that is explicitly out of C3a's
|
||
scope). One consequence documented for C3c: because
|
||
RuntimeInitialCreateResidenceState.BindRetirementNotification is a single-
|
||
subscriber seam already bound to InitialCreateExecution.DiscardProgress
|
||
inside RuntimeEntityObjectLifetime's constructor, the conductor built here
|
||
does NOT receive a push notification on external residence retirement; it
|
||
relies on lazy re-validation at the top of every Advance call plus an
|
||
explicit Forget(key) a future host can call. C3c will need to either fan the
|
||
single notification out to both subscribers or route it through the
|
||
conductor.
|
||
|
||
## Implementation complete — gates passed
|
||
|
||
Final design (RuntimeLocalPlayerFirstEntryState.cs, 423 lines) — a five-stage
|
||
resumable machine (AwaitingMoverPreparation -> MoverPrepared ->
|
||
PublicationCommitted -> ActivationCommitted -> Acknowledged), one
|
||
`Advance(record, residenceToken, options, activationPreparation,
|
||
collisionSource, gameTime, inputs, out receipt)` entry point, an
|
||
`_executing` HashSet reentrancy guard mirroring the executor's own, a
|
||
`Progress` class (LeaseId + Stage + the exact token/receipt/projection
|
||
structs) keyed by `RuntimeEntityKey`, and a `Discard`/`Forget` pair that
|
||
unconditionally calls `Publication.Discard`/`DiscardActivation` (both
|
||
harmless no-ops against a default/unreached-stage token).
|
||
|
||
Bugs found and fixed during test-driven verification (all via a temporary
|
||
diagnostic build with Console.WriteLine probes, removed before the final
|
||
commit-ready state):
|
||
1. **EvaluateActivation's overloaded DeferredCell status.** Once a PRIOR
|
||
CommitActivation call has registered a lease as awaiting a specific cell
|
||
(`IsDormantLocalActivationAwaitingCell`), a REPEATED EvaluateActivation
|
||
call that is still not ready returns DeferredCell WITHOUT populating its
|
||
receipt (stays default/invalid) — confirmed against
|
||
`DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake` in the
|
||
publication suite, which asserts exactly `waiting.IsValid == false` on
|
||
that repeat and never calls CommitActivation with it. The conductor now
|
||
checks `evalReceipt.IsValid` before ever calling CommitActivation,
|
||
short-circuiting straight to AwaitingActivation when it is false, instead
|
||
of blindly forwarding an invalid receipt (which CommitActivation's own
|
||
`!receipt.IsValid` guard would reject as RejectedAuthority).
|
||
2. **Re-acknowledging an already-consumed projection token.** An earlier
|
||
draft used a single "Acknowledged" stage value to mean both "just
|
||
committed, ack not yet attempted" and "ack already succeeded", causing a
|
||
retry AFTER a successful acknowledgement (e.g. because Execute yielded
|
||
AwaitingContinuationPlacement) to call AcknowledgeProjection a second
|
||
time against a token AcknowledgeProjection had already removed from its
|
||
FIFO — always failing. Split into two distinct stages
|
||
(ActivationCommitted = ack not yet attempted; Acknowledged = ack done,
|
||
only Execute remains) so the ack call only ever runs once per projection.
|
||
3. **RejectedToken vs RejectedAuthority at the residence-lookup checks.**
|
||
Mirrored `RuntimeInitialCreateResidenceState.Complete`'s own convention:
|
||
"nothing was ever tracked for this key" (no Progress entry, residence not
|
||
found) is RejectedToken; "something WAS in flight and just got
|
||
invalidated" (Progress entry existed, now stale) is RejectedAuthority —
|
||
matching the executor's identical split for the analogous case.
|
||
|
||
Real (not test-only) findings surfaced by writing the tests, documented in
|
||
the test file itself:
|
||
- `RuntimeEntityRecord.Key` is computed from a nullable `LocalEntityId` and
|
||
becomes `null` the instant `ReleaseLocalId` runs (part of delete's
|
||
teardown in `RuntimeEntityObjectLifetime.CompleteProjectionRetirement`).
|
||
`Advance`'s entry check (`record.Key is not {} key -> RejectedToken`)
|
||
EXACTLY mirrors `RuntimeInitialCreateContinuationExecutor.Execute`'s own
|
||
entry check — once Key is null, the conductor cannot compute its own
|
||
dictionary key to reach stale progress at all. A caller that wants
|
||
deterministic cleanup after full teardown must capture the
|
||
`RuntimeEntityKey` BEFORE deletion and call `Forget(key)` explicitly; this
|
||
is a pre-existing convention in the codebase (the executor has the
|
||
identical limitation), not a defect introduced here.
|
||
- `RuntimeLocalPlayerPhysicsPublicationState` holds exactly ONE global
|
||
`_candidate`/`_activation` (instance fields, not per-key) — correct, since
|
||
there is only ever one local player — but it means an orphaned, un-Forgot
|
||
first-entry attempt for a stale incarnation will structurally block a
|
||
fresh incarnation's own `Publication.Prepare` (CanPrepare requires
|
||
`_activation is null`) until `Forget` runs. Proven by
|
||
`DeleteAndSameGuidReincarnationRequiresForgetBeforeTheFreshIncarnationCanUseThePublicationSlot`.
|
||
- `Movement.ResetSession()` proactively nulls Publication's `_activation`
|
||
directly (unlike delete, which only makes it stale via
|
||
`_entities.IsCurrent`/epoch checks) — so a retry after ResetSession sees
|
||
EvaluateActivation report RejectedToken (activation genuinely gone), not
|
||
RejectedAuthority (activation found but stale) — and, because
|
||
ResetSession never touches `RuntimeEntityRecord.Key`, ordinary retry alone
|
||
(no captured-key Forget) reaches Discard and converges.
|
||
|
||
### Final gate results
|
||
|
||
- `dotnet build src/AcDream.Runtime/AcDream.Runtime.csproj -c Release`:
|
||
0 errors, 0 warnings.
|
||
- `dotnet build AcDream.slnx -c Release`: 0 errors, 21 pre-existing warnings
|
||
(same count/files as the C3-1 checkpoint; none new).
|
||
- Focused filter
|
||
`FullyQualifiedName~Publication|~Residence|~Executor|~SetPositionState|~FirstEntry`:
|
||
308/308 passed.
|
||
- Complete `AcDream.Runtime.Tests`: 944/944 passed (932 baseline + 12 new),
|
||
0 skips.
|
||
- Complete solution (`dotnet test AcDream.slnx -c Release --no-build -m:1`):
|
||
every project reports 0 failed — App 4028/3 skips, Bake 15/0, Cli 4/0,
|
||
Content 124/0, Core.Net 762/0, Core 4242/1 skip, Headless 77/0, Runtime
|
||
944/0, UI.Abstractions 543/0.
|
||
- `git diff --check`: clean (only pre-existing LF/CRLF notices on the same
|
||
eight paths as every prior checkpoint; AGENTS.md is the only one with a
|
||
real, pre-existing, untouched content diff; `RuntimeSetPositionState.cs`
|
||
shows exactly my own 60/8 insertion/deletion extraction, nothing else).
|
||
- No staging, no commits, HEAD unchanged at `277ef5d0`.
|
||
|
||
### Files touched (final)
|
||
|
||
- `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` — extracted
|
||
`TryPrepareAuthoredMover` (new internal method, Setup-read + PrepareMover
|
||
only); `TryPrepareAndSubmitAuthoredPlacement` now delegates to it then
|
||
submits, byte-identical observable behavior (its own two existing tests
|
||
pass unmodified).
|
||
- `src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs` — NEW,
|
||
423 lines. Dormant; zero production callers (verified by the focused/
|
||
complete/full-solution gates above, which exercise it only from the new
|
||
test file).
|
||
- `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerFirstEntryStateTests.cs`
|
||
— NEW, 12 tests: full-sequence happy path; AwaitingCollisionSource retry
|
||
+resume; AwaitingActivation retry+resume (generation wake); AwaitingReceipt
|
||
Acknowledgement retry+resume (FIFO-head contention via a second entity's
|
||
unacknowledged Place); AwaitingContinuationPlacement propagation+resume;
|
||
reentrant Advance during a collision callback (Contention, outer call
|
||
still completes); two retry-idempotency tests (mover-preparation and
|
||
AwaitingActivation stages never re-create a candidate/duplicate a body);
|
||
mid-flight delete-during-collision-callback abandonment (no Place/shadow
|
||
published, full convergence); delete-while-AwaitingActivation requiring an
|
||
explicit captured-key Forget; ResetSession mid-flight converging through
|
||
ordinary retry; delete+same-GUID-reincarnation requiring Forget before the
|
||
fresh incarnation can use Publication's one global slot.
|
||
|
||
RuntimeLocalPlayerPhysicsPublicationState.cs, RuntimeInitialCreateResidenceState.cs,
|
||
RuntimeInitialCreateContinuationExecutor.cs, RuntimeEntityObjectLifetime.cs, and
|
||
GameRuntime.cs are all untouched, exactly as scoped.
|
||
|
||
================================================================
|
||
Review round 2 -- F1 (MAJOR) + F2 (MINOR) fixes
|
||
================================================================
|
||
|
||
## F1 (MAJOR) -- ack-failure authority re-validation
|
||
|
||
Root cause confirmed: the ActivationCommitted stage treated EVERY
|
||
AcknowledgeProjection failure as the generic "not yet FIFO head" case.
|
||
TryAcceptDelete -> CompleteProjectionRetirement -> Physics.SetPosition.Forget
|
||
-> CancelCore rewrites the SAME pending slot from Place to Discard with a
|
||
bumped Revision; the RuntimePlacementProjectionToken struct this class
|
||
already cached in progress.Projection can then never match the FIFO head
|
||
again, so AcknowledgeProjection would fail forever -- infinite
|
||
AwaitingReceiptAcknowledgement for a dead entity, Progress retained,
|
||
IsConverged false forever, exactly as reported.
|
||
|
||
Fix: added IsAcknowledgementStillPending(record, residenceToken, expected),
|
||
called on every failed acknowledge before deciding retryable vs abandon.
|
||
Two checks, either failing means authority moved:
|
||
1. _residences.TryGetCurrent(record, out lease) && lease.Token ==
|
||
residenceToken -- the SAME residence-lookup pattern stage0/stage1
|
||
already use.
|
||
2. _physics.SetPosition.TryPeekProjection(out head) -- if the FIFO head
|
||
belongs to THIS entity (head.Token.Entity == expected.Entity) but is no
|
||
longer the exact Place token expected (kind changed, or revision
|
||
bumped), authority for THIS SPECIFIC placement moved even if the
|
||
residence lookup alone would not have caught it. A head belonging to a
|
||
DIFFERENT entity is the genuine "not our turn yet" case and stays
|
||
retryable.
|
||
Both failing -> Discard(key) + RejectedAuthority. Read
|
||
RuntimeSetPositionState.TryPeekProjection directly (Runtime-internal-to-
|
||
internal), not through the public generation-gated
|
||
RuntimePlacementProjectionChannel -- this class is part of Runtime, not an
|
||
external host crossing that boundary, exactly like its existing direct
|
||
AcknowledgeProjection call.
|
||
|
||
Real discovery made while testing this: FinalizeActivation nulls
|
||
Publication's OWN tracked `_activation` the INSTANT CommitActivation's
|
||
final commit succeeds (RuntimeLocalPlayerPhysicsPublicationState.cs
|
||
FinalizeActivation, `_activation = null;` right after
|
||
TryApplyDormantLocalActivationFinalCommit succeeds) -- the controller is
|
||
genuinely live/published from that point on, not a discardable in-progress
|
||
candidate. So once Stage.ActivationCommitted is reached,
|
||
Publication.DiscardActivation is ALREADY a no-op on the controller/body;
|
||
abandoning a stuck acknowledgement never retroactively un-publishes an
|
||
already-live entity -- that is ordinary entity teardown's job, not this
|
||
class's. Documented on Discard's own doc comment and confirmed empirically
|
||
(a diagnostic build showed PendingActivationCount == 0 already at the FIRST
|
||
ack attempt, before any authority change).
|
||
|
||
New test:
|
||
`DeleteWhileAwaitingReceiptAcknowledgementAbandonsInsteadOfRetryingForeverAndConverges`
|
||
-- reaches ActivationCommitted (blocked behind another entity's own
|
||
unacknowledged Place, same technique as the existing FIFO-head test), then
|
||
calls the EXACT narrower mechanism TryAcceptDelete itself uses
|
||
(`Physics.SetPosition.Forget(record, releasePreparedMover: true)` +
|
||
`PublishCancellation`) directly rather than a full entity delete -- this
|
||
was deliberate: a FULL delete now ALSO retires the residence, and F2's
|
||
automatic retirement fan-out (below) would converge everything before a
|
||
second Advance ever ran, masking whether THIS authority-recheck code path
|
||
itself works. The narrower call proves the fix independent of F2's
|
||
wiring. Asserts RejectedAuthority, ActiveCount/PendingActivationCount
|
||
converge to 0, and the UNRELATED entity's own placement remains
|
||
unaffected and acknowledgeable.
|
||
|
||
## F2 (MINOR) -- ownership fold + cleanup wiring
|
||
|
||
(a) RuntimeInitialCreateResidenceState.BindRetirementNotification converted
|
||
from a single nullable Action field (throw-on-second-bind) to a
|
||
`List<Action<RuntimeEntityKey>>` (ordered, registration-order invocation via
|
||
a new private NotifyRetirement(key) helper). Null-arg throw preserved
|
||
(ArgumentNullException.ThrowIfNull); the "already bound" throw is gone by
|
||
design since multiple subscribers are now the point. All 5 existing
|
||
invocation sites (Forget x2, Clear's two loops, Retire(Entry),
|
||
Retire(CompletedEntry)) now call NotifyRetirement instead of
|
||
`_retirementNotification?.Invoke`.
|
||
|
||
(b) RuntimeLocalPlayerFirstEntryState's constructor no longer takes
|
||
RuntimeLocalPlayerPhysicsPublicationState (RuntimeEntityObjectLifetime is
|
||
constructed BEFORE Publication exists -- GameRuntime builds
|
||
RuntimeLocalPlayerMovementState and attaches its publication only after the
|
||
entity-object lifetime). Added the SAME late-bind pattern already used
|
||
throughout this class family (BindGeneration, BindRetirementNotification,
|
||
BindLiveInputs, RuntimeLocalPlayerMovementState.PhysicsPublication's own
|
||
throws-if-unbound accessor): `BindPublication(publication)` (bind-once,
|
||
throws on null/double-bind) + a private `Publication` accessor that throws
|
||
if unbound. All internal `_publication.X` call sites became `Publication.X`.
|
||
Added `DiscardAll()` mirroring the executor's own (discards every tracked
|
||
key's candidate/activation, then clears `_progress`).
|
||
RuntimeEntityObjectLifetime now constructs `LocalPlayerFirstEntry` in all 3
|
||
constructors (right after InitialCreateExecution, same pattern), binds a
|
||
SECOND retirement notification (`key => LocalPlayerFirstEntry.Forget(key)`,
|
||
alongside the executor's existing one), and `BeginSessionClear` calls
|
||
`LocalPlayerFirstEntry.DiscardAll()` right after
|
||
`InitialCreateExecution.DiscardAll()`. `RuntimeEntityObjectOwnershipSnapshot`
|
||
gained `LocalPlayerFirstEntryActiveCount = 0` (trailing default, matching
|
||
the file's existing convention), folded into `IsConverged` and into
|
||
`CaptureOwnership()`'s construction. GameRuntime.cs itself was NOT touched
|
||
(BindPublication is never called in production) -- deliberate: since
|
||
Advance is never called in production, `_progress` stays permanently empty,
|
||
so Forget/DiscardAll never actually dereference Publication regardless of
|
||
binding state; wiring the production BindPublication call is left as a
|
||
natural part of C3c's Advance-caller work, not manufactured here.
|
||
|
||
(c) Updated the two named tests plus my own new delete tests to use
|
||
`Lifetime.LocalPlayerFirstEntry` (bound via `.BindPublication(Publication)`)
|
||
instead of a separately-constructed conductor instance -- this is what
|
||
actually exercises the real wiring; a standalone instance would never see
|
||
the fan-out at all.
|
||
- `DeleteWhileAwaitingActivationRequiresForgetOfTheCapturedKeyToDiscardTheDormantActivation`
|
||
renamed to `DeleteWhileAwaitingActivationConvergesAutomaticallyThroughTheRetirementFanOut`:
|
||
delete alone now converges ActiveCount/PendingActivationCount to 0 with
|
||
NO explicit host Forget call; a follow-up Advance is a safe RejectedToken
|
||
no-op (Key already null).
|
||
- `DeleteAndSameGuidReincarnationRequiresForgetBeforeTheFreshIncarnationCanUseThePublicationSlot`
|
||
renamed to `DeleteAndSameGuidReincarnationAutomaticallyFreesThePublicationSlotForTheFreshIncarnation`:
|
||
the fresh incarnation's own Prepare now succeeds immediately after delete,
|
||
no Forget call in between.
|
||
No standalone-conductor variant was kept -- delete-triggered convergence
|
||
IS the production path once RuntimeEntityObjectLifetime owns construction,
|
||
so an explicit-Forget test would only be meaningful for a conductor built
|
||
outside the lifetime, which is not a real usage shape this slice needs to
|
||
cover (the F1 test's narrower Physics.SetPosition.Forget-only scenario
|
||
already demonstrates the authority-recheck's own logic independent of the
|
||
fan-out, satisfying that documentation need instead).
|
||
|
||
Fixture.Dispose ordering bug found and fixed along the way: disposing
|
||
Movement (which tears down Publication) BEFORE Lifetime (whose Dispose runs
|
||
BeginSessionClear, which now reaches LocalPlayerFirstEntry.DiscardAll ->
|
||
Publication.Discard for any still-tracked entity) threw
|
||
ObjectDisposedException whenever a test left real progress untracked at
|
||
teardown (e.g. the AwaitingActivation retry-idempotency test, which never
|
||
completes or deletes within the test body). Fixed by disposing Lifetime
|
||
FIRST. Documented as a real ordering constraint for whoever eventually
|
||
disposes GameRuntime in production, since the identical dependency exists
|
||
there (RuntimeEntityObjectLifetime's conductor holds a bound reference to
|
||
Publication via BindPublication).
|
||
|
||
## Final gate results (round 2)
|
||
|
||
- `dotnet build src/AcDream.Runtime/AcDream.Runtime.csproj -c Release`:
|
||
0 errors, 0 warnings.
|
||
- `dotnet build AcDream.slnx -c Release`: 0 errors, 21 pre-existing warnings
|
||
(unchanged).
|
||
- Focused filter
|
||
`FullyQualifiedName~Publication|~Residence|~Executor|~SetPositionState|~FirstEntry`:
|
||
309/309 passed (308 + 1 new F1 test).
|
||
- Complete `AcDream.Runtime.Tests`: 945/945 passed (932 baseline + 13 new),
|
||
0 skips.
|
||
- Complete solution (`dotnet test AcDream.slnx -c Release --no-build -m:1`):
|
||
every project reports 0 failed -- App 4028/3 skips, Bake 15/0, Cli 4/0,
|
||
Content 124/0, Core.Net 762/0, Core 4242/1 skip, Headless 77/0, Runtime
|
||
945/0, UI.Abstractions 543/0.
|
||
- `git diff --check`: clean. Modified files now include
|
||
`RuntimeEntityObjectLifetime.cs` and `RuntimeInitialCreateResidenceState.cs`
|
||
in addition to the round-1 `RuntimeSetPositionState.cs` -- all three
|
||
diffs are additive/expected (64, 39, 68 changed lines respectively via
|
||
`git diff --stat`); the pre-existing eight dirty paths are otherwise
|
||
unchanged (line-ending noise only).
|
||
- No staging, no commits, HEAD unchanged at `277ef5d0`.
|
||
|
||
## Files touched (round 2 additions)
|
||
|
||
- `src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs` --
|
||
now 663 lines (was 540): F1's IsAcknowledgementStillPending; F2's
|
||
BindPublication/Publication accessor replacing the constructor
|
||
parameter; DiscardAll().
|
||
- `src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs` --
|
||
BindRetirementNotification multicast conversion.
|
||
- `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs` --
|
||
LocalPlayerFirstEntry property + construction in all 3 ctors + second
|
||
retirement-notification bind + BeginSessionClear wiring +
|
||
RuntimeEntityObjectOwnershipSnapshot field/IsConverged/CaptureOwnership
|
||
fold.
|
||
- `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerFirstEntryStateTests.cs`
|
||
-- now 827 lines (was 758): 1 new F1 test; Fixture now binds/uses
|
||
`Lifetime.LocalPlayerFirstEntry` instead of a standalone instance; fixed
|
||
Dispose ordering; 2 tests renamed and rewritten for automatic
|
||
convergence per F2(c).
|
||
|
||
GameRuntime.cs remains untouched (see F2(b) note above for why). No
|
||
production caller of Advance anywhere.
|
||
|
||
================================================================
|
||
Review round 3 -- H1 + H2 hardening (final verdicts: retail PASS,
|
||
architecture PASS)
|
||
================================================================
|
||
|
||
## H1 -- NotifyRetirement snapshot before iterating
|
||
|
||
`RuntimeInitialCreateResidenceState.NotifyRetirement` iterated the live
|
||
`_retirementNotifications` List<> directly. A subscriber binding a NEW
|
||
notification from inside a retirement callback it is itself receiving
|
||
(unreachable today -- only 2 subscribers exist, neither rebinds -- but
|
||
becomes reachable the instant C3c adds a runtime-bound third subscriber)
|
||
would throw "Collection was modified" on the very next iteration step.
|
||
Fixed per the reviewer's exact instruction, matching
|
||
`RuntimeEntityObjectEventStream`'s own copy-on-write dispatch precedent:
|
||
`foreach (... in _retirementNotifications.ToArray())`. `ToArray()` (not a
|
||
`Volatile`-guarded array swap like the event stream) was the right
|
||
granularity here since binding only ever happens a handful of times at
|
||
construction, never on a hot per-frame path -- documented on the method's
|
||
own doc comment with that reasoning spelled out.
|
||
|
||
New test (`RuntimeInitialCreateResidenceStateTests.cs`):
|
||
`RetirementNotificationBoundReentrantlyDuringDispatchDoesNotCorruptTheCurrentIteration`
|
||
-- binds a notification that, on its first invocation, reentrantly binds a
|
||
THIRD one; triggers a real retirement via `Forget`; asserts no exception,
|
||
that the newly-bound subscriber does NOT see the in-flight retirement (not
|
||
required to), and that it DOES see the next one.
|
||
|
||
## H2 -- unbound-Publication transactional failure
|
||
|
||
Root cause confirmed: `AdvanceCore` had no check for an unbound
|
||
`_publication` before mutating anything. An Advance call in the window
|
||
before a host calls `BindPublication` would run the FULL authored-mover
|
||
Setup-read/PrepareMover call (mutating `RuntimeSetPositionState`'s own
|
||
`_preparedMovers`) and create/store this class's own `Progress` entry
|
||
BEFORE the FIRST `Publication` dereference (inside the `MoverPrepared`
|
||
stage) throws -- leaving a poisoned `Progress` entry in `_progress` that a
|
||
LATER, unrelated `Discard`/`DiscardAll` call (from a retirement
|
||
notification or session-clear fan-out) would ALSO throw on, corrupting
|
||
someone else's teardown.
|
||
|
||
Fix: `AdvanceCore`'s very first statement (before `_progress.TryGetValue`,
|
||
before the ABA check, before anything) is now `_ = Publication;` -- the
|
||
existing throws-if-unbound accessor, referenced purely for its side
|
||
effect, so the whole call fails transactionally with nothing yet mutated.
|
||
Also hardened `Discard`/`DiscardAll` to tolerate an unbound `_publication`
|
||
defensively (`if (_publication is null) return;` before touching
|
||
`Publication.Discard`/`DiscardActivation`) -- both documented as
|
||
structurally unreachable post-H2 (a `Progress` entry can only exist if
|
||
`Advance` already ran, which now requires a bound `Publication` first) and
|
||
guarded anyway as belt-and-suspenders so no future caller shape can turn
|
||
an already-surfaced `Advance` failure into a SECOND throw from inside an
|
||
unrelated fan-out.
|
||
|
||
New tests (`RuntimeLocalPlayerFirstEntryStateTests.cs`):
|
||
- `AdvanceWithUnboundPublicationThrowsTransactionallyBeforeAnyStateMutation`
|
||
-- constructs a bare `RuntimeEntityObjectLifetime` (its own
|
||
`LocalPlayerFirstEntry` is naturally unbound, since only this test file's
|
||
own `Fixture` calls `BindPublication`), registers a residence, calls
|
||
`Advance` with no bound Publication, asserts the throw AND that the
|
||
residence lease/`ActiveCount` are completely untouched, THEN
|
||
binds a real Publication and confirms the SAME token still drives
|
||
correctly to `AwaitingActivation` -- proving nothing was corrupted by the
|
||
failed attempt.
|
||
- `BindPublicationTwiceThrows` -- the standard bind-once guard test,
|
||
matching every other `BindX` method in this class family.
|
||
|
||
## Final gate results (round 3, last of the slice)
|
||
|
||
- `dotnet build src/AcDream.Runtime/AcDream.Runtime.csproj -c Release`:
|
||
0 errors, 0 warnings.
|
||
- `dotnet build AcDream.slnx -c Release`: 0 errors (warning count reported
|
||
as 0 on this incremental rebuild since no other project's files changed
|
||
and MSBuild skipped re-analyzing them as up-to-date; the prior two
|
||
rounds already confirmed 21 pre-existing warnings, all in untouched test
|
||
files, with a from-scratch build).
|
||
- Focused filter
|
||
`FullyQualifiedName~Publication|~Residence|~Executor|~SetPositionState|~FirstEntry`:
|
||
312/312 passed (309 + 3 new: 1 H1 + 2 H2).
|
||
- Complete `AcDream.Runtime.Tests`: 948/948 passed (932 baseline + 16 new
|
||
across the whole C3a slice), 0 skips.
|
||
- Complete solution (`dotnet test AcDream.slnx -c Release --no-build -m:1`):
|
||
every project reports 0 failed -- App 4028/3 skips, Bake 15/0, Cli 4/0,
|
||
Content 124/0, Core.Net 762/0, Core 4242/1 skip, Headless 77/0, Runtime
|
||
948/0, UI.Abstractions 543/0.
|
||
- `git diff --check`: clean. Modified files now additionally include
|
||
`tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs`
|
||
(+47/-0, the new H1 test) alongside
|
||
`RuntimeInitialCreateResidenceState.cs` (54 changed lines, up from 39 in
|
||
round 2 -- the ToArray snapshot + doc comment) and the unchanged round-1/2
|
||
files. The pre-existing eight dirty paths remain line-ending noise only.
|
||
- No staging, no commits, HEAD unchanged at `277ef5d0`.
|
||
|
||
## Files touched (round 3 additions)
|
||
|
||
- `src/AcDream.Runtime/Entities/RuntimeInitialCreateResidenceState.cs` --
|
||
`NotifyRetirement` now snapshots via `ToArray()`.
|
||
- `src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs` --
|
||
now 690 lines (was 663): upfront unbound-Publication check in
|
||
`AdvanceCore`; `Discard`/`DiscardAll` unbound-Publication tolerance.
|
||
- `tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs`
|
||
-- +1 new H1 test.
|
||
- `tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerFirstEntryStateTests.cs`
|
||
-- now 922 lines (was 827): +2 new H2 tests.
|
||
|
||
This is the last round of code changes for C3a per the coordinator's
|
||
message. GameRuntime.cs remains untouched throughout the whole slice; no
|
||
production caller of Advance anywhere.
|
||
|
||
## C3b implementer progress (remote body construction at Create)
|
||
|
||
- Verified HEAD d62b9950 on codex/port-claude-agents; 8 protected dirty paths untouched.
|
||
- Read: plan (C3b scope), float-gates doc (3 byte-certain gates), retail-notes.md
|
||
(CreateObject 0x00558870 order; set_description 0x00514F40 order; PhysicsDesc::UnPack),
|
||
writer map (6 canonical SetPhysicsBody writers), PhysicsBody.cs,
|
||
RuntimeInitialCreateResidenceState.cs, RuntimeLocalPlayerFirstEntryState.cs (C3a shape),
|
||
RuntimeSetPositionState mover/submit/ack/park/retry paths, RuntimePhysicsState bind sites,
|
||
route classifier, C3a test harness.
|
||
- Resolved SetMotionTableID(0) semantics from pseudo-C: CPhysicsObj::SetMotionTableID
|
||
0x00512780 (pc:280528) fails ONLY when part_array==0 (005127da) or
|
||
MotionTableManager::Create fails for a NONZERO id (CPartArray::SetMotionTableID
|
||
0x005186E0, pc:286732, 0051872f); id==0 skips manager creation (0051871f) and
|
||
returns 1 -> gate PASSES for zero id. CPhysicsObj skips MakeMovementManager for
|
||
INVALID_DID (005127ca).
|
||
- Recovered PhysicsDesc ctor defaults 0x0051D4D0 (pc:292056): friction "33s?" =
|
||
0x3F733333 = 0.95f; elasticity 0.05f; translucency 0 (memset); scale 1; state 0x400c08.
|
||
- Recovered set_elasticity 0x0050FD40 (pc:277817): <0 -> 0; <=0.1 -> value; >0.1 -> 0.1.
|
||
Cross-checked ACE PhysicsGlobals.MaxElasticity = 0.1f (PhysicsObj.cs:3586-3599).
|
||
- Design: RuntimeRemoteFirstEntryState (Entities/, no publication chain) with stages
|
||
MoverPreparation -> BodyConstruction (via canonical RuntimePhysicsState.GetOrCreatePhysicsBody
|
||
factory, retail set_description order) -> Submit (lease.Placement) -> Withdraw/Place ack
|
||
(TryPeekProjection loop for deferred parks) -> Execute. Pure builder
|
||
RuntimeRemoteBodyDescription + construction receipt for gate proofs.
|
||
- IMPLEMENTED: RuntimeRemoteBodyDescription.cs (288 L, pure set_description-ordered
|
||
construction + gated receipt), RuntimeRemoteFirstEntryState.cs (620 L, six-stage
|
||
resumable conductor, no publication chain), lifetime wiring (+51/-2: construction in
|
||
all 3 ctors, third multicast retirement binding, BeginSessionClear DiscardAll,
|
||
RemoteFirstEntryActiveCount snapshot field + IsConverged clause), tests (879 L, 29
|
||
tests, all pass first run).
|
||
- GATES: Runtime build 0W/0E; solution build 0 errors, 21 pre-existing warnings none in
|
||
changed files; focused filter (Residence|Executor|SetPositionState|FirstEntry|
|
||
RemoteEntry) 247/247; complete Runtime 977/977; git diff --check clean; 8 protected
|
||
dirty paths + AGENTS.md untouched; nothing staged/committed.
|
||
- No conflicts stopped on: motion-table zero-id semantics resolved from pseudo-C
|
||
(gate passes for id 0); elasticity clamp recovered (0..0.1) + ACE cross-check;
|
||
PhysicsDesc ctor defaults recovered for absent-wire fields.
|
||
- REVIEW ROUND (arch M1/M2 + retail R1/R2) closed: construction receipt now rides the
|
||
terminal Advance out-param (M1); shared RuntimeFirstEntryAcknowledgement.IsStillPending
|
||
used by BOTH conductors + new delete-after-commit-before-ack abandonment test (M2);
|
||
movement branch re-gated on buffer non-emptiness per UnPack's buff_length!=0 assignment
|
||
(R1, both-ways tests; parser's empty-buffer wrapper confirmed at CreateObject.cs:597);
|
||
elasticity NaN -> 0f per retail's first-arm unordered (R2a, ACE divergence noted);
|
||
friction NaN skip kept + commented per gates-doc quirk (R2b; doc addendum text in
|
||
final report); translucency NaN matches retail apply-bucket already (noted).
|
||
- FINAL GATES: focused 252/252; complete Runtime 982/982; build 0 errors, no warnings in
|
||
changed files; git diff --check exit 0; nothing staged; protected paths untouched.
|
||
|
||
# ============================================================
|
||
# C3c — THE HOST FLIP (new session)
|
||
# ============================================================
|
||
|
||
## Reading phase (complete)
|
||
- Verified worktree HEAD 78f1eb18 on codex/port-claude-agents; 8 dirty files = protected list.
|
||
- Read: c3c-contract.md, plan doc C0-C3b notes, route inventory routes 1+8 + cross-cutting,
|
||
canonical-body-writer-map, both conductors, executor surface, channel + subscription +
|
||
retry slot, both sinks, both session routes, GameRuntime, RuntimeLocalPlayerMovementState,
|
||
PlayerModeController (protected-noise), LiveEntityRuntime placement family, materializer,
|
||
HeadlessSessionWorldProjection, RuntimeLiveEntitySessionController, publication Prepare,
|
||
RuntimeFirstEntryAcknowledgement, synchronous event-stream dispatch, AcknowledgeProjection
|
||
(strict FIFO-head, non-idempotent), PublishExecutorCompletion (snapshot carries body
|
||
position/orientation + token.ExactCellId; residence released before publish).
|
||
|
||
## Design conclusions
|
||
1. One acknowledger per receipt: conductors consume their own initial Place/Withdraw
|
||
(proven by AwaitingReceiptAcknowledgement tests). Both host sinks must return false
|
||
(leave-at-head) for Place/Withdraw of an entity with an ACTIVE initial-create
|
||
residence (TryGetInitialCreateResidence discriminator).
|
||
2. ExecutorCompleted = the presentation-binding receipt (C3-1's purpose). Graphical sink
|
||
applies Place-shaped presentation from its snapshot (celless => ack-and-ignore);
|
||
returns false until sidecar+backend ready (existing FIFO-head retry semantics).
|
||
3. Drive points: graphical = post-hydration in the Create flow + wrap the per-frame retry
|
||
slot callback (drive conductors then RetryPending). Continuation placements completed
|
||
via C0 fused TryPrepareAndSubmitAuthoredPlacement + AcknowledgeProjection.
|
||
4. GameRuntime first act: LocalPlayerFirstEntry.BindPublication(Movement.PhysicsPublication)
|
||
after AttachPhysicsPublication (GameRuntime.cs:260-265).
|
||
|
||
## C3c implementation state at session end
|
||
- ALL FIVE SCOPE ITEMS IMPLEMENTED in production code; complete solution builds 0 errors.
|
||
- Runtime tests 982/982 GREEN (3 direct-sink tests rewritten to started-generation + driven-conductor form).
|
||
- Headless tests 74/77 (3 failures: WorldProjectionHydratesCanonicalMovementAndTeleportState + 2 others — old
|
||
SynchronizeLocalPlayer expectations; fixtures need conductor-driven rework).
|
||
- App tests 3,865/4,031 passing, 163 failing after central fixture repair (LiveEntityRuntimeFixture now binds
|
||
generation 1). Remaining classes: (a) ~90 fixtures with private lifetimes and no generation bound
|
||
("cannot acquire a structurally valid initial residence lease"); (b) ~40 hand-built spawns failing
|
||
HasConsistentCreateIdentityAndParent ("inconsistent instance or parent projections");
|
||
(c) ~30 behavior-expectation updates (suppressed-until-receipt visibility, FullCellId staying 0 for
|
||
undriven residences).
|
||
- New contract-required integration tests NOT yet written; connected gates NOT reached (automated gates
|
||
not green — per gate order, stopped and reported).
|
||
- Nothing staged or committed. Protected dirty paths untouched except the sanctioned surgical
|
||
PlayerModeController.cs touch (flagged).
|
||
|
||
## Continuation session (fixture repair)
|
||
- App: 443 -> 163 -> 93 -> 50 failing (3,978/4,031 passing). Repairs, all mechanical, NO assertion changes:
|
||
(1) generation binds: LiveEntityRuntimeFixture (all 5 overloads), LiveEntityHydrationControllerTests fixture,
|
||
EquippedChildProjectionWithdrawalTests fixture (own lifetime + production drive controller + landblock
|
||
collision generation);
|
||
(2) consistent-spawn repair: new shared tests/AcDream.App.Tests/LiveEntitySpawnFixture.cs
|
||
(WithConsistentPhysics extension deriving the nested PhysicsDesc from flattened fields), applied to
|
||
CreateSupersessionRecovery, Vfx light, DeferredLifecycle, LocalPlayerTeleport, LiveAppearanceAnimation,
|
||
StreamingFrame builders; per-file physics blocks for LiveEntityPhysicsHostOwnershipTests (19->0) and
|
||
EquippedChildProjectionWithdrawalTests child/embedded-parent spawns (23->0);
|
||
(3) conductor-completion where legacy direct application now requires a released residence
|
||
(NoPositionCreateParent test: CompleteFirstEntry before TryApplyCreateParent).
|
||
- Headless: 77/77 GREEN. The 3 SynchronizeLocalPlayer-era tests rewritten CONDUCTOR-DRIVEN with the real
|
||
host wiring (host.Start -> RegisterEntityWithInitialResidence -> drive controller constructed BEFORE
|
||
registration -> HeadlessSessionWorldProjection pump). All original assertions preserved verbatim
|
||
(controller identity, LocalEntityId, positions, PortalSpace/InWorld, CenterCount 3/2, receipt-validation
|
||
no-authority invariants). Coverage not shrunk; WorldProjectionHydratesCanonicalMovementAndTeleportState now
|
||
doubles as a headless first-entry integration flow.
|
||
- Runtime: 982/982 GREEN (incl. probe-restoration build).
|
||
- Autowalk probes RESTORED at the Runtime site (diagnostic-owner pattern, PhysicsDiagnostics.ProbeAutoWalkEnabled):
|
||
[autowalk-target] + [autowalk-end reason=interrupt] in RuntimeLocalPlayerPhysicsPublicationState.Prepare's
|
||
host/motion closures; [autowalk-end reason=complete] retained App-side on the approach-lifetime MoveToComplete.
|
||
|
||
## Expectation changes
|
||
(Continuation 4 — the logged pass. Clause key: (1) suppressed-until-receipt
|
||
visibility; (2) undriven-residence semantics; (3) residence-gated
|
||
Place/Withdraw with ExecutorCompleted as the presentation-binding receipt;
|
||
(4) sealed-setter lifecycle routing. Where the true mechanism is committed
|
||
C3a/C3b/C3-1 residence design the clause labels do not literally name —
|
||
SameIncarnationCreate FIFO staging (AD-59), conductor-built bodies at Create
|
||
(C3b), no create-authority advance outside the residence transaction — the
|
||
line cites the closest clause PLUS the design mechanism; all such lines are
|
||
flagged [interp] for coordinator veto.)
|
||
|
||
1. CurrentGameRuntimeAdapterTests.DirectAndGraphicalHosts_ProduceIdenticalEntityObjectTrace
|
||
→ old: both hosts register + hand-driven apply chain produce identical
|
||
entity-object traces → new: at the deliberate zero generation BOTH hosts
|
||
refuse the initial Create transactionally with the identical structural
|
||
error ("cannot acquire a structurally valid initial residence lease"),
|
||
identical EMPTY traces, zero entity/object counts → clause (2). Full
|
||
driven-flow parity moved to the new C3c integration tests.
|
||
2. UpdateFrameOrchestratorTests.GameplayInputOwnersUseTypedSeamsWithoutGameWindowBackReferences
|
||
→ old pinned PlayerModeController source order: PreparePositionForCommit
|
||
→ EnterChaseMode → SelectStableHostWithoutRebind → SyncPose →
|
||
InstallOrRebind → SetPosition(initial) → CommitPreparedPosition →
|
||
`_controllerSlot.Controller = controller` → IsPlayerMode=true → new pinned
|
||
order: controller.IsRuntimePublished gate → EnterChaseMode → SyncPose →
|
||
`_hostSlot.Host = playerHost` → IsPlayerMode=true → clause (4) (the
|
||
deleted markers were exactly the App-side controller construction+commit).
|
||
3. LiveEntityHydrationControllerTests.CompletedSpatialRecovery_DetectsCreateVersionDriftWithoutNestedHydrationRequest
|
||
→ old: PositionSequences [1,1,2], last purpose CreateSupersessionRecovery,
|
||
InstalledCreateIntegrationVersion 2UL → new: [1,1], SpatialRecovery, 1UL
|
||
→ clause (3) [interp]: a post-residence same-generation Create is
|
||
description-only at registration; its position churn flows through the
|
||
freshness-gated events tail; create authority advances only inside the
|
||
residence transaction, so no drift-replay fires.
|
||
4. LiveEntityHydrationControllerTests.TimestampCallbackFresherSameGeneration_StopsOuterCreateVersion
|
||
→ old: single materialize from the nested newer spawn (PositionSequences[0]
|
||
== 2) → new: single materialize from the admission-frozen seq-1 create
|
||
(== 1); the seq-2 facts commit at the executor drain in array order →
|
||
clause (3) [interp, AD-59 FIFO staging].
|
||
5. LiveEntityHydrationControllerTests.ObjectRemovalCallbackFresherSameGeneration_WinsOuterReplacement
|
||
→ old: last materialize seq 3 + DoesNotContain(2, Skip(1)) → new: last
|
||
materialize is the admission-frozen seq-2 replacement + DoesNotContain(3)
|
||
(the seq-3 facts commit at the drain and never re-materialize) →
|
||
clause (3) [interp, AD-59].
|
||
6. LiveEntityHydrationControllerTests.FailedCompletedSupersession_RemainsPendingUntilExactRetry (both rows)
|
||
→ old: InstalledCreateIntegrationVersion == (retryFromLandblock ? 2 : 3)
|
||
→ new: == 2 in both rows (the retry retransmit no longer advances create
|
||
authority) → clause (3) [interp]. Drift probe swapped from a nested
|
||
OnCreate to record.Canonical.AdvanceCreateAuthority() (the drain-stage
|
||
advance) so the retained-obligation/exact-retry machinery under test
|
||
still fires; every other assertion verbatim.
|
||
7. LiveEntityInboundAuthorityGateTests.Position_CanonicalInventoryObjectIsAcceptedBeforeProjectionExists
|
||
→ old: accepted.PositionAuthorityVersion == 2 (post-apply) → new: == 1
|
||
(admission-time; the merge is queued behind the pending residence and its
|
||
authority advance commits at the drain) → clause (2).
|
||
8. Probe swaps with ALL assertions verbatim (logged for transparency,
|
||
mechanism = same-generation Creates no longer advance create authority at
|
||
registration; modeled by record.Canonical.AdvanceCreateAuthority(), the
|
||
exact drain-stage advance) [interp, clause (3)]:
|
||
- LiveEntityHydrationControllerTests.CompletedSupersessionReadyFailure_RetriesWholeCommitBoundary (both rows)
|
||
- LiveEntityHydrationControllerTests.ReadyPublisher_RevalidatesExactRecordBetweenEveryStage (3 rows)
|
||
- LiveEntityHydrationControllerTests.ReadyPublisher_RevalidatesAfterReentrantRenderProjectionCallback
|
||
- LiveEntityHydrationControllerTests.EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback
|
||
- LiveEntityCreateSupersessionRecoveryTests.Recovery_CreateVersionAdvanceDuringAppearance_StopsLaterOwners
|
||
9. LiveEntityHydrationControllerTests.ParentSupersession_CompletesAtExactAttachedReadyBoundary (both rows)
|
||
→ scenario repairs: (a) parent registered up front (retail queues a child
|
||
Create under an unaddressable parent — R5-1 QueueBlobForObject — instead
|
||
of applying it); (b) drift probe as in item 8; (c) the OnSpawnAction hook
|
||
now mirrors the production relationship owner's sticky-residence
|
||
conversion (AwaitRuntimePlacement → LegacyImmediate) at the
|
||
world→attached kind transition. Assertions verbatim → [interp, clause (3)].
|
||
10. LiveEntityHydrationControllerTests.PositionAfterPickup_ReentersWithSameEntityBodyAndResources
|
||
→ body scaffolding changed from seeding a fresh PhysicsBody to capturing
|
||
the conductor-built canonical body (C3b bodies-at-Create; factory now
|
||
throws if missing — a STRONGER assertion). Identity-preservation
|
||
assertions verbatim → [interp, C3b].
|
||
11. LiveEntityRuntimeTests.PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder
|
||
→ old: seeded RemoteMotionRuntime bodies observe state sync across
|
||
bind/state arrival orders → new: the conductor-built canonical bodies
|
||
are observed (C3b never-clobber forbids seeding a replacement); "arrival
|
||
order" is now SetState-FIFO'd-before-the-drain vs
|
||
SetState-after-completion; the two expected state-flag transforms are
|
||
UNCHANGED → [interp, C3b + clause (3)].
|
||
|
||
## Remaining at session end (App 50)
|
||
- LiveEntityHydrationControllerTests 21 (supersession/recovery semantics + 5 "publication owner not bound"
|
||
= the fixture drive needs a bound publication chain for isLocalPlayer leases: add
|
||
RuntimeLocalPlayerMovementState+Identity+PublicationState+BindPublication to that fixture);
|
||
- RuntimePlacementPresentationSinkTests 4 (sink behavior changed by design - residence gate + ExecutorCompleted
|
||
presentation; these need logged expectation updates per contract clauses);
|
||
- LiveEntityRuntimeTests 4, LiveEntityPresentationControllerTests 4, remaining consistent-spawn stragglers in
|
||
builders that already carry physics blocks (field-level mismatches), CurrentGameRuntimeAdapterTests 2
|
||
(deliberate zero-generation binds), misc singles.
|
||
- NOT STARTED: the five contract integration tests; Release build; complete solution (-m:1, ACDREAM_PAK_PATH);
|
||
connected gates (blocked behind green automated gates per the pinned gate order).
|
||
- No production regression found: the one suspicious trace (FullCellId=wire-cell in a pickup-supersession test)
|
||
was ruled out against ApplyAcceptedSpawn (object-table only, no cell write) - it is a fixture-flow artifact.
|
||
- Post-log continuation: hydration fixture publication chain bound (Movement/Identity/PublicationState +
|
||
BindPublication + ServerGuid) -> hydration 21 -> 18; App total 50 -> 47 (3,981/4,031). Runtime 982/982 and
|
||
Headless 77/77 re-verified green after all repairs. Nothing staged; protected paths still untouched
|
||
(PlayerModeController touch remains the one sanctioned exception).
|
||
|
||
## Continuation 2 (guard-zone stop)
|
||
- Hydration fixture now also carries the REAL RuntimePlacementProjectionSubscription (ack-only sink mirroring
|
||
production rules: Discard/ExecutorCompleted ack, Place/Withdraw left for conductors) - fixture is now the
|
||
full production wiring shape (generation + collision generation + publication chain + drive pump +
|
||
subscription). Hydration failures unchanged at 18 => the FIFO-wedge hypothesis is ruled OUT; the 18
|
||
supersession/recovery failures are NOT a simple fixture gap.
|
||
- STOP under instruction-1 guard: the observed symptom (nested same-generation Create during
|
||
RecoverProjection no longer triggers CreateSupersessionRecovery; Assert.Throws sees no throw at line 1247)
|
||
means the initial residence is STILL ACTIVE when the nested create arrives, i.e. the drive after the first
|
||
OnCreate did not complete the conductor in this fixture. Before editing ANY of these 18 tests' assertions I
|
||
need to establish WHY the conductor yields here (candidate: something in the fixture's stub materializer /
|
||
spawn shape leaves the placement deferred or the executor pending) - because if the same yield can happen in
|
||
PRODUCTION's composed path, this is a real flip defect (login supersession burst leaving residences pending),
|
||
not an expectation update. That determination requires a focused diagnostic run I could not complete in the
|
||
remaining session budget.
|
||
- Expectation changes so far: STILL ZERO.
|
||
- Totals at stop: Runtime 982/982, Headless 77/77, App 3,981/4,031 (47 failed, 3 skipped).
|
||
- Items 2-4 (zero-generation adapter tests, physics-block mismatch stragglers, five integration tests,
|
||
Release/solution/connected gates) not reached.
|
||
|
||
## Continuation 3 — DIAGNOSTIC + CLASSIFICATION (coordinator-directed)
|
||
- Instrumented RuntimeFirstEntryDriveController.DriveOne (yield status + FIFO head + residence-active +
|
||
FullCellId per step, file-logged); ran FailedCompletedSupersession_RemainsPendingUntilExactRetry.
|
||
- YIELD CHAIN (before fix): first and only drive step yielded RejectedToken with residenceActive=False and
|
||
fullCell=0x01010001 ALREADY COMMITTED — the residence was retired out-of-band before the pump ever ran.
|
||
- ROOT CAUSE: the hydration fixture's RecordingMaterializer called the internal
|
||
LiveEntityRuntime.MaterializeLiveEntity overload WITHOUT the residence parameter -> LegacyImmediate ->
|
||
legacy RebucketLiveEntity fall-through (LiveEntityRuntime.cs:777) -> CommitRebucket committed the wire cell
|
||
and advanced placement/spatial authority -> residence IsCurrent detected the out-of-band commit and retired
|
||
the lease -> conductor correctly RejectedToken.
|
||
- CLASSIFICATION: FIXTURE ARTIFACT. Production's route-1 materializer
|
||
(src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs, MaterializeProjection: residence =
|
||
retainedRecord?.MaterializationResidence ?? AwaitRuntimePlacement) never takes LegacyImmediate for an
|
||
initial world create, so production cannot reach this retirement path. Post-fix diagnostic re-run:
|
||
status=Completed, fullCell committed by the CONDUCTOR, FIFO drained — the drive converges.
|
||
- Fixture fixed at the choke point (stub materializer now passes AwaitRuntimePlacement; subscription upgraded
|
||
to a production-mirroring FixturePlacementSink: Discard ack, ExecutorCompleted ->
|
||
TryApplyInitialCreateCompletionPresentation, Place/Withdraw residence-gated ->
|
||
TryApplyRuntimePlacementProjection). Diagnostics stripped; Runtime rebuilt 0 errors.
|
||
- Result: hydration failures 18 -> 19 (group did NOT shrink; composition changed — the flows now complete
|
||
their conductors and fail on POST-flip semantic assertions: supersession/recovery expectations against
|
||
suppressed-until-receipt visibility + conductor-committed cells). These 19 + the rest of the inventory are
|
||
now genuinely the logged-expectation-update pass (guard rules unchanged, zero changes logged so far).
|
||
- Totals at stop: Runtime 982/982, Headless 77/77, App 3,980/4,031 (48 failed, 3 skipped). No staging/commits.
|
||
|
||
## Continuation 4 — expectation-update pass + integration tests + gate ladder
|
||
- Session start state re-verified: HEAD 78f1eb18, protected dirty paths intact, nothing staged.
|
||
- Full App run (Release): 47 failed / 3,981 passed / 3 skipped of 4,031 (one fewer
|
||
than the Continuation-3 count of 48; the inventory below is the current truth).
|
||
- Classified inventory: Hydration 19; Withdrawal 6; Sink 4; LiveEntityRuntime 4;
|
||
Presentation 4; RemotePhysicsUpdater 2; CurrentGameRuntimeAdapter 2; singles 6
|
||
(LifecycleStress, UpdateFrameOrchestrator, LiveSessionResetPlan,
|
||
InboundAuthorityGate, CreateSupersessionRecovery, LiveAppearanceAnimation).
|
||
- CLASSIFICATION POLICY DECLARED (auditable): where a required change's true
|
||
mechanism is committed, dual-reviewed C3a/C3b/C3-1 residence design that the
|
||
four clause labels do not literally name (SameIncarnationCreate FIFO staging
|
||
per AD-59; conductor-constructed bodies at Create per C3b; no create-authority
|
||
advance outside the residence transaction), the change is logged under the
|
||
closest clause WITH an explicit mechanism note and flagged in the final
|
||
report for coordinator veto. Verbatim-assertion fixture repairs get no
|
||
clause line per the guard's own exemption, but are listed below.
|
||
|
||
### Fixture repairs (assertions verbatim, no expectation-change lines)
|
||
1. RuntimePlacementPresentationSinkTests.Fixture.Materialize — normalize the
|
||
stale residence (legacy-immediate materialization commits the wire cell
|
||
out-of-band; the query performs lazy retirement) BEFORE tests capture
|
||
ownership snapshots; previously the retirement happened inside the sink's
|
||
own first residence query and shifted SetPositionOperationCount/
|
||
AwaitingSetPositionPreparationCount 1->0 mid-TryApply. 14/14 green.
|
||
2. LiveEntityProjectionWithdrawalControllerTests.Fixture.Spawn — spawn had no
|
||
Physics block but non-null Position/Setup/Scale + InstanceSequence!=0;
|
||
wrapped with the shared WithConsistentPhysics (sanctioned for block-less
|
||
builders). 6/6 green.
|
||
3. LiveEntityPresentationControllerTests.Fixture.Spawn — builder ALREADY
|
||
carries a physics block; field-level fix only (Timestamps.Instance was
|
||
hardcoded 1 vs the instanceSequence parameter used by guid-reuse tests).
|
||
NO blind wrap; RawState untouched. 12/12 green.
|
||
4. RemotePhysicsUpdaterTests boundary Spawn(ushort instanceSequence, ...) —
|
||
same field-level Timestamps.Instance fix; RawState untouched. Both green.
|
||
5. LiveAppearanceAnimationTests.Capture_... — block-less cellless spawn with
|
||
InstanceSequence 1; WithConsistentPhysics wrap (file's other builder
|
||
already used it). Green.
|
||
|
||
### Continuation 4 — production changes beyond the inherited diff (both
|
||
### flagged for coordinator review; each is one revertible hunk)
|
||
1. src/AcDream.App/Rendering/EquippedChildRenderController.cs (TryAttach,
|
||
before MaterializeLiveEntity): converts a retained residence-managed
|
||
child's sticky residence AwaitRuntimePlacement → LegacyImmediate at the
|
||
world→attached kind transition. WITHOUT this, equipping a world-created
|
||
(cut-over) item throws at LiveEntityRuntime.cs:655's residence-change
|
||
guard ("cannot change its materialization residence from
|
||
AwaitRuntimePlacement to LegacyImmediate") because the attach path passes
|
||
the default LegacyImmediate — while the flip's own materializer comment
|
||
(DatLiveEntityProjectionMaterializer.cs:712-722) states equipped children
|
||
must carry LegacyImmediate so a later drop-to-world stays legacy "by
|
||
construction". Found via ParentSupersession_CompletesAtExactAttachedReadyBoundary;
|
||
production-reachable (pickup → CreateParent → TryAttach with a retained
|
||
sidecar). This completes the flip's documented design, not new invention.
|
||
2. src/AcDream.App/World/LiveEntityRuntime.cs (RebucketLiveEntity,
|
||
residence-managed branch): while the initial-create residence is still
|
||
ACTIVE, the presentation-only move now refuses (returns false) — the
|
||
completion receipt is the entity's first world-visible moment (clauses
|
||
1/3). The inherited flip had made the presentation-only move
|
||
unconditional, re-opening the mid-registration reentrancy hazard pinned
|
||
by RuntimePlacementPendingMaterialization_OwnsResourcesWithoutPublishingResidence
|
||
(a resource-registration observer could install a bucket for a suppressed
|
||
record before its placement committed). A STALE residence lazily retires
|
||
inside the same query, so post-residence legacy moves are unaffected; the
|
||
completion-receipt path calls RebucketLiveEntityPresentationOnly directly
|
||
and never crosses this gate.
|
||
|
||
### Continuation 4 — additional fixture repairs (assertions verbatim)
|
||
6. LiveEntityLifecycleStressTests fixture — generation bind on its private
|
||
lifetime. 7. LiveSessionResetPlanTests.GraphicalResetHost — entity seeded
|
||
through the legacy direct RegisterEntity (session-less GameRuntime has
|
||
generation 0; the subject is reset/teardown retry, not the create flow).
|
||
8. CurrentGameRuntimeAdapterTests.GraphicalObserverFailure — session started
|
||
first (its siblings' existing pattern). 9. Hydration RecordingMaterializer
|
||
— mirrors the production materializer's self-projection branch (committed
|
||
cell + no active residence → presentation-only rebucket); fixed
|
||
PositionAfterPickup/PositionAfterInventoryOnlyCreate spatial-projection
|
||
truth. 10. PartialProjection_IsRetriedInsteadOfMistakenForCompletedHydration
|
||
— models the production per-frame pump (FirstEntry.DriveAll) before the
|
||
streaming recovery (the failed Create unwound before OnCreateCore's own
|
||
pump; an undriven residence leaves FullCellId 0 and streaming candidates
|
||
key off the committed cell). 11. LiveEntityRuntimeFixture.CreateDriven —
|
||
NEW driven variant (collision generation + engine landblock + production
|
||
RuntimeFirstEntryDriveController + ack-only subscription mirroring host
|
||
rules); applied to InitialChildCreate_PreservesParentEventQueued...,
|
||
PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp, and
|
||
PhysicsStateAndRemoteBodyStaySynchronizedAcrossEitherArrivalOrder.
|
||
|
||
### Checkpoint: FULL App suite GREEN — 4,028 passed / 0 failed / 3 skipped
|
||
### of 4,031 (Release). Next: Runtime + Headless re-verification, then the
|
||
### five contract integration tests.
|
||
|
||
### Continuation 4 — five contract integration tests (all green, real host
|
||
### wiring: registration -> subscription -> RuntimeFirstEntryDriveController;
|
||
### no hand-called conductor sequences)
|
||
NEW tests/AcDream.App.Tests/World/RuntimeFirstEntryHostIntegrationTests.cs
|
||
(fixture: real RuntimeEntityObjectLifetime + generation + committed collision
|
||
generation + LiveEntityHydrationController.OnCreate route + production
|
||
RuntimePlacementPresentationSink behind the real
|
||
RuntimePlacementProjectionSubscription + real drive controller + local-player
|
||
publication chain; materializer is the production-mirroring double):
|
||
1. InitialCreate_ResidenceConductorReceipt_BindsWorldVisibilityExactlyOnce —
|
||
residence begins once, sidecar provably suppressed at materialize time,
|
||
conductor completes inside the Create transaction, exactly one visibility
|
||
edge, world snapshot at the committed pose, all ledgers drained.
|
||
2. DeferredParentCreate_StaysInvisibleUntilParentReplay — unaddressable-parent
|
||
child: no canonical/sidecar/presentation, queued under the parent GUID;
|
||
parent Create replays it (real registration delegate), next-frame pump
|
||
completes the parented conductor; child celless + presentation-suppressed,
|
||
only the parent world-visible.
|
||
3. LocalLogin_PresentationAttachFailure_RetriesWithoutRuntimeRollback — the
|
||
camera/shadow-analog App attach failure (first visibility binding throws)
|
||
AFTER the Runtime commit: published PlayerMovementController
|
||
(IsRuntimePublished) + canonical body + committed cell all survive, the
|
||
completion receipt stays pending, RetryPending() binds presentation with
|
||
the SAME controller instance. NOTE: the literal PlayerModeController
|
||
camera object is not constructed here (its ~20-dependency graph has no
|
||
focused harness); its presentation-only rollback is pinned by the updated
|
||
UpdateFrameOrchestrator source assertions + this receipt-level analog.
|
||
5. GraphicalAndDirectHosts_CommitIdenticalFirstEntryRuntimeFacts — the same
|
||
spawn through the full graphical wiring vs the no-window direct host
|
||
shape (RegisterEntityWithInitialResidence + pump + ack-only subscription)
|
||
commits byte-identical canonical first-entry facts (cell, versions, body
|
||
pose/state/InWorld, snapshot sequence, local id).
|
||
NEW in tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs:
|
||
4. MissingPreparedCollisionYieldsTypedRetryAndCompletesWhenAvailable — flaky
|
||
IPreparedCollisionSource (Missing -> Loaded) through the REAL
|
||
HeadlessSessionWorldProjection.ProjectSpawn pump: no exception escapes,
|
||
entity stays tracked/pending with no controller and cell 0, the session
|
||
tick's retry pump completes placement + publishes the controller.
|
||
Totals after the tests: Headless 78/78 (77+1); App integration class 4/4.
|
||
|
||
### Continuation 4 — gate ladder
|
||
1. Complete test projects (Release): App 4,032 passed / 0 failed / 3 skipped
|
||
of 4,035 (4,031 + 4 new integration tests); Runtime 982/982; Headless
|
||
78/78 (77 + 1 new). GREEN.
|
||
2. dotnet build AcDream.slnx -c Release --nologo: 0 errors, 18 warnings
|
||
(pre-existing test-project warnings; none in files this session touched).
|
||
GREEN.
|
||
3. Complete solution (-m:1, ACDREAM_PAK_PATH=C:\Users\erikn\Documents\
|
||
Asheron's Call\acdream.pak): IN FLIGHT (background).
|
||
4. Connected gates: ACE confirmed listening on UDP 9000 (PID 22100),
|
||
C:\ACE\Server\ACE_Log.txt present, no AcDream.App/acclient processes
|
||
running. Will run after gate 3.
|
||
|
||
### Continuation 4 — gate ladder RESULT (stopped at gate 4 per pinned order)
|
||
3. Complete solution (Release, -m:1, ACDREAM_PAK_PATH): GREEN — App 4,032/3
|
||
skips (4,035), Bake 15, Cli 4, Content 124, Core.Net 762, Core 4,242/1
|
||
skip (4,243), Headless 78, Runtime 982, UI.Abstractions 543 = 10,782
|
||
passed / 0 failed / 4 skipped of 10,786.
|
||
4. CONNECTED tools/run-connected-world-lifecycle-gate.ps1: **FAIL — STOPPED
|
||
HERE.** Artifacts: logs/connected-world-gate-20260802-122749/
|
||
(report.json Passed=false; capped/ + uncapped-reconnect/ each with
|
||
stdout.log, stderr.log, artifacts/). Both sessions connected, entered
|
||
world as 0x5000000A, ran the 54-command UI probe, requested AND received
|
||
graceful logout confirmation, then CRASHED identically with an unhandled
|
||
System.InvalidOperationException: "A sealed, retired, or discarded
|
||
Runtime movement controller cannot be mutated."
|
||
EXACT CHAIN (identical in both sessions):
|
||
PlayerMovementController.EnsureConfigurationMutable
|
||
(src/AcDream.Runtime/Gameplay/PlayerMovementController.cs:859)
|
||
<- SetCharacterSkills(:1248)
|
||
<- RuntimeMovementSkillProjection.ApplyTo(
|
||
src/AcDream.Runtime/Gameplay/RuntimeMovementSkillProjection.cs:21)
|
||
<- LiveSessionRuntimeFactory.ApplyMovementStats(
|
||
src/AcDream.App/Net/LiveSessionRuntimeFactory.cs:327; recompute
|
||
callback registered at :300 in CreateCharacterBindings)
|
||
<- LiveSessionEventRouter.RecomputePlayerQualities(
|
||
src/AcDream.Runtime/Session/LiveSessionEventRouter.cs:406)
|
||
<- ClientObjectTable.Ingest(WeenieData)
|
||
<- ObjectTableWiring.ApplyEntitySpawn
|
||
<- RuntimeEntityObjectLifetime.ApplyAcceptedSpawn(:926)
|
||
<- LiveEntityHydrationController.OnCreateCore(:298) — an ordinary
|
||
inbound Create datagram, processed AFTER the graceful-logout
|
||
confirmation (stdout timeline: probe -> logout confirmed ->
|
||
post-logout stat-chain recomputes -> crash).
|
||
DIAGNOSIS (report-only, no fix attempted): the flip made the local
|
||
movement controller Runtime-published with a SEALED configuration
|
||
lifecycle (mutable only pre-publication; retired at teardown). The
|
||
character-bindings quality-recompute subscription
|
||
(LiveSessionRuntimeFactory.CreateCharacterBindings ->
|
||
ApplyMovementStats -> controller.SetCharacterSkills) still mutates the
|
||
controller directly on EVERY player-quality recompute; once the
|
||
controller is sealed (published) or retired (logout teardown), that
|
||
mutation throws. This is exactly the review-focus item "Sealed
|
||
Controller setter: audit every compile break's fix — each must route
|
||
through the publication lifecycle" — this site was missed because it is
|
||
a RUNTIME mutation (EnsureConfigurationMutable), not a compile break.
|
||
A fix must decide where server skill updates route post-flip (e.g. the
|
||
Runtime movement-state seam / construction options at first entry +
|
||
a Runtime-owned live-skill channel), which is a production design
|
||
decision outside this session's mechanical remit — per the pinned gate
|
||
order the session STOPS at this failure and reports.
|
||
Secondary observation from the same logs (likely the same defect class,
|
||
recorded for the fixer): after the unhandled exception the shutdown path
|
||
reported "status=AbandonedIncomplete, blocked=native window" with
|
||
Silk.NET "You cannot call `Reset` inside of the render loop!" — a
|
||
crash-path artifact, not an independent bug.
|
||
The nine-stop soak (tools/run-connected-r6-soak.ps1) was NOT run (gated
|
||
behind the lifecycle gate's pass).
|
||
- Nothing staged, no commits at any point; HEAD remains 78f1eb18; the 7
|
||
protected dirty paths + AGENTS.md carry only their inherited state
|
||
(AGENTS.md the sole real pre-existing content diff; PlayerModeController
|
||
touched only by the inherited sanctioned flip surgery — this session
|
||
changed only the marker TEST for it, not the file).
|
||
|
||
## C3c-F1 — movement-stat application through the Runtime seam
|
||
### Candidate-window investigation (resolved with evidence)
|
||
- The crash state was RuntimeOwnedDormant, not retired: the failing gate run's
|
||
stdout shows the login world-reveal never completed (collision=False at every
|
||
readiness line; event=cancel at logout with completed=False) — first-entry
|
||
activation stayed DeferredCell for the whole session, so
|
||
RuntimeLocalPlayerMovementState held the dormant controller when the
|
||
post-logout ingest recompute fired. EnsureConfigurationMutable throws for
|
||
dormant (PlayerMovementController.cs:848-861: only Standalone/
|
||
CandidatePreparing/RuntimePublished/dormant+groundPhase return).
|
||
- CandidateSealed is UNREACHABLE through the seam: the sealed candidate is
|
||
never installed into the movement owner (CanPrepare requires
|
||
`_movement.Controller is null`, RuntimeLocalPlayerPhysicsPublicationState.cs:908;
|
||
IsCurrent requires CanCommitRuntimeOwnedController(epoch, null), :937-939),
|
||
and Prepare→Commit runs back-to-back inside one synchronous Advance step
|
||
(RuntimeLocalPlayerFirstEntryState.cs:395-431) with no callback dispatch
|
||
between (Prepare's construction is callback-free by design, publication
|
||
state :319-334).
|
||
- RuntimeOwnedDormant IS reachable across pump iterations: Commit installs
|
||
the dormant controller (publication state :436) and Evaluate/CommitActivation
|
||
DeferredCell yields AwaitingActivation with stage kept
|
||
(RuntimeLocalPlayerFirstEntryState.cs:493-497) — inbound quality events pump
|
||
between Advance calls. DECISION: apply-immediately-to-dormant (not
|
||
defer-at-commit) — the dormant instance IS the controller that goes live
|
||
(ActivateRuntimePublication at RuntimeSetPositionState.cs:2464 flips the
|
||
same object), the writes touch only PlayerWeenie fields + the mover-flag
|
||
latch (nothing the activation envelope validates), and the precedent is the
|
||
existing dormant channels RefreshDormantRuntimePhysicsState/Vector
|
||
(PlayerMovementController.cs:735-759) that land accepted server facts on
|
||
the dormant owner mid-window. Deferring would need an activation hook and
|
||
would leave the weenie stale for the activation ground-phase dispatch.
|
||
### Implementation
|
||
- PlayerMovementController: internal ApplyCharacterMovementStats(in snapshot)
|
||
— lifecycle switch (live/dormant apply, terminal typed drop) + private
|
||
ApplyCharacterMovementStatsCore (verbatim body of the deleted
|
||
RuntimeMovementSkillProjection.ApplyTo, field writes not gated setters) +
|
||
internal ReportExhaustionAtMovementBoundary (live-only exhaustion dispatch).
|
||
- RuntimeLocalPlayerMovementState: public ApplyCharacterMovementStats(
|
||
RuntimeMovementSkillState) + public ReportExhaustion() + public enum
|
||
RuntimeMovementStatsApplication {AppliedLive, AppliedDormant,
|
||
DroppedNoController, DroppedIncompleteSnapshot, DroppedDisplacedController}.
|
||
Disposed-owner tolerant (typed drop, not ObjectDisposedException) per J3.6.
|
||
- DELETED src/AcDream.Runtime/Gameplay/RuntimeMovementSkillProjection.cs
|
||
(zero remaining consumers).
|
||
- NEW src/AcDream.App/Net/LiveMovementStatsApplier.cs — owns the
|
||
StaminaExhaustionEdgeTracker + logging; observes the exhaustion edge for
|
||
both applied outcomes, dispatches only AppliedLive (dormant: no in-flight
|
||
movement; activation reads the current stamina gate).
|
||
- LiveSessionRuntimeFactory: constructs the applier; OnSkillsUpdated/
|
||
OnMovementStatsUpdated route through it; ApplyMovementStats +
|
||
_staminaExhaustion deleted; ResetPlayerPresentation resets the applier.
|
||
App keeps zero direct configuration mutations on the stats path.
|
||
### Tests (all green at write time)
|
||
- Runtime (7 new in RuntimeLocalPlayerMovementStateTests): live byte-identity
|
||
vs the old direct path (InqRunRate/InqJumpVelocity/CanJump/JumpStaminaCost/
|
||
OwnPvpFlags), dormant-window write lands on the instance that goes live,
|
||
terminal typed drop with unchanged observables + setter-still-throws,
|
||
sealed-candidate defensive row, absent/incomplete drops, disposed-owner
|
||
tolerance, ReportExhaustion lifecycle gating. 19/19 file total.
|
||
- App (3 new in LiveMovementStatsApplierTests): the REAL crash chain
|
||
(real WorldSession + LiveSessionEventRouter + ClientObjectTable.Ingest of
|
||
the player row + real applier callback) against a retired-installed
|
||
controller — no throw, typed displaced drop logged; dormant-window ingest
|
||
applies + values current at activation; absent/incomplete silent skips.
|
||
### Connected gate run 1 (post stats-fix): FAIL — second site of the same class
|
||
- logs/connected-world-gate-20260802-125907: BOTH sessions confirmed graceful
|
||
logout, then crashed on the SAME EnsureConfigurationMutable throw via the
|
||
OTHER App-side mutation my sweep had already classified residual-unsafe:
|
||
LiveEntityNetworkUpdateController.OnState:1006 →
|
||
PlayerMovementController.ApplyPhysicsState(:366) at the dormant controller
|
||
(post-logout inbound SetState; the login reveal never completes in this
|
||
gate profile so the first-entry controller stays dormant all session —
|
||
same as the original 122749 failure). The stats seam itself WORKED: the
|
||
stdout shows repeated "player: applied server movement stats run=10205..."
|
||
dormant applications with no ingest crash.
|
||
- Disposition per the sweep clause ("route each through the seam or report
|
||
why it is already lifecycle-safe" — this site cannot be reported safe):
|
||
routed through the same owner seam pattern. NOT a guard at the throw site:
|
||
the lifecycle decision moved into the owner.
|
||
### Second routing (inbound local-player SetState)
|
||
- PlayerMovementController.ApplyServerPhysicsState (internal, typed):
|
||
live → exact ApplyPhysicsState body; dormant →
|
||
DroppedDormantActivationOwned (the activation transaction re-reads the
|
||
canonical FinalPhysicsState itself via RefreshDormantRuntimePhysicsState
|
||
at both activation phases, and while the accepted SetState is queued
|
||
behind the initial residence the App push carries that same UNCHANGED
|
||
record value — RuntimeEntityObjectLifetime.TryApplyState:1351-1378 queues
|
||
without advancing the record — so the drop is value-preserving by
|
||
construction); terminal → DroppedDisplacedController. New enum
|
||
RuntimeServerPhysicsStateApplication beside the stats enum.
|
||
- LiveEntityNetworkUpdateController.cs:1005-1018 routes through the typed
|
||
entry (result discarded; comment records both gate crash chains).
|
||
- Tests: Runtime ApplyServerPhysicsState_DormantDropsForActivationAndLiveAppliesExactly
|
||
(990/990 Runtime); App source pin C3cF1ProductionWiringTests.
|
||
LocalPlayerInboundSetState_RoutesThroughTheTypedOwnerEntry (no direct
|
||
ApplyPhysicsState caller remains in the file). App 4,036/3 skips of 4,039;
|
||
Headless 78/78.
|
||
### Connected gate run 2 (130455) — FAIL, root cause classified INHERITED; STOPPED per directive
|
||
- Both sessions ran CRASH-FREE (0 unhandled exceptions; capped alive 7+ min
|
||
until my graceful WM_CLOSE, uncapped alive the full 420 s timeout) — the
|
||
F1 crash-class fixes hold live. 418 login entities + 10,384 total ingested
|
||
cleanly with 158 dormant stat applications.
|
||
- Harness failures: capped "client exited waiting for probe complete" (my
|
||
directed close), uncapped "timed out after 420 s waiting for probe
|
||
complete".
|
||
- Wedge mechanism (link-by-link):
|
||
1. First-entry conductor Prepares+Commits the DORMANT controller within
|
||
the first frames (stats lines from stdout line 62).
|
||
2. Activation stays DeferredCell; even after streaming collision readiness
|
||
completes (~40 s, reveal line 223 collision=True ready=True) the
|
||
activation/publication never commits — PlayerModeController logs ONE
|
||
"Runtime first-entry controller ... not committed yet" (line 224,
|
||
PlayerModeController.cs:247-250) and player mode never enters.
|
||
3. Reveal (kind=Login) completes on readiness alone with
|
||
materialized=False (RuntimeWorldTransitState.Complete:685-720 requires
|
||
Materialized only for Portal kind), then per-frame readiness
|
||
re-acknowledgements spam event=rejected reason=readiness-after-terminal
|
||
(9,000+ lines).
|
||
4. AcknowledgeWorldViewportVisible never fires (visible=False forever) →
|
||
probe line 5 "timed out waiting for normal world viewport"
|
||
(RetailUiAutomationScriptRunner.cs:308-313; route line 4-5
|
||
tools/connected-world-lifecycle.route.txt) → probe never prints
|
||
complete → harness fails.
|
||
- NOT an F1 regression — three proofs:
|
||
1. 122749 (zero F1 changes) fails with the IDENTICAL harness failure
|
||
string ("client exited waiting for probe complete", report.json), has
|
||
NO probe-complete/checkpoint/player-mode lines, and its own crash
|
||
(SetCharacterSkills throwing) proves the controller was never published
|
||
there either.
|
||
2. 122749's login was WORSE pre-F1: the per-Create ingest-recompute throw
|
||
killed the process 26 s in (StartedUtc→FinishedUtc) with only 1 entity
|
||
seen vs 418/10,384 under F1 — the coordinator's "122749 entered world
|
||
normally" premise is contradicted by its own artifacts.
|
||
3. The F1 write set cannot affect activation currency: the stats core
|
||
touches only PlayerWeenie fields + the OwnPvpFlags latch; the
|
||
activation envelope checks lifecycle/epochs/body identity only
|
||
(RuntimeLocalPlayerPhysicsPublicationState.cs:961-992, :747-775); the
|
||
DeferredCell decision is RuntimeSetPositionState cell-residency
|
||
machinery untouched by this slice.
|
||
- Residual root cause home: the C3c first-entry activation never resolves
|
||
its deferred destination cell in the graphical host (or its pending drive
|
||
entry stops being re-armed) — RuntimeLocalPlayerFirstEntryState.cs:433-501
|
||
(PublicationCommitted → EvaluateActivation/CommitActivation DeferredCell
|
||
loop) against RuntimeSetPositionState's dormant-activation cell gate.
|
||
That is conductor/publication semantics OUTSIDE the F1 contract →
|
||
STOPPED, no fix attempted, per coordinator directive 3.
|
||
- Client terminated via graceful-close discipline (WM_CLOSE → logout
|
||
confirmed by ACE, clean [session] lines, no crash); harness concluded and
|
||
recorded FAIL; artifacts preserved at
|
||
logs/connected-world-gate-20260802-130455/.
|
||
|
||
## C3c-F2 — the login DeferredCell activation wedge
|
||
|
||
### Step 1 — artifact re-verification (the pinned contract's evidence chain is
|
||
### WRONG on run 122749; both prior classifications were partly unsound)
|
||
Read directly from the primary artifacts, not from either prior report:
|
||
- 122749 (flip WITHOUT F1), capped/stdout.log is 67 lines TOTAL. It shows
|
||
`[UI-PROBE] running 54 UI probe command(s)` (L52) — that line is the probe
|
||
ANNOUNCING its command count, not 54 commands completing. The reveal shows
|
||
ONLY `event=begin` (collision=False) and ONE `event=readiness`
|
||
(collision=False, ready=False, materialized=False, visible=False), then
|
||
`event=cancel ... visible=False`. The smoke plugin saw 1 entity total.
|
||
report.json StartedUtc -> FinishedUtc = 26 SECONDS for BOTH sessions.
|
||
=> the contract's "world became visible / probe's `wait world-visible`
|
||
passed / 54-command probe ran" is falsified. The world was NEVER visible in
|
||
122749; the process died ~9 s after entering world from the F1 crash, i.e.
|
||
BEFORE the ~40 s collision-readiness edge where the wedge manifests.
|
||
- Consequence: 122749 proves NOTHING either way about post-readiness
|
||
activation (it never got there). The F1 agent's "identical harness failure
|
||
string" proof is equally weak (identical string, different causes: crash vs
|
||
timeout). Both prior attributions are unsound; attribution has to come from
|
||
CODE, not from these two logs.
|
||
- 130455 (flip WITH F1) is the only run that reaches the readiness edge:
|
||
L223 readiness collision=True ready=True; L224 `not committed yet` (ONE
|
||
line); L225 `event=complete ... materialized=False`; then 5,577+
|
||
`readiness-after-terminal`; probe L1193 `wait world-visible 30000` timeout.
|
||
|
||
### Step 2 — the contract's primary suspect is STRUCTURALLY disproven
|
||
F1's sweep change (LiveEntityNetworkUpdateController.cs:1005-1018) passes
|
||
`record.FinalPhysicsState` into the typed owner entry. The activation cell
|
||
gate reads that SAME canonical value directly off the record —
|
||
RuntimeSetPositionState.cs:1747-1752 builds `canonicalRequest` with
|
||
`MoverPhysicsState = record.FinalPhysicsState`. The activation path never
|
||
reads the controller's copy. A drop on the CONTROLLER therefore cannot
|
||
deprive the activation of anything, "value-preserving" or not. The primary
|
||
suspect cannot produce a DeferredCell wedge. Contract constraint 3's
|
||
"retained-to-activation admission" is not the fix; the real defect is below
|
||
and is entirely inside C3c-flip code that F1 never touched.
|
||
|
||
### Step 3 — VERIFIED MECHANISM, link by link
|
||
L1. Login: the first-entry conductor prepares the authored mover and
|
||
Prepare+Commits the publication in one synchronous step
|
||
(RuntimeLocalPlayerFirstEntryState.cs:395-431). The controller is now
|
||
RuntimeOwnedDormant (RuntimeLocalPlayerPhysicsPublicationState.cs:433-436)
|
||
— confirmed live by the 158 `player: applied server movement stats`
|
||
dormant lines in 130455.
|
||
L2. Stage PublicationCommitted -> EvaluateActivation -> engine SetPosition
|
||
(RuntimeSetPositionState.cs:1770). The destination landblock's collision
|
||
is still being published (the reveal reports collision=False for ~40 s),
|
||
so the result is DEFERRED.
|
||
L3. CommitActivation -> TryApplyDormantLocalActivationCommit PARKS the
|
||
operation (RuntimeSetPositionState.cs:2066-2107): Stage=AwaitingCell,
|
||
WakeableLostCell=true, CollisionGenerationReady=false, and
|
||
`CollisionGeneration = prepared.DeferredCollisionGeneration`, computed at
|
||
:1939-1940 as `_physics.ExpectedCollisionGeneration(cellId)` = the
|
||
IN-FLIGHT admission's generation G (RuntimePhysicsState.cs:2934-2939).
|
||
Bucket key = (cell, prefix, G).
|
||
L4. ~40 s later the landblock's collision generation commits.
|
||
RuntimePhysicsState.cs:2503 calls
|
||
`SetPosition.CommitCollisionGeneration(lb, G, ready:true)`, which finds
|
||
bucket (cell, prefix, G), verifies `IsSpawnCellReady`, and sets
|
||
`operation.CollisionGenerationReady = true`
|
||
(RuntimeSetPositionState.cs:3886). The push-side `RetryDeferred` is
|
||
deliberately a no-op for this operation — it returns immediately when
|
||
`operation.DormantLocalActivation` is set (:4044-4045) by design: the
|
||
local-player lease must re-enter through the sealed evaluation/commit
|
||
path. So the ONLY door left is the PULL-side rearm on the conductor's
|
||
next Advance.
|
||
L5. Immediately after :2503, `AdvanceCommittedActivation` REMOVES the
|
||
admission from `_collisionAdmissions` (RuntimePhysicsState.cs:2552-2558)
|
||
while leaving `_collisionGenerations[lb] == G` (set at
|
||
BeginCollisionAdmission, :2066). From this instant on,
|
||
`ExpectedCollisionGeneration(cellId)` no longer returns G — with no
|
||
admission it returns `_collisionGenerations[lb] + 1` = G+1
|
||
(RuntimePhysicsState.cs:2940-2944).
|
||
L6. THE DEFECT. The next Advance reaches
|
||
`TryRearmDeferredDormantLocalActivation`
|
||
(RuntimeSetPositionState.cs:1851-1882), whose gate includes
|
||
`operation.CollisionGeneration != _physics.ExpectedCollisionGeneration(
|
||
operation.ExactCellId)` (:1870-1871) -> `G != G+1` -> rearm refused,
|
||
permanently. `ExpectedCollisionGeneration` means "the generation that
|
||
will make me ready" at PARK time and "the next, not-yet-begun generation"
|
||
at WAKE time; the rearm compares against the wrong one. Nothing else ever
|
||
clears WakeableLostCell, so the operation is wedged for the session.
|
||
L7. EvaluateActivation's fallback then reports DeferredCell forever
|
||
(publication state :469-476 -> IsDormantLocalActivationAwaitingCell true),
|
||
the conductor yields AwaitingActivation and keeps its pending entry
|
||
(first-entry state :493-497), the controller never activates, and
|
||
`IsRuntimePublished` stays false -> PlayerModeController.cs:243-251 logs
|
||
"not committed yet".
|
||
L8. SECOND LINK (flip-introduced, independent). `PlayerModeAutoEntry.TryEnter`
|
||
sets `_armed = false` BEFORE invoking EnterPlayerMode
|
||
(PlayerModeAutoEntry.cs:227-228) — a one-shot. Its
|
||
`IsPlayerControllerReady` precondition in the PRODUCTION context is the
|
||
constant `true` (PlayerModeAutoEntry.cs:86). That was harmless pre-flip
|
||
because TryEnter CONSTRUCTED the controller and could not fail on it
|
||
(deleted block, `git diff src/AcDream.App/Input/PlayerModeController.cs`);
|
||
post-flip TryEnter returns false when the conductor has not committed. So
|
||
the single shot is burned on the exact frame readiness flips, and
|
||
`LivePlayerModeAutoEntryContext.EnterPlayerMode` calls
|
||
`_worldReveal.Complete()` UNCONDITIONALLY (PlayerModeAutoEntry.cs:97-101)
|
||
— sealing the reveal (materialized=False) and producing the 5,577
|
||
readiness-after-terminal rejections. The flip's own comment ("auto-entry
|
||
retries on a later frame", PlayerModeController.cs:242) is false as
|
||
written. Ordering note: UpdateFrameOrchestrator.cs:201-207 runs streaming
|
||
-> live frame (DriveAll) -> auto-entry, so with L6 fixed the same frame
|
||
would usually succeed — but "usually" is a race, and the log proves a
|
||
failed attempt burns the shot and seals the reveal.
|
||
|
||
### Why the existing rearm test never caught L6
|
||
RuntimeLocalPlayerPhysicsPublicationStateTests
|
||
.DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake (:337-383)
|
||
wakes the operation by calling `SetPosition.BeginCollisionGeneration` /
|
||
`CommitCollisionGeneration` DIRECTLY, bypassing RuntimePhysicsState's
|
||
admission ledger. `_collisionAdmissions` and `_collisionGenerations` therefore
|
||
stay EMPTY, so `ExpectedCollisionGeneration` returns 1 at both park and wake
|
||
and the identity check accidentally holds. Production always goes through
|
||
BeginCollisionAdmission -> CommitCollisionGeneration, which is exactly the
|
||
path that breaks it.
|
||
|
||
### The fix (2 hunks)
|
||
F2-1 (root cause, Runtime): the rearm's generation identity check compares
|
||
against the LIVE collision generation authority
|
||
(`_physics.CollisionGenerationAuthority`, RuntimePhysicsState.cs:2953-2962 =
|
||
`_collisionGenerations[lb]`), not `ExpectedCollisionGeneration`. Post-commit
|
||
that is exactly G, so the parked lease rearms. Every stale case still
|
||
refuses: a superseding admission or a cancel bumps `_collisionGenerations`
|
||
away from G. Park-side semantics (:1939) untouched — "park against the
|
||
generation that will make me ready" is the established convention shared with
|
||
the remote ParkDeferred path (:3974). No latch is loosened: WakeableLostCell,
|
||
CollisionGenerationReady and IsSpawnCellReady remain mandatory.
|
||
F2-2 (second link, App): `LivePlayerModeAutoEntryContext
|
||
.IsPlayerControllerReady` stops lying — it reports the exact precondition
|
||
PlayerModeController.TryEnter enforces (Runtime-published controller +
|
||
committed EntityPhysicsHost). The one-shot latch, the reveal latch, and the
|
||
readiness-after-terminal rejection are all UNCHANGED; the trigger simply
|
||
cannot burn its shot before the conductor has committed, so
|
||
`_worldReveal.Complete()` runs only on a real entry. PlayerModeController.cs
|
||
is NOT touched by this fix.
|
||
|
||
### Step 4 — LIVE PROBE CORRECTION (temporary attributed probe, since stripped)
|
||
The Step-3 analysis was right about the parts but wrong about which link fires
|
||
first. A temporary change-only probe on the rearm gate terms and the drive
|
||
controller's local status (env-gated, stripped before the gate) was run against
|
||
ACE. Evidence, run `logs/c3c-f2-probe.out.log` (F2-1 only, no admissibility
|
||
term):
|
||
```
|
||
L61 [c3cf2-rearm] ... ready=False ... gen=1 auth=0 expected=1 spawnReady=True
|
||
L220 [c3cf2-rearm] ... ready=False ... gen=1 auth=1 expected=1 spawnReady=True
|
||
L221 [c3cf2-rearm] ... ready=True ... gen=1 auth=1 expected=1 spawnReady=True
|
||
L222 [c3cf2-drive] local status=RejectedAuthority step=0 pending=59
|
||
```
|
||
- L61: the lease parks at generation 1 with NO admission and NO committed
|
||
generation yet (auth=0), i.e. `ExpectedCollisionGeneration`'s 1UL default.
|
||
- L221: the collision-generation commit marks it ready — and `expected` is
|
||
STILL 1, proving the admission is still registered at that instant: the
|
||
commit reenters the host's first-entry pump between
|
||
RuntimePhysicsState.cs:2503 and the retirement at :2552-2558.
|
||
- L222: the rearm succeeds inside that window, the immediately following
|
||
evaluation fails `TrySealCollisionEvaluationAuthority` on the still-
|
||
registered admission, and because the lease is no longer AwaitingCell,
|
||
EvaluateActivation answers RejectedAuthority — TERMINAL. The conductor
|
||
discards and the drive entry is dropped for the session.
|
||
=> LINK 3 (the reentrant-window rearm) is the DOMINANT live blocker, and it
|
||
fires BEFORE the L6 generation mismatch can. L6 is still real and still
|
||
load-bearing — see the next run.
|
||
|
||
Second live run with both terms (`logs/c3c-f2-probe2.out.log`):
|
||
```
|
||
L223 [c3cf2-rearm] ... ready=True gen=1 auth=1 expected=1 -> refused (window)
|
||
L224 [c3cf2-rearm] ... ready=True gen=1 auth=1 expected=2 -> REARMED
|
||
L225 [c3cf2-drive] local status=Completed step=0 pending=2
|
||
L230 live: auto-entered player mode for 0x5000000A
|
||
L231 [world-reveal] event=complete ...
|
||
L234 [world-reveal] event=world-visible ... visible=True
|
||
```
|
||
L224 is the direct proof that F2-1 is load-bearing: at the frame the rearm
|
||
actually happens the admission is gone, so `expected` is 2 and only the
|
||
committed authority still equals the parked generation 1. Zero
|
||
readiness-after-terminal lines in the whole run.
|
||
|
||
### Final fix (3 hunks, all required)
|
||
F2-1 RuntimeSetPositionState.TryRearmDeferredDormantLocalActivation — compare
|
||
the parked generation against `CollisionGenerationAuthority` (the generation
|
||
the collision world HOLDS) instead of `ExpectedCollisionGeneration` (which
|
||
means "the next, not-yet-begun generation" once the admission retires).
|
||
F2-3 same method — refuse the rearm while the destination prefix is not
|
||
evaluable, using the seal's own predicate, now factored as
|
||
`RuntimePhysicsState.IsCollisionEvaluationPrefixAdmissible` and consumed by
|
||
BOTH the seal and the rearm so they cannot drift. This is the same shape the
|
||
remote wake path already had via `TryGetBlockingQuiescence` (:4069-4095).
|
||
F2-2 LivePlayerModeAutoEntryContext.IsPlayerControllerReady — report the
|
||
Runtime first-entry commit instead of the constant `true`, so the one-shot
|
||
guard cannot burn its single attempt (and unconditionally complete the world
|
||
reveal) before the conductor has published the controller.
|
||
|
||
### Tests
|
||
- NEW RuntimeLocalPlayerPhysicsPublicationStateTests
|
||
.DeferredCommitRearmsAfterProductionAdmissionCommitsItsGeneration — pins
|
||
F2-1; fails pre-fix.
|
||
- NEW RuntimeLocalPlayerPhysicsPublicationStateTests
|
||
.DeferredCommitStaysParkedWhileTheCommittingAdmissionIsStillRegistered —
|
||
pins F2-3; pre-fix it fails with the exact live symptom
|
||
(`Expected: DeferredCell / Actual: RejectedAuthority`).
|
||
- NEW tests/AcDream.App.Tests/Input/C3cF2AutoEntryWiringTests
|
||
.ProductionAutoEntryRequiresTheRuntimePublishedController — pins F2-2
|
||
(source pin; the production context's ~15-dependency graph has no focused
|
||
harness, matching the C3c-F1 precedent).
|
||
- CONVERTED to the production wake path (assertions verbatim; only the wake
|
||
DRIVER changed, from the raw SetPosition seam to the collision admission
|
||
ledger, because the raw seam leaves the ledger empty — a state production
|
||
can never reach, and precisely why these tests missed the wedge):
|
||
RuntimeLocalPlayerPhysicsPublicationStateTests
|
||
.DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake,
|
||
.DeferredAuthoredActivationSuspendsRowsAndExactWakeRestoresThem,
|
||
RuntimeLocalPlayerFirstEntryStateTests
|
||
.AwaitingActivationRetriesWhileCellUnresolvedThenResumesAfterGenerationWake,
|
||
.DeleteAndSameGuidReincarnationAutomaticallyFreesThePublicationSlotForTheFreshIncarnation.
|
||
All four fail pre-fix once converted.
|
||
|
||
### Gates
|
||
Runtime 992/992; App 4,037/0/3 skips; Headless 78/78; Release solution build
|
||
0 errors / 18 pre-existing test-project warnings; complete solution (-m:1,
|
||
ACDREAM_PAK_PATH) 10,797 passed / 0 failed / 4 skipped of 10,801;
|
||
`git diff --check` clean.
|
||
|
||
CONNECTED GATE: logs/connected-world-gate-20260802-135444 — RESULT=FAIL, but
|
||
NOT on the wedge, which is gone in both sessions:
|
||
- capped: login reveal reached world-visible, the probe captured checkpoint
|
||
`capped_login` AND screenshot `capped_login.png`, then teleported
|
||
(`old lb=(169,180) new lb=(9,4)`), and generation 2 (kind=Portal) reached
|
||
materialized=True, visible=True, completed=True. 17,043 entities ingested.
|
||
- uncapped-reconnect: login at cell 0x09040008 reached visible=True,
|
||
completed=True.
|
||
- Zero `readiness-after-terminal` lines, zero "not committed yet", zero
|
||
F1-class controller-mutation crashes in either session.
|
||
|
||
### BLOCKER (pre-existing, out of C3c-F2 scope) — map-corner landblock
|
||
Both sessions then died identically:
|
||
`System.ArgumentOutOfRangeException (Parameter 'landblockId')` at
|
||
RuntimeSetPositionState.BeginCollisionPrefixQuiescence
|
||
(src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs:783)
|
||
<- RuntimePhysicsState.BeginCollisionPrefixQuiescence(:1993)
|
||
<- RuntimePhysicsState.CommitCollisionGeneration(:2432)
|
||
<- LandblockPhysicsPublisher.AdvanceCompleteOne
|
||
(src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs:626)
|
||
<- LandblockPresentationPipeline.Advance <- StreamingController.Tick.
|
||
Mechanism: the teleport destination is landblock (9,4). The far streaming
|
||
radius is 12 and StreamingRegion only SKIPS out-of-range indices
|
||
(`nx < 0 || nx > 0xFF`, StreamingRegion.cs:80/117/155/225) — it does not skip
|
||
(0,0) — so the window includes landblock id `(0<<24)|(0<<16)|0xFFFF` =
|
||
0x0000FFFF. `CanonicalLandblock` keeps that as 0x0000FFFF, and
|
||
BeginCollisionPrefixQuiescence computes `prefix = landblockId & 0xFFFF0000`
|
||
= 0x00000000 and throws its `prefix == 0u` guard (:781-783). Dereth's
|
||
south-west corner landblock therefore cannot be collision-published, and any
|
||
position within the far radius of it crashes the client.
|
||
This code is untouched by the C3c flip, by C3c-F1, and by C3c-F2 (`git diff
|
||
src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` contains no change to
|
||
BeginCollisionPrefixQuiescence); it was simply unreachable while login itself
|
||
was wedged — 130455 never left Holtburg and never teleported.
|
||
NOT fixed here, and deliberately not a one-line guard removal: prefix 0 is
|
||
also overloaded as an "absent prefix" sentinel in the same class — e.g.
|
||
ParkDeferred derives `operation.CollisionQuiescenceHeld = collisionPrefixOverride
|
||
!= 0u` (:3975-3978), so a genuine landblock-(0,0) quiescence override would be
|
||
read as "no override". A correct fix needs an explicit has-prefix flag (or a
|
||
nullable prefix), which is a physics-ownership design change outside this
|
||
contract. Recommend a dedicated slice.
|
||
|
||
## C3c-F3 — corner-landblock prefix-0 sentinel conversion
|
||
|
||
### Chain audit (complete, every prefix-sentinel site classified)
|
||
CONVERTED (the has-prefix representation is nullable uint for the override
|
||
pair + OperationId-based token presence + explicit landblockId==0 absent-id
|
||
input guards):
|
||
1. RuntimeSetPositionState.cs RuntimeCollisionPrefixQuiescenceToken.IsValid
|
||
(:112) — dropped `LandblockPrefix != 0u`; presence now discriminated by
|
||
OperationId != 0 (monotonic from 1) + CollisionGeneration != 0 (from 1).
|
||
This was load-bearing beyond the crash site: a real prefix-0 token read
|
||
as invalid wedged TryGetCurrentQuiescence (:839/:887/:905 callers),
|
||
permission currency (:886), and TryGetBlockingQuiescence's `excluded`
|
||
term (:3617 — RetryDeferred's restoringQuiescence would have blocked
|
||
itself).
|
||
2. RuntimeSetPositionState.BeginCollisionPrefixQuiescence (:782) — the
|
||
crash-site `prefix == 0u` throw replaced by `landblockId == 0u` (absent
|
||
id), prefix computed unconditionally.
|
||
3. RuntimeSetPositionState.ParkDeferred (:3977-78, :4013-19) — override
|
||
params converted `ulong collisionGenerationOverride = 0UL, uint
|
||
collisionPrefixOverride = 0u` -> `ulong?/uint? = null`;
|
||
CollisionQuiescenceHeld = collisionPrefixOverride.HasValue. Both
|
||
quiescence-token call sites (:2842, :2893) pass values unchanged
|
||
(implicit lift); byte-identical for nonzero prefixes.
|
||
4. RuntimeSetPositionState.ParkCollisionResidents (:3414-17) — `?? 0UL` /
|
||
`?? 0u` collapse removed; `quiescence?.Token.X` now flows null/value.
|
||
5. RuntimePhysicsState.BeginCollisionPrefixQuiescence (:1991) — dead
|
||
`canonical == 0u` (CanonicalLandblock ORs 0xFFFF, never 0) replaced by a
|
||
LIVE `landblockId == 0u` guard.
|
||
6. RuntimePhysicsState.AdvanceCollisionRetirementMutation (:2626) — same
|
||
dead-guard replacement.
|
||
7. RuntimePhysicsState.BeginCollisionAdmission (:2037) — NEW landblockId==0
|
||
guard at the admission entrance: an absent id canonicalizes to
|
||
0x0000FFFF (the REAL corner landblock), and the old accidental
|
||
commit-time protection (the prefix throw) is gone.
|
||
8. ShadowObjectRegistry.DeriveOutdoorSeed (:651, Core) — `lbPrefix == 0u ->
|
||
no seed` replaced by `landblockId == 0u`; corner-block baked statics now
|
||
derive real seeds 0x0000000N (previously silently dropped from the
|
||
shadow world). Same sentinel class, exercised by the corner collision
|
||
publication chain.
|
||
|
||
KEPT with justification (no collision with prefix 0):
|
||
- Generation-0 "unbound" sentinel (RuntimeSetPositionState :3727/:3741-45/
|
||
:3811/:3842/:5135/:5180/:5255): generations allocate from 1
|
||
(checked(current+1), Begin throws on 0) — 0 is unreachable as a real
|
||
generation.
|
||
- ExactCellId==0 "absent cell" (IndexDeferred/IndexUnboundDeferred/
|
||
UnindexDeferred): cell-part 0x0000 is not a valid cell; corner cells are
|
||
0x00000001+.
|
||
- RuntimePhysicsState dead post-canonicalization zero checks
|
||
(ExpectedCollisionGeneration :2932, IsCollisionEvaluationPrefixAdmissible
|
||
:2963, CollisionGenerationAuthority :2977, TrySealCollisionEvaluationAuthority
|
||
Add :3030): CanonicalLandblock never returns 0; harmless dead defensive
|
||
terms, prefix-agnostic. (Noted follow-up: CanonicalLandblock(0) aliases
|
||
cellId 0 -> 0x0000FFFF in these helpers; unreachable with real
|
||
operation cells today, flagged rather than redesigned.)
|
||
- Core PhysicsEngine bare-id compat (:1355/:1362 SampleTerrainWalkableInCell,
|
||
:2093 HasCellSurface): `requestedPrefix == 0` there means "caller passed a
|
||
bare pre-#106 test-fixture id", a different semantic; corner cells resolve
|
||
correctly through the world-position filter. Follow-up cleanup candidate,
|
||
out of this contract's chain.
|
||
- Convention pinned by test: raw input 0x00000000 remains "absent"
|
||
everywhere; landblock (0,0) is addressed by canonical 0x0000FFFF (or any
|
||
cell inside it) — matching what production streaming always passes.
|
||
|
||
### Tests (7 new, all fail pre-fix / pass post-fix; pre-fix run of the
|
||
### production-chain test reproduced the EXACT 135444 crash signature:
|
||
### ArgumentOutOfRangeException 'landblockId' at RuntimeSetPositionState.cs:783
|
||
### <- RuntimePhysicsState.cs:1993 <- :2432)
|
||
New partial tests/AcDream.Runtime.Tests/Physics/
|
||
RuntimeCollisionPrefixQuiescenceTests.CornerLandblock.cs:
|
||
- CornerLandblockCollisionGenerationCommitsThroughTheProductionAdmissionChain
|
||
(corner 0x0000FFFF + neighbor 0x0001FFFF, empty engine, full
|
||
admission->prepare->stage->seal->commit, ownership converged)
|
||
- CornerResidentParksAndRestoresAcrossAnActivationReplacement (SetPosition
|
||
commit into corner cell; activation replacement parks the resident under
|
||
the prefix-0 quiescence override, wakes, restores)
|
||
- CornerPrefixQuiescenceHoldsAndReleasesExactlyLikeANonzeroPrefix (contract
|
||
test 2: identical held-placement script vs PrefixP, step-for-step log
|
||
parity incl. QuiescenceHeld hold/acquire/cancel/restore)
|
||
- CornerLandblockDemotesAndWithdrawsThroughRetirementMutations
|
||
- AbsentLandblockIdStillCannotBeginQuiescence (id-0 keeps throwing at
|
||
quiescence AND at the new admission-entrance guard; corner token IsValid)
|
||
tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs:
|
||
- Register_CornerLandblock_DerivesRealOutdoorSeed
|
||
- Register_AbsentLandblockId_StillKeepsWhenEmpty (passed pre-fix too —
|
||
pins the preserved keep-when-empty guard)
|
||
Two in-flight test corrections during TDD: seeded corner Place asserts
|
||
CommittedHostAcknowledgementPending (the ordinary bound-events commit
|
||
status — over-strict first draft); parity run addresses the corner by
|
||
canonical 0x0000FFFF, not raw 0 (which is the absent sentinel by design).
|
||
|
||
### Gate ladder
|
||
1. Focused corner tests: 5/5 Runtime + 39/39 ShadowObjectRegistry suite.
|
||
Complete projects: Runtime 997/997 (992+5), App 4,037/3 skips of 4,040,
|
||
Headless 78/78.
|
||
2. Release solution build: 0 errors. Complete solution (-m:1, PAK): running.
|
||
2 (cont). Complete solution (-m:1, ACDREAM_PAK_PATH): 10,804 passed / 0
|
||
failed / 4 skipped (App 4,037/3, Bake 15, Cli 4, Content 124, Core.Net
|
||
762, Core 4,244/1, Headless 78, Runtime 997, UI 543).
|
||
3. Connected gate: run logs/connected-world-gate-20260802-142539 launched
|
||
against live ACE (UDP 9000 confirmed listening, no stale client); result
|
||
pending.
|
||
4. git diff --check: clean (line-ending metadata warnings only, matching the
|
||
known worktree pattern). Nothing staged.
|
||
3 (result). Connected gate PASS: logs/connected-world-gate-20260802-142539/
|
||
report.json Passed=true, Failures=[], both sessions exit 0, one warning
|
||
("capped: 25 expected world-edge landblock miss(es)" — the expected
|
||
class). The 135444 corner crash is gone: the capped session completed the
|
||
full route (capped_login, facility_hub, aerlinthe_first, rynthid,
|
||
holtburg_after_dungeon, aerlinthe_revisit checkpoints + screenshots
|
||
under capped/artifacts/), uncapped-reconnect completed
|
||
uncapped_reconnect. Coordinator confirmed and directed the soak.
|
||
5. R6 soak launched (run-connected-r6-soak.ps1, same ACE, detached PID
|
||
14036); result pending.
|
||
5 (result). R6 soak logs/connected-r6-soak-20260802-143157: RESULT=FAIL per
|
||
the PRIMARY artifact (report.json Passed=false, 37 failures) — the
|
||
coordinator's "concluded successfully" summary was based on the markers
|
||
log (route-complete + graceful close are true) but the pass criterion is
|
||
report.json. Failure class: streamingWork convergence
|
||
(deferredCompletions/deferredAdoptedCpuBytes/pendingPublications=1/
|
||
farBacklog nonzero) at 8 of 9 canonical checkpoints (aerlinthe clean),
|
||
plus Caul plateau loadedLandblocks/totalLandblocks 283->189 and mesh
|
||
cache growth 547->588 / +40.5 MB. No crash, no exception, graceful
|
||
logout confirmed; 9/9 checkpoints + screenshots captured.
|
||
ATTRIBUTION ANALYSIS (verified, not guessed):
|
||
- Last passing soak (logs/connected-r6-soak-20260727-004942.report.json,
|
||
Passed=true, 0 failures) enforced the SAME streamingWork expect-zero
|
||
criterion and met it — but predates the ENTIRE uncommitted C3c
|
||
flip+F1+F2+F3 stack, so it separates {branch} from {baseline}, not F3
|
||
from the flip.
|
||
- F3's blast radius was provably NOT exercised in the failing run:
|
||
grep 0x0000FFFF over the soak out.log = 0 hits (the corner landblock
|
||
never entered any route window), zero exceptions (the new absent-id
|
||
guards never fired), and every F3 conversion is byte-identical for
|
||
nonzero prefixes (pinned by the parity test). The failing criterion is
|
||
streaming/publication convergence — a C3c-flip/F1/F2-era surface.
|
||
- The lifecycle gate 142539 (WITH F3) passed cleanly the same day.
|
||
STOPPED per contract — no retry, no further code work, nothing staged.
|
||
|
||
## C3c-F4 — soak streaming-convergence regression: DIAGNOSED, STOPPED (no fix landed)
|
||
|
||
**Verdict: NOT a wedge and NOT in the C3c flip/F1/F2/F3 diff.** It is a
|
||
throughput regression in the committed collision-generation atomic-replacement
|
||
mechanism.
|
||
|
||
### Named mechanism (link by link)
|
||
1. `StreamingController.DrainAndApply` (src/AcDream.App/Streaming/StreamingController.cs:1662-1690)
|
||
advances the completion-queue head and `break`s when it does not complete —
|
||
at most ONE landblock publication per frame.
|
||
2. `LandblockPresentationPipeline.Advance` stage `publication-index-physics`
|
||
(src/AcDream.App/Streaming/LandblockPresentationPipeline.cs:612-627) charges
|
||
`EntityOperations: PreparationCursor < Entities.Count ? 1 : 0`. A FAR build is
|
||
`Array.Empty<WorldEntity>()` (PublishAsFar, :419-447), so every step is FREE
|
||
and only the 2 ms elapsed-time ceiling bounds it.
|
||
3. `LandblockPhysicsPublisher.AdvancePreparationOne`
|
||
(src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs:297-309) gates on
|
||
`RuntimePhysicsState.AdvanceCollisionGenerationPreparation`.
|
||
4. That calls `PreparedLandblockCollisionGeneration.AdvanceStagingClone`
|
||
(src/AcDream.Runtime/Physics/RuntimePhysicsState.cs:531-560) →
|
||
`PhysicsEngine.CollisionStagingBuilder.Advance`
|
||
(src/AcDream.Core/Physics/PhysicsEngine.cs:842-925): ONE leaf per step of an
|
||
off-side draft of the COMPLETE collision world minus the target prefix
|
||
(landblock slots, CellStruct, FlatCellStruct, FlatEnvCell, Buildings,
|
||
EnvCells, Terrain, OutdoorCells, shadow-owner slots).
|
||
5. `PhysicsEngine.CommitLandblockReplacement` (:304-321) does
|
||
`stagingCache.CollisionWorld.TransferTo(activeCache.CollisionWorld)` — the
|
||
atomic unit is the WHOLE world, which is why the whole world must be cloned.
|
||
|
||
### Measured (attributed probes, lifecycle-gate route, since stripped)
|
||
- median 19,736 / p90 32,135 / max 38,021 clone leaves per landblock
|
||
publication; median 3.64 ms CPU each; 1,584 preparations = 8.53 s CPU in one
|
||
4-minute capped session.
|
||
- Far-queue drain measured at ~10 landblocks/s; `[queue-stale]` showed the head
|
||
seq advancing 234→488→548→603→…→1360 and far count 411→351→296→242→190 —
|
||
the queue drains, it never catches up. `pendingPublications=1` is the one
|
||
head in flight, not a stuck item.
|
||
- `[fifo-wedge]` never fired: `pendingProjections=0`. The placement projection
|
||
FIFO, the sink's C3c residence gate, and the conductors are NOT involved.
|
||
|
||
### Attribution
|
||
`git log -S CollisionStagingBuilder` → introduced by `6b28ff99`
|
||
"fix(physics): make collision activation starvation-free" (2026-07-31), on top
|
||
of `be94bc9b` (atomic activation, 2026-07-31) / `9b0f59bd` (2026-08-01). The
|
||
last PASSING soak is `a9a822f2` (2026-07-27) — before all three. `git diff`
|
||
touches no file on this path.
|
||
|
||
### Why no fix landed (contract STOP rule)
|
||
Tried the one semantics-preserving lever: batch 256 leaves per metered
|
||
preparation step. Measured 1.8x (gate far backlog 334→187 / 264→130, loaded
|
||
landblocks 291→438 / 261→395) — real but NOT convergence. It also fails
|
||
`RuntimePhysicsStateTests.DenseResidentWorldAdmissionIsConstantAndMaterializesOneLeafPerStep`
|
||
(tests/AcDream.Runtime.Tests/Physics/RuntimePhysicsStateTests.cs:983,
|
||
`Assert.InRange(step.WorkUnits, 0, 1)`) — one-leaf-per-step is an ASSERTED
|
||
invariant of the slice that introduced the clone. Reverted; tree is byte-identical
|
||
to flip+F1+F2+F3.
|
||
|
||
Convergence requires the clone to become O(changed) instead of O(resident world)
|
||
— structural sharing in `CollisionWorldState`, or a per-landblock (not
|
||
whole-world) atomic replacement unit. That is a semantics change to the C2-era
|
||
mechanism and belongs to its own slice.
|
||
|
||
### Secondary symptoms — same mechanism, not separate defects
|
||
`loadedLandblocks` 283→189 and `visibleLandblocks` 30 vs the baseline's 180 are
|
||
the far ring never converging (baseline held 625 loaded at every checkpoint).
|
||
Mesh cache 547→588 / +40.5 MB is different far-ring subsets resident at the two
|
||
visits; retest for a true leak only after convergence is restored.
|
||
|
||
## C3c-F5 — local-player first-entry contact seeding
|
||
|
||
### Research (grep-named-first, complete before design)
|
||
- Retail local-player seeding point: `SmartBox::HandleCreateObject` 0x00454C80
|
||
runs `SmartBox::init_player` 0x00455010 then `CPhysicsObj::enter_world`
|
||
(call site 0x00455095; body 0x00516170) for the LOCAL player — the same
|
||
enter_world the non-player branch uses at 0x004550EC. enter_world builds
|
||
SetPositionStruct flags 0x11, calls CPhysicsObj::SetPosition, sets
|
||
`transient_state |= 0x80` (ACTIVE) and HandleEnterWorld — NO contact
|
||
seeding anywhere in it (pseudo-C 284198-284249). Contact arrives from the
|
||
first gravity frame (digest #270 section; find_placement_pos validates the
|
||
spot but records no touch). Local player and remote spawn CONFIRMED to
|
||
share the retail mechanism.
|
||
- Legacy local path (deleted by the flip): BuildControllerAndCamera ran
|
||
Resolve(100f drop) + ResolvePlacement then PreparePositionForCommit ->
|
||
SetPositionCore, which FORCE-seeded `Contact | OnWalkable | Active`
|
||
("Treat as grounded after a server-side position snap",
|
||
PlayerMovementController.cs:1830-1834) — an unconditional non-retail seed
|
||
(Contact-without-plane, the state the landing family calls
|
||
unrepresentable). The flip deleted the call chain without an equivalent.
|
||
- New path today: conductor -> publication -> dormant activation ->
|
||
PhysicsEngine.SetPosition (faithful port, result.InContact=false for a
|
||
clean placement) -> TryApplyDormantLocalActivationCommit commits
|
||
contact=false -> FinalizeActivation activates. Body starts airborne;
|
||
outbound CanSendPositionEvent (= InContact && OnWalkable,
|
||
PlayerMovementController.cs:1517) stays false -> MTS contact byte 0
|
||
(LocalPlayerOutboundController.cs:203) -> ACE says 'while in the air'.
|
||
- DO-NOT-RETRY compliance: settle passes isOnGround:false + real body (no
|
||
caller-bool seeding); no ContactPlaneValid gating; no forced transients —
|
||
contact only from the sweep's real touch; airborne spawn stays airborne.
|
||
|
||
### Design (pinned-contract shape)
|
||
- Move RemoteSpawnPlacementSettler (App) -> Core.Physics
|
||
`SpawnPlacementSettler` (public, like PhysicsObjUpdate; Core internals are
|
||
NOT visible to App). Remote caller + tests updated; semantics byte-identical.
|
||
- Seed at RuntimeLocalPlayerPhysicsPublicationState.FinalizeActivation,
|
||
after TryApplyDormantLocalActivationFinalCommit + shadow dispatch +
|
||
IsCommittedActivationSuffixCurrent, before placement dispatch (no
|
||
reentrant-sink hazard; stale-authority path skips the settle). Inputs:
|
||
activation Body/Record key/ActivationPreparation radius+height,
|
||
IsPlayer|EdgeSlide|OwnPvpFlags, Movement.HitGround/Motion.LeaveGround —
|
||
the same callback pair the per-tick landing path uses.
|
||
- Propagation: body transients (a) are THE controller grounded state (b)
|
||
(controller reads _body directly) and THE outbound bit (c)
|
||
(CanSendPositionEvent -> contactByte). No second copy exists.
|
||
|
||
### Implementation (seams touched)
|
||
1. src/AcDream.Core/Physics/SpawnPlacementSettler.cs — NEW (moved from
|
||
src/AcDream.App/Physics/RemoteSpawnPlacementSettler.cs, deleted; public
|
||
like PhysicsObjUpdate because Core internals are not visible to App).
|
||
TrySettle body byte-identical to the #270 shipped version; only the
|
||
class name/namespace/doc changed.
|
||
2. src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:193 — the
|
||
one legacy remote caller now calls
|
||
AcDream.Core.Physics.SpawnPlacementSettler.TrySettle (unchanged args).
|
||
3. src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerPhysicsPublicationState.cs
|
||
— FinalizeActivation now calls new private
|
||
SettleFirstEntryGroundContact(activation) AFTER
|
||
TryApplyDormantLocalActivationFinalCommit + shadow dispatch +
|
||
IsCommittedActivationSuffixCurrent, BEFORE placement dispatch (no
|
||
reentrant-sink window; stale-authority early return skips the settle).
|
||
Inputs: activation.Body position/cell, ActivationPreparation
|
||
radius/height, IsPlayer|EdgeSlide|OwnPvpFlags,
|
||
Controller.LocalEntityId, Movement.HitGround/Motion.LeaveGround (the
|
||
per-tick landing pair). try/catch matches the existing post-commit
|
||
ground-edge dispatch containment (_activationDispatchFailureCount).
|
||
4. Tests: tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs
|
||
(moved from App.Tests, bodies unchanged, layer rule 6);
|
||
Issue270ProductionWiringTests source pin updated to the new name;
|
||
RuntimeLocalPlayerPhysicsPublicationStateTests +2
|
||
(CommitActivationOnFlatGroundSeedsRetailFirstGravityFrameContact,
|
||
CommitActivationOverVoidLeavesFirstEntryGenuinelyAirborne) + fixture
|
||
moverSphereOriginZ param (default 0 = pre-existing shape);
|
||
RuntimeFirstEntryHostIntegrationTests +2
|
||
(LocalLogin_FlatGround_ReportsGroundedOutboundContactBit,
|
||
LocalLogin_AirborneSpawn_StaysGenuinelyAirborne) + HostFixture
|
||
terrainHeight/moverSphereOriginZ params.
|
||
|
||
### Gate ladder
|
||
1. Focused: publication-state 92/92 (90+2), first-entry conductor 15/15,
|
||
settler 3/3 (Core), App first-entry integration + issue-270 wiring 8/8.
|
||
Complete projects (Release): Runtime 999/0, App 4,036/3 skips,
|
||
Headless 78/0, Core 4,247/1 skip.
|
||
NOTE (pre-existing, not this slice):
|
||
LandblockBuildOriginTests.FarLoad_StripsEnvCellsAndPhysics... fails in
|
||
DEBUG only — the test feeds an intentional near payload to a FarLoad and
|
||
LandblockStreamer.cs:505 Debug.Assert fires; Release (the gate config)
|
||
compiles it out and it passes. Reproduced 3x isolated in Debug, passes
|
||
in Release; untouched by this slice's diff.
|
||
2. Release solution build 0 warn/0 err; complete solution (-m:1,
|
||
ACDREAM_PAK_PATH): 10,808 passed / 0 failed / 4 skips
|
||
(App 4,036/3, Bake 15, Cli 4, Content 124, Core.Net 762, Core 4,247/1,
|
||
Headless 78, Runtime 999, UI 543).
|
||
3. Connected lifecycle gate: launched against live ACE (process 22100,
|
||
UDP 9000); result pending.
|
||
3 (result). Connected lifecycle gate PASS:
|
||
logs/connected-world-gate-20260802-164432/report.json Passed=true,
|
||
Failures=[], both sessions ExitCode=0, one warning ("capped: 25 expected
|
||
world-edge landblock miss(es)" — the exact 142539/161138 class). All 6
|
||
capped checkpoints + uncapped_reconnect captured with screenshots.
|
||
"while in the air" grep: 0 in capped/stdout.log, 0 in
|
||
uncapped-reconnect/stdout.log (0x042C also 0/0). Note: the two prior
|
||
PASS runs (142539/161138) also contained 0 occurrences — the scripted
|
||
route never surfaced the rejection string; the behavioral proof of the
|
||
fix is the outbound-bit integration tests + the settle assertions, and
|
||
the gate proves no regression.
|
||
4. git diff --check exit 0 (CRLF metadata warnings only, the known worktree
|
||
pattern); nothing staged; no probes added by this slice.
|
||
|
||
### Register note
|
||
No divergence-register row existed for the #270 compressed settle (it is
|
||
classified as timing compression — it produces exactly the state retail's
|
||
first gravity frame produces); extending it symmetrically to the local
|
||
player follows the same classification. The commit that lands C3c should
|
||
also delete/refresh AD-42 (its cited legacy GameWindow/PlayerModeController
|
||
resolve path no longer exists in the flipped tree) — flagged for the
|
||
closeout, not acted on here (report-only bookkeeping, no doc edits in this
|
||
slice's scope).
|
||
|
||
### Purple-haze note (observation only)
|
||
The haze script is the Hidden/UnHide materialization path
|
||
(EntityEffectController.PlayTypedFromHiddenTransition — retail set_hidden
|
||
0x00514C60); nothing in it keys off contact/airborne state, so the F5
|
||
contact gap does NOT plausibly drive the re-fire. A re-fired UnHide implies
|
||
the local player's presentation saw a hidden/visibility edge while standing
|
||
— consistent with F4's streaming/publication convergence regression
|
||
re-bucketing the player's surroundings, which remains the plausible driver.
|
||
|
||
## C3c-R1
|
||
Fix round for review round 1 (contract c3c-r1-fixes.md). Every finding
|
||
verified at source before editing; dispositions below.
|
||
|
||
### Verification-at-source results (pre-edit)
|
||
- R1 CONFIRMED: CommitPreparedPosition (PlayerMovementController.cs:1784)
|
||
had ZERO production callers post-flip (grep); PreparePositionForCommit
|
||
(called at RuntimeLocalPlayerPhysicsPublicationState.cs:219) uses
|
||
publishSharedState:false and the PositionManager binds at :318 (after the
|
||
position seed), so no login path armed the leash. The final commit
|
||
(RuntimeSetPositionState.cs:2494-2516) is where the accepted position is
|
||
final, the shared cell is published (:2508), and the controller activates
|
||
(:2516).
|
||
- R2 CONFIRMED: LiveEntityRuntime.cs:806-828 keyed the presentation-only
|
||
branch off the sticky enum; post-residence entities skipped CommitRebucket
|
||
(:882-892) + the prepare_to_enter_world clock edges (:897-924). All six
|
||
cited unflipped-route callers verified (grep RebucketLiveEntity).
|
||
- R3 CONFIRMED: RuntimeLiveEntitySessionController.OnSpawned opened the
|
||
residence unconditionally; HeadlessSessionHost builds drive+projection only
|
||
under `_contentLease is { } content` (:539-565); content-less is
|
||
validated-legal (HeadlessConfigurationLoader.ValidateContent :88-98
|
||
returns on null). worldProjection==null is exactly "no drive exists"
|
||
(single production constructor call site).
|
||
- F2 CONFIRMED: SpawnPlacementSettler.cs:61 commits settle.Position;
|
||
settle.CellId never read.
|
||
- F4 CONFIRMED: F3's landblockId==0 guards (RuntimePhysicsState
|
||
.BeginCollisionAdmission :2037-area) throw through
|
||
HeadlessCollisionGenerationTransaction.Begin (:62) reachable from
|
||
CenterOn; the two cited sites passed the raw wire LandblockId unguarded.
|
||
- F5/F6/F7/F8/F9 confirmed as cited (drive `_pending` outside every ledger;
|
||
both route Disposes cleared a SHARED drive unconditionally; far-remote
|
||
DeferredCell park has no wake outside the 3x3 neighborhood; the three
|
||
stale comments; the per-pump TryGetWorldEntity+GetSetupCylinder).
|
||
|
||
### F3 — STOPPED, pinned design conflicts with source (file:line evidence)
|
||
The pinned probe ("nested production OnCreate, not hand-called
|
||
AdvanceCreateAuthority") cannot produce create-authority drift in the
|
||
items-6/8 tests' post-residence window:
|
||
1. RuntimeEntityObjectLifetime.cs:660-665 — the ExistingGeneration branch
|
||
calls Entities.AdvanceCreateAuthority ONLY when !beginInitialResidence;
|
||
the graphical route always registers with residence
|
||
(LiveEntityRuntime.cs:544-548), so a post-residence same-generation
|
||
Create is description-only.
|
||
2. The FIFO-adoption alternative is closed: ConsumeExecuted removes the
|
||
completed residence entry at Released
|
||
(RuntimeInitialCreateResidenceState.cs:1246-1251), and the fixtures
|
||
complete+release during the initial OnCreate, so TryGetTransaction
|
||
(:729-748) misses and no continuation can be staged post-release.
|
||
3. The executor drain (RuntimeInitialCreateContinuationExecutor.cs:2424-2429)
|
||
is the ONLY production site that advances create authority for an
|
||
existing incarnation - exactly what the hand-call models.
|
||
4. EMPIRICAL: temporarily restoring the nested-OnCreate probe in
|
||
FailedCompletedSupersession_RemainsPendingUntilExactRetry made BOTH rows
|
||
fail "Assert.Throws() Failure: No exception was thrown" (no drift, no
|
||
CreateSupersessionRecovery). Experiment reverted; tree byte-identical
|
||
for that file except nothing.
|
||
Restoring the probe requires either accepting dead drift machinery or a
|
||
test-scenario redesign (drift staged during the ACTIVE-residence window and
|
||
drained mid-recovery by a reentrant pump) - a design call for the
|
||
coordinator, not a probe swap. NOT implemented; reported.
|
||
|
||
### R4(c) progress-log correction (retail minor M1)
|
||
The C3c-F5 section above says the legacy local force-seed path was
|
||
"deleted by the flip". CORRECTION: the force-seed at
|
||
PlayerMovementController.cs SetPositionCore ("Treat as grounded after a
|
||
server-side position snap", Contact|OnWalkable|Active) still RUNS during
|
||
publication-candidate preparation (PreparePositionForCommit ->
|
||
SetPositionCore) and is then OVERWRITTEN by the faithful activation commit
|
||
(which commits the SetPosition result's contact=false) and the settle. What
|
||
the flip deleted was the CALLER CHAIN (BuildControllerAndCamera's
|
||
Resolve/ResolvePlacement + CommitPreparedPosition), not the seed statement.
|
||
Register row AD-61 records the "overwritten, not deleted" truth.
|
||
|
||
### Implemented (all others)
|
||
- R1: PlayerMovementController.ArmConstraintLeashAtCommittedPlacement
|
||
(internal, published-guarded) + RuntimeLocalPlayerPhysicsPublicationState
|
||
.ArmFirstEntryConstraintLeash called in FinalizeActivation after the
|
||
final commit + shadow dispatch, inside the IsCommittedActivationSuffixCurrent
|
||
gate, BEFORE the settle; exactly-once via `_activation = null` preceding
|
||
the suffix. Ordering justified in the new doc comment with file:line.
|
||
- R2: the public RebucketLiveEntity now suppresses ONLY while
|
||
HasActiveInitialCreateResidence (exact-token activity view); post-residence
|
||
falls through to the FULL legacy branch (CommitRebucket + clock edges).
|
||
RebucketLiveEntityPresentationOnly is now called only from
|
||
TryApplyInitialCreateCompletionPresentation.
|
||
- R3: content-less direct host (worldProjection null) registers via
|
||
RegisterEntity + direct accepted-frame commit (exact pre-flip shape),
|
||
with the C4/C5 revisit note.
|
||
- R4: register AD-61 filed (local-player settle compression, overwritten
|
||
force-seed, settle-CellId caveat); AD-42 refreshed (repointed at
|
||
RemoteTeleportController.ResolvePlacement + headless portal resync;
|
||
login split retired); section count 46->47.
|
||
- F1: LiveEntityRuntime.ConvertMaterializationResidenceToLegacyImmediate
|
||
(throws while residence active); EquippedChildRenderController uses it.
|
||
- F4: `{ LandblockId: not 0u }` guards at both cited CenterOn sites.
|
||
- F5: RuntimeEntityObjectOwnershipSnapshot.FirstEntryDrivePendingCount
|
||
(IsConverged-gated) + RegisterFirstEntryDriveOwnership; the drive
|
||
registers its pending-count provider at construction.
|
||
- F6: RuntimeFirstEntryDriveController.AttachRoute/DetachRoute one-route
|
||
latch (Clear deleted); GraphicalSessionEventRoute + HeadlessSessionEventRoute
|
||
attach/detach with `this`.
|
||
- F7: RuntimeAuthoritativePositionRouteClassifier.ToCellessCreateRoute
|
||
(exact Parented/PickedUp branch shape, preserving authority/operation/
|
||
collision-batch); RuntimeInitialCreateResidenceState.TryConvertToCellessRoute
|
||
(active+unplaced only: ForgetExactPlacement + lease rewrite +
|
||
PublishCancellation + retirement fan-out for conductor progress reset,
|
||
entry retained); RuntimeEntityObjectLifetime
|
||
.TryConvertInitialResidenceToCellessRoute facade;
|
||
IHeadlessCollisionNeighborhood.IsWithinServiceWindow (3x3 around the
|
||
requested center; no-center = within) consumed by
|
||
HeadlessSessionWorldProjection.ProjectSpawn for far remotes before the
|
||
pump.
|
||
- F8: both conductor "nothing calls Advance in production" headers
|
||
corrected; PlayerModeController's two false "auto-entry retries" claims
|
||
replaced with the actual disarm-before-invoke semantics (verified against
|
||
PlayerModeAutoEntry.TryEnter :232-248 - _armed=false precedes the
|
||
invoke, and a throw propagates before the context's reveal Complete()).
|
||
- F9: the graphical activation-preparation provider caches the resolved
|
||
setup cylinder per incarnation (keyed by LocalEntityId; unresolved
|
||
results not cached; shadow disposition stays live - it is validated
|
||
against the exact registry at activation commit and may change between
|
||
pumps).
|
||
|
||
### New tests
|
||
- Runtime: CommitActivationArmsTheLoginConstraintLeashAtTheCommittedPlacement,
|
||
CommitActivationNeverRearmsTheLeashOnAStaleRetry (publication suite);
|
||
ContentLessDirectSink_KeepsPreFlipLegacyRegistration,
|
||
FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner
|
||
(session-controller suite; DirectSinkOwnsCanonicalCreateUpdateDelete...
|
||
updated to pass a world projection so it keeps exercising the residence
|
||
route per R3's semantics).
|
||
- App: PostResidenceRebucket_TakesTheFullLegacyPathIncludingTheClockEdge
|
||
(active-suppression + F5 ledger visibility + retired->legacy CommitRebucket
|
||
+ the pending-reentry clock rebase),
|
||
ResidenceConversionToLegacyImmediate_RefusesWhileTheResidenceIsActive.
|
||
- Headless: FarRemoteCreateCompletesCelllessWithoutPinningItsResidence
|
||
(+ Spawn(guid, cellId) helper param; FixtureCollisionNeighborhood
|
||
implements IsWithinServiceWindow).
|
||
- One in-flight test correction during TDD: the leash test's anchor
|
||
assertions initially assumed the committed placement kept the raw spawn
|
||
Z=3; the faithful placement transaction already floor-snaps (2.705), so
|
||
the anchor asserts the committed floor-snapped band + committed cell.
|
||
|
||
### Gate ladder (this round)
|
||
1. Runtime 1,003/1,003; App 4,038/3 skips of 4,041; Headless 79/79.
|
||
2. Release solution build: 0 errors. Complete solution (-m:1,
|
||
ACDREAM_PAK_PATH): 10,815 passed / 0 failed / 4 skipped of 10,819
|
||
(App 4,038/3, Bake 15, Cli 4, Content 124, Core.Net 762, Core 4,247/1,
|
||
Headless 79, Runtime 1,003, UI 543).
|
||
3. Connected gate run 1: logs/connected-world-gate-20260802-174811 —
|
||
Passed=false, ATTRIBUTED TO USER INTERFERENCE per the coordinator
|
||
(the user manually drove the client, including a teleport, during the
|
||
run). Fingerprint: every failure at the capped_login checkpoint only
|
||
(transitOwnership.activeTeleportCount=1 at the stable checkpoint,
|
||
activeRevealCount=1, pendingDestinationReadinessCount=1,
|
||
hostProjectionCount=1, reveal/viewport/composites/collision not ready,
|
||
216 staged mesh uploads + 44 composite warmups mid-stream to the manual
|
||
teleport destination). No crash, no exception; only the expected
|
||
world-edge warning class. Coordinator sanctioned exactly ONE clean
|
||
re-run (external interference, not a blind retry); graceful-close
|
||
discipline held (no stale client process, ACE endpoint intact).
|
||
4. git diff --check: exit 0 (CRLF metadata warnings only, the known
|
||
worktree pattern); nothing staged.
|
||
3 (result). Sanctioned clean re-run PASS:
|
||
logs/connected-world-gate-20260802-175401/report.json Passed=true,
|
||
Failures=[], only the expected world-edge warning — the established
|
||
passing signature (142539/161138/164432 class). 174811 interference
|
||
attribution confirmed. Round complete; nothing staged.
|
||
|
||
## P1 — origin-recenter retirement-receipt exception loop
|
||
|
||
Root cause pinned in
|
||
`p1-retirement-receipt-loop.md`. An already-detached landblock can retain
|
||
live projections in `GpuWorldState._pendingByLandblock` while its one exact
|
||
full-cleanup ticket advances. The recenter swap incorrectly promoted that
|
||
pending-only spatial bucket into a second full presentation receipt. The
|
||
coordinator rejected the duplicate correctly, but the controller's broad
|
||
resume catch replayed the already-committed detach 243 times in the captured
|
||
feel-test session.
|
||
|
||
Implemented:
|
||
|
||
- pending-only live buckets are retained through `_projectionLocations` but
|
||
no longer manufacture a landblock retirement receipt;
|
||
- loaded/pending-render/pending-near/tier/bounds owners still receive exact
|
||
receipts;
|
||
- a genuine receipt-ledger invariant after the spatial commit is surfaced as
|
||
a committed `StreamingMutationException` and cannot enter retry work;
|
||
- the existing duplicate-receipt guard remains unchanged.
|
||
|
||
TDD evidence: the new pending-only regression failed before the source edit
|
||
(`Assert.Empty`, one receipt returned) and passes afterward. The production
|
||
controller/recenter regression and committed-invariant fail-fast regression
|
||
also pass. Focused `OriginRecenter` group: 20/20.
|
||
|
||
Final gates: Release build passed; the complete Release suite passed
|
||
10,815/10,815 with 4 skips; the connected lifecycle gate passed at
|
||
`logs/connected-world-gate-20260802-203751/report.json`; and the nine-stop
|
||
soak passed at `logs/connected-r6-soak-20260802-204309.report.json` with all
|
||
9 canonical checkpoints, zero failures, zero wait cues, zero pending
|
||
landblock retirements, and zero recurrence of the 243x exception signature.
|
||
|
||
### F3 addendum (coordinator resolution accepted, implemented)
|
||
Hand-calls KEPT as honest documented models: enriched comments at all six
|
||
item-6/8 sites (LiveEntityHydrationControllerTests x5 sites incl. the
|
||
shared item-6/CompletedSupersessionReadyFailure text,
|
||
LiveEntityCreateSupersessionRecoveryTests x1) citing
|
||
ApplyWeenieDescriptionAction as the sole production site,
|
||
RuntimeEntityObjectLifetime :660-665 !beginInitialResidence gate, and
|
||
ConsumeExecuted. NEW source pin
|
||
tests/AcDream.App.Tests/World/C3cR1F3DriftModelSourcePinTests
|
||
.HandCalledDriftProbe_StillModelsTheExecutorDrainAdvance (single
|
||
_entities.AdvanceCreateAuthority in the executor, inside
|
||
ApplyWeenieDescriptionAction; registration advance still residence-gated).
|
||
Focused files 82/82; Runtime 1,003/1,003; App 4,039/3 skips of 4,042;
|
||
git diff --check exit 0. No staging/commits.
|
||
|
||
## 2026-08-03 stabilization checkpoint and handoff
|
||
|
||
Five separately bisectable root-cause fixes followed the O1/O2/O3 checkpoint:
|
||
|
||
1. `01f4791e` — pending-only live projection buckets no longer manufacture a
|
||
second origin-recenter retirement receipt. Complete Release, lifecycle,
|
||
and canonical nine-stop soak passed on this binary; the soak had nine
|
||
checkpoints, zero failures, zero wait cues, and zero pending retirements.
|
||
2. `670f307c` — Runtime converts remote Create frames through the accepted
|
||
local-player world center and publishes the local physics body's world
|
||
position. The user accepted monster/static placement, chase, and attacks.
|
||
3. `1fc529cd` — distant Use materializes the canonical minimal static physics
|
||
host before MoveTo, and presentation attach reconciles the pre-PartArray
|
||
startup motion suffix. The user accepted near and distant object use.
|
||
4. `f24532ad` — exact-incarnation effect packets wait for canonical
|
||
presentation binding; projectile and static-animation sidecars retry on the
|
||
committed visibility edge; effect cells follow rebuckets. The user accepted
|
||
buffs, recalls, arrows, combat spell projectiles, portals, and statics.
|
||
5. `175ad6b0` — LoginComplete is emitted from the local first-placement
|
||
terminal edge instead of raw PlayerCreate receipt. The user accepted login
|
||
materialization haze behavior.
|
||
|
||
Post-fix focused evidence: 90 App effect/projectile/static-scheduler tests,
|
||
two Runtime login tests, the exact live-entity cell tracking test, all 79
|
||
Headless tests, and a Release solution build with zero errors passed. The
|
||
complete suite and connected nine-stop soak were not rerun after the final
|
||
four stabilization commits. A broader selected fixture run exposed five
|
||
still-open `LiveEntityRuntimeTests` failures associated with the placement
|
||
cutover and one old remote-first-entry fixture that supplies an empty
|
||
collision source. These are campaign work, not evidence to weaken the new
|
||
production contracts.
|
||
|
||
Open campaign finish: classify/fix those six fixtures; finish placement routes
|
||
2–7 and delete legacy writers; resolve #276/#277 and portal-prefetch #280; run
|
||
the final-binary complete suite/lifecycle/nine-stop/two-client gates; perform
|
||
the #269 slope-glide visual check; retire AP-1/AD-1/AP-131/AD-60 only when the
|
||
legacy paths are gone; then land AP-22 and AD-10 and close the ledger.
|