# Sky DAT Schema — Full Map

**Date:** 2026-04-23
**Scope:** Every field in the retail Region→SkyDesc→DayGroup→SkyObject/SkyTimeOfDay tree, with units, comments, and cross-references. Covers SurfaceType flags and the acdream interpretation.
**Purpose:** Pre-port audit. No code changes.
**Sources:** DatReaderWriter auto-generated Ghidra-parsed types; ACE; ACViewer; WorldBuilder. Retail decompile (`docs/research/decompiled/chunk_*.c`) has NO sky-related matches (confirmed in prior audit, `2026-04-21-sky-deep-audit.md:9-15`).

---

## 1. Structure Tree

```
Region (DB_TYPE_REGION, 0x13000000–0x1300FFFF)
  ├── RegionNumber      : uint32   // internal ID
  ├── Version           : uint32
  ├── RegionName        : PString
  ├── LandDefs, GameTime, PartsMask, SoundInfo, SceneInfo, TerrainInfo, RegionMisc
  └── SkyInfo           : SkyDesc (only if PartsMask has HasSkyInfo)

SkyDesc
  ├── TickSize          : double      // seconds per game-tick (day length unit)
  ├── LightTickSize     : double      // seconds per lighting update tick
  └── DayGroups         : List<DayGroup>

DayGroup
  ├── ChanceOfOccur     : float       // weight in [0, 1] for PDF rolling
  ├── DayName           : PString     // e.g. "Clear", "Sunny" — internal-only label
  ├── SkyObjects        : List<SkyObject>         // celestial meshes (sun/moon/clouds/...)
  └── SkyTime           : List<SkyTimeOfDay>      // keyframes through the day

SkyObject                                          // ONE celestial layer
  ├── BeginTime         : float       // [0, 1] day-fraction visibility start
  ├── EndTime           : float       // [0, 1] day-fraction visibility end; wraps if End<Begin
  ├── BeginAngle        : float       // degrees arc-start
  ├── EndAngle          : float       // degrees arc-end; lerped across [BeginTime,EndTime]
  ├── TexVelocityX      : float       // UV scroll speed along U (per-second)
  ├── TexVelocityY      : float       // UV scroll speed along V
  ├── DefaultGfxObjectId: QualifiedDataId<GfxObj>  // the mesh (0 = hidden)
  ├── DefaultPesObjectId: QualifiedDataId<PhysicsScript>  // optional particle emitter
  └── Properties        : uint32      // flag bits (billboard? follow-camera? — not decoded)

SkyTimeOfDay                                       // ONE keyframe at time T
  ├── Begin             : float       // [0, 1] when this keyframe becomes the "active" interpolant
  ├── DirBright         : float       // sun intensity multiplier (unit unspecified; typically 0..~1.5)
  ├── DirHeading        : float       // sun compass heading (degrees; 0=N, 90=E, …)
  ├── DirPitch          : float       // sun elevation above horizon (degrees; -90..+90)
  ├── DirColor          : ColorARGB   // sun RGB, bytes 0..255 each
  ├── AmbBright         : float       // ambient intensity multiplier
  ├── AmbColor          : ColorARGB   // ambient RGB, bytes 0..255 each
  ├── MinWorldFog       : float       // meters — fog starts
  ├── MaxWorldFog       : float       // meters — fog saturates
  ├── WorldFogColor     : ColorARGB   // fog tint, bytes 0..255 each
  ├── WorldFog          : uint32      // mode enum: 0=off, 1=D3DFOG_LINEAR, 2=exp, 3=exp2
  └── SkyObjReplace     : List<SkyObjectReplace>

SkyObjectReplace                                   // per-keyframe override of one SkyObject
  ├── ObjectIndex       : uint32      // index into owning DayGroup.SkyObjects
  ├── GfxObjId          : QualifiedDataId<GfxObj>  // mesh override (0 = keep default)
  ├── Rotate            : float       // heading override — **degrees**
  ├── Transparent       : float       // UNIT DISPUTED — see §4
  ├── Luminosity        : float       // UNIT DISPUTED — see §4
  └── MaxBright         : float       // UNIT DISPUTED — see §4
```

File citations: `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/Region.generated.cs:27-67`, `Types/SkyDesc.generated.cs:23-30`, `Types/DayGroup.generated.cs:23-31`, `Types/SkyObject.generated.cs:23-41`, `Types/SkyTimeOfDay.generated.cs:23-47`, `Types/SkyObjectReplace.generated.cs:23-35`.

---

## 2. Field Table with Units & Sources

| Field | Type | Unit / Range | Comment source | Example |
|---|---|---|---|---|
| `SkyDesc.TickSize` | double | seconds? | none in generator | 1.0 in test fixture (`tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs:26`) |
| `SkyDesc.LightTickSize` | double | seconds? | none | 2.0 in test fixture (same) |
| `DayGroup.ChanceOfOccur` | float | probability weight [0,1]? | none; r12 §11 describes PDF use (`docs/research/deepdives/r12-weather-daynight.md:544`) | 1.0 |
| `DayGroup.DayName` | PString<byte> | UTF-8 | none | "Sunny" (memory; diag dump referenced in `SkyDescLoader.cs:253`) |
| `SkyObject.BeginTime` | float | day-fraction [0,1] | r12 deepdive §2 "normalized day-time" (`deepdives/r12-weather-daynight.md:161`) | 0.0 (always-visible dome) |
| `SkyObject.EndTime` | float | day-fraction [0,1]; Begin==End → always visible; Begin>End → wraps midnight | our `SkyDescLoader.cs:46-52` encodes this; matches WorldBuilder `SkyboxRenderManager.cs:188-197` | 1.0, 0.25 |
| `SkyObject.BeginAngle` | float | degrees | r12 §2 "where on the sky arc" | 0° |
| `SkyObject.EndAngle` | float | degrees | r12 §2 lerped linearly | 360° |
| `SkyObject.TexVelocityX` | float | UV/second | r12 §2.1 "UV scroll rate — cloud drift, star twinkle" | ~0.001 typical |
| `SkyObject.TexVelocityY` | float | UV/second | same | ~0.001 |
| `SkyObject.DefaultGfxObjectId` | QualifiedDataId<GfxObj> | dat-ID; 0=hidden | none | 0x01000XYZ typical |
| `SkyObject.DefaultPesObjectId` | QualifiedDataId<PhysicsScript> | dat-ID; 0=none | r12 §2.1 "unused at top" | 0 |
| `SkyObject.Properties` | uint32 | flag bits | not decoded in references; our code ignores | 0 |
| `SkyTimeOfDay.Begin` | float | day-fraction [0,1] | our `SkyState.cs:45` "day-fraction this keyframe kicks in" | 0.0, 0.25, 0.5, 0.75 typical |
| `SkyTimeOfDay.DirBright` | float | multiplier, **> 1 legal** | our `SkyState.cs:40-42` "channels above 1.0 and the frag shader clamps" | 1.5 in test |
| `SkyTimeOfDay.DirHeading` | float | compass degrees, 0=N clockwise | r12 §3 (`deepdives/r12-weather-daynight.md:262`) | 180° at noon |
| `SkyTimeOfDay.DirPitch` | float | degrees, -90..+90 from horizontal | r12 §3 | 70° at noon |
| `SkyTimeOfDay.DirColor` | ColorARGB | 4×byte BGRA-packed in file | `ColorARGB.generated.cs:27-45` "0-255" | {R=255,G=250,B=220} at noon |
| `SkyTimeOfDay.AmbBright` | float | multiplier | none; same semantics as DirBright | 0.4 in test |
| `SkyTimeOfDay.AmbColor` | ColorARGB | 4×byte BGRA | same | noon ~(82,82,89) |
| `SkyTimeOfDay.MinWorldFog` | float | meters | r12 §5.1 (`deepdives/r12-weather-daynight.md:346`) | 120 |
| `SkyTimeOfDay.MaxWorldFog` | float | meters | r12 §5.1 | 350 |
| `SkyTimeOfDay.WorldFogColor` | ColorARGB | 4×byte BGRA | none | similar to horizon band |
| `SkyTimeOfDay.WorldFog` | uint32 | D3D fog mode: 0 off, 1 linear, 2 exp, 3 exp2 | r12 §5.1 "D3DFOG_LINEAR, etc" (line 349) | 1 (linear) |
| `SkyObjectReplace.ObjectIndex` | uint32 | index into DayGroup.SkyObjects | none | 0..6 |
| `SkyObjectReplace.GfxObjId` | QualifiedDataId<GfxObj> | dat ID; 0 = keep default | r12 §2.3 "swap mesh" | 0x01000... or 0 |
| `SkyObjectReplace.Rotate` | float | **degrees** heading override | tested in `SkyDescLoader.cs:260-263`: "270° values in the data are genuinely heading-degrees, not percentages" | 270 observed |
| `SkyObjectReplace.Transparent` | float | **UNIT UNCONFIRMED** — see §4 | none | values up to 100 observed in acdream diag dump |
| `SkyObjectReplace.Luminosity` | float | **UNIT UNCONFIRMED** — see §4 | none | values up to 100 observed |
| `SkyObjectReplace.MaxBright` | float | **UNIT UNCONFIRMED** — see §4 | none | values up to 100 observed |

Note on `ColorARGB`: the wire/byte order is B, G, R, A per `ColorARGB.generated.cs:49-52`; the member names are semantic (Red=red channel 0..255 regardless of file byte position). Our loader's `ColorToVec3` at `SkyDescLoader.cs:311-315` uses the semantic `.Red/.Green/.Blue / 255f` which is correct.

---

## 3. SurfaceType Enum (full)

From `references/DatReaderWriter/DatReaderWriter/Generated/Enums/SurfaceType.generated.cs:13-41` (cross-checked `dats.xml:744-758`). Flags, parent uint:

| Name | Value | Notes / inferred meaning |
|---|---|---|
| `Base1Solid` | `0x00000001` | Surface is a flat color (no texture); `Surface.ColorValue` field populated |
| `Base1Image` | `0x00000002` | Surface has a texture (SurfaceTexture DataId) |
| `Base1ClipMap` | `0x00000004` | Alpha-keyed texture; fragment shader discards low-alpha pixels |
| `Translucent` | `0x00000010` | Implies standard alpha blending |
| `Diffuse` | `0x00000020` | Receives diffuse lighting (default state for most surfaces) |
| `Luminous` | `0x00000040` | Self-illuminated / unshaded — NOT additive blend; see §5 |
| `Alpha` | `0x00000100` | Explicit alpha blend |
| `InvAlpha` | `0x00000200` | Inverted alpha blend (rare) |
| `Additive` | `0x00010000` | Additive blend (sun, glow, particle) |
| `Detail` | `0x00020000` | Detail texture layer |
| `Gouraud` | `0x10000000` | Gouraud shading (vs. flat) |
| `Stippled` | `0x40000000` | Stipple pattern (AC-era hardware dithering) |
| `Perspective` | `0x80000000` | Perspective-correct texturing |

Our `TranslucencyKind.FromSurfaceType` at `src/AcDream.Core/Meshing/TranslucencyKind.cs:61-74` maps these to blend modes:

- `Additive` (0x10000) → `TranslucencyKind.Additive` (src,one blend)
- `InvAlpha` (0x200) → `TranslucencyKind.InvAlpha`
- `Alpha | Translucent` (0x100 | 0x10) → `TranslucencyKind.AlphaBlend` (src_alpha, one_minus_src_alpha)
- `Base1ClipMap` (0x04) → `TranslucencyKind.ClipMap` (opaque draw, fragment discard)
- else → `Opaque`

`Luminous` (0x40) is **not** mapped to any translucency category — it's a lighting hint, not a blend mode. The code comment at `SkyRenderer.cs:345-350` documents a past bug where we treated Luminous as additive and "blew the whole sky to white." That call is consistent with ACE/ACViewer usage (Luminous sets `OrigLuminosity` on the `PhysicsPart.Surface`; see `references/ACViewer/ACViewer/Physics/Common/Surface.cs:15-19`).

No generator comments in Chorizite.ACProtocol (it has no dat-surface types — it's network-message-only). No explicit "SrcBlend/DestBlend" comments anywhere in the references.

---

## 4. acdream SkyDescLoader vs Dat Reality — What's Likely Right / Wrong

### Likely correct

- **`ColorARGB` → `Vector3/255f`** (`SkyDescLoader.cs:311-315`) — matches the generator comment "0-255" on each channel.
- **Sun/Amb color pre-multiplied by bright** (`SkyDescLoader.cs:289-291`) — matches r12 §4 formula (`deepdives/r12-weather-daynight.md:307-309`); test pins this (`SkyDescLoaderTests.cs:84`).
- **`WorldFog` enum mapping** (`SkyDescLoader.cs:278-284`) — matches r12 §5.1 (D3DFOG_LINEAR = 1, exp = 2, exp2 = 3).
- **Visibility / arc-progress wrap logic** (`SkyDescLoader.cs:46-76`) — matches WorldBuilder `SkyboxRenderManager.cs:188-240` line-by-line.
- **`Rotate` kept as degrees** (`SkyDescLoader.cs:260`) — confirmed by our own observation that diag values of 270 are headings.

### Likely WRONG: the `/100` on Transparent / Luminosity / MaxBright

**This is the load-bearing speculative decision.**

Code: `SkyDescLoader.cs:273-275` divides all three by 100. The comment at 249-267 claims "confirmed from live diag dump … have Luminosity=100 and Transparent=100" — but there is NO supporting evidence anywhere else in the references:

1. **DatReaderWriter generator has no comment** on those three fields (`SkyObjectReplace.generated.cs:28-34`). Unlike `Surface.Luminosity` which is documented as "Self-illumination / emissive strength" and used as a **0..1 fraction** in the test fixture (`SurfaceTests.cs:27`, value `0.3f`), the `SkyObjectReplace` fields have no generator comment.
2. **ACE's `SkyObjectReplace.cs:10-12`** defines them as `float` with no unit comment.
3. **ACViewer** just displays them raw as `"Transparent: {value}"` without transform (`references/ACViewer/ACViewer/Entity/SkyObjectReplace.cs:35-48`).
4. **WorldBuilder doesn't use them at all** — `SkyboxRenderManager.cs:180-264` ignores `Transparent/Luminosity/MaxBright` entirely. So WorldBuilder gives us zero evidence either way about units.
5. **No decompile data**: `docs/research/decompiled/chunk_*.c` has no matches for sky/SkyObject/Luminosity (confirmed in the prior audit `2026-04-21-sky-deep-audit.md:9-15`).
6. **Our existing Surface.Luminosity uses a fraction** (test shows 0.3f), so there's a strong cross-field analogy that `SkyObjectReplace.Luminosity` is also a fraction, not a percentage.
7. **Our own commit `eeae83a` added the /100 based on a live diag dump** — but we don't have the raw dump file in `docs/research/` and the prior audit `2026-04-22-sky-lighting-decompile.md:130-135` observed "Diag shows luminosity values max at 0.78 (not > 1)" — which is already in the 0..1 range **before** any /100 transform.

**Status: SPECULATIVE.** Two conflicting claims in our own codebase:
- `SkyDescLoader.cs:253-257`: "noon keyframes have Luminosity=100 and Transparent=100" (said to justify /100).
- Prior audit `2026-04-22-sky-lighting-decompile.md:143`: "luminosity values max at 0.78".

These cannot both be true for the same dat. At least one of those observation logs is incorrect — or they were looking at different fields.

**Recommendation for the next decompile pass:** capture a fresh raw-bytes dump of one DayGroup's SkyTimeOfDay.SkyObjReplace list from the live Dereth dat (region 0x13000000), print as hex + parsed float, and pin the unit empirically. Until then, treat the `/100` as "best guess based on one diag observation" rather than a confirmed fact.

### Fields we IGNORE that the dat has

- `SkyObject.Properties` (uint32 flag bits) — we read it into `SkyObjectData.Properties` but never consult it. Retail-faithful rendering probably reads one of these bits for billboard-vs-mesh orientation.
- `SkyObject.DefaultPesObjectId` — we never load the PhysicsScript; retail probably attaches it as a rain/snow particle emitter (r12 §6.1, `deepdives/r12-weather-daynight.md:423-426`).
- `SkyTimeOfDay.WorldFog` (currently mapped to `FogMode` but unused past loading — we don't switch between linear/exp/exp2 per keyframe in our shader).
- `SkyDesc.TickSize` / `LightTickSize` — loaded but not consulted; retail uses these for the lighting-update quantisation rate.

---

## 5. The 7 Sky Objects (Dereth DayGroup)

The `SkyDescLoader.cs` code comment asserts there are 7 sky objects per DayGroup. r12 deepdive (`deepdives/r12-weather-daynight.md:235-237`) says "roughly 4–6 sky objects (one background, one cloud sheet, one sun, one moon, one star sheet)".

### Without a fresh diag dump, we cannot positively assign GfxObjId → role.

What we can say from the primary-day-group pattern and the pipeline:

| Index | Expected role | BeginTime/EndTime pattern | Mesh shape | Blend |
|---|---|---|---|---|
| 0 | Sky dome / gradient background | Begin==End (always visible) | Large hemisphere with baked gradient in V | AlphaBlend or Opaque |
| 1 | Cloud layer (upper) | Begin==End (always) | Large hemisphere with cloud texture, UV-scrolled | AlphaBlend |
| 2 | Cloud layer (lower) | Begin==End (always) | Same shape, different texture | AlphaBlend |
| 3 | Sun | BeginTime ≈ 0.25, EndTime ≈ 0.75 (day) | Small billboard quad with bright body | Additive |
| 4 | Moon | Wraps midnight (Begin > End) | Small billboard quad | Additive |
| 5 | Star dome | Wraps midnight (Begin > End) | Large hemisphere with star texture | Additive |
| 6 | Secondary moon or decoration | varies | billboard | Additive or AlphaBlend |

**This is the expected pattern from r12, NOT a verified per-index dump.** Verifying requires running the client with `ACDREAM_DUMP_SKY=1` or equivalent and logging every SkyObject.

**Action to confirm in a future pass**: write a one-shot CLI or log-dump in `SkyDescLoader.LoadFromRegion` that prints `[i] GfxObjId=0x… BeginTime=… EndTime=… BeginAngle=… EndAngle=… Properties=…` for every sky object in each DayGroup, then capture the output. File it under `docs/research/` as the "Dereth sky object table" so future work has ground truth.

### What the prior audit found but was lost to memory

`2026-04-21-sky-deep-audit.md:128-131` describes a white-sky bug and documents 7 objects but does NOT list their GfxObjIds. The comment at `SkyRenderer.cs:214-218` captures typical ambient colors per keyframe (noon, dusk, midnight, dawn) but says nothing specific about which GfxObj is which role.

---

## 6. Open Questions / Verify Next

1. **Unit of Transparent/Luminosity/MaxBright on SkyObjectReplace.** Strongest evidence (cross-reference to Surface.Luminosity and ACE/ACViewer silence) suggests it's a 0..1 fraction and our `/100` is wrong. Verify with fresh raw-bytes dump.
2. **What is `SkyObject.Properties`?** Retail probably reads a "billboard" bit here. Decompile needed.
3. **Per-SkyObject blend mode — do we classify it correctly?** Current code uses `sm.Translucency == TranslucencyKind.Additive` but only reads the `Additive` (0x10000) flag. If the dat ALSO uses `Luminous` as an additive cue in conjunction with `Alpha` (a combined "luminous alpha mesh"), we'd miss it. Needs a dat dump of every SkyObject → GfxObj → Surface.Type for the 7 Dereth sky objects.
4. **`SkyDesc.TickSize` units.** r12 says day length is 7620 game-ticks. If `TickSize` is seconds-per-tick, it gives total day seconds. Need to verify by reading the value out of the live dat.
5. **`WorldFog` mode — linear only, or do keyframes actually switch to exp?** Our FogMode mapping accepts 1/2/3 but the shader only implements linear. If any retail keyframe uses 2 or 3, we render wrong fog falloff.

---

## 7. References

- DatReaderWriter sky types: `references/DatReaderWriter/DatReaderWriter/Generated/Types/{SkyDesc,SkyObject,SkyTimeOfDay,SkyObjectReplace,DayGroup}.generated.cs`
- DatReaderWriter Region: `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/Region.generated.cs:27-120`
- DatReaderWriter Surface: `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/Surface.generated.cs:27-101`
- DatReaderWriter SurfaceType: `references/DatReaderWriter/DatReaderWriter/Generated/Enums/SurfaceType.generated.cs:13-41`
- DatReaderWriter Polygon: `references/DatReaderWriter/DatReaderWriter/Generated/Types/Polygon.generated.cs:23-87`
- DatReaderWriter ColorARGB: `references/DatReaderWriter/DatReaderWriter/Generated/Types/ColorARGB.generated.cs:22-65`
- DatReaderWriter Surface test fixture: `references/DatReaderWriter/DatReaderWriter.Tests/DBObjs/SurfaceTests.cs:21-45`
- ACE SkyObjectReplace: `references/ACE/Source/ACE.DatLoader/Entity/SkyObjectReplace.cs:5-24`
- ACE Physics Surface: `references/ACE/Source/ACE.Server/Physics/Common/Surface.cs:5-35`
- ACViewer SkyObjectReplace tree: `references/ACViewer/ACViewer/Entity/SkyObjectReplace.cs:14-52`
- WorldBuilder SkyboxRenderManager: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs:180-274`
- acdream loader: `src/AcDream.Core/World/SkyDescLoader.cs`
- acdream renderer: `src/AcDream.App/Rendering/Sky/SkyRenderer.cs`
- acdream translucency classifier: `src/AcDream.Core/Meshing/TranslucencyKind.cs:59-74`
- r12 deep dive: `docs/research/deepdives/r12-weather-daynight.md:135-240, 290-330, 340-400, 430-475`
- Prior audits: `docs/research/2026-04-21-sky-deep-audit.md`, `docs/research/2026-04-22-sky-lighting-decompile.md`
- dats.xml (protocol definition): `references/DatReaderWriter/DatReaderWriter/dats.xml:744-758`
