# 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 450–700 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 7–8, sprites `0x01001A61..63` = single ±137.5 m quads whose 64×64 additive textures are soft glow blobs (row/col profiled — no banding; peak RGB 36–71/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.03–0.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.