From 3361641655864fbdcf2c696f6d4710bb748575b1 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Apr 2026 23:00:46 +0200 Subject: [PATCH] docs(plans): #36 sky-PES dispatch port plan + .gitignore for retail-debugger scratch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 23 ++++ docs/plans/2026-04-30-sky-pes-port.md | 187 ++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 docs/plans/2026-04-30-sky-pes-port.md diff --git a/.gitignore b/.gitignore index acdabd2..af968b2 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,26 @@ tmp/ # Git worktrees for isolated feature work .worktrees/ + +# Per-session retail-debugger scratch — cdb scripts, logs, analysis helpers. +# The committed reference workflow lives in CLAUDE.md "Retail debugger toolchain"; +# session-specific traces should not pollute the repo. +*.cdb +launch_*.log +launch_*.err +launch_*.ps1 +launch[0-9]*.log +analyze_*.ps1 +peek_*.ps1 +run_cdb_*.ps1 +find_cdb.ps1 +find_acclient.ps1 +kill_cdb.ps1 +append_memory.ps1 +sky_*.log +smoke_test* +steep_roof_trace* +substep_trace* +sg_built.txt +# Stray bash-mangled path artifacts from PowerShell-via-bash escaping +C[€-￿]* diff --git a/docs/plans/2026-04-30-sky-pes-port.md b/docs/plans/2026-04-30-sky-pes-port.md new file mode 100644 index 0000000..9eba80f --- /dev/null +++ b/docs/plans/2026-04-30-sky-pes-port.md @@ -0,0 +1,187 @@ +# 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::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`? + +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.