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

@ -38,6 +38,22 @@ public sealed class SkyObjectData
public uint PesObjectId;
public uint Properties;
/// <summary>
/// The carrier Setup's own <c>DefaultScript</c> PES id (zero when the
/// gfx id is not a Setup or the Setup has none). This — not
/// <see cref="PesObjectId"/> — is what retail plays: a Setup with a
/// default script marks its physics object <c>state |= 0x80000</c>
/// (<c>CPhysicsObj::makeObject @0x00513970</c> →
/// <c>InitPartArrayObject</c>) and the static-animating tick
/// (<c>CPhysicsObj::animate_static_object @0x00513DF0</c>) drives
/// <c>ScriptManager::UpdateScripts</c> + the object's ParticleManager.
/// In Dereth's Region DAT the two ids are byte-equal for every sky
/// carrier (verified 2026-08-23); the dead <c>pes_id</c> column stays
/// parsed as a cross-check only. Full chain:
/// <c>docs/research/2026-08-23-sky-default-script-port.md</c>.
/// </summary>
public uint DefaultScriptId;
/// <summary>
/// Source GfxObj sort centre. Celestial billboards are authored at their
/// apparent direction from the camera, so transforming and normalizing
@ -525,11 +541,40 @@ public static class SkyDescLoader
GfxObjId = s.DefaultGfxObjectId?.DataId ?? 0u,
PesObjectId = s.DefaultPesObjectId?.DataId ?? 0u,
Properties = s.Properties,
DefaultScriptId = ResolveSetupDefaultScript(
s.DefaultGfxObjectId?.DataId ?? 0u,
dats),
AuthoredSortCenter = ResolveSortCenter(
s.DefaultGfxObjectId?.DataId ?? 0u,
dats),
};
/// <summary>
/// Fetches a sky carrier Setup's <c>DefaultScript</c> id (the retail
/// sky-PES source — see <see cref="SkyObjectData.DefaultScriptId"/>).
/// Non-Setup ids (the dome, star layers, cloud sheets are raw GfxObjs)
/// and missing DATs resolve to zero.
/// </summary>
private static uint ResolveSetupDefaultScript(
uint gfxObjId,
IDatObjectSource? dats)
{
if (dats is null || (gfxObjId & 0xFF000000u) != 0x02000000u)
return 0u;
try
{
return dats.TryGet<Setup>(gfxObjId, out var setup) && setup is not null
? setup.DefaultScript.DataId
: 0u;
}
catch
{
// Enhancement metadata cannot make authoritative sky loading fail.
return 0u;
}
}
private static DatSkyKeyframeData ConvertTimeOfDay(
SkyTimeOfDay s,
IDatObjectSource? dats)