acdream/docs/research/2026-08-23-sky-default-script-port.md
Erik 18fce7bb5a fix #28: port retail's sky default-script playback (aurora) and the particle facing law
The aurora was never missing data — it was a missing mechanism plus a
misread. New decompile evidence closes the April-2026 contradiction:
retail plays the sky carriers' PES through the Setup's own DefaultScript
(GameSky::MakeObject @0x00506EE0 -> CPhysicsObj::makeObject @0x00513970
sets state|=0x80000; animate_static_object @0x00513DF0 ticks
ScriptManager + ParticleManager). The pes_id column stays dead — that
half of the April finding stands; the ids are byte-equal mirrors.

- SkyPesFrameController is now the production owner (ACDREAM_ENABLE_SKY_PES
  deleted): script ids resolve from the Setup DefaultScript
  (SkyObjectData.DefaultScriptId; the pes_id column is a one-time-logged
  cross-check), slots persist by (index, gfx id, properties) per
  CreateDeletePhysicsObjects @0x005073C0 — a day-group swap keeping the
  carrier no longer restarts its emitters — and stale slots stop before
  replacements claim the slot-derived owner id.
- RetailParticleFacing ports calc_draw_frame @0x0050DFA0: degrade mode 2
  faces the viewer roll-free (set_vector_heading) instead of the camera
  plane; modes 3/4/5 spin the authored frame around one local axis
  (rotate_around_axis_to_vector) — Dereth authors 54 mode-5 emitters that
  previously got no facing at all; 1,583 mode-2 emitters get the exact
  law; authored/mode-1 paths are unchanged.
- The 2026-08-23 'whole-sky tint' was the Rainy-group lightning/thunder
  PES playing at the debug anchor inside their 0.03-0.19 window, not the
  aurora: the aurora is nine faint viewer-facing glows pulsing on
  6.7/15/55-minute rebirth cycles, in every day group, all day.

Research: docs/research/2026-08-23-sky-default-script-port.md.
Register: AD-112 filed (camera-anchored synthetic owners vs sky-cell
physics objects). ISSUES #2 corrected (the playback ban is lifted by the
new evidence); #28 fix landed pending the connected night gate.
Tests: RetailParticleFacingTests (16), SkyPesFrameControllerTests (6);
hermetic suites App 6,076/0, Core 4,905/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 15:28:59 +02:00

119 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sky default-script (aurora/lightning/thunder) — the retail mechanism, proven
**Date:** 2026-08-23 · **Issues:** #28 (aurora), #2 (lightning, partial), #29 (clouds — not addressed here) · **Phase:** C.1.5c
## The April contradiction, resolved
Two prior research passes (`2026-04-23-sky-pes-wiring.md`, `2026-04-28-pes-pseudocode.md`)
correctly proved `GameSky` never **reads** `CelestialPosition.pes_id`
(`SkyDesc::GetSky @0x00501EC0` writes it at 0x00501FC9; no reader exists), and
issue #2 therefore banned per-SkyObject PES playback "without new decompile
evidence". This document is that evidence. Both were right and both missed the
actual route:
**The sky PES ids ride the sky Setups' own `DefaultScript`, and retail plays
them through the ordinary object default-script machinery — the `pes_id`
column is a dead mirror of the same ids.**
Verified in the installed Dereth DAT: Setup `0x02000714` (aurora carrier,
parts `0x010001EC`) has `DefaultScript = 0x330007DB` — byte-equal to its
SkyObject's `PesObjectId`. Same holds for `0x02000589→0x3300042C` (thunder
ping-pong), `0x02000588→0x33000428` (thunder variant), `0x02000BA6→0x33000453`
(lightning flash).
## The retail chain (named decomp, every link read this session)
1. `GameSky::UseTime @0x005075B0` (30 Hz) → `CRegionDesc::GetSky`
`SkyDesc::GetSky @0x00501EC0` rebuilds the `CelestialPosition` list.
Per-object visibility: **if `begin_time == end_time` the object is always
included**; otherwise `begin <= t <= end` gates the gfx id (out-of-window →
`INVALID_DID`). The aurora object has `begin=end=0.00` in **all 20 day
groups** → always present, all day.
2. `GameSky::CreateDeletePhysicsObjects @0x005073C0`: an existing sky object
is **kept** when its current DataID equals the wanted gfx id AND the
properties word is unchanged AND (`props & 4``LScape::weather_enabled`
did not flip). Only a mismatch destroys/recreates. ⇒ the aurora object —
same id in every group — **persists across day-group changes**, and its
emitters keep their particle population.
3. `GameSky::MakeObject @0x00506EE0`: `props & 4` objects are only created
when `LScape::weather_enabled != 0`; `props & 1` selects `after_sky_cell`
(post-scene) vs `before_sky_cell`. Creation is
`CPhysicsObj::makeObject(gfx_id, 0, 0)`.
4. `CPhysicsObj::makeObject @0x00513970``InitPartArrayObject`: a Setup
with `default_script_id != 0` sets `state |= 0x80000` and registers via
`CPhysics::AddStaticAnimatingObject @0x00509AF0`.
5. `CPhysicsObj::animate_static_object @0x00513DF0` (per tick): state 0x80000
`ScriptManager::UpdateScripts` (starts/advances the default script — the
PES) and updates the object's `ParticleManager`. This is the ~150/min
`CallPES` churn the 2026-04-30 live trace counted (the thunder PES
`0x3300042C` ping-pongs via `CallPES` chains).
## The particle laws confirmed against our port
- `ParticleEmitterInfo::GetRandomOffset @0x005174A0`: random vector, **minus
its projection onto `offset_dir`**, normalized, × rand[min,max] — a disk
PERPENDICULAR to the dir (a shell when dir is zero). Our
`ParticleSystem.RandomOffset` is byte-faithful. The aurora's 450700 m
"Z-dir" offsets are therefore a horizontal RING around the sky-object
origin, not a column above it.
- `Particle::Update @0x0051C290` writes **only the part origin** per type
formula (our `ComputePosition` matches, incl. Swarm cos/sin); only the
GR/LR parabolic variants rotate.
- `ParticleEmitter::SetInfo @0x0051CE90`: parts are ordinary
`CPhysicsPart::makePhysicsPart(hw_gfxobj_id)`; there is no special "2D
particle" draw. `CPhysicsPart::Draw @0x0050D7A0` always feeds
`DrawMesh(gfxobj[deg_level], &draw_pos)`.
- **The facing law** — `CPhysicsPart::calc_draw_frame @0x0050DFA0`, driven by
the FIRST degrade entry's mode (`GfxObjDegradeInfo::get_degrade
@0x0051E4B0`; `viewer_heading` = normalized part→viewer from
`UpdateViewerDistance @0x0050E030`):
```
draw = pos
switch deg_mode:
2: Frame::set_vector_heading(draw, viewer_heading) // face viewer, roll-free
3/4/5: Frame::rotate_around_axis_to_vector(draw, X/Y/Z, vh) // cylindrical, one free axis
else: authored orientation (mode 1, mode 0, out of range)
```
Our renderer camera-plane-aligned every "billboard" particle
(`cameraRight`/`cameraUp`); retail faces each part toward the viewer
per-part and honors constrained modes. `CPhysicsPart::Always2D @0x0050D8A0`
(mode != 1) is only consulted for **cell membership**
(`CLandCell::add_all_outside_cells @0x00533360`), not drawing.
- The aurora emitters (`0x32000455/56/57`): BirthratePerSec 10, max 3,
initial 3, lifespans 3300/900/400 s, StartTrans 0.8 → FinalTrans 1.0
(≤20 % opacity fading to nothing), StartScale 78, sprites
`0x01001A61..63` = single ±137.5 m quads whose 64×64 additive textures are
soft glow blobs (row/col profiled — no banding; peak RGB 3671/255). First
degrade mode = **2** on all three. The visible aurora is therefore nine
huge, faint, viewer-facing glows in a slow Swarm drift — a pulse that
re-brightens when a cohort is reborn (~6.7/15/55 min cycles), not a steady
fixture.
## Why the 2026-08-23 experiment looked like "whole-sky tint"
The debug controller (`ACDREAM_ENABLE_SKY_PES=1`) ran under day group 16
(Rainy) at t=0.125 — inside the lightning window (0.030.19) — so the
**lightning-flash and thunder PES** played at the camera anchor alongside the
aurora. A flash sprite at the anchor IS a full-screen additive wash. The
aurora itself was drowned under a faithful-but-wrong-conditions storm.
## Port deltas (this change)
1. Sky default-script playback becomes **production** (no env flag): every
visible sky object whose Setup carries a `DefaultScript` plays it through
`PhysicsScriptRunner`, anchored at the camera (retail sky-cell space is
viewer-centered), pass-routed by `props & 1`.
2. **Persistence contract**: script/emitter state is keyed by
(gfx id, properties) per `CreateDeletePhysicsObjects` — a day-group flip
that keeps the same carrier Setup must NOT restart its emitters.
3. Weather gating: `props & 4` objects follow the weather-enabled state
(acdream's weather system), matching `MakeObject`'s guard.
4. `calc_draw_frame` facing law in the particle renderer: per-sprite degrade
mode picks face-viewer (2), axis-constrained (3/4/5), or authored (else)
orientation — replacing the blanket camera-plane alignment.
Script ids resolve from the **Setup's DefaultScript** (the retail source);
the `PesObjectId` column is only a cross-check.