acdream/docs/plans/2026-04-30-sky-pes-port.md
Erik 3361641655 docs(plans): #36 sky-PES dispatch port plan + .gitignore for retail-debugger scratch
Plan doc `docs/plans/2026-04-30-sky-pes-port.md` captures the full
porting plan for #36 (sky-PES dispatch chain). Three phases:
  M.1 — decomp dive (no code yet) for CallPESHook::Execute,
        CPhysicsObj::CallPES, CreateParticleHook::Execute,
        GameSky::CreateDeletePhysicsObjects, and the dynamic-spawn
        trigger (region/weather/time-of-day handler).
  M.2 — optional cdb verification with detailed args (this pointer +
        pes_id + caller stack walk).
  M.3 — implementation: persistent emitter creation at cell load,
        dynamic spawn on transitions, PES script-timeline driver,
        particle-system render wire-up.
  M.4 — live side-by-side verification.

Acceptance: aurora visible at right moments, clouds dense like retail,
storm flashes during Rainy storm windows, PES dispatch rate matches
retail's ~150/min.

.gitignore extended to suppress per-session retail-debugger scratch
files (cdb scripts, launch logs, analysis ps1 helpers). The
canonical workflow lives in CLAUDE.md "Retail debugger toolchain";
session-specific traces should not pollute the repo.

Closes #36 plan stage. Implementation work begins next session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:00:46 +02:00

187 lines
7.8 KiB
Markdown

# Plan — Sky-PES dispatch port (Issue #36)
**Filed:** 2026-04-30 from a live cdb trace of retail acclient.exe.
**Owner:** next session.
**Closes:** #28 (aurora), #29 (cloud density), partially #2 (lightning).
## What we know (from the live trace, 24,576 GameSky::Draw frames)
Retail's sky-PES dispatch chain runs as follows. All counts are from
the cdb trace summarized in `memory/project_retail_debugger.md`:
```
GameSky::Draw = 24,576 (60Hz render rate)
GameSky::UseTime = 12,288 (30Hz, MinQuantum gate)
GameSky::CreateDeletePhysicsObjects = 12,288 (30Hz)
CPhysicsObj::CallPES = 372 (~150/min)
CallPESHook::Execute = 372 (1:1 with CallPES)
CreateParticleHook::Execute = 62 (15 initial + 47 burst)
CPhysicsObj::create_particle_emitter = 62 (matches CreateParticleHook)
```
Three concrete findings:
1. **Persistent particle emitters on celestial / sky objects.** 15 are
created at cell load. More are spawned dynamically on region /
weather / time-of-day transitions (the trace caught a +47 burst on
one such transition).
2. **Periodic PES dispatch drives existing emitters.**
`CallPESHook::Execute` runs script-scheduled actions which call
`CPhysicsObj::CallPES` 1:1. ~150 dispatches/min on average.
3. **Earlier research said "GameSky doesn't read pes_id" — true but
misleading.** GameSky doesn't read it directly; the script-hook
system does. `CelestialPosition.pes_id` (struct offset +0x004) is
populated by `SkyDesc::GetSky` and consumed downstream by
`CallPESHook` / `CreateParticleHook` invocations scheduled from
region/weather handlers.
## Decomp anchors
All addresses verified live against `refs/acclient.pdb`:
| Function | Address | Role |
|---|---|---|
| `CallPESHook::Execute` | `0x00526e20` | Script-hook action that fires CallPES |
| `CreateParticleHook::Execute` | `0x00526ec0` | Particle-creation hook |
| `CPhysicsObj::CallPES` | `0x00511af0` | Top-level PES dispatch |
| `CPhysicsObj::CallPESInternal` | `0x00511ac0` | Inner dispatch (never fires alone in trace — likely inlined) |
| `CPhysicsObj::create_particle_emitter` | `0x0050f360` | Creates a new emitter |
| `CPhysicsObj::create_blocking_particle_emitter` | `0x0050f3b0` | Creates a blocking emitter |
| `CPhysicsObj::stop_particle_emitter` | `0x0050f420` | |
| `CPhysicsObj::destroy_particle_emitter` | `0x0050f400` | |
| `CPhysicsObj::ShouldDrawParticles` | `0x0050fe60` | |
| `CPhysicsObj::makeParticleObject` | `0x00512640` | |
| `CPartArray::CreateParticle` | `0x005194f0` | |
| `CSetup::makeParticleSetup` | `0x005201f0` | |
| `LongNIHash<ParticleEmitter>::add` | `0x005198c0` | Emitter registry insertion |
| `GameSky::CreateDeletePhysicsObjects` | `0x005073c0` | Already partially decoded; entry point for sky-object lifecycle |
| `GameSky::UseTime` | `0x005075b0` | Per-frame sky update (30Hz) |
| `GameSky::Draw` | `0x00506ff0` | Sky render (60Hz) |
| `SkyDesc::GetSky` | `0x00501ec0` | Populates `CelestialPosition.pes_id` |
`CelestialPosition` struct layout:
```
+0x000 gfx_id : 4 bytes
+0x004 pes_id : 4 bytes ← THIS is what gets PES-driven
+0x008 heading : float
+0x00c rotation : float
+0x010 tex_velocity : Vector3
+0x01c transparent : float
+0x020 luminosity : float
+0x024 max_bright : float
+0x028 properties : 4 bytes
```
## Phase plan
### Phase M.1 — Decomp dive (no code changes)
Read these functions in order. Save findings to
`docs/research/2026-04-30-sky-pes-pseudocode.md`:
1. **`CallPESHook::Execute`** at `0x00526e20`. What state does it
read? What does it call? Answer: probably "look up the target
CPhysicsObj by some ID, call CallPES on it." Confirm.
2. **`CPhysicsObj::CallPES`** at `0x00511af0`. What does it do?
Probably: "look up or load the PES file referenced by `pes_id`,
start running its script timeline." Find the script-evaluation
loop and where it dispatches `CallPESHook` events.
3. **`CreateParticleHook::Execute`** at `0x00526ec0`. When this hook
fires, what does it create? What CPhysicsObj does it attach the
emitter to? Probably calls `CPhysicsObj::create_particle_emitter`.
4. **`CPhysicsObj::create_particle_emitter`** at `0x0050f360`. What
does it instantiate? What goes into the `LongNIHash<ParticleEmitter>`?
5. **`GameSky::CreateDeletePhysicsObjects`** at `0x005073c0`. The
prior research said this doesn't read pes_id. Confirm — but ALSO
check: does it set up the script-hook timeline somewhere? Or does
that happen in a separate caller?
6. **The dynamic-emitter-spawn trigger.** The trace caught a +47
burst — find what fires CreateParticleHook on region / weather /
time-of-day transitions. Likely candidates:
- `LScape` weather handler
- `CDayCycle` / `CWorldFog` region handler
- Cell-load or cell-cross handler
### Phase M.2 — Verify with detailed cdb trace (one focused session)
After M.1 reveals the wiring, attach cdb to retail and capture:
- `this` pointer + `pes_id` arg on every `CPhysicsObj::CallPES`
- GfxObj being attached on every `create_particle_emitter`
- Stack walk at `CallPESHook::Execute` to confirm the caller chain
- Watch for the dynamic +N burst — what global state changed at
that frame?
The data should match the M.1 decomp predictions. If it diverges,
the decomp interpretation needs another pass.
### Phase M.3 — Implementation (acdream port)
1. **Persistent emitter creation at cell load.** When a sky-bearing
landblock loads, walk SkyDesc / CelestialPosition entries; for
each entry with non-zero `pes_id`, instantiate a ParticleEmitter
on the corresponding sky CPhysicsObj.
2. **Dynamic emitter spawn on transitions.** Hook into our region /
weather / day-cycle change events; replicate retail's dispatch.
3. **PES script-timeline driver.** Port the scheduler that fires
`CallPESHook` events at script-defined moments. May reuse
`references/holtburger` if there's a Rust port. If not, port from
decomp directly.
4. **Particle-system rendering wire-up.** acdream already has a
particle system (R3 era). Verify it can accept emitter spawns
from this path. If so, just wire. If not, identify the gap.
5. **Surface 0x08000023 / cloud GfxObjs.** Once dynamic emitters spawn,
#29's "clouds too thin" should resolve naturally.
### Phase M.4 — Live verification
1. Retail + acdream side-by-side. Aurora moment (Rainy DayGroup,
dusk/dawn). Compare visual.
2. Cloudy moment — clouds should look as dense as retail.
3. Storm moment — lightning flashes (covers part of #2).
4. Run another cdb trace; counts should match retail's counts within
~10%.
## Acceptance
- Aurora visible in acdream at the same in-game moments retail shows it.
- Cloud sheets look as dense / purple as retail.
- Storm flash visible during Rainy storm windows (part of #2).
- New cdb trace shows similar PES dispatch rate (~150/min) and similar
emitter spawn pattern (initial population + transition bursts).
- Closes #28, #29. Updates #2 with the storm-flash story.
## What this doesn't fix
- **#4 horizon-glow** is a separate issue (fog parameters, not particles).
Tackle that in a different session — different code path, different
cdb trace.
- **Lightning timing / thunder** (the audio half of #2) is separate;
needs the audio system wired.
## How to start
The next session, having read CLAUDE.md (auto-loaded) and
`memory/project_retail_debugger.md` (auto-loaded), should:
1. Read this plan top to bottom (~5 minutes).
2. Begin Phase M.1 decomp dive — no code yet, just understand the
wiring. Save findings to a research doc.
3. After M.1 lands, decide whether M.2 (verify with cdb) is needed
before M.3 (implement). Often the decomp alone is enough; M.2 is
for resolving ambiguity.
The cdb tooling is ready (CLAUDE.md "Retail debugger toolchain").
The user can launch retail with `C:\Turbine\Asheron's Call\acclient.exe`
on demand.