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

6.5 KiB
Raw Blame History

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).

  1. GameSky::UseTime @0x005075B0 (30 Hz) → CRegionDesc::GetSkySkyDesc::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 & 4LScape::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 @0x00513970InitPartArrayObject: 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 lawCPhysicsPart::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.