# Sky Decompile Hunt C — Globals & Keyframe Interpolator **Date:** 2026-04-23 **Hunter:** Agent C (globals / keyframe math) **Scope:** Find the runtime state block the sky renderer reads, the keyframe-interp math, and the per-frame entry point. **Source tree:** `C:\Users\erikn\source\repos\acdream\docs\research\decompiled\chunk_*.c` (55 chunks, 688K lines). All citations use `{chunk_file}:{line}` relative to the decompile tree. --- ## ⚠ 2026-04-24 correction Sections §1, §2, §5 of this doc label `DAT_00842778` as "AmbColor" and `DAT_0084277c` as "DirColor/Fog". **That labeling is backwards.** The correct mapping — cross-verified against the DatReaderWriter schema (`SkyTimeOfDay.Unpack` field order) and the `FUN_00501600` output map: - `DAT_00842778` = **DirColor** (directional/sun color ARGB) - `DAT_0084277c` = **AmbColor** (ambient color ARGB) - `DAT_00842780` = **AmbBright** (ambient brightness scalar, *not* fog start) The `FUN_00532440` per-vertex Lambert at `chunk_00530000.c:2118-2124` reads `DAT_00842778` as the N·L-modulated color (→ directional) and `DAT_0084277c × DAT_00842780` as the flat / brightness-scaled color (→ ambient × ambBright). The pre-multiply at line 2107 takes `DAT_00842780 * DAT_0084277c` which is the textbook "ambient scalar × ambient color" retail ambient term. See `docs/research/2026-04-24-lambert-brightness-split.md` for the full re-analysis and `SkyTimeOfDay.generated.cs` for the field offsets (+0x10 DirColor, +0x18 AmbColor). All entries below should be read with this swap in mind; the decompile math quotes themselves are correct. --- ## 1. Global Inventory — the sky state block All globals live in a contiguous block at **`0x00842778..0x008427c0`** with a second cluster at **`0x00842950..0x00842960`**. Every field is read by landblock/draw code and written only by the per-frame updater `FUN_005062e0` via the interp delegate `FUN_00501600`. Initial values are set in `FUN_00505dd0` (the sky-system constructor). | Address | Size | Inferred semantic | Initial value | Writers | Readers | |---|---|---|---|---|---| | **`DAT_00842778`** | 4 B (ARGB packed u32) | **AmbColor / SunLight color** — unpacked to 3 floats via `FUN_00451a60` into the active D3D material/light | `0` (default white via fallback) | `FUN_00505f30:6047`; `FUN_00501600` (via out-param `param_5`) | `FUN_00451a60(DAT_00842778)` in `chunk_00450000.c:4341, 4468`; `chunk_00500000.c:6061` — applied to D3D light state | | **`DAT_0084277c`** | 4 B (ARGB packed u32) | **Fog color** — passed to `FUN_004530e0(fogStart, DAT_0084277c)` and also decomposed into 3 floats `(R,G,B)*1/255` for per-vertex terrain lighting | `0` | `FUN_00505f30:6042` (param_3); `FUN_00501600` (via `param_3`) | `chunk_00530000.c:2095,2097,2099` (terrain lighting); `chunk_00500000.c:6069`; `chunk_00450000.c:4347,4474` | | **`DAT_00842780`** | 4 B (float) | **Fog start distance / luminosity offset** — added to `\|sun\|·_DAT_0079a1e8` and passed as first arg to fog setup `FUN_004530e0` | `0x3ecccccd` = **0.4f** | `FUN_00505f30:6041` (param_2); `FUN_00501600` (via out-param `param_2`) | `chunk_00500000.c:6067-6069`; `chunk_00530000.c:2094, 2107-2109` (terrain mul) | | `DAT_00842784` | 4 B (ARGB u32) | Sky-fog secondary color (bucket pick helper — see §6) | `0` | `chunk_00550000.c:11851/61/67/80/92` (palette decision in render-sample path), `FUN_005062e0:6295,6301` | `chunk_00500000.c:6295,6301,6305,6311,6317` | | `DAT_00842788` | 4 B (ARGB u32) | Sky-fog primary color (same role, paired with 0x842784) | `0` | `chunk_00550000.c:11850/62/67/80/91`, `FUN_005062e0:6251,6259` | `chunk_00500000.c:6251,6259,6263,6269,6275` | | `DAT_00842790` | 4 B (ptr) | **Cloud / stars heightmap buffer** — `operator_delete__`'d on teardown, re-alloc'd to `(N+1)*0x100` bytes where N is cloud-layer count | `NULL` | `chunk_00500000.c:5994, 6543` | `chunk_00500000.c:6571, 6586, 6597, 6599, 6539` | | `_DAT_00842798` | 8 B (double) | **Next cloud/weather update deadline** — compared to the global game-clock `_DAT_008379a8` each frame, advanced by `TickSize` (from `*(double *)(DAT_0084247c + 0x50) + 8`) | `0` | `FUN_00505d40:5923` (reset); `FUN_005062e0:6241` | `FUN_005062e0:6240` | | `_DAT_0084279c` | (pairs with 0x842798 as high half of double) | — | `0` | `FUN_00505d40:5924` | (via 0x842798 as double) | | **`_DAT_008427a0`** | 8 B (double) | **Next sky-keyframe update deadline** — compared to `_DAT_008379a8`, advanced by `LightTickSize` (from `*(double *)(DAT_0084247c + 0x50) + 0x10`) | `0` | `FUN_00505d40:5925`; `FUN_005062e0:6285,6289` | `FUN_005062e0:6249` | | `_DAT_008427a4` | (pairs as high half of 0x8427a0) | — | `0` | `FUN_00505d40:5926` | — | | **`DAT_008427a8`** | 1 B (bool) | **FixedLight override enable** — `true` means "run live keyframe interp each frame"; `false` means the caller of `FUN_00505f30` supplies the values directly | `0` | `FUN_00505d40:5922` (set from `param_1 != 0`) | `FUN_00505f30:6043` | | `DAT_008427a9` | 1 B (bool) | **Crossfade in progress** flag — enables blending from the previous keyframe output into the new one over `_DAT_007c7208` per-frame step | — | `chunk_00500000.c:7270` | `FUN_005062e0:6256, 6297`; `chunk_00500000.c:7281` | | `DAT_008427ac` | 4 B (float) | **Crossfade target: fog start** (previous frame's fog start, held for lerp) | — | — | `FUN_005062e0:6258, 6279` | | `DAT_008427b0` | 4 B (float) | **Crossfade target: fog start/secondary-1** | — | — | `FUN_005062e0:6299, 6320` | | `DAT_008427b4` | 4 B (float) | **Crossfade target: fog start/secondary-2** | — | — | `FUN_005062e0:6300, 6321` | | `_DAT_008427b8` | 4 B (float) | **Crossfade u-parameter** — 0..1; advanced by `_DAT_007c7208` per sample | — | `FUN_005062e0:6280, 6322` | `FUN_005062e0:6257, 6279, 6298, 6320` | | **`DAT_00842950`** | 4 B (float) | **Sun direction X** | `0x3f99999a` = **1.2f** (default before first interp) | `FUN_00505f30:6044`; `FUN_00501600` → `param_4[0]` | `chunk_00500000.c:6057,6067,6058`; `chunk_00450000.c:4086,4337,4464`; `chunk_00530000.c:2029,2118,2137,2156,2175,2206` | | **`DAT_00842954`** | 4 B (float) | **Sun direction Y** | `0` | `FUN_00505f30:6045`; `FUN_00501600` → `param_4[1]` | same file set; appears as `local_44[0]` in terrain lighting | | **`DAT_00842958`** | 4 B (float) | **Sun direction Z** | `0x3f000000` = **0.5f** | `FUN_00505f30:6046`; `FUN_00501600` → `param_4[2]` | same file set; appears as `local_44[1]` in terrain lighting | | **`DAT_0084295c`** | 4 B (float) | **Minimum fog-start clamp** — if interp result < this, snap up | — | (loaded from region config, probably MinWorldFog) | `FUN_00505f30:6051-6052`; `FUN_005062e0:6253-6254` | | `DAT_00842960` | 4 B (int) | Heightmap dim counter (used alongside DAT_00842790) | — | `FUN_00500000.c:6544, 6540` | — | **D3D light/material slots written from the block:** | D3D slot | Source | Written at | |---|---|---| | `_DAT_008682bc/c0/c4` | `fVar1/2/3` = copy of `DAT_00842950/54/58` (a second sun-dir-like slot — likely "light direction copy") | `FUN_00505f30:6062-6064` | | `_DAT_008682c8/cc/d0` | `DAT_00842950/54/58` directly (primary sun direction vector3) | `FUN_00505f30:6058-6060` | | `DAT_008682d4` | `0` (light enable flag) | `FUN_00505f30:6065` | --- ## 2. Keyframe Interpolator — `FUN_00501600` (0x00501600) **Signature:** ```c void FUN_00501600(float param_1, // u = dayFraction (0..1) float *param_2, // out: interpolated fog start scalar undefined1 *param_3, // out: interpolated fog color (4 bytes ARGB) float *param_4, // out: interpolated sun direction vec3 (scaled by brightness) undefined1 *param_5); // out: interpolated ambient color (4 bytes ARGB) ``` The function first brackets `u` against the keyframe table (`FUN_00501530`, see §3), then interpolates. **Full decompile** (`chunk_00500000.c:1151-1232`): ```c void FUN_00501600(float param_1,float *param_2,undefined1 *param_3,float *param_4, undefined1 *param_5) { ... iVar5 = FUN_00501530(param_1,&local_14,&local_10,¶m_1); uVar2 = local_10; // next keyframe if (iVar5 != 0) { // fog-start scalar: lerp field +0x14 *param_2 = (*(float *)(local_10 + 0x14) - *(float *)(local_14 + 0x14)) * param_1 + *(float *)(local_14 + 0x14); // fog color ARGB (3 bytes) — each byte is (lerp between k_i[0x18..0x1a] and k_i+1[0x18..0x1a]) // FUN_005df4c4 is a clamp-to-byte helper param_2 = (float *)(uint)*(byte *)(local_10 + 0x19); // G uVar3 = FUN_005df4c4(); param_2 = (float *)(uint)*(byte *)(local_10 + 0x18); // R uVar4 = FUN_005df4c4(); local_10 = (uint)*(byte *)(local_10 + 0x1a); // B param_2 = (float *)CONCAT31(param_2._1_3_,uVar4); uVar4 = FUN_005df4c4(); param_3[2] = uVar4; // B param_3[1] = uVar3; // G *param_3 = param_2._0_1_; // R param_3[3] = 0xff; // A // Sun direction — polar (heading, pitch) w/ magnitude scalar (field +0x04) fVar9 = ((float10)*(float *)(uVar2 + 4) - (float10)*(float *)(local_14 + 4)) * (float10)param_1 + (float10)*(float *)(local_14 + 4); // fVar9 = magnitude (DirBright/length) fVar1 = ((*(float *)(uVar2 + 0xc) - *(float *)(local_14 + 0xc)) * param_1 + *(float *)(local_14 + 0xc)) * (float)_DAT_0079c6b0; // pitch_rad fVar6 = (((float10)*(float *)(uVar2 + 8) - (float10)*(float *)(local_14 + 8)) * (float10)param_1 + (float10)*(float *)(local_14 + 8)) * (float10)_DAT_0079c6b0; // yaw_rad fVar7 = (float10)fcos((float10)fVar1); // cos(pitch) fVar8 = (float10)fsin(fVar6); // sin(yaw) local_c = (float)(fVar9 * fVar8 * fVar7); // x = mag * sin(yaw)*cos(pitch) fVar6 = (float10)fcos(fVar6); // cos(yaw) *param_4 = local_c; local_8 = (float)(fVar9 * fVar6 * fVar7); // y = mag * cos(yaw)*cos(pitch) fVar6 = (float10)fsin((float10)fVar1); // sin(pitch) param_4[1] = local_8; local_4 = (float)(fVar6 * fVar9); // z = mag * sin(pitch) param_4[2] = local_4; // Ambient color ARGB (3 bytes) — lerp field +0x10..0x12 param_2 = (float *)(uint)*(byte *)(uVar2 + 0x11); // G uVar3 = FUN_005df4c4(); param_2 = (float *)(uint)*(byte *)(uVar2 + 0x10); // R uVar4 = FUN_005df4c4(); param_3 = (undefined1 *)(uint)*(byte *)(uVar2 + 0x12); // B ... param_5[2] = uVar4; // B param_5[1] = uVar3; // G *param_5 = param_2._0_1_; // R param_5[3] = 0xff; // A return; } // Fallback: no keyframe → white ambient, white fog, sun (0.5, 0, 0.8), fog_start 0.3 *param_2 = 0.3; param_3[2] = 0xff; param_3[1] = 0xff; *param_3 = 0xff; param_3[3] = 0xff; param_5[2] = 0xff; param_5[1] = 0xff; *param_5 = 0xff; param_5[3] = 0xff; *param_4 = 0.5; param_4[1] = 0.0; param_4[2] = 0.8; return; } ``` **Keyframe struct layout** (inferred from FUN_00501600 + the loader/saver `FUN_00501a20`/`FUN_00501b20` at `chunk_00500000.c:1316-1446`): | Offset | Type | Semantic | |---|---|---| | `+0x00` | `float *` (back-pointer to `this`) | self | | `+0x04` | `float` | **t0 / Begin** — day-fraction at which this keyframe is active (compared in bracket search) **AND** reused as **DirBright (sun magnitude)** scalar inside the interp! Actually this is DirBright per ACE schema; bracket search at FUN_00501530:1115 uses `*(float *)*puVar4` which dereferences the back-pointer — so the actual `t0` lives at `*this` (offset 0), and the field at +0x04 is DirBright. **But** the save-order in FUN_00501a20 starts from +0x04, so the on-disk file does NOT store t0 explicitly here — it's elsewhere (the outer container holds t0; see §3). | | `+0x08` | `float` | **DirHeading** (degrees, converted to radians via `_DAT_0079c6b0` = π/180) | | `+0x0c` | `float` | **DirPitch** (degrees → radians) | | `+0x10..+0x12` | 3×`byte` | **AmbColor** (R, G, B at +0x10, +0x11, +0x12) | | `+0x13` | `byte` | AmbColor alpha (unused; always 0xff on write) | | `+0x14` | `float` | **MinWorldFog / fog start scalar** (or AmbBright; see §7 gaps) | | `+0x18..+0x1a` | 3×`byte` | **FogColor** (R, G, B) | | `+0x1b` | `byte` | FogColor alpha | | `+0x1c` | `ptr` | Optional per-keyframe mesh/sky-object chain (non-zero used by `FUN_00501860`) | | `+0x20` | `float` | **Secondary field 1** (ACE calls this `DirColor.Luminosity` or `MaxWorldFog`; used in `FUN_00501860:1250`) | | `+0x24` | `float` | **Secondary field 2** (used in `FUN_00501860:1252` — pairs with +0x20) | | `+0x28..+0x2a` | 3×`byte` | **Secondary color** (used in `FUN_00501860:1254-1263`) | | `+0x2b` | `byte` | Secondary color alpha | Total struct size: **`0x2c` bytes** (44 B), which matches the `FUN_005df0f5(0x2c)` alloc in `chunk_00500000.c:5972`. This is the in-memory SkyTimeOfDay (one keyframe). The outer keyframe-table header is at `param_1` in FUN_00501530: - `*(int *)(param_1 + 8)` = pointer to array of keyframe pointers - `*(int *)(param_1 + 0x10)` = keyframe count - `*(float *)*puVar4` = keyframe's `t0` (reached via the back-pointer at struct +0x00) --- ## 3. Keyframe bracket search — `FUN_00501530` (0x00501530) `chunk_00500000.c:1093-1129`: ```c undefined4 __thiscall FUN_00501530(int param_1,float param_2,undefined4 *param_3,undefined4 *param_4,float *param_5) { iVar1 = *(int *)(param_1 + 0x10); // count if (iVar1 == 0) return 0; uVar3 = 0; if (iVar1 != 1) { puVar4 = *(undefined4 **)(param_1 + 8); // array base do { puVar4 = puVar4 + 1; if (param_2 < *(float *)*puVar4) break; // u < keyframe[i+1].t0 ? uVar3 = uVar3 + 1; } while (uVar3 < iVar1 - 1U); } *param_3 = *(undefined4 *)(*(int *)(param_1 + 8) + uVar3 * 4); // lower keyframe if (uVar3 == *(int *)(param_1 + 0x10) - 1U) { *param_4 = **(undefined4 **)(param_1 + 8); // wrap to keyframe[0] *param_5 = (param_2 - *(float *)*param_3) / (_DAT_007938b0 - *(float *)*param_3); // u01 = (t - lower.t0) / (1.0 - lower.t0) — wrap-around case return 1; } pfVar2 = (float *)(*(undefined4 **)(param_1 + 8))[uVar3 + 1]; *param_4 = pfVar2; // upper keyframe *param_5 = (param_2 - *(float *)*param_3) / (*pfVar2 - *(float *)*param_3); // u01 = (t - lower.t0) / (upper.t0 - lower.t0) return 1; } ``` This is exactly the interpolator-finder pattern the hunt asked for. The `t0` for each keyframe is stored at the FIRST FIELD of each keyframe record (via the back-pointer layer). The last keyframe wraps to keyframe[0] using `1.0` as the upper bound (`_DAT_007938b0` = 1.0). --- ## 4. Sun direction formula — confirmed `chunk_00500000.c:1192-1205` computes the sun unit vector from `(DirHeading, DirPitch)`: ``` yaw_rad = DirHeading * π/180 pitch_rad = DirPitch * π/180 mag = DirBright (interpolated, scales the output length) x = mag * sin(yaw) * cos(pitch) y = mag * cos(yaw) * cos(pitch) z = mag * sin(pitch) ``` This matches the hunt-brief formula exactly **except** it multiplies by the per-keyframe magnitude `fVar9` (DirBright). So `DAT_00842950/54/58` is **not a unit vector**; it is `DirBright * unit_direction`. The terrain lighting code at `chunk_00530000.c:2118` dot-products this with per-vertex normals, so the `DirBright` scaling baked into the vector directly modulates the diffuse lighting strength. Coordinate frame: retail AC is **Y-up world** for sky azimuth but stores sun in world coords where **Z-axis is vertical** (common in retail AC: X=east, Y=north, Z=up). `sin(pitch)` → Z means pitch=+90° → straight up; pitch=0 → horizontal; that matches AC's "degrees above horizon" convention. --- ## 5. Day-fraction / tick source `chunk_00500000.c:6239-6248` (inside `FUN_005062e0`): ```c local_14 = _DAT_008379a8; // game-clock (seconds, double) if ((_DAT_00842798 <= _DAT_008379a8) && (DAT_0084247c != 0)) { // cloud-deadline met & region loaded _DAT_00842798 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 8) + _DAT_008379a8; // ^--- SkyDesc.TickSize (from the Region DB object) if (DAT_008ee9c8 == 0) { fVar1 = (float)_DAT_00795610; // = 0.0f (no world loaded) } else { fVar1 = *(float *)(DAT_008ee9c8 + 0x48); // <-- THIS is the dayFraction } ``` And at `chunk_00500000.c:6285-6289`: ```c _DAT_008427a0 = _DAT_007c7200; // default LightTickSize if (DAT_0084247c != 0) { _DAT_008427a0 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 0x10); // ^--- SkyDesc.LightTickSize (from Region DB object) } _DAT_008427a0 = _DAT_008427a0 + local_14; // advance deadline ``` **Day-fraction source:** `*(float *)(DAT_008ee9c8 + 0x48)`. `DAT_008ee9c8` is the **world/time global** (a CTimeStamp-like struct). Its fields used by the sky: - `+0x10` : int — "tickCount A" component (used in `FUN_00501990:1293`, a PRNG seed hint) - `+0x48` : **float dayFraction** (0..1) — directly consumed by FUN_00501600 as `param_1` - `+0x64` : int — "tickCount B" - `+0x68` : int — "tickCount C" I did NOT locate the writer of `+0x48` (the conversion from wall-clock seconds to dayFraction) in this hunt — see §7 "Gaps". **Region-config pointer:** `DAT_0084247c` points at the loaded `Region` object; `Region + 0x50` is a pointer into `SkyDesc`, and `SkyDesc + 8` = TickSize (double seconds) and `SkyDesc + 0x10` = LightTickSize (double seconds) — matching the dat schema (`docs/research/2026-04-23-sky-dat-schema.md:21-22`). --- ## 6. Per-frame sky update entry point — `FUN_005062e0` (0x005062E0) **The function the hunt brief is asking for.** `chunk_00500000.c:6213-6333`. Skeleton (key flow, comments added): ```c void __fastcall FUN_005062e0(int param_1) { ... if (*(int *)(param_1 + 0x10) != 0) { if (*(int *)(param_1 + 0x20) != 0) { FUN_00508010(); // prepare cloud buffers } local_14 = _DAT_008379a8; // game-clock snapshot if ((_DAT_00842798 <= _DAT_008379a8) && (DAT_0084247c != 0)) { _DAT_00842798 = SkyDesc.TickSize + _DAT_008379a8; // next tick deadline fVar1 = (DAT_008ee9c8 == 0) ? 0.0f : *(float *)(DAT_008ee9c8 + 0x48); // dayFraction // ---- Sky-keyframe update (gated by LightTickSize) ---- if (_DAT_008427a0 < _DAT_008379a8) { iVar7 = FUN_004ff440(fVar1,&local_24,&local_20,local_c,&local_18); // ^ primary interp: fogStart, fogColorARGB, sunDirVec3, ambColorARGB if (iVar7 != 0) { if (local_24 < DAT_0084295c) local_24 = DAT_0084295c; // clamp min fog start if (DAT_008427a9 != '\0') { // crossfade active // blend towards previous frame's held values (DAT_008427ac, DAT_00842788) // step crossfade u by _DAT_007c7208 each tick ... } FUN_00505f30(local_24, local_20, local_c, local_18); // ^ commits the interp result to the DAT_008427xx / DAT_008429xx globals // AND pushes them to D3D light/material/fog state. } _DAT_008427a0 = SkyDesc.LightTickSize + local_14; // advance light deadline } // ---- Secondary interp (starfield / alt palette — DAT_00842784/88) ---- FUN_005a4010(DAT_0081dbf8 == '\0'); if (DAT_0081dbf8 != '\0') { FUN_005a3f90(DAT_0081dbf8); iVar7 = FUN_004ff480(fVar1,&local_1c,&local_24,&local_20); // ^ secondary interp (starfield): 2 floats + 1 color if (iVar7 != 0) { // same crossfade pattern targeting DAT_008427b0/b4 and DAT_00842784 ... FUN_005a41b0(&local_20, local_1c, local_24); // push to starfield state } } } } } ``` **Entry-point contract:** - Called once per world/frame tick from the top-level world update (see §7). - Reads `DAT_008ee9c8 + 0x48` for the current dayFraction. - Runs **two** independent interpolators against the same keyframe tables, throttled by `TickSize`/`LightTickSize` (from `SkyDesc`). - `FUN_00505f30` (see below) writes the final values into the globals that the rest of the render pipeline samples. --- ## 6b. Commit-to-globals — `FUN_00505f30` (0x00505F30) `chunk_00500000.c:6026-6092`. This is the function that *writes* the `DAT_00842xxx` globals. It is the public "set sky state" entry point — called with either (a) fresh interp output (from `FUN_005062e0:6283`) or (b) caller-supplied values (the FixedLight override path when `DAT_008427a8 == 0`). ```c void FUN_00505f30(int param_1, float param_2, undefined4 param_3, float *param_4, undefined4 param_5) { DAT_00842780 = param_2; // fog start DAT_0084277c = param_3; // fog color ARGB if (DAT_008427a8 == '\0') { // FixedLight mode: caller provides raw values DAT_00842950 = *param_4; // sun X DAT_00842954 = param_4[1]; // sun Y DAT_00842958 = param_4[2]; // sun Z DAT_00842778 = param_5; // ambient color ARGB } else { // dynamic mode: re-run interp against current dayFrac snapshot iVar4 = FUN_004ff440(0x3f000000, // 0.5 placeholder (?) &DAT_00842780, &DAT_0084277c, &DAT_00842950, &DAT_00842778); if ((iVar4 != 0) && (DAT_00842780 < DAT_0084295c)) { DAT_00842780 = DAT_0084295c; // min fog clamp } } // Push sun direction to the active D3D directional light fVar3 = DAT_00842958; fVar2 = DAT_00842954; fVar1 = DAT_00842950; _DAT_008682c8 = DAT_00842950; _DAT_008682cc = DAT_00842954; _DAT_008682d0 = DAT_00842958; // Unpack ambient color ARGB → 3 floats into the D3D material FUN_00451a60(DAT_00842778); _DAT_008682bc = fVar1; _DAT_008682c0 = fVar2; _DAT_008682c4 = fVar3; DAT_008682d4 = 0; // Fog: distance = length(sunDir) * 0079a1e8 + fogStart; color = fog color ARGB if (DAT_0083da58 != 0) { FUN_004530e0(SQRT(DAT_00842950 * DAT_00842950 + DAT_00842954 * DAT_00842954 + DAT_00842958 * DAT_00842958) * _DAT_0079a1e8 + DAT_00842780, DAT_0084277c); } // Touch each dungeon EnvCell (re-apply fog/lighting state to each) if (*(int *)(param_1 + 8) != 0) { ... FUN_00532440(); ... } } ``` **Dispatch glue** — `FUN_004ff440` (at `chunk_004F0000.c:10692-10704`) is a null-check wrapper around `FUN_00501600`: ```c undefined4 FUN_004ff440(param_1, param_2, param_3, param_4, param_5) { if ((DAT_0084247c != 0) && (*(int *)(DAT_0084247c + 0x50) != 0)) { FUN_00501600(param_1, param_2, param_3, param_4, param_5); return 1; } return 0; } ``` --- ## 7. Call graph ``` FUN_004554b0 (0x004554B0 — world update root) chunk_00450000.c:3831 └── FUN_005062e0 (0x005062E0 — per-frame sky update) chunk_00500000.c:6213 ├── reads: _DAT_008379a8 (game-clock) │ DAT_008ee9c8 + 0x48 (dayFraction) │ DAT_0084247c + 0x50 (SkyDesc ptr) │ DAT_0084247c + 0x50 + 8 (TickSize double) │ DAT_0084247c + 0x50 + 0x10 (LightTickSize double) ├── writes: _DAT_00842798 (next cloud tick) │ _DAT_008427a0 (next light tick) │ _DAT_008427b8 (crossfade u) │ DAT_008427a9 (crossfade active) ├── FUN_004ff440 → FUN_00501600 (primary interp) chunk_00500000.c:1151 │ └── FUN_00501530 (bracket search) chunk_00500000.c:1093 ├── FUN_00505f30 (commits globals + D3D state) chunk_00500000.c:6026 │ ├── writes: DAT_00842778, DAT_0084277c, DAT_00842780 │ │ DAT_00842950, DAT_00842954, DAT_00842958 │ ├── writes: _DAT_008682bc..d4 (D3D directional light) │ ├── FUN_00451a60 (unpack ambient ARGB → 3 floats) chunk_00450000.c:608 │ └── FUN_004530e0 (push fog state) └── FUN_004ff480 → FUN_00501860 (secondary interp: starfield) chunk_00500000.c:1236 └── FUN_00505f30-equivalent callers use DAT_00842784/88 --- Readers (consumers of sky globals per frame) --- chunk_00450000.c:4086,4337-4341,4464-4468 // landblock draw re-applies to D3D chunk_00450000.c:4347, 4474 // landblock draw re-applies fog color chunk_00530000.c:2094-2230 // per-vertex terrain shading: // diffuse = max(dot(vertexNormal, sunDir), minAmbient) // lit_color = ambRGB*fogStart + diffuse*fogRGB // clamp to 1.0 per channel ``` Note: `chunk_00530000.c:2094-2230` is the canonical per-vertex lit-color calculation (retail's CPU lighting path). It consumes: - `DAT_00842950/54/58` as the sun direction (NOT normalized — carries `DirBright` baked in). - `DAT_00842780` as the "ambient base multiplier" (named `DirBright` in the schema but semantically `AmbBright` here: it's the scalar multiplying the **ambient** ARGB, not the sun). - `DAT_00842778` ARGB → (R,G,B)/255 as **ambient color** (confirmed: line 2100,2104,2105). - `DAT_0084277c` ARGB → (R,G,B)/255 as **diffuse color** (confirmed: line 2095,2097,2099). - `DAT_00796344` = minimum-dot clamp (avoids fully-black back faces). - `_DAT_007938c0` (probably 1.0 or slightly above) = per-channel saturation clamp. This flips my §1/§2 preliminary labels: **`DAT_0084277c` is actually the DIFFUSE/sun color** (not fog) and **`DAT_00842778` is the AMBIENT color**. The `FUN_004530e0` call at `FUN_00505f30:6067-6069` passes `DAT_0084277c` to the fog setup, but fog in retail AC also takes the **diffuse/sun color** as its tint — that's the known design (distant fog matches sun tint). So the semantic naming that unifies both consumers is: - **`DAT_0084277c`** = **DirColor** (sun/diffuse ARGB) — doubles as fog tint - **`DAT_00842778`** = **AmbColor** (ambient ARGB) - **`DAT_00842780`** = **scalar brightness mul** applied to the AMBIENT in the lit-color formula, AND as the fog-start offset (it's a dual-purpose "ambient base" scalar) This matches the FUN_00501600 output mapping from §2: - interp out `*param_2` ← keyframe `+0x14` → `DAT_00842780` → "ambient scalar / fog base" - interp out `*param_3` ← keyframe `+0x18..+0x1a` → `DAT_0084277c` → **DirColor (sun + fog)** - interp out `*param_4[0..2]` ← keyframe `+0x04,+0x08,+0x0c` via polar→cartesian → `DAT_00842950..58` → **sun direction * DirBright** - interp out `*param_5` ← keyframe `+0x10..+0x12` → `DAT_00842778` → **AmbColor** So the on-disk SkyTimeOfDay→in-memory field offsets are: | Offset | Field (ACE schema name) | Matched consumer | |---|---|---| | `+0x04` | `DirBright` | sun-vector magnitude `fVar9` | | `+0x08` | `DirHeading` (deg) | yaw → `sin/cos` | | `+0x0c` | `DirPitch` (deg) | pitch → `cos/sin` | | `+0x10/+0x11/+0x12` | `AmbColor` (R,G,B) | `DAT_00842778` | | `+0x14` | `AmbBright` (!) | `DAT_00842780` — multiplies AmbColor in lit_color; also fog-start offset | | `+0x18/+0x19/+0x1a` | `DirColor` (R,G,B) | `DAT_0084277c` — diffuse AND fog tint | | `+0x20` | `MinWorldFog` | secondary interp `local_1c` via `FUN_00501860:1250` | | `+0x24` | `MaxWorldFog` | secondary interp `local_24` via `FUN_00501860:1252` | | `+0x28/+0x29/+0x2a` | `WorldFogColor` (R,G,B) | starfield ARGB in `FUN_005a41b0` | This **matches exactly** the published ACE SkyTimeOfDay struct (`docs/research/2026-04-23-sky-dat-schema.md:42-54`) — every field has a decompile home now. --- ## 8. Gaps / next hunts 1. **Day-fraction writer.** I found `*(float *)(DAT_008ee9c8 + 0x48)` is READ as `dayFraction`. I did NOT locate the WRITER — i.e. which function computes `dayFraction = (gameClockSeconds mod dayLength) / dayLength` and stamps it to `+0x48`. Look in chunk_004F0000.c for `DAT_008ee9c8 + 0x48` write-sites, likely driven by a world-tick function using `SkyDesc.TickSize`. 2. **`DAT_00796344` and `_DAT_007938c0` constants.** In the per-vertex lighting (`chunk_00530000.c:2119`), `DAT_00796344` is the minimum-dot clamp. `_DAT_007938c0` is the per-channel saturation clamp. Both should be extracted to confirm they are `0.0f` and `1.0f` respectively (or something like `0.0` and `> 1.0` to allow HDR). 3. **FixedLight (static sky) path.** When `DAT_008427a8 == 0` (FixedLight mode), the caller supplies raw sun direction, ambient, fog color. This is likely the path used for dungeons (no day/night cycle). The caller of `FUN_00505d40(0)` (which sets `DAT_008427a8 = 0`) needs to be mapped — it's the dungeon-load entry. 4. **Starfield secondary keyframe.** `FUN_00501860` / `FUN_005a41b0` (starfield state) uses keyframe fields `+0x20, +0x24, +0x28..+0x2a`. If acdream renders stars, port `FUN_005a41b0` too. 5. **Crossfade parameters `_DAT_007c7208` and `DAT_008427ac`.** The crossfade (`DAT_008427a9`) is a smoothing blend when keyframes are force-swapped (e.g. weather change). `_DAT_007c7208` is the per-tick u-step; `DAT_008427ac` is the held previous value. If acdream wants faithful weather transitions, port this loop from `FUN_005062e0:6256-6281`. 6. **Fog calling convention `FUN_004530e0`.** I did not decompile this but its signature — `FUN_004530e0(fogDistance, fogColorARGB)` — is unambiguous. Port as `SetFog(distance, ARGB)`. The `distance` arg is `|sunDir| * _DAT_0079a1e8 + fogStart` — so distant fog grows with the "weighted sun-vector length". This is AC's recognizable fog-near-horizon effect. 7. **The already-known globals hint resolved.** The hunt brief cited `DAT_00842778/7c`, `DAT_00842950..58` — all mapped in §1. No other sky-specific globals exist in this address neighborhood. --- ## 9. Quick summary for the porter **What the retail sky pipeline does per frame:** 1. Pull dayFraction from `world.time.dayFraction` (`DAT_008ee9c8 + 0x48`, a float 0..1). 2. **Gate** on `LightTickSize` elapsed — only interpolate every `LightTickSize` seconds; otherwise reuse last frame's values (hence the visible ~2-second "step" in AC's time-of-day tint). 3. **Bracket** dayFraction across the current DayGroup's `SkyTimeOfDay[]` by `t0`. Compute u01. 4. **Interpolate** each channel: fogStart, fogColorARGB (byte-lerp), sunDir via polar-to-cartesian of (DirHeading, DirPitch) scaled by DirBright, ambColorARGB (byte-lerp). 5. **Clamp** fogStart ≥ `DAT_0084295c` (the MinWorldFog floor from the Region). 6. **Crossfade** blend if a keyframe swap was forced (weather). 7. **Commit** to six floats (sunDir x3, fogStart x1) + two ARGB dwords (fog=sun, amb). 8. **Push** those values to D3D: `_DAT_008682bc..d0` = sun vec3 duplicated into "direction copy" and "direction primary" slots; `_DAT_008682c8..d0` = sun vec3 copy 2; a material ambient RGB (unpacked from the ARGB); and fog(distance, color). 9. **Re-apply** the state on every landblock/EnvCell draw via `FUN_00451a60` + `FUN_004530e0` (because D3D resets between passes). All values to port live in `DAT_008427xx` / `DAT_00842950..58` — no secrets elsewhere.