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>
This commit is contained in:
parent
86e2a4dc90
commit
3361641655
2 changed files with 210 additions and 0 deletions
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -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[-]*
|
||||
|
|
|
|||
187
docs/plans/2026-04-30-sky-pes-port.md
Normal file
187
docs/plans/2026-04-30-sky-pes-port.md
Normal file
|
|
@ -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<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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue