test(vfx): harden live missile and effect lifetimes
Add deterministic 96-owner lifecycle and renderer-resource stress gates plus an exact twelve-cycle recall/portal ownership gate. Prove zero retained records, projectiles, spatial buckets, script queues, particle/light owners, shadows, pending effects, and mesh references after churn, GUID reuse, deletion, and session reset. Make logical teardown incarnation-specific and reentrancy-safe with lifetime epochs, atomic resource registration, generation-aware effect/teleport cleanup, projection mutation tokens, and failure-isolated visibility fan-out. Finish canonical spatial transactions before reporting observer failures and never discard superseded cleanup failures. Synchronize architecture, roadmap, milestones, retail research, divergence bookkeeping, and durable memory. All three independent review tracks are clean; Release build and the full 5,454-pass/5-skip suite are green. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
1e98d81448
commit
8d63e5c28a
21 changed files with 2704 additions and 100 deletions
|
|
@ -60,7 +60,7 @@ well-defined interfaces that the retail client never had.
|
|||
├──────────────────────────────────────────────────────────────┤
|
||||
│ LAYER 1: Renderer │
|
||||
│ Silk.NET OpenGL 4.3 core profile │
|
||||
│ TerrainRenderer, StaticMeshRenderer, TextureCache │
|
||||
│ TerrainModernRenderer, WbDrawDispatcher, EnvCellRenderer │
|
||||
│ Shaders (terrain blending, mesh lighting, translucency) │
|
||||
│ ► completely different from retail (D3D7), same visual │
|
||||
│ output │
|
||||
|
|
@ -186,13 +186,14 @@ src/
|
|||
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
|
||||
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
|
||||
World/
|
||||
LiveEntityRuntime.cs -> canonical logical identity/state/spatial ownership
|
||||
LiveEntityRuntime.cs -> canonical identity/state/body/projectile/spatial ownership
|
||||
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
|
||||
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
|
||||
Rendering/
|
||||
GameWindow.cs -> still owns too much runtime wiring
|
||||
TerrainRenderer.cs -> done
|
||||
StaticMeshRenderer.cs -> done
|
||||
TerrainModernRenderer.cs -> mandatory bindless+MDI terrain path
|
||||
Wb/WbDrawDispatcher.cs -> ordinary live/static entity draw dispatch
|
||||
Wb/EnvCellRenderer.cs -> indoor cell-shell draw path
|
||||
TextureCache.cs -> done
|
||||
ChaseCamera.cs -> done
|
||||
FlyCamera.cs -> done
|
||||
|
|
@ -285,7 +286,12 @@ What exists and is active:
|
|||
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
|
||||
rollback/rebucket inside a visibility observer therefore cannot expose a
|
||||
remove/add pulse, leave stale final visibility, or reorder the final
|
||||
presentation edge.
|
||||
presentation edge. Observer failures are reported only after both spatial and
|
||||
canonical runtime cell/projection state have committed.
|
||||
Every projection operation also carries a per-record mutation token; if a
|
||||
synchronous observer replaces the GUID or reprojects that same record, the
|
||||
displaced outer operation cannot overwrite the newer cell or presentation
|
||||
indices.
|
||||
- `BSPQuery` contains the partial retail-style BSP collision dispatcher used by
|
||||
the transition path.
|
||||
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,
|
||||
|
|
@ -326,12 +332,20 @@ Ownership by phase:
|
|||
|
||||
`LiveEntityRuntime` is now the shipped bridge to this target. It owns one
|
||||
`LiveEntityRecord` per accepted server-object incarnation, ServerGuid-to-local-id
|
||||
translation, accepted snapshots/timestamp gates, animation and remote-motion
|
||||
components, parent-event state, effect-profile defaults, and exact logical
|
||||
translation, accepted snapshots/timestamp gates, the canonical `PhysicsBody`,
|
||||
animation/remote-motion/projectile components, parent-event state,
|
||||
effect-profile defaults, and exact logical
|
||||
teardown. Live `PhysicsDesc` effect fields replace Setup defaults on that same
|
||||
record; rebucketing never recreates them. `GpuWorldState`
|
||||
owns spatial buckets only: register/rebucket/unregister are separate operations,
|
||||
and landblock reloads reuse the same `WorldEntity` without replaying renderer or
|
||||
logical teardown removes the exact `WorldEntity` incarnation rather than every
|
||||
projection sharing its server GUID, and per-GUID/session mutation epochs prevent
|
||||
a callback from resurrecting an outer CreateObject after delete/reset. Per-GUID
|
||||
epoch tombstones remain until session clear, so delete-then-create cannot repeat
|
||||
an outer operation's epoch (the ABA problem). Resource registration is an atomic
|
||||
boundary, superseded cleanup failures surface at the runtime boundary, and
|
||||
visibility observers are failure-isolated without interrupting canonical commits.
|
||||
Landblock reloads reuse the same `WorldEntity` without replaying renderer or
|
||||
script creation. Its canonical materialized view remains stable across pending
|
||||
landblocks, while a separate visible-only view feeds radar, picking, status, and
|
||||
targeting. Raw server PhysicsState and the final state produced by retail's
|
||||
|
|
@ -416,7 +430,7 @@ public sealed class GameEntity
|
|||
// Motion (ported from CMotionInterp)
|
||||
public MotionInterpreter Motion { get; } // walk/run/turn state
|
||||
|
||||
// Render output (consumed by StaticMeshRenderer)
|
||||
// Render output (consumed by WbDrawDispatcher)
|
||||
public IReadOnlyList<MeshRef> MeshRefs { get; }
|
||||
|
||||
// Per-frame update (matches retail update_object)
|
||||
|
|
@ -467,8 +481,10 @@ matches retail's `CPhysicsObj::DoObjDescChangesFromDefault` behavior.
|
|||
|
||||
5. Render tick
|
||||
└── Read current entity mesh refs, draw
|
||||
TerrainRenderer.Draw, StaticMeshRenderer.Draw
|
||||
(frustum cull, translucency pass, etc.)
|
||||
TerrainModernRenderer + WbDrawDispatcher + EnvCellRenderer
|
||||
(frustum cull, translucency pass, portal visibility, etc.)
|
||||
Projectiles remain ordinary live-entity draws; there is no global or
|
||||
projectile-specific render pass.
|
||||
|
||||
6. Plugin tick
|
||||
└── Fire IEvents, drain IActions queue
|
||||
|
|
@ -484,8 +500,7 @@ matches retail's `CPhysicsObj::DoObjDescChangesFromDefault` behavior.
|
|||
|
||||
## Render Pipeline (SSOT — current state + unified-PView target)
|
||||
|
||||
> **The per-frame render step above is STALE** (it names deleted classes
|
||||
> `TerrainRenderer` / `StaticMeshRenderer`). The modern path (Phase N.5, mandatory) is
|
||||
> The modern path (Phase N.5, mandatory) is
|
||||
> `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) +
|
||||
> `TerrainModernRenderer` (terrain), fed by the portal-visibility stack. This section is
|
||||
> the authoritative description of how indoor/outdoor rendering is *supposed* to work and
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 88 active rows
|
||||
## 3. Documented approximation (AP) — 87 active rows
|
||||
|
||||
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
|
||||
|
|
@ -188,8 +188,8 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-82 | **StickyManager deep-overlap back-off sign pin**: when the stick-gap overlap exceeds one tick's step (`speed×quantum < \|dist\|`, `dist < 0`), acdream applies `delta = −(speed×quantum)` (rate-limited back-off); ACE's literal port keeps `+delta` there — a runaway that steers INTO the target with equilibrium at centers-coincident. The BN mush (0x00555554-0x00555597) is unreadable on exactly this compare; the pin is refuted-by-evidence against ACE-literal: #171 gate-3 probe showed 1661 deep-overlap ticks all steering inward (monsters converged to centerDist≈0 — "monster inside the player") while retail side-by-side on the same ACE shows separation. ACE servers essentially never reach the branch (quantum ≥1/30 → threshold ~1 m; render-rate quanta → ~0.13 m) | `src/AcDream.Core/Physics/Motion/StickyManager.cs` (`AdjustOffset` delta clamp; conformance `StickyManagerTests.AdjustOffset_DeepOverlap_BacksOff_RateLimited`) | Minimal interpretation consistent with the mush structure AND observed retail; identical to ACE-literal in every shallow/outside case | If retail's true deep-overlap behavior differs (e.g. no movement at all), our back-off rate diverges in that rare state; verify via cdb `StickyManager::adjust_offset` trace with a forced overlap when convenient | `StickyManager::adjust_offset` 0x00555430 (x87 mush); ACE StickyManager.cs:117-121 (the literal branch this pin overrides) |
|
||||
| AP-85 | **Point-light pool = single 128-cap player-nearest list, optionally FILTERED by LAST FRAME's rendered visible-cell set, vs retail's dual pools (7 dynamic + 40 static, degrade-scaled) collected from a DBObj-load/flush-bounded resident registry** (A7.L1, 2026-07-09 — third revision, Town Network starvation fix #79/#93/#176/#177): retail's `CEnvCell::visible_cell_table` (`add_visible_cell` 0x0052de40) is populated ON DEMAND as cells are approached/seen (`DBObj::Get`-loads) and pruned by `flush_cells` — so a real dungeon's per-frame candidate set stays small (naturally proximity-bounded) even though the collection walk itself (`add_dynamic_lights` 0x0052d410) is "the whole resident table, not a re-flood." acdream's `_all` list instead registers at LANDBLOCK-granularity load/unload (a whole single-landblock dungeon streams as ONE unit), so for the Town Network (463 registered fixtures, one landblock) `_all` is effectively "everything ever loaded in this dungeon," not a proximity-bounded set — wide enough that the player-nearest-128 cap alone let a straight-line-closer-but-wall-disconnected corridor's fixtures out-rank the player's own room, starving it. Fix: `BuildPointLightSnapshot(playerWorldPos, visibleCells)` takes an optional candidacy FILTER — a light joins the pool iff `CellId==0` (cell-less, always in) or `visibleCells.Contains(CellId)` — narrowing candidates to the frame's actual visible cells BEFORE the existing dynamics-first player-nearest cap runs; `GameWindow` feeds LAST FRAME's already-rendered `RetailPViewFrameResult.DrawableCells` (one frame / ~16 ms latency, chosen specifically to avoid re-threading a mid-`DrawInside` callback — the exact mechanism, `c500912b`, that caused the #176 seam-floor flicker regression when it re-flooded an independent CAMERA-seeded set mid-frame). The distance-sort anchor stays the PLAYER (unchanged from the prior revision) — only candidacy narrows. Remaining deviation: this is a RENDER-visibility approximation of retail's true on-demand-load/flush RESIDENCY bound, with one frame of latency, not a port of the DBObj-load/flush mechanism itself; and the pool is still ONE 128-cap list vs retail's separate 7-dynamic/40-static degrade-scaled pools | `src/AcDream.Core/Lighting/LightManager.cs` (`BuildPointLightSnapshot`, `MaxGlobalLights`); `src/AcDream.App/Rendering/GameWindow.cs` (`_lightPoolVisibleCells`/`_lightPoolVisibleCellsValid`); pins `PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant`, `PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics`, `PointSnapshot_ResidentCollection_CellTagDoesNotFilter`, `BuildPointLightSnapshot_VisibleCellScoping_RoomLightsSurviveOverEuclideanCloserInvisibleCell`, `BuildPointLightSnapshot_VisibleCellScoping_CellLessLightAlwaysIncluded` | The render already computes a visible-cell set every frame for drawing (single source of truth, no duplicate flood) — reusing it as a candidacy filter approximates retail's proximity-bounded residency without porting DBObj on-demand load/flush; one-frame latency is imperceptible at normal camera speeds and structurally differs from the reverted mechanism (no independent re-flood mid-frame) | On a portal crossing, the FIRST indoor frame after re-entry (or after any outdoor-only frame) is unscoped (fail-open) — one frame may show slightly wider pool composition than steady-state; a room with >7 resident dynamics still shows them all (retail trims to 7 player-nearest) — slightly purpler wedge than retail; adopt the dual pools + degrade caps + true DBObj-bounded residency in later A7-arc work | `insert_light` 0x0054d1b0 (player-sorted, capped); `add_visible_cell` 0x0052de40 (on-demand-load resident registry + flush); `add_dynamic_lights` 0x0052d410 (whole-table walk); caller 0x00452d30; `calc_point_light` 0x0059c8b0 (static 1/d³ curve — A7 fix #2) |
|
||||
| AP-84 | **BSP shadow-shape part poses = motion-table default-state frame snapshot at registration, not retail's live CPhysicsPart pose** (#175): server entities with a wire MotionTableId register their BSP part shapes at the default style's first-cycle LowFrame pose (the closed pose for doors — `GameWindow.MotionTableDefaultPose`); retail collision reads each part's CURRENT pose every test. Equivalent for the door lifecycle (closed = default pose; open = ETHEREAL bypasses collision entirely, #150) and for idle statics | `src/AcDream.App/Rendering/GameWindow.cs` (`MotionTableDefaultPose` + the RegisterServerEntityCollision override); `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`partPoseOverride`) | Registration is one-shot in acdream (retail re-poses parts per frame); the default-state pose is the correct idle pose and the only non-ethereal pose doors ever collide in | An entity whose server-driven motion state materially MOVES a BSP-bearing part while NON-ethereal would collide at the stale default pose (no known case — doors are the dominant BSP-part weenies); revisit if animated non-ethereal BSP movers appear | `CPhysicsPart` live pose (see #150 notes); motion-table default state = CPartArray init; ShadowShapeBuilder placement-frame fallback for table-less entities |
|
||||
| AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). NOT exercised in M1.5: no mover sets PerfectClip (players never do; the non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified). Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail is dead code until missiles arm PerfectClip | If missiles (F.3) arm PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping missiles | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
|
||||
| AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch is dormant until missiles | When missiles arm PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
|
||||
| AP-83 | **CylCollideWithPoint PerfectClip TOI sub-branches decoded via ACE, not the binary**: the CCylSphere family port (2026-07-05, retires AP-6) reads `collide_with_point`'s PerfectClip time-of-impact math (0x0053adb6+) from ACE `CylSphere.CollideWithPoint` because the BN x87 mush is unreadable there; two ACE-verbatim quirks ported as-is (`movement.Z + radius` in the not-definite ascending case; `GlobalCurrCenter[0]` used even for head-sphere hits — the latter matches the raw decomp read). No current mover sets PerfectClip: players never do, and shipped ordinary missiles add PathClipped only. The non-PerfectClip path — SetCollisionNormal + Collided — is decomp-verified. Separately, the grounded head-sphere slide passes the HEAD disp per retail 0x0053b843 where ACE passes the foot disp — retail wins (ACE bug, not copied) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CylCollideWithPoint`; pseudocode doc `docs/research/2026-07-05-ccylsphere-collision-family-pseudocode.md` §7-8) | The load-bearing paths (non-PerfectClip Collided; the family's step-up/step-down/land) are decomp-verified; the TOI tail remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, the two ACE quirks may diverge from retail — clip-through or wrong deflection on cylinder targets; re-decompile 0x0053acb0 in Ghidra before shipping that mover | `CCylSphere::collide_with_point` 0x0053acb0 (pc:324173, x87 mush from 0x0053adb6); ACE CylSphere.cs `CollideWithPoint` |
|
||||
| AP-91 | **CSphere `collide_with_point` PerfectClip TOI decoded via ACE, not the binary**: the CSphere family port reads the unreadable x87 tail from ACE `Sphere.CollideWithPoint`/`FindTimeOfCollision`; no current mover sets PerfectClip, and shipped ordinary missiles add PathClipped only | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SphereCollideWithPoint`; `FindSphereTimeOfCollision`) | Load-bearing non-PerfectClip behavior is named-decomp verified; the adapted branch remains dormant unless a future mover explicitly enables PerfectClip | If a future mover explicitly enables PerfectClip, an ACE/retail TOI delta could cause clip-through or wrong sphere-target deflection | `CSphere::collide_with_point @ 0x00537230`; ACE `Sphere.CollideWithPoint` |
|
||||
| AP-86 | **Remote SHADOW-follows-resolved via a movement-gated per-tick re-flood** (remote-creature de-overlap #184, 2026-07-07): a moving NPC remote's collision shadow is re-registered at its RESOLVED body position (`SyncRemoteShadowToBody` → `ShadowObjects.UpdatePosition`) so neighbours + the player de-overlap / collide against where the monster actually is (== where it renders), and the de-overlap PERSISTS. This is retail-faithful in EFFECT (retail re-registers a moved object's shadow every accepted transition step, `SetPositionInternal`→`remove/add_shadows_to_cells` 0x00515330), but the IMPLEMENTATION differs: acdream runs the FULL `RegisterMultiPart` cell-flood, gated on `|Body−LastShadowSyncPos| > 1 cm`, rather than an in-place sphere translate with cell-relink-only-on-change; and the per-UP raw-worldPos shadow sync is RETIRED for EVERY remote (Slice 2b, 2026-07-08 — was players-only through Slice 1): every remote's shadow (player + NPC) is written ONLY by this per-tick loop + the UP-branch tail, both to the RESOLVED body, since Slice 2b collapsed the player/NPC fork so grounded PLAYER remotes also run the sweep + shadow-follows-resolved. Proven: `RemoteDeOverlapMechanismTests` (with-sync 0.86 m stable vs without-sync <0.40 m; real-interp loop absorbs the stall-blip — incl. the `ConvergingPlayers_RealInterpLoop` player-config pair added in Slice 2b) | `src/AcDream.App/Rendering/GameWindow.cs` (`RemotePhysicsUpdater.SyncRemoteShadowToBody` + the DR-tick movement gate; the NPC UP-branch tail sync + the player UP-branch tail sync; the RETIRED raw-pos sync site — a comment where the players-only `:5669` block used to be); `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`UpdatePosition`) | The movement gate is exact at the de-overlap equilibrium (the resolved position genuinely stops moving there, so no needed sync is skipped) and the flood result is identical to retail's per-step re-register; the cost is the only divergence | A dense town of many animated remotes re-floods per moving creature per tick (Gen0 churn — the MP-track FPS class); a still crowd is gated out. Optimize with an in-place shadow-move if profiling shows it. `rm.CellId==0` on a partial resolve keeps the prior cell (same pre-existing exposure as `:5669`) — only a landblock-crossing on that tick misplaces the shadow | `SetPositionInternal` 0x00515bd0 → `change_cell`/`add_shadows_to_cells` 0x00515330 (registers shadow from resolved `m_position`) |
|
||||
| AP-87 | **NPC MoveOrTeleport placement adds a 4 m body-to-target snap + a no-Sequencer snap** beyond retail's <96 m-unconditional interpolate (remote-creature de-overlap #184, 2026-07-07): retail `CPhysicsObj::MoveOrTeleport` (0x00516330) hard-places only on the teleport-timestamp / cell==0 branch or the ≥96 m far-snap, and InterpolateTo-queues every near correction; acdream ADDS two snap conditions — `|Body.Position − worldPos| > 4 m` (a large correction / an unplaced first-UP body) and `!willBeDrTicked` (no Sequencer to consume the queue). Without them an unplaced body (origin / spawn seed) would enqueue, the InterpolationManager's 100 m far-blip would fire, and the per-tick sweep would run over a huge distance in a cell not containing the body → garbage resolved pos → the reverted attempt's INVISIBLE monster. `firstUp` (`LastServerPosTime<=0`) is a belt hint only — the 4 m guard is the load-bearing backstop | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC MoveOrTeleport routing, `BodySnapThresholdNpc`/`willBeDrTickedNpc`) | acdream's catch-up+sweep needs the body already near the target (a valid nearby cell) for the per-frame sweep to be small; the 4 m snap keeps it there, and retail's own large-correction path (the 100 m far-blip) is upstream of it. The de-overlap sweep also uses the fixed human sphere (R 0.48 / H 1.835) for the mover regardless of creature size, so large packed creatures de-overlap at human radii — inherits **TS-46** | A grounded remote that legitimately lags >4 m from its server pos snaps (a small pop) where retail would slide; a no-Sequencer server-moved entity hard-snaps every UP (no DR smoothing). Both are rare | `CPhysicsObj::MoveOrTeleport` 0x00516330 (near-interpolate <96 m; teleport/cell-0 snap; far-snap ≥96 m); `InterpolationManager` 100 m `AutonomyBlipDistance` (the retail large-correction path) |
|
||||
| AP-88 | **Remote omega is reconstructed with a player/NPC fork retail does not have** (remote-creature de-overlap #184 Slice 2b, 2026-07-08): retail's `UpdateObjectInternal` applies ONE angular-velocity integration to every object; acdream reconstructs remote omega from the wire and keeps a player/NPC split inherited from the two former DR paths — a grounded PLAYER remote applies `ObservedOmega ∥ seqOmega` (falls back to the sequencer's synthesised cycle omega when the wire-TurnCommand-derived `ObservedOmega` is 0 — the "circling player sends RunForward+TurnLeft on ONE UM" case) in the WORLD frame (pre-multiply, `Quaternion.Concatenate`); an NPC or an AIRBORNE body applies `ObservedOmega`-only in the BODY frame (post-multiply, `Quaternion.Multiply`). Both feed the same downstream integrate; `calc_acceleration` zeroes `Body.Omega` for grounded bodies so `UpdatePhysicsInternal` never double-integrates | `src/AcDream.App/Physics/RemotePhysicsUpdater.cs` (`Tick`, Step 2 omega fork) | For an UPRIGHT body (the only remote pose — creatures/players never pitch or roll; the wire orientation is yaw-only) rotating about world-Z (the only turn axis) the pre- and post-multiply orders COMMUTE and both branches reduce to the same yaw increment; the seqOmega fallback only adds rotation a circling player genuinely has, and applying it to NPCs would spuriously add their baked cycle omega — so the fork is behaviourally faithful for every reachable pose (it is what the pre-2b Path A / Path B already did, now explicit in one method). ALSO: the merge applies omega BEFORE `ComputeOffset` for everyone (Path B's order), whereas pre-2b Path A applied a grounded PLAYER's omega AFTER its compose — so the `ori` fed to the anim-root-motion fallback (`Transform(seqVel·dt, ori)`, used only when the interp queue is empty/head-reached) is yaw-advanced by one tick (~ω·dt ≈ 2° at 2.24 rad/s, non-accumulating, zero while the catch-up is active). Keeping Path B's order leaves the shipped/gate-passed NPC omega untouched and only nudges a turning grounded player's fallback direction ~2° in rare queue-empty frames — cosmetically negligible; adopting Path A's order instead would have perturbed NPCs by the same amount | If a remote ever acquires a non-yaw orientation (pitch/roll — a future flying mount / ragdoll) the two multiplication orders diverge and a player would rotate differently from an NPC at the same omega; collapse to one order + a shared fallback policy then | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 (single apply_physics omega integration, no object-class fork); `apply_interpreted_movement` 0x00528600 (retail's server-driven omega source acdream reconstructs) |
|
||||
|
|
@ -215,7 +215,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The M2 attack contract and authored basic panel are live; the remaining seams require the jump/movement command owner and a distinct Recklessness treatment rather than UI-local guesses | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
|
||||
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 39 rows (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-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)
|
||||
## 4. Temporary stopgap (TS) — 34 active rows (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-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation)
|
||||
|
||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
|
|
|
|||
|
|
@ -518,6 +518,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **Missile/portal VFX campaign Step 6 implemented and independently reviewed 2026-07-14.** Pure Core now owns the retail projectile clock/integration/sweep primitive: 0.2-second catch-up quanta, 50-unit/second clamp, final-state acceleration, world-space omega, complete 3-D AlignPath, scaled Setup-local collision spheres, terrain/BSP/object continuous sweeps, exact `OBJECTINFO::missile_ignore`, self-shadow rejection, elastic/inelastic response, and correct full-cell rebasing across loaded landblocks. App live ownership and authoritative corrections remain Step 7. TS-2 is retired; ordinary missiles add PathClipped but not PerfectClip, so AP-83/AP-91 remain dormant.
|
||||
- **Missile/portal VFX campaign Step 7 implemented and independently reviewed 2026-07-14.** `ProjectileController` now attaches the Step 6 body to the canonical `LiveEntityRecord` after materialization and advances arrows, bolts, and spell missiles without another GUID map, renderer registration, or stale-spawn reconstruction. It commits predicted frames through `WorldEntity.SetPosition`, republishes static effect roots, and keeps collision shadows plus canonical full-cell buckets synchronized. A predicted crossing into an unloaded bucket suspends the body and shadow in that same quantum; pending/pickup residence retains the shadow registration for exact re-entry, and hydration plus the 96-unit retail activity gate restart at the current clock without backlog. Fresh State/Vector/Position/Movement packets correct or stop the same body after retail timestamp gates; SetState reaches the canonical body before optional projectile acquisition, and a non-finite local receipt clock cannot consume first classification. Removing Missile retains ordinary active-body physics (delegating to MovementManager when present), while both owners share the body and incarnation-scoped live-record cell. CreateObject vectors are installed only at body construction, never replayed by later SetState. Delete, reset, and GUID reuse use normal logical teardown; ACE remains authoritative for impact, damage, effects, and deletion.
|
||||
- **Missile/portal VFX campaign Step 8 implemented and independently reviewed 2026-07-14.** `LiveEntityRuntime` now distinguishes raw server PhysicsState from retail's side-effect-derived final state, beginning from constructor state `0x00400C08` and applying `set_state` in Lighting → NoDraw → Hidden order. `LiveEntityPresentationController` keeps Hidden objects logically alive while suppressing root mesh, collision, picking, radar, status, and new target acquisition; typed `PS_Hidden`/`PS_UnHide` plays remain DAT-driven, direct equipped children inherit NoDraw, particles/lights survive, and repeated/stale state cannot replay a transition. A normal visible CreateObject never fabricates UnHide. Effect-owner preparation is separate from pending F754/F755 replay so construction-state effects always run first. Remote fresh-teleport/cell-less placement now executes the exact ordered `teleport_hook` action bundle and a full-cell hard placement before contact/airborne routing, preventing an airborne correction from restoring the old location. Projectile Hidden state pauses the retained body without consuming its active identity or accumulating a clock backlog. TS-43 is retired; AP-69 remains because the separate 25-second/384-metre liveness cull is still unported.
|
||||
- **Missile/portal VFX campaign Step 9 automated hardening implemented and independently reviewed 2026-07-14; final two-client visual gate pending.** A deterministic 96-owner App fixture drives canonical missiles and effect owners through projectile updates, repeated DAT-effect scheduling, loaded↔pending↔loaded landblock churn, light/particle withdrawal and recovery, accepted deletes, same-GUID generation reuse, never-created F754 queues, and session reset. It asserts balanced logical render registration and zero residual records/bodies/projectiles, spatial buckets/rescues, shadows (including suspended registrations), effect profiles/packets, PhysicsScript FIFOs/anchors/delayed calls, particle bindings/logical IDs/render-pass owners, poses, light-controller/sink/manager owners, and stale record component references. A companion twelve-cycle gate drives exact recall motion `0x10000153` through AnimationSequencer/CallPES, Hidden, deferred remote placement, hydration, and UnHide; a second 96-owner `EntitySpawnAdapter` gate balances actual mesh-adapter reference counts without GL. The pass fixed undrained persistent rescue retention, delayed stale visibility edges, GUID-scoped teardown, create resurrection after a nested delete/reset, non-atomic resource registration, double teardown from a re-entrant session-clear callback, observer failure stranding later visibility edges, and stale outer projection transactions overwriting callback-created replacements. Teardown now removes exact projection references and generation/local-ID owners, uses per-GUID/session lifetime epochs plus per-record projection tokens, drains visibility fan-out before aggregating failures, and finishes or supersedes canonical commits before surfacing observer errors. Core remains GL/backend-free, panels remain on UI abstractions, and projectiles still draw through ordinary `WbDrawDispatcher` live-entity submission rather than a global projectile pass. AP-69 and TS-49 remain; AP-83/AP-91 now explicitly describe only a future mover that enables PerfectClip.
|
||||
|
||||
**Reference docs:** `docs/research/retail-ui/00-master-synthesis.md` + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.
|
||||
|
||||
|
|
|
|||
|
|
@ -471,8 +471,8 @@ include dungeons.
|
|||
`PlayerDescription.Options1` preserves retail's player secure-trade
|
||||
preference. See
|
||||
`docs/research/2026-07-13-retail-give-item-pseudocode.md` and issue #216.
|
||||
- **M2/M3 missile, effect, and portal presentation campaign (Steps 0–8
|
||||
independently reviewed 2026-07-14)** — named-retail projectile and
|
||||
- **M2/M3 missile, effect, and portal presentation campaign (automated Steps 0–9
|
||||
complete and independently reviewed 2026-07-14)** — named-retail projectile and
|
||||
physics-script behavior is pinned in one oracle, the complete `PhysicsDesc`
|
||||
and F754/F755 wire surfaces are parsed with retail timestamp gates, and
|
||||
`LiveEntityRuntime` now separates logical lifetime from spatial rebucketing.
|
||||
|
|
@ -533,8 +533,34 @@ include dungeons.
|
|||
`teleport_hook` teardown followed by a placement snap before the airborne
|
||||
gate. Ordinary visible CreateObject does not fabricate UnHide; pending
|
||||
F754/F755 packets replay only after construction-state effects. TS-43 is
|
||||
retired. The remaining Step 9 is final hardening plus the two-client visual
|
||||
gate.
|
||||
retired. Step 9 adds a deterministic 96-owner whole-lifecycle gate spanning
|
||||
projectile ticks, repeated DAT-effect plays, loaded↔pending landblock churn,
|
||||
dynamic lights, particle bindings, collision shadows, rapid deletion,
|
||||
same-GUID generation reuse, unknown pre-create effect packets, and session
|
||||
reset. A separate 96-owner renderer-seam gate balances every
|
||||
`EntitySpawnAdapter` mesh reference. Teardown now proves zero retained live
|
||||
records/bodies/projectiles, render owners, spatial buckets/rescues,
|
||||
PhysicsScript queues/anchors, effect profiles/packets, emitters/logical IDs,
|
||||
poses, lights, and active/suspended shadow registrations. The hardening pass
|
||||
also fixed undrained persistent-rescue retention and rejects queued spatial
|
||||
visibility edges that no longer match current incarnation truth. A companion
|
||||
twelve-cycle gate drives exact recall motion `0x10000153` through CallPES,
|
||||
Hidden, unloaded-destination placement, hydration, and UnHide. Logical
|
||||
teardown is incarnation-specific across callback-driven GUID reuse; per-GUID
|
||||
epochs reject create resurrection after a nested delete/reset, resource
|
||||
registration is atomic, terminal session clear rejects registrations outside
|
||||
its teardown snapshot, and visibility fan-out drains all owners before
|
||||
aggregating observer failures. Canonical cell/projection state finishes
|
||||
committing before those aggregates rethrow; per-GUID epoch tombstones survive
|
||||
accepted delete until session clear, and superseded creates cannot discard a
|
||||
prior-incarnation cleanup failure. Per-record projection tokens also prevent
|
||||
an outer rebucket/withdrawal from overwriting a replacement incarnation or a
|
||||
newer same-record projection created synchronously by an observer.
|
||||
TS-26's
|
||||
`UpdatePosition` freshness gap is retired by the nine-channel timestamp
|
||||
gates; TS-49 remains the explicit
|
||||
DetectionManager/`LeftDetection` gap. The final two-client visual gate is
|
||||
still pending.
|
||||
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)** —
|
||||
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted
|
||||
funnel and action-stamp gate, so ACE's server-selected melee/missile action
|
||||
|
|
|
|||
|
|
@ -1076,3 +1076,96 @@ publishes final part poses later in the same frame. PhysicsScript/particle/light
|
|||
ticks therefore observe the projectile's current predicted root. The controller never
|
||||
creates impact effects, collision messages, damage, or deletion; ACE's later
|
||||
packets remain authoritative for every gameplay outcome.
|
||||
|
||||
## Step 9 — deterministic lifecycle-hardening map (2026-07-14)
|
||||
|
||||
Step 9 adds no new AC algorithm and therefore no replacement retail formula.
|
||||
It verifies that the mechanisms above compose without retaining a displaced
|
||||
object incarnation. The acceptance oracle is ownership symmetry:
|
||||
|
||||
```text
|
||||
create 96 live missile/effect owners at a fixed clock and RNG seed
|
||||
queue F754 before CreateObject
|
||||
register one canonical live record/local ID
|
||||
materialize through the ordinary live-entity resource seam
|
||||
bind the projectile to that record's canonical PhysicsBody
|
||||
register its collision shadow, effect profile, pose, emitter, and light
|
||||
|
||||
advance all owners together
|
||||
execute the initial PES hook and retain a future hook in each serial FIFO
|
||||
tick the ordinary ProjectileController path
|
||||
never create a projectile-specific renderer registration or draw pass
|
||||
|
||||
churn spatial residence
|
||||
rebucket half loaded -> pending -> loaded
|
||||
unload/reload that destination landblock
|
||||
retain the exact local ID, body, renderer owner, effect owner, and FIFO
|
||||
pause/resume particle presentation and dynamic lights at projection edges
|
||||
|
||||
churn logical lifetime
|
||||
rapidly delete 32 owners
|
||||
recreate the same server GUIDs with a newer INSTANCE_TS
|
||||
prove the replacement owns new local IDs and no prior-generation resource
|
||||
mix individual deletes with session reset and never-created pending F754
|
||||
|
||||
terminal assertions
|
||||
zero live/materialized records and null body/projectile/effect/world refs
|
||||
zero loaded/pending/rescued live projection references
|
||||
zero render owners and balanced EntitySpawnAdapter mesh refcounts
|
||||
zero active, retained, and suspended collision-shadow registrations
|
||||
zero ready effect profiles and pending F754/F755 packets
|
||||
zero PhysicsScript owner queues, anchors, and delayed CallPES hooks
|
||||
zero particle emitters/particles/bindings/logical IDs/owner/render-pass maps
|
||||
zero effect poses and light controller/sink/manager owner maps
|
||||
|
||||
repeat the recall/portal lifecycle
|
||||
resolve exact action motion 0x10000153 through an in-memory MotionTable
|
||||
drain its animation CallPES through the shared router and serial scheduler
|
||||
apply Hidden, park authoritative placement in an unloaded destination
|
||||
hydrate and collision-seat that same body, then apply UnHide
|
||||
repeat twelve times while retaining one local ID and one logical owner
|
||||
finish with zero placement, presentation, script, particle, pose, and shadow owners
|
||||
```
|
||||
|
||||
The stress design exposed five ownership defects rather than masking them:
|
||||
|
||||
1. `GpuWorldState::RemoveLandblock` can rescue a persistent player projection
|
||||
before the next frame drains it. Rebucket/delete now scrubs an undrained
|
||||
rescue reference, while the session-scoped persistent GUID classification
|
||||
deliberately survives same-GUID generation replacement and clears only at
|
||||
accepted delete/session teardown.
|
||||
2. Spatial visibility callbacks are serialized and can be re-entrant. An old
|
||||
queued GUID edge is now ignored unless it still equals current spatial truth,
|
||||
so deleting/recreating a GUID inside an observer cannot hide or re-register
|
||||
the replacement incarnation. Each rebucket/withdrawal also captures the
|
||||
record reference plus a projection mutation token; an observer that replaces
|
||||
the GUID or reprojects the same record supersedes the outer transaction.
|
||||
3. Logical delete and generation replacement previously tore down spatial and
|
||||
runtime indices by GUID after arbitrary callbacks. Teardown now captures and
|
||||
removes the accepted record before callbacks, removes its exact WorldEntity
|
||||
reference, and clears generation/local-ID owners only when they still belong
|
||||
to that record. A per-GUID mutation epoch prevents an outer CreateObject from
|
||||
resurrecting after a nested delete/reset, while unrelated GUID activity does
|
||||
not cancel it. Its tombstone remains until session clear so nested
|
||||
delete-then-create cannot repeat the outer operation's epoch. A teardown
|
||||
failure is thrown at the runtime boundary even when a nested newer generation
|
||||
supersedes the outer create. Materialization is an atomic boundary: logical mutation from a
|
||||
resource-registration callback is rejected and rolled back, and a replacement
|
||||
registered during old teardown cannot materialize until that teardown ends.
|
||||
4. Session Clear previously snapshotted records while allowing callbacks to
|
||||
register owners outside the snapshot. Registration is now rejected during
|
||||
terminal clear, preventing untracked renderer/script resources; snapshot
|
||||
entries already deleted by an earlier callback are skipped rather than torn
|
||||
down twice.
|
||||
5. Visibility dispatch previously stopped at the first throwing observer. Both
|
||||
spatial and live-presentation fan-outs now drain every subscriber and every
|
||||
queued owner edge before aggregating failures, so one faulty sink cannot
|
||||
strand lights/particles or leak a stale edge into GUID reuse. Rebucket and
|
||||
withdrawal finish their canonical cell, projection, and presentation-index
|
||||
commits before rethrowing those observer aggregates.
|
||||
|
||||
The deterministic gate now validates the exact recall action ID and its
|
||||
animation-hook/PES route plus Hidden/remote-placement/UnHide ownership. Installed
|
||||
DAT colors, trail/cloud appearance, remote portal observation, and impact-origin
|
||||
presentation remain visual acceptance items; an in-memory conformance fixture
|
||||
cannot claim pixel appearance.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue