feat(vfx): bind effects to live animated poses

This commit is contained in:
Erik 2026-07-14 10:56:01 +02:00
parent 96ddfdf175
commit 542dcfc384
41 changed files with 3246 additions and 741 deletions

View file

@ -740,10 +740,84 @@ Repository links:
2. Logical entity creation/destruction is separate from spatial rebucketing.
3. Server Position/Vector/State/Delete packets remain authoritative.
4. No client-side impact, explosion, damage, or deletion is synthesized.
5. Per-object frame ordering preserves retail: animation/timed object hooks,
movement commit and child updates, particle tick, then PhysicsScript tick.
A script-created emitter begins particle simulation on the following tick.
5. Presentation frame ordering is fixed: advance animation/root motion,
publish indexed parts, compose equipped children, drain animation hooks,
tick owner PhysicsScripts, refresh attached emitters/lights, then advance
particle simulation and draw.
Retail stages animation hooks inside `CSequence::update`, then executes them
from `CPhysicsObj::process_hooks` before the new root is committed. The same
update subsequently calls `CPhysicsObj::set_frame` (`0x00514090`), which runs
`CPartArray::SetFrame` and `UpdateChildrenInternal`; that relocates the newly
parented emitter/held child to the final pose before `ParticleManager` advances
or anything draws. acdream's explicit publish-then-drain barrier preserves that
observable ordering without exposing a half-committed pose to modern sinks.
6. Hidden suppresses mesh/collision/interaction but does not destroy the effect
owner.
7. DAT assets determine models, colors, timing, and attachment behavior.
8. Every ported branch receives a conformance test and a named-retail citation.
## 14. Step 5 implementation map (2026-07-14)
- `EntityEffectPoseRegistry` exposes current root and indexed part-local poses
through `IEntityEffectPoseSource`. The indexed channel contains retail's
rigid `CPhysicsPart` frames, not drawable mesh scale: `UpdateParts
0x005190F0` multiplies only the animation origin by object scale, while
`SetScaleInternal 0x00518A00` stores Setup `DefaultScale` separately on
`gfxobj_scale`. Animated entities publish those rigid frames beside their
visually scaled `MeshRef` composition; equipped children then publish
`holdingFrame * parentPart * parentRoot` plus the child's own indexed part
locals. Appearance replacement republishes immediately.
- `AnimationHookFrameQueue` captures hooks during sequence advance and drains
only after equipped-child composition. `AnimationDone` and `UseTime` remain
paired with the sequencer even if its render pose vanished during teardown.
- `ParticleHookSink` binds each live handle to local owner, exact part index
(including root `-1`), complete offset Frame, logical emitter ID, and render
pass. It applies the offset origin through `partLocal * rootWorld`; the
retained offset quaternion remains intentionally unused per
`Particle::Init 0x0051C930`.
- Normal nonzero logical IDs replace, blocking nonzero IDs suppress while the
old emitter remains live, and zero IDs create anonymously. Natural emitter
retirement releases the logical ID. `StopParticleEmitter` retains the ID
until that retirement, while destroy removes it immediately. Missing
`ParticleEmitterInfo` DAT
records log owner + DID and create nothing; the former synthesized generic
cloud is removed.
- World-released particles retain their emission origin while future emissions
follow the refreshed anchor. Parent-local particles consume the refreshed
anchor for existing particles, matching `Particle::Update 0x0051C290` and
`ParticleEmitter::UpdateParticles 0x0051D180`.
Loaded-to-pending spatial transitions skip particle update/draw under retail's
cell-less `update_object 0x00515D10` gate while retaining the emitter,
particles, original absolute creation times, and logical ID. Authoritative pose
correction may still refresh the anchor. Re-entry evaluates elapsed particle
age/emitter duration once and emits no absent-time backlog; timestamps are not
shifted to freeze the effect.
- Setup lights retain their complete local `LIGHTINFO` frame.
`LiveEntityLightController` owns live registration/re-entry and every frame
`LightingHookSink` composes it through the current owner root/cell. Held
objects use the child root published after `CPhysicsObj::UpdateChild
0x00512D50`; top-level roots follow their `WorldEntity`. PhysicsState's
Lighting bit and `SetLightHook` both drive the same create/destroy state,
matching `CPhysicsObj::set_state 0x00514DD0` and `set_lights 0x0050FCF0`.
This retires AP-67 and TS-10.
Light presentation follows `LiveEntityRuntime`'s final projection edge, so a
loaded-to-loaded rebucket does not replay registration and a pending owner
contributes no stale light. Attached lights wait for the composed-child-pose
barrier before their first registration.
- Setup-part indices are retained separately from drawable `MeshRefs`; a
missing middle GfxObj cannot shift later hook/holding indices. Nested held
objects update parent-before-child and compose through the parent's already
published child root. The buffers are retained by the attachment owner and
reused each frame.
- Logical teardown removes queues, emitters, lights, and poses exactly once;
rebucketing does not. Pose loss withdraws an attached projection and its
descendants while retaining accepted relations for valid later recovery.
A post-publish parent-first recovery drain realizes the complete descendant
chain, preventing an A→B→C graph from stranding C after B returns. The drain
is edge-triggered by object/appearance/pose publication, never a per-frame
retry loop for a permanently absent Setup or holding part.
Session reset clears captured hooks and live poses
through normal owner teardown while retaining DAT-static/synthetic poses.
TS-11 and TS-12 are retired with the emitter-binding and
live-part mechanisms.