refactor(world): own live environment state

Move the DAT sky, selected day group, world clock, weather, AdminEnvirons bridge, and debug cycles into a one-shot WorldEnvironmentController while preserving GameWindow's public aliases and accepted startup/session/render order. Correct the named retail citations and register the remaining environment audio and fog/radar gaps.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 10:54:33 +02:00
parent 557eb7ef6b
commit d09e246d3a
19 changed files with 606 additions and 368 deletions

View file

@ -4,11 +4,16 @@
**Status:** RESEARCH COMPLETE — algorithm recovered, ready for C# port.
**Companion docs:** `2026-04-23-sky-retail-verbatim.md` (full sky pipeline map).
> **Named-oracle correction (2026-07-22):** the Sept 2013 PDB identifies this
> body as `SkyDesc::CalcPresentDayGroup @ 0x00500E10`. References below to
> `FUN_00501990 @ 0x00501990` describe the older fallback binary/chunk only;
> that address is inside `SkyDesc::Pack` in the named 2013 build.
---
## 0. TL;DR
Retail's DayGroup picker is a **simple integer LCG mixed with `(YEAR, DAY_OF_YEAR)` from the live `TimeOfDay` struct**, scaled by `DayGroupCount`. It is **not** a weighted walk over `ChanceOfOccur` — it's uniform over all DayGroups. Dereth's 20 DayGroups all carry `ChanceOfOccur = 5.0f` anyway, so uniform matches the intent. The algorithm lives in `FUN_00501990` (previously mis-labeled "deterministic PES roll" in the sky-verbatim doc; it is actually **SkyDesc::PickCurrentDayGroup**).
Retail's DayGroup picker is a **simple integer LCG mixed with `(YEAR, DAY_OF_YEAR)` from the live `TimeOfDay` struct**, scaled by `DayGroupCount`. It is **not** a weighted walk over `ChanceOfOccur` — it's uniform over all DayGroups. Dereth's 20 DayGroups all carry `ChanceOfOccur = 5.0f` anyway, so uniform matches the intent. The named algorithm is **`SkyDesc::CalcPresentDayGroup @ 0x00500E10`**; `FUN_00501990` is its older-build fallback identity.
---
@ -41,7 +46,7 @@ Note: `FUN_004ff4b0` (the OTHER mapped trampoline → `FUN_00502a10`) is called
```c
// chunk_00500000.c:1274 FUN_00501990 @ 0x00501990 size=138
// SkyDesc::PickCurrentDayGroup(this)
// Older build of SkyDesc::CalcPresentDayGroup(this)
void __fastcall FUN_00501990(uint *param_1)
{
float fVar1;
@ -127,7 +132,7 @@ Global: `DAT_008ee9c8` (type `void *`). Populated by `FUN_005a7fd0` (TimeOfDay::
| 0x00 | double | `EpochBase` (dat-declared) | `FUN_005a7fd0:6045` |
| 0x08 | int | `BaseYear` (dat-declared) | `FUN_005a7fd0:6051` |
| 0x0C | float | `SecondsPerDay` | `FUN_005a7fd0:6056` |
| **0x10** | int | **`SecondsPerDay` (int copy — source of `iVar6`)** | `FUN_005a7fd0:6061` |
| **0x10** | int | **`DaysPerYear` (source of `iVar6`; live-probed as 360)** | `FUN_005a7fd0:6061`; `2026-04-23-retail-memory-probe.md` |
| 0x40 | double | `SecondsPerYear = DaysPerYear × SecondsPerDay` | `FUN_005a7fd0:6068` |
| 0x48 | float | `DayFraction` (0..1) | `FUN_005a7800:5497` |
| 0x50 | double | CurrentDay startTick | `FUN_005a7510` (via `FUN_005a75b0`) |
@ -136,24 +141,28 @@ Global: `DAT_008ee9c8` (type `void *`). Populated by `FUN_005a7fd0` (TimeOfDay::
| **0x68** | int | **`DayOfYear`** | `FUN_005a7510:5304`: `= floor(withinYearSec / secsPerDay)` |
| 0x6c | int | `SeasonIndex` | `FUN_005a7510:5313` |
The `iVar6 = *(int *)(DAT_008ee9c8 + 0x10)` read in `FUN_00501990` picks up `SecondsPerDay` as a multiplier — acting as a spread-factor to guarantee different days in different years yield different hash inputs. (If both sides of the multiplication were small — Year in [0, ~200], DayOfYear in [0, 365] — and `SecondsPerDay` were 1, the seed range would be tiny. Using the full `SecondsPerDay` integer makes the seed uniform over a ~31-bit range before the LCG step.)
The `days_per_year = GameTime::current_game_time->days_per_year` read in
`SkyDesc::CalcPresentDayGroup` is the year-to-day multiplier. The live memory
probe measured 360 at the older build's corresponding `TimeOfDay+0x10` field,
so `Year × DaysPerYear + DayOfYear` is a flat total-day index.
Retail Dereth values (from dat — confirm when we next parse a Region): `SecondsPerDay = 1800` (30 real-minutes = one Dereth-day per r12 §11, times some scale factor baked into the dat). Any value works — the algorithm is agnostic.
Retail Dereth's DAT-backed `DaysPerYear` value is 360.
---
## 5. Pseudocode (ready for C# port)
```csharp
// SkyDesc.PickCurrentDayGroup — ports acclient.exe FUN_00501990 @ 0x00501990
// SkyDesc.CalcPresentDayGroup — named retail @ 0x00500E10
// Older-build cross-reference: FUN_00501990 @ 0x00501990
// Decompile: docs/research/decompiled/chunk_00500000.c:1276
// Must be called every frame before SkyDesc.UpdateSkyObjectTable; value is
// stable across frames within one Dereth-day so repeated calls are cheap.
public static int PickCurrentDayGroup(int year, int secondsPerDay, int dayOfYear, int dayGroupCount)
public static int CalcPresentDayGroup(int year, int daysPerYear, int dayOfYear, int dayGroupCount)
{
// Step 1: deterministic per-day seed.
// (This mirrors retail's 3-int read from TimeOfDay+0x64/0x10/0x68.)
int seed = year * secondsPerDay + dayOfYear;
int seed = year * daysPerYear + dayOfYear;
// Step 2: 32-bit signed LCG (x86 wraps silently; force it in C#).
int mixed = unchecked(seed * 0x6A42FDB2 + (int)0x8ABE1652);
@ -180,14 +189,15 @@ Call from the per-frame sky render hook, **before** the keyframe-bracket interpo
```csharp
// once per frame, before sky object/light interp
skyDesc.CurrentDayGroupIndex = SkyDesc.PickCurrentDayGroup(
skyDesc.CurrentDayGroupIndex = SkyDesc.CalcPresentDayGroup(
year: world.TimeOfDay.Year,
secondsPerDay: world.TimeOfDay.SecondsPerDayInt, // the int copy at TimeOfDay+0x10
daysPerYear: world.TimeOfDay.DaysPerYear,
dayOfYear: world.TimeOfDay.DayOfYear,
dayGroupCount: skyDesc.DayGroups.Count);
```
If ACE and acdream agree on `(Year, SecondsPerDay, DayOfYear)` (trivially true — ACE computes these server-side from the same epoch), both clients will converge to the same index every Dereth-day. This replaces the current SplitMix64 path.
Retail and acdream converge whenever their `(Year, DaysPerYear, DayOfYear)`
inputs agree. ACE does not select a day group; each client computes it locally.
---
@ -204,12 +214,10 @@ So retail's decompile IS our only source, and we have it.
## 7. Gaps
None critical. Two minor open items:
None critical. One minor open item:
1. **`_DAT_007c6f10` exact bit value.** I inferred `1.0/2^32` from usage pattern but did not find a data-section dump that prints the literal. If the live client ever shows off-by-one drift on a boundary day, re-verify by dumping the .rdata section at 0x007c6f10. (ACDREAM_DUMP_SKY already logs picked index + inputs; we can correlate against a retail client session to confirm.)
2. **`TimeOfDay+0x10` value for live Dereth.** Retail `SecondsPerDay` has been quoted as both 1800 and 3600 in various reverse-engineering docs depending on whether "seconds" means realtime seconds or Dereth-clock seconds. The algorithm itself is insensitive — whatever integer the dat provides is what both retail and acdream must use. If our Region parser stores it in a differently-named field, make sure the call site passes the dat-raw value (the int at TimeOfDay+0x10), not a re-derived one.
---
## 8. Summary table
@ -217,8 +225,8 @@ None critical. Two minor open items:
| Question | Answer | Evidence |
|---|---|---|
| Caller of `FUN_00502a10` | `FUN_004ff4b0` → called from `FUN_00508010:7560` | `chunk_004F0000.c:10732`, `chunk_00500000.c:7560` |
| DayGroup selection function | `FUN_00501990` | `chunk_00500000.c:1276` |
| Hash formula | `(Year × SecondsPerDay + DayOfYear) × 0x6A42FDB2 + 0x8ABE1652` | `chunk_00500000.c:1296` |
| DayGroup selection function | `SkyDesc::CalcPresentDayGroup @ 0x00500E10` | named Sept 2013 PDB/pseudo-C; older fallback `FUN_00501990`, `chunk_00500000.c:1276` |
| Hash formula | `(Year × DaysPerYear + DayOfYear) × 0x6A42FDB2 + 0x8ABE1652` | `SkyDesc::CalcPresentDayGroup @ 0x00500E10`; older `chunk_00500000.c:1296` |
| Weighted by ChanceOfOccur? | **No.** Uniform over DayGroupCount. | No CDF loop in FUN_00501990 |
| Selected index stored at | `SkyDesc + 0x00` (via `*param_1` write) | `chunk_00500000.c:1307,1309` |
| Read back by | `FUN_00502a10:2429`, `FUN_00501600`, `FUN_00501860` | `chunk_00500000.c:2429` |

View file

@ -35,7 +35,8 @@ DayOfYear = 47 ✓
The decompile agent's C trail
(`docs/research/2026-04-23-sky-decompile-hunt-C.md` §1 + §4) labeled
`TimeOfDay+0x10` as *"SecondsPerDay (int copy — source of iVar6)"*
for the `FUN_00501990` (`SkyDesc::PickCurrentDayGroup`) LCG seed.
for the named `SkyDesc::CalcPresentDayGroup @ 0x00500E10` LCG seed
(older-build cross-reference `FUN_00501990 @ 0x00501990`).
The live probe disproves that. The value is **360** — the same as
`GameTime.DaysPerYear` from the dat.
@ -43,7 +44,8 @@ The live probe disproves that. The value is **360** — the same as
Implication for the retail LCG seed formula:
```c
// FUN_00501990 line 1296 of chunk_00500000.c:
// SkyDesc::CalcPresentDayGroup @ 0x00500E10
// (older build: FUN_00501990 line 1296 of chunk_00500000.c):
iVar4 = (iVar3 * iVar6 + iVar4) * 0x6a42fdb2 + -0x7541e9ae;
// ^Year ^x0x10 ^DayOfYear
```