fix(physics): restore a cancelled park instead of leaving the entity withdrawn
Shipped-code defect affecting committed route-2 code, found while reviewing route 4b-1. RuntimeSetPositionState.ParkDeferred withdraws an entity from the world: body.InWorld = false, TransientStateFlags.Active cleared, WithdrawCanonical, SuspendObjectClock. CancelCoreDeferred then removed the operation and rewrote the pending Withdraw into a Discard while restoring NONE of it. So cancelling a wakeable park was strictly worse than keeping one — the park is wakeable, the cancel destroys the only object that could ever wake it, and the entity is left invisible AND intangible with nothing to bring it back. Route 2's re-issue funnel masked this: re-issuing is correct for a one-shot ForcePosition ACE never repeats, and wrong for a repeated remote stream, so the hole was hidden rather than fixed. Retail's own answer is a working park, verified in the decomp rather than assumed: CPhysicsObj::SetPositionInternal @0x00515BD0, when AdjustPosition yields no cell @0x00515C1D, calls prepare_to_leave_visibility @0x00515CDA, store_position @0x00515CE2 (the DESTINATION pose is committed), GotoLostCell @0x00515CF2 registering at m_position.objcell_id read AFTER store_position (so the destination cell), clears transient 0x80 @0x00515CF7, and returns OK @0x00515D07. InitObjCell @0x00508260 drains the lost list on cell load and calls reenter_visibility @0x00516250, which re-places from the object's OWN m_position with flags 0x11. Two corrections to the direction I gave, both forced by evidence and both right: The pose must NOT be rolled back — only the withdrawal. Three shipped route-2 tests capture positionAtPark AFTER the park and assert it survives the cancel, and retail agrees: store_position commits the destination and nothing un-commits it. Restoring residency at the body's committed cell is therefore retail's own cell choice, not merely self-consistent. The gate defaults to FALSE with four explicit opt-ins, rather than defaulting true with opt-outs at the withdrawal callers. That keeps every one of the ~20 shipped Forget/ForgetExactPlacement sites at exactly its current behaviour instead of depending on having correctly enumerated the withdrawal transactions. Review had already found the broad version corrupting five of them (TryApplyPickup, CommitAcceptedParent, CommitAcceptedParentCellless, CommitWithdrawal, CommitPositionChannelUpdate): they hand-roll a partial re-withdrawal that undoes the clock and FullCellId but not InWorld or the _spatialRoots re-registration, leaving a picked-up item both in inventory and an InWorld cellless spatial root in the physics workset. ParkDeferred's restorableOnCancel is opt-in for exactly one of its four callers — the plain unplaceable-destination park. Every quiescence and retirement park is excluded deliberately: those entities are withdrawn because their world is going away, and restoring residency inside a quiescing prefix blocks its retirement. VerifyPositionChannelCancellation now asserts InWorld and IsSpatialRoot per channel — Position is a cancellation and must restore; Pickup and Parent are withdrawals and must not. It previously asserted only !IsDeferred and counts, which is why five green states hid this. Register row AP-136 measured against GotoLostCell/reenter_visibility rather than labelled "retail-shaped". Files #309 (the restore-on-cancel residual, with park-survives recorded as the retail-faithful target and its two blockers named: the NewerPositionPickupAndParentEachCancelExactLostOperation invariant and teardown convergence) and #310 (an unbounded retirement stall — a retained preparation retry pins its prefix through HasOldPrefixPlacementDebt forever, and TickLostCellDeadlines has no production caller so the 25 s timer never fires). This is a user-observable change to shipped paths: restorableOnCancel: true sits in SubmitPreparedPlacementCore, the shared core behind every production placement. AP-136 and #309 carry the proposed two-client check. Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline 10,938). Every new test discrimination-verified by reverting the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9e97be1896
commit
634bc5513a
9 changed files with 666 additions and 10 deletions
149
docs/ISSUES.md
149
docs/ISSUES.md
|
|
@ -24,6 +24,155 @@ What does NOT go here:
|
|||
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
|
||||
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
|
||||
|
||||
## C4 route 4b-1 review — park lifecycle — 2026-08-04
|
||||
|
||||
#309 and #310 filed from the route 4b-1 dual-review round; #311 filed from
|
||||
the delta-review round on the same route's remediation. Evidence:
|
||||
[`2026-08-04-c4-route-4b-1-review-findings.md`](research/2026-08-04-c4-route-4b-1-review-findings.md).
|
||||
|
||||
## #309 — Cancelled lost-cell park re-shows the entity where retail would keep it hidden
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** MEDIUM
|
||||
**Filed:** 2026-08-04
|
||||
**Component:** physics / placement
|
||||
|
||||
**Description:** When a `DeferredCell` park is cancelled by the accepted-Position
|
||||
merge, we now roll the withdrawal back (`InWorld`, object clock, canonical
|
||||
residency) so the entity is no longer stranded invisible-and-intangible. But the
|
||||
entity becomes **visible immediately at the committed destination pose, without
|
||||
collision**, whereas retail keeps it hidden and re-shows it only when the cell
|
||||
loads.
|
||||
|
||||
**Root cause / status:** Retail's lost-cell mechanism has no cancel at all.
|
||||
`CPhysicsObj::SetPositionInternal` @0x00515BD0 commits the destination pose via
|
||||
`store_position` @0x00515CE2 and registers the object with
|
||||
`CObjectMaint::GotoLostCell` @0x00515CF2 (@0x00508210). That registration is
|
||||
removed by exactly one thing — `CObjectMaint::InitObjCell` @0x00508260, which
|
||||
drains the lost list on cell load and calls `CPhysicsObj::reenter_visibility`
|
||||
@0x00508296 (@0x00516250), re-placing at the pose `store_position` committed.
|
||||
An update that performs no SetPosition leaves the registration untouched.
|
||||
|
||||
So the retail-faithful end state is a park that **survives** cancellation. That
|
||||
was implemented and reverted this round because it reverses a shipped, tested
|
||||
invariant — `NewerPositionPickupAndParentEachCancelExactLostOperation`
|
||||
(`tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs`, helper
|
||||
`VerifyPositionChannelCancellation`) asserts `Assert.False(IsDeferred(record))`,
|
||||
i.e. a newer Position cancels the park — and because surviving parks broke
|
||||
`GameRuntime` teardown convergence (stage 10). Re-deciding that invariant plus
|
||||
teardown convergence is the blocker.
|
||||
|
||||
Residual in practice: a remote at 5-10 Hz is superseded within ~150 ms. The case
|
||||
that bites is a remote that teleports into a non-resident landblock and then
|
||||
**stops moving**, because ACE stops broadcasting for a stationary entity.
|
||||
|
||||
Also unrestored: the `ShadowObjectRegistry.Suspend` applied by
|
||||
`WithdrawCanonical`. Un-suspending requires a real placement dispatch
|
||||
(`ReplacePositionRows`), so the entity rejoins the collision broadphase on its
|
||||
next placement rather than at cancel time.
|
||||
|
||||
**Files:** `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` —
|
||||
`ParkDeferred`'s `restorableOnCancel`, `Forget`, `RestoreParkWithdrawal`.
|
||||
|
||||
**Acceptance:** Park survives a superseding no-placement Position and wakes via
|
||||
`InitObjCell`-equivalent collision-generation arrival, with teardown converging
|
||||
and the newer-Position invariant deliberately re-decided.
|
||||
|
||||
**Connected check required — this is NOT a no-behaviour-change slice.** The
|
||||
route 4b-1 contract's "no connected gate" line does not apply to the park fix.
|
||||
`restorableOnCancel: true` is set in `SubmitPreparedPlacementCore`, the shared
|
||||
core behind EVERY production placement, and the merge-time
|
||||
`restoreCancelledPark: true` is on the accepted-Position path every remote and
|
||||
the local player traverse. So shipped behaviour changes for any entity whose
|
||||
placement parks.
|
||||
|
||||
Proposed connected check (two clients, local ACE):
|
||||
1. Walk the observed character to a landblock boundary so a remote sits in a
|
||||
landblock the observer has not streamed, forcing a `DeferredCell` park.
|
||||
2. Confirm the remote no longer vanishes permanently — the pre-fix symptom was
|
||||
invisible AND intangible for the rest of the session.
|
||||
3. Confirm it appears at the SERVER-authoritative destination pose, not at a
|
||||
stale pre-park pose, and that it becomes collidable once the landblock
|
||||
publishes.
|
||||
4. Confirm the local player's own ForcePosition corrections (route 2) still
|
||||
land unchanged — that path shares the same cancel.
|
||||
Steps 1-3 are the user-visible acceptance for AP-136's residual.
|
||||
|
||||
## #310 — Retained preparation retry stalls landblock retirement with no bound
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** HIGH
|
||||
**Filed:** 2026-08-04
|
||||
**Component:** physics / streaming
|
||||
|
||||
**Description:** An entity holding a retained preparation retry keeps its
|
||||
landblock prefix in placement debt, so
|
||||
`TryAcquireCollisionPrefixMutationPermission` refuses on **every** poll and the
|
||||
landblock never retires. There is no bound and no timeout.
|
||||
|
||||
**Root cause / status:** `HasOldPrefixPlacementDebt` refuses permission while any
|
||||
affected root holds an operation, so `LandblockRetirementStage.Physics` never
|
||||
completes and the retirement coordinator simply retries forever.
|
||||
`TickLostCellDeadlines` — the only expiry that could break the cycle — has **no
|
||||
production caller**, so its deadline never fires. The only thing that clears the
|
||||
debt is an inbound packet for that same entity, which is exactly what a
|
||||
`RetrySetupUnavailable` on an asset that never loads does not produce.
|
||||
|
||||
Pre-existing and independent of route 4b-1; 4b-1 does not bound it, it only
|
||||
avoids widening it by declining to retain operations for destinations it cannot
|
||||
service. Pinned by
|
||||
`RuntimeCollisionPrefixQuiescenceTests.RetainedPreparationRetryStallsPrefixRetirementIndefinitely`
|
||||
(1,000 consecutive refusals), which also shows retiring the operation is what
|
||||
releases the prefix.
|
||||
|
||||
Note this is also why `ParkCollisionResidents`'s overlap throw is unreachable:
|
||||
permission is refused before `ParkCollisionResidents` is ever entered.
|
||||
|
||||
**Files:** `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` —
|
||||
`HasOldPrefixPlacementDebt`, `TryAcquireCollisionPrefixMutationPermission`,
|
||||
`TickLostCellDeadlines` (uncalled).
|
||||
|
||||
**Acceptance:** A retained preparation retry cannot block a landblock retirement
|
||||
indefinitely — either the deadline is driven in production or the retirement can
|
||||
proceed past stale placement debt.
|
||||
|
||||
## #311 — RetryPendingProjections allocates a fresh array on every non-empty call
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (perf, not correctness)
|
||||
**Filed:** 2026-08-04
|
||||
**Component:** physics / headless
|
||||
|
||||
**Description:** `RuntimeSetPositionState.RetryPendingProjections` (reached via
|
||||
`RuntimePlacementProjectionChannel.RetryPending` →
|
||||
`RuntimePlacementProjectionSubscription.RetryPending`) snapshots the entire
|
||||
pending-projection dictionary into a fresh array on every call —
|
||||
`_pendingProjection.Values.ToArray()`. C4 route 4b-1's N3 fix
|
||||
(`HeadlessSessionEventRoute.RetryPending`, wired from `HeadlessSessionHost.Tick`)
|
||||
now reaches this path every headless host tick instead of once per session —
|
||||
new per-tick pressure K4's 30-session resource envelope was not measured with.
|
||||
|
||||
**Root cause / status:** The empty-FIFO case is closed —
|
||||
`RuntimePlacementProjectionSubscription.HasPendingReceipts` (added alongside
|
||||
this issue) lets a host early-out before ever reaching
|
||||
`RetryPendingProjections` when nothing is outstanding, which is the
|
||||
overwhelming common case in steady state. The non-empty case still
|
||||
allocates: every call that DOES have an outstanding receipt pays a fresh
|
||||
`.ToArray()` copy. Closing it needs a non-allocating rewrite of
|
||||
`RetryPendingProjections` itself (e.g. a reusable scratch buffer, mirroring
|
||||
the `_driveScratch` pattern `RuntimeRemotePlacementDriveController.Advance`
|
||||
and `RuntimeFirstEntryDriveController` already use) — deferred rather than
|
||||
attempted in the C4 route 4b-1 delta-review session that filed this, whose
|
||||
task scope held `RuntimeSetPositionState.cs` off-limits for that session
|
||||
(a concurrent, separately-owned change was landing in the same file).
|
||||
|
||||
**Files:** `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` —
|
||||
`RetryPendingProjections`. Call site:
|
||||
`src/AcDream.Headless/Hosting/HeadlessSessionEventRoute.cs` — `RetryPending`.
|
||||
|
||||
**Acceptance:** A headless tick with N>0 outstanding placement receipts does
|
||||
not allocate a new array per tick.
|
||||
|
||||
## Recent-regression cleanup — 2026-08-03
|
||||
|
||||
Plan: [`2026-08-03-recent-regression-cleanup.md`](plans/2026-08-03-recent-regression-cleanup.md).
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ readiness/requeue adaptation. See
|
|||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 93 active rows (AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-1 narrowed 2026-07-31 by placement/streaming Slice 4A — the pure canonical retail `SetPosition` transaction exists, but production routes and lost-cell lifetime remain on the legacy resolver until Slice 4B; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
|
||||
## 3. Documented approximation (AP) — 94 active rows (AP-136 filed 2026-08-04, C4 route 4b-1 review — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-1 narrowed 2026-07-31 by placement/streaming Slice 4A — the pure canonical retail `SetPosition` transaction exists, but production routes and lost-cell lifetime remain on the legacy resolver until Slice 4B; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55)
|
||||
|
||||
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
||||
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
||||
|
|
@ -282,6 +282,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-133 | **Filed 2026-08-03 (#282).** A retail `CPhysicsObj` has exactly ONE `cell`; `ShouldDrawParticles` @0x0050fe60 reads that same field and calls `IsInView` on it, and `set_cell_id` @0x0050f4f0 / `change_cell` @0x00513390 are the only things that move it. acdream splits the concept into `WorldEntity.ParentCellId` (render parent, null for outdoor dat stabs and building shells) and `WorldEntity.EffectCellId` (authored landcell for those parentless stabs). Every consumer now resolves through the single `WorldEntity.VisibilityCellId` accessor (`ParentCellId ?? EffectCellId`); live entities carry `ParentCellId` only. | `src/AcDream.Core/World/WorldEntity.cs` (`VisibilityCellId`); writers `LandblockLoader.cs:80,97`, `LandblockBuildFactory.cs:408` | Outdoor dat stabs deliberately keep a null render parent so portal visibility does not filter them as interior geometry, yet retail still gives their physics object a landcell for particle gating. One accessor keeps the two fields from being read in conflicting orders, which is exactly how #282 arose - `EntityEffectPoseRegistry` preferred `EffectCellId` while `WbDrawDispatcher` and the remote spawn seed preferred `ParentCellId`. | A future writer that sets `EffectCellId` on a live entity re-creates #282: it wins `VisibilityCellId` while the 11 per-tick `ParentCellId` writers leave it frozen, stranding that entity's particles and lights on a stale cell so they fail `IsInView` after it crosses a boundary. | `CPhysicsObj::ShouldDrawParticles` 0x0050fe60; `CPhysicsObj::set_cell_id` 0x0050f4f0; `CPhysicsObj::change_cell` 0x00513390 |
|
||||
| AP-134 | **Filed 2026-08-03 (#297).** Retail keeps ONE `PublicWeenieDesc::_bitfield` per object and mutates it in place — `SetPlayerKillerStatus` @0x005AC7C0 rewrites bits 5/21/25 (PK `0x20` / Free `0x200000` / PKLite `0x2000000`, mutually exclusive), driven from `ACCWeenieObject::OnStatUpdated` @0x0058DF20 `case 0x86`, and `IsPK`/`IsImpenetrable`/`IsPKLite` @0x0058C8xx read that same field. acdream replicates the value into FIVE stores: `ClientObject.PublicWeenieBitfield` (the source, written only by `ClientObjectTable.UpdateIntProperty` on PropertyInt 134), `InboundPhysicsStateController._snapshots[guid].ObjectDescriptionFlags`, `RuntimeEntityRecord.Snapshot.ObjectDescriptionFlags`, the decoded `ShadowObjectRegistry` registration + per-cell `ShadowEntry.Flags`, and the local player's `RuntimeMovementSkillState` own-PWD bitfield. Coherence is maintained by two `ObjectUpdated` subscribers (`RuntimeEntityPvpBitfieldSnapshotSync` for the two snapshot stores, `LiveEntityPvpBitfieldSync` for the decoded shadow flags) plus the appearance-rebuild path re-deriving from the snapshot. The two shadow-flag writers are the SAME invalidation applied at the two edges that can invalidate it, not competing authorities. | `src/AcDream.Runtime/Entities/RuntimeEntityPvpBitfieldSnapshotSync.cs`; `src/AcDream.App/Physics/LiveEntityPvpBitfieldSync.cs`; source writer `src/AcDream.Core/Items/ClientObjectTable.cs` (`UpdateIntProperty`, PropertyInt 134); decode `EntityCollisionFlagsExt.FromPwdBitfield` | ACE never re-sends a `PublicWeenieDesc` after login (`EnqueueBroadcastUpdateObject` has zero live callers), so PropertyInt 134 over 0x02CE/0x02CD is the ONLY signal a PK status changed — a client cannot learn it from the bitfield itself. The replication exists because acdream separates wire snapshots, canonical records, and the collision shadow registry, which retail does not; each layer needs the decoded value at a different lifetime. Before #297 the snapshot stores were immutable wire captures; this commit is what converts them into write-through caches, and therefore what creates the invariant. | Any future write path that sets `ClientObject.PublicWeenieBitfield` outside `UpdateIntProperty`, or any NEW decoded cache of the PK bits, silently re-creates #297: the player walks through PKLite opponents and melee/missile admission refuses them, with no test failing. Note the same class already exists one field over — `Properties.Ints[134]` is written by `UpsertProperties` (PlayerDescription 0x0013) and `UpdateProperties` (IdentifyObjectResponse) WITHOUT mirroring into the bitfield (#300), and retail's `OnStatUpdated` also rewrites `_blipColor` (`case 0x5f`) and `_radar_enum` (`case 0x85`) which acdream ignores entirely (#301). | `PublicWeenieDesc::SetPlayerKillerStatus` 0x005AC7C0; `ACCWeenieObject::OnStatUpdated` 0x0058DF20 (`case 0x86`); `ACCWeenieObject::IsPK`/`IsImpenetrable`/`IsPKLite` 0x0058C8xx; retail `PKStatusEnum` `acclient.h:6412-6427` |
|
||||
| AP-135 | **Filed 2026-08-03 (C4 route 4a).** Retail `CPhysicsObj::MoveOrTeleport` 0x00516330 writes NOTHING on the airborne no-op (`arg4 == 0` -> `return 0` @0x0051636D), and `SmartBox::HandleReceivedPosition` 0x00453FD0 skips `ConstrainTo` with it (@0x00454272 sits inside `if (MoveOrTeleport(...) != 0)` @0x00454254). acdream honours that for every retail-modeled write — body pose, interpolation queue, leash, render entity, collision shadow, and the AP-80 velocity-derived animation cycle — but deliberately KEEPS two acdream-only per-packet bookkeeping writes on that branch: `RemoteMotion.CellId = wire landblock` and the `LastServerPos`/`LastServerPosTime` sample. This was pre-existing player-remote behaviour; route 4a extends it to NPC remotes so both arms are identical | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition`, both remote airborne-no-op returns) | The cell id is what acdream's OWN per-tick free-fall `ResolveWithTransition` sweep gates on (`rm.CellId != 0`); without it an airborne remote's sphere sweep is skipped and it falls through the floor (#42's neighbourhood). The server sample is what the first grounded packet after the arc synthesizes its velocity from; dropping it would make that velocity span the whole jump. Neither is a retail `CPhysicsObj` field being written | A remote's cell membership tracks the server's landblock during an arc where retail would keep the cell its own physics last resolved. Visible only if the server's mid-arc landblock disagrees with the client's swept cell — the wire cell is authoritative in every case acdream has observed. Retire together with the free-fall sweep gate, when the remote arc is resolved by the same transition machinery the local player uses | `CPhysicsObj::MoveOrTeleport` 0x00516330 (@0x0051636D `return 0`); `SmartBox::HandleReceivedPosition` 0x00453FD0 (@0x00454254/@0x00454272) |
|
||||
| AP-136 | **Filed 2026-08-04 (C4 route 4b-1 review).** Retail has NO cancel for a lost-cell park. `CPhysicsObj::SetPositionInternal` @0x00515BD0 commits the destination pose with `store_position` @0x00515CE2 and registers the object via `CObjectMaint::GotoLostCell` @0x00515CF2 (@0x00508210); the registration is removed by exactly one thing, `CObjectMaint::InitObjCell` @0x00508260, which drains the lost list on cell load and calls `CPhysicsObj::reenter_visibility` @0x00508296 (@0x00516250) to re-place at the committed pose. An update that performs no SetPosition leaves the registration untouched, so retail keeps the object HIDDEN until its cell loads. acdream's accepted-Position merge cancels the park instead (a shipped, tested invariant), so on cancel we roll the withdrawal back — `InWorld`, object clock, canonical residency — and the entity becomes VISIBLE IMMEDIATELY at the committed destination pose, uncollidable until its landblock publishes. The pose itself is retail-exact and is deliberately not rolled back. The `ShadowObjectRegistry.Suspend` applied by `WithdrawCanonical` is also not lifted, because un-suspending needs a real placement dispatch (`ReplacePositionRows`); the entity rejoins the broadphase on its next placement | `src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs` (`ParkDeferred`'s `restorableOnCancel`, `Forget`, `RestoreParkWithdrawal`) | The alternative — leaving the cancelled park's withdrawal in place — strands the entity invisible AND intangible for the rest of the session, because `CancelCoreDeferred` restores none of it and the operation that was the only thing able to wake it is gone. Restoring at the pre-park pose was tried and is wrong: retail commits the destination pose, and route 2's tests pin that the pose survives the cancel. Restore is scoped to the plain unplaceable-destination park only; quiescence/retirement parks are deliberately NOT restored, since re-admitting a spatial root into a retiring prefix blocks the retirement | A remote that teleports into a non-resident landblock and then STOPS MOVING stays visible at the destination without collision, where retail would hide it and re-show it on cell load — ACE stops broadcasting for a stationary entity, so no later packet corrects it. At 5-10 Hz the ordinary case is superseded within ~150 ms. Retire by making the park SURVIVE cancellation (issue #309), which is blocked on re-deciding the newer-Position-cancels-the-park invariant pinned by `NewerPositionPickupAndParentEachCancelExactLostOperation` and on teardown convergence. **This row carries a user-observable change to shipped paths** — `restorableOnCancel: true` sits in `SubmitPreparedPlacementCore`, the shared core behind every production placement — so it needs the two-client connected check written up in #309 (force a remote to park across a landblock boundary; confirm it no longer vanishes permanently, appears at the server-authoritative destination pose rather than a stale one, becomes collidable once the landblock publishes, and that route 2's local ForcePosition corrections are unchanged) | `CPhysicsObj::SetPositionInternal` @0x00515BD0 (@0x00515C1D/@0x00515CDA/@0x00515CE2/@0x00515CF2/@0x00515CF7/@0x00515D07); `CObjectMaint::GotoLostCell` @0x00508210; `CObjectMaint::InitObjCell` @0x00508260 (@0x00508296); `CPhysicsObj::reenter_visibility` @0x00516250 |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 36 active rows (TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState` — **narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source `PublicWeenieBitfield` was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: `RuntimeSetPositionMoverPreparation.cs:183-188` is a SEVENTH mover-flags site that decodes `record.Snapshot.ObjectDescriptionFlags` directly rather than calling `ResolveMoverPvpState`, and it also derives `ObjectInfoState.IsPlayer` from the PWD bit, contradicting `EntityCollisionFlags.cs:119-123`'s claim that every site uses a GUID-prefix heuristic. See AP-134.** — and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue