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>
This commit is contained in:
Erik 2026-08-23 15:28:59 +02:00
parent 6f47740af0
commit 18fce7bb5a
13 changed files with 906 additions and 113 deletions

View file

@ -16095,7 +16095,7 @@ additional flash-shader work).
**Description:** Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that `SkyObject.PesObjectId` drives sky-render flash particles: `SkyDesc::GetSky` copies the field into `CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`, `GameSky::MakeObject`, and `GameSky::UseTime` never read it.
**Root cause / status:** Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside `GameSky`; do not reintroduce per-SkyObject PES playback without new decompile evidence.
**Root cause / status:** CORRECTED 2026-08-23 — the "must stay disabled" ban is lifted by new decompile evidence: retail DOES play the sky carriers' PES, via the Setup `DefaultScript``CPhysicsObj::makeObject` default-script path (never via the `pes_id` column, which remains dead — that half of the April finding stands). Sky default-script playback is production as of the C.1.5c port (`SkyPesFrameController`, research `2026-08-23-sky-default-script-port.md`). The lightning-flash carriers (`0x02000BA6``0x33000453`, windows 0.030.19 / 0.400.50 / 0.910.98 on Rainy groups) now fire through it; whether the flash PRESENTATION matches retail (sky-wide crossfade vs a sprite) still needs its own storm-window gate.
**Files:**
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky/weather mesh draw, material state, pre/post split
@ -16142,11 +16142,26 @@ additional flash-shader work).
## #28 — Aurora ("northern lights") effect not rendered
**Status:** OPEN
**Status:** FIX LANDED 2026-08-23 — awaiting the connected visual gate
**Severity:** LOW (aesthetic feature-parity)
**Filed:** 2026-04-26
**Component:** sky / vfx
**2026-08-23 resolution (the C.1.5c mechanism, found):** the sky PES ids ride
the carrier Setups' own `DefaultScript` (byte-equal to the dead `pes_id`
column) and retail plays them through the ordinary default-script machinery —
`GameSky::MakeObject @0x00506EE0``CPhysicsObj::makeObject @0x00513970`
(`state |= 0x80000`) → `CPhysicsObj::animate_static_object @0x00513DF0`. The
aurora carrier (`0x02000714` → PES `0x330007DB`) is present in ALL 20 day
groups around the clock; visibility is purely "faint additive glows over a
dark sky", pulsing on the emitters' 6.7/15/55-min rebirth cycles. The April
"colored wash" experiment was run inside a Rainy day group's lightning window
— the flash/thunder PES at the camera anchor, not the aurora, made the wash.
`SkyPesFrameController` is now the production owner (no env flag), with
retail's slot-identity persistence and the `calc_draw_frame @0x0050DFA0`
facing law in the particle renderer. Full chain:
`docs/research/2026-08-23-sky-default-script-port.md`; register AD-112.
**Description:** Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect.
**Root cause / status:** Open again. The prior root cause was wrong: `CelestialPosition.pes_id` exists in the retail header and is populated by `SkyDesc::GetSky`, but named retail `GameSky` code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind `ACDREAM_ENABLE_SKY_PES=1`.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,119 @@
# 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.