# Sky Decompile Hunt — Agent A **Date:** 2026-04-23 **Status:** SUCCESSFUL HUNT. Found the sky render + unpack code that prior audits (`2026-04-21-sky-deep-audit.md`, `2026-04-22-sky-lighting-decompile.md`) declared missing. **Entry point insight:** The Ghidra strip removed all class names. Prior audits searched for `sky`/`SkyDesc`/`SkyObject` strings and found nothing. The trail that worked: start from the `Region` dat-ID registration (`chunk_00410000.c:12952`, factory-name slot `DAT_00796a6c` → registry `DAT_0079653c`, ID range `0x13000000..0x1300FFFF`), find the Region loader entry (`FUN_00415730(..., 0x1c)` call, type-index 0x1c = REGION per the `"REGION"` string lookup at `chunk_00410000.c:11795`), then follow the global region pointer (`DAT_0084247c`) outward. Every hit is in `chunk_00500000.c` (0x00501530 – 0x00508010 range). --- ## 1. Function Map All citations are `chunk_00500000.c` unless noted. | FUN | Line | Signature (inferred class method) | Purpose | |-----|------|------------------|---------| | `FUN_004ff370` | chunk_004F0000.c:10610 | `Region::Load(uint dataId)` | Calls `FUN_00415730(param_1, 0xb, 0x1c)` to fetch Region from dat; stores in global `DAT_0084247c`. | | `FUN_004ff3b0` | chunk_004F0000.c:10630 | `Region::Unload()` | Releases current Region. | | `FUN_004ff420` | chunk_004F0000.c:10680 | `Region::UpdateSkyObjectsTrampoline()` | Delegates to `FUN_00501990` when Region and SkyDesc both non-null. | | `FUN_004ff440` | chunk_004F0000.c:10692 | `Region::LerpSunAndAmbient(t,...)` | Guards and delegates to `FUN_00501600`. | | `FUN_004ff480` | chunk_004F0000.c:10708 | `Region::LerpFogAmbient(t,...)` | Guards and delegates to `FUN_00501860`. | | `FUN_004ff4b0` | chunk_004F0000.c:10724 | `Region::UpdateSkyObjectTable(t, table*)` | Guards and delegates to `FUN_00502a10`. | | **`FUN_00501530`** | **:1097** | `SkyTimeOfDayList::FindBracket(t, k1*, k2*, u*)` | Walks the keyframe array. For `uVar3 < count-1` picks k1=arr[uVar3], k2=arr[uVar3+1]; if at the last slot, wraps to arr[0] and uses a 1.0 denominator. Returns `u = (t - k1.Begin) / (k2.Begin - k1.Begin)`. **This is the two-keyframe bracket picker.** | | **`FUN_00501600`** | **:1155** | `Region::InterpolateSunLight(t, outDirBright*, outDirColor*, outSunVec*, outAmbColor*)` | The **sun/ambient keyframe interpolator** and spherical-to-cartesian sun-direction builder. See §5. | | **`FUN_00501860`** | **:1238** | `Region::InterpolateFog(t, outMinFog*, outMaxFog*, outFogColor*)` | Lerps fog min/max and fog color between two SkyTimeOfDay keyframes. | | `FUN_00501990` | :1276 | `SkyObjectTable::RollPhysicsScriptFrame(table*)` | Uses a global deterministic hash (from `DAT_008ee9c8`, the player-weenie?) to update an integer at `table[0]` from `table[8]` (count of physics scripts). Likely rolls which variant PES to show. | | `FUN_00501b20` | :1384 | `SkyObject::Unpack(stream*, size*)` | Reads 6 floats (`+4,+8,+0xc,+0x10,+0x14,+0x18`) then 3 uints (`+0x24,+0x28,+0x20`). See §4 offsets. | | `FUN_00501cd0` | :1500 | `SkyObjectReplace::Unpack(stream*, size*)` | Reads 6 dwords at struct indices 0,2,3,4,5,6 (skips index 1 which is runtime pointer). | | `FUN_00501de0` | :1577 | `SkyTimeOfDay::Pack(stream*, size*)` | Writes 11 floats+uint and the SkyObjReplace list (mirror of `FUN_00502100`). | | `FUN_00501f50` | :1669 | `Region::PreloadSkyAssets(region*)` | Iterates every SkyObject and SkyObjectReplace, issues `FUN_0041a4e0(type=8, id)` for every GfxObjId — **this is the sky-mesh preloader**. | | **`FUN_00502100`** | **:1784** | `SkyTimeOfDay::Unpack(stream*, size*)` | Reads 11 dwords into struct indices 0,1,2,3,4,5,6,8,9,10,**7** (index 7 last — the WorldFog enum) and then the SkyObjectReplace list. See §4. | | **`FUN_005025c0`** | **:2124** | `DayGroup::Unpack(stream*, size*)` | Reads `ChanceOfOccur` + PString `DayName` + SkyObject list + SkyTime list. | | **`FUN_00502820`** | **:2279** | `SkyDesc::Unpack(stream*, size*)` | **THE CORE UNPACK.** Reads 2 doubles (TickSize `+8`, LightTickSize `+0x10`) + DayGroup list. See §5. | | **`FUN_00502a10`** | **:2407** | `Region::BuildPerFrameSkyObjectTable(table*, t, weatherChance)` | Iterates DayGroup.SkyObjects, applies visibility check (`Begin==End OR Begin 0". ### Per-Frame SkyObject Render Entry (0x2c bytes, table produced by `FUN_00502a10`): | Offset | Field | |---|---| | +0x00 | GfxObjId (from Default or overridden SkyObjectReplace.GfxObjId) | | +0x04 | PesObjectId (DefaultPesObjectId) | | +0x08 | **[zeroed]** or reset marker | | +0x0c | **Current arc angle (float, degrees)** = BeginAngle + (EndAngle - BeginAngle) * u | | +0x10, +0x14, +0x18 | TexVelocityX, TexVelocityY, (runtime slot) | | +0x1c | Transparent (from SkyObjectReplace.Transparent if > 0) | | +0x20 | Luminosity (from SkyObjectReplace.Luminosity if > 0) | | +0x24 | MaxBright (from SkyObjectReplace.MaxBright if > 0) | | +0x28 | Properties (from SkyObject.Properties) | --- ## 3. Globals Inventory | Global | Use | Confidence | |---|---|---| | `DAT_0084247c` | Current `Region*` (set by `FUN_004ff370`, cleared by `FUN_004ff3b0`) | **HIGH** — guarded by all sky accessors (:10683,:10699,:10715,:10731) | | `DAT_00842798` | Next frame-tick timestamp (next time FUN_00505f30 fires) | HIGH — :6241 `= TickSize + now` | | `_DAT_008379a8` | Current game time (double) — "now" | HIGH — used as current time everywhere | | `_DAT_008427a0` | Next light-tick timestamp (LightTickSize cadence) | HIGH — :6289 | | `_DAT_00842780` | Current DirBright (float) | HIGH — :6041 | | `_DAT_0084277c` | Current DirColor (packed BGRA uint) | HIGH — :6042 | | `_DAT_00842950/4/8` | Current sun vector (x,y,z floats) | HIGH — :6044–6046 | | `_DAT_00842778` | Current AmbColor (packed BGRA uint) | HIGH — :6047 | | `_DAT_008682b0/b4/b8` | D3D directional-light COLOR (R,G,B floats = sun color × brightness) | HIGH — :2084–2086 in FUN_004530e0 | | `_DAT_008682bc/c0/c4` | D3D directional-light DIRECTION (x,y,z floats) | HIGH — :6062–6064 writes sun vector to these | | `_DAT_008682c8/cc/d0` | Second copy of sun vector (likely view-space transform target, or "to-sun" vector) | MEDIUM — :6058–6060 | | `_DAT_0079c6b0` | Degrees-to-radians constant (π/180) | HIGH — used in both `FUN_00501600` (sun direction) and `FUN_005079e0` (mesh rotation) | | `_DAT_00799208` | 1/255.0f constant (byte → float color) | HIGH — :618 in FUN_00451a60 | | `_DAT_007a1870` | Scale for Transparent/Luminosity/MaxBright when overridden | MEDIUM — :7588,7591,7594 | | `DAT_00796344` | Sentinel `0.0f` (likely) — "don't override" marker | MEDIUM — compared against T/L/MB overrides in FUN_00502a10 and FUN_00508010 | | `DAT_008ee9c8` | PlayerWeenie pointer (used to drive PES roll) | MEDIUM | | `DAT_0079653c` | Region dat registry entry (type-index 0x1c) | HIGH — :12964 | | `DAT_00796a6c` | Literal string "REGION" or its dat-type-name slot | HIGH — :12952 | --- ## 4. Call Graph ``` Region::FrameTick (FUN_005062e0) [per-frame] ├── Region::RenderSkyObjects (FUN_00508010) [always] │ ├── Region::UpdateSkyObjectTable (FUN_004ff4b0) │ │ └── Region::BuildPerFrameSkyObjectTable (FUN_00502a10) │ │ └── SkyTimeOfDayList::FindBracket (FUN_00501530) │ ├── per-object: SkyObjectTransform::ApplyRotations (FUN_005079e0) │ │ ├── FUN_00536b80(angleA) // rotate axis 1 │ │ └── FUN_005364e0({0,-angleB*π/180,0}) // rotate axis 2 │ ├── per-object: FUN_00514b90(xform) // enqueue mesh draw │ ├── per-object: FUN_00512360 (MaxBright override) │ ├── per-object: FUN_005124b0 (Luminosity override) │ └── per-object: FUN_005120c0 (Transparency override) │ └── (every TickSize s) Region::ApplyLightState (FUN_00505f30) ├── Region::LerpSunAndAmbient (FUN_004ff440 → FUN_00501600) │ └── SkyTimeOfDayList::FindBracket (FUN_00501530) ├── FUN_00451a60 (packed color → float[3]) ├── FUN_004530e0 (write DirColor × DirBright to _DAT_008682b0..b8) └── loop landblock grid → FUN_00532440 (AdjustPlanes per-cell lighting) Region::Load (FUN_004ff370) [on entering world] └── FUN_00415730(dataId, 0xb, 0x1c) // dat loader SkyDesc::Unpack (FUN_00502820) [called by dat loader] ├── read TickSize (double) ├── read LightTickSize (double) ├── PString (skip? no — seems to skip at line 2308 FUN_00500610) └── DayGroup list: FUN_005025c0 → DayGroup::Unpack ├── read ChanceOfOccur (float) ├── FUN_004fd460 → PString DayName ├── SkyObject list (0x2c-byte each): │ FUN_00501b20 → SkyObject::Unpack └── SkyTime list (0x38-byte each): FUN_00502100 → SkyTimeOfDay::Unpack └── SkyObjectReplace list (0x1c-byte each): FUN_00501cd0 → SkyObjectReplace::Unpack ``` --- ## 5. Quoted Decompile — Key Functions ### 5.1 Keyframe bracket picker (`FUN_00501530`) ```c undefined4 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 ptr do { puVar4 = puVar4 + 1; if (param_2 < *(float *)*puVar4) break; // found k1 such that next.Begin > t uVar3 = uVar3 + 1; } while (uVar3 < iVar1 - 1U); } *param_3 = *(undefined4 *)(*(int *)(param_1 + 8) + uVar3 * 4); // k1 if (uVar3 == iVar1 - 1U) { *param_4 = **(undefined4 **)(param_1 + 8); // k2 = first (wrap) *param_5 = (param_2 - *(float *)*param_3) / (_DAT_007938b0 - *(float *)*param_3); return 1; } pfVar2 = (float *)(*(undefined4 **)(param_1 + 8))[uVar3 + 1]; *param_4 = pfVar2; // k2 *param_5 = (param_2 - *(float *)*param_3) / (*pfVar2 - *(float *)*param_3); return 1; } ``` Notes: `_DAT_007938b0` is `1.0f` (day-fraction upper bound). Walks while `t >= arr[idx+1].Begin`. When at last keyframe, wraps to arr[0].Begin but with normalizer `1.0` instead of `(arr[0].Begin - arr[last].Begin)`. ### 5.2 Sun + ambient color interpolation (`FUN_00501600`) ```c void FUN_00501600(float t, float *outDirBright, undefined1 *outDirColor, float *outSunVec, undefined1 *outAmbColor) { iVar5 = FUN_00501530(region + 0x50 + 0x..., &local_14 /*k1*/, &local_10 /*k2*/, &t /*u*/); if (iVar5 != 0) { // DirBright lerp: k1+0x14 → k2+0x14 ... WAIT, this is on a SkyTimeOfDay which // starts at +0x00 = Begin. So the keyframe struct offsets here are OFFSET -0 from // the keyframe start. Actually this function uses +0x14 directly which would be // DirHeading in our struct mapping — BUT the two SkyTime structs in FUN_00501530 // are identified via their Begin at offset +0. And here FUN_00501600 indexes // from the same base. *outDirBright = (*(float *)(k2 + 0x14) - *(float *)(k1 + 0x14)) * u + *(float *)(k1 + 0x14); // Wait — this is struct offset +0x14 = DirBright per our map. The field WAS read // FIRST into SkyTimeOfDay at +0x04, but FUN_00501530 may pass a DIFFERENT struct // (a SkyLightKeyframe, NOT the SkyTimeOfDay). The offsets +4,+8,+0xc,+0x14,+0x18 // suggest this keyframe record reflects a DIFFERENT internal layout than the // SkyTimeOfDay we parse. *** see Gap §7 *** // DirColor unpack (bytes at k2+0x18..0x1a) param_3[0] = byte@(k2 + 0x1a); // R? (per our map 0x1a is not set) param_3[1] = byte@(k2 + 0x19); param_3[2] = byte@(k2 + 0x18); param_3[3] = 0xff; // fVar9 = scale = lerp(k1+4, k2+4, u) // radius? // fVar1 = pitch = lerp(k1+0xc, k2+0xc, u) * DEG_TO_RAD // fVar6 = head = lerp(k1+8, k2+8, u) * DEG_TO_RAD fVar7 = fcos(fVar1); // cos(pitch) fVar8 = fsin(fVar6); // sin(heading) local_c = fVar9 * fVar8 * fVar7; // X = R * sin(head)*cos(pitch) fVar6 = fcos(fVar6); // cos(heading) *outSunVec = local_c; local_8 = fVar9 * fVar6 * fVar7; // Y = R * cos(head)*cos(pitch) fVar6 = fsin(fVar1); // sin(pitch) outSunVec[1] = local_8; local_4 = fVar6 * fVar9; // Z = R * sin(pitch) outSunVec[2] = local_4; // AmbColor bytes at k2+0x10..0x12 (our map: AmbColor at +0x18... DISCREPANCY) outAmbColor[1] = byte@(k2 + 0x11); outAmbColor[2] = byte@(k2 + 0x12); *outAmbColor = byte@(k2 + 0x10); outAmbColor[3] = 0xff; } // else fallback: brightness 0.3, white dir/amb, sun=(0.5,0,0.8) } ``` **NOTE: THE FIELD OFFSETS HERE CONTRADICT OUR SkyTimeOfDay STRUCT MAP.** See §7 Gaps. ### 5.3 Sky mesh renderer (`FUN_00508010`) ```c void FUN_00508010(int *param_1) { // param_1 = sky-object table FUN_004ff420(); // ensure SkyDesc loaded fVar1 = (DAT_008ee9c8 == 0) ? _DAT_00795610 : *(float *)(DAT_008ee9c8 + 0x48); iVar6 = FUN_004ff4b0(fVar1, param_1); // refresh per-frame table if (iVar6 != 0) { FUN_00507e20(); uVar2 = param_1[2]; uVar7 = 0; if (uVar2 != 0) { iVar6 = 0; do { if (*(int *)(param_1[3] + uVar7 * 4) != 0) { // slot has a GfxObjId? uVar3 = *(undefined4 *)(iVar6 + 8 + *param_1); // (unknown field at +8) uVar4 = *(undefined4 *)(iVar6 + *param_1 + 0xc); // arc angle at +0xc local_48 = 0x3f800000; local_44 = 0; local_40 = 0; local_3c = 0; local_14 = 0; local_10 = 0; local_c = 0; FUN_00535b30(); // reset current transform (possibly sets identity) if ((*(byte *)(param_1[6] + uVar7 * 4) & 4) != 0) { iVar5 = *(int *)param_1[3]; local_14 = *(undefined4 *)(iVar5 + 0x84); // custom position override? local_10 = *(undefined4 *)(iVar5 + 0x88); local_c = *(undefined4 *)(iVar5 + 0x8c); } FUN_005079e0(&local_48, uVar3, uVar4); // apply rotations (axis1=uVar3, axis2=uVar4) FUN_00514b90(&local_48); // queue mesh draw if (DAT_00796344 < *(float *)(iVar6 + 0x20 + *param_1)) // Luminosity override FUN_00512360(0, *(float *)(iVar6 + 0x20 + *param_1) * _DAT_007a1870, 0, 0); if (DAT_00796344 < *(float *)(iVar6 + 0x24 + *param_1)) // MaxBright override FUN_005124b0(0, *(float *)(iVar6 + 0x24 + *param_1) * _DAT_007a1870, 0, 0); if (DAT_00796344 <= *(float *)(iVar6 + 0x1c + *param_1)) // Transparent override FUN_005120c0( *(float *)(iVar6 + 0x1c + *param_1) * _DAT_007a1870, 0, 0); } uVar7 = uVar7 + 1; iVar6 = iVar6 + 0x2c; } while (uVar7 < uVar2); } } } ``` ### 5.4 Region frame tick + D3D light setup (`FUN_005062e0` + `FUN_00505f30`) Key excerpt from `FUN_005062e0` at :6235–6290: ```c if (*(int *)(skyTable + 0x10) != 0) { if (*(int *)(skyTable + 0x20) != 0) FUN_00508010(skyTable); // draw sky every frame local_14 = _DAT_008379a8; // now if ((_DAT_00842798 <= _DAT_008379a8) && (DAT_0084247c != 0)) { // next TickSize-delay firing time _DAT_00842798 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 8) + _DAT_008379a8; // acquire current scene-light time "fVar1" from player weenie or default fVar1 = (DAT_008ee9c8 == 0) ? _DAT_00795610 : *(float *)(DAT_008ee9c8 + 0x48); if (_DAT_008427a0 < _DAT_008379a8) { iVar7 = FUN_004ff440(fVar1, &local_24 /*DirBright*/, &local_20 /*DirColor*/, local_c /*sunVec[3]*/, &local_18 /*AmbColor*/); if (iVar7 != 0) { if (local_24 < DAT_0084295c) local_24 = DAT_0084295c; // clamp min brightness // Optional: crossfade to a custom state (DAT_008427a9 flag) if (DAT_008427a9 != '\0') { … blend to DAT_008427ac/0xb0/0xb4 over _DAT_008427b8 … } FUN_00505f30(local_24, local_20, local_c, local_18); // push to D3D + AdjustPlanes } _DAT_008427a0 = _DAT_007c7200; if (DAT_0084247c != 0) _DAT_008427a0 = *(double *)(*(int *)(DAT_0084247c + 0x50) + 0x10); // LightTickSize _DAT_008427a0 = _DAT_008427a0 + local_14; } } } ``` ### 5.5 Light state publisher (`FUN_00505f30`) ```c void FUN_00505f30(int landblockGrid, float DirBright, uint DirColor, float *sunVec, uint AmbColor) { DAT_00842780 = DirBright; DAT_0084277c = DirColor; if (DAT_008427a8 == '\0') { DAT_00842950 = sunVec[0]; DAT_00842954 = sunVec[1]; DAT_00842958 = sunVec[2]; DAT_00842778 = AmbColor; } else { // special-case crossfade … } _DAT_008682c8 = DAT_00842950; // sun vec copy 1 (to-sun?) _DAT_008682d0 = DAT_00842958; _DAT_008682cc = DAT_00842954; FUN_00451a60(DAT_00842778); // unpack AmbColor bytes → _DAT_008682bc/c0/c4 (scratch) _DAT_008682c0 = fVar2; // sun vec copy 2 (forward?) _DAT_008682bc = fVar1; _DAT_008682c4 = fVar3; DAT_008682d4 = 0; if (DAT_0083da58 != 0) FUN_004530e0(|sun| * _DAT_0079a1e8 + DirBright, DirColor); // D3D SetLight // Recompute per-cell lighting iVar4 = *(int *)(landblockGrid + 4); // grid width for each (x,y): if (cell[y*w+x] != 0) FUN_00532440(cell); } ``` --- ## 6. D3D calls — what retail actually does The decompile shows **no direct `SetRenderState` / `SetMaterial` / `SetLight` constant numbers** (no `14`, no `0x20`, etc.). Retail uses a C++ wrapper where the D3D device is instance-pointer-based (probably at `_DAT_008682xx` family of globals). The actual D3D bridge is elsewhere — a per-frame "render" sweep reads `_DAT_008682b0..d4` and feeds them to whatever lighting model D3D is set to. What we CAN say with high confidence: - Sky meshes are drawn by `FUN_00514b90` with a per-object transform. This enqueues a mesh draw in the normal render queue. - The directional light (sun) color is `DirColor × DirBright` and gets written to `_DAT_008682b0..b8` — these globals feed the D3D directional light. - The ambient color is `AmbColor` (packed uint), but **NOT** scaled by `AmbBright` at this location. The AmbBright-multiply must happen somewhere downstream (maybe inside the D3D bridge's SetAmbient, or baked into material setup). - Per-cell landblock lighting is recomputed AT THIS MOMENT (FUN_00532440 AdjustPlanes), so terrain + statics re-light each time the sky keyframe ticks. **No matrix-identity-with-zero-translation pattern** was found for a camera-anchored sky. Retail appears to render sky objects in world-space with a per-frame transform built from `FUN_00535b30` (reset transform) + `FUN_005079e0` (apply rotations). That's consistent with r12 deepdive description of sky meshes being camera-follow via the transform system, not a traditional infinite-far skybox. --- ## 7. Gaps / What I could NOT resolve 1. **Keyframe struct offsets in `FUN_00501600` disagree with `SkyTimeOfDay` struct as parsed by `FUN_00502100`.** The unpack writes DirBright at +0x04, DirHeading at +0x08, DirPitch at +0x0c, DirColor at +0x10, AmbBright at +0x14, AmbColor at +0x18. But the interpolator reads DirBright at +0x14, ambient bytes at +0x10, and uses +0x18..+0x1a for the "sun color" bytes. These offsets map better to a **DIFFERENT keyframe layout** — possibly the code passes a keyframe-sublist or a different struct (e.g. the 0x20-byte scaled-color record). **Next hunt:** locate what `region + 0x50 + X` pointer is passed to `FUN_00501530` inside `FUN_00501600`. The `DayGroup` struct likely has a light-keyframe sublist separate from the time-of-day list. Worth checking: is there a SEPARATE light-keyframe list at DayGroup +0x20 that FUN_00501600 uses? 2. **Two-axis rotation semantics in `FUN_005079e0`.** FUN_00536b80 is "axis 1" and FUN_005364e0 rotates around axis 2 with `{0, -angleB*DEG_TO_RAD, 0}` (Z-axis or Y-axis depending on internal convention). Need to disassemble those two to confirm. 3. **"Activity flag" at param_1[6] bit 0x04 inside `FUN_00508010`.** Some sky objects apparently get a different translation from +0x84/+0x88/+0x8c of the first slot (camera origin?). Not yet decoded. 4. **`DAT_00796344` sentinel value.** I assumed `0.0f` but didn't confirm by grep. If it's `-1.0f` that changes the "only-if-positive" interpretation of override fields. 5. **Unit of Transparent/Luminosity/MaxBright.** Retail multiplies the 3 values by `_DAT_007a1870` before applying. If that constant is `0.01f`, the raw dat values ARE percentages and our `/100` fix is retail-correct. If it's `1.0f`, the raw values are already fractions and the recent commit `eeae83a` is wrong. **Next hunt:** a 4-byte grep for `_DAT_007a1870` to find its initializer. One way: find where `_DAT_007a1870` is written, look at any `0x3c23d70a` (=0.01f as IEEE float) or `0x3f800000` (=1.0f) constant in the same function. 6. **AmbBright handling.** FUN_00505f30 doesn't visibly scale AmbColor by AmbBright. The bright-multiply either happens inside `FUN_00451a60`'s caller chain or it's a different globally-set D3D register. Our acdream code pre-multiplies AmbColor by AmbBright in the loader which might DOUBLE-apply if the D3D bridge also does it. 7. **GfxObj preload list.** `FUN_00501f50` preloads every sky GfxObjId — that's the function acdream should call when entering a Region to prefetch all sky meshes. 8. **Sky mesh vertex format / shader setup.** Not searched; `FUN_00514b90` is the entry to the rendering queue and is shared with all meshes. Need to disassemble to see if sky meshes get any distinguishing material flag. --- ## 8. Actionable implications for acdream - **Port `FUN_00508010` as the sky draw loop.** Our WorldBuilder port is mostly right but: - The current arc angle should come from a **per-frame recomputed table** (like FUN_00502a10), not recomputed every draw call. - Transparent/Luminosity/MaxBright overrides are applied **ONLY IF > 0** (sentinel `DAT_00796344`). Our code should replicate this "don't override" gate. - **Port `FUN_00501530` keyframe bracketing VERBATIM** — WorldBuilder's equivalent may not match the retail wrap-around (where k2 = arr[0] and denominator = 1.0 in the last-slot case). - **Resolve the keyframe-struct mystery in FUN_00501600 before trusting the sun-direction math.** The offsets disagree with our dat-unpack struct. Something is reordering fields, and we need to know what before the ambient tint fix locks in. - **The scale constant `_DAT_007a1870` is load-bearing for the T/L/MB units question.** Find its value BEFORE shipping any fix that divides by 100. - **Retail does re-light EVERY cell (`FUN_00532440`) when the sky keyframe ticks.** This is the per-vertex lighting update that our port currently doesn't do — we set a uniform tint but don't recompute per-cell baked lighting. For faithful look, we need to re-run AdjustPlanes per-cell on each lighting tick. --- ## 9. Summary Prior audits said "no retail sky code found." That was wrong — the code is all in `chunk_00500000.c` between 0x00501530 and 0x00508010 plus the global accessors in `chunk_004F0000.c:10610-10740`. The trail starts with `DAT_0084247c` (current Region global) and field `+0x50` (SkyDesc pointer). The trail to find it: search for the Region-dat-type-index (0x1c) as a parameter to the dat loader, not for sky strings. **Every question the prior audits left open can now be answered by reading the specific functions listed in §1.** The main remaining work is to disassemble the two axes in `FUN_005079e0` and resolve the keyframe-struct-offset discrepancy in `FUN_00501600`.