Redguard Preservation — Documentation
Documented findings from reverse-engineering The Elder Scrolls Adventures: Redguard (1998) — file formats, engine behavior, and undocumented features. Analysis is primarily based on the GOG release (Glide renderer), with original-CD differences noted where known.
GOG Version Contents
Main Files
| File | Size | Description |
|---|---|---|
REDGUARD.EXE | 980 K | Main game executable |
RGFX.EXE | 1.9 M | Glide (3dfx) renderer executable |
DOS4GW.EXE | 260 K | DOS/4GW protected-mode extender |
3DfxSpl2.dll | 1.1 M | 3dfx Splash/Glide support library |
glide2x.dll | 1.3 M | Glide 2.x runtime |
ENGLISH.RTX | 177 M | Dialogue text + voice audio container (largest file in the game) |
REDGUARD.SWG / redguard.swx | 20 K each | Swap/workspace files |
OBJECT.SAV | 4 K | Object state persistence |
*.INI (7 files) | — | Configuration: COMBAT, ITEM, KEYS, MENU, REGISTRY, surface, SYSTEM, WORLD |
*.LOG (14 files) | — | Runtime log files (BITMAP, CAMERA, COMBAT, ERROR, EXIT, GENERAL, GRID, MAINLOOP, MENU, OBJECT, PATH, RAI, SAVEFILE, STARTUP, TESTMAPS) |
*.TXT (3 files) | — | BETHESDA.TXT, CREDITS.TXT, ReadMe.TXT |
Asset Directories
| Directory | Size | Files | Contents |
|---|---|---|---|
fonts/ | 604 K | 29 | 29 .FNT bitmap font files (Arial variants, HI/LO menu fonts, Redguard-styled fonts) |
fxart/ | 102 M | 755 | Glide-version 3D assets: 204 .3DC, 65 .3D, 31 .ROB, 27 .COL, 415 TEXBSI.xxx textures, FOG.INI, more (build manifest from Hugh’s 3D tool) |
maps/ | 12 M | 45 | 27 .RGM scene files, 5 .PVO visibility octrees, 4 .WLD terrain files, 9 .TSG trigger-state files |
sound/ | 5.3 M | 49 | MAIN.SFX (all 118 sound effects), R212.WAV, Miles Sound System drivers (.mdi, .dig), STATE.RST, audio configs |
soup386/ | 48 K | 1 | SOUP386.DEF — script function/flag definitions for the SOUP engine |
system/ | 18 M | 69 | 65 .GXA UI graphics (menus, inventory, compass, maps, skies), gui.anm, gui.lbm, pointers.bmp, SKY_61.PCX |
SAVEGAME/ | 79 M | 594 | 17 save slots (SAVEGAME.000–016), each containing per-map .TSG trigger-state snapshots + LOGBOOK.TXT |
GOG vs Original CD Differences
Redguard shipped with two parallel sets of 3D model assets for different renderers:
| Directory | Renderer | Model versions | Description |
|---|---|---|---|
3dart/ | Software | v2.6, v2.7 | Original software-rendered assets. 120 v2.6 .3DC + 52 v2.7 .3D + 27 v2.7 .3DC. Also contains art_pal.col (the software renderer palette). |
fxart/ | Glide (3dfx) / GOG | v4.0, v5.0 | Glide-accelerated assets used by the GOG release. 204 v4.0 .3DC + 26 v4.0 .3D + 39 v5.0 .3D. 415 TEXBSI texture files, 27 COL palette files. |
The two directories contain the same models re-exported for different renderers. The Glide versions (v4.0/v5.0) have a cleaner header layout and different texture encoding. See 3D — v2.6/v2.7 Header Differences for details.
Note: The GOG distribution contains
fxart/only. The software-renderer3dart/directory shipped on the original CD but is not present in the GOG release.
File Formats Overview
Binary, little-endian file formats.
| File Type | Extension(s) | Parser | Output | Docs | Description |
|---|---|---|---|---|---|
| Sound Effects | .sfx | yes | .wav files (directory extract) | SFX.md | All game sound effects in a single container (MAIN.SFX); 118 raw PCM clips. |
| Dialogue Audio | .rtx | yes | .wav files + index.json (directory extract) | RTX.md | Dialogue text + voice clip container (ENGLISH.RTX) with chunk index footer (RNAV); 4866 entries (3933 voice clips, 933 text-only). |
| Audio | .ogg | — | — | Ogg Vorbis spec | Ogg Vorbis format used for game audio. |
| TEXBSI | .### | yes | .png files (directory extract) + metadata .json | TEXBSI.md | Texture container (TEXBSI.###); indexed-color images with optional palettes and animation. |
| Palette | .col | yes | swatch .png + palette .json | COL.md | 256-color palette files; 776 bytes (8-byte header + 256×RGB). |
| Font | .fnt | yes | .png + BMFont .fnt + glyph .json, or .ttf | FNT.md | Font graphics files—56-byte header + optional palette data. |
| Model | .3d | yes | .glb | 3D | Static 3D models. |
| Animated Model | .3dc | yes | .glb | 3DC | Animated 3D models (multi-frame). |
| ROB Archive | .rob | yes | .glb | ROB.md | Contains world/dungeon model data; used within maps. |
| Map Data | .rgm | yes | .glb + metadata .json | RGM.md | Game map files containing sections for objects, scripts, locations, collisions, etc. |
| World Geometry | .wld | yes | .glb + metadata .json, or map .png set | WLD.md | World geometry/height-map data with 4 sections and 128×128 maps; supports terrain GLB export (and companion RGM merge). |
| Visibility Octree | .pvo | yes | .json | PVO | Pre-computed visibility octree for level geometry culling. |
| Cheat States | .cht | yes | — | CHT.md | Cheat persistence file (REDGUARD.CHT); 256-byte raw dump of 64 u32 LE cheat state slots. |
| SOUP386.DEF | .def | yes | — | SOUPDEF.md | Definition-file format for SOUP callable functions, references/equates, attributes, and global flags. |
3D Model File Format
Binary, little-endian static (single-frame) model format.
.3D files are static (single-frame) models. For the animated multi-frame variant, see 3DC.md.
Versions
Four versions exist: v2.6, v2.7, v4.0, v5.0. See GOG vs Original CD Differences for which directories contain which versions.
- v2.6 / v2.7 — Different header semantics and face data encoding from v4.0/v5.0.
- v5.0 — Adds Section4 (SubObject BVH).
Header (64 bytes)
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | version | ASCII version string, e.g. v4.0 |
| 0x04 | 4 | u32 | num_vertices | Vertex count |
| 0x08 | 4 | u32 | num_faces | Face count |
| 0x0C | 4 | u32 | radius | Collision sphere radius |
| 0x10 | 4 | u32 | num_frames | Animation frame count. Always 1 for .3D files. |
| 0x14 | 4 | u32 | offset_frame_data | Offset to frame data array |
| 0x18 | 4 | u32 | total_face_vertices | Sum of all face vertex counts. Also the entry count for the vertex-normal indirection table. |
| 0x1C | 4 | u32 | offset_section4 | Offset to Section4 (SubObject BVH). 0 when absent. Engine header copier zeroes this field at load time. |
| 0x20 | 4 | u32 | section4_count | Number of Section4 entries. |
| 0x24 | 4 | u32 | unused_24 | Unused — the engine bulk-copies the 64-byte header but never reads this field. Always 0 in v4.0/v5.0. Non-zero in 5 v2.6 files in /3dart (values 4490, 6434), likely leftovers from an older build pipeline. Parsers should ignore. |
| 0x28 | 4 | u32 | offset_normal_indices | Offset to vertex-normal indirection table. Each entry is a u32 file offset into the vertex-normal data section. Count = total_face_vertices. |
| 0x2C | 4 | u32 | offset_vertex_normals | Offset to per-vertex normal data (f32 × 3 per vertex). |
| 0x30 | 4 | u32 | offset_vertex_coords | Offset to vertex coordinate data. |
| 0x34 | 4 | u32 | offset_face_normals | Offset to face normal data. |
| 0x38 | 4 | u32 | total_face_vertices_dup | Duplicate of field at 0x18 in v4.0/v5.0 (engine header copier copies this from file). Different value in v2.6/v2.7 but engine rejects those versions. |
| 0x3C | 4 | u32 | offset_face_data | Offset to face data. Always 64 (0x40) in v4.0/v5.0. Variable (112–3736) in v2.6/v2.7. |
v2.6/v2.7 Header Differences
The header is the same 64 bytes, but many fields have different semantics:
| Field | v4.0/v5.0 meaning | v2.6/v2.7 meaning |
|---|---|---|
| 0x18 | total_face_vertices (count) | Offset to vertex-normal data. Parsers should remap this into the 0x2C slot (vertex-normal offset) and zero this field. The engine also zeroes it during frame construction. Not a vertex count despite sharing the offset with v4.0’s total_face_vertices. |
| 0x1C | offset_section4 (0 in .3DC) | Often non-zero but zeroed by the engine header copier regardless of file value. Not Section4 (SubObject BVH does not exist in v2.6/v2.7). Discarded at load time. |
| 0x24 | Always 0 | Non-zero in 5 files (RICHA001=4490, GARDA001-004=6434) |
| 0x2C | offset_vertex_normals | Unrelated to vertex normals in v2.x files. Acts as a variant marker: 0 for v2.7 .3D, 1 for .3DC (both v2.6 and v2.7). Not an offset — treat as a flag. |
| 0x38 | Duplicate of 0x18 | Does NOT equal 0x18 in v2.7. Copied by the engine header copier. However, the engine rejects v2.6/v2.7 files at load time (version byte < '4'), so this field is only used in practice for v4.0+ where it duplicates 0x18. |
| 0x3C | Always 64 | Variable (112–3736) — face data does NOT start after header |
v2.6/v2.7 Frame Data Differences
The 16-byte frame data record has different field semantics:
| Field | v4.0/v5.0 | v2.6/v2.7 |
|---|---|---|
| offset 0x08 (reserved) | Always 0 | Vertex offset adjustment (values up to 63,662) |
| offset 0x0C (frame_type) | 0, 2, 4, or 8 | Values like 8209 (0x2011), 16402 (0x4012) — different encoding |
v2.6/v2.7 Face Data Differences
| Aspect | v4.0/v5.0 | v2.6/v2.7 |
|---|---|---|
| Face texture header | 5 bytes (u16 + u16 + u8) | 3 bytes (u8 + u16) |
| tex_hi values | 61–91, 255 | 0, 6, 15, 18, 20, 30, 36, 44, 50, 60, 63 |
| Solid-color faces | ~5% of faces (tex_hi=0xFF) | Rare — texture_id < 2 indicates solid color |
| Texture ID decoding | BCD-like: (raw >> 8) - 4000000 | Simple: texture_id = raw >> 7 |
| Vertex index | Direct index | Byte offset (divide by 12) |
Parsers must remap header fields before use. See External References for the remap logic.
Engine version gate: The shipped engine rejects files where the version byte (offset 0x01) is less than ASCII '4' (0x34). This means v2.6/v2.7 .3DC files on the game disc are not loaded at runtime — they are development-era leftovers. The engine only loads v4.0+ files. External parsers (DaveHumphrey, RGUnity, this project) support both versions for preservation completeness.
Section Layout
Sections appear in this order (offsets from header):
- Face Data — at 0x40,
num_facesvariable-size records - Vertex Coordinates —
num_vertices× 12 bytes - Face Normals —
num_faces× 12 bytes - Frame Data —
num_frames× 16-byte records - Section4 — SubObject BVH entries (when present)
- Vertex-Normal Indirection Table —
total_face_vertices×u32 - Vertex Normals —
num_vertices× 12 bytes
Frame Data
Located at offset_frame_data. Array of num_frames × 16-byte records.
For .3D files this is always a single entry pointing to the base geometry.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | vertex_offset | File offset to vertex position data |
| 0x04 | 4 | u32 | normal_offset | File offset to face normal data |
| 0x08 | 4 | u32 | reserved | Always 0 in v4.0/v5.0. Used as vertex offset adjustment in v2.6/v2.7. |
| 0x0C | 4 | u32 | frame_type | 0 for static .3D models in v4.0. See 3DC.md for animated frame types. Different encoding in v2.6/v2.7. |
vertex_offset and normal_offset point to the base vertex coordinate and face normal sections (same values as header fields 0x30 and 0x34).
Face Data
Located at offset_face_data (always 0x40). Array of num_faces variable-size records.
v4.0 / v5.0 Face Record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | vertex_count | Number of vertices in this face (3–10) |
| 0x01 | 1 | u8 | tex_hi | Per-face flags/high byte field (often 0xFF for solid-color faces) |
| 0x02 | 4 | u32 | texture_data_raw | Packed texture encoding value |
| 0x06 | 4 | u32 | unused_04 | Unused — always 0x00000000. Parsers should skip. |
| 0x0A | 8×N | FaceVertex[N] | vertices | N = vertex_count |
Texture Decoding (v4.0 / v5.0)
Solid color (if texture_data_raw >> 20 == 0x0FFF):
color_index = (texture_data_raw >> 8) & 0xFFtex_hiis always 0xFF for solid-color faces.
Textured (otherwise):
-
Texture file ID (BCD-like encoding):
tmp = (texture_data_raw >> 8) - 4000000 ones = (tmp / 250) % 40 tens = ((tmp - ones*250) / 1000) % 100 hundreds = (tmp - ones*250 - tens*1000) / 4000 texture_id = ones + tens + hundredsThis yields the TEXBSI.### file number.
-
Image sub-ID within that TEXBSI file:
ones = (texture_data_raw & 0xFF) % 10 tens = ((texture_data_raw & 0xFF) / 40) * 10 image_id = ones + tens
v2.6 / v2.7 Face Record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | vertex_count | Number of vertices |
| 0x01 | 1 | u8 | u1 | Separate byte field (not part of texture encoding) |
| 0x02 | 2 | u16 | texture_data | Packed texture encoding |
| 0x04 | 4 | u32 | unused_04 | Unused — always 0x00000000. Parsers should skip. |
| 0x08 | 8×N | FaceVertex[N] | vertices | N = vertex_count |
Texture decode from texture_data (the u16 at offset 0x02):
- Solid color if
(texture_data >> 7) < 2:color_index = texture_data & 0xFF - Textured:
texture_id = texture_data >> 7,image_id = texture_data & 0x7F
FaceVertex
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | vertex_index | Index into vertex coordinate array. For v2.6/v2.7: byte offset, divide by 12. For v4.0+: direct index. |
| 0x04 | 2 | i16 | u_delta | U texture coordinate delta |
| 0x06 | 2 | i16 | v_delta | V texture coordinate delta |
UV coordinates are cumulative deltas in 4-bit fixed-point format: each vertex’s absolute U/V = previous vertex’s U/V + this delta. First vertex in the face starts from 0.
The raw i16 values are in 1/16th pixel precision. To convert to pixel-space texture coordinates, multiply by 1/16 (or divide by 16). To normalize to 0–1 UV range, divide by 16 × texture_width (U) and 16 × texture_height (V). This fixed-point scale applies identically to first-vertex values and subsequent deltas.
Face Vertex Count Distribution
| Vertices/Face | Frequency |
|---|---|
| 3 (triangle) | ~93% |
| 4 (quad) | ~6.5% |
| 5–10 | <0.5% |
Vertex Coordinates
Located at offset_vertex_coords. Array of num_vertices × 12 bytes.
Each vertex is 3 × i32 (signed 32-bit integers), scaled by 1/256.0 to get world-space coordinates.
Face Normals
Located at offset_face_normals. Array of num_faces × 12 bytes.
Each normal is 3 × i32, scaled by 1/256.0. Per-face normals (one per face, not per vertex).
Vertex Normals
Located at offset_vertex_normals (header offset 0x2C). Array of num_vertices × 12 bytes.
Each entry is 3 × f32 (IEEE 754 single-precision). Per-vertex normals (unit vectors). Some entries may be NaN (bit pattern 0xFFC00000 for all three components), indicating no normal was computed for that vertex.
Vertex-Normal Indirection Table
Located at offset_normal_indices (header offset 0x28). Array of total_face_vertices × u32.
Each entry is a file offset pointing into the vertex-normal data section. Maps each face-vertex to its vertex normal, allowing faces that share a vertex to use different normals (for hard edges).
Shading Model Selection
Per-vertex shading is determined by the indirection table and vertex normal validity:
| Condition | Shading | Normal used |
|---|---|---|
| Indirection table present, referenced vertex normal is valid | Smooth (Gouraud) | Vertex normal from indirection lookup |
Indirection table absent, vertex_normals[vertex_index] is valid | Smooth (Gouraud) | Vertex normal by direct index |
Vertex normal is NaN (0xFFC00000) or unavailable | Flat | Face normal |
.3D files typically have the indirection table (per-face-vertex normal control). .3DC files always omit it (offset_normal_indices = 0) and fall back to direct vertex-index lookup.
Section4: SubObject BVH
Located at offset_section4 (header offset 0x1C). Contains section4_count variable-size entries. Present when offset and count are non-zero.
Each entry is a bounding-volume node with face references:
| Size | Type | Description |
|---|---|---|
| 4 | i32 | Center X |
| 4 | i32 | Center Y |
| 4 | i32 | Center Z |
| 4 | u32 | Radius |
| 2 | u16 | Face reference count |
| 4 | f32 | Extent X |
| 4 | f32 | Extent Y |
| 4 | f32 | Extent Z |
| 6×N | Face references (u32 offset + u16 index, divide index by 4) |
External References
- UESP: Mod:Model Files — community-maintained model-format notes and version behavior.
- uesp/redguard-3dfiletest
Redguard3dFile.cpp— DaveHumphrey’s C++ parser, including v2.6/v2.7 header remap logic (ConvertHeader27). - RGUnity/redguard-unity
RG3DFile.cs— production parser implementation used by the Unity reimplementation (cross-check for face/texture decoding behavior).
3DC Animated Model File Format
Animated variant of the 3D model format. Same binary layout — identical header and section structure — but with multiple animation frames.
Differences from .3D
| Aspect | .3D | .3DC |
|---|---|---|
| Frames | Always 1 | 2+ (animated) |
| frame_type (frame 0) | 0 | 2, 4, or 8 |
| Animated frame data | None | Frames 1+ with compressed or full-precision geometry |
| Section4 (SubObject BVH) | Present (v5.0) | Always absent (offset=0, count=0) |
| Vertex-normal indirection table | Present | Always absent (offset=0) |
| Version | v4.0 or v5.0 | Always v4.0 |
All shared structures (header, face data, vertex coordinates, face normals, vertex normals, texture encoding) are documented in 3D.md.
Animation Frame Data
The frame data array at offset_frame_data contains num_frames × 16-byte records (see 3D.md — Frame Data for the record layout).
Frame 0 always points to the base geometry (same as .3D files). Frames 1+ contain per-frame vertex positions and face normals in a compact encoding determined by frame_type:
frame_type Values
| Value | Meaning | Frame 1+ vertex encoding | Frame 1+ normal encoding |
|---|---|---|---|
| 2 | Compressed animation | i16 × 3 (6 bytes/vertex) | 10-10-10-2 packed (4 bytes/face) |
| 4 | Full-precision animation | i32 × 3 (12 bytes/vertex) | 10-10-10-2 packed (4 bytes/face) |
| 8 | Static (single-frame .3DC) | N/A | N/A |
frame_type is only meaningful in frame 0’s record. Frame 1+ records always have frame_type = 0.
10-10-10-2 Packed Normal Format
Used for face normals in frames 1+:
Bits 0- 9: nx (10-bit signed, subtract 1024 if >= 512)
Bits 10-19: ny (10-bit signed, subtract 1024 if >= 512)
Bits 20-29: nz (10-bit signed, subtract 1024 if >= 512)
Bits 30-31: unused (values: 0 and 3)
Each component is divided by 256.0 to produce the final normal vector.
The engine’s normal decoder extracts the three 10-bit signed components via sign-extending shifts and discards bits 30–31:
nx = (float)((packed << 22) >> 22) * scale; // bits 0–9, sign-extended
ny = (float)((packed << 12) >> 22) * scale; // bits 10–19, sign-extended
nz = (float)((packed << 2) >> 22) * scale; // bits 20–29, sign-extended
// bits 30–31 are shifted out by << 2 — never read
The values (0 and 3) are likely packing artifacts — 3 (0b11) can result from sign extension during the build tool’s encoding step. Parsers should mask or discard these bits.
Section Layout
Same as 3D.md — Section Layout, but with animated frame data inserted and .3D-only sections absent:
- Face Data — at 0x40
- Vertex Coordinates — base frame positions
- Face Normals — base frame normals
- Frame Data —
num_frames× 16-byte records - Animated Frame Vertex/Normal Data — frames 1+ geometry
- Vertex Normals — per-vertex normals (f32 × 3)
External References
- UESP: Mod:Model Files — baseline notes for shared
.3D/.3DCstructures and version differences. - RGUnity/redguard-unity
RG3DFile.cs— practical reference for animated-frame decoding paths used in a full game implementation.
ROB File Format
Binary container format. Holds multiple 3D model segments — either embedded inline or as references to external .3DC files.
ROB stores model geometry buckets, not per-instance scene placement transforms. World/object placement comes from scene files (RGM), which reference these models.
Overall Structure
[Header — 20 bytes]
[Segment 0 — 80-byte header + data]
[Segment 1 — 80-byte header + data]
...
[Segment N-1]
[Footer — 4 bytes: "END "]
Some fields use big-endian encoding (a remnant of the original Sega Saturn development), while most use little-endian. Endianness is noted per field.
Header (20 bytes)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | magic | "OARC" — possibly “Object ARChive”; exact expansion unknown. |
| 0x04 | 4 | u32 | BE | unused_04 | Always 4. Unused at runtime — the engine reads the file but never dereferences or tests this field. Likely a build-tool artifact. |
| 0x08 | 4 | u32 | LE | num_segments | Number of segments. |
| 0x0C | 4 | [u8; 4] | — | magic2 | "OARD" — possibly “Object ARchive Data”; exact expansion unknown. |
| 0x10 | 4 | u32 | BE | payload_size | File size minus 24 (= file size - 20-byte header - 4-byte footer). |
Segment Header (80 bytes)
Each segment has a fixed 80-byte header followed by data_size bytes of payload.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | u32 | LE | total_size | Data size + 80 (total segment size including header). |
| 0x04 | 8 | [u8; 8] | — | name | Segment name, ASCII null-padded. Model name or external .3DC filename stem. |
| 0x0C | 2 | u16 | LE | segment_type | 0 = embedded 3D data, 512 = external .3DC reference. See below. |
| 0x0E | 2 | u16 | LE | segment_flags | Render mode flags. Only the high byte (at file offset 0x0F) is used at runtime — it becomes a render mode selector. A value of 0 defaults to 0xFF (normal rendering). See Segment Flags below. |
| 0x10 | 1 | u8 | — | segment_attribs | Per-segment attribute flags. Only this single byte is read at runtime. Bit 1 (0x02) triggers texture pre-loading. Value 0x40 marks special objects (inventory items, shop objects). See Segment Attributes below. |
| 0x11 | 3 | — | — | face_count_low | Build tool artifact: the last byte (0x13) equals face_count mod 256. Not read at runtime. |
| 0x14 | 4 | u32 | BE | unused_14 | Unused — never read at runtime. Most commonly 1 (3,690 segments), but other values exist. |
| 0x18 | 4 | u32 | — | reserved_18 | Always 0. |
| 0x1C | 4 | u32 | LE | bbox_extent_x | Bounding box total X extent. |
| 0x20 | 4 | u32 | LE | bbox_extent_y | Bounding box total Y extent. |
| 0x24 | 4 | u32 | LE | bbox_extent_z | Bounding box total Z extent. |
| 0x28 | 4 | u32 | — | reserved_28 | Always 0. |
| 0x2C | 4 | u32 | — | reserved_2C | Always 0. |
| 0x30 | 4 | u32 | — | reserved_30 | Always 0. |
| 0x34 | 4 | u32 | LE | bbox_positive_x | Positive X extent from center. |
| 0x38 | 4 | u32 | LE | bbox_positive_y | Positive Y extent from center. |
| 0x3C | 4 | u32 | LE | bbox_positive_z | Positive Z extent from center. |
| 0x40 | 4 | u32 | LE | bbox_negative_x | Negative X extent from center. |
| 0x44 | 4 | u32 | LE | bbox_negative_y | Negative Y extent from center. |
| 0x48 | 4 | u32 | LE | bbox_negative_z | Negative Z extent from center. |
| 0x4C | 4 | u32 | LE | data_size | Byte count of the data payload that follows. 0 for external references. |
Bounding Box Invariant
bbox_extent == bbox_positive + bbox_negative for all three axes.
For symmetric models: bbox_positive == bbox_negative (center at origin). For asymmetric models, the difference encodes the center offset.
Segment Types
| Type | Description |
|---|---|
| 0 | Embedded 3D model data. Payload is a complete 3D file (v5.0 format). |
| 256 | Embedded 3D model data (menu-specific). Only in MENU.ROB. Structurally identical to type 0 — payload is a complete 3D file. Uses versions v4.0 and v4.02 (v4.02 is unique to these segments). |
| 512 | External reference. name is the .3DC filename stem (e.g. "CYRSA001" → CYRSA001.3DC). data_size is 0. |
Segment Flags (0x0E)
The engine reads only the high byte (file offset 0x0F) of this u16 field. The low byte is always 0x00 and is ignored. The high byte is stored as a render mode selector in the model’s internal data — a value of 0 defaults to 0xFF (standard rendering).
| Value | High byte | Names | Render mode |
|---|---|---|---|
| 0x0000 | 0x00 → 0xFF | (all normal) | Standard (default) |
| 0x8C00 | 0x8C | DR_WA01, DR_WA02, LH_MIRR | Transparency / mirror |
| 0xC800 | 0xC8 | WATERWAT | Water |
| 0x8000 | 0x80 | VR_OHT | Stored as render-mode metadata |
| 0x5A00 | 0x5A | BEAMA001 | Beam / light effect |
Segment Attributes (0x10)
A single byte of per-segment attribute flags.
| Value | Segments | Meaning |
|---|---|---|
| 0x00 | (all normal) | No special attributes |
| 0x02 | PALMTR01–04 (CAVERNS, EXTPALAC, ISLAND) | Texture pre-load trigger (bit 1). Engine calls a texture pre-caching function for all face textures in this segment. |
| 0x40 | SS_OBJ01–06, IGRING, IWATER1 (shop items, inventory) | Special object flag (bit 6). Stored as model metadata. |
Segment Data
For segment_type == 0 (embedded): the data payload is a complete 3D model file starting with its own 64-byte header. Parse with the standard 3D parser.
For segment_type == 256 (menu-specific embedded): same as type 0 — payload is a complete 3D model file. Parse identically. See MENU.ROB Segments below.
For segment_type == 512 (external reference): no data payload. Load the referenced .3DC file from the asset directory using name as the filename stem. External references are exclusively .3DC (animated models) — static .3D geometry is always embedded inline as type 0 segments, never referenced externally.
MENU.ROB Segments
MENU.ROB is the only ROB file containing type 256 segments. It holds 3D models used for the game’s menu screens.
| # | Name | Type | Size | Version | Notes |
|---|---|---|---|---|---|
| 0 | MENUA001 | 256 | 79,328 | v4.0 | Menu character model (also exists as standalone MENUA001.3DC in /fxart) |
| 1 | MB_TABLE | 0 | 886 | v5.0 | Small prop |
| 2 | MB_PG01 | 256 | 42,208 | v4.02 | Menu page model |
| 3 | MB_PG02 | 256 | 42,208 | v4.02 | Menu page model (same size as PG01) |
| 4 | MB_PG03 | 256 | 42,208 | v4.02 | Menu page model (same size as PG01) |
| 5 | SCROLL | 0 | 8,136 | v5.0 | Scroll decoration prop |
Version v4.02 appears only in these three MB_PG segments — it is not found in any other ROB file or standalone 3D/3DC file.
Footer
4-byte ASCII marker: "END " (with trailing space). Always present.
External References
- UESP: Mod:ROB File Format — high-level ROB structure and historical notes.
- RGUnity/redguard-unity
RGROBFile.cs— segment parsing behavior in a working loader.
RGM Scene File Format
Scene/map container with sectioned records for placed objects, script metadata, and auxiliary world data. The RA*-prefixed sections (RASC, RAHD, RAAT, RAHK, etc.) form the per-map SOUP scripting layer — see SOUP Scripting for a consolidated map of all script data sources and runtime boundaries.
Section Framing
Each section starts with an 8-byte header. Some sections then include a 4-byte little-endian record_count word at the beginning of section data.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | section_name | ASCII section tag (for example RAHD, MPOB, MPSO, END ) |
| 0x04 | 4 | u32 | BE | data_length | Payload size in bytes (0 for END ). Big-endian in section-framed formats (RGM, PVO, ROB, TEXBSI). |
For count-prefixed sections (MPOB, MPSO, MPRP, and several others), section payload begins with:
Little-endian.
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | record_count | Number of fixed-size records in this section |
Sections are parsed sequentially until END .
MPOB (Object Instances)
MPOB starts with a little-endian object count, followed by 66-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 1 | u8 | object_type | Object kind discriminator |
| 0x05 | 1 | u8 | is_active | Activation flag |
| 0x06 | 9 | [u8; 9] | script_name | Script/object name |
| 0x0F | 9 | [u8; 9] | model_name | Model reference |
| 0x18 | 1 | u8 | is_static | Static/dynamic flag |
| 0x19 | 2 | i16 | reserved | Not read at runtime. |
| 0x1B | 3 | i24 | pos_x | Position X (fixed scale) |
| 0x1E | 1 | u8 | pad_x | Alignment byte |
| 0x1F | 3 | i24 | pos_y | Position Y (fixed scale) |
| 0x22 | 1 | u8 | pad_y | Alignment byte |
| 0x23 | 3 | u24 | pos_z | Position Z (fixed scale) |
| 0x26 | 4 | u32 | angle_x | Bethesda 2048-unit Euler angle |
| 0x2A | 4 | u32 | angle_y | Bethesda 2048-unit Euler angle |
| 0x2E | 4 | u32 | angle_z | Bethesda 2048-unit Euler angle |
| 0x32 | 2 | i16 | texture_data | Packed texture id/image id |
| 0x34 | 2 | i16 | intensity | Light/intensity-like field |
| 0x36 | 2 | i16 | radius | Radius-like field |
| 0x38 | 2 | i16 | model_id | Model index/id-like field |
| 0x3A | 2 | i16 | world_id | World index/id-like field |
| 0x3C | 2 | i16 | red | Color channel |
| 0x3E | 2 | i16 | green | Color channel |
| 0x40 | 2 | i16 | blue | Color channel |
Position decode used by current exporter:
- scale constant:
1 / 5120 x = -(pos_x * 256) * scaley = -(pos_y * 256) * scalez = -(0x00FF_FFFF - (pos_z * 256)) * scale
Bethesda 2048-unit Euler angles:
2048 discrete units represent a full 360° rotation — a power-of-two binary angle encoding. The raw u32 is reduced modulo 2048 (equivalently masked with & 0x7FF), giving a value in the range [0, 2047]. Each unit equals 180/1024 ≈ 0.176°.
degrees = (value % 2048) * (180.0 / 1024.0)
| Units | Degrees |
|---|---|
| 0 | 0° |
| 512 | 90° |
| 1024 | 180° |
| 1536 | 270° |
| 2048 | 360° (wraps to 0) |
MPOB model lookup behavior:
- Primary source is
model_name(9 bytes, null-trimmed). - If
model_nameis empty, exporter falls back toRAANusingRAHDscript metadata (see RAAN). - If RAAN also yields no result,
script_nameis used as a last resort. - Example:
script_name = FAVISresolves via RAAN toFVPRA001.
MPSO uses a 12-byte model_name field (no fallback chain — model name is always present).
MPSO (Static Objects)
MPSO starts with a little-endian object count, followed by 66-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 12 | [u8; 12] | model_name | Model reference |
| 0x10 | 3 | i24 | pos_x | Position X |
| 0x13 | 1 | u8 | pad_x | Alignment byte |
| 0x14 | 3 | i24 | pos_y | Position Y |
| 0x17 | 1 | u8 | pad_y | Alignment byte |
| 0x18 | 3 | u24 | pos_z | Position Z |
| 0x1B | 1 | u8 | pad_z | Alignment byte |
| 0x1C | 36 | i32[9] | rotation_matrix | 3x3 Q4.28 rotation matrix |
| 0x40 | 2 | u8[2] | unused | Always 0. |
The exporter converts rotation_matrix from Q4.28 to float and emits a node matrix with translation.
Rotation parity note:
- MPSO
rotation_matrixmust be interpreted with transposed index mapping when building the scene rotation matrix:- row 0 =
[m0, m3, m6] - row 1 =
[m1, m4, m7] - row 2 =
[m2, m5, m8]
- row 0 =
- Earlier row-major mapping (
[m0,m1,m2],[m3,m4,m5],[m6,m7,m8]) produced incorrect static-object orientation for cases likeTV_SEATandBT_BOARD.
MPRP (Rope Chains)
MPRP starts with a little-endian record count, followed by 80-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Rope/object id |
| 0x04 | 1 | u8 | reserved | Not read at runtime. |
| 0x05 | 3 | i24 | pos_x | Base position X |
| 0x08 | 1 | u8 | pad_x | Alignment byte |
| 0x09 | 3 | i24 | pos_y | Base position Y |
| 0x0C | 1 | u8 | pad_y | Alignment byte |
| 0x0D | 3 | i24 | pos_z | Base position Z |
| 0x10 | 4 | i32 | angle_y | Rope heading field |
| 0x14 | 4 | i32 | type | Type/discriminator |
| 0x18 | 4 | i32 | swing | Swing parameter |
| 0x1C | 4 | i32 | speed | Speed parameter |
| 0x20 | 2 | i16 | length | Number of rope links |
| 0x22 | 9 | [u8; 9] | static_model | Optional terminal model |
| 0x2B | 9 | [u8; 9] | rope_model | Link model name (for example ROPELINK) |
| 0x34 | 28 | i32[7] | reserved | Not read at runtime. |
Rope instancing behavior:
- Decode base translation with the same MPOB scale/sign rules.
- Spawn
lengthcopies ofrope_model. - For each link: subtract
0.8from Y and place one instance. - If
static_modelis present, place one additional instance after the chain.
Current parser behavior: MPRP is parsed into typed 80-byte records only when section payload is an exact fit for record_count; otherwise raw fallback is kept.
RALC (Location Data)
RALC contains scripted coordinate offsets for objects (e.g. the Boatman’s waypoints). Records are 12-byte entries.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | s32 | offset_x | X coordinate offset (applied to MPOB translated position) |
| 0x04 | 4 | s32 | offset_y | Y coordinate offset |
| 0x08 | 4 | s32 | offset_z | Z coordinate offset |
Offsets are applied to the object’s base MPOB position (pos × 256) by script commands MoveToLocation and WanderToLocation. Per-object RALC entry counts and offsets are stored in the corresponding RAHD record.
RAVC (VCollide)
RAVC uses 9-byte entries and appears only in a subset of maps (CATACOMB and DRINT — collision data for the dragon and golem).
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | i8 | offset_x | Local collision offset X |
| 0x01 | 1 | i8 | offset_y | Local collision offset Y |
| 0x02 | 1 | i8 | offset_z | Local collision offset Z |
| 0x03 | 2 | u16 | vertex | Model vertex index used as reference point for the collision sphere |
| 0x05 | 4 | u32 | radius | Collision sphere radius |
Current parser behavior: records are parsed as fixed 9-byte entries only when section payload is an exact fit; otherwise raw fallback is kept. RAVC is flat-out missing (not just empty) in RGM files without collision objects.
WDNM (Walk Node Map)
WDNM defines walk node maps for AI pathfinding. Count-prefixed: record count is the number of walk-map blocks.
All fields little-endian.
WalkMap Record
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | map_length | Total byte length of this walk-map |
| +0x04 | 4 | u32 | node_count | Number of walk-nodes in this map |
| +0x08 | 4 | u32 | node_count_dup | Duplicate of node_count |
| +0x0C | 3 | s24 | map_pos_x | Map position X |
| +0x0F | 1 | u8 | pad_x | Alignment byte |
| +0x10 | 3 | s24 | map_pos_y | Map position Y |
| +0x13 | 1 | u8 | pad_y | Alignment byte |
| +0x14 | 3 | s24 | map_pos_z | Map position Z |
| +0x17 | 1 | u8 | pad_z | Alignment byte |
| +0x18 | 4 | u32 | radius | Map bounding radius |
| +0x1C | … | variable | walk_nodes | node_count × WalkNode records |
WalkNode Record
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | node_length | Total byte length of this walk-node |
| +0x04 | 2 | u16 | node_pos_x | Local position X |
| +0x06 | 2 | s16 | node_pos_y | Local position Y |
| +0x08 | 2 | u16 | node_pos_z | Local position Z |
| +0x0A | 1 | u8 | reserved | Not read at runtime. |
| +0x0B | 1 | u8 | route_count | Number of routes from this node |
| +0x0C | … | variable | routes | route_count × NodeRoute records |
NodeRoute Record (4 bytes)
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 2 | u16 | target_node_id | Destination walk-node index |
| +0x02 | 2 | u16 | cost | Route traversal cost |
RAHD (Actor Header)
RAHD is a count-prefixed section with 165-byte records. Each record provides per-actor metadata: script name, bytecode location in RASC, string/variable table pointers, animation references, collision data, and attribute hooks.
Section payload starts with a 4-byte LE record count, followed by 4 fixed bytes (1B 80 37 00), then count × 165 bytes of records. The engine reads the count and prefix separately, then bulk-reads count × 165 bytes as the record array. Offsets below are within each 165-byte record (starting at payload offset 8 + i × 165). The Rust parser in this repo starts records 4 bytes earlier (at 4 + i × 165) and adds 4 to all field offsets.
At load time, the engine converts most offset fields into absolute pointers by adding the corresponding section’s data pointer (rebasing). Offset 0x00 is overwritten with a linked-list next-pointer at runtime.
All typed fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | — | field_00 | Overwritten at runtime (linked-list next-pointer) |
| 0x04 | 9 | [u8; 9] | script_name | Script/actor name, null-padded |
| 0x0D | 2 | u16 | instances | Number of instances for this actor |
| 0x0F | 2 | — | padding | Always 0. |
| 0x11 | 4 | i32 | instance_counter | Runtime instance counter (incremented during setup; advances variable_offset by num_variables × 4 per instance) |
| 0x15 | 4 | u8[4] | anim_speed | Read as individual bytes. Byte 0x15: frame limit — animation advances only while frame_counter < byte_0x15. Byte 0x16: frame increment added per tick. |
| 0x19 | 4 | u32 | ranm_offset | Byte offset into RANM section data (rebased to pointer at load) |
| 0x1D | 4 | u32 | raat_offset | Byte offset into RAAT section data (rebased to pointer at load) |
| 0x21 | 4 | i32 | raan_count | Number of RAAN entries for this actor |
| 0x25 | 4 | u32 | raan_data_size | Total byte size of this actor’s RAAN entries. Zero when raan_count = 0. |
| 0x29 | 4 | i32 | raan_offset | Byte offset into RAAN section data (rebased to pointer at load) |
| 0x2D | 4 | — | anim_control_prefix | Low byte stored at animation control struct offset +0x12 during RAGR loading. Remaining bytes are not decoded. |
| 0x31 | 4 | u32 | ragr_offset | Byte offset into RAGR section data (rebased to pointer at load) |
| 0x35 | 8 | — | padding | Always 0. |
| 0x3D | 4 | u32 | rafs_index | Index into RAFS data (rebased: rafs_data + index × 11; RAFS records are 11 bytes) |
| 0x41 | 4 | u32 | num_strings | Number of strings used by this actor’s script |
| 0x45 | 4 | — | padding | Always 0. |
| 0x49 | 4 | u32 | string_offsets_index | Byte offset into RASB section data |
| 0x4D | 4 | u32 | script_length | Byte length of this actor’s bytecode block in RASC |
| 0x51 | 4 | u32 | script_data_offset | Byte offset into RASC section data (rebased to pointer at load) |
| 0x55 | 4 | u32 | script_pc | Execution start address; rebased at load to script_data_offset + script_pc (absolute pointer) |
| 0x59 | 4 | — | anim_buffer_swap | Read as byte at 0x59. Boolean: non-zero triggers animation frame buffer swap (copies between offsets +0xCB and +0x108 in actor struct). Zero uses primary buffer only. |
| 0x5D | 4 | u32 | rahk_offset | Byte offset into RAHK section data (rebased to pointer at load) |
| 0x61 | 8 | — | dialogue_lock | Read as byte at 0x61. Set during dialogue initiation; prevents animation transitions while dialogue is active. Checked alongside actor-type and combat-state guards. |
| 0x69 | 4 | u32 | ralc_offset | Byte offset into RALC section data (rebased: ralc_data + (offset ÷ 12) × 12) |
| 0x6D | 4 | u8[4] | actor_flags | Read as individual bytes. Byte 0x6D: animation state ID loaded into a global during dialogue setup, compared against hook data at +0x247 for state matching. Byte 0x6E: item/equipment flag (toggled at runtime). Byte 0x6F: passed to animation/sound function. |
| 0x71 | 4 | u32 | raex_offset | Byte offset into RAEX section data (rebased to pointer at load) |
| 0x75 | 4 | u32 | num_variables | Number of local variables for this actor |
| 0x79 | 4 | u8[4] | visibility_flags | Read as individual bytes. Byte 0x79: visibility test bypass (non-zero = always visible, skip LOD culling). Byte 0x7B: LOD culling mode (0 = fixed distance threshold, non-zero = dynamic distance threshold). |
| 0x7D | 4 | u32 | variable_offset | Byte offset into RAVA section data (÷ 4 = variable array index) |
| 0x81 | 4 | u32 | variable_offset_dup | Runtime copy of variable_offset; advanced by num_variables × 4 per instance |
| 0x85 | 4 | u32 | anim_frame_data | Animation frame count or group index. Upper 16 bits used as count (× 11 bytes per frame for allocation). |
| 0x89 | 4 | i32 | soup_func_primary | SOUP386 function table index (primary). Multiplied by 49 to index into function table. -1 = disabled. |
| 0x8D | 4 | i32 | soup_func_secondary | SOUP386 function table index (secondary). Same indexing. -1 = disabled. |
| 0x91 | 4 | i32 | soup_func_tertiary | SOUP386 function table index (tertiary). -1 = disabled. |
| 0x95 | 2 | i16 | combat_flag | Combat/state flag. |
| 0x97 | 2 | i16 | raex_stat | Stored at actor state +0x97 after SOUP function lookup. -1 = disabled. |
| 0x99 | 2 | i16 | reserved_99 | Always -1. Not read at runtime. |
| 0x9B | 2 | i16 | reserved_9b | Always 0. Not read at runtime. |
| 0x9D | 4 | i32 | ravc_offset | Byte offset into RAVC section data (rebased to pointer at load; -1 = none) |
| 0xA1 | 4 | i32 | ravc_count | Number of RAVC collision entries for this actor |
Total record size: 165 bytes (0xA5).
RAAN (Animation File References)
RAAN contains animation/model file path entries. Records are variable-length null-terminated strings with a 6-byte prefix.
Entry structure at a given byte offset (from RAHD raan_offset):
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | reserved | Not read by any engine function. Skipped during entry iteration. |
| 0x04 | 1 | u8 | frame_count | Used as loop count for animation handle table entries. Capped at 255. |
| 0x05 | 1 | u8 | model_type | Type flag, converted to lowercase at load. Values: 0x63 (ASCII ‘c’) and 0x73 (ASCII ‘s’). |
| 0x06 | var | [u8] | file_path | Null-terminated file path string (e.g. 3dart\cyrsa001.3d) |
The engine iterates RAAN entries by skipping the 6-byte prefix, then scanning forward to the null terminator of file_path. The 4-byte dword at offset 0x00 is NOT used for seeking — the next entry is found purely by string scan.
The model name is extracted by stripping directory separators, file extension, and uppercasing the stem.
Model Fallback Resolution
When an MPOB record has an empty model_name, the exporter resolves a model via RAHD/RAAN:
- Look up
script_namein the RAHD index to get(raan_offset, raan_count). - Parse the RAAN entry at
raan_offsetto extract the file path. - Strip the path to a bare filename stem (e.g.
fxart\FVPRA001.3DC→FVPRA001). - Use the stem as the model name for asset lookup.
- If no RAHD/RAAN match, fall back to using
script_nameas the model name.
RAFS (FSphere)
RAFS contains bounding-sphere data for actors. Records are 11 bytes each (RAHD rafs_index rebases as rafs_data + index × 11). Internal per-field layout is not decoded. The engine only loads this section if its size exceeds 10 bytes.
RAST (String Data)
RAST contains all script string literals as null-terminated strings concatenated into a single blob. No count prefix; it is a flat byte array. Individual strings are located by offsets stored in RASB.
During loading, RASB offsets are rebased by adding the RAST data pointer, converting relative offsets into direct pointers.
RASB (String Offset Table)
RASB contains u32 LE offsets into RAST, one per string per actor. Each actor’s portion starts at string_offsets_index (from RAHD) and contains num_strings entries.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | string_offset | Byte offset into RAST where the null-terminated string begins |
Total section length: sum of all actors’ num_strings × 4.
RAVA (Local Variables)
RAVA contains initial values for local script variables as a flat array of i32 LE integers. The first 4 bytes are always zero (sentinel).
Each actor’s portion starts at byte offset variable_offset (from RAHD; divide by 4 for the array index) and contains num_variables entries. When an actor has multiple instances, variables are replicated instances times. Variables are addressed by index in script bytecode (opcode 0x0A).
RASC (Script Bytecode)
RASC contains compiled SOUP386 scripting bytecode as a contiguous byte blob. Per-actor instruction blocks are located via RAHD offsets (script_data_offset, script_length, script_pc) and executed by the SOUP virtual machine.
For VM architecture, opcode encoding, operator tables, and execution semantics, see SOUP Scripting. For the definition-file format, see SOUP386.DEF.
Section Layout
| Region | Size | Description |
|---|---|---|
| Preamble | script_data_offset bytes | Zero-padded address space used by rebased hook/offset references |
| Script blocks | remainder | Concatenated per-actor bytecode blocks in RAHD order |
Total payload = first actor script_data_offset + sum of all actors’ script_length values. For each actor, execution starts at script_pc relative to that actor’s block.
Other RA* sections (for example RAHK, RALC, RAVC) provide data referenced by RASC, often as u32 offset arrays rebased to loaded script memory.
RAHK (Hook Data)
RAHK contains hook offset tables. Entries are u32 LE offsets that are rebased against the RASC bytecode base address at load time, enabling scripts to register named entry points (hooks) that external events can invoke.
Per-actor hook counts and offsets are stored in the corresponding RAHD record. Entries are accessed at base + index × 4 + 0x25 — the 0x25-byte region before the offset array is a section header. The upper 16 bits of each u32 entry are extracted separately (>> 0x10) as a secondary field. No additional per-entry structure beyond the u32 offset array exists.
RAEX (Extra Data)
RAEX contains per-actor extra data with 30-byte fixed-size records (15 × i16 LE fields). The engine requires this section during loading. Record count is section_data_length ÷ 30. Per-actor RAEX offsets are stored in RAHD (raex_offset).
Field names Grip0 through RangeMax are from the in-game debug console.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | i16 | grip0 | Named from console. Used as animation frame offset during weapon transitions (consumed by the combat animation subsystem, not the attachment vertex system). |
| 0x02 | 2 | i16 | grip1 | Same subsystem as grip0. |
| 0x04 | 2 | i16 | scabbard0 | Named from console. Same subsystem as grip0. |
| 0x06 | 2 | i16 | scabbard1 | Same subsystem as scabbard0. |
| 0x08 | 2 | i16 | anim_frame_ref | Matches RAHD anim_frame_data at 0x85. Set on mobile actors only. |
| 0x0A | 2 | u16 | texture_id | Texture override id for actor skin variants. |
| 0x0C | 2 | i16 | v_vertex | Vertex-related field. |
| 0x0E | 2 | i16 | v_size | Size-related field. |
| 0x10 | 2 | i16 | taunt_id | First taunt animation id; additional taunts count up from this value. |
| 0x12 | 2 | i16 | field_12 | Set only on large creatures (dragon, gremlin). |
| 0x14 | 2 | i16 | field_14 | Source value for RAHD raex_stat at 0x97. Set on combat actors. |
| 0x16 | 2 | i16 | field_16 | Set on some combat actors. |
| 0x18 | 2 | i16 | range_min | Combat engagement minimum range. Multiply by 256 for world units. Only set on dragon, golem, serpent. |
| 0x1A | 2 | i16 | range_ideal | Combat ideal range. Same scaling. |
| 0x1C | 2 | i16 | range_max | Combat maximum range. Same scaling. |
Total record size: 30 bytes (0x1E). Record count is section_data_length / 30.
RAAT (Attribute Data)
RAAT contains per-actor attribute tables. Each actor has a 256-byte attribute block, ordered sequentially (actor 0 at offset 0, actor 1 at offset 256, etc.).
Attribute names are defined in the auto…endauto section of SOUP386.DEF (see SOUP386.DEF). Each byte is a named attribute value; zero means unset. Attributes are read/written by the script functions GetAttribute and SetAttribute.
Total section length: record_count × 256.
RAGR (Animation Groups)
RAGR contains animation group definitions that link actors to their animation data in RAAN. RAGR provides the RGM-embedded equivalent of the AIAN section found in standalone .AI files; the engine selects one or the other source based on a runtime mode flag.
Per-actor RAGR data is located via RAHD.ragr_offset (offset 0x31). Entries are size-prefixed: the first u16 is the entry payload size (excluding itself); a value of 0 terminates the list. Advance to the next entry: current_position + 2 + entry_size.
The prefix byte used by the AIAN (standalone .AI) path is NOT present in RGM RAGR — instead, that value comes from RAHD offset 0x2D.
Animation Group Entry
All fields little-endian.
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 2 | u16 | entry_size | Payload size in bytes after this field. 0 = end of groups. Should equal 8 + frame_count × 3. |
| +0x02 | 2 | u16 | group_index | Animation group slot (0–177; validated ≤ 0xB1 at load) |
| +0x04 | 2 | u16 | anim_id | Animation identifier |
| +0x06 | 2 | u16 | anim_type | Animation type (only low byte used). Values: 0 = interruptible (idle/panic), 1 = must complete (combat), 2 = no panic revert (ledge-hang loops). |
| +0x08 | 2 | u16 | frame_count | Number of animation frames in this group |
| +0x0A | var | [u8; frame_count × 3] | commands | Packed 3-byte animation commands, one per frame |
In ISLAND.RGM, Cyrus (RAHD record 22, ragr_offset=1318) has 152 animation groups. 58 groups contain attachment commands (opcode 0/4/10 with non-zero vertex index). Vertex 1 = hand attachment (sword combat), vertex -10 = scabbard attachment.
Animation Command (3 bytes, packed LE)
Each command is a 24-bit little-endian packed value. The low 4 bits select the opcode type, which determines how the remaining 20 bits are allocated to parameters.
Opcode 0 (ShowFrame) — the only opcode that sets the attachment vertex:
byte 0 byte 1 byte 2
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
├─hdl─┤ ├─op──┤ ├v┤ ├──handle─┤ ├───vertex────┤
opcode = byte0 & 0x0F (4 bits)
handle_index = (byte0 >> 4) | ((byte1 & 0x3F) << 4) (10-bit signed)
vertex_index = (byte1 >> 6) | (byte2 << 2) (10-bit signed)
Both handle_index and vertex_index are 10-bit sign-extended values (range −512..+511). The handle_index is a relative index into the per-actor animation handle lookup table (built from RAAN entries at load time; patched to absolute runtime handles during loading). The vertex_index identifies which vertex to track for item attachment — see Item Attachment System.
Opcodes 4 (PlaySound) and 10 (ChangeAnimGroup) share the same 10+10 bit layout but their parameters are NOT handle/vertex — they are sound params and animation jump targets respectively. See attachment.md for the full 16-opcode table with names, bit layouts, and playback behavior.
RANM (Namespace)
RANM contains object namespace strings used for cross-script object references. Each actor’s portion is located by offset and length fields stored in RAHD (within the undecoded gap at 0x5D). The extracted string provides the actor’s canonical name for ObjDot* opcodes using selector byte 4 (named object from string table).
MPSL (Lights)
MPSL starts with a little-endian record count, followed by 42-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 3 | u8[3] | color_rgb | Color bytes (R, G, B). |
| 0x03 | 1 | u8 | light_type | Light type. Values: 0, 130 (0x82), 131 (0x83), 132 (0x84). |
| 0x04 | 4 | u32 | light_param | Zero for ambient lights; 28 for directional lights. |
| 0x08 | 3 | i24 | pos_x | Position X |
| 0x0B | 1 | u8 | pad_x | Alignment byte |
| 0x0C | 3 | i24 | pos_y | Position Y |
| 0x0F | 1 | u8 | pad_y | Alignment byte |
| 0x10 | 3 | i24 | pos_z | Position Z |
| 0x13 | 1 | u8 | pad_z | Alignment byte |
| 0x14 | 2 | i16 | param0 | Intensity or range parameter |
| 0x16 | 2 | i16 | param1 | Intensity or range parameter |
| 0x18 | 6 | i16[3] | direction | Direction/attenuation vector (3 × i16). Non-zero in active lights. |
| 0x1E | 8 | u8[8] | channel_map | Light channel enable. Always either 00 01 02 03 04 05 06 07 (active, identity mapping to 8 channels) or all zeros (inactive). |
| 0x26 | 4 | u8[4] | reserved_26 | Always 0. |
Position fields use the same i24+pad encoding as MPOB/MPSO.
MPMK (Markers)
MPMK starts with a little-endian record count, followed by 13-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 3 | i24 | pos_x | Position X |
| 0x03 | 1 | u8 | pad_x | Alignment byte |
| 0x04 | 3 | i24 | pos_y | Position Y |
| 0x07 | 1 | u8 | pad_y | Alignment byte |
| 0x08 | 3 | i24 | pos_z | Position Z |
| 0x0B | 1 | u8 | pad_z | Alignment byte |
| 0x0C | 1 | u8 | reserved | Not read by the engine at runtime. Engine uses bytes 0x04 (type) and 0x05 (subtype) from the runtime marker struct for processing. |
Position fields use the same i24+pad encoding as MPOB/MPSO. No explicit record ID field. The engine branches on marker type (byte +0x04 in runtime struct) with values 0x02 and 0x06 triggering distinct paths.
MPSZ (Sizes)
MPSZ contains per-actor state data. Unlike other MP* sections, MPSZ does NOT use the standard count-prefixed layout — the first u32 is data, not a record count.
The engine allocates actor_count × 0x1A (26) bytes at runtime and builds a linked list of 26-byte records. Each record is populated from RAHD fields during loading.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | ptr | next | Next record in linked list (0 = last) |
| +0x04 | 4 | ptr | actor_ptr | Pointer to actor object (from RAHD) |
| +0x08 | 4 | u32 | field_08 | From RAHD +0x51 |
| +0x0C | 4 | ptr | resource_0 | Allocated resource pointer |
| +0x10 | 4 | ptr | resource_1 | Allocated resource pointer |
| +0x14 | 4 | ptr | resource_2 | Allocated resource pointer |
| +0x18 | 2 | i16 | field_18 | From RAHD +0x0D (instances - 1) |
File-level record sizes vary across maps (7–26+ bytes per actor). The file-to-runtime unpacking involves conditional rebasing from RAHD fields. Present in all 27 shipped RGM files (245–7056 bytes).
MPSF (Flat Objects)
MPSF starts with a little-endian record count, followed by 24-byte records. Each record places a textured quad in the scene.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 4 | i32 | reserved | Not read at runtime. |
| 0x08 | 3 | i24 | pos_x | Position X |
| 0x0B | 1 | u8 | pad_x | Alignment byte |
| 0x0C | 3 | i24 | pos_y | Position Y |
| 0x0F | 1 | u8 | pad_y | Alignment byte |
| 0x10 | 3 | u24 | pos_z | Position Z |
| 0x13 | 1 | u8 | pad_z | Alignment byte |
| 0x14 | 2 | u16 | texture_data | Packed: texture_id = data >> 7, image_id = data & 0x7F |
| 0x16 | 2 | i16 | reserved | Not read at runtime. |
Position decode uses the same MPOB scale/sign rules. MPSF items are flat quads with zero rotation.
Redguard Preservation CLI
Scene Export Notes
RGMcarries scene placement transforms;ROBalone does not.- Practical scene assembly requires
MPOB+MPSO+MPRP+MPSF. - Model lookup needs direct file stems, ROB segment-name resolution, and
RAHD/RAANfallback for emptyMPOB.model_name(see RAHD and RAAN). - Some names in shipped RGM files are truncated forms like
NAME.3and require normalization (strip from last.to get the segment stem). - MPOB rotation parity uses degree-angle conversion; MPSO parity depends on the transposed matrix mapping above.
- MPSO rotation parity in scene export is obtained by interpreting Q4.28 values as a row-major 3x3 matrix, converting through quaternion space, applying mesh-axis flip in YZX Euler space (
-X, +Y, -Z), then rebuilding the final rotation. - Non-visual MPOB entries (sound triggers like
WATERSND/WINDSND, door scripts likeLOCKDOOR, lighting markers likeNTLIGHT, entrance triggers likeENT*) have no model geometry; they emit transform-only nodes in the scene graph. - Node naming convention:
B_NNN_<script>for MPOB,SNNN_<model>for MPSO,FNNN_<texid>/<imgid>for MPSF. - MPOB actors may carry a script-specific texture override from RAHD (
textureIdnear record tail). Applying that override is required for correct character skin variants (for example Cartographer NPCs).
ROB segment resolution order
When a model name is not found as a direct file, the exporter scans all registered ROB files for a matching embedded segment. ROBs are scanned in source-priority order: fxart (v4.0/v5.0 models) before maps before 3dart (v2.6/v2.7 models).
v2.6/v2.7 models in 3dart/ ROBs have a known vertex-parsing limitation (vertex coordinates read as zero for ROB-embedded segments). Using fxart ROBs avoids this and produces correct geometry.
JSON Sidecar Output
When converting an RGM file via cargo run -- convert, a .json sidecar is written alongside the .glb containing all actor metadata that does not fit in the glTF format:
- Per-actor RAGR animation groups with every frame command decoded by opcode type
- Per-actor RAEX records (grip, scabbard, combat ranges, texture overrides)
- RAHD cross-reference (actor index and script name)
Each animation command is decoded with opcode-specific parameter names:
| Opcode layout | Fields in JSON |
|---|---|
| 10 + 10 (opcodes 0, 4, 10) | param_a, param_b |
| 6 + 6 + 6 (opcodes 6, 8) | x, y, z |
| 2 + 18 (opcodes 7, 9) | axis, value |
| 6 + 7 + 7 (opcode 15) | trigger_mask, start_frame, target_group |
| 20-bit (opcodes 1–3, 5, 11–14) | value |
All commands include opcode (numeric) and name (e.g. "ShowFrame", "PlaySound").
External References
- UESP: Mod:RGM File Format
- UESP: Mod:Redguard File Formats
- RGUnity/redguard-unity
RGRGMFile.cs— RGM section parser - RGUnity/redguard-unity
RGRGMScriptStore.cs— RASC bytecode interpreter with dispatch loop and complete flags table (369 entries) - RGUnity/redguard-unity
soupdeffcn_nimpl.cs— Complete SOUP function ID-to-name table (367 functions) - Dillonn241/redguard-mod-manager
ScriptReader.java— RASC bytecode disassembler (bytecode to readable script text) - Dillonn241/redguard-mod-manager
ScriptParser.java— RASC bytecode assembler (script text to bytecode); confirms round-trip encoding - Dillonn241/redguard-mod-manager
MapFile.java— RGM section reader/writer with complete chunk tag list - Dillonn241/redguard-mod-manager
MapHeader.java— RAHD record parser with field offsets - Dillonn241/redguard-mod-manager
MapDatabase.java— SOUP386.DEF parser (function, flag, reference, attribute definitions)
WLD World Geometry File Format
Terrain/world-grid container with a fixed 4-section layout; each section stores four 128x128 byte maps.
4 WLD files exist in /maps: EXTPALAC.WLD, HIDEOUT.WLD, ISLAND.WLD, NECRISLE.WLD.
Overall Structure
All WLD files are exactly 263,432 bytes.
[Header — 1184 bytes]
[Section 0 — 65558 bytes]
[Section 1 — 65558 bytes]
[Section 2 — 65558 bytes]
[Section 3 — 65558 bytes]
[Footer — 16 bytes]
Each section (65,558 bytes) is:
[Section Header — 22 bytes]
[Map 1 — 128×128 bytes (heightmap)]
[Map 2 — 128×128 bytes (unused, zero-filled)]
[Map 3 — 128×128 bytes (texture/material)]
[Map 4 — 128×128 bytes (unused, zero-filled)]
Header (1184 bytes)
The file header is 296 dwords (u32[296]). Most are zero; 12 are non-zero.
Logical field groups within the 296-dword header:
unknown1[6](u32[6]) at0x00..0x17sec_hdr_size(u32) at0x18file_size(u32) at0x1Cunknown2[28](u32[28]) at0x20..0x8Fsec_ofs[4](u32[4]) at0x90..0x9Funknown3[256](u32[256]) at0xA0..0x49F
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | unknown1_0 | Always 16. |
| 0x04 | 4 | u32 | section_cols | Always 2. |
| 0x08 | 4 | u32 | section_rows | Always 2. |
| 0x0C | 4 | u32 | reserved_0c | Always 0. |
| 0x10 | 4 | u32 | unknown1_4 | Always 160 (0xA0). |
| 0x14 | 4 | u32 | unknown1_5 | Always 1. |
| 0x18 | 4 | u32 | section_header_size | Always 22. |
| 0x1C | 4 | u32 | file_size_field | Always 263416 (file_size - 16). |
| 0x90 | 4 | u32 | section0_offset | Always 1184 (0x4A0). |
| 0x94 | 4 | u32 | section1_offset | Always 66742 (0x104B6). |
| 0x98 | 4 | u32 | section2_offset | Always 132300 (0x204CC). |
| 0x9C | 4 | u32 | section3_offset | Always 197858 (0x304E2). |
| 0xA0 | 4 | u32 | unknown3_0 | Always 2135957017 (0x7F501E19); first element of unknown3[256]. |
0xA0..0x49F is a contiguous u32[256] block (unknown3), not a single field. Only unknown3[0] is non-zero; the remaining 255 dwords are zero.
The header is byte-identical across all 4 shipped WLD files. All remaining header dwords not listed above are zero.
The loader reads the first 0x90 bytes, validates 0x14 == 1 and 0x18 == 22, and uses only section_cols, section_rows, and section_header_size at runtime. Fields beyond 0x1C — including sec_ofs[4] and unknown3[256] — are not read by the terrain loader.
Section Header (22 bytes)
Each section starts with 11 little-endian words (u16[11]):
| Offset (in section) | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 6 | u16[3] | unknown1 | Section-local unknown values. |
+0x06 | 2 | u16 | texbsi_file | Section-declared texture archive id (TEXBSI.%03d). In the original engine’s terrain path, texture-bank loading is hard-wired to texbsi.302 (see notes below). |
+0x08 | 2 | u16 | map_size | Always 256 (2 x 128). |
+0x0A | 12 | u16[6] | unknown2 | Always 0. |
Section headers are identical across all 4 shipped WLD files. unknown1[0] varies by section index: section0=2152, section1=568, section2=1308, section3=10. texbsi_file is always 302. map_size is always 256.
The loader reads each 22-byte section header but does not decode or reference any fields — it proceeds directly to map-plane reads. The engine hard-wires texbsi.302 for terrain textures regardless of the per-section texbsi_file value.
unknown1[0] cannot be a TEXBSI id (values like 568 and 1308 have no matching files). It appears to be build-tooling metadata fixed per section slot.
Section headers (identical across all files):
| Section | Header bytes (hex) |
|---|---|
| 0 | 68 08 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 1 | 38 02 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 2 | 1C 05 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 3 | 0A 00 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
Map Planes
After each 22-byte section header, four 128x128 byte maps follow.
Map 1: heightmap plane; low 7 bits are height (0–127), high bit is a build-time flag stripped at load time. See Map 1 High Bit.Map 2: unused — always zero-filled; skipped by the engine at load time.Map 3: texture/material plane; packed bits:- low 6 bits (
0..63) = texture index - high 2 bits (
0..3) = quarter-turn rotation
- low 6 bits (
Map 4: unused — always zero-filled; skipped by the engine at load time.
The engine only uses Map 1 and Map 3. Maps 2 and 4 are skipped during loading — their bytes are read to advance the file stream, but the data is never stored or used.
texbsi_file = 302 matches on-disk texture archive fxart/TEXBSI.302.
In TEXBSI.302, image names follow D02xxx, which aligns with TEXBSI naming rules:
- filename low two digits (
02) match image-name file number (D02xxx) - filename hundreds digit (
3) matches type-char group (D)
Notes:
Map 3bit packing (index + rotation) is corroborated by UESP documentation.- Original-engine terrain initialization hard-loads
texbsi.302and precomputes a 64-entry terrain texture lookup table from that archive, matching Map 3’s 0..63 index space. - Engine string analysis confirms
texbsi.302is referenced by the terrain initialization path;texbsi.%sis referenced by the generic TEXBSI loader path, not the terrain-table initialization path. - Conclusion: per-section
texbsi_fileswitching is not used by the original engine’s terrain renderer; terrain textures come fromTEXBSI.302.
Cross-check against shipped fxart/TEXBSI.302:
Map 3index range0..63is fully covered by image ids inTEXBSI.302(name suffixes00..63).- The previously “missing” ids (
00,05,06,07,30,31,32,52) are present as alternate record forms in the TEXBSI stream (not just the main BSIF image-record form), matching UESP’s note that index mapping is not a simple 1:1 record-order lookup. - Those alternate-form ids are
IFHDanimated records inTEXBSI.302, consistent with lookup by image-id suffix rather than simple static-record order. - Rotation bits are still populated when texture index is
0; this suggests engine-side handling likely treats index0as dominant (rotation may be ignored for empty/default tiles).
Bank summary (fxart/TEXBSI.*, D-prefix terrain-style banks):
- Only
TEXBSI.302has full0..63coverage (56BSIFstatic records + 8IFHDanimated records). OtherD*banks are partial/specialized. - Combined with engine evidence (the engine hard-wires
texbsi.302), the original engine’s terrain path should be treated as fixed-bank (302) rather than generic per-bank Map 3 lookup behavior.
Map 1 High Bit (0x80)
The game engine strips the high bit (& 0x7F) from every Map 1 byte at WLD load time, before storing height values in its runtime buffer. The high bit is discarded and never used for rendering, height lookup, texture selection, or any other runtime purpose.
The high bit tends to appear on outer-border cells and cells with large height deltas. It may be a build-time artifact (e.g., marking boundary or steep cells for the level editor) that the runtime engine does not consume.
Each section contributes one 128x128 tile per map plane. The four section tiles combine into a 2x2 world grid (256x256) as described by UESP.
Terrain Rendering Pipeline
At runtime, Map 1 and Map 3 are loaded into separate buffers and processed independently — there is no interaction between the two during rendering.
Grid Coordinate System
Each grid cell is 256 engine units wide. Terrain vertex positions are computed as:
world_x = grid_index_x × 256
world_z = grid_index_z × 256
world_y = -height_table[heightmap_byte & 0x7F]
No origin offset is applied to vertex positions. A separate world→grid reverse-lookup (used for camera cell detection) applies half-cell offsets (−0.5 on X, +0.5 on Z), but these do not affect terrain geometry.
Height Values (Map 1)
Each Map 1 byte is masked to 7 bits (& 0x7F) at load time, producing height values in the range 0–127. These values index into a 128-entry float lookup table to produce world-space Y coordinates.
The engine stores a static source table of positive float values and negates them at initialization (-ABS(source)), so terrain heights are negative — the terrain surface sits below a reference plane. A second initialization mode computes water_level - ABS(source), adjusting heights relative to a configurable water-level parameter.
Height Lookup Table
The 128-entry source table (values in engine units before negation):
0: 0 40 40 40 80 80 80 120 120 120
10: 160 160 160 200 200 200 240 240 240 280
20: 280 320 320 320 360 360 400 400 400 440
30: 440 480 480 480 520 520 560 560 600 600
40: 600 640 640 680 680 720 720 760 760 800
50: 800 840 840 880 880 920 920 960 1000 1000
60: 1040 1040 1080 1120 1120 1160 1160 1200 1240 1240
70: 1280 1320 1320 1360 1400 1440 1440 1480 1520 1560
80: 1600 1600 1640 1680 1720 1760 1800 1840 1880 1920
90: 1960 2000 2040 2080 2120 2200 2240 2280 2320 2400
100: 2440 2520 2560 2640 2680 2760 2840 2920 3000 3080
110: 3160 3240 3360 3440 3560 3680 3800 3960 4080 4280
120: 4440 4680 4920 5200 5560 6040 6680 7760
The table uses a non-linear encoding with three regions:
- Indices 0–52 (values 0–2120): near-linear with ~40-unit steps and repeated values, giving maximum height precision for flat and gently sloped terrain where the player spends most time.
- Indices 53–69 (values 2120–3240): transition zone with gradually increasing step sizes.
- Indices 70–127 (values 3240–7760): accelerating steps (80 → 120 → 240 → 1080), compressing tall cliffs and peaks into fewer index values.
This is a hand-tuned gamma-like curve that allocates more precision to common terrain heights (flat ground, gentle slopes) while still supporting the full elevation range with 7 bits of storage.
Texture Selection (Map 3)
Each Map 3 byte encodes two fields:
| Bits | Mask | Field | Range |
|---|---|---|---|
| 5:0 | & 0x3F | texture index | 0–63 |
| 7:6 | >> 6 & 3 | quarter-turn rotation | 0–3 |
In the original engine, the texture index selects one of 64 preloaded terrain texture entries sourced from TEXBSI.302 (not a per-section runtime bank switch). The rotation selects one of four UV orientation states (0°, 90°, 180°, 270° counter-clockwise).
Terrain Texture Blending (SURFACE.INI)
The engine loads a SURFACE.INI configuration file (from the game directory) that defines per-texture-index blend behavior and surface-type sound remapping. This drives a pixel-level alpha-blending system for terrain tile transitions — it does not affect geometry, UVs, or material assignment.
Water Tiles
The terrain renderer treats certain texture indices as water or special tiles. When all four corners of a grid cell have texture indices in the set {0, 5, 30, 31}, the cell is rendered as a water surface instead of normal terrain geometry. This applies water-plane rendering with wave animation effects. See Water Waves for the per-frame displacement formula and rendering pipeline.
Terrain Normals
The engine computes smooth vertex normals for terrain in three passes:
- Face normals — Each grid cell is split into two triangles (TL→BR→TR and BR→TL→BL). A cross-product normal is computed per triangle.
- Vertex averaging — At each grid vertex, the face normals from all adjacent triangles are summed and normalized. Each interior vertex touches 6 triangles from 4 cells: both triangles of the cell to the upper-left and lower-right, plus one triangle each from the cells above and to the left.
- Rendering — The averaged vertex normals are used for Gouraud-interpolated shading across each triangle.
This produces smooth terrain shading. The per-triangle face normals (pass 1) are retained as intermediate values but are not used directly for rendering.
Footer (16 bytes)
The final 16 bytes are constant in all files:
54 55 4C 4F 28 C0 43 00 FF FF FF FF 35 37 04 00
Interpreted as four dwords (u32, little-endian):
0x4F4C5554("TULO"bytes in file order)0x0043C0280xFFFFFFFF0x00043735
The WLD loader does not read or parse this footer; it stops after reading the 4 section data blocks. The footer is a build-time artifact or reserved metadata ignored at runtime. Field-level semantics are unknown.
Relationships to Other Formats
- RGM stores scene/object placement for the same levels.
- PVO stores octree-style spatial/visibility data for some of the same levels. WLD terrain is not included in PVO visibility culling. The engine’s PVO visibility check operates exclusively on placed objects — it searches MLST entries against MPSO records and actor pointers, and is called only from the placed-object render loop. Terrain is rendered through a separate unconditional path (the engine’s terrain surface subsystem). Default visibility is 1 (visible) when PVO data is absent.
- TEXBSI supplies textures referenced by
Map 3indices (Map 3 index range 0–63 is fully covered byTEXBSI.302).
External References
- UESP: Mod:World Files
- UESP: Mod:Redguard File Formats
- uesp/redguard-3dfiletest
3DFileTest/Common/ - RGUnity/redguard-unity
RGWLDFile.cs
SFX Sound Effects File Format
Single-file container for all game sound effects, stored as MAIN.SFX in the SOUND directory. Does not include voice clips (those are in ENGLISH.RTX).
Overall Structure
FXHD section (44 bytes)
FXDT section (variable)
"END " (4 bytes)
Effects are stored sequentially with no offset table. The game references effects by their 0-based index in the file.
FXHD (Header Section)
44 bytes total. Section size word is big-endian; remaining fields are little-endian.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | u32 | BE | section_size | Payload size excluding this field |
| 0x04 | 32 | [u8; 32] | — | description | ASCII string, set by internal tool “SoupFX” |
| 0x24 | 4 | u32 | LE | effect_count | Number of sound effects (118 in MAIN.SFX) |
FXDT (Data Section)
Begins with a big-endian u32 section size (excluding itself), followed immediately by sequential effect records.
Effect Record
27-byte header followed by raw PCM audio data.
All fields little-endian unless noted.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | type_id | Audio type: 0 = 8-bit mono, 1 = 16-bit mono, 2 = 8-bit stereo (unused), 3 = 16-bit stereo |
| 0x04 | 4 | u32 | bit_depth | 0 = 8-bit, 1 = 16-bit |
| 0x08 | 4 | u32 | sample_rate | Always 11025 or 22050 Hz |
| 0x0C | 1 | u8 | unused_0c | Always 64. Runtime behavior is driven by the surrounding 26-byte header block (0x00–0x19), with no separate per-field behavior for this byte. Likely a vestigial default volume value (64/127 ≈ 50% on the Miles Sound System scale). |
| 0x0D | 1 | i8 | loop_flag | 0 = no loop, non-zero = enable looping. The engine checks only != 0; values -1 (0xFF) and -31 (0xE1) are functionally identical. |
| 0x0E | 4 | u32 | loop_offset | Byte offset into PCM data for loop restart point (always 0) |
| 0x12 | 4 | u32 | loop_end | Sample count before looping (always 0xFFFFFFFF) |
| 0x16 | 4 | u32 | data_length | Byte count of raw PCM data following this header |
| 0x1A | 1 | u8 | reserved_1a | Padding between header and PCM data. Always 0. |
| 0x1B | var | [u8] | pcm_data | Raw PCM audio: u8 samples for 8-bit, i16 LE samples for 16-bit |
Loop Behavior
The engine checks only whether loop_flag is non-zero — the specific value is not interpreted.
loop_flag = 0: play once (non-looping effects)loop_flag = -1 (0xFF): enable looping (used for ambient loops like fire, water, wind)loop_flag = -31 (0xE1): enable looping (functionally identical to -1; only used on effect 117, the snake charmer tune)
loop_offset and loop_end appear to be unused features — always loop_offset = 0 and loop_end = 0xFFFFFFFF.
Runtime Effect Structure
The engine allocates a 34-byte (0x22) runtime structure per effect, reading 26 bytes (0x00–0x19) from the file. The remaining 8 bytes are computed at runtime:
| Struct Offset | Size | Source | Contents |
|---|---|---|---|
| 0x00–0x19 | 26 | File | Header fields (type_id through data_length) |
| 0x1A–0x1D | 4 | Runtime | Pointer to allocated PCM data buffer |
| 0x1E–0x21 | 4 | Runtime | Computed duration value: (data_length << 8) / (sample_rate × bytes_per_sample × channels) |
Related Formats
- RTX — dialogue audio container. Uses the same 27-byte audio header structure (offsets 0x00–0x1A) as SFX effect records. SFX stores sound effects; RTX stores voice clips.
External References
RTX Dialogue Audio Format
Container format used for dialogue strings and audio assets (for example ENGLISH.RTX).
Overview
RTX stores two payload kinds under a common chunk/index system:
- String-only entries (ASCII text payload)
- Audio entries (string metadata + fixed audio header + audio bytes)
The file uses:
- per-chunk on-disk headers (
tag+ big-endian payload size) - a footer that points to a central index table
All values below are validated against ENGLISH.RTX.
File Layout
[chunk records ...]
[index table]
[footer]
Footer (12 bytes, file end)
| Offset (from EOF) | Size | Type | Name | Description |
|---|---|---|---|---|
| -12 | 4 | ASCII | footer_tag | Always RNAV |
| -8 | 4 | u32 LE | index_offset | Absolute file offset of index table |
| -4 | 4 | u32 LE | index_count | Number of index entries |
For ENGLISH.RTX:
footer_tag = RNAVindex_offset = 184629473index_count = 4866
Chunk Record (on disk)
Every payload in the data region has an 8-byte chunk header before it:
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | ASCII | — | tag | 4-character chunk label |
| 0x04 | 4 | u32 | BE | payload_size | Size of payload bytes that follow |
| 0x08 | var | [u8] | — | payload | Entry payload |
Index Table
The index is an array of 12-byte entries. Each entry points to one chunk payload.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | ASCII | — | tag | Same tag as the chunk header |
| 0x04 | 4 | u32 | LE | payload_offset | Absolute file offset of payload (not header) |
| 0x08 | 4 | u32 | LE | payload_size | Payload byte size |
Validation notes (all 4866 entries):
payload_offset - 8points to a chunk header with matchingtag- header
payload_size(big-endian) matches indexedpayload_size - all indexed ranges are in bounds (
offset + size <= file_size) - entries are ordered by descending payload offset
Payload Types
Payload type is identified by byte payload[1]:
0= string-only entry1= audio entry
String-Only Payload (payload[1] = 0)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 1 | u8 | — | kind | Always 0 |
| 0x01 | 1 | u8 | — | subtype | Always 0 for text entries |
| 0x02 | 2 | u16 | LE | string_len | Byte length of ASCII text |
| 0x04 | 2 | u16 | LE | reserved | Always 0 |
| 0x06 | var | [u8] | — | text | ASCII text bytes, no terminator |
Payload size rule:
payload_size = 6 + string_len
Audio Payload (payload[1] = 1)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 1 | u8 | — | kind | Always 0 |
| 0x01 | 1 | u8 | — | subtype | Always 1 for voice entries |
| 0x02 | 2 | u16 | LE | string_len | Byte length of ASCII label |
| 0x04 | 2 | u16 | LE | reserved | Always 0 |
| 0x06 | var | [u8] | — | label | ASCII label bytes, no terminator |
| 0x06+N | 27 | struct | — | audio_header | Audio metadata (below), N = string_len |
| 0x21+N | var | [u8] | — | audio_data | Raw PCM audio bytes |
Audio payload size rule:
payload_size = 6 + string_len + 27 + audio_length
Audio Header (27 bytes)
All fields little-endian unless noted.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | type_id | 0 = 8-bit mono, 1 = 16-bit mono |
| 0x04 | 4 | u32 | bit_depth | 0 = 8-bit, 1 = 16-bit |
| 0x08 | 4 | u32 | sample_rate | 11025 or 22050 |
| 0x0C | 1 | u8 | level_0c | Always 100 |
| 0x0D | 1 | i8 | loop_flag | Always 0 |
| 0x0E | 4 | u32 | loop_offset | Always 0 |
| 0x12 | 4 | u32 | loop_end | Always 0xFFFFFFFF |
| 0x16 | 4 | u32 | audio_length | Byte length of audio_data |
| 0x1A | 1 | u8 | reserved_1a | Always 0 |
Notes
- Tags are 4-byte IDs and are not globally reused in
ENGLISH.RTX. - A few tags include punctuation (for example
#bon,?vql). - The index points to payload starts; on-disk chunk headers are always 8 bytes earlier.
Related Formats
- SFX — sound effects container. Uses the same 27-byte audio header structure (offsets 0x00–0x1A) as RTX audio entries. RTX stores voice clips; SFX stores sound effects.
Redguard Preservation CLI
Read
cargo run -- read ENGLISH.RTX parses the file and prints a per-entry summary: tag, type (TEXT or AUDIO), audio format, sample rate, duration, and a label preview.
Convert
cargo run -- convert ENGLISH.RTX -o output_dir/ extracts all audio entries as individual .wav files (named by 4-character tag, e.g. zbza.wav) and writes an index.json sidecar containing metadata for all 4866 entries (both text-only and audio).
Validated against ENGLISH.RTX: 3933 .wav files + 933 text entries in index.json.
External References
- RGUnity/redguard-unity
RGRTXFile.cs| RTX container reader - Dillonn241/redguard-mod-manager
RtxDatabase.java| RTX read/write with round-trip support - Dillonn241/redguard-mod-manager
RtxEntry.java| RTX entry structure and audio format definitions - UESP: Redguard Console (script command context)
TEXBSI Texture File Format
Container format for indexed-color texture images. Files are named TEXBSI.### where ### is the texture bank number.
Overall Structure
A TEXBSI file is a flat sequence of image records with no file-level header. The sequence ends when a 9-byte null sentinel is encountered.
[Image Record 0]
[Image Record 1]
...
[Image Record N]
[9 × 0x00] ← end sentinel
Image Record
Image-record envelope fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 9 | [u8; 9] | image_name | Image name, null-padded. Format: {type_char}{file_num:02d}{image_idx:03d}. All-zero = end of file. |
| 9 | 4 | u32 | subrecord_bytes | Total size of all subrecords that follow (excludes this 13-byte envelope). |
| 13 | … | subrecords | subrecords | Tagged subrecords until END . |
Image Name Encoding
The 9-byte name encodes the file number and sub-image index:
"E01005\0\0\0" → type 'E', file 01, image index 005
"A02003\0\0\0" → type 'A', file 02, image index 003
The image index (last 3 digits) is how 3D model faces reference sub-images via image_id.
Filename/name coupling across shipped TEXBSI.### files:
### % 100matches the 2-digit file number in image names.### / 100maps to type-char family:0->A,1->B,2->C,3->D,4->E,5->F.
Examples:
TEXBSI.302containsD02xxximages.TEXBSI.114containsB14xxximages.
Type characters range from A through F.
The type character has no semantic meaning — it is a deterministic artifact of the base-40 name encoding used internally. The game converts numeric texture IDs to 6-character strings using the alphabet 0123456789abcdefghijklmnopqrstuvwxyz~_#%, then shifts the first character by subtracting 0x31. The type letter is simply which base-40 digit range the texture ID falls into (A=digit 27, B=28, …, F=32). The game never tests or filters by the type character — it round-trips through the numeric ID.
Subrecord Structure
Every subrecord has an 8-byte header:
Subrecord size words are big-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 4 | [u8; 4] | tag | Tag: BSIF, IFHD, BHDR, CMAP, DATA, or END |
| 4 | 4 | u32 | payload_size | Payload size in bytes (not including this 8-byte header) |
| 8 | … | payload | payload | Tag-specific data |
Subrecords always appear in this order:
BSIF or IFHD (mutually exclusive)
BHDR (required)
CMAP (optional, only with IFHD)
DATA (required)
END (terminates the record — no size field)
Subrecord Payloads
BSIF — Static Image Marker
Payload size: 0 bytes (empty). Marks a static (non-animated) image.
IFHD — Animated Image Marker
Payload size: 44 bytes (always 01 followed by 43 00 bytes). Marks an animated image.
When IFHD is present, the DATA subrecord uses the animated offset-table format.
BHDR — Image Header (26 bytes)
All fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 2 | i16 | x_offset | X position hint (placement on virtual canvas) |
| 2 | 2 | i16 | y_offset | Y position hint |
| 4 | 2 | i16 | width | Image width in pixels |
| 6 | 2 | i16 | height | Image height in pixels |
| 8 | 1 | u8 | has_cmap | Export-tool flag, not read at runtime. Set to 1 when the image has an embedded CMAP palette (always co-occurs with IFHD animated images). Per UESP: “images that have 1 are all animated effects such as fire and water.” |
| 9 | 1 | u8 | export_flags | Export-tool metadata. Values: 0, 1, or 9. Packed into the same u16 as has_cmap during export. Purpose within the build pipeline unknown. |
| 10 | 4 | — | reserved | Always 0 |
| 14 | 2 | i16 | frame_count | 1 = static, 2–16 = animated |
| 16 | 2 | i16 | anim_delay | Read at runtime. Animation frame duration in milliseconds. Converted to DOS PIT timer ticks via round(anim_delay × 18.2 / 1000); clamped to minimum 1. Typical value 71 → 1 tick (~55 ms); value 500 → 9 ticks (~495 ms). Range: 0–500. |
| 18 | 4 | — | reserved | Always 0 |
| 22 | 2 | u16 | tex_scale | Read at runtime as a single LE u16. 8.8 fixed-point texture coordinate scale factor: scale = tex_scale / 256.0. Default 0x0100 (= 1.0, neutral) is substituted when zero. Multiplied into polygon UV mapping during rendering. Known values: 0 (defaulted to 1.0), 128 (scale 0.5), 163 (scale ~0.637), 256 (scale 1.0), 512 (scale 2.0). Previously documented as two separate bytes (effect_id / effect_param); they are the low and high bytes of this single fixed-point field. |
| 24 | 2 | i16 | data_encoding | Pixel data encoding mode. Selects which compression method the DATA subrecord uses. Known values: 0 = raw uncompressed, 4 = animated offset table. Values 1–3 are engine-supported but unused. |
CMAP — Embedded Palette (768 bytes)
256 × RGB triplets (3 bytes each, values 0–255). Same layout as COL files.
Optional — always co-occurs with IFHD (animated images). When absent, the image uses an external .COL palette file.
DATA — Pixel Data
Static images (BSIF present, frame_count == 1):
Payload is width × height bytes of 8-bit indexed color, row-major, top-to-bottom. Each byte is a palette index (0–255). Index 0 = transparent.
Animated images (IFHD present, frame_count > 1):
Payload starts with an offset table of height × frame_count LE u32 entries. Each entry is a byte offset from the start of the DATA payload to the first byte of that row. Rows can be shared across frames (identical rows point to the same data).
offset_table[frame * height + row] → byte offset to row data (width bytes)
END — Record Terminator
4-byte tag "END " (with trailing space). No size field. Followed by 4 zero bytes.
Pixel Decoding
for each pixel byte:
if byte == 0: → transparent (alpha = 0)
else: → palette[byte] as RGB (alpha = 255)
Palette values are raw 8-bit RGB (0–255). No gamma correction needed.
Palette Selection
When decoding pixel data, palette lookup order is:
- External
.COLpalette — the scene-level palette file passed to the converter (e.g.ISLAND.COL). Which COL file to use is determined per-scene by the game engine. - Embedded
CMAP— the 256-color palette stored inside the image record (only present in animatedIFHDimages). - Grayscale fallback — if neither is available, each index maps to a gray value.
How 3D Models Reference Textures
Each face in a 3D model encodes a texture_id and image_id:
texture_id→ selects the TEXBSI.### file numberimage_id→ selects the sub-image within that file (0-indexed, matching the 3-digit suffix in the image name)
See 3D.md — Texture Decoding for the BCD encoding.
External References
- UESP: Mod:TEXBSI File Format — community byte-layout notes for TEXBSI records and subrecords.
- uesp/redguard-3dfiletest
RedguardTexBsiFile.cpp - RGUnity/redguard-unity
RGTEXBSIFile.cs - RGUnity/redguard-unity
RGBSIFile.cs
COL Palette File Format
256-color RGB palette format.
Each file maps color indices (0–255) to RGB values. Models reference palette entries via color_index in solid-color face data (see 3D.md).
Overall Structure
All COL files are exactly 776 bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | file_size | Always 776. |
| 0x04 | 4 | u32 | magic | Always 0x0000B123. |
| 0x08 | 768 | [u8; 768] | palette | 256 × RGB triplets (3 bytes each, values 0–255). |
Palette entry N is at offset 8 + N × 3, yielding bytes (R, G, B).
Usage
COL files are per-scene, not per-model. Different levels use different palettes (e.g. ISLAND.COL, CATACOMB.COL). The same color_index in a model’s face data produces different colors depending on which palette is active.
Entry 0 is always (0, 0, 0) — black.
Related Formats
- TEXBSI — the
CMAPsubrecord in animated TEXBSI images uses the identical 256 × RGB triplet layout (768 bytes). CMAP palettes are embedded per-image; COL palettes are per-scene. - FNT — font files embed their own
BPAL/FPALpalettes (same 768-byte layout), independent of scene COL palettes.
Redguard Preservation CLI
The convert command exports a COL file as two companion files:
- Swatch PNG — 256×256 image with a 16×16 grid of color swatches (16 px per cell). Index 0 is top-left, 255 is bottom-right, row-major order.
- Palette JSON — structured metadata with per-entry
index,r,g,b(0–255), andhexfields. Versioned asredguard-col-v1.
The output path determines the primary filename; the companion file shares the same stem with the other extension. Passing -o ISLAND.png produces ISLAND.png + ISLAND.json; passing -o ISLAND.json produces the same pair.
External References
- UESP: Mod:Palette Files — dedicated Redguard COL page with the 8-byte header + 768-byte palette layout.
- UESP: User:Daveh/Redguard File Formats — COL size and palette usage notes.
FNT Bitmap Font File Format
Chunked bitmap-font format with per-file palette and per-glyph indexed image data.
.FNT stores UI/dialog font glyphs as palette-indexed bitmaps. Each file embeds its own palette (not scene COL palettes), then stores glyph records in ASCII order. For palette structure background, see COL.md.
Top-Level Layout
The file is a sequence of named chunks, followed by an end marker:
FNHD(always present)BPALorFPAL(always present)FBMP(always present)RDAT(optional)ENDmarker
Common chunk orders:
FNHD -> BPAL -> FBMP -> ENDFNHD -> BPAL -> FBMP -> RDAT -> ENDFNHD -> FPAL -> FBMP -> END(ARIALVS.FNTonly)
Chunk Header
Each chunk begins with an 8-byte header:
Chunk-length fields are big-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | tag | Chunk name (FNHD, BPAL, FPAL, FBMP, RDAT) |
| 0x04 | 4 | u32 | length | Chunk payload size in bytes |
END is a 4-byte marker tag with no payload. ARIALVS.FNT has 4 additional trailing zero bytes after END .
FNHD Chunk (56 bytes)
FNHD payload is always 56 bytes.
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 32 | [u8; 32] | description | Font/tool description string; may contain NUL padding or multiple NUL-terminated fragments |
| 0x20 | 2 | u16 | unknown_24 | Not read at runtime — overwritten during glyph loading. Known values: 0, 1, 3. Export-tool metadata. |
| 0x22 | 2 | u16 | has_rdat | 1 if RDAT chunk present; 0 otherwise. Not checked at runtime — the engine never searches for RDAT. |
| 0x24 | 2 | u16 | reserved_28 | Always 0. Not read at runtime. |
| 0x26 | 2 | u16 | reserved_2a | Always 0. Not read at runtime. |
| 0x28 | 2 | u16 | reserved_2c | Always 0. Not read at runtime. |
| 0x2A | 2 | u16 | max_width | Export-tool hint (range 11–23). Not read at runtime — overwritten with the width of glyph ‘W’ during loading. |
| 0x2C | 2 | u16 | line_height | Used by the engine for text layout and baseline positioning. Values: 9, 10, 12, 14, 16, 22, 25, 26. |
| 0x2E | 2 | u16 | character_start | First encoded codepoint; always 32 (0x20, space). Not read at runtime — engine assumes fixed start. |
| 0x30 | 2 | u16 | character_count | Used by the engine as the glyph loop bound (clamped to 256). Number of glyph records in FBMP. Values: 95, 97, 98, 104, 112, 208. |
| 0x32 | 2 | u16 | reserved_36 | Always 0. Overwritten to 0xFF during glyph loading. |
| 0x34 | 2 | u16 | reserved_38 | Always 0. Overwritten to 0xFF during glyph loading. |
| 0x36 | 2 | u16 | has_palette | Used by the engine to control palette loading. Non-zero = allocate 768-byte palette and search for FPAL/BPAL chunk. Always 1. |
BPAL / FPAL Chunk (Palette)
Palette payload is always 768 bytes (256 RGB triplets).
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 768 | [u8; 768] | rgb_triplets | 256 entries x 3 bytes (R, G, B), palette indices referenced by FBMP pixel bytes |
Notes:
BPALis the normal tag.FPALappears inARIALVS.FNTwith the same 768-byte payload shape.- These palettes are local to each font file, independent of scene palettes in COL.md.
FBMP Chunk (Glyph Records)
FBMP payload contains character_count glyph records in sequential codepoint order starting at character_start.
Each glyph record:
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | u16 | enabled | 0 = disabled/unrendered glyph; non-zero = active glyph |
| 0x02 | 2 | i16 | offset_left | Horizontal draw offset in pixels |
| 0x04 | 2 | i16 | offset_top | Vertical draw offset in pixels |
| 0x06 | 2 | u16 | width | Glyph bitmap width in pixels |
| 0x08 | 2 | u16 | height | Glyph bitmap height in pixels |
| 0x0A | width*height | [u8] | pixels | Row-major palette indices |
Value ranges:
offset_left: 0..5offset_top: 0..19width: 1..22height: 1..25
FBMP payload length equals the sum of all glyph record sizes (10-byte header + width * height pixels each).
RDAT Chunk (Optional, 173 bytes)
RDAT is optional and always 173 bytes when present.
Layout (partially decoded):
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 136 | [u8; 136] | source_name | NUL-padded source/tool string |
| 0x88 | 4 | u32 | unknown_90 | Non-zero metadata field |
| 0x8C | 4 | u32 | unknown_94 | Non-zero metadata field |
| 0x90 | 4 | u32 | unknown_98 | Always 0 |
| 0x94 | 4 | u32 | unknown_9c | Usually 0; value 2 in ARIALBG.FNT |
| 0x98 | 4 | u32 | unknown_a0 | Near max_width-like values |
| 0x9C | 4 | u32 | unknown_a4 | Near line_height-like values |
| 0xA0 | 4 | u32 | unknown_a8 | Small enum-like values (1..3) |
| 0xA4 | 4 | u32 | unknown_ac | Small enum-like values (1..2) |
| 0xA8 | 4 | u32 | unknown_b0 | Always 0 |
| 0xAC | 1 | u8 | unknown_b4 | Always 0 |
RDAT is metadata. The font loader uses FNHD, FPAL/BPAL, and FBMP; it does not parse RDAT payload data.
External References
- UESP: Mod:Font Files — primary external reference for
FNHD/BPAL/FBMP/RDATchunk semantics. - UESP: Redguard:Glide Differences — renderer-specific font usage differences (non-structure behavior).
- UESP: Mod:RedguardFNTImporter — tooling-oriented evidence for practical import/export expectations.
PVO File Format
Pre-computed Visibility Octree. Binary spatial data format located in the /maps directory alongside .RGM (scene) and .WLD (terrain) files.
5 PVO files exist, each corresponding to a game level: CATACOMB, CAVERNS, DRINT, ISLAND, PALACE.
For a complete reference parser with pseudocode, see PVO Parser.
Purpose
PVO files store pre-computed visibility data used for geometry culling at runtime. Instead of calculating which polygons are visible from the camera every frame, the game looks up the camera position in the octree and retrieves a pre-built list of visible group IDs.
The runtime lookup works as follows: take the camera’s world-space position, walk the octree from root to leaf by comparing coordinates against each node’s center, then collect the polygon indices stored in that leaf’s PLST entries (which reference ranges in the MLST table). Only those polygons are submitted for rendering — everything else is skipped.
The data is generated offline by placing a virtual camera at every point on a uniform 256-unit grid across the level, running a visibility query at each point, and baking the results into the octree. See Generation Process for details.
Overall Structure
The file uses the same section-framing as RGM: 4-byte ASCII tag + 4-byte big-endian data length.
[OCTH — header]
[OCTR — octree node records]
[PLST — leaf polygon-list records]
[MLST — master polygon index table]
[END — footer]
Each section is framed as:
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | tag | ASCII section tag |
| 0x04 | 4 | u32 | BE | data_length | Payload size in bytes (0 for END ) |
Section layout
| File | OCTH | OCTR | PLST | MLST | END | Total |
|---|---|---|---|---|---|---|
| CATACOMB | @0x00 (52) | @0x3C (256,091) | @0x3E89F (827,861) | @0x108A7C (110,788) | @0x123B48 (0) | 1,194,832 |
| CAVERNS | @0x00 (52) | @0x3C (271,546) | @0x424FE (900,844) | @0x11E3F2 (33,630) | @0x126758 (0) | 1,206,112 |
| DRINT | @0x00 (52) | @0x3C (300,688) | @0x496D4 (994,987) | @0x13C587 (81,074) | @0x150241 (0) | 1,376,841 |
| ISLAND | @0x00 (52) | @0x3C (213,134) | @0x340D2 (1,053,725) | @0x1354F7 (216,378) | @0x16A239 (0) | 1,483,329 |
| PALACE | @0x00 (52) | @0x3C (48,125) | @0xBC41 (224,647) | @0x429D0 (108,922) | @0x5D352 (0) | 381,786 |
A sixth section tag PTCH exists in the executable’s string table but is not present in shipped files. The engine writes and loads PTCH sections through dedicated save/load paths (the pvopatchsave console command triggers a write).
Best-effort runtime characterization from engine behavior:
PTCHsection length is serialized aspatch_count * 6bytes.- The loader allocates and reads 6-byte records from
PTCH. - Patch application expands record data into MLST object-id lists used by PVO visibility checks.
Per-record layout (resolved from add/remove/apply behavior):
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | u32 | LE | octr_node_index | OCTR-node index ((node_ptr - octr_base) / 5) identifying which octree node receives the patch object id. |
| 0x04 | 2 | u16 | LE | object_index | Object index appended to the runtime-visible MLST id list for that node. |
Engine behavior:
- Add patch: writes
octr_node_indexatrecord+0andobject_indexatrecord+4. - Delete patch: matches/removes records by the same pair (
octr_node_index,object_index). - Apply patch: scans records matching current
octr_node_indexand appendsobject_indexvalues into the runtime visibility-id buffer.
OCTH Section — Header (52 bytes payload)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | magic | OCTH |
| 0x04 | 4 | u32 | BE | header_data_size | Always 52 (0x34). |
| 0x08 | 4 | u32 | LE | depth | Always 10. Maximum octree depth. |
| 0x0C | 4 | u32 | LE | total_nodes | Total octree node count. Equals leaf_nodes + interior_nodes. |
| 0x10 | 4 | u32 | LE | leaf_nodes | Leaf node count. Equals total_nodes - interior_nodes. |
| 0x14 | 4 | u32 | LE | mlst_polygon_count | Total entries in the MLST polygon index table. Invariant: mlst_polygon_count * 2 == MLST data_length. |
| 0x18 | 4 | u32 | LE | reserved | Always 0. |
| 0x1C | 4 | u32 | LE | cell_size | Root cell half-extent. Power of 2: 16384 (4 files) or 8192 (PALACE). |
| 0x20 | 4 | i32 | LE | center_x | Octree root center X coordinate. |
| 0x24 | 4 | i32 | LE | center_y | Octree root center Y coordinate. |
| 0x28 | 4 | i32 | LE | center_z | Octree root center Z coordinate. |
| 0x2C | 16 | — | — | reserved | Always zero (4 × u32). |
Node count relationship
total_nodes = leaf_nodes + interior_nodes where interior_nodes equals the number of 0xFFFFFFFF leaf_ref values in the OCTR section:
| File | total_nodes | leaf_nodes | interior_nodes |
|---|---|---|---|
| CATACOMB | 23,287 | 16,787 | 6,500 |
| CAVERNS | 25,694 | 19,618 | 6,076 |
| DRINT | 29,112 | 22,717 | 6,395 |
| ISLAND | 23,154 | 18,971 | 4,183 |
| PALACE | 5,113 | 4,105 | 1,008 |
mlst_polygon_count confirmation
| File | mlst_polygon_count | MLST data_length | count × 2 == length |
|---|---|---|---|
| CATACOMB | 55,394 | 110,788 | yes |
| CAVERNS | 16,815 | 33,630 | yes |
| DRINT | 40,537 | 81,074 | yes |
| ISLAND | 108,189 | 216,378 | yes |
| PALACE | 54,461 | 108,922 | yes |
Center coordinates and extents
| File | center_x | center_y | center_z | cell_size |
|---|---|---|---|---|
| CATACOMB | 35,584 | -11,520 | 29,952 | 16,384 |
| CAVERNS | 27,392 | -9,984 | 21,504 | 16,384 |
| DRINT | 23,808 | -13,056 | 33,024 | 16,384 |
| ISLAND | 35,584 | -16,384 | 36,608 | 16,384 |
| PALACE | 31,488 | -6,144 | 30,720 | 8,192 |
The octree root spans [center - cell_size, center + cell_size] on each axis.
OCTR Section — Octree Node Records
Serialized octree nodes written sequentially. Each node is a variable-length record addressed by byte offset within the section.
Node record format
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 1 | u8 | child_mask | Bit field. Bit i set = child i is present (octants 0..7). |
| 1 | 4 | u32 | leaf_ref | Byte offset into the PLST section. 0xFFFFFFFF = no leaf data (interior-only node). |
| 5 | 4 × n | u32[n] | child_refs | One entry per set bit in child_mask, low bit first. Each is a byte offset into the OCTR section pointing to a child node. 0xFFFFFFFE = uninitialized-child sentinel (see below). |
Record size = 5 + popcount(child_mask) * 4
Possible sizes: 5, 9, 13, 17, 21, 25, 29, 33, 37 bytes.
Octant assignment
The 3-bit octant index encodes spatial position relative to the node center:
bit 0 (value 1) = z > center_z
bit 1 (value 2) = y > center_y
bit 2 (value 4) = x > center_x
Octant 0 = (x-, y-, z-) Octant 4 = (x+, y-, z-)
Octant 1 = (x-, y-, z+) Octant 5 = (x+, y-, z+)
Octant 2 = (x-, y+, z-) Octant 6 = (x+, y+, z-)
Octant 3 = (x-, y+, z+) Octant 7 = (x+, y+, z+)
Common child_mask patterns
| Pattern | Binary | Meaning |
|---|---|---|
0x00 | 00000000 | Leaf node, no children |
0x33 | 00110011 | Children in octants 0,1,4,5 (one face) |
0xCC | 11001100 | Children in octants 2,3,6,7 (opposite face) |
0xAA | 10101010 | Children in octants 1,3,5,7 (axis-aligned half) |
0x55 | 01010101 | Children in octants 0,2,4,6 (other half) |
0xFF | 11111111 | All 8 children present |
Child and leaf sentinel values
Two sentinel values appear in octree records:
0xFFFFFFFFin theleaf_reffield marks interior-only nodes — nodes with children but no directly associated polygon list. The count of these values equalsinterior_nodesfrom the header. In runtime traversal code,0xFFFFFFFFalso serves as the null terminator that ends octree walks.0xFFFFFFFEinchild_refsmarks an uninitialized/placeholder child node. During runtime octree traversal, this value indicates an unpopulated slot; when traversal visits it, the slot is overwritten with the current position. This is distinct from a null child (0xFFFFFFFF) and from a valid child offset.
PLST Section — Leaf Polygon-List Records
Serialized leaf data written sequentially. This is the largest section in every file. Each leaf describes which polygon groups are visible from an octree cell.
Leaf record format
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 1 | u8 | entry_count | Number of entries in this leaf. |
| 1 | 6 × n | — | entries | Array of entry_count entries (see below). |
Record size = 1 + 6 * entry_count
Each entry:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 2 | u16 | count | Number of polygon indices in this sub-list. |
| 2 | 4 | u32 | mlst_start | Starting index into the MLST array. |
Each entry references a contiguous slice: mlst[mlst_start .. mlst_start + count].
Constraint: mlst_start + count <= mlst_polygon_count.
Entries within a leaf represent distinct polygon groups. The full visible set for a leaf is the union of all its entry slices. Multiple leaves may share the same MLST ranges.
leaf_ref values in OCTR are byte offsets into this section, pointing to the start of a leaf record.
MLST Section — Master Polygon Index Table
A flat array of u16 visibility group indices.
- Length:
mlst_polygon_count * 2bytes.
This table is the master list of visibility groups referenced by the octree. PLST entries reference contiguous ranges within this table.
Index semantics
Each u16 value is a placed-object visibility ID, not an individual face index. The indices form a dense, zero-based sequential range with no gaps. The visibility check at runtime uses two lookup paths based on the index value:
- Indices 0 .. MPSO_count-1: direct MPSO record index. The runtime multiplies the index by 66 (0x42 = MPSO record size) and adds the MPSO array base to get the placed-object record.
- Indices MPSO_count .. N-1: secondary table index. The runtime subtracts MPSO_count and uses the result as an index into a separate pointer table. This table holds additional static objects loaded at runtime (e.g. via
LoadStaticscript commands, MPRP rope chains, or other non-MPSO visibility targets).
| File | Total IDs | MPSO range | Secondary range | MPSO objects | Secondary count |
|---|---|---|---|---|---|
| CATACOMB | 591 | 0–500 | 501–590 | 501 | 90 |
| CAVERNS | 233 | 0–203 | 204–232 | 204 | 29 |
| DRINT | 284 | 0–257 | 258–283 | 258 | 26 |
| ISLAND | 1,704 | 0–1,690 | 1,691–1,703 | 1,691 | 13 |
| PALACE | 289 | 0–262 | 263–288 | 263 | 26 |
The MPSO record size (66 bytes = 0x42) appears as the multiplier in visibility lookups.
END Section — Footer
8 bytes: END (4 ASCII bytes) followed by 0x00000000 (4 zero bytes). Data length is 0.
Generation Process
PVO files are generated by iterating a uniform 3D grid over the world bounding box:
- Compute world bounding box from level geometry.
- Iterate a uniform 3D grid at 256-unit spacing.
- At each grid point, run a visibility query to determine which polygons are visible.
- Insert the visible polygon set as a leaf into the octree.
- Prune single-child branches, then write the file.
Debug console commands
| Command | Description |
|---|---|
pvoi / pvotreeinfo | Display PVO tree statistics |
pvoa / pvoaddpatch | Add object to PVO visibility patch |
pvod / pvodeletepatch | Remove object from PVO patch |
pvoonoff | Toggle PVO visibility system on/off |
pvos / pvopatchsave | Save PVO patches to PTCH section |
pvol / pvotreeload | Load PVO tree from file |
Secondary Visibility Table
MLST indices >= MPSO_count reference a secondary pointer table built at runtime. This table is populated by iterating the placed object list and collecting objects whose visibility flag (offset +0x7a in the runtime object struct) is non-zero.
The visibility flag is set by SOUP script functions during object initialization. Objects that receive this flag — such as dynamically loaded static models — become trackable by the PVO system alongside the primary MPSO-based objects.
| Step | Description |
|---|---|
| 1 | Count placed objects with visibility flag set → secondary_count |
| 2 | Allocate secondary_count × 4 bytes for pointer array |
| 3 | Iterate placed objects; for each with flag +0x7a != 0, append pointer to array |
| 4 | At runtime, MLST index - MPSO_count indexes into this array |
External References
- UESP: Mod:Redguard File Formats
- UESP: User:Daveh/Redguard File Formats
- RGUnity/redguard-unity
RGFileImport/RGGFXImport/
PVO Parser — Pseudocode
Reference parser for the PVO (Pre-computed Visibility Octree) file format. See PVO Format for format specification.
All field-level details below are validated against all 5 shipped PVO files with byte-exact section boundary matches.
Data Types
u8 — unsigned 8-bit
u16_le — unsigned 16-bit, little-endian
u32_le — unsigned 32-bit, little-endian
i32_le — signed 32-bit, little-endian
u32_be — unsigned 32-bit, big-endian
tag — 4 ASCII bytes (e.g. "OCTH")
Structures
struct PvoFile {
header: OcthHeader,
nodes: Vec<OctrNode>, // OCTR section, indexed by byte offset
leaves: Vec<PlstLeaf>, // PLST section, indexed by byte offset
mlst: Vec<u16_le>, // MLST section, flat polygon index table
}
struct OcthHeader {
depth: u32, // always 10 — max octree depth
total_nodes: u32, // len(nodes)
leaf_nodes: u32, // nodes with leaf_ref != 0xFFFFFFFF
mlst_polygon_count: u32, // len(mlst)
reserved: u32, // always 0
cell_size: u32, // root half-extent (16384 or 8192)
center_x: i32, // octree root center
center_y: i32,
center_z: i32,
_pad: [u32; 4], // always zero
}
struct OctrNode {
byte_offset: usize, // position within OCTR data (for child_ref lookups)
child_mask: u8, // bit i set → child i present (octants 0..7)
leaf_ref: u32, // byte offset into PLST, or 0xFFFFFFFF (no leaf)
child_refs: Vec<(u8, u32)>, // (octant_index, byte_offset into OCTR) per set bit
}
struct PlstLeaf {
byte_offset: usize, // position within PLST data (for leaf_ref lookups)
entries: Vec<PlstEntry>,
}
struct PlstEntry {
count: u16, // number of polygon indices in this sub-list
mlst_start: u32, // starting index into the MLST array
}
Section Framing
Every section uses identical framing. Parse sections sequentially until END .
fn read_section_header(reader) -> (tag: [u8; 4], data_length: u32):
tag = reader.read_bytes(4)
data_length = reader.read_u32_be()
return (tag, data_length)
Top-Level Parser
fn parse_pvo(reader) -> PvoFile:
// --- OCTH ---
(tag, data_length) = read_section_header(reader)
assert tag == "OCTH"
assert data_length == 52
header = parse_octh(reader)
// --- OCTR ---
(tag, data_length) = read_section_header(reader)
assert tag == "OCTR"
nodes = parse_octr(reader, data_length)
// --- PLST ---
(tag, data_length) = read_section_header(reader)
assert tag == "PLST"
leaves = parse_plst(reader, data_length)
// --- MLST ---
(tag, data_length) = read_section_header(reader)
assert tag == "MLST"
assert data_length == header.mlst_polygon_count * 2
mlst = parse_mlst(reader, data_length)
// --- END ---
(tag, data_length) = read_section_header(reader)
assert tag == "END "
assert data_length == 0
// --- Validation ---
assert len(nodes) == header.total_nodes
leaf_count = count(n for n in nodes if n.leaf_ref != 0xFFFFFFFF)
assert leaf_count == header.leaf_nodes
return PvoFile { header, nodes, leaves, mlst }
OCTH Parser
fn parse_octh(reader) -> OcthHeader:
header = OcthHeader {
depth: reader.read_u32_le(), // 0x08
total_nodes: reader.read_u32_le(), // 0x0C
leaf_nodes: reader.read_u32_le(), // 0x10
mlst_polygon_count: reader.read_u32_le(), // 0x14
reserved: reader.read_u32_le(), // 0x18 — always 0
cell_size: reader.read_u32_le(), // 0x1C
center_x: reader.read_i32_le(), // 0x20
center_y: reader.read_i32_le(), // 0x24
center_z: reader.read_i32_le(), // 0x28
_pad: reader.read_bytes(16), // 0x2C — always zero
}
return header
OCTR Parser
Octree nodes are serialized as a flat sequence of variable-length records. Each record describes one octree node. The records are addressed by byte offset within the OCTR data section.
fn parse_octr(reader, data_length: u32) -> Vec<OctrNode>:
nodes = []
bytes_read = 0
while bytes_read < data_length:
node_offset = bytes_read
child_mask = reader.read_u8()
leaf_ref = reader.read_u32_le()
bytes_read += 5
// Read one child_ref per set bit in child_mask (low bit first)
child_refs = []
for octant in 0..8:
if child_mask & (1 << octant) != 0:
ref = reader.read_u32_le()
child_refs.push((octant, ref))
bytes_read += 4
nodes.push(OctrNode {
byte_offset: node_offset,
child_mask,
leaf_ref,
child_refs,
})
assert bytes_read == data_length
return nodes
Node record binary layout
Byte 0 Bytes 1..4 Bytes 5..5+4n
┌──────────┬─────────────────┬────────────────────────────────┐
│child_mask│ leaf_ref │ child_ref[0] .. child_ref[n-1] │
│ (u8) │ (u32_le) │ (u32_le each) │
└──────────┴─────────────────┴────────────────────────────────┘
n = popcount(child_mask)
record_size = 5 + 4n
Octant numbering
The 3-bit octant index encodes spatial position relative to the node center:
bit 0 (1) = z > center_z
bit 1 (2) = y > center_y
bit 2 (4) = x > center_x
Octant 0 = (x-, y-, z-) Octant 4 = (x+, y-, z-)
Octant 1 = (x-, y-, z+) Octant 5 = (x+, y-, z+)
Octant 2 = (x-, y+, z-) Octant 6 = (x+, y+, z-)
Octant 3 = (x-, y+, z+) Octant 7 = (x+, y+, z+)
Interpreting references
leaf_ref: byte offset into the PLST section data.0xFFFFFFFF= no leaf (interior-only node).child_refs: byte offset into the OCTR section data. Use to look up child nodes by theirbyte_offsetfield.
Reference consistency in shipped files:
- 100% of
child_refsmatch a nodebyte_offset. - 100% of
leaf_refsfall within PLST bounds.
PLST Parser
Leaf records describe which polygon groups are visible from an octree cell. Each leaf contains a list of (count, mlst_start) entries that reference ranges within the MLST polygon index table. Multiple leaves may share the same MLST ranges.
fn parse_plst(reader, data_length: u32) -> Vec<PlstLeaf>:
leaves = []
bytes_read = 0
while bytes_read < data_length:
leaf_offset = bytes_read
entry_count = reader.read_u8()
bytes_read += 1
entries = []
for _ in 0..entry_count:
count = reader.read_u16_le()
mlst_start = reader.read_u32_le()
entries.push(PlstEntry { count, mlst_start })
bytes_read += 6
leaves.push(PlstLeaf {
byte_offset: leaf_offset,
entries,
})
assert bytes_read == data_length
return leaves
Leaf record binary layout
Byte 0 Bytes 1..1+6n
┌────────────┬──────────────────────────────────────────┐
│entry_count │ entry[0] .. entry[n-1] │
│ (u8) │ [count:u16][mlst_start:u32] each │
└────────────┴──────────────────────────────────────────┘
record_size = 1 + 6 * entry_count
Entry semantics
Each entry references a contiguous slice of the MLST array:
polygons = mlst[mlst_start .. mlst_start + count]
Constraint: mlst_start + count <= header.mlst_polygon_count.
Entries within a leaf represent distinct polygon groups (e.g. different model faces, terrain sections). The full set of visible polygons for a leaf is the union of all its entry slices.
MLST Parser
Flat array of u16_le placed-object visibility IDs. Each entry is an MPSO record index identifying a placed object for visibility determination, not an individual face/polygon index.
fn parse_mlst(reader, data_length: u32) -> Vec<u16>:
count = data_length / 2
mlst = []
for _ in 0..count:
mlst.push(reader.read_u16_le())
return mlst
Octree Reconstruction
To build the tree in memory from the flat node list:
fn build_tree(nodes: Vec<OctrNode>) -> OctrNode:
// Build lookup: byte_offset -> node index
offset_to_idx = {}
for (i, node) in nodes.enumerate():
offset_to_idx[node.byte_offset] = i
// The root is always the first node (byte_offset 0)
root = nodes[0]
// Recursively link children
fn link(node_idx, nodes, offset_to_idx):
node = nodes[node_idx]
for (octant, child_offset) in node.child_refs:
child_idx = offset_to_idx[child_offset]
node.children[octant] = link(child_idx, nodes, offset_to_idx)
return node
return link(0, nodes, offset_to_idx)
Visibility Query
Given a world-space point, traverse the octree to find which polygons are visible:
fn query_visible(tree: OctrNode, header: OcthHeader, leaves: Vec<PlstLeaf>,
mlst: Vec<u16>, point_x: i32, point_y: i32, point_z: i32) -> Set<u16>:
node = tree
cx, cy, cz = header.center_x, header.center_y, header.center_z
half = header.cell_size
// Walk from root to leaf
while true:
// Determine octant for query point
octant = 0
if point_z > cz: octant |= 1
if point_y > cy: octant |= 2
if point_x > cx: octant |= 4
// Descend into child
if node.child_mask & (1 << octant) == 0:
break // no child in this octant
child_offset = node.child_ref_for_octant(octant)
node = tree.lookup(child_offset)
// Update center and half-extent for child cell
half = half / 2
if point_x > cx: cx += half else: cx -= half
if point_y > cy: cy += half else: cy -= half
if point_z > cz: cz += half else: cz -= half
// Collect visible polygons from the leaf
visible = Set()
if node.leaf_ref != 0xFFFFFFFF:
leaf = leaves.lookup(node.leaf_ref)
for entry in leaf.entries:
for i in entry.mlst_start .. entry.mlst_start + entry.count:
visible.add(mlst[i])
return visible
SOUP386.DEF
Runtime text definition file that declares the SOUP scripting API surface (functions, references, attributes, and flags).
Overview
SOUP386.DEF is loaded by the runtime and used to build script-call metadata at startup. Compiled script bytecode in RASC/.AI refers to function and flag indices that are resolved using this definition file.
The file is plain text and organized as named sections.
Section Layout
| Section | Delimiter | Content |
|---|---|---|
| Functions | [functions] … [refs] | One callable entry per line: <type> <name> params <count> where type is task or function |
| References | [refs] … auto | One reference/equate name per line |
| Attributes | auto … endauto | One attribute name per line (maps to per-actor RAAT byte slots) |
| Flags | [flags] … EOF | One flag per line: <type> <name> <value>[;<comment>] with types BOOL, NUMBER, FLIPFLOP |
Function index 0 is treated as NullFunction in runtime behavior.
Relationship to RGM Script Data
RASCand standalone.AIcontain compiled script bytecode. No.AIfiles ship with the game — see SOUP Scripting for details.- Bytecode function calls encode function IDs (u16 indices).
- Those indices are resolved using the runtime function table built from
[functions]inSOUP386.DEF. - RAAT attribute bytes are interpreted using names declared in the
auto…endautoattribute block.
See RGM.md for RASC/RAAT container details and SOUP.md for script-source boundaries.
Notes
- Runtime behavior includes a DEF-to-script compatibility/checksum validation path in the engine.
- Some declared functions do not appear in shipped map scripts; declaration in DEF does not imply invocation.
External References
- RGUnity/redguard-unity
soupdeffcn_nimpl.cs - Dillonn241/redguard-mod-manager
MapDatabase.java - UESP
Mod:RGM File Format
Configuration Files
Text-based INI configuration files shipped with the game.
Game Root Directory
| File | Size | Docs | Description |
|---|---|---|---|
SYSTEM.INI | 5.2 KB | system-ini.md | Primary engine configuration: rendering, gameplay, camera, dialog, debug, and 3D subsystem parameters. |
COMBAT.INI | 54 KB | combat-ini.md | Combat system: attack/defense moves, combos, and dialogue taunts for all combatants. |
ITEM.INI | 30 KB | item-ini.md | Item database: all collectible objects, weapons, potions, keys, and quest items. |
WORLD.INI | 19 KB | world-ini.md | World/level database: map files, palettes, lighting, sky, weather, and PVO node maps. |
MENU.INI | 42 KB | menu-ini.md | Menu system layout: page structure, text placement, textures, and movie definitions. |
KEYS.INI | 3 KB | keys-ini.md | Input bindings: keyboard scancodes, mouse buttons, and joystick axes to game actions. |
REGISTRY.INI | 302 B | registry-ini.md | File-system abstraction: archive lookup and 32-bit file access (non-functional in shipped game). |
SURFACE.INI | 4.3 KB | surface-ini.md | Terrain surface-type assignments, blend behavior, and sound remapping. |
Asset Directory Files
| File | Path | Docs | Description |
|---|---|---|---|
FOG.INI | fxart/ | fog-ini.md | Fog density ramp table for the terrain renderer. |
DIG.INI | sound/ | — | Miles Sound System digital audio driver configuration. |
MDI.INI | sound/ | — | Miles Sound System MIDI driver configuration. |
SYSTEM.INI
Primary engine configuration file controlling rendering, gameplay, camera, dialog, debug, and 3D subsystem parameters.
Shipped sample path: /Redguard/SYSTEM.INI (e.g. .../GOG Galaxy/Redguard/Redguard/SYSTEM.INI).
File Structure
Standard Windows-style INI file with 10 sections:
[screen]— display resolution and palette settings[system]— core engine paths, audio, physics, and HUD layout[debug]— developer diagnostics and logging flags[game]— gameplay physics thresholds and interaction radii[cyrus]— player character movement and control parameters[3dmanager]— 3D object cache and memory budget[xngine]— renderer texture memory, clipping planes, and sky behavior[camera]— camera rig offsets, distances, and glide factors for each mode[dialog]— dialog menu layout and speech settings[3dfx]— Glide/GOG-specific resolution and font scaling overrides
[screen]
Display mode and palette initialization settings.
| Key | Default Value | Description |
|---|---|---|
candle_mode | 2 | Candle/torch lighting mode index. |
colour_bits | 8 | Color depth in bits per pixel (8 = paletted). |
resolution | 1 | Software renderer resolution index. |
Palette_red | 0 | Red component of the initial palette background color. |
Palette_green | 0 | Green component of the initial palette background color. |
Palette_blue | 0 | Blue component of the initial palette background color. |
smk_interlace | 0 | Smacker video interlace mode. 0 = disabled. |
[system]
Core engine paths, audio configuration, physics constants, HUD element positions, and subsystem enable flags.
world_ini and item_ini point to WORLD.INI and ITEM.INI respectively, which the engine loads for world and item definitions.
| Key | Default Value | Description |
|---|---|---|
game_bitmap | system\powerup.gxa | Path to the startup/powerup UI bitmap (GXA format). |
pointers | system\pointers.bmp | Path to the cursor sprite sheet. |
system_font | fonts\redguard.fnt | Path to the primary system font. |
icon_font | fonts\arialvs.fnt | Path to the icon/small font. |
gui_font | fonts\arialbg.fnt | Path to the main GUI font. |
gui_low_font | fonts\arialvb.fnt | Path to the low-resolution GUI font. |
animation_drive | D:\ | Drive letter used for animation streaming. |
back_texture | 0 | Background texture index. 0 = none. |
volume | 255 | Master sound volume (0..255). |
sound | 1 | Sound system enabled. 1 = on. |
fast_sound | 1 | Fast sound mixing mode. 1 = on. |
fidelity | 0 | Sound fidelity level. |
redbook | on | CD audio (Red Book) playback enabled. |
redbook_volume | 200 | CD audio volume level. |
sound_distance | 64 | Maximum distance at which sounds are audible. |
post_collide_height | 50 | Post-collision step height for ground snapping. |
hpost_collide_height | 50 | Post-collision step height for hanging/climbing. |
pre_validate | no | Pre-validate collision geometry on load. |
normal_frame_rate | 12 | Target frame rate for normal gameplay. |
use_smooth_fps | yes | Enable frame-rate smoothing. |
use_smooth_divisor | yes | Enable frame-rate smoothing divisor. |
min_frame_rate | 6 | Minimum allowed frame rate. |
max_frame_rate | 300 | Maximum allowed frame rate. |
jump_time | 6 | Duration of a jump in frames. |
jump_height | 80 | Jump height in engine units. |
sphere_object_scale | 256 | Scale factor for sphere collision objects. |
standing_height | -2 | Vertical offset for the player standing position. |
disable_text | yes | Disable on-screen text rendering. |
disable_debug_text | yes | Disable debug text overlay. |
normal_ink | 1 | Normal ink/outline rendering mode. |
slide_range | -60000 | Slide detection range threshold. |
statics_load | yes | Load static objects. |
flats_load | yes | Load flat/billboard objects. |
objects_load | yes | Load dynamic objects. |
lights_load | yes | Load light objects. |
ropes_load | yes | Load rope objects. |
task_system | yes | Enable the task/AI system. |
animation_system | yes | Enable the animation system. |
floating_point_physics | yes | Use floating-point physics calculations. |
rtx_filename | ENGLISH.RTX | Path to the dialogue/voice container file. |
world_ini | WORLD.INI | Path to the world definitions INI. |
item_ini | ITEM.INI | Path to the item definitions INI. |
start_fade | on | Fade in on game start. |
compass_xco | 546 | Compass HUD element X coordinate. |
compass_yco | 396 | Compass HUD element Y coordinate. |
candle_xco | 12 | Candle HUD element X coordinate. |
candle_yco | 8 | Candle HUD element Y coordinate. |
logbook_xco | 540 | Logbook HUD element X coordinate. |
logbook_yco | 20 | Logbook HUD element Y coordinate. |
pickup_xco | 12 | Pickup prompt HUD element X coordinate. |
pickup_yco | 386 | Pickup prompt HUD element Y coordinate. |
pickup_text_yco | 436 | Pickup text HUD element Y coordinate. |
game_xco1 | 576 | Game UI element 1 X coordinate. |
game_yco1 | 6 | Game UI element 1 Y coordinate. |
game_xco2 | 576 | Game UI element 2 X coordinate. |
game_yco2 | 96 | Game UI element 2 Y coordinate. |
lock_windows | no | Lock window position/size. |
disable_drive_check | yes | Skip CD drive presence check on startup. |
disable_cpu_check | yes | Skip CPU speed check on startup. |
disable_svga_check | yes | Skip SVGA capability check on startup. |
report_machine | no | Log machine hardware info on startup. |
max_active_objects | 32 | Maximum number of simultaneously active objects. |
max_effects | 32 | Maximum number of simultaneous particle effects. |
max_particles | 512 | Maximum number of simultaneous particles. |
max_remap_objects | 64 | Maximum number of palette-remapped objects. |
disable_effects | no | Disable particle effects. |
[debug]
Developer diagnostics, logging, and display flags. Most are disabled in the shipped build.
network_marker_file=g:\PROJECTS\REDGUARD\DEMO\NETWORK.MRK is a build-time artifact: a hardcoded developer machine path left in the shipped file.
| Key | Default Value | Description |
|---|---|---|
final_version | 1 | Marks this as a final/release build. Suppresses some developer output. |
enable_logs | 0 | Enable runtime log file writing. |
map_log | no | Log map loading events. |
console_error | yes | Print errors to the console. |
software_interrupt | yes | Enable software interrupt handling. |
software_break | yes | Enable software breakpoint handling. |
attempt_recover | no | Attempt to recover from errors rather than aborting. |
object_log | no | Log object system events. |
video_log | no | Log video/renderer events. |
node_marker | no | Display navigation node markers. |
task_debug | no | Enable task/AI debug output. |
memory_manager | 0 | Memory manager debug level. |
monitor_object | (empty) | Name of an object to monitor for debug output. |
display_masters | no | Display master object markers. |
display_slaves | no | Display slave object markers. |
display_edges | no | Display edge/collision markers. |
ignore_errors | yes | Continue running on non-fatal errors. |
disable_family | no | Disable object family grouping. |
disable_master_slaves | no | Disable master/slave object relationships. |
disable_slaves | no | Disable slave objects. |
family_log | no | Log object family events. |
script_log | no | Log script execution events. |
show_manager | no | Display the object manager overlay. |
display_node_map | no | Display the navigation node map. |
display_nodes | no | Display individual navigation nodes. |
display_markers | no | Display world markers. |
network_marker_file | g:\PROJECTS\REDGUARD\DEMO\NETWORK.MRK | Path to the network marker file. Build-time developer path; not used in the shipped game. |
object_system_log | no | Log object system events. |
convert_static_angles | no | Convert static object angles on load. |
def_checksum | yes | Verify SOUP386.DEF checksum on load. |
render_log | no | Log renderer events. |
idebug | 0 | Interactive debug level. |
idebug_refresh | no | Refresh interactive debug display each frame. |
memory_monitor | no | Enable memory usage monitoring. |
[game]
Gameplay physics thresholds, fall damage, and interaction radii.
| Key | Default Value | Description |
|---|---|---|
fall_bounce_height | 100 | Fall height (engine units) below which the player bounces without damage. |
fall_death_height | 568 | Fall height at which the player dies. |
fall_hurt_height | 300 | Fall height at which the player takes damage. |
fall_hurt_zap | 10 | Damage amount applied at fall_hurt_height. |
slide_hurt_height | 1024 | Slide distance at which the player takes damage. |
slide_hurt_zap | 10 | Damage amount applied at slide_hurt_height. |
rope_jump_add | 18 | Velocity added when jumping from a rope. |
rope_attach_angle | 512 | Angle threshold for rope attachment (engine angle units). |
slide_speed | 36 | Player slide speed in engine units per frame. |
rtx_pickup_override_time | 24 | Duration (frames) of the pickup text override display. |
swim_depth | 40 | Depth threshold for switching to swim mode. |
player_dead_time | 3 | Time (seconds) before respawn after death. |
player_fall_dead_time | 12 | Time (frames) before death is registered after a fatal fall. |
old_combat | off | Use legacy combat system. |
lineup_distance | 128 | Distance at which enemies line up for combat. |
dialog_radius | 512 | Radius within which NPCs can initiate dialog. |
combat_sphere_scale | 200 | Scale factor for combat hit sphere. |
[cyrus]
Player character (Cyrus) movement, control, and camera-follow parameters.
| Key | Default Value | Description |
|---|---|---|
sheath_sword_delay | 6 | Frames before the sword auto-sheathes after combat. |
auto_defend | 1 | Enable automatic defense. 1 = on. |
walk_mode | 1 | Walk mode index. |
turn_speed | 12 | Base turning speed. |
turn_max_speed | 48 | Maximum turning speed. |
mouse_turn | 0 | Mouse-driven turning. 0 = disabled. |
joy_tolerance | 60 | Joystick dead-zone tolerance. |
camera_distance | 200 | Default camera follow distance. |
tap_time | 12 | Maximum frames between taps for a double-tap input. |
poly_push_units | 16 | Distance (engine units) the player is pushed out of geometry on collision. |
auto_grab | off | Automatically grab ledges and ropes. |
smooth_post_min | 2 | Minimum smoothing steps for post-collision position. |
smooth_post_max | 10 | Maximum smoothing steps for post-collision position. |
smooth_post_divisor | 2 | Divisor for post-collision position smoothing. |
[3dmanager]
3D object cache, memory budgets, and manager behavior.
| Key | Default Value | Description |
|---|---|---|
buffer_kbytes | 800 | Size of the 3D streaming buffer in kilobytes. |
heap_kbytes | 22000 | Size of the 3D object heap in kilobytes. |
max_objects | 255 | Maximum number of 3D objects loaded simultaneously. |
compress | 1 | Enable compressed object loading. 1 = on. |
save_compressed | 0 | Save objects in compressed form. 0 = off. |
cache_objects | yes | Cache loaded 3D objects in memory. |
cache_lifetime | 64 | Number of frames a cached object is retained after last use. |
dummy_manager | 0 | Use a dummy (no-op) 3D manager. 0 = off. |
in_view_enabled | yes | Enable in-view culling for 3D objects. |
shutdown_mode | yes | Perform full shutdown cleanup on exit. |
[xngine]
Renderer memory budgets, clipping planes, perspective settings, and sky behavior.
| Key | Default Value | Description |
|---|---|---|
texture_kbytes | 12000 | Texture memory budget in kilobytes. |
gfx_kbytes | 256 | General graphics buffer size in kilobytes. |
front_plane | 7 | Near clipping plane distance. |
back_plane | 3800 | Far clipping plane distance. |
perspective_low_x | 190 | Perspective correction X extent at low detail. |
perspective_low_y | 190 | Perspective correction Y extent at low detail. |
perspective_med_x | 400 | Perspective correction X extent at medium detail. |
perspective_med_y | 400 | Perspective correction Y extent at medium detail. |
perspective_high_x | 800 | Perspective correction X extent at high detail. |
perspective_high_y | 800 | Perspective correction Y extent at high detail. |
perspective_ultra_x | 800 | Perspective correction X extent at ultra detail. |
perspective_ultra_y | 800 | Perspective correction Y extent at ultra detail. |
detail | 8 | Renderer detail level. |
ambient_light | 32 | Global ambient light level (0..255). |
screen_scale | 256 | Screen-space scale factor. |
haze_depth | 768 | Distance at which atmospheric haze begins. |
sky_disable | 0 | Disable sky rendering. 0 = sky enabled. |
sky_move | 1 | Enable sky scrolling. 1 = on. |
sky_xrotate | 3 | Sky X-axis rotation speed. |
sky_yrotate | 40 | Sky Y-axis rotation speed. |
game_detail | 2 | In-game detail preset index. |
exclusion | 0 | Exclusion zone rendering mode. 0 = off. |
[camera]
Camera rig configuration for all gameplay modes: normal follow, combat, hanging, rope, and debug. Coordinates and angles use engine units. Floating-point glide factors control camera lag.
| Key | Default Value | Description |
|---|---|---|
static_rope_threshold | 5 | Velocity threshold below which a rope is considered static. |
obstacle_size | 20 | Radius used for camera obstacle avoidance. |
camera_size | 10 | Camera collision sphere radius. |
camera_scape_size | 5 | Camera collision sphere radius in scape/exterior areas. |
target_offset_x | -6000 | Target point X offset from the player. |
target_offset_y | -14000 | Target point Y offset from the player. |
target_offset_z | 0 | Target point Z offset from the player. |
offset_pos_x | 0 | Camera position X offset. |
offset_pos_y | -14000 | Camera position Y offset. |
offset_pos_z | 0 | Camera position Z offset. |
offset_angle_x | 0 | Camera angle X offset. |
offset_angle_y | 0 | Camera angle Y offset. |
offset_angle_z | 0 | Camera angle Z offset. |
camera_distance | 250 | Default follow camera distance. |
camera_min_distance | 120 | Minimum allowed camera distance. |
camera_right_pos | 5000 | Camera right-side position limit. |
camera_left_pos | 5000 | Camera left-side position limit. |
camera_combat_angle_offset_x | 134 | Combat camera X angle offset. |
camera_combat_angle_offset_y | 245 | Combat camera Y angle offset. |
camera_combat_angle_offset_z | 0 | Combat camera Z angle offset. |
camera_combat_distance | 260 | Camera distance in combat mode. |
camera_hang_angle_offset_x | 256 | Hanging camera X angle offset. |
camera_hang_angle_offset_y | 0 | Hanging camera Y angle offset. |
camera_hang_angle_offset_z | 0 | Hanging camera Z angle offset. |
camera_hang_distance | 250 | Camera distance in hanging mode. |
camera_rope_max_vel | 18000 | Maximum camera velocity when following a rope. |
camera_rope_angle_offset_x | 96 | Rope camera X angle offset. |
camera_rope_angle_offset_y | 128 | Rope camera Y angle offset. |
camera_rope_angle_offset_z | 0 | Rope camera Z angle offset. |
camera_rope_distance | 300 | Camera distance in rope mode. |
camera_rope_above_angle_offset_x | 96 | Rope-above camera X angle offset. |
camera_rope_above_angle_offset_y | 128 | Rope-above camera Y angle offset. |
camera_rope_above_angle_offset_z | 0 | Rope-above camera Z angle offset. |
camera_rope_above_distance | 300 | Camera distance in rope-above mode. |
camera_rope_above_aim_x | -512 | Rope-above camera aim X offset. |
camera_rope_above_aim_y | 0 | Rope-above camera aim Y offset. |
camera_rope_above_aim_z | 0 | Rope-above camera aim Z offset. |
camera_rope_below_angle_offset_x | 96 | Rope-below camera X angle offset. |
camera_rope_below_angle_offset_y | 128 | Rope-below camera Y angle offset. |
camera_rope_below_angle_offset_z | 0 | Rope-below camera Z angle offset. |
camera_rope_below_distance | 300 | Camera distance in rope-below mode. |
camera_rope_below_aim_x | 512 | Rope-below camera aim X offset. |
camera_rope_below_aim_y | 0 | Rope-below camera aim Y offset. |
camera_rope_below_aim_z | 0 | Rope-below camera aim Z offset. |
camera_debug_angle_offset_x | 128 | Debug camera X angle offset. |
camera_debug_angle_offset_y | 256 | Debug camera Y angle offset. |
camera_debug_angle_offset_z | 0 | Debug camera Z angle offset. |
camera_debug_distance | 300 | Camera distance in debug mode. |
max_x_angle | 1692 | Maximum camera X angle (engine angle units). |
min_x_angle | 400 | Minimum camera X angle (engine angle units). |
max_vel | 8000 | Maximum camera velocity. |
max_y_vel | 5000 | Maximum camera Y-axis velocity. |
max_acc | 5000 | Maximum camera acceleration. |
player_control_x_inc | 70 | Player-controlled camera X increment per frame. |
player_control_y_inc | -70 | Player-controlled camera Y increment per frame. |
glide_x | 0.9 | Camera position X glide (lag) factor. |
glide_y | 0.2 | Camera position Y glide (lag) factor. |
glide_z | 0.9 | Camera position Z glide (lag) factor. |
glide_angle_x | 0.7 | Camera angle X glide (lag) factor. |
glide_angle_y | 0.1 | Camera angle Y glide (lag) factor. |
glide_angle_z | 0.7 | Camera angle Z glide (lag) factor. |
cam_prox_up | 70.0 | Camera proximity upward adjustment distance. |
[dialog]
Dialog menu layout, line limits, and speech settings.
| Key | Default Value | Description |
|---|---|---|
menu_start_x | 35 | Dialog menu X start position in screen pixels. |
menu_start_y | 25 | Dialog menu Y start position in screen pixels. |
dialog_max_menu_items | 20 | Maximum number of items in a dialog menu. |
dialog_max_dialog_lines | 20 | Maximum number of lines in a dialog text block. |
dialog_max_text_width | 500 | Maximum width of dialog text in pixels. |
dialog_print_text | 1 | Display dialog as on-screen text. 1 = on. |
dialog_use_speech | 1 | Play voiced speech audio during dialog. 1 = on. |
dialog_max_distance | 1600 | Maximum distance at which dialog audio is played. |
menu_traverse_delay | 4 | Frames of delay between menu cursor movements. |
[3dfx]
Glide renderer overrides, active in the GOG release which runs via the Glide/3dfx code path. These settings take precedence over the software-renderer equivalents where applicable.
| Key | Default Value | Description |
|---|---|---|
resolution | 12 | Glide renderer resolution index. |
text_scale | 1, 1 | Text scaling factors (X, Y) for the Glide renderer. |
anim_text_scale | 1, 1 | Animated text scaling factors (X, Y) for the Glide renderer. |
font_sel | 255,255 | Font selection color values (foreground, background) for selected menu items. |
font_norm | 125,255 | Font color values (foreground, background) for normal menu items. |
font_used | 125,128 | Font color values (foreground, background) for used/visited menu items. |
External References
COMBAT.INI
Combat system configuration defining attack moves, defense moves, and combat dialogue (taunts) for all combatants.
Shipped sample path: /Redguard/COMBAT.INI (e.g. .../GOG Galaxy/Redguard/Redguard/COMBAT.INI).
The file is 54 KB and 3,728 lines, the largest INI in the game. Every move, defense, and voice line for every combatant is defined here, making the combat system fully data-driven.
File Structure
The file contains 474 sections in four types:
| Section type | Count | Index range | Purpose |
|---|---|---|---|
[misc] | 1 | — | Global combat parameters |
[attackNN] | 89 | 00–88 | Attack move definitions |
[defendNN] | 5 | 00–04 | Defense move definitions |
[tauntNN] | 379 | 00–599 (with gaps) | Combat dialogue and voice lines |
The file opens with a block of commented-out constants defining animation group IDs and attack type enums, followed by the sections in the order listed above.
Animation Groups
The header comments define the animation group ID table used by animation fields throughout the file:
| ID | Name |
|---|---|
| 1 | anim_defend_low |
| 2 | anim_defend_right |
| 3 | anim_defend_left |
| 4 | anim_defend_high |
| 5 | anim_attack_1 |
| 6 | anim_attack_2 |
| 7 | anim_attack_3 |
| 8 | anim_attack_thrust |
| 9 | anim_attack_lunge |
| 10 | anim_attack_1_end |
| 11 | anim_attack_2_end |
| 12 | anim_fight_disarm |
| 13 | anim_fight_low |
| 14 | anim_fight_jump_start |
| 15 | anim_fight_jump |
| 16 | anim_fight_fall |
| 17 | anim_fight_land |
| 18 | anim_fight_fall_attack |
| 19 | anim_fight_land_attack |
| 20 | anim_fight_hurt_1 |
| 21 | anim_fight_hurt_2 |
| 22 | anim_death_fight_stab |
| 23 | anim_death_fight_hard |
| 24 | anim_sheath_sword |
| 25 | anim_explore_hurt_1 |
| 26 | anim_explore_hurt_2 |
| 27 | anim_death_explore |
[misc] Section
Global parameters for the combat system.
| Key | Value (shipped) | Description |
|---|---|---|
sword_clank_1 | 57 | Sound effect ID for sword clash, variant 1 |
sword_clank_2 | 58 | Sound effect ID for sword clash, variant 2 |
non_engaged_dist | 100 | Distance threshold for non-engaged state |
in_face_dist | 100 | Distance threshold for in-face proximity |
non_engaged_spacing | 500 | Spacing between non-engaged combatants, in engine angles |
in_combat_threshold | 200 | Distance threshold to enter combat state, in units × 256 |
player_can_die | 1 | Whether the player can be killed (1 = yes) |
cyrus_defend_interval | 2 | Minimum frames between Cyrus defends |
node_timer | 120 | Timer value for combat node transitions |
max_taunts | 600 | Maximum taunt index (exclusive upper bound for taunt pool) |
[attackNN] Sections
Each [attackNN] section defines one attack move. There are 89 sections (attack00 through attack88).
Attack Types
The type field selects the attack category:
| Value | Name |
|---|---|
| 0 | melee |
| 1 | missile |
| 2 | combo |
| 3 | stab |
| 4 | finishing |
Elevation Values
The elevation field controls the vertical targeting zone:
| Value | Meaning |
|---|---|
| 0 | regular (default when omitted) |
| 1 | above |
| 2 | below |
Field Schema
Standard attack (type 0, 1, 3, 4):
| Field | Type | Description |
|---|---|---|
type | integer | Attack category (see enum above) |
arc | integer | Horizontal hit arc width, in engine angle units |
arc_center | integer | Horizontal offset of arc center from forward direction; omitted when centered (0) |
elevation | integer | Vertical targeting zone (see enum above); omitted when regular (0) |
min_range | integer | Minimum distance to target for hit to register |
max_range | integer | Maximum distance to target for hit to register |
first_collide_frame | integer | Animation frame on which collision detection begins |
collide_duration | integer | Number of frames collision detection remains active |
damage | integer | Hit point damage dealt on successful hit |
force | integer | Knockback force applied to target |
defense | integer | Defense animation group ID triggered on the target |
first_defend_frame | integer | Animation frame on which the attacker can be defended against; omitted on some attacks |
defend_duration | integer | Number of frames the attacker is vulnerable to defense; omitted on some attacks |
animation | integer | Animation group ID (see animation group table above) |
col_vertex | integer | Model vertex index used as the collision sphere origin; present only on large creature attacks |
col_sphere_size | integer | Radius of the collision sphere; present only on large creature attacks |
Combo attack (type 2):
Combo sections reference other attack sections by index rather than defining collision geometry directly.
| Field | Type | Description |
|---|---|---|
type | integer | Always 2 for combos |
num_attacks | integer | Number of sub-attacks in the combo (2 or 3) |
attack00 | integer | Index of the first sub-attack section |
attack01 | integer | Index of the second sub-attack section |
attack02 | integer | Index of the third sub-attack section; present only when num_attacks = 3 |
Attack Allocation by Combatant
The file comments identify which attack indices belong to each combatant:
| Combatant | Attack indices |
|---|---|
| Cyrus (early) | 00–14 |
| Skeleton | 15–17 |
| Golem | 18–22 |
| Zombie | 23–25 |
| Serpent | 26 |
| Troll | 27–29 |
| Guard (low attack) | 30 |
| Goblin | 31–34 |
| Dragon | 35–37 |
| Richton | 38–47 |
| Dram | 48–52 |
| Ogre | 53 |
| Pirate (initial, weaker) | 54–64 |
| Cyrus (later) | 65–74 |
| Vermai | 75–77 |
| Tavern thugs (no damage) | 78–88 |
Large creature attacks (Golem, Serpent, Dragon) use col_vertex and col_sphere_size to attach the collision sphere to a specific model vertex rather than the combatant’s origin point.
[defendNN] Sections
Each [defendNN] section defines one defense move. There are 5 sections (defend00 through defend04), shared by Cyrus and guards.
Field Schema
| Field | Type | Description |
|---|---|---|
arc | integer | Horizontal arc width covered by the defense; omitted on defend00 |
min_range | integer | Minimum distance at which the defense is effective |
max_range | integer | Maximum distance at which the defense is effective |
first_collide_frame | integer | Animation frame on which the defense window opens |
collide_duration | integer | Number of frames the defense window remains active |
animation | integer | Animation group ID (see animation group table above) |
The five shipped defenses correspond to: default (defend00), low (defend01), right (defend02), left (defend03), and high (defend04).
[tauntNN] Sections
Each [tauntNN] section defines one combat voice line or audio cue. There are 379 sections with indices spanning 00–599, with large gaps between combatant blocks.
Taunt Types
The type field identifies when the taunt fires:
| Value | Trigger |
|---|---|
| 0 | begin combat |
| 1 | attack |
| 2 | hurt by opponent |
| 3 | hit opponent |
| 4 | defend |
| 5 | misc, during move |
| 6 | death |
| 7 | after kill opponent |
| 8 | want to switch out |
| 9 | want to switch in |
| 10 | opponent is unarmed (male) |
| 11 | opponent is unarmed (female) |
Field Schema
| Field | Type | Description |
|---|---|---|
type | integer | Trigger condition (see enum above) |
rtx_label | string | Label key into ENGLISH.RTX for the voice line; may be a 4-character code or a &NNN numeric reference |
animation | integer | Animation group ID to play alongside the voice line; 0 in all shipped entries |
Taunt Allocation by Combatant
The file comments define the index ranges reserved for each combatant:
| Index range | Combatant |
|---|---|
| 0–49 | Cyrus |
| 50–149 | Guards |
| 150–154 | Skeletons |
| 155–159 | Zombies |
| 160–179 | Tavern Thug 1 |
| 180–199 | Tavern Thug 2 |
| 200–219 | Tavern Thug 3 (Dagoo) |
| 220–228 | Brennan |
| 229–234 | Golem |
| 235–239 | Serpent / Vermai |
| 240–249 | Troll |
| 250–259 | Goblin |
| 260–279 | Richton |
| 280–299 | Dram |
| 300–319 | Pirate 1 |
| 320–339 | Pirate 2 |
| 340–349 | Ngasta |
| 350–359 | Urik |
| 360–369 | Zombie |
| 370–389 | Vander |
| 390–409 | Island Thug 1 |
| 410–429 | Island Thug 2 |
| 430–449 | Island Thug 3 |
| 450–469 | Island Thug 4 |
| 470–489 | Island Thug 5 |
| 490–509 | Island Thug 6 |
| 510–529 | Pirate Hideout 1 |
| 530–549 | Pirate Hideout 2 |
| 550–569 | Pirate Hideout 3 |
| 570–589 | Pirate Hideout 4 |
| 580–587 | Dragon |
| 591–599 | Jail interior (reuses guard lines) |
Not all reserved slots are populated. Many combatants use only a subset of their allocated range, and some ranges overlap in the comments (Dragon at 580–587 falls inside Pirate Hideout 4 at 570–589).
External References
ITEM.INI
Item database defining all collectible objects, weapons, potions, keys, and quest items with their properties, models, and scripted behaviors.
Shipped sample path: /Redguard/ITEM.INI (e.g. .../GOG Galaxy/Redguard/Redguard/ITEM.INI).
The file is data-driven: every game object from the compass to the soul sword is defined here with associated 3D models, inventory bitmaps, and AI scripts. Item 0 is always the compass, a special case — giving the player this item automatically turns on the compass display. Any item index declared as 0 in a weapon or hand-object field is treated as “no object” for this reason.
File Structure
The file contains a single [items] section with two parts:
- A header block with global inventory settings.
- Per-item field blocks indexed by item ID (e.g.
name[0],type[1]).
[items] Header Fields
| Field | Value | Description |
|---|---|---|
bitmap_file | SYSTEM\PICKUPS.GXA | GXA texture atlas for unselected inventory icons. |
bitmap_selected_file | SYSTEM\PICKUPSS.GXA | GXA texture atlas for selected inventory icons. |
start_item_list | 1, 2, 4, 18 | Item IDs the player starts the game with. |
start_item_select | 1 | Item ID selected by default at game start. |
additional_length | 8 | Extra inventory display length (slots). |
weapon_sphere_size | 10 | Collision sphere radius for weapon hit detection. |
default_weapon_item | 1 | Item ID used as the default weapon (the sabre). |
torch_time | 60 | Torch burn duration in seconds (tolerance of +/- 3 seconds). |
Per-Item Field Schema
All fields are optional. Missing fields are treated as unused. The index x in each field name is the item ID used in all script commands.
General fields
| Field | Description |
|---|---|
type[x] | Item type: 0 = general item, 1 = sword/weapon (can be drawn and sheathed, has both drawn and sheathed 3D files), 2 = hand object (may override a weapon, e.g. torch). |
flags[x] | Bit-field of misc flags. 1 = remove on drop regardless of player total; 2 = drop item after use; 4 = remove item after use. Add values together to combine. |
hide[x] | When 1, the item is hidden from screen and inventory but still exists. Scripts may still call UseItem() on it; the player cannot select or use it directly. |
total[x] | Total number of this item in the game world. |
player_max[x] | Maximum number of this item the player can carry. |
player_total[x] | Number of this item the player starts the game with. |
name[x] | Short name RTX dialogue label (e.g. ?xsw). |
description[x] | Longer item description RTX dialogue label (e.g. cisw). |
name[x] and description[x] values are keys into the RTX dialogue/text system, not literal strings. Labels beginning with ? are short display names; labels beginning with ci or ti are longer descriptions.
AI script fields
| Field | Description |
|---|---|
use_script[x] | AI script file to execute when the item is used through the shell system. |
script_instances[x] | Maximum concurrent instances of the use script. 1 prevents a new instance from starting if one is already running. 0 means unlimited instances. |
Objects and bitmaps
| Field | Description |
|---|---|
bitmap[x] | Bitmap index into the bitmap_file GXA atlas for in-game display. |
inventory_object_file[x] | 3D object file used in the inventory screen. |
game_object_file[x] | 3D object file used in the game world. |
Drop and add lists
| Field | Description |
|---|---|
add_item_list[x] | List of item IDs to add to the player when this item is picked up. |
drop_add_item_list[x] | List of item IDs to drop into the world when this item is picked up. |
remove_drop_item_list[x] | List of item IDs to remove from the world when this item is dropped. |
required_item_list[x] | List of item IDs the player must already have before picking up this item. |
drop_use_item_list[x] | List of item IDs to drop into the world when this item is used. |
Weapon fields
| Field | Description |
|---|---|
hand_object_file[x] | 3D object for the item when held in hand. For weapon-type items this must be a weapon object. Ignored for general-type items. |
hilt_object_file[x] | 3D object for the item when sheathed. |
Object file paths reference 3DART\ assets, which are the software-renderer models from the original CD release. The GOG release uses fxart/ equivalents at runtime; the 3DART\ paths in this file reflect the original CD asset layout.
Item Catalog
87 items are defined (IDs 0 through 86). Item 0 is always the compass.
| ID | Comment | Type |
|---|---|---|
| 0 | compass | 0 (general) |
| 1 | sabre | 1 (weapon) |
| 2 | gold | 0 (general) |
| 3 | Potion of ironskin | 0 (general) |
| 4 | health potion | 0 (general) |
| 5 | ring of invisibility | 0 (general) |
| 6 | Voa’s ring | 0 (general) |
| 7 | guard sword | 1 (weapon) |
| 8 | rusty key | 0 (general) |
| 9 | gold Key | 0 (general) |
| 10 | silver key | 0 (general) |
| 11 | amulet | 0 (general) |
| 12 | soul gem | 0 (general) |
| 13 | soul sword | 1 (weapon) |
| 14 | crow bar | 0 (general) |
| 15 | rune 1 | 0 (general) |
| 16 | rune 2 | 0 (general) |
| 17 | rune 3 | 0 (general) |
| 18 | letter | 0 (general) |
| 19 | orc’s blood | 0 (general) |
| 20 | orc’s blood w/stuff | 0 (general) |
| 21 | spider’s milk | 0 (general) |
| 22 | spider’s milk w/stuff | 0 (general) |
| 23 | ectoplasm | 0 (general) |
| 24 | ectoplasm w/stuff | 0 (general) |
| 25 | hist sap | 0 (general) |
| 26 | hist sap w/ stuff | 0 (general) |
| 27 | book of dw lore | 0 (general) |
| 28 | dwarven gear | 0 (general) |
| 29 | vial | 0 (general) |
| 30 | vial w/elixir | 0 (general) |
| 31 | iron weight | 0 (general) |
| 32 | bucket | 0 (general) |
| 33 | bucket w/water | 0 (general) |
| 34 | fist rune | 0 (general) |
| 35 | elven book (copy) DO NOT USE | 0 (general) |
| 36 | elven book | 0 (general) |
| 37 | redguard book | 0 (general) |
| 38 | flora of hammerfell book | 0 (general) |
| 39 | reference map | 0 (general) |
| 40 | pouch | 0 (general) |
| 41 | trithick’s map piece | 0 (general) |
| 42 | silver ship | 0 (general) |
| 43 | shovel | 0 (general) |
| 44 | aloe | 0 (general) |
| 45 | torch | 3 (hand object) |
| 46 | monocle | 0 (general) |
| 47 | red flag | 0 (general) |
| 48 | silver locket that talks of lakene | 0 (general) |
| 49 | redguard insignia | 0 (general) |
| 50 | joto’s piece | 0 (general) |
| 51 | flask of lillandril | 0 (general) |
| 52 | talisman of hunding | 0 (general) |
| 53 | izara’s journal open | 0 (general) |
| 54 | canah feather | 0 (general) |
| 55 | kithral’s journal | 0 (general) |
| 56 | starsign’s book | 0 (general) |
| 57 | izara’s journal closed | 0 (general) |
| 58 | starstone | 0 (general) |
| 59 | krisandra’s key | 0 (general) |
| 60 | key to iszara’s lodge | 0 (general) |
| 61 | necro book - view only | 0 (general) |
| 62 | bar mug ..for thugs hands, etc | 0 (general) |
| 63 | mariah’s watering can | 0 (general) |
| 64 | glass bottle | 0 (general) |
| 65 | glass bottle with water | 0 (general) |
| 66 | glass bottle w/aloe and water | 0 (general) |
| 67 | Strength Potion | 0 (general) |
| 68 | bandage | 0 (general) |
| 69 | bloody bandage | 0 (general) |
| 70 | skeleton sword | 1 (weapon) |
| 71 | keep out poster…mage’s guild | 0 (general) |
| 72 | keep out poster…dwarven ruins | 0 (general) |
| 73 | tobias mar mug | 0 (general) |
| 74 | Bone Key | 0 (general) |
| 75 | flaming sabre | 1 (weapon) |
| 76 | goblin sword | 1 (weapon) |
| 77 | ogre’s axe | 1 (weapon) |
| 78 | dram’s sword | 1 (weapon) |
| 79 | palace key | 0 (general) |
| 80 | dram’s bow | 0 (general) |
| 81 | dram’s arrow | 0 (general) |
| 82 | silver locket that just says silver locker | 0 (general) |
| 83 | island map | 0 (general) |
| 84 | wanted poster | 0 (general) |
| 85 | palace diagram | 0 (general) |
| 86 | last | 0 (general) |
Notes on specific entries:
- Items 19/20, 21/22, 23/24, 25/26 are paired: the base ingredient and the same ingredient already combined with another substance. Both variants share the same
name[x]label. - Items 29/30 are the empty vial and the vial filled with elixir.
- Items 32/33 are the empty bucket and the bucket filled with water.
- Items 53/57 are the open and closed versions of Izara’s journal, sharing the same
name[x]label. - Item 35 is marked “DO NOT USE” in the file comment; item 36 is the live elven book.
- Item 45 (torch) uses type
3in the file, which the comment block does not define. The comment block describes type2as the hand-object type; type3appears to be an extension of that for the torch specifically. - Items 62 and 63 (bar mug, watering can) reuse the journal inventory model (
Ijourn.3D) as a placeholder, with distinctgame_object_filepaths for the actual in-world mesh. - Items 80 and 81 (dram’s bow and arrow) reuse the bone key inventory model (
ibone.3d) as a placeholder. - Item 86 is labeled “last” with no meaningful content, serving as a sentinel or end-of-list marker.
External References
- UESP: Redguard:Console — documents the
item addcommand, which uses item IDs matching the index numbers in this file.
WORLD.INI
World/level database defining map files, palettes, lighting, sky, weather, and PVO node maps for all game locations.
Shipped path: /Redguard/WORLD.INI. Referenced by SYSTEM.INI via world_ini=WORLD.INI. The file contains a single [world] section with four global keys followed by per-world-index entries for every game location.
File Structure
Single section: [world]
Global Keys
| Key | Example value | Description |
|---|---|---|
start_world | 0 | World index loaded on new game start. |
start_marker | 0 | Spawn marker index within the start world. |
test_map_delay | 1 | Delay (seconds) between worlds during test-map cycling. |
test_map_order | 0,1,2,1,...,-2 | Comma-separated world index sequence for the test-map loop. -2 terminates the list. The shipped file includes a commented-out compact variant and an active interleaved variant that returns to world 1 between each location. |
Per-World Keys
Each world is identified by an integer index N. Keys follow the pattern key_name[N]=value.
Core
| Key | Type | Description |
|---|---|---|
world_map[N] | path | RGM scene file for this world. Present on every entry. |
world_world[N] | path | WLD terrain file. Only present for outdoor worlds with a heightmap terrain mesh. |
world_redbook[N] | integer | CD audio track number for background music. |
world_palette[N] | path | COL palette file. |
world_shade[N] | integer | Shade table index. |
world_haze[N] | integer | Haze/distance-fog table index. |
world_background[N] | integer | Background fill color index. 0 = black, 2 = sky color, other values are palette indices. |
world_compass[N] | integer | Compass heading offset (fixed-point). Omitted for most worlds; present where the player can see a compass. |
world_flash_filename[N] | path | GXA file used for screen-flash transitions when entering or leaving this world. |
Lighting
| Key | Type | Description |
|---|---|---|
world_sun[N] | x, y, z, intensity | Sun direction vector and intensity. The three components are large fixed-point integers defining the light direction; intensity is a scalar. |
world_ambient[N] | integer | Global ambient light level. |
world_ambientfx[N] | integer | Ambient effect intensity (affects dynamic lighting). |
world_ambientrgb[N] | r, g, b | Ambient light color as three 0..255 components. |
world_sunangle[N] | integer | Sun angle (fixed-point). Controls the horizontal rotation of the sun direction. |
world_sunskew[N] | integer | Sun skew (fixed-point). Controls the vertical tilt of the sun direction. |
world_sunrgb[N] | r, g, b, scale | Sun color as three 0..255 components plus a scale factor. |
world_fogrgb[N] | r, g, b | Distance fog color as three 0..255 components. |
Sky
| Key | Type | Description |
|---|---|---|
world_sky[N] | path | GXA skybox texture. Only present for outdoor worlds. |
world_skyfx[N] | filename | BSI sky texture (scrolling sky layer). |
world_skyscale[N] | integer | Scale factor for the sky layer. |
world_skylevel[N] | integer | Vertical offset of the sky layer (negative = below horizon). |
world_skyspeed[N] | integer | Scroll speed of the sky layer. |
world_sky_xrotate[N] | integer | Sky rotation rate around the X axis. |
world_sky_yrotate[N] | integer | Sky rotation rate around the Y axis. |
Sun Disc
| Key | Type | Description |
|---|---|---|
world_sunimg[N] | filename | BSI texture for the rendered sun disc. |
world_sunimgrgb[N] | r, g, b | Tint color for the sun disc texture. |
world_sunscale[N] | integer | Size scale of the sun disc. |
Water
| Key | Type | Description |
|---|---|---|
world_wave[N] | a, b, c | Wave animation parameters for water surfaces. Three integers: a = amplitude (vertical displacement scale), b = speed (phase advance per frame), c = spatial frequency (ripple density, multiplies the squared-distance term). See Water Waves for the displacement formula. |
PVO Visibility
| Key | Type | Description |
|---|---|---|
world_node_mapN[W] | path | PVO .noo node map file, where N is a 1-based sequence index and W is the world index. Each world can have multiple node maps. See PVO for the octree format. |
Rain / Weather
Only world 6 (necrisle) uses these keys. No other world has weather effects.
| Key | Type | Description |
|---|---|---|
world_rain_delay[N] | integer | Frames between rain drop spawns. |
world_rain_drops[N] | integer | Maximum simultaneous rain drops. |
world_rain_start[N] | integer | Vertical start height for rain drops (negative = above ground). |
world_rain_end[N] | integer | Vertical end height where drops are removed. |
world_rain_sphereN[W] | x, y, z, r | Sphere defining a rain zone: center coordinates plus radius. Multiple spheres (indexed 1..4 in world 6) define the areas where rain falls. |
World Catalog
IDs 9, 10, and 16 have no entries in the file and are skipped.
Worlds 0, 1, 6, 27, 28, and 30 have world_world entries and use a WLD terrain mesh. All other worlds are indoor or dungeon locations with no terrain.
| ID | RGM File | WLD File | Palette | Location |
|---|---|---|---|---|
| 0 | MAPS\start.rgm | MAPS\hideout.WLD | 3DART\sunset.COL | Starting hideout (exterior, sunset) |
| 1 | MAPS\ISLAND.rgm | MAPS\ISLAND.WLD | 3DART\island.COL | Stros M’Kai island (daytime) |
| 2 | MAPS\catacomb.rgm | — | 3DART\catacomb.COL | Catacombs |
| 3 | MAPS\PALACE.rgm | — | 3DART\palace00.COL | Palace interior |
| 4 | MAPS\caverns.rgm | — | 3DART\REDcave.COL | Caverns |
| 5 | MAPS\observe.rgm | — | 3DART\observat.COL | Observatory |
| 6 | MAPS\necrisle.rgm | MAPS\necrisle.WLD | 3DART\necro.COL | Necromancer’s Isle (rain, rotating sky) |
| 7 | MAPS\necrtowr.rgm | — | 3DART\necro.COL | Necromancer’s Tower interior |
| 8 | MAPS\drint.rgm | — | 3DART\observat.COL | Dwemer ruin interior |
| 11 | MAPS\jailint.rgm | — | 3DART\necro.COL | Jail interior |
| 12 | MAPS\temple.rgm | — | 3DART\island.COL | Temple |
| 13 | MAPS\mguild.rgm | — | 3DART\redcave.COL | Mages Guild |
| 14 | MAPS\vile.rgm | — | 3DART\island.COL | Vile Lair |
| 15 | MAPS\tavern.rgm | — | 3DART\island.COL | Tavern |
| 17 | MAPS\hideint.rgm | MAPS\hideout.WLD | 3DART\hideout.COL | Hideout interior (shares hideout WLD) |
| 18 | maps\silver1.rgm | — | 3DART\island.COL | Silversmith (area 1) |
| 19 | maps\silver2.rgm | — | 3DART\island.COL | Silversmith (area 2) |
| 20 | maps\belltowr.rgm | — | 3DART\island.COL | Bell Tower |
| 21 | maps\harbtowr.rgm | — | 3DART\island.COL | Harbor Tower |
| 22 | maps\gerricks.rgm | — | 3DART\island.COL | Gerrick’s |
| 23 | maps\cartogr.rgm | — | 3DART\island.COL | Cartographer’s |
| 24 | maps\smden.rgm | — | 3DART\island.COL | Smuggler’s Den |
| 25 | maps\rollos.rgm | — | 3DART\island.COL | Rollo’s |
| 26 | maps\jffers.rgm | — | 3DART\island.COL | Jeffers’ |
| 27 | MAPS\island.rgm | MAPS\ISLand.WLD | 3DART\nightsky.COL | Island (night variant) |
| 28 | MAPS\ISLAND.rgm | MAPS\ISLAND.WLD | 3DART\sunset.COL | Island (sunset variant) |
| 29 | maps\brennans.rgm | — | 3DART\island.COL | Brennan’s Farm |
| 30 | MAPS\extpalac.rgm | MAPS\ISLAND.WLD | 3DART\sunset.COL | Palace exterior (sunset, uses island WLD) |
| 99 | maps\brennans.rgm | — | 3DART\island.COL | Brennan’s Farm (alternate entry) |
Notes
Typos in the shipped file. Line 286 reads orld_ambientfx[8]=128 (missing leading w) and line 498 reads orld_sunangle[23]=256. Both are present in the shipped file as-is; the engine likely ignores the malformed keys and falls back to defaults for those fields.
World 6 rain zones. The four world_rain_sphere entries for necrisle define overlapping spheres centered on different parts of the island. Sphere 2 has the largest radius (1000 units) and covers the main approach area.
World 17 shares terrain with world 0. Both the starting exterior (world 0) and the hideout interior (world 17) reference MAPS\hideout.WLD. The interior uses the same terrain data but a different RGM scene and palette.
Worlds 27 and 28 are time-of-day variants of world 1. All three share MAPS\ISLAND.WLD and the same PVO node maps (islan001..islan004, lighths). World 27 uses a night palette and sky; world 28 uses a sunset palette matching world 0.
Redguard Preservation CLI
When converting RGM or WLD files without an explicit --palette flag, the CLI reads WORLD.INI from the asset root to auto-resolve the correct palette. If multiple world entries match the input file (e.g. ISLAND.RGM appears in worlds 1, 27, and 28), the first match is used and alternatives are logged. Use --palette to override.
External References
- UESP: Redguard:Console — the
show worldconsole command displays the current world index at runtime
MENU.INI
Menu system layout configuration defining page structure, text placement, textures, selectable items, and embedded movie (Smacker) playback definitions.
Shipped path: MENU.INI (game root directory, 42 KB, 961 lines).
Sections
[general]
Global menu system flags:
| Key | Default | Description |
|---|---|---|
preload_sprites | 1 | Preload menu sprite textures at startup. |
hi_res_menu | 1 | Use high-resolution menu rendering. |
ignore_autosave | 0 | Skip auto-save slot handling. |
force_movies | 0 | Force movie playback (skip user-skip). |
[pageN] (pages 0–7)
Each page section defines the visual layout and interactive elements for one menu screen. 8 pages are defined in the shipped file.
Page assignments
| Page | Menu Screen |
|---|---|
| 0 | Main Menu (Save, Load, New, Options, Movies, Continue, Quit) |
| 1 | Save Game |
| 2 | Load Game |
| 3 | Movies (cinematic list with Smacker playback) |
| 4 | Options (Sound Volume, Music Volume, Text toggle, Speech toggle) |
| 5 | Confirm New Game |
| 6 | Confirm Overwrite Save |
| 7 | Key Binding Options |
Texture fields
Each page uses up to 2 background textures sourced from TEXBSI archives:
| Key | Description |
|---|---|
texture_set[0], texture_set[1] | Texture set number (TEXBSI archive id). |
texture_index[0], texture_index[1] | Index within the texture set. |
All shipped pages use texture set 289.
Per-element fields
Elements are indexed sequentially within each page (e.g. text_x[0], text_x[1], etc.):
| Key | Description |
|---|---|
text_x[N] | X position of the text element. |
text_y[N] | Y position of the text element. |
text[N] | Display text string. |
justify[N] | Text alignment: 0 = left, 1 = center, 2 = right. |
selectable[N] | 1 if the user can select/interact with this element. |
default[N] | 1 if this element is selected when the page first opens. |
texture[N] | Which background texture to place text on (0 or 1). |
action[N] | Action id dispatched when selected (program-defined). |
grayed[N] | 1 to display in the grayed-out font. |
output_x[N] | X position for special output (key name text, slider art). |
output_y[N] | Y position for special output. |
slider_min[N] | Minimum value for slider elements. |
slider_max[N] | Maximum value for slider elements. |
Movie fields (page 3)
Page 3 defines cinematic movie entries with timed subtitle overlays:
| Key | Description |
|---|---|
movie_name[N] | Smacker (.SMK) filename in the anims directory. |
movie_keys[N] | Comma-separated key frames for user fast-forward. |
movieM_text[N] | Subtitle overlay: red,green,blue,start_frame,stop_frame,text. |
The shipped file defines 2 movies: INTRO.SMK (game intro, 88 subtitle lines) and OUTRO.SMK (ending, 34 subtitle lines).
Action id ranges
Action ids follow a convention per page:
| Range | Meaning |
|---|---|
-1 | Continue / resume game. |
-2 | Quit game. |
| 1–10 | Main menu navigation (Save=1, Load=2, Movies=3, Options=4, New=10). |
| 100–102 | Save game page actions. |
| 200–202 | Load game page actions. |
| 300–302 | Movie page actions. |
| 400–411 | Options page actions (volume sliders, toggles). |
| 500–501 | Confirm new game actions. |
| 600–601 | Confirm overwrite actions. |
| 700–724 | Key binding page actions. |
External References
KEYS.INI
Input binding configuration mapping keyboard scancodes, mouse buttons, and joystick axes to game actions.
Shipped path: KEYS.INI (game root directory).
Sections
[input]
Action-to-scancode bindings. Two binding slots exist per directional/action key (index [0] for keyboard, index [1] for joystick/gamepad).
| Key | Default | Description |
|---|---|---|
next_key | 52 (period) | Next item in inventory. |
prev_key | 51 (comma) | Previous item in inventory. |
inventory_key | 23 (I) | Open inventory screen. |
log_key | 38 (L) | Open logbook. |
quick_sword_key | 16 (Q) | Quick-draw sword. |
quick_health_key | 35 (H) | Quick-use health potion. |
map_key | 50 (M) | Open map. |
key_up[0] | 17 (W) | Move forward (keyboard). |
key_down[0] | 31 (S) | Move backward (keyboard). |
key_left[0] | 30 (A) | Turn left (keyboard). |
key_right[0] | 32 (D) | Turn right (keyboard). |
key_a[0] | 42 (L Shift) | Action / Use (keyboard). |
key_b[0] | 57 (Space) | Jump (keyboard). |
key_c[0] | 56 (Alt) | View / Look (keyboard). |
key_d[0] | 18 (E) | Walk toggle (keyboard). |
key_up[1] | 138 | Move forward (joystick). |
key_down[1] | 139 | Move backward (joystick). |
key_left[1] | 136 | Turn left (joystick). |
key_right[1] | 137 | Turn right (joystick). |
key_a[1] | 135 | Action / Use (joystick button 4). |
key_b[1] | 133 | Jump (joystick button 2). |
key_c[1] | 134 | View / Look (joystick button 3). |
key_d[1] | 132 | Walk toggle (joystick button 1). |
user_key_up | 0 | User-remapped forward key. |
user_key_down | 0 | User-remapped backward key. |
user_key_left | 0 | User-remapped left key. |
user_key_right | 0 | User-remapped right key. |
user_key_a | 0 | User-remapped action key. |
user_key_b | 0 | User-remapped jump key. |
user_key_c | 0 | User-remapped view key. |
user_key_d | 0 | User-remapped walk key. |
A value of 0 in user remap fields means no override (use default binding).
The file footer also contains uppercase duplicates NEXT_KEY = 37 and PREV_KEY = 38 that appear to be runtime-written values (the engine writes updated bindings back to the file).
[misc]
| Key | Default | Description |
|---|---|---|
waiting_message | HIT KEY | Text displayed during key-binding prompt. |
[defined]
Scancode-to-name lookup table used by the key binding UI. Maps scancode indices 0–139 to human-readable names.
Scancodes 0–127 are keyboard keys (standard PC AT scancodes):
| Range | Keys |
|---|---|
| 0 | empty (no key) |
| 1 | escape |
| 2–11 | 1 through 0 |
| 12–13 | minus, plus |
| 14 | backspace |
| 15 | tab |
| 16–25 | q through p |
| 26–27 | l bracket, r bracket |
| 28 | enter |
| 29 | ctrl |
| 30–38 | a through l |
| 39–41 | :, ", tildy |
| 42 | l shift |
| 43 | \ |
| 44–50 | z through m |
| 51–53 | comma, period, slash |
| 54 | r shift |
| 55 | prt scr |
| 56 | alt |
| 57 | space |
| 58 | caps lock |
| 59–68 | f1 through f10 |
| 69–70 | num lock, scrl lock |
| 71–73 | home, kbd up, page up |
| 74 | num minus |
| 75 | kbd left |
| 77 | kbd right |
| 78 | num plus |
| 79–81 | end, kbd down, page down |
| 82–83 | insert, delete |
| 87–88 | f11, f12 |
| 76, 84–86, 89–127 | unkn NN (unused scancodes) |
Scancodes 128–139 are mouse/joystick inputs:
| Scancode | Name |
|---|---|
| 128 | left mouse |
| 129 | right mouse |
| 130 | mouse x (axis) |
| 131 | mouse y (axis) |
| 132–135 | button 1 through button 4 (joystick) |
| 136–137 | joy left, joy right |
| 138–139 | joy up, joy down |
External References
REGISTRY.INI
File-system abstraction configuration controlling archive (.ZAP) lookup, 32-bit file access, and registry path resolution.
Shipped path: REGISTRY.INI (game root directory, 302 bytes).
The registry subsystem configured by this file is non-functional in the shipped game. The console command show registry reports the registry system is not open, and altering this file has no observable effect at runtime.
Sections
[registry]
Archive-to-path mapping entries. Each line maps a .ZAP archive file to a file glob pattern:
3DART\OBJECTS.ZAP 3DART\*.3D
SYSTEM\GXA.ZAP SYSTEM\*.GXA
INI.ZAP *.INI
The shipped file also contains a commented-out entry: SOUP386\SOUP386.ZAP SOUP386\*.ai.
These entries would allow the engine to look up files inside .ZAP archives instead of (or in addition to) loose files on disk. In practice, the GOG release does not ship any .ZAP archive files — all assets are loose.
[file32]
File access configuration flags:
| Key | Default | Description |
|---|---|---|
use_registry | false | Enable the Windows registry path lookup subsystem. |
use_32bit | true | Use 32-bit file I/O (DOS/4GW extended file access). |
archive_file_first | false | Check .ZAP archives before loose files. |
local_path | true | Use local (relative) file paths. |
ignore_registry_errors | false | Suppress errors from registry path lookup failures. |
registry_log | false | Enable logging of registry system operations. |
advanced_exist | true | Use advanced file-existence checking. |
External References
- UESP: Redguard:Console (documents
show registrycommand)
SURFACE.INI
Text-based INI configuration file that defines per-texture-index blend behavior and surface-type sound remapping for the terrain renderer.
Shipped sample path: /Redguard/surface.ini (e.g. .../GOG Galaxy/Redguard/Redguard/surface.ini).
Engine string analysis confirms the engine expects this file at startup, requires a surfaces section/header, and validates surface type values plus texture set/index bounds (with some sound-section validation).
File Structure
Top-level sections:
[surfaces]section: texture-set/index to surface-type assignments- Sound remap sections:
[unknown],[water],[deepwater],[scapewater],[scapedeepwater],[lava],[sand],[wood],[tile],[scape],[rock],[gloop]
Surface Assignment Records ([surfaces])
set, index, typeset, all, type
Where:
set: texture set id, documented range 0..511index: texture index inside the settype: case-insensitive token from{WATER, DEEPWATER, LAVA, SAND, WOOD, TILE, ROCK, GLOOP}
Sound Remap Records
Each sound section contains entries of the form:
<sound_id> = <remap_sound_id>
Behavior/constraints from shipped comments and runtime validation strings:
- Sound ids are 0..255 at animation-system call sites; remaps may point to other effect ids.
- Separate
water/scapewateranddeepwater/scapedeepwatersections are expected. - Empty sections are valid (for example
[unknown],[lava],[scape]in shipped sample). - The file is parser-tolerant regarding token case (
WOODandwoodboth appear).
Terrain Texture Blending
SURFACE.INI drives a pixel-level alpha-blending system for terrain tile transitions. The blend system uses a 64-entry type table (one per texture index) with these categories:
| Blend type | Behavior |
|---|---|
| None (default) | Tile rendered as fully opaque; no transition blending. |
| Full | Tile is always fully blended (alpha = 256). Used for index 0 (default/empty tile). |
| Gradient | Alpha ramps linearly across the tile based on pixel position. Used for indices 6 and 52. |
| Hard edge | Sharp alpha cutoff at a fixed distance into the tile. Used for index 31. |
| Custom pattern | Per-pixel alpha from a 256x256 lookup buffer, configured by .CFG files loaded via SURFACE.INI. Used for indices 5, 7, 30, and 32. |
Complete shipped-runtime taxonomy for terrain texture indices 0..63:
- Explicit special indices are
{0, 5, 6, 7, 30, 31, 32, 52}. - Water-surface shortcut set is
{0, 5, 30, 31}(all four tile corners in this set trigger water-surface rendering). - All other indices in
0..63use the default non-special blend/type behavior (no additional hardcoded special handling).
The file also defines a palette-remap table per texture type, mapping source palette indices to blended output indices.
This blending is a pixel-level renderer effect. It does not affect geometry, UVs, or material assignment — it only controls how adjacent terrain textures cross-fade at their shared edges.
Relationships to Other Formats
- WLD terrain Map 3 texture indices are the primary consumer of the blend type table and surface-type assignments.
- TEXBSI supplies the texture images referenced by the 64-entry terrain texture table.
External References
FOG.INI
Fog density ramp table defining distance-based fog intensity for the terrain renderer.
Shipped path: fxart/FOG.INI (inside the Glide asset directory).
Format
Comma-separated index,value pairs defining a piecewise fog density curve. The engine interpolates between entries to fill a 64-entry fog table.
index: fog table position (0–63), corresponding to distance bands.value: fog intensity (0–255), where 0 = fully clear and 255 = fully opaque.
The final entry (63) has no value — it marks the end of the table and uses the last specified density.
Shipped Values
| Index | Value | Description |
|---|---|---|
| 0 | 0 | No fog at close range. |
| 33 | 1 | Fog begins at index 33. |
| 34–48 | 2–255 | Rapid ramp from barely visible to fully opaque. |
| 63 | — | End marker (holds value 255 from index 48). |
From the shipped comments: “46 is the final value for 3800 render distance” — this ties the fog ramp to the back_plane=3800 setting in SYSTEM.INI [xngine].
External References
Engine Details
Findings from engine analysis that go beyond file format documentation.
| Topic | Description | Docs |
|---|---|---|
| Cheat System | 13 XOR-obfuscated cheat codes built into the engine | cheats.md |
| Item Attachment | Vertex-tracking system for positioning held items (swords, shields) on characters. No skeleton — the engine tracks a vertex index per animation frame via RAGR opcode 0 (ShowFrame). All 16 animation opcodes documented. | attachment.md |
| SOUP Scripting | SOUP386 virtual machine architecture, bytecode encoding (22 opcodes), value modes, operator tables, threading model, function dispatch (367 functions), and global flag system (369 flags) | SOUP.md |
| Sky Renderer | Two-layer sky system: static GXA skybox + scrolling BSI texture, sun disc billboard, per-world configuration, and runtime console commands. | sky.md |
| Water Waves | Per-frame sine-table vertex displacement on water terrain cells. Radial concentric ripples driven by world_wave INI parameters (amplitude, speed, spatial frequency) with runtime console tuning. | water.md |
Cheat System
Hidden cheat system built into the Redguard game engine. The 13 cheat codes are XOR-obfuscated in the binary (each byte XOR’d with 0xAA) and not documented in any official or community source at the time of writing.
Activation
Cheats can be triggered in three ways:
During Gameplay (no console)
Type the cheat name on the keyboard during normal gameplay and press Enter. The engine reads keypresses every frame into a hidden 127-character input buffer. On Enter, the buffer is compared against all cheat names. No visual feedback is given — the cheat silently toggles.
Developer Console
Press F12 to open the console, type the cheat name, and press Enter. The console prints Cheat <name> turned on or Cheat <name> turned off.
Console with Explicit Value
In the console, use <cheatname> = <value> to set a specific integer value instead of toggling. For example, magiccarpet = 1 forces the cheat on, magiccarpet = 0 forces it off.
Cheat List
| # | Name | Effect |
|---|---|---|
| 0 | oracle | Lists all 13 cheats and their current on/off state in the console |
| 1 | nodemarker | Debug display of pathfinding node markers — renders the navigation waypoint network used for actor movement. Corresponds to the node_marker key in SYSTEM.INI [debug] (related settings: display_node_map, display_nodes, display_markers). The SOUP scripting layer uses this network via functions like movenodemarker, atnodemarker, disablenodemaps, and enablenodemaps. |
| 2 | moonraker | For the player actor, skips several combat/movement processing calls and flips a player state bit used in combat flow |
| 3 | task | Toggles the task scheduling system — a high-level bytecode-driven behavior scheduler that runs on top of SOUP scripts. SOUP is the low-level scripting VM (“do X right now”); the task system is the higher-level sequencer (“do X, then Y, then Z over time”). Each actor has 6 independent task slots with their own bytecode pointers and frame counters. When enabled, a state machine interpreter processes ~30 task opcodes (0x00–0x1e) per actor per frame, handling action queuing, behavior transitions, AI/combat processing (pathfinding, combat stances, NPC-vs-player logic), and actor destruction. When disabled, only SOUP script execution runs — actors lose multi-step behaviors, combat AI, and pathfinding. Corresponds to task_system in SYSTEM.INI [system] (default: yes). |
| 4 | animation | Toggles the animation state machine — the system that manages animation playback, transitions, blending, and frame timing for all actors. When enabled, actors play animations (walk, fight, idle, etc.) with managed transitions and priority blending, and combat actions are gated by animation timing (a new attack cannot start until the current animation allows it). When disabled, all animation management early-returns: actors freeze on their current frame, combat action timing checks are bypassed (attacks fire without animation constraints), and animation progress always reports zero. Corresponds to animation_system in SYSTEM.INI [system] (default: yes). |
| 5 | magiccarpet | Fly mode — disables gravity and falling, allowing the player to walk horizontally through the air at current altitude |
| 6 | savecheats | Writes all cheat states to REDGUARD.CHT so they persist across sessions |
| 7 | drevil | Prevents player death — suppresses death handling when health reaches zero and bypasses some player damage application paths |
| 8 | drno | Disables part of the non-player actor update path, reducing/skipping some NPC behavior processing |
| 9 | goldfinger | God mode — continuously refreshes the player’s post-hit invulnerability timer while active, preventing it from expiring. Once the player takes a hit (which starts the timer), subsequent damage is blocked indefinitely. Differs from drevil, which prevents death when health reaches zero rather than blocking incoming damage. |
| 10 | neversaydie | Dead code. The flag can be toggled and persisted in REDGUARD.CHT, but it does not change gameplay behavior. |
| 11 | oddjob | Dead code. The flag can be toggled and persisted in REDGUARD.CHT, but it does not change gameplay behavior. |
| 12 | yeahbaby | Vertical surface bypass — while active, Page Up moves the player upward through surfaces and Page Down clips below the current surface. The collision bypass only engages while one of these keys is held; normal surface collision applies otherwise. Not a permanent noclip — horizontal collision is unaffected. |
The cheat names are themed after James Bond films (Moonraker, Dr. No, Goldfinger, Oddjob, Never Say Never Again) and Austin Powers (“Yeah Baby”).
Persistence — REDGUARD.CHT
The savecheats command (cheat #6) writes the current state of all 13 cheats to a file called REDGUARD.CHT in the game directory. On startup, the engine checks for this file and restores any previously saved cheat states. Delete the file to reset all cheats. See CHT format specification for the binary layout.
Obfuscation
The cheat names are stored in the game binary as XOR-encoded strings (each byte XOR’d with the constant 0xAA). This is a simple obfuscation to prevent discovery via hex editors or string dumps. The names are decoded into memory at startup.
External References
- UESP Redguard Console — Documents the developer console but not the cheat system
- UESP Redguard Cheats — Lists only the
item addconsole command and a geometry exploit; does not mention the built-in cheat system
Item Attachment System
How the engine positions held items (swords, shields) on animated characters at runtime. This is a vertex-tracking system — no skeleton or bone hierarchy exists in any Redguard format.
Overview
Character models (3D/3DC) are flat polygon meshes with per-frame vertex animation. There are no bones, joints, or named attachment points in the model data. Instead, the engine tracks a specific vertex index from the character’s animation, reads that vertex’s world position each frame, and places the held item there.
The tracked vertex index is encoded per-frame in packed 3-byte animation commands within the RGM RAGR section (or the equivalent AIAN section in standalone .AI files). Each animation frame can specify which vertex to track, allowing the attachment point to change as the animation progresses.
Locating the Vertex Index in a File
To find an actor’s attachment vertex index in an RGM file:
RAHD record (165 bytes per actor, at payload offset 8 + i × 165)
└─ offset 0x31: ragr_offset (u32 LE)
│
▼
RAGR section payload + ragr_offset
└─ read u16 entry_size (0 = end, else payload bytes follow)
└─ animation group entry:
+0x02: group_index (u16)
+0x04: anim_id (u16)
+0x06: flag (u16, low byte only)
+0x08: frame_count (u16)
+0x0A: commands (frame_count × 3 bytes)
└─ next entry at: current_position + 2 + entry_size
│
▼
Per-frame command (3 bytes, packed LE):
byte0 & 0x0F = opcode
If opcode is 0, 4, or 10:
vertex_index = (byte1 >> 6) | (byte2 << 2)
Sign-extend from 10 bits: if value & 0x200, subtract 0x400
In ISLAND.RGM, Cyrus has 152 animation groups, 58 with attachment commands. Key vertex indices are 1 (hand/sword grip) and −10 (scabbard/hip).
The full animation command format is documented in RGM § RAGR.
Animation Command Stream
Animation group data consists of a 10-byte entry header followed by frame_count × 3-byte packed commands (see RGM § RAGR for the entry layout).
Animation Command (3 bytes, packed)
Each command is a 24-bit packed value. The low 4 bits select the opcode type, which determines how remaining bits are allocated to parameters.
Opcode 0 — ShowFrame (Set Handle + Vertex)
The only opcode that sets the attachment vertex. Encoding: 10-bit handle + 10-bit vertex.
byte 0 byte 1 byte 2
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
├─hdl─┤ ├─op──┤ ├v┤ ├──handle─┤ ├───vertex────┤
opcode = byte0 & 0x0F (4 bits)
handle_index = (byte0 >> 4) | ((byte1 & 0x3F) << 4) (10-bit signed)
vertex_index = (byte1 >> 6) | (byte2 << 2) (10-bit signed)
During animation playback, the decoded values are written to the actor struct:
handle_index→ actor animation handle (which 3D object to read from)vertex_index→ actor tracked vertex (which vertex to read position of)
In ISLAND.RGM, Cyrus uses vertex 1 (hand grip) in 664 commands across combat animations.
Complete Opcode Table
Semantics from engine analysis. Names from UESP where available.
| Opcode | UESP Name | Bit Layout | Parameters | Playback Behavior |
|---|---|---|---|---|
| 0 | ShowFrame | 10 + 10 | handle_index, vertex_index | Advance frame; set attachment handle + vertex. The only opcode that drives item positioning. |
| 1 | EndAnimation | 20-bit | (unused, always 0) | Set animation handle to −1; stop associated sound; call playback recursively for next animation. |
| 2 | GoToPrevious | 20-bit | target_frame | Jump backward to an earlier frame in this group. Used for walk/run loops. |
| 3 | GoToFuture | 20-bit | target_frame | Jump forward to a later frame. Conditional — checks animation state flags and pending transitions. |
| 4 | PlaySound | 10 + 10 | sound_param, volume_shift | Play SFX. Calls sound system with sound_param as setup and volume_shift << 6 as volume. Same bit layout as opcode 0 but params are NOT handle/vertex. |
| 5 | BreakPoint | 20-bit | (unused, always 0) | Set vertex-enable flag at anim control +0x0B. Often the target of GoToFuture jumps. |
| 6 | SetRotationXYZ | 6 + 6 + 6 | rot_x, rot_y, rot_z | Set 3-axis rotation (each param × 256). Actor orientation override. |
| 7 | SetRotationAxis | 2 + 18 | axis (0=X, 1=Y, 2=Z), value | Set rotation on a single axis. Finer precision than opcode 6. |
| 8 | SetPositionXYZ | 6 + 6 + 6 | pos_x, pos_y, pos_z | Set 3-axis position offset (each param × 8). |
| 9 | SetPositionAxis | 2 + 18 | axis (0=X, 1=Y, 2=Z), value | Set position offset on a single axis. |
| 10 | ChangeAnimGroup | 10 + 10 | target_group, target_frame | Jump to a different animation group and frame. Same bit layout as opcode 0, but writes to anim control fields, not attachment. |
| 11 | Rumble/SFX | 20-bit | effect_bitmask | 5-bit mask combining sounds + screen shake. Bit 0 → SFX 0x2580 if actor state=5; bit 1/2 → SFX 0x2500 if state=1; bit 3 → SFX 0x2700 if state=6; bit 4 → screen shake (camera pitch oscillation with exponential decay, ±0x20 units). Cyrus has param=0 (placeholder, no effect). Golem in DRINT has param=16 (bit 4 only = screen shake on attack). |
| 12 | DelayCounter | 20-bit | counter_value | Set frame delay counter; pause animation until counter expires. |
| 13 | ConditionalDelay | 20-bit | counter_value | Set conditional delay; direction-dependent counter. |
| 14 | LoopControl | 20-bit | target_frame | Decrement loop counter; jump to target frame if counter > 0. |
| 15 | Transition | 6 + 7 + 7 | trigger_mask, start_frame, target_group | Mid-animation transition to another group. trigger_mask bits: 0=jump right, 1=jump left, 2=anim trigger (0x2500), 3=anim trigger (0x2700), 4=counter increment, 5=unused. Used by 8 combat actors. |
Opcodes 6–10 and 12–14 are implemented in the engine but never appear in any of the 27 shipped RGM files. They may exist in standalone .AI files or be entirely vestigial.
Opcode Usage Census (all 27 shipped maps)
| Opcode | Total Cmds | Maps | Actors | Notes |
|---|---|---|---|---|
| 0 (ShowFrame) | 93,750 | 27 | 280 | Every animated actor |
| 1 (EndAnimation) | 8,513 | 27 | 280 | Every animated actor |
| 2 (GoToPrevious) | 3,258 | 27 | 275 | Walk/run loops |
| 3 (GoToFuture) | 7,121 | 27 | 276 | Conditional jumps |
| 4 (PlaySound) | 6,913 | 27 | 98 | Combat actors |
| 5 (BreakPoint) | 13,167 | 27 | 105 | Combat actors |
| 11 (ActorSound) | 164 | 27 | 2 | Cyrus + Golem only |
| 15 (Transition) | 461 | 27 | 22 | Guards only |
| 6–10, 12–14 | 0 | 0 | 0 | Dead code in shipped game |
Bit Layout Summary
| Layout | Opcodes | Extraction |
|---|---|---|
| 10 + 10 | 0, 4, 10 | (packed >> 4) & 0x3FF, (packed >> 14) & 0x3FF (both sign-extended) |
| 6 + 6 + 6 | 6, 8 | 6-bit signed at positions 4, 10, 16 |
| 2 + 18 | 7, 9 | 2-bit selector at 4, 18-bit signed at 6 |
| 6 + 7 + 7 | 15 | 6-bit at 4, 7-bit signed at 10, 7-bit signed at 17 |
| 20-bit | 1, 2, 3, 5, 11, 12, 13, 14 | 20-bit signed at 4 |
Handle Index Patching
During loading, commands with opcode type 0 are post-processed. The handle_index field contains a relative index into a per-actor animation handle lookup table. The engine rewrites the packed command in-place to replace the relative index with the resolved runtime animation handle. This table is built from RAAN entries loaded for the actor.
Vertex Position Lookup
At each frame, the engine reads the tracked vertex position through this call chain:
- Entry point — resolves animation handle and reads the tracked vertex.
- 3D object manager — looks up handle in a table. For “virtual” animations (type
0x02), follows a parent handle chain recursively. - Frame builder — reads
(x, y, z)float position of the given vertex from the current animation frame. For frame 0: readsbase_vertices[vertex_index × 12]. For animated frames: applies delta-compressed offsets (i8×3 or i16×3) from the base frame. Returnsfloat[3]. - Result is scaled by a global constant and rounded to integer world coordinates.
Item Data (from INVENTRY.ROB)
Items are loaded from a ROB file keyed as "ITEMS" (INVENTRY.ROB). The item initialization function iterates all items and populates per-item runtime fields:
| Item Struct Offset | Source | Description |
|---|---|---|
+0x10 | Item type | Type discriminator: 1 = weapon/hand-object, 3 = general item |
+0x4a | ROB handle | 3D model handle for the item |
+0x77 | ROB handle (type 1 only) | Hand model — the 3D model shown when weapon is drawn |
+0x7b | ROB handle (type 1 only) | Hilt model — the 3D model shown when weapon is sheathed |
+0x7f | ROB segment data | Length/offset — read from the ROB segment’s internal metadata. Used to offset the weapon collision sphere along the item axis. |
Attachment Transform
Two nearly-identical functions compute the held item’s world transform. Both:
- Read two vertex positions from the actor’s current animation frame (via the tracked vertex index).
- Add world position offsets.
- Rotate by the actor’s orientation matrix (actor struct
+0x51). - Compute heading and pitch from the direction between the two points.
- Build item rotation from the computed direction + actor roll.
- Set item world position = vertex position + (actor radius × scale factor).
The two routines differ in scale factor, corresponding to the “in-hand” and “on-hip/scabbard” attachment positions.
Weapon State Machine
The weapon state machine selects which attachment routine and which model (hand vs hilt) to use based on the actor’s weapon state:
State (actor +0x1b4) | Condition | Action |
|---|---|---|
0x14 (drawing sword) | Frame < draw threshold | Position hilt model at scabbard |
0x14 (drawing sword) | Frame ≥ draw threshold | Position hand model at hand; set drawn flag |
0x15 (sheathing sword) | Frame < sheath threshold | Position hand model at hand |
0x15 (sheathing sword) | Frame ≥ sheath threshold | Position hilt model at scabbard; clear drawn flag |
0x00 (idle, sheathed) | — | Position hilt model at scabbard |
0x00 (idle, drawn) | — | Position hand model at hand |
The draw/sheath frame thresholds are read from the actor’s attribute data (offsets +0x22 and +0x23 from an attribute block pointer at actor +0x272).
The collision sphere tip is offset from the grip point by item.length × -0x100, positioning it along the weapon axis for combat hit detection.
SOUP Script Interface
Scripts drive weapon state transitions through these SOUP functions:
| Function | Purpose |
|---|---|
handitem | Assign a held item to an actor |
displayhandmodel | Show the hand (drawn) model |
displayhanditem | Show the item in the hand |
displayhiltmodel | Show the hilt (sheathed) model |
displayhiltitem | Show the item at the hilt position |
drawsword | Trigger draw animation/state transition |
sheathsword | Trigger sheath animation/state transition |
isholdingweapon | Query: is actor holding a weapon? |
iscarryingweapon | Query: is actor carrying (has) a weapon? |
isdrawingsword | Query: is actor in draw animation? |
issheathingsword | Query: is actor in sheath animation? |
Additional runtime state tracked per actor: hand_pos.vx/vy/vz, hand_angle.vx/vy/vz, hand_type, hand_length, weapon_drawn, hand_item.
Data Flow Summary
File data (RGM):
RAHD record → ragr_offset (offset 0x31)
│
▼
RAGR section payload + ragr_offset
→ size-prefixed entries (u16 entry_size; 0 = end):
+0x02 group_index, +0x04 anim_id, +0x06 flag,
+0x08 frame_count, +0x0A commands (frame_count × 3 bytes)
command bits 14–23 = vertex_index (for opcode 0/4/10)
Map load:
RAAN entries → load .3DC animation files → get runtime handles
RAGR → load animation command streams
→ patch handle_index from relative to absolute
Runtime (per frame):
Animation playback → decode 3-byte command for current frame
→ extract vertex_index + handle_index
→ store in actor struct (+0x24f, +0x251)
Item attachment → read vertex position from current anim frame
using stored handle + vertex index
→ compute world transform (position + orientation)
→ place item model at computed transform
External References
- UESP: Mod:RGM File Format § RAEX — RAEX field names from in-game console
- RGUnity/redguard-unity
RGRGMFile.cs— RGMRAEXItem struct definition
SOUP Scripting
Bytecode scripting engine used by the Redguard runtime for actor behavior, dialogue, puzzle logic, and scene control.
Script Data Sources
| Source | Container | Notes |
|---|---|---|
| Map script bytecode | maps/*.RGM (RASC section) | Main per-actor compiled script payload. Offsets and lengths are stored per actor in RAHD (script_data_offset, script_length, script_pc). |
| Standalone AI script files | soup386/*.AI (for example CAMERA.AI, sword.ai, projtle.ai) | Loaded separately from map RGM files. Uses the same SOUP bytecode model as map scripts. No .AI files are included in the shipped game — the soup386/ directory contains only SOUP386.DEF in both the GOG and original CD releases. The engine’s .AI loading path was most likely used during development. |
| VM definition table | SOUP386/SOUP386.DEF | Text definition file loaded at runtime. Defines function/task names, flags, equates, and attributes used by script execution. See SOUP386.DEF. |
| EXE-embedded interpreter | Runtime binary | The executable contains the SOUP VM interpreter and native function handlers; script payload bytes are file-sourced (RGM sections and .AI files). |
What Is In the Runtime vs Data Files
- Runtime contains the SOUP VM/interpreter and native handlers for script-callable operations.
- Script payload data is loaded from map/script assets (
RGMand.AI) and executed by the VM. - Function names and metadata come from
SOUP386.DEF, while actor-local script bytes come fromRASC/.AIcontent.
VM Architecture
The SOUP386 VM is a register-free, program-counter-driven bytecode interpreter. There is no operand stack — values flow through function return values and are consumed directly by the calling instruction. Local variables (RAVA section, per-actor int[]) and global flags (369 entries shared across all scripts) provide persistent state.
Threading Model
Each script supports up to two concurrent threads sharing the same bytecode buffer:
| Thread | Start Address | Purpose |
|---|---|---|
| Thread 0 | RAHD.script_pc | Main execution (dialogue, activation, behavior) |
| Thread 1 | Offset 0x00 | Interrupt handler — only created when script_pc != 0 |
Thread 1 enables actors to remain activatable while performing another action (for example, an NPC walking a patrol route can still respond to player interaction). The two threads share the bytecode array but maintain independent program counters and independent call stacks.
Execution uses cooperative multitasking: tickScript advances each thread by one instruction per tick. runScript drives thread 0 to completion (with an infinite-loop guard at 1024 iterations).
Endint (opcode 0x13) terminates thread 1 by resetting its PC to 0x00. End (opcode 0x05) terminates thread 0.
Value Modes
The same opcode byte is interpreted differently depending on the calling context. There is no separate addressing-mode byte — the calling instruction determines how trailing bytes are consumed.
| Mode | Context | Effect |
|---|---|---|
| MAIN | Top-level statement | Performs assignment (writes to flag/variable/property) |
| LHS | Left side of if comparison | Reads value; consumes 1 extra operator byte |
| RHS | Right side of if comparison | Reads value; consumes 1 extra operator byte |
| PARAMETER | Argument to a task/function call | Reads value; consumes mode-specific padding bytes |
| FORMULA | Right side of an assignment expression | Reads value only, no extra bytes |
The number of trailing bytes consumed after an opcode depends on both the opcode and the current value mode. See Operand Encoding for per-opcode details.
Bytecode Encoding
Opcode Table
All integers are little-endian. The VM reads a leading opcode byte and dispatches. Unrecognized bytes are fatal.
| Opcode | Name | Encoding (after opcode byte) | Description |
|---|---|---|---|
0x00 | Task | u16 func_id, u8 param_count, params… | Call task on self |
0x01 | Multitask | same as 0x00 | Async task on self (non-blocking) |
0x02 | Function | same as 0x00 | Call function on self (returns value) |
0x03 | If | condition chain, u32 end_offset, block | Conditional branch |
0x04 | Goto | u32 target | Unconditional jump |
0x05 | End | u32 target | Halt — terminates execution |
0x06 | Flag | u16 flag_id, mode-dependent | Global flag read/write |
0x07 | Numeric | i32 value | Immediate 32-bit integer |
0x0A | LocalVar | u8 var_index, mode-dependent | Local variable read/write |
0x0F | ObjDot++ | object encoding, u16 ref_id | Increment object property |
0x10 | ObjDot– | object encoding, u16 ref_id | Decrement object property |
0x11 | Gosub | u32 target | Subroutine call (pushes return address) |
0x12 | Return | (none) | Return from subroutine |
0x13 | Endint | (none) | End secondary thread |
0x14 | ObjectDot | object encoding, u16 ref_id, mode-dependent | Object property read/write |
0x15 | String | u32 string_index | String literal from RASB/RAST table |
0x16 | NumericAlt | i32 value | Same as 0x07; used for global flag function arguments |
0x17 | Anchor | u8 anchor_value | Anchor assignment |
0x19 | ObjDotTask | object encoding, task encoding | Task call on named object |
0x1A | ObjDotFunc | object encoding, task encoding | Function call on named object |
0x1B | TaskPause | u32 label | Pause until task completes |
0x1E | ScriptRV | u8 expected, block | Branch on script return value |
Bytes with no known opcode: 0x08, 0x09, 0x0B–0x0E, 0x18, 0x1C–0x1D, 0x1F+. The RGUnity interpreter throws a fatal error for any unrecognized byte, confirming these are not valid opcodes in shipped scripts.
Operand Encoding
Flag (0x06) and LocalVar (0x0A) consume different trailing bytes depending on value mode:
Flag (0x06)
| Mode | Bytes after u16 flag_id |
|---|---|
| MAIN | Formula (assignment to flag) |
| LHS, RHS | u8 operator byte |
| PARAMETER | u16 padding (always 0x0000) |
| FORMULA | (none) |
LocalVar (0x0A)
| Mode | Bytes after u8 var_index |
|---|---|
| MAIN | Formula (assignment to variable) |
| LHS, RHS | u8 operator byte |
| PARAMETER | 3 padding bytes (0x000000) |
| FORMULA | (none) |
ObjectDot (0x14)
| Mode | Bytes after object encoding + u16 ref_id |
|---|---|
| MAIN | Formula (assignment to property) |
| LHS | u8 operator byte |
| Other | (none) |
Object Name Encoding
Used by opcodes 0x0F, 0x10, 0x14, 0x19, 0x1A. A leading byte selects the object target:
| Byte | Additional | Object |
|---|---|---|
0x00 | u8 padding | Me — the script’s own actor |
0x01 | u8 padding | Player — Cyrus |
0x02 | u8 padding | Camera |
0x04 | u8 string index | Named object from per-script string table (RASB/RAST) |
0x0A | u8 var index | Object name from local variable |
Reference Name Encoding
After object name in property/method opcodes: u16 reference ID. Only the low byte is used as an index into the global references table (loaded from [refs] section of SOUP386.DEF). The high byte is discarded.
Operators
Arithmetic Operators
Assignments use a terminated list of (value, operator) pairs. The formula loop reads a value (any value-producing opcode in FORMULA mode), then an operator byte, repeating until the terminator.
| Byte | Operator | Arity | Engine instruction |
|---|---|---|---|
| 0 | ; (end) | — | (return result) |
| 1 | + | binary | add eax, ebx |
| 2 | - | binary | sub eax, ebx |
| 3 | * | binary | imul ebx (signed) |
| 4 | / | binary | div ebx (unsigned, zero-check via CPU trap) |
| 5 | << | binary | shl eax, cl |
| 6 | >> | binary | sar eax, cl (arithmetic/signed) |
| 7 | & | binary | and eax, ebx |
| 8 | | | binary | or eax, ebx |
| 9 | ^ | binary | xor eax, ebx |
| 10 | ++ | unary | (increment; terminates formula) |
| 11 | -- | unary | (decrement; terminates formula) |
Operator bytes outside 1–9 terminate the formula loop. Bytes 10–11 (unary increment/decrement) are consumed as the final operator and also terminate. Bytes 12+ are fatal. Division by zero is not checked in software — it raises a CPU divide-by-zero exception caught by the C runtime signal handler.
Note: The Dillonn241 disassembler/assembler tools swap operators 3 and 4 (
/and*). This swap is internally consistent within those tools (scripts round-trip correctly) but does not match the engine binary, where byte 3 maps toimul(multiply) and byte 4 maps todiv(divide). The RGUnity runtime implementation also confirms byte 3 = multiply, byte 4 = divide.
Comparison Operators
Used in if conditions. Each comparison pairs a LHS value, comparison byte, and RHS value.
| Byte | Operator |
|---|---|
| 0 | = (equal) |
| 1 | != |
| 2 | < |
| 3 | > |
| 4 | <= |
| 5 | >= |
Conjunctions
Multiple comparisons in a single if are chained with conjunction bytes:
| Byte | Meaning |
|---|---|
| 0 | End of condition list |
| 1 | and |
| 2 | or |
Conditions are evaluated left-to-right with no operator precedence — each conjunction folds the running boolean with the next comparison result.
Control Flow
Conditional Branch (If — 0x03)
Evaluates one or more comparisons chained with and/or. If the condition is false, the PC jumps to end_offset (absolute). If true, execution falls through into the inline block.
[0x03]
repeat:
[value: LHS mode]
[u8 comparison]
[value: RHS mode]
[u8 conjunction] // 0 = end, 1 = and, 2 = or
until conjunction == 0
[u32 end_offset LE] // false-branch target (absolute)
[block of instructions] // executed when true; ends at end_offset
Goto (0x04)
Unconditional jump. Sets PC to the u32 target address (absolute).
End (0x05)
Reads a u32 target offset, sets PC to that address, and signals script termination (returns 0xDEAD sentinel to the run loop).
Gosub / Return (0x11, 0x12)
Gosub pushes the current PC onto the per-thread call stack, then jumps to the target address. Return pops the return address and resumes. Subroutines share the same local variable scope — no parameters are passed through the call stack.
Endint (0x13)
Resets the secondary thread’s PC to 0x00 and signals termination (0xDEAD). Used to end thread 1’s current activation while leaving thread 0 running.
Function Dispatch
Call Types
SOUP386 distinguishes three call types, encoded in the opcode byte:
| Type | Behavior | Script syntax |
|---|---|---|
| Task | Blocking — script waits for completion | FunctionName(...) |
| Multitask | Asynchronous — script continues immediately | @FunctionName(...) |
| Function | Immediate — returns a value | FunctionName(...) (context determines) |
Self-calls use opcodes 0x00/0x01/0x02. Object-targeted calls use 0x19/0x1A with an additional dispatch-type byte (0x00 = task, 0x01 = multitask, 0x02 = function) when used as a top-level statement. In non-MAIN modes, the dispatch-type byte is absent and the call is always treated as a function.
Call Encoding
[u16 func_id LE] // index into SOUP386.DEF function table
[u8 param_count] // number of parameters (0 if func_id == 0)
[param_count × value in PARAMETER mode]
Function index 0 is always NullFunction (synthetic; prepended by the parser, not present in SOUP386.DEF). The func_id is multiplied by the function-table entry stride (49 bytes) to index the runtime table.
Parameter Type Overrides
When a Numeric (0x07) or NumericAlt (0x16) opcode appears in PARAMETER mode, the 4-byte value may be reinterpreted based on the calling function:
| Functions | Type | Encoding |
|---|---|---|
ACTIVATE, AddLog, AmbientRtx, menuAddItem, RTX, rtxAnim, RTXp, RTXpAnim, TorchActivate | Dialogue key | 4-byte ASCII string (RTX lookup key) |
LoadWorld | Map ID | i32 map identifier |
ActiveItem, AddItem, DropItem, HandItem, HaveItem, SelectItem, ShowItem, ShowItemNoRtx | Item ID | i32 item identifier |
| All others | Integer | i32 signed integer |
Function Table
SOUP386.DEF declares 367 callable functions (indices 1–367; index 0 is the synthetic NullFunction). Functions span the following categories:
| Category | Examples | Count |
|---|---|---|
| Movement | Move, WalkForward, MoveToLocation, WanderToLocation | ~16 |
| Rotation / Facing | Rotate, RotateByAxis, FacePlayer, FaceAngle, FaceObject | ~10 |
| Camera | showObj, showPlayer, lookCyrus, showCyrusPan | ~13 |
| Dialogue / RTX | RTX, rtxAnim, menuNew, menuProc, menuAddItem, menuSelection | ~8 |
| Animation | PlayAnimation, PushAnimation, WaitAnimFrame, SetAction | ~10 |
| Combat | beginCombat, endCombat, isDead, adjustHealth, shoot, shootPlayer | ~16 |
| Lighting / FX | Light, LightRadius, LightFlicker, FxPhase, FxFlickerOnOff | ~14 |
| Object control | EnableObject, DisableObject, HideMe, ShowMe, KillMe | ~12 |
| Inventory | AddItem, DropItem, HaveItem, HandItem, ShowItem, SelectItem | ~9 |
| Sound | Sound, AmbientSound, EndSound, StopAllSounds | ~5 |
| Weapon / Attachment | handitem, displayhandmodel, drawsword, sheathsword | ~11 |
| Spatial queries | InRectangle, InCircle, DistanceFromStart, AtPos | ~5 |
| Flat (billboards) | Flat, FlatSetTexture, FlatAnimate, FlatOff | ~7 |
| AI | SetAiType, SetAiMode, Guard, Animal | ~5 |
| Static objects | LoadStatic, UnLoadStatic, PointAt | ~3 |
| Global flags | SetGlobalFlag, TestGlobalFlag, ResetGlobalFlag | 3 |
| Attributes | SetAttribute, GetAttribute, SetMyAttr, GetMyAttr | 4 |
| Debug | PrintParms, LogParms, PrintStringParm | ~3 |
For the weapon/attachment function interface, see Item Attachment — SOUP Script Interface. For the definition-file format and section layout, see SOUP386.DEF.
Global Flags
369 global flags (indices 0–368), shared across all scripts. Each flag has a declared type:
| Type | Semantics |
|---|---|
BOOL | Binary state (0 or 1) |
NUMBER | Integer counter or timer |
FLIPFLOP | Toggled state (doors, switches, puzzle elements) |
Flags are declared in the [flags] section of SOUP386.DEF and accessed by bytecode via SetGlobalFlag, TestGlobalFlag, and ResetGlobalFlag (which use the NumericAlt opcode 0x16 for their flag-ID parameter).
Six flags have non-zero defaults: TimeOfDay (1), OB_TelV (1), OB_TelH (12), At_Shoals (1), Rock_1_Down (1), Rock_2_Down (1).
Flag categories span narrative progression (acts 1–8, for example After_Catacombs, After_League, Won_Game), inventory state (HaveAmulet, HaveGem, Equipped_Torch), NPC dialogue tracking (DreekiusTalk, TobiasTalk, SionaFriend), puzzle mechanics for catacombs (CTDoor*, CTWeight), caverns (CV_Lock*, CV_Pillar*), observatory (OB_TelV, OB_Platform), palace (PI_Door*, PI_Throne*), dwarven ruins (DR_Steam, DR_Boiler, DR_Pipe*), and the scarab (SCB_Position, SCB_ArmL, SCB_ArmR), as well as runtime control (Talking, MenuRet, StrengthTimer, MapTimer).
Relationship to RGM Sections
RAHDstores per-actor script pointers, variable counts, and function-table indices.RASCstores compiled script bytecode as a contiguous blob.RAST/RASBstore script string literal data and offset tables.RAVAstores initial local variable values (i32array).RAHKstores hook entry offsets rebased against script base addresses.RAATstores per-actor attribute tables (256 bytes each), addressed by names fromSOUP386.DEF.
See RGM.md for record-level layouts and offsets.
Open Questions
TaskPause(0x1B) semantics are not fully understood — the RGUnity implementation is a stub (“TODO: do the pause somehow; also whats the taskval?”).ScriptRV(0x1E) branches on a script return value whose source is unknown — RGUnity comment: “TODO: where does the return val come from?”- The
Anchoropcode (0x17) has an encoding discrepancy between the disassembler (reads0x17then a byte) and assembler (writes the anchor value directly as the opcode byte). - The operator byte consumed after flag/variable reads in LHS/RHS mode has unclear effect — RGUnity marks it “TODO: does this operator do anything?”
- Some SOUP API surface does not appear in shipped
RGMscripts; usage may be limited to.AIflows that were available during development but not included in the final release.
External References
- UESP
Mod:RGM File Format - RGUnity/redguard-unity
RGRGMScriptStore.cs— SOUP VM interpreter with dispatch loop, threading model, formula evaluator, and complete flags table (369 entries) - RGUnity/redguard-unity
soupdeffcn_nimpl.cs— Complete SOUP function ID-to-name table (367 functions) - Dillonn241/redguard-mod-manager
ScriptReader.java— RASC bytecode disassembler with full opcode and value-mode decoding - Dillonn241/redguard-mod-manager
ScriptParser.java— RASC bytecode assembler (round-trip verified) - Dillonn241/redguard-mod-manager
MapHeader.java— RAHD record parser with verified field offsets - Dillonn241/redguard-mod-manager
MapDatabase.java— SOUP386.DEF parser (function, flag, reference, attribute definitions)
Sky Renderer
Two-layer sky rendering system used by the xngine engine for outdoor environments. Combines a static GXA skybox texture with an optional scrolling BSI texture layer, plus a separate sun disc billboard.
Overview
The sky system is initialized when a world is loaded and torn down when the session closes. Each frame, the engine renders up to three sky elements in order:
- Background fill — a solid color behind everything
- GXA skybox — a static panoramic texture
- BSI scrolling layer — an animated texture scrolling over the skybox
Only outdoor worlds (those with a WLD terrain mesh) use the sky system. Indoor and dungeon worlds have no sky; their background is a solid fill color.
Lifecycle
| Phase | Trigger | Action |
|---|---|---|
| Load | World open | Engine reads per-world sky keys from WORLD.INI, then calls the sky system opener with the BSI texture filename |
| Render | Every frame in the main loop | Sky layers are drawn before terrain and scene geometry |
| Close | Session close | Sky textures are released and the system is shut down |
Background Fill
The world_background[N] key sets what is drawn behind the sky layers:
| Value | Behavior |
|---|---|
0 | Black |
2 | Sky color (derived from the palette) |
| Other | Palette index used as a solid fill color |
GXA Skybox Layer
The world_sky[N] key points to a GXA file in the system/ directory. This is a static panoramic texture that wraps the horizon. Only outdoor worlds define this key.
The GXA skybox provides the base sky appearance — cloud formations, horizon gradient, and sky color. Time-of-day variants (day, sunset, night) are achieved by loading different worlds that share the same terrain but use different GXA textures and palettes.
BSI Scrolling Layer
The world_skyfx[N] key names a BSI texture that scrolls on top of the skybox. Four parameters control its behavior:
| Key | Description | Default |
|---|---|---|
world_skyscale[N] | Size/scale of the scrolling texture | 0x3200 (12800) if 0 or omitted |
world_skylevel[N] | Vertical offset (negative = below horizon) | 0xFFFFF254 (−3500) if 0 or omitted |
world_skyspeed[N] | Scroll speed | 0 |
world_sky_xrotate[N] | Rotation rate around the X axis | 0 |
world_sky_yrotate[N] | Rotation rate around the Y axis | 0 |
The scrolling layer creates the appearance of moving clouds or atmospheric effects. Rotation parameters allow the sky to slowly rotate, used on Necromancer’s Isle (world 6) for its unsettling spinning-sky effect.
Global Engine Toggles
The [xngine] section of SYSTEM.INI provides master controls that apply to all worlds:
| Key | Default | Description |
|---|---|---|
sky_disable | 0 | Disable sky rendering entirely. 0 = enabled. |
sky_move | 1 | Enable sky scrolling. 0 = frozen. |
sky_xrotate | 3 | Global X-axis rotation speed |
sky_yrotate | 40 | Global Y-axis rotation speed |
Per-world world_sky_xrotate / world_sky_yrotate values override these globals for that world.
Sun Disc
A separate billboard renders the sun as a textured sprite in the sky, independent of both sky layers:
| Key | Description |
|---|---|
world_sunimg[N] | BSI texture for the sun disc |
world_sunimgrgb[N] | Tint color (r, g, b) applied to the sun texture |
world_sunscale[N] | Size scale of the sun disc |
The sun disc position is derived from the world’s sun direction vector (world_sun[N]) and sun angle/skew parameters. It is a visual element only — the lighting system uses the sun direction independently.
Console Commands
The developer console (F12) exposes runtime sky adjustment:
| Command | Alias | Description |
|---|---|---|
fxskyscale <value> | skysc | Set the BSI layer scale |
fxskylevel <value> | skyl | Set the BSI layer vertical offset |
fxskyspeed <value> | skysp | Set the BSI layer scroll speed |
The show world console command displays current sky parameters in the on-screen debug overlay, including the sky texture name, scale, level, and speed.
World Sky Assignments
Of the 31 shipped worlds, only 6 outdoor worlds define sky parameters. All others are indoor/dungeon locations with no sky.
| World | Location | Sky Features |
|---|---|---|
| 0 | Starting hideout (exterior) | Sunset skybox |
| 1 | Stros M’Kai island (daytime) | Daytime skybox + scrolling clouds |
| 6 | Necromancer’s Isle | Skybox + rotating BSI layer + rain weather |
| 27 | Island (night variant) | Night skybox (nightsky.COL palette) |
| 28 | Island (sunset variant) | Sunset skybox (sunset.COL palette) |
| 30 | Palace exterior | Sunset skybox (shares island WLD) |
Worlds 1, 27, and 28 share the same ISLAND.WLD terrain and PVO node maps. The visual difference is entirely driven by different palettes, sky textures, and lighting parameters — demonstrating that time-of-day in Redguard is implemented as separate world entries rather than dynamic sky transitions.
External References
- WORLD.INI documentation — per-world sky, sun disc, and background fill keys
- SYSTEM.INI documentation — global sky engine toggles in the
[xngine]section - UESP: Redguard Console —
show worldcommand displays sky parameters at runtime
Water Wave Animation
Per-frame vertex displacement system that animates water surfaces on the terrain grid. Water cells are identified by texture index, then their height values are replaced with a sine-table lookup producing radial concentric ripples.
Water Tile Detection
A grid cell is classified as water when all four corner vertices have a texture index (lower 6 bits of Map 3) in the set {0, 5, 30, 31}. The check runs per-cell inside the wave renderer — only cells passing all four corners receive wave displacement. Non-water cells retain their static height-table value.
Wave Parameters
Three values configure the wave system per world, set via world_wave[N] in WORLD.INI:
| Parameter | INI position | Console command | Description |
|---|---|---|---|
| Amplitude | 1st | fxwaveamp | Vertical scale of wave displacement. Multiplies the sine-table value. |
| Speed | 2nd | fxwavespeed | Rate of phase advance per frame. Higher values = faster ripple animation. |
| Spatial frequency | 3rd | fxwavefreq | Controls ripple density. Multiplies the squared-distance term in the phase calculation. |
The setup function stores amplitude and spatial frequency directly; speed passes through a conversion function before storage.
Displacement Formula
For each water vertex at grid position (x, z), the engine computes:
distance_sq = x_offset² + z_offset²
phase = (distance_sq * spatial_freq + frame_count * speed) & 0x7FF
wave_offset = sine_table[phase] * amplitude
bias = amplitude * centering_constant
vertex.y = wave_offset + height_table[heightmap_byte] - bias
Where:
- x_offset, z_offset — grid-relative coordinates from the center of the visible terrain window
- frame_count — global frame counter, advances each tick
& 0x7FF— wraps the phase to 2048 entries (the sine table length)- height_table — the same 128-entry height lookup table used for all terrain
- bias — centers the oscillation so waves ripple symmetrically around the base water level
The squared-distance term produces concentric circular ripples radiating outward. This is not a planar wave — the phase depends on radial distance from the grid center, so ripples form rings rather than parallel lines.
Sine Lookup Table
The wave animation indexes a 2048-entry float table allocated at runtime. The table is addressed as:
value = table[(phase & 0x7FF) * 4] (byte offset; effectively table[phase & 0x7FF] as float)
The table stores one full period of a periodic waveform across 2048 samples. Multiple engine systems share this table — it is also used for camera rotation interpolation and sky animation, confirming it is a general-purpose sine/cosine lookup rather than a water-specific waveform.
Water Level Initialization
The terrain height table has two initialization modes:
| Mode | Formula | When used |
|---|---|---|
| Default | height[i] = -ABS(source[i]) | No water level specified |
| Water-relative | height[i] = water_level - ABS(source[i]) | Water level parameter is non-zero |
When a non-zero water level is provided, a secondary rendering flag is set that enables the wave displacement pass. The same 128-entry source table is used in both modes — only the sign/offset changes.
Rendering Pipeline
The wave renderer runs each frame as part of the terrain update:
1. Build vertex grid
└─ 33×33 vertices, each with X, Y (height), Z, texture index
└─ stride: 76 bytes per vertex, 2584 bytes per row
2. Wave displacement
├─ Clear dirty flags
├─ For each grid cell:
│ ├─ Check all 4 corners for water texture indices {0, 5, 30, 31}
│ ├─ If water: replace vertex.y with sine_table[phase] * amplitude + height - bias
│ └─ Set dirty flags on affected cells and neighbors
└─ Recompute normals on displaced geometry:
├─ Face normals (cross products per triangle)
├─ Vertex normals (average adjacent face normals)
└─ Smooth normals (per-vertex lighting pass)
3. Lighting
└─ Per-vertex RGB from ambient + directional dot-product, clamped to [0, 255]
4. Rasterize
The normal recomputation after displacement ensures water surfaces receive correct per-frame lighting as waves move — the normals tilt with the displaced geometry rather than remaining flat.
Terrain Vertex Layout
Each vertex in the 33×33 grid occupies 76 bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | f32 | x | World X position (grid_x * 256.0) |
| 0x04 | 4 | f32 | y | Height (from lookup table, or wave-displaced) |
| 0x08 | 4 | f32 | z | World Z position (grid_z * 256.0) |
| 0x18–0x30 | face_normals | Two face normals per cell (upper/lower triangle) | ||
| 0x30–0x3C | vertex_normal | Averaged vertex normal for lighting | ||
| 0x48 | 4 | u32 | texture_index | Terrain texture ID (lower 6 bits used for water detection) |
Grid stride: 76 bytes between adjacent X-axis vertices, 2584 bytes (34 × 76) between rows.
Console Commands
Three runtime console commands allow tuning wave parameters without restarting:
| Command | Syntax | Effect |
|---|---|---|
fxwaveamp | fxwaveamp <value> | Set wave amplitude |
fxwavespeed | fxwavespeed <value> | Set wave speed |
fxwavefreq | fxwavefreq <value> | Set spatial frequency |
These modify the same globals as the INI parameters and take effect on the next frame.
External References
- WLD § Water Tiles — texture-index detection criteria for water cells
- WLD § Height Lookup Table — the 128-entry height table shared by terrain and water base heights
- WORLD.INI § Water —
world_wave[N]INI parameter format - SURFACE.INI — surface-type definitions including
[water],[deepwater],[scapewater]sound sections - Redguard:Glide Differences — software vs Glide renderer behavior (wave rendering may differ between renderers)