Commit graph

2 commits

Author SHA1 Message Date
Erik
845d70248c weather(phase-6a): port retail PhysicsScript runtime
The central runtime for every client-visible scripted effect server
triggers via PlayScript (opcode 0xF754) — spell casts, emote
gestures, combat flinches, AND lightning flashes during storms.
Previously acdream parsed PhysicsScript from the dat (via DRW) but
had no runner; the PlayScript stub in ParticleSystem.cs was a
no-op.

Decompile provenance (`docs/research/2026-04-23-physicsscript.md`,
`docs/research/2026-04-23-lightning-real.md`):

  FUN_0051bed0 — play_script(scriptId) public API — resolves the
                 dat id, queues into the owner's ScriptManager list.
  FUN_0051be40 — ScriptManager::Start — alloc 16-byte node
                 {startTime, script*, next}.
  FUN_0051bf20 — advance one hook, schedule next fire by next
                 hook's StartTime.
  FUN_0051bfb0 — per-frame tick: while head.NextHookAbsTime ≤
                 globalClock, fire via vtable dispatch.

Port choices:
  - Flat List<ActiveScript> vs retail linked list — iteration is
    simpler, N is small.
  - Scripts keyed by (scriptId, entityId) — replay replaces instead
    of stacking, matches retail's "play_script on the same obj
    doesn't double-schedule".
  - Anchor world pos cached at Play() time — good enough for
    short-lived effects (lightning, spell casts). Callers that
    need fresh positions for long emote animations can Play()
    again each frame (idempotent).
  - Constructor takes Func<uint, PhysicsScript?> resolver so tests
    don't need DatCollection; production uses the DatCollection
    overload that wraps Get<PhysicsScript> with null-on-fail.
  - CallPESHook recurses Play() with Pause baked into the
    sub-script's StartTimeAbs. Matches retail semantics where
    nested scripts fire on the NEXT tick (list iteration order).

Diag: ACDREAM_DUMP_PLAYSCRIPT=1 logs every Play() and every fire as
[pes] lines. Use this to identify the actual script IDs your ACE
server is sending so we can confirm the lightning pipeline when the
server sends a strike.

Test coverage (9 new tests, all passing):
  - unknown script returns false, zero id silent-ignore
  - hooks fire in order at their scheduled times
  - entityId + anchor pass through to sink
  - replay same (scriptId, entityId) replaces, doesn't stack
  - different entities run independently
  - StopAllForEntity cancels that entity's scripts only
  - CallPES nested spawn semantics (fires next tick)
  - CallPES with Pause delays correctly

No GameWindow wiring yet — Phase 6b handles the 0xF754 packet
handler and Phase 6c plugs the runner into the frame loop.

Build + 742 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:20:39 +02:00
Erik
d3165f99d7 feat(vfx): Phase E.3 particle system + hook wiring + registry
Full runtime particle pipeline consuming Phase E.1's animation hooks.
13 motion integrators, per-emitter particle pools with overwrite-oldest
eviction, colour / scale / alpha interpolation over life, and a
ParticleHookSink routing CreateParticle / DestroyParticle / StopParticle /
CreateBlockingParticle hooks from the animation-hook router.

Core layer:
- ParticleSystem: handle-based emitter pool, per-tick emission
  accumulator (retail Birthrate = time-between-spawns → our emit rate
  via 1/B), 13 integrators covering the full ParticleType enum:
  Still, LocalVelocity, GlobalVelocity, 7 Parabolic variants (all
  apply Gravity * dt to velocity), Swarm (orbital drift),
  Explode (outward from anchor), Implode (inward to anchor, dies at
  convergence).
- EmitterDescRegistry: id-keyed EmitterDesc cache with fallback-to-
  default for unknown ids. Replaces the dat-loaded path until
  Chorizite.DatReaderWriter exposes ParticleEmitterInfo (v2.1.7 does
  not; upgraded from 2.1.4 anyway for future types).
- ParticleHookSink: wires the full hook family:
  - CreateParticleHook → SpawnEmitterById at entity pose + hook offset
  - CreateBlockingParticleHook → marker only (blocking semantics live
    in the sequencer not here)
  - DestroyParticleHook → StopEmitter(handle, fadeOut=false)
  - StopParticleHook   → StopEmitter(handle, fadeOut=true)
  - (Default/CallPES deferred until PhysicsScript dat is loadable)

GameWindow integration:
- ParticleSystem created eagerly (no driver dep), sink registered with
  hook router, Tick advanced per OnRender frame after animation tick so
  hooks fired this frame get integrated.

Tests (11 new): spawn-handle, emit-over-time steady state, lifetime
death curve, LocalVelocity movement, Parabolic gravity arc, Explode
outward trajectory, StopEmitter instant kill vs fadeOut, MaxParticles
cap enforcement, registry default fallback, registry custom
registration.

Upgraded Chorizite.DatReaderWriter 2.1.4 → 2.1.7 across Core + Cli.

Build green, 508 tests pass (up from 497).

Ref: r04 §2 (CParticleManager), §3 (13 integrators), §6 (PhysicsScript).
Renderer (instanced billboarded quads in translucent pass) ships next
commit; this one covers the data / logic / wiring layer in full.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:48:17 +02:00