feat(physics): integrate live projectile runtime

Attach the retail projectile driver to canonical LiveEntityRecord ownership, sharing one PhysicsBody and full-cell identity with RemoteMotion regardless of creation order. Apply timestamp-gated state, vector, and position corrections in place, preserve active ordinary-body behavior when Missile clears, and keep renderer, effects, shadows, and spatial buckets synchronized across loaded/pending transitions.

Validate malformed packets before canonical timestamp mutation, validate adopted bodies from their current frame rather than stale CreateObject data, serialize late Setup resolution with streaming DAT reads, and preserve classification across clock anomalies. Retain shadow registrations through temporary leave-world residence while logical teardown remains generation-scoped.

Add 31 App lifecycle/adversarial tests plus Core shadow suspension coverage, and synchronize the retail pseudocode, architecture, milestones, roadmap, and durable physics memory.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-14 12:51:42 +02:00
parent f02b995e4f
commit a51ebc66e9
12 changed files with 2559 additions and 32 deletions

View file

@ -179,6 +179,8 @@ src/
ISelectionService.cs -> done
AcDream.App/ Layer 1 + Layer 4 wiring
Physics/
ProjectileController.cs -> live-record projectile orchestration/corrections
Rendering/
GameWindow.cs -> still owns too much runtime wiring
TerrainRenderer.cs -> done
@ -224,6 +226,28 @@ What exists and is active:
AlignPath, Setup-sphere transition sweeps, and collision response. It owns no
live entity, network, rendering, or App state; the App controller supplies
those boundaries.
- `ProjectileController` is that App boundary. It installs one body on the
canonical `LiveEntityRecord`, advances it on the update/render thread,
commits through `WorldEntity.SetPosition`, publishes the effect root,
synchronizes the existing shadow, and calls `RebucketLiveEntity` only when
the full cell changes. Pending/leave-world residence suspends the shadow's
cell rows while retaining its registration for exact re-entry. Timestamp-
gated State/Vector/Position/Movement packets correct or stop that same body;
every production MovementManager delegates full-cell reads/writes to the
incarnation-scoped live record, and projectile acquisition adopts its body
without replaying vectors already installed when that body was created,
regardless of component creation order. Accepted SetState flags reach that
canonical body before optional projectile acquisition, so absent/unsupported
DAT shape data cannot leave collision state stale; a non-finite local receipt
clock uses the controller's last finite game-clock value and cannot consume
first-time Missile classification. Shared-body acquisition validates the
adopted body's current frame/vectors, not obsolete CreateObject inputs.
Initial and late Setup resolution share GameWindow's `_datLock`, preserving
the single-reader `DatCollection` invariant while streaming is active.
Generic remote translation is suppressed while projectile integration
owns the frame;
delete, generation replacement, pickup/parent leave-world, and session reset
use `LiveEntityRuntime`'s normal lifecycle and never create a second GUID map.
- `BSPQuery` contains the partial retail-style BSP collision dispatcher used by
the transition path.
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,

View file

@ -516,6 +516,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- ~~**D.8 — Sound.**~~ **Superseded — shipped as Phase E.2** (`SoundTable`/`Sound` dat decode, OpenAL 16-voice engine, per-entity 3D positional audio via `AudioHookSink`). Entry kept here for history; see the shipped table.
- **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.
**Reference docs:** `docs/research/retail-ui/00-master-synthesis.md` + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.

View file

@ -471,7 +471,7 @@ 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 06
- **M2/M3 missile, effect, and portal presentation campaign (Steps 07
implemented 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
@ -515,9 +515,19 @@ include dungeons.
Setup-local sphere sweeps, exact missile target/ethereal exclusions,
self-shadow rejection, retail collision response, and full-cell landblock
rebasing. Missile adds PathClipped but never PerfectClip. AP-67, TS-2, TS-10,
TS-11, and TS-12 are retired; AP-83/AP-91 remain dormant. The remaining
steps integrate live projectile ownership/corrections and port Hidden/UnHide
portal presentation.
TS-11, and TS-12 are retired; AP-83/AP-91 remain dormant. Step 7 attaches
that primitive to the canonical `LiveEntityRecord`: arrows, bolts, and spell
missiles share one body and one renderer identity across prediction,
authoritative State/Vector/Position/Movement corrections, full-cell
rebucketing, pending-landblock residence, delete, session reset, and GUID
reuse. MovementManager presentation shares the same body and the canonical
live-record cell regardless of which component was created first;
pending/leave-world shadows retain their registration for exact re-entry even
if Missile classification changes while pending; static missile effect
roots follow every predicted frame, and materialized rehydration never uses a
stale spawn cell. No impact, damage, effect, or delete event is synthesized
client-side. The remaining steps port
Hidden/UnHide portal presentation and run final hardening/visual gates.
- **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

View file

@ -872,3 +872,105 @@ observable ordering without exposing a half-committed pose to modern sinks.
Boundary conformance covers positive and negative X and Y crossings.
- TS-2 is retired. AP-83 and AP-91 remain because ordinary missiles are
PathClipped and do not arm their dormant PerfectClip time-of-impact paths.
## 16. Step 7 live integration map (2026-07-14)
The App integration preserves the same retail object across every spatial and
network operation:
```text
after materialized CreateObject:
if final PhysicsState has never carried Missile: no projectile component
if Setup does not have the audited one ordinary sphere: diagnose, no guess
otherwise:
construct one PhysicsBody, or adopt the same body already owned by the
object's MovementManager
validate the inbound frame/vectors, canonicalize the outdoor cell,
retain valid friction in [0,1], and clamp elasticity to [0,0.1]
apply velocity through set_velocity (immediate 50-unit/second clamp)
bind it to LiveEntityRecord.ProjectileRuntime
when MovementManager and projectile classification arrive in either order:
bind MovementManager cell reads/writes to this exact live-record incarnation
share its PhysicsBody with ProjectileRuntime
install CreateObject velocity/omega when the ordinary body is constructed
if projectile classification arrives second, preserve that body's CURRENT
vectors/Active state, validate that current body rather than stale
CreateObject vectors, normalize its current cell frame, and translate
its clock to the App game clock without replaying retained vectors;
a safely ignored malformed CreateObject vector cannot veto a later
finite body, while a non-finite current body cannot enter simulation
each frame:
for each canonical live record with a projectile component:
skip/clear Active through update_object when parented, cell-less,
Frozen, Static, or out of world
if the object has a PartArray, clear Active beyond the retail
96-world-unit player-distance boundary; reactivation stamps now
run ProjectilePhysicsStepper with local WorldEntity.Id for self-shadow
exclusion and target id zero
if this exact record generation still owns this exact component:
WorldEntity.SetPosition(resolved body frame)
update orientation and full parent cell
RebucketLiveEntity only when the full cell changed
move the existing collision shadow to the resolved frame
publish the new root to the effect-pose owner
if Missile is now clear and MovementManager exists: that ordinary owner
advances the shared body instead
if Missile is clear with no MovementManager: keep advancing the active
retained CPhysicsObj; collision reads final state, so missile_ignore
and Missile-derived PathClipped are disabled automatically
fresh State/Vector/Position/Movement packet:
reject structurally invalid Position/Vector values before advancing the
snapshot or timestamp, even if Missile classification has not arrived
first pass the existing per-channel retail timestamp gate
State: update the canonical body first, even when missile acquisition is
impossible because Setup or sphere data is unavailable; the accepted
SetState collision flags must never remain only on the live record.
A non-finite local receipt clock does not discard first classification:
bind against the controller's last finite game clock (the clock epoch
is zero before the first frame), while keeping the wire state authoritative.
Update this body without newly activating it; removing Missile
disables missile classification but retains the component/body on this
object. MovementManager becomes the update owner when present;
otherwise the retained active CPhysicsObj continues ordinary physics.
A later Missile state reuses that same current body and cell;
align the projectile clock at that ownership transfer, preserve the
existing Active bit, and never activate a body that was already inactive
Vector: set_velocity + omega on this body; clamp immediately and stamp the
receipt clock only when the packet reactivates an inactive object; if
Missile is currently clear and MovementManager exists, its ordinary
Airborne/LeaveGround funnel owns those writes on the shared body
Position: hard-correct world frame plus canonical wire cell-local frame,
shadow, spatial bucket, and effect pose; do not overwrite velocity
Movement: unpack through the normal MovementManager/animation funnel while
sharing this exact PhysicsBody and the live record's canonical cell;
projectile stepping suppresses the generic remote translation tick,
so there is still only one integrator and one spatial authority
leave_world (pickup/parent):
clear InWorld and Active, withdraw collision shadows but retain their exact
registration payload and the component with the incarnation
fresh Position re-entry restores the same shadows, stamps the current
physics clock, and never consumes the time spent outside the world
loaded/pending transition:
the rebucket result is checked in the same physics quantum
pending clears InWorld/Active and suspends shadows without teardown
hydration restores body/shadow membership even if Missile was cleared while
pending; projectile integration itself remains disabled
landblock rehydration keys materialized movers by LiveEntityRecord.FullCellId,
never by the retained create-time Snapshot.Position
DeleteObject / generation replacement / session reset:
normal LiveEntityRuntime teardown destroys the owning record; the
projectile controller has no second GUID dictionary to clear
```
`ProjectileController` runs before animation advancement in the render/update
phase and publishes its current root directly; an animated projectile then
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.