The uncommitted uTint=AmbientColor-for-alpha-submeshes experiment (from
the 2026-04-22 inference) dimmed the sky dome's baked gradient — a
user-verified visual regression. Reverting to the eeae83a baseline
(uTint=Vector4.One for every submesh) while we execute the proper
retail-verbatim port.
Research: three parallel decompile-hunt agents landed verifying
retail's ground-truth sky pipeline for the first time (prior audits
searched for stripped symbol names; the trail opened via the Region
dat-type-index 0x1c registration at chunk_00410000.c:12952). Key
retail functions now mapped in chunk_00500000.c:1097-7535:
- FUN_00501530: keyframe bracket-picker (with 1.0f wrap denominator)
- FUN_00501600: sun+ambient interpolator (sunVec = DirBright ×
(sin yaw·cos pit, cos yaw·cos pit, sin pit))
- FUN_00501860: fog interpolator
- FUN_00502820: SkyDesc::Unpack (2 doubles + DayGroup list)
- FUN_00502a10: build per-frame sky-object table
- FUN_00505f30: apply light state + per-cell AdjustPlanes relight
- FUN_005062e0: per-frame sky tick (throttled by LightTickSize)
- FUN_00508010: sky-object render loop (enqueues through the NORMAL
mesh pipeline via FUN_00514b90 — not a bespoke path)
Surprise findings:
- D3DRS_AMBIENT is set to 0 once at init and NEVER changes per-frame
(chunk_005A0000.c). The r12-inferred "clouds = texture × D3DRS_
AMBIENT" formula is falsified. Retail instead routes keyframe
AmbColor through per-vertex lighting on non-Luminous sky meshes
via _DAT_008682bc/c0/c4.
- Retail does NOT anchor the sky to the camera or use a separate
sky projection. Sky meshes live in world space and follow the
camera via scene-graph parent.
- FUN_00532440 (AdjustPlanes) re-lights every terrain cell on every
keyframe tick — the "terrain follows the sky" effect we don't yet
reproduce.
Phase 1 code change (this commit):
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: revert uTint to white
for all submeshes (the per-submesh blend split stays — sun gets
additive, clouds get alpha). Keep the `keyframe` parameter in the
signature for Phase 2 readiness. Comments now cite the retail
functions and reference docs instead of the (disproven) r12 formula.
- src/AcDream.Core/World/SkyDescLoader.cs: ACDREAM_DUMP_SKY=1 logs
the entire Region SkyDesc on load — DayGroups, SkyObjects, every
SkyTimeOfDay keyframe, and every SkyObjectReplace with RAW pre-/100
Transparent/Luminosity/MaxBright values so we can settle the unit
question empirically.
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: ACDREAM_DUMP_SKY=1
additionally logs each sky GfxObj's Surfaces and their SurfaceType
flags on first load, so we can identify which meshes carry the
Luminous bit (dome? sun? moon? stars?) vs which are lit.
- src/AcDream.App/Rendering/GameWindow.cs: passes the interpolated
keyframe to the sky renderer (kept — needed for Phase 2).
Research docs (pushed as part of this commit):
- docs/research/2026-04-23-sky-retail-verbatim.md: full synthesis
with retail function map, struct layouts, globals, pseudocode, and
a 4-phase port plan.
- docs/research/2026-04-23-sky-decompile-hunt-{A,B,C}.md: raw hunt
outputs.
- docs/research/2026-04-23-sky-references-crossref.md: WorldBuilder/
ACE/ACViewer/holtburger/Chorizite coverage.
- docs/research/2026-04-23-sky-dat-schema.md: full dat schema + unit
analysis.
- docs/research/2026-04-22-sky-lighting-decompile.md: prior agent's
(superseded) inference — kept for provenance.
Phase 2 will port Surface.Luminous-flag-aware per-vertex lighting for
sky submeshes once the dump resolves the open questions (Luminous-flag
distribution per Dereth sky mesh; _DAT_007a1870 scale constant value).
Build + 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
492 lines
30 KiB
Markdown
492 lines
30 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|
||
## 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.
|