Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

FileSizeDescription
REDGUARD.EXE980 KMain game executable
RGFX.EXE1.9 MGlide (3dfx) renderer executable
DOS4GW.EXE260 KDOS/4GW protected-mode extender
3DfxSpl2.dll1.1 M3dfx Splash/Glide support library
glide2x.dll1.3 MGlide 2.x runtime
ENGLISH.RTX177 MDialogue text + voice audio container (largest file in the game)
REDGUARD.SWG / redguard.swx20 K eachSwap/workspace files
OBJECT.SAV4 KObject 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

DirectorySizeFilesContents
fonts/604 K2929 .FNT bitmap font files (Arial variants, HI/LO menu fonts, Redguard-styled fonts)
fxart/102 M755Glide-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 M4527 .RGM scene files, 5 .PVO visibility octrees, 4 .WLD terrain files, 9 .TSG trigger-state files
sound/5.3 M49MAIN.SFX (all 118 sound effects), R212.WAV, Miles Sound System drivers (.mdi, .dig), STATE.RST, audio configs
soup386/48 K1SOUP386.DEF — script function/flag definitions for the SOUP engine
system/18 M6965 .GXA UI graphics (menus, inventory, compass, maps, skies), gui.anm, gui.lbm, pointers.bmp, SKY_61.PCX
SAVEGAME/79 M59417 save slots (SAVEGAME.000016), 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:

DirectoryRendererModel versionsDescription
3dart/Softwarev2.6, v2.7Original 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) / GOGv4.0, v5.0Glide-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-renderer 3dart/ directory shipped on the original CD but is not present in the GOG release.

File Formats Overview

Binary, little-endian file formats.

File TypeExtension(s)ParserOutputDocsDescription
Sound Effects.sfxyes.wav files (directory extract)SFX.mdAll game sound effects in a single container (MAIN.SFX); 118 raw PCM clips.
Dialogue Audio.rtxyes.wav files + index.json (directory extract)RTX.mdDialogue text + voice clip container (ENGLISH.RTX) with chunk index footer (RNAV); 4866 entries (3933 voice clips, 933 text-only).
Audio.oggOgg Vorbis specOgg Vorbis format used for game audio.
TEXBSI.###yes.png files (directory extract) + metadata .jsonTEXBSI.mdTexture container (TEXBSI.###); indexed-color images with optional palettes and animation.
Palette.colyesswatch .png + palette .jsonCOL.md256-color palette files; 776 bytes (8-byte header + 256×RGB).
Font.fntyes.png + BMFont .fnt + glyph .json, or .ttfFNT.mdFont graphics files—56-byte header + optional palette data.
Model.3dyes.glb3DStatic 3D models.
Animated Model.3dcyes.glb3DCAnimated 3D models (multi-frame).
ROB Archive.robyes.glbROB.mdContains world/dungeon model data; used within maps.
Map Data.rgmyes.glb + metadata .jsonRGM.mdGame map files containing sections for objects, scripts, locations, collisions, etc.
World Geometry.wldyes.glb + metadata .json, or map .png setWLD.mdWorld geometry/height-map data with 4 sections and 128×128 maps; supports terrain GLB export (and companion RGM merge).
Visibility Octree.pvoyes.jsonPVOPre-computed visibility octree for level geometry culling.
Cheat States.chtyesCHT.mdCheat persistence file (REDGUARD.CHT); 256-byte raw dump of 64 u32 LE cheat state slots.
SOUP386.DEF.defyesSOUPDEF.mdDefinition-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)

OffsetSizeTypeNameDescription
0x004[u8; 4]versionASCII version string, e.g. v4.0
0x044u32num_verticesVertex count
0x084u32num_facesFace count
0x0C4u32radiusCollision sphere radius
0x104u32num_framesAnimation frame count. Always 1 for .3D files.
0x144u32offset_frame_dataOffset to frame data array
0x184u32total_face_verticesSum of all face vertex counts. Also the entry count for the vertex-normal indirection table.
0x1C4u32offset_section4Offset to Section4 (SubObject BVH). 0 when absent. Engine header copier zeroes this field at load time.
0x204u32section4_countNumber of Section4 entries.
0x244u32unused_24Unused — 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.
0x284u32offset_normal_indicesOffset to vertex-normal indirection table. Each entry is a u32 file offset into the vertex-normal data section. Count = total_face_vertices.
0x2C4u32offset_vertex_normalsOffset to per-vertex normal data (f32 × 3 per vertex).
0x304u32offset_vertex_coordsOffset to vertex coordinate data.
0x344u32offset_face_normalsOffset to face normal data.
0x384u32total_face_vertices_dupDuplicate 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.
0x3C4u32offset_face_dataOffset 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:

Fieldv4.0/v5.0 meaningv2.6/v2.7 meaning
0x18total_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.
0x1Coffset_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.
0x24Always 0Non-zero in 5 files (RICHA001=4490, GARDA001-004=6434)
0x2Coffset_vertex_normalsUnrelated 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.
0x38Duplicate of 0x18Does 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.
0x3CAlways 64Variable (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:

Fieldv4.0/v5.0v2.6/v2.7
offset 0x08 (reserved)Always 0Vertex offset adjustment (values up to 63,662)
offset 0x0C (frame_type)0, 2, 4, or 8Values like 8209 (0x2011), 16402 (0x4012) — different encoding

v2.6/v2.7 Face Data Differences

Aspectv4.0/v5.0v2.6/v2.7
Face texture header5 bytes (u16 + u16 + u8)3 bytes (u8 + u16)
tex_hi values61–91, 2550, 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 decodingBCD-like: (raw >> 8) - 4000000Simple: texture_id = raw >> 7
Vertex indexDirect indexByte 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):

  1. Face Data — at 0x40, num_faces variable-size records
  2. Vertex Coordinatesnum_vertices × 12 bytes
  3. Face Normalsnum_faces × 12 bytes
  4. Frame Datanum_frames × 16-byte records
  5. Section4 — SubObject BVH entries (when present)
  6. Vertex-Normal Indirection Tabletotal_face_vertices × u32
  7. Vertex Normalsnum_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.

OffsetSizeTypeNameDescription
0x004u32vertex_offsetFile offset to vertex position data
0x044u32normal_offsetFile offset to face normal data
0x084u32reservedAlways 0 in v4.0/v5.0. Used as vertex offset adjustment in v2.6/v2.7.
0x0C4u32frame_type0 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

OffsetSizeTypeNameDescription
0x001u8vertex_countNumber of vertices in this face (3–10)
0x011u8tex_hiPer-face flags/high byte field (often 0xFF for solid-color faces)
0x024u32texture_data_rawPacked texture encoding value
0x064u32unused_04Unused — always 0x00000000. Parsers should skip.
0x0A8×NFaceVertex[N]verticesN = vertex_count

Texture Decoding (v4.0 / v5.0)

Solid color (if texture_data_raw >> 20 == 0x0FFF):

  • color_index = (texture_data_raw >> 8) & 0xFF
  • tex_hi is 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 + hundreds
    

    This 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

OffsetSizeTypeNameDescription
0x001u8vertex_countNumber of vertices
0x011u8u1Separate byte field (not part of texture encoding)
0x022u16texture_dataPacked texture encoding
0x044u32unused_04Unused — always 0x00000000. Parsers should skip.
0x088×NFaceVertex[N]verticesN = 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

OffsetSizeTypeNameDescription
0x004u32vertex_indexIndex into vertex coordinate array. For v2.6/v2.7: byte offset, divide by 12. For v4.0+: direct index.
0x042i16u_deltaU texture coordinate delta
0x062i16v_deltaV 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/FaceFrequency
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:

ConditionShadingNormal used
Indirection table present, referenced vertex normal is validSmooth (Gouraud)Vertex normal from indirection lookup
Indirection table absent, vertex_normals[vertex_index] is validSmooth (Gouraud)Vertex normal by direct index
Vertex normal is NaN (0xFFC00000) or unavailableFlatFace 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:

SizeTypeDescription
4i32Center X
4i32Center Y
4i32Center Z
4u32Radius
2u16Face reference count
4f32Extent X
4f32Extent Y
4f32Extent Z
6×NFace references (u32 offset + u16 index, divide index by 4)

External References

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
FramesAlways 12+ (animated)
frame_type (frame 0)02, 4, or 8
Animated frame dataNoneFrames 1+ with compressed or full-precision geometry
Section4 (SubObject BVH)Present (v5.0)Always absent (offset=0, count=0)
Vertex-normal indirection tablePresentAlways absent (offset=0)
Versionv4.0 or v5.0Always 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

ValueMeaningFrame 1+ vertex encodingFrame 1+ normal encoding
2Compressed animationi16 × 3 (6 bytes/vertex)10-10-10-2 packed (4 bytes/face)
4Full-precision animationi32 × 3 (12 bytes/vertex)10-10-10-2 packed (4 bytes/face)
8Static (single-frame .3DC)N/AN/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:

  1. Face Data — at 0x40
  2. Vertex Coordinates — base frame positions
  3. Face Normals — base frame normals
  4. Frame Datanum_frames × 16-byte records
  5. Animated Frame Vertex/Normal Data — frames 1+ geometry
  6. Vertex Normals — per-vertex normals (f32 × 3)

External References

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)

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]magic"OARC" — possibly “Object ARChive”; exact expansion unknown.
0x044u32BEunused_04Always 4. Unused at runtime — the engine reads the file but never dereferences or tests this field. Likely a build-tool artifact.
0x084u32LEnum_segmentsNumber of segments.
0x0C4[u8; 4]magic2"OARD" — possibly “Object ARchive Data”; exact expansion unknown.
0x104u32BEpayload_sizeFile 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.

OffsetSizeTypeEndianNameDescription
0x004u32LEtotal_sizeData size + 80 (total segment size including header).
0x048[u8; 8]nameSegment name, ASCII null-padded. Model name or external .3DC filename stem.
0x0C2u16LEsegment_type0 = embedded 3D data, 512 = external .3DC reference. See below.
0x0E2u16LEsegment_flagsRender 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.
0x101u8segment_attribsPer-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.
0x113face_count_lowBuild tool artifact: the last byte (0x13) equals face_count mod 256. Not read at runtime.
0x144u32BEunused_14Unused — never read at runtime. Most commonly 1 (3,690 segments), but other values exist.
0x184u32reserved_18Always 0.
0x1C4u32LEbbox_extent_xBounding box total X extent.
0x204u32LEbbox_extent_yBounding box total Y extent.
0x244u32LEbbox_extent_zBounding box total Z extent.
0x284u32reserved_28Always 0.
0x2C4u32reserved_2CAlways 0.
0x304u32reserved_30Always 0.
0x344u32LEbbox_positive_xPositive X extent from center.
0x384u32LEbbox_positive_yPositive Y extent from center.
0x3C4u32LEbbox_positive_zPositive Z extent from center.
0x404u32LEbbox_negative_xNegative X extent from center.
0x444u32LEbbox_negative_yNegative Y extent from center.
0x484u32LEbbox_negative_zNegative Z extent from center.
0x4C4u32LEdata_sizeByte 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

TypeDescription
0Embedded 3D model data. Payload is a complete 3D file (v5.0 format).
256Embedded 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).
512External 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).

ValueHigh byteNamesRender mode
0x00000x00 → 0xFF(all normal)Standard (default)
0x8C000x8CDR_WA01, DR_WA02, LH_MIRRTransparency / mirror
0xC8000xC8WATERWATWater
0x80000x80VR_OHTStored as render-mode metadata
0x5A000x5ABEAMA001Beam / light effect

Segment Attributes (0x10)

A single byte of per-segment attribute flags.

ValueSegmentsMeaning
0x00(all normal)No special attributes
0x02PALMTR01–04 (CAVERNS, EXTPALAC, ISLAND)Texture pre-load trigger (bit 1). Engine calls a texture pre-caching function for all face textures in this segment.
0x40SS_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 is the only ROB file containing type 256 segments. It holds 3D models used for the game’s menu screens.

#NameTypeSizeVersionNotes
0MENUA00125679,328v4.0Menu character model (also exists as standalone MENUA001.3DC in /fxart)
1MB_TABLE0886v5.0Small prop
2MB_PG0125642,208v4.02Menu page model
3MB_PG0225642,208v4.02Menu page model (same size as PG01)
4MB_PG0325642,208v4.02Menu page model (same size as PG01)
5SCROLL08,136v5.0Scroll 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.

4-byte ASCII marker: "END " (with trailing space). Always present.

External References

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.

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]section_nameASCII section tag (for example RAHD, MPOB, MPSO, END )
0x044u32BEdata_lengthPayload 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 OffsetSizeTypeNameDescription
+0x004u32record_countNumber 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.

OffsetSizeTypeNameDescription
0x004u32idObject id
0x041u8object_typeObject kind discriminator
0x051u8is_activeActivation flag
0x069[u8; 9]script_nameScript/object name
0x0F9[u8; 9]model_nameModel reference
0x181u8is_staticStatic/dynamic flag
0x192i16reservedNot read at runtime.
0x1B3i24pos_xPosition X (fixed scale)
0x1E1u8pad_xAlignment byte
0x1F3i24pos_yPosition Y (fixed scale)
0x221u8pad_yAlignment byte
0x233u24pos_zPosition Z (fixed scale)
0x264u32angle_xBethesda 2048-unit Euler angle
0x2A4u32angle_yBethesda 2048-unit Euler angle
0x2E4u32angle_zBethesda 2048-unit Euler angle
0x322i16texture_dataPacked texture id/image id
0x342i16intensityLight/intensity-like field
0x362i16radiusRadius-like field
0x382i16model_idModel index/id-like field
0x3A2i16world_idWorld index/id-like field
0x3C2i16redColor channel
0x3E2i16greenColor channel
0x402i16blueColor channel

Position decode used by current exporter:

  • scale constant: 1 / 5120
  • x = -(pos_x * 256) * scale
  • y = -(pos_y * 256) * scale
  • z = -(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)
UnitsDegrees
0
51290°
1024180°
1536270°
2048360° (wraps to 0)

MPOB model lookup behavior:

  • Primary source is model_name (9 bytes, null-trimmed).
  • If model_name is empty, exporter falls back to RAAN using RAHD script metadata (see RAAN).
  • If RAAN also yields no result, script_name is used as a last resort.
  • Example: script_name = FAVIS resolves via RAAN to FVPRA001.

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.

OffsetSizeTypeNameDescription
0x004u32idObject id
0x0412[u8; 12]model_nameModel reference
0x103i24pos_xPosition X
0x131u8pad_xAlignment byte
0x143i24pos_yPosition Y
0x171u8pad_yAlignment byte
0x183u24pos_zPosition Z
0x1B1u8pad_zAlignment byte
0x1C36i32[9]rotation_matrix3x3 Q4.28 rotation matrix
0x402u8[2]unusedAlways 0.

The exporter converts rotation_matrix from Q4.28 to float and emits a node matrix with translation.

Rotation parity note:

  • MPSO rotation_matrix must 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]
  • Earlier row-major mapping ([m0,m1,m2], [m3,m4,m5], [m6,m7,m8]) produced incorrect static-object orientation for cases like TV_SEAT and BT_BOARD.

MPRP (Rope Chains)

MPRP starts with a little-endian record count, followed by 80-byte records.

All fields little-endian.

OffsetSizeTypeNameDescription
0x004u32idRope/object id
0x041u8reservedNot read at runtime.
0x053i24pos_xBase position X
0x081u8pad_xAlignment byte
0x093i24pos_yBase position Y
0x0C1u8pad_yAlignment byte
0x0D3i24pos_zBase position Z
0x104i32angle_yRope heading field
0x144i32typeType/discriminator
0x184i32swingSwing parameter
0x1C4i32speedSpeed parameter
0x202i16lengthNumber of rope links
0x229[u8; 9]static_modelOptional terminal model
0x2B9[u8; 9]rope_modelLink model name (for example ROPELINK)
0x3428i32[7]reservedNot read at runtime.

Rope instancing behavior:

  • Decode base translation with the same MPOB scale/sign rules.
  • Spawn length copies of rope_model.
  • For each link: subtract 0.8 from Y and place one instance.
  • If static_model is 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.

OffsetSizeTypeNameDescription
0x004s32offset_xX coordinate offset (applied to MPOB translated position)
0x044s32offset_yY coordinate offset
0x084s32offset_zZ 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.

OffsetSizeTypeNameDescription
0x001i8offset_xLocal collision offset X
0x011i8offset_yLocal collision offset Y
0x021i8offset_zLocal collision offset Z
0x032u16vertexModel vertex index used as reference point for the collision sphere
0x054u32radiusCollision 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 OffsetSizeTypeNameDescription
+0x004u32map_lengthTotal byte length of this walk-map
+0x044u32node_countNumber of walk-nodes in this map
+0x084u32node_count_dupDuplicate of node_count
+0x0C3s24map_pos_xMap position X
+0x0F1u8pad_xAlignment byte
+0x103s24map_pos_yMap position Y
+0x131u8pad_yAlignment byte
+0x143s24map_pos_zMap position Z
+0x171u8pad_zAlignment byte
+0x184u32radiusMap bounding radius
+0x1Cvariablewalk_nodesnode_count × WalkNode records

WalkNode Record

Relative OffsetSizeTypeNameDescription
+0x004u32node_lengthTotal byte length of this walk-node
+0x042u16node_pos_xLocal position X
+0x062s16node_pos_yLocal position Y
+0x082u16node_pos_zLocal position Z
+0x0A1u8reservedNot read at runtime.
+0x0B1u8route_countNumber of routes from this node
+0x0Cvariableroutesroute_count × NodeRoute records

NodeRoute Record (4 bytes)

Relative OffsetSizeTypeNameDescription
+0x002u16target_node_idDestination walk-node index
+0x022u16costRoute 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.

OffsetSizeTypeNameDescription
0x004field_00Overwritten at runtime (linked-list next-pointer)
0x049[u8; 9]script_nameScript/actor name, null-padded
0x0D2u16instancesNumber of instances for this actor
0x0F2paddingAlways 0.
0x114i32instance_counterRuntime instance counter (incremented during setup; advances variable_offset by num_variables × 4 per instance)
0x154u8[4]anim_speedRead as individual bytes. Byte 0x15: frame limit — animation advances only while frame_counter < byte_0x15. Byte 0x16: frame increment added per tick.
0x194u32ranm_offsetByte offset into RANM section data (rebased to pointer at load)
0x1D4u32raat_offsetByte offset into RAAT section data (rebased to pointer at load)
0x214i32raan_countNumber of RAAN entries for this actor
0x254u32raan_data_sizeTotal byte size of this actor’s RAAN entries. Zero when raan_count = 0.
0x294i32raan_offsetByte offset into RAAN section data (rebased to pointer at load)
0x2D4anim_control_prefixLow byte stored at animation control struct offset +0x12 during RAGR loading. Remaining bytes are not decoded.
0x314u32ragr_offsetByte offset into RAGR section data (rebased to pointer at load)
0x358paddingAlways 0.
0x3D4u32rafs_indexIndex into RAFS data (rebased: rafs_data + index × 11; RAFS records are 11 bytes)
0x414u32num_stringsNumber of strings used by this actor’s script
0x454paddingAlways 0.
0x494u32string_offsets_indexByte offset into RASB section data
0x4D4u32script_lengthByte length of this actor’s bytecode block in RASC
0x514u32script_data_offsetByte offset into RASC section data (rebased to pointer at load)
0x554u32script_pcExecution start address; rebased at load to script_data_offset + script_pc (absolute pointer)
0x594anim_buffer_swapRead 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.
0x5D4u32rahk_offsetByte offset into RAHK section data (rebased to pointer at load)
0x618dialogue_lockRead as byte at 0x61. Set during dialogue initiation; prevents animation transitions while dialogue is active. Checked alongside actor-type and combat-state guards.
0x694u32ralc_offsetByte offset into RALC section data (rebased: ralc_data + (offset ÷ 12) × 12)
0x6D4u8[4]actor_flagsRead 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.
0x714u32raex_offsetByte offset into RAEX section data (rebased to pointer at load)
0x754u32num_variablesNumber of local variables for this actor
0x794u8[4]visibility_flagsRead 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).
0x7D4u32variable_offsetByte offset into RAVA section data (÷ 4 = variable array index)
0x814u32variable_offset_dupRuntime copy of variable_offset; advanced by num_variables × 4 per instance
0x854u32anim_frame_dataAnimation frame count or group index. Upper 16 bits used as count (× 11 bytes per frame for allocation).
0x894i32soup_func_primarySOUP386 function table index (primary). Multiplied by 49 to index into function table. -1 = disabled.
0x8D4i32soup_func_secondarySOUP386 function table index (secondary). Same indexing. -1 = disabled.
0x914i32soup_func_tertiarySOUP386 function table index (tertiary). -1 = disabled.
0x952i16combat_flagCombat/state flag.
0x972i16raex_statStored at actor state +0x97 after SOUP function lookup. -1 = disabled.
0x992i16reserved_99Always -1. Not read at runtime.
0x9B2i16reserved_9bAlways 0. Not read at runtime.
0x9D4i32ravc_offsetByte offset into RAVC section data (rebased to pointer at load; -1 = none)
0xA14i32ravc_countNumber 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):

OffsetSizeTypeNameDescription
0x004u32reservedNot read by any engine function. Skipped during entry iteration.
0x041u8frame_countUsed as loop count for animation handle table entries. Capped at 255.
0x051u8model_typeType flag, converted to lowercase at load. Values: 0x63 (ASCII ‘c’) and 0x73 (ASCII ‘s’).
0x06var[u8]file_pathNull-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:

  1. Look up script_name in the RAHD index to get (raan_offset, raan_count).
  2. Parse the RAAN entry at raan_offset to extract the file path.
  3. Strip the path to a bare filename stem (e.g. fxart\FVPRA001.3DCFVPRA001).
  4. Use the stem as the model name for asset lookup.
  5. If no RAHD/RAAN match, fall back to using script_name as 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.

OffsetSizeTypeNameDescription
+0x004u32string_offsetByte 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

RegionSizeDescription
Preamblescript_data_offset bytesZero-padded address space used by rebased hook/offset references
Script blocksremainderConcatenated 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.

OffsetSizeTypeNameDescription
0x002i16grip0Named from console. Used as animation frame offset during weapon transitions (consumed by the combat animation subsystem, not the attachment vertex system).
0x022i16grip1Same subsystem as grip0.
0x042i16scabbard0Named from console. Same subsystem as grip0.
0x062i16scabbard1Same subsystem as scabbard0.
0x082i16anim_frame_refMatches RAHD anim_frame_data at 0x85. Set on mobile actors only.
0x0A2u16texture_idTexture override id for actor skin variants.
0x0C2i16v_vertexVertex-related field.
0x0E2i16v_sizeSize-related field.
0x102i16taunt_idFirst taunt animation id; additional taunts count up from this value.
0x122i16field_12Set only on large creatures (dragon, gremlin).
0x142i16field_14Source value for RAHD raex_stat at 0x97. Set on combat actors.
0x162i16field_16Set on some combat actors.
0x182i16range_minCombat engagement minimum range. Multiply by 256 for world units. Only set on dragon, golem, serpent.
0x1A2i16range_idealCombat ideal range. Same scaling.
0x1C2i16range_maxCombat 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 autoendauto 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 OffsetSizeTypeNameDescription
+0x002u16entry_sizePayload size in bytes after this field. 0 = end of groups. Should equal 8 + frame_count × 3.
+0x022u16group_indexAnimation group slot (0–177; validated ≤ 0xB1 at load)
+0x042u16anim_idAnimation identifier
+0x062u16anim_typeAnimation type (only low byte used). Values: 0 = interruptible (idle/panic), 1 = must complete (combat), 2 = no panic revert (ledge-hang loops).
+0x082u16frame_countNumber of animation frames in this group
+0x0Avar[u8; frame_count × 3]commandsPacked 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.

OffsetSizeTypeNameDescription
0x003u8[3]color_rgbColor bytes (R, G, B).
0x031u8light_typeLight type. Values: 0, 130 (0x82), 131 (0x83), 132 (0x84).
0x044u32light_paramZero for ambient lights; 28 for directional lights.
0x083i24pos_xPosition X
0x0B1u8pad_xAlignment byte
0x0C3i24pos_yPosition Y
0x0F1u8pad_yAlignment byte
0x103i24pos_zPosition Z
0x131u8pad_zAlignment byte
0x142i16param0Intensity or range parameter
0x162i16param1Intensity or range parameter
0x186i16[3]directionDirection/attenuation vector (3 × i16). Non-zero in active lights.
0x1E8u8[8]channel_mapLight channel enable. Always either 00 01 02 03 04 05 06 07 (active, identity mapping to 8 channels) or all zeros (inactive).
0x264u8[4]reserved_26Always 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.

OffsetSizeTypeNameDescription
0x003i24pos_xPosition X
0x031u8pad_xAlignment byte
0x043i24pos_yPosition Y
0x071u8pad_yAlignment byte
0x083i24pos_zPosition Z
0x0B1u8pad_zAlignment byte
0x0C1u8reservedNot 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.

OffsetSizeTypeNameDescription
+0x004ptrnextNext record in linked list (0 = last)
+0x044ptractor_ptrPointer to actor object (from RAHD)
+0x084u32field_08From RAHD +0x51
+0x0C4ptrresource_0Allocated resource pointer
+0x104ptrresource_1Allocated resource pointer
+0x144ptrresource_2Allocated resource pointer
+0x182i16field_18From 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.

OffsetSizeTypeNameDescription
0x004u32idObject id
0x044i32reservedNot read at runtime.
0x083i24pos_xPosition X
0x0B1u8pad_xAlignment byte
0x0C3i24pos_yPosition Y
0x0F1u8pad_yAlignment byte
0x103u24pos_zPosition Z
0x131u8pad_zAlignment byte
0x142u16texture_dataPacked: texture_id = data >> 7, image_id = data & 0x7F
0x162i16reservedNot 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

  • RGM carries scene placement transforms; ROB alone does not.
  • Practical scene assembly requires MPOB + MPSO + MPRP + MPSF.
  • Model lookup needs direct file stems, ROB segment-name resolution, and RAHD/RAAN fallback for empty MPOB.model_name (see RAHD and RAAN).
  • Some names in shipped RGM files are truncated forms like NAME.3 and 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 like LOCKDOOR, lighting markers like NTLIGHT, entrance triggers like ENT*) 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 (textureId near 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 layoutFields 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

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]) at 0x00..0x17
  • sec_hdr_size (u32) at 0x18
  • file_size (u32) at 0x1C
  • unknown2[28] (u32[28]) at 0x20..0x8F
  • sec_ofs[4] (u32[4]) at 0x90..0x9F
  • unknown3[256] (u32[256]) at 0xA0..0x49F
OffsetSizeTypeNameDescription
0x004u32unknown1_0Always 16.
0x044u32section_colsAlways 2.
0x084u32section_rowsAlways 2.
0x0C4u32reserved_0cAlways 0.
0x104u32unknown1_4Always 160 (0xA0).
0x144u32unknown1_5Always 1.
0x184u32section_header_sizeAlways 22.
0x1C4u32file_size_fieldAlways 263416 (file_size - 16).
0x904u32section0_offsetAlways 1184 (0x4A0).
0x944u32section1_offsetAlways 66742 (0x104B6).
0x984u32section2_offsetAlways 132300 (0x204CC).
0x9C4u32section3_offsetAlways 197858 (0x304E2).
0xA04u32unknown3_0Always 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)SizeTypeNameDescription
+0x006u16[3]unknown1Section-local unknown values.
+0x062u16texbsi_fileSection-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).
+0x082u16map_sizeAlways 256 (2 x 128).
+0x0A12u16[6]unknown2Always 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):

SectionHeader bytes (hex)
068 08 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00
138 02 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00
21C 05 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00
30A 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
  • 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 3 bit packing (index + rotation) is corroborated by UESP documentation.
  • Original-engine terrain initialization hard-loads texbsi.302 and precomputes a 64-entry terrain texture lookup table from that archive, matching Map 3’s 0..63 index space.
  • Engine string analysis confirms texbsi.302 is referenced by the terrain initialization path; texbsi.%s is referenced by the generic TEXBSI loader path, not the terrain-table initialization path.
  • Conclusion: per-section texbsi_file switching is not used by the original engine’s terrain renderer; terrain textures come from TEXBSI.302.

Cross-check against shipped fxart/TEXBSI.302:

  • Map 3 index range 0..63 is fully covered by image ids in TEXBSI.302 (name suffixes 00..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 IFHD animated records in TEXBSI.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 index 0 as dominant (rotation may be ignored for empty/default tiles).

Bank summary (fxart/TEXBSI.*, D-prefix terrain-style banks):

  • Only TEXBSI.302 has full 0..63 coverage (56 BSIF static records + 8 IFHD animated records). Other D* 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:

BitsMaskFieldRange
5:0& 0x3Ftexture index0–63
7:6>> 6 & 3quarter-turn rotation0–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:

  1. 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.
  2. 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.
  3. 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.

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):

  1. 0x4F4C5554 ("TULO" bytes in file order)
  2. 0x0043C028
  3. 0xFFFFFFFF
  4. 0x00043735

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 3 indices (Map 3 index range 0–63 is fully covered by TEXBSI.302).

External References

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.

OffsetSizeTypeEndianNameDescription
0x004u32BEsection_sizePayload size excluding this field
0x0432[u8; 32]descriptionASCII string, set by internal tool “SoupFX”
0x244u32LEeffect_countNumber 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.

OffsetSizeTypeNameDescription
0x004u32type_idAudio type: 0 = 8-bit mono, 1 = 16-bit mono, 2 = 8-bit stereo (unused), 3 = 16-bit stereo
0x044u32bit_depth0 = 8-bit, 1 = 16-bit
0x084u32sample_rateAlways 11025 or 22050 Hz
0x0C1u8unused_0cAlways 64. Runtime behavior is driven by the surrounding 26-byte header block (0x000x19), with no separate per-field behavior for this byte. Likely a vestigial default volume value (64/127 ≈ 50% on the Miles Sound System scale).
0x0D1i8loop_flag0 = no loop, non-zero = enable looping. The engine checks only != 0; values -1 (0xFF) and -31 (0xE1) are functionally identical.
0x0E4u32loop_offsetByte offset into PCM data for loop restart point (always 0)
0x124u32loop_endSample count before looping (always 0xFFFFFFFF)
0x164u32data_lengthByte count of raw PCM data following this header
0x1A1u8reserved_1aPadding between header and PCM data. Always 0.
0x1Bvar[u8]pcm_dataRaw 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 OffsetSizeSourceContents
0x00–0x1926FileHeader fields (type_id through data_length)
0x1A–0x1D4RuntimePointer to allocated PCM data buffer
0x1E–0x214RuntimeComputed duration value: (data_length << 8) / (sample_rate × bytes_per_sample × channels)
  • 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]
Offset (from EOF)SizeTypeNameDescription
-124ASCIIfooter_tagAlways RNAV
-84u32 LEindex_offsetAbsolute file offset of index table
-44u32 LEindex_countNumber of index entries

For ENGLISH.RTX:

  • footer_tag = RNAV
  • index_offset = 184629473
  • index_count = 4866

Chunk Record (on disk)

Every payload in the data region has an 8-byte chunk header before it:

OffsetSizeTypeEndianNameDescription
0x004ASCIItag4-character chunk label
0x044u32BEpayload_sizeSize of payload bytes that follow
0x08var[u8]payloadEntry payload

Index Table

The index is an array of 12-byte entries. Each entry points to one chunk payload.

OffsetSizeTypeEndianNameDescription
0x004ASCIItagSame tag as the chunk header
0x044u32LEpayload_offsetAbsolute file offset of payload (not header)
0x084u32LEpayload_sizePayload byte size

Validation notes (all 4866 entries):

  • payload_offset - 8 points to a chunk header with matching tag
  • header payload_size (big-endian) matches indexed payload_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 entry
  • 1 = audio entry

String-Only Payload (payload[1] = 0)

OffsetSizeTypeEndianNameDescription
0x001u8kindAlways 0
0x011u8subtypeAlways 0 for text entries
0x022u16LEstring_lenByte length of ASCII text
0x042u16LEreservedAlways 0
0x06var[u8]textASCII text bytes, no terminator

Payload size rule:

payload_size = 6 + string_len

Audio Payload (payload[1] = 1)

OffsetSizeTypeEndianNameDescription
0x001u8kindAlways 0
0x011u8subtypeAlways 1 for voice entries
0x022u16LEstring_lenByte length of ASCII label
0x042u16LEreservedAlways 0
0x06var[u8]labelASCII label bytes, no terminator
0x06+N27structaudio_headerAudio metadata (below), N = string_len
0x21+Nvar[u8]audio_dataRaw 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.

OffsetSizeTypeNameDescription
0x004u32type_id0 = 8-bit mono, 1 = 16-bit mono
0x044u32bit_depth0 = 8-bit, 1 = 16-bit
0x084u32sample_rate11025 or 22050
0x0C1u8level_0cAlways 100
0x0D1i8loop_flagAlways 0
0x0E4u32loop_offsetAlways 0
0x124u32loop_endAlways 0xFFFFFFFF
0x164u32audio_lengthByte length of audio_data
0x1A1u8reserved_1aAlways 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.
  • 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

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.

OffsetSizeTypeNameDescription
09[u8; 9]image_nameImage name, null-padded. Format: {type_char}{file_num:02d}{image_idx:03d}. All-zero = end of file.
94u32subrecord_bytesTotal size of all subrecords that follow (excludes this 13-byte envelope).
13subrecordssubrecordsTagged 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:

  • ### % 100 matches the 2-digit file number in image names.
  • ### / 100 maps to type-char family: 0->A, 1->B, 2->C, 3->D, 4->E, 5->F.

Examples:

  • TEXBSI.302 contains D02xxx images.
  • TEXBSI.114 contains B14xxx images.

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.

OffsetSizeTypeNameDescription
04[u8; 4]tagTag: BSIF, IFHD, BHDR, CMAP, DATA, or END
44u32payload_sizePayload size in bytes (not including this 8-byte header)
8payloadpayloadTag-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.

OffsetSizeTypeNameDescription
02i16x_offsetX position hint (placement on virtual canvas)
22i16y_offsetY position hint
42i16widthImage width in pixels
62i16heightImage height in pixels
81u8has_cmapExport-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.”
91u8export_flagsExport-tool metadata. Values: 0, 1, or 9. Packed into the same u16 as has_cmap during export. Purpose within the build pipeline unknown.
104reservedAlways 0
142i16frame_count1 = static, 2–16 = animated
162i16anim_delayRead 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.
184reservedAlways 0
222u16tex_scaleRead 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.
242i16data_encodingPixel 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:

  1. External .COL palette — 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.
  2. Embedded CMAP — the 256-color palette stored inside the image record (only present in animated IFHD images).
  3. 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 number
  • image_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

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.

OffsetSizeTypeNameDescription
0x004u32file_sizeAlways 776.
0x044u32magicAlways 0x0000B123.
0x08768[u8; 768]palette256 × 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.

  • TEXBSI — the CMAP subrecord 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/FPAL palettes (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), and hex fields. Versioned as redguard-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

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:

  1. FNHD (always present)
  2. BPAL or FPAL (always present)
  3. FBMP (always present)
  4. RDAT (optional)
  5. END marker

Common chunk orders:

  • FNHD -> BPAL -> FBMP -> END
  • FNHD -> BPAL -> FBMP -> RDAT -> END
  • FNHD -> FPAL -> FBMP -> END (ARIALVS.FNT only)

Chunk Header

Each chunk begins with an 8-byte header:

Chunk-length fields are big-endian.

OffsetSizeTypeNameDescription
0x004[u8; 4]tagChunk name (FNHD, BPAL, FPAL, FBMP, RDAT)
0x044u32lengthChunk 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.

OffsetSizeTypeNameDescription
0x0032[u8; 32]descriptionFont/tool description string; may contain NUL padding or multiple NUL-terminated fragments
0x202u16unknown_24Not read at runtime — overwritten during glyph loading. Known values: 0, 1, 3. Export-tool metadata.
0x222u16has_rdat1 if RDAT chunk present; 0 otherwise. Not checked at runtime — the engine never searches for RDAT.
0x242u16reserved_28Always 0. Not read at runtime.
0x262u16reserved_2aAlways 0. Not read at runtime.
0x282u16reserved_2cAlways 0. Not read at runtime.
0x2A2u16max_widthExport-tool hint (range 11–23). Not read at runtime — overwritten with the width of glyph ‘W’ during loading.
0x2C2u16line_heightUsed by the engine for text layout and baseline positioning. Values: 9, 10, 12, 14, 16, 22, 25, 26.
0x2E2u16character_startFirst encoded codepoint; always 32 (0x20, space). Not read at runtime — engine assumes fixed start.
0x302u16character_countUsed by the engine as the glyph loop bound (clamped to 256). Number of glyph records in FBMP. Values: 95, 97, 98, 104, 112, 208.
0x322u16reserved_36Always 0. Overwritten to 0xFF during glyph loading.
0x342u16reserved_38Always 0. Overwritten to 0xFF during glyph loading.
0x362u16has_paletteUsed 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).

OffsetSizeTypeNameDescription
0x00768[u8; 768]rgb_triplets256 entries x 3 bytes (R, G, B), palette indices referenced by FBMP pixel bytes

Notes:

  • BPAL is the normal tag.
  • FPAL appears in ARIALVS.FNT with 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.

OffsetSizeTypeNameDescription
0x002u16enabled0 = disabled/unrendered glyph; non-zero = active glyph
0x022i16offset_leftHorizontal draw offset in pixels
0x042i16offset_topVertical draw offset in pixels
0x062u16widthGlyph bitmap width in pixels
0x082u16heightGlyph bitmap height in pixels
0x0Awidth*height[u8]pixelsRow-major palette indices

Value ranges:

  • offset_left: 0..5
  • offset_top: 0..19
  • width: 1..22
  • height: 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.

OffsetSizeTypeNameDescription
0x00136[u8; 136]source_nameNUL-padded source/tool string
0x884u32unknown_90Non-zero metadata field
0x8C4u32unknown_94Non-zero metadata field
0x904u32unknown_98Always 0
0x944u32unknown_9cUsually 0; value 2 in ARIALBG.FNT
0x984u32unknown_a0Near max_width-like values
0x9C4u32unknown_a4Near line_height-like values
0xA04u32unknown_a8Small enum-like values (1..3)
0xA44u32unknown_acSmall enum-like values (1..2)
0xA84u32unknown_b0Always 0
0xAC1u8unknown_b4Always 0

RDAT is metadata. The font loader uses FNHD, FPAL/BPAL, and FBMP; it does not parse RDAT payload data.

External References

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:

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]tagASCII section tag
0x044u32BEdata_lengthPayload size in bytes (0 for END )

Section layout

FileOCTHOCTRPLSTMLSTENDTotal
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:

  • PTCH section length is serialized as patch_count * 6 bytes.
  • 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):

OffsetSizeTypeEndianNameDescription
0x004u32LEoctr_node_indexOCTR-node index ((node_ptr - octr_base) / 5) identifying which octree node receives the patch object id.
0x042u16LEobject_indexObject index appended to the runtime-visible MLST id list for that node.

Engine behavior:

  • Add patch: writes octr_node_index at record+0 and object_index at record+4.
  • Delete patch: matches/removes records by the same pair (octr_node_index, object_index).
  • Apply patch: scans records matching current octr_node_index and appends object_index values into the runtime visibility-id buffer.

OCTH Section — Header (52 bytes payload)

OffsetSizeTypeEndianNameDescription
0x004[u8; 4]magicOCTH
0x044u32BEheader_data_sizeAlways 52 (0x34).
0x084u32LEdepthAlways 10. Maximum octree depth.
0x0C4u32LEtotal_nodesTotal octree node count. Equals leaf_nodes + interior_nodes.
0x104u32LEleaf_nodesLeaf node count. Equals total_nodes - interior_nodes.
0x144u32LEmlst_polygon_countTotal entries in the MLST polygon index table. Invariant: mlst_polygon_count * 2 == MLST data_length.
0x184u32LEreservedAlways 0.
0x1C4u32LEcell_sizeRoot cell half-extent. Power of 2: 16384 (4 files) or 8192 (PALACE).
0x204i32LEcenter_xOctree root center X coordinate.
0x244i32LEcenter_yOctree root center Y coordinate.
0x284i32LEcenter_zOctree root center Z coordinate.
0x2C16reservedAlways 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:

Filetotal_nodesleaf_nodesinterior_nodes
CATACOMB23,28716,7876,500
CAVERNS25,69419,6186,076
DRINT29,11222,7176,395
ISLAND23,15418,9714,183
PALACE5,1134,1051,008

mlst_polygon_count confirmation

Filemlst_polygon_countMLST data_lengthcount × 2 == length
CATACOMB55,394110,788yes
CAVERNS16,81533,630yes
DRINT40,53781,074yes
ISLAND108,189216,378yes
PALACE54,461108,922yes

Center coordinates and extents

Filecenter_xcenter_ycenter_zcell_size
CATACOMB35,584-11,52029,95216,384
CAVERNS27,392-9,98421,50416,384
DRINT23,808-13,05633,02416,384
ISLAND35,584-16,38436,60816,384
PALACE31,488-6,14430,7208,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

OffsetSizeTypeNameDescription
01u8child_maskBit field. Bit i set = child i is present (octants 0..7).
14u32leaf_refByte offset into the PLST section. 0xFFFFFFFF = no leaf data (interior-only node).
54 × nu32[n]child_refsOne 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

PatternBinaryMeaning
0x0000000000Leaf node, no children
0x3300110011Children in octants 0,1,4,5 (one face)
0xCC11001100Children in octants 2,3,6,7 (opposite face)
0xAA10101010Children in octants 1,3,5,7 (axis-aligned half)
0x5501010101Children in octants 0,2,4,6 (other half)
0xFF11111111All 8 children present

Child and leaf sentinel values

Two sentinel values appear in octree records:

  • 0xFFFFFFFF in the leaf_ref field marks interior-only nodes — nodes with children but no directly associated polygon list. The count of these values equals interior_nodes from the header. In runtime traversal code, 0xFFFFFFFF also serves as the null terminator that ends octree walks.
  • 0xFFFFFFFE in child_refs marks 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

OffsetSizeTypeNameDescription
01u8entry_countNumber of entries in this leaf.
16 × nentriesArray of entry_count entries (see below).

Record size = 1 + 6 * entry_count

Each entry:

OffsetSizeTypeNameDescription
02u16countNumber of polygon indices in this sub-list.
24u32mlst_startStarting 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 * 2 bytes.

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 LoadStatic script commands, MPRP rope chains, or other non-MPSO visibility targets).
FileTotal IDsMPSO rangeSecondary rangeMPSO objectsSecondary count
CATACOMB5910–500501–59050190
CAVERNS2330–203204–23220429
DRINT2840–257258–28325826
ISLAND1,7040–1,6901,691–1,7031,69113
PALACE2890–262263–28826326

The MPSO record size (66 bytes = 0x42) appears as the multiplier in visibility lookups.

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:

  1. Compute world bounding box from level geometry.
  2. Iterate a uniform 3D grid at 256-unit spacing.
  3. At each grid point, run a visibility query to determine which polygons are visible.
  4. Insert the visible polygon set as a leaf into the octree.
  5. Prune single-child branches, then write the file.

Debug console commands

CommandDescription
pvoi / pvotreeinfoDisplay PVO tree statistics
pvoa / pvoaddpatchAdd object to PVO visibility patch
pvod / pvodeletepatchRemove object from PVO patch
pvoonoffToggle PVO visibility system on/off
pvos / pvopatchsaveSave PVO patches to PTCH section
pvol / pvotreeloadLoad 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.

StepDescription
1Count placed objects with visibility flag set → secondary_count
2Allocate secondary_count × 4 bytes for pointer array
3Iterate placed objects; for each with flag +0x7a != 0, append pointer to array
4At runtime, MLST index - MPSO_count indexes into this array

External References

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 their byte_offset field.

Reference consistency in shipped files:

  • 100% of child_refs match a node byte_offset.
  • 100% of leaf_refs fall 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

SectionDelimiterContent
Functions[functions][refs]One callable entry per line: <type> <name> params <count> where type is task or function
References[refs]autoOne reference/equate name per line
AttributesautoendautoOne attribute name per line (maps to per-actor RAAT byte slots)
Flags[flags] … EOFOne 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

  • RASC and standalone .AI contain compiled script bytecode. No .AI files 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] in SOUP386.DEF.
  • RAAT attribute bytes are interpreted using names declared in the autoendauto attribute 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

Configuration Files

Text-based INI configuration files shipped with the game.

Game Root Directory

FileSizeDocsDescription
SYSTEM.INI5.2 KBsystem-ini.mdPrimary engine configuration: rendering, gameplay, camera, dialog, debug, and 3D subsystem parameters.
COMBAT.INI54 KBcombat-ini.mdCombat system: attack/defense moves, combos, and dialogue taunts for all combatants.
ITEM.INI30 KBitem-ini.mdItem database: all collectible objects, weapons, potions, keys, and quest items.
WORLD.INI19 KBworld-ini.mdWorld/level database: map files, palettes, lighting, sky, weather, and PVO node maps.
MENU.INI42 KBmenu-ini.mdMenu system layout: page structure, text placement, textures, and movie definitions.
KEYS.INI3 KBkeys-ini.mdInput bindings: keyboard scancodes, mouse buttons, and joystick axes to game actions.
REGISTRY.INI302 Bregistry-ini.mdFile-system abstraction: archive lookup and 32-bit file access (non-functional in shipped game).
SURFACE.INI4.3 KBsurface-ini.mdTerrain surface-type assignments, blend behavior, and sound remapping.

Asset Directory Files

FilePathDocsDescription
FOG.INIfxart/fog-ini.mdFog density ramp table for the terrain renderer.
DIG.INIsound/Miles Sound System digital audio driver configuration.
MDI.INIsound/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:

  1. [screen] — display resolution and palette settings
  2. [system] — core engine paths, audio, physics, and HUD layout
  3. [debug] — developer diagnostics and logging flags
  4. [game] — gameplay physics thresholds and interaction radii
  5. [cyrus] — player character movement and control parameters
  6. [3dmanager] — 3D object cache and memory budget
  7. [xngine] — renderer texture memory, clipping planes, and sky behavior
  8. [camera] — camera rig offsets, distances, and glide factors for each mode
  9. [dialog] — dialog menu layout and speech settings
  10. [3dfx] — Glide/GOG-specific resolution and font scaling overrides

[screen]

Display mode and palette initialization settings.

KeyDefault ValueDescription
candle_mode2Candle/torch lighting mode index.
colour_bits8Color depth in bits per pixel (8 = paletted).
resolution1Software renderer resolution index.
Palette_red0Red component of the initial palette background color.
Palette_green0Green component of the initial palette background color.
Palette_blue0Blue component of the initial palette background color.
smk_interlace0Smacker 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.

KeyDefault ValueDescription
game_bitmapsystem\powerup.gxaPath to the startup/powerup UI bitmap (GXA format).
pointerssystem\pointers.bmpPath to the cursor sprite sheet.
system_fontfonts\redguard.fntPath to the primary system font.
icon_fontfonts\arialvs.fntPath to the icon/small font.
gui_fontfonts\arialbg.fntPath to the main GUI font.
gui_low_fontfonts\arialvb.fntPath to the low-resolution GUI font.
animation_driveD:\Drive letter used for animation streaming.
back_texture0Background texture index. 0 = none.
volume255Master sound volume (0..255).
sound1Sound system enabled. 1 = on.
fast_sound1Fast sound mixing mode. 1 = on.
fidelity0Sound fidelity level.
redbookonCD audio (Red Book) playback enabled.
redbook_volume200CD audio volume level.
sound_distance64Maximum distance at which sounds are audible.
post_collide_height50Post-collision step height for ground snapping.
hpost_collide_height50Post-collision step height for hanging/climbing.
pre_validatenoPre-validate collision geometry on load.
normal_frame_rate12Target frame rate for normal gameplay.
use_smooth_fpsyesEnable frame-rate smoothing.
use_smooth_divisoryesEnable frame-rate smoothing divisor.
min_frame_rate6Minimum allowed frame rate.
max_frame_rate300Maximum allowed frame rate.
jump_time6Duration of a jump in frames.
jump_height80Jump height in engine units.
sphere_object_scale256Scale factor for sphere collision objects.
standing_height-2Vertical offset for the player standing position.
disable_textyesDisable on-screen text rendering.
disable_debug_textyesDisable debug text overlay.
normal_ink1Normal ink/outline rendering mode.
slide_range-60000Slide detection range threshold.
statics_loadyesLoad static objects.
flats_loadyesLoad flat/billboard objects.
objects_loadyesLoad dynamic objects.
lights_loadyesLoad light objects.
ropes_loadyesLoad rope objects.
task_systemyesEnable the task/AI system.
animation_systemyesEnable the animation system.
floating_point_physicsyesUse floating-point physics calculations.
rtx_filenameENGLISH.RTXPath to the dialogue/voice container file.
world_iniWORLD.INIPath to the world definitions INI.
item_iniITEM.INIPath to the item definitions INI.
start_fadeonFade in on game start.
compass_xco546Compass HUD element X coordinate.
compass_yco396Compass HUD element Y coordinate.
candle_xco12Candle HUD element X coordinate.
candle_yco8Candle HUD element Y coordinate.
logbook_xco540Logbook HUD element X coordinate.
logbook_yco20Logbook HUD element Y coordinate.
pickup_xco12Pickup prompt HUD element X coordinate.
pickup_yco386Pickup prompt HUD element Y coordinate.
pickup_text_yco436Pickup text HUD element Y coordinate.
game_xco1576Game UI element 1 X coordinate.
game_yco16Game UI element 1 Y coordinate.
game_xco2576Game UI element 2 X coordinate.
game_yco296Game UI element 2 Y coordinate.
lock_windowsnoLock window position/size.
disable_drive_checkyesSkip CD drive presence check on startup.
disable_cpu_checkyesSkip CPU speed check on startup.
disable_svga_checkyesSkip SVGA capability check on startup.
report_machinenoLog machine hardware info on startup.
max_active_objects32Maximum number of simultaneously active objects.
max_effects32Maximum number of simultaneous particle effects.
max_particles512Maximum number of simultaneous particles.
max_remap_objects64Maximum number of palette-remapped objects.
disable_effectsnoDisable 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.

KeyDefault ValueDescription
final_version1Marks this as a final/release build. Suppresses some developer output.
enable_logs0Enable runtime log file writing.
map_lognoLog map loading events.
console_erroryesPrint errors to the console.
software_interruptyesEnable software interrupt handling.
software_breakyesEnable software breakpoint handling.
attempt_recovernoAttempt to recover from errors rather than aborting.
object_lognoLog object system events.
video_lognoLog video/renderer events.
node_markernoDisplay navigation node markers.
task_debugnoEnable task/AI debug output.
memory_manager0Memory manager debug level.
monitor_object(empty)Name of an object to monitor for debug output.
display_mastersnoDisplay master object markers.
display_slavesnoDisplay slave object markers.
display_edgesnoDisplay edge/collision markers.
ignore_errorsyesContinue running on non-fatal errors.
disable_familynoDisable object family grouping.
disable_master_slavesnoDisable master/slave object relationships.
disable_slavesnoDisable slave objects.
family_lognoLog object family events.
script_lognoLog script execution events.
show_managernoDisplay the object manager overlay.
display_node_mapnoDisplay the navigation node map.
display_nodesnoDisplay individual navigation nodes.
display_markersnoDisplay world markers.
network_marker_fileg:\PROJECTS\REDGUARD\DEMO\NETWORK.MRKPath to the network marker file. Build-time developer path; not used in the shipped game.
object_system_lognoLog object system events.
convert_static_anglesnoConvert static object angles on load.
def_checksumyesVerify SOUP386.DEF checksum on load.
render_lognoLog renderer events.
idebug0Interactive debug level.
idebug_refreshnoRefresh interactive debug display each frame.
memory_monitornoEnable memory usage monitoring.

[game]

Gameplay physics thresholds, fall damage, and interaction radii.

KeyDefault ValueDescription
fall_bounce_height100Fall height (engine units) below which the player bounces without damage.
fall_death_height568Fall height at which the player dies.
fall_hurt_height300Fall height at which the player takes damage.
fall_hurt_zap10Damage amount applied at fall_hurt_height.
slide_hurt_height1024Slide distance at which the player takes damage.
slide_hurt_zap10Damage amount applied at slide_hurt_height.
rope_jump_add18Velocity added when jumping from a rope.
rope_attach_angle512Angle threshold for rope attachment (engine angle units).
slide_speed36Player slide speed in engine units per frame.
rtx_pickup_override_time24Duration (frames) of the pickup text override display.
swim_depth40Depth threshold for switching to swim mode.
player_dead_time3Time (seconds) before respawn after death.
player_fall_dead_time12Time (frames) before death is registered after a fatal fall.
old_combatoffUse legacy combat system.
lineup_distance128Distance at which enemies line up for combat.
dialog_radius512Radius within which NPCs can initiate dialog.
combat_sphere_scale200Scale factor for combat hit sphere.

[cyrus]

Player character (Cyrus) movement, control, and camera-follow parameters.

KeyDefault ValueDescription
sheath_sword_delay6Frames before the sword auto-sheathes after combat.
auto_defend1Enable automatic defense. 1 = on.
walk_mode1Walk mode index.
turn_speed12Base turning speed.
turn_max_speed48Maximum turning speed.
mouse_turn0Mouse-driven turning. 0 = disabled.
joy_tolerance60Joystick dead-zone tolerance.
camera_distance200Default camera follow distance.
tap_time12Maximum frames between taps for a double-tap input.
poly_push_units16Distance (engine units) the player is pushed out of geometry on collision.
auto_graboffAutomatically grab ledges and ropes.
smooth_post_min2Minimum smoothing steps for post-collision position.
smooth_post_max10Maximum smoothing steps for post-collision position.
smooth_post_divisor2Divisor for post-collision position smoothing.

[3dmanager]

3D object cache, memory budgets, and manager behavior.

KeyDefault ValueDescription
buffer_kbytes800Size of the 3D streaming buffer in kilobytes.
heap_kbytes22000Size of the 3D object heap in kilobytes.
max_objects255Maximum number of 3D objects loaded simultaneously.
compress1Enable compressed object loading. 1 = on.
save_compressed0Save objects in compressed form. 0 = off.
cache_objectsyesCache loaded 3D objects in memory.
cache_lifetime64Number of frames a cached object is retained after last use.
dummy_manager0Use a dummy (no-op) 3D manager. 0 = off.
in_view_enabledyesEnable in-view culling for 3D objects.
shutdown_modeyesPerform full shutdown cleanup on exit.

[xngine]

Renderer memory budgets, clipping planes, perspective settings, and sky behavior.

KeyDefault ValueDescription
texture_kbytes12000Texture memory budget in kilobytes.
gfx_kbytes256General graphics buffer size in kilobytes.
front_plane7Near clipping plane distance.
back_plane3800Far clipping plane distance.
perspective_low_x190Perspective correction X extent at low detail.
perspective_low_y190Perspective correction Y extent at low detail.
perspective_med_x400Perspective correction X extent at medium detail.
perspective_med_y400Perspective correction Y extent at medium detail.
perspective_high_x800Perspective correction X extent at high detail.
perspective_high_y800Perspective correction Y extent at high detail.
perspective_ultra_x800Perspective correction X extent at ultra detail.
perspective_ultra_y800Perspective correction Y extent at ultra detail.
detail8Renderer detail level.
ambient_light32Global ambient light level (0..255).
screen_scale256Screen-space scale factor.
haze_depth768Distance at which atmospheric haze begins.
sky_disable0Disable sky rendering. 0 = sky enabled.
sky_move1Enable sky scrolling. 1 = on.
sky_xrotate3Sky X-axis rotation speed.
sky_yrotate40Sky Y-axis rotation speed.
game_detail2In-game detail preset index.
exclusion0Exclusion 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.

KeyDefault ValueDescription
static_rope_threshold5Velocity threshold below which a rope is considered static.
obstacle_size20Radius used for camera obstacle avoidance.
camera_size10Camera collision sphere radius.
camera_scape_size5Camera collision sphere radius in scape/exterior areas.
target_offset_x-6000Target point X offset from the player.
target_offset_y-14000Target point Y offset from the player.
target_offset_z0Target point Z offset from the player.
offset_pos_x0Camera position X offset.
offset_pos_y-14000Camera position Y offset.
offset_pos_z0Camera position Z offset.
offset_angle_x0Camera angle X offset.
offset_angle_y0Camera angle Y offset.
offset_angle_z0Camera angle Z offset.
camera_distance250Default follow camera distance.
camera_min_distance120Minimum allowed camera distance.
camera_right_pos5000Camera right-side position limit.
camera_left_pos5000Camera left-side position limit.
camera_combat_angle_offset_x134Combat camera X angle offset.
camera_combat_angle_offset_y245Combat camera Y angle offset.
camera_combat_angle_offset_z0Combat camera Z angle offset.
camera_combat_distance260Camera distance in combat mode.
camera_hang_angle_offset_x256Hanging camera X angle offset.
camera_hang_angle_offset_y0Hanging camera Y angle offset.
camera_hang_angle_offset_z0Hanging camera Z angle offset.
camera_hang_distance250Camera distance in hanging mode.
camera_rope_max_vel18000Maximum camera velocity when following a rope.
camera_rope_angle_offset_x96Rope camera X angle offset.
camera_rope_angle_offset_y128Rope camera Y angle offset.
camera_rope_angle_offset_z0Rope camera Z angle offset.
camera_rope_distance300Camera distance in rope mode.
camera_rope_above_angle_offset_x96Rope-above camera X angle offset.
camera_rope_above_angle_offset_y128Rope-above camera Y angle offset.
camera_rope_above_angle_offset_z0Rope-above camera Z angle offset.
camera_rope_above_distance300Camera distance in rope-above mode.
camera_rope_above_aim_x-512Rope-above camera aim X offset.
camera_rope_above_aim_y0Rope-above camera aim Y offset.
camera_rope_above_aim_z0Rope-above camera aim Z offset.
camera_rope_below_angle_offset_x96Rope-below camera X angle offset.
camera_rope_below_angle_offset_y128Rope-below camera Y angle offset.
camera_rope_below_angle_offset_z0Rope-below camera Z angle offset.
camera_rope_below_distance300Camera distance in rope-below mode.
camera_rope_below_aim_x512Rope-below camera aim X offset.
camera_rope_below_aim_y0Rope-below camera aim Y offset.
camera_rope_below_aim_z0Rope-below camera aim Z offset.
camera_debug_angle_offset_x128Debug camera X angle offset.
camera_debug_angle_offset_y256Debug camera Y angle offset.
camera_debug_angle_offset_z0Debug camera Z angle offset.
camera_debug_distance300Camera distance in debug mode.
max_x_angle1692Maximum camera X angle (engine angle units).
min_x_angle400Minimum camera X angle (engine angle units).
max_vel8000Maximum camera velocity.
max_y_vel5000Maximum camera Y-axis velocity.
max_acc5000Maximum camera acceleration.
player_control_x_inc70Player-controlled camera X increment per frame.
player_control_y_inc-70Player-controlled camera Y increment per frame.
glide_x0.9Camera position X glide (lag) factor.
glide_y0.2Camera position Y glide (lag) factor.
glide_z0.9Camera position Z glide (lag) factor.
glide_angle_x0.7Camera angle X glide (lag) factor.
glide_angle_y0.1Camera angle Y glide (lag) factor.
glide_angle_z0.7Camera angle Z glide (lag) factor.
cam_prox_up70.0Camera proximity upward adjustment distance.

[dialog]

Dialog menu layout, line limits, and speech settings.

KeyDefault ValueDescription
menu_start_x35Dialog menu X start position in screen pixels.
menu_start_y25Dialog menu Y start position in screen pixels.
dialog_max_menu_items20Maximum number of items in a dialog menu.
dialog_max_dialog_lines20Maximum number of lines in a dialog text block.
dialog_max_text_width500Maximum width of dialog text in pixels.
dialog_print_text1Display dialog as on-screen text. 1 = on.
dialog_use_speech1Play voiced speech audio during dialog. 1 = on.
dialog_max_distance1600Maximum distance at which dialog audio is played.
menu_traverse_delay4Frames 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.

KeyDefault ValueDescription
resolution12Glide renderer resolution index.
text_scale1, 1Text scaling factors (X, Y) for the Glide renderer.
anim_text_scale1, 1Animated text scaling factors (X, Y) for the Glide renderer.
font_sel255,255Font selection color values (foreground, background) for selected menu items.
font_norm125,255Font color values (foreground, background) for normal menu items.
font_used125,128Font 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 typeCountIndex rangePurpose
[misc]1Global combat parameters
[attackNN]8900–88Attack move definitions
[defendNN]500–04Defense move definitions
[tauntNN]37900–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:

IDName
1anim_defend_low
2anim_defend_right
3anim_defend_left
4anim_defend_high
5anim_attack_1
6anim_attack_2
7anim_attack_3
8anim_attack_thrust
9anim_attack_lunge
10anim_attack_1_end
11anim_attack_2_end
12anim_fight_disarm
13anim_fight_low
14anim_fight_jump_start
15anim_fight_jump
16anim_fight_fall
17anim_fight_land
18anim_fight_fall_attack
19anim_fight_land_attack
20anim_fight_hurt_1
21anim_fight_hurt_2
22anim_death_fight_stab
23anim_death_fight_hard
24anim_sheath_sword
25anim_explore_hurt_1
26anim_explore_hurt_2
27anim_death_explore

[misc] Section

Global parameters for the combat system.

KeyValue (shipped)Description
sword_clank_157Sound effect ID for sword clash, variant 1
sword_clank_258Sound effect ID for sword clash, variant 2
non_engaged_dist100Distance threshold for non-engaged state
in_face_dist100Distance threshold for in-face proximity
non_engaged_spacing500Spacing between non-engaged combatants, in engine angles
in_combat_threshold200Distance threshold to enter combat state, in units × 256
player_can_die1Whether the player can be killed (1 = yes)
cyrus_defend_interval2Minimum frames between Cyrus defends
node_timer120Timer value for combat node transitions
max_taunts600Maximum 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:

ValueName
0melee
1missile
2combo
3stab
4finishing

Elevation Values

The elevation field controls the vertical targeting zone:

ValueMeaning
0regular (default when omitted)
1above
2below

Field Schema

Standard attack (type 0, 1, 3, 4):

FieldTypeDescription
typeintegerAttack category (see enum above)
arcintegerHorizontal hit arc width, in engine angle units
arc_centerintegerHorizontal offset of arc center from forward direction; omitted when centered (0)
elevationintegerVertical targeting zone (see enum above); omitted when regular (0)
min_rangeintegerMinimum distance to target for hit to register
max_rangeintegerMaximum distance to target for hit to register
first_collide_frameintegerAnimation frame on which collision detection begins
collide_durationintegerNumber of frames collision detection remains active
damageintegerHit point damage dealt on successful hit
forceintegerKnockback force applied to target
defenseintegerDefense animation group ID triggered on the target
first_defend_frameintegerAnimation frame on which the attacker can be defended against; omitted on some attacks
defend_durationintegerNumber of frames the attacker is vulnerable to defense; omitted on some attacks
animationintegerAnimation group ID (see animation group table above)
col_vertexintegerModel vertex index used as the collision sphere origin; present only on large creature attacks
col_sphere_sizeintegerRadius 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.

FieldTypeDescription
typeintegerAlways 2 for combos
num_attacksintegerNumber of sub-attacks in the combo (2 or 3)
attack00integerIndex of the first sub-attack section
attack01integerIndex of the second sub-attack section
attack02integerIndex 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:

CombatantAttack indices
Cyrus (early)00–14
Skeleton15–17
Golem18–22
Zombie23–25
Serpent26
Troll27–29
Guard (low attack)30
Goblin31–34
Dragon35–37
Richton38–47
Dram48–52
Ogre53
Pirate (initial, weaker)54–64
Cyrus (later)65–74
Vermai75–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

FieldTypeDescription
arcintegerHorizontal arc width covered by the defense; omitted on defend00
min_rangeintegerMinimum distance at which the defense is effective
max_rangeintegerMaximum distance at which the defense is effective
first_collide_frameintegerAnimation frame on which the defense window opens
collide_durationintegerNumber of frames the defense window remains active
animationintegerAnimation 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:

ValueTrigger
0begin combat
1attack
2hurt by opponent
3hit opponent
4defend
5misc, during move
6death
7after kill opponent
8want to switch out
9want to switch in
10opponent is unarmed (male)
11opponent is unarmed (female)

Field Schema

FieldTypeDescription
typeintegerTrigger condition (see enum above)
rtx_labelstringLabel key into ENGLISH.RTX for the voice line; may be a 4-character code or a &NNN numeric reference
animationintegerAnimation 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 rangeCombatant
0–49Cyrus
50–149Guards
150–154Skeletons
155–159Zombies
160–179Tavern Thug 1
180–199Tavern Thug 2
200–219Tavern Thug 3 (Dagoo)
220–228Brennan
229–234Golem
235–239Serpent / Vermai
240–249Troll
250–259Goblin
260–279Richton
280–299Dram
300–319Pirate 1
320–339Pirate 2
340–349Ngasta
350–359Urik
360–369Zombie
370–389Vander
390–409Island Thug 1
410–429Island Thug 2
430–449Island Thug 3
450–469Island Thug 4
470–489Island Thug 5
490–509Island Thug 6
510–529Pirate Hideout 1
530–549Pirate Hideout 2
550–569Pirate Hideout 3
570–589Pirate Hideout 4
580–587Dragon
591–599Jail 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:

  1. A header block with global inventory settings.
  2. Per-item field blocks indexed by item ID (e.g. name[0], type[1]).

[items] Header Fields

FieldValueDescription
bitmap_fileSYSTEM\PICKUPS.GXAGXA texture atlas for unselected inventory icons.
bitmap_selected_fileSYSTEM\PICKUPSS.GXAGXA texture atlas for selected inventory icons.
start_item_list1, 2, 4, 18Item IDs the player starts the game with.
start_item_select1Item ID selected by default at game start.
additional_length8Extra inventory display length (slots).
weapon_sphere_size10Collision sphere radius for weapon hit detection.
default_weapon_item1Item ID used as the default weapon (the sabre).
torch_time60Torch 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

FieldDescription
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

FieldDescription
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

FieldDescription
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

FieldDescription
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

FieldDescription
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.

IDCommentType
0compass0 (general)
1sabre1 (weapon)
2gold0 (general)
3Potion of ironskin0 (general)
4health potion0 (general)
5ring of invisibility0 (general)
6Voa’s ring0 (general)
7guard sword1 (weapon)
8rusty key0 (general)
9gold Key0 (general)
10silver key0 (general)
11amulet0 (general)
12soul gem0 (general)
13soul sword1 (weapon)
14crow bar0 (general)
15rune 10 (general)
16rune 20 (general)
17rune 30 (general)
18letter0 (general)
19orc’s blood0 (general)
20orc’s blood w/stuff0 (general)
21spider’s milk0 (general)
22spider’s milk w/stuff0 (general)
23ectoplasm0 (general)
24ectoplasm w/stuff0 (general)
25hist sap0 (general)
26hist sap w/ stuff0 (general)
27book of dw lore0 (general)
28dwarven gear0 (general)
29vial0 (general)
30vial w/elixir0 (general)
31iron weight0 (general)
32bucket0 (general)
33bucket w/water0 (general)
34fist rune0 (general)
35elven book (copy) DO NOT USE0 (general)
36elven book0 (general)
37redguard book0 (general)
38flora of hammerfell book0 (general)
39reference map0 (general)
40pouch0 (general)
41trithick’s map piece0 (general)
42silver ship0 (general)
43shovel0 (general)
44aloe0 (general)
45torch3 (hand object)
46monocle0 (general)
47red flag0 (general)
48silver locket that talks of lakene0 (general)
49redguard insignia0 (general)
50joto’s piece0 (general)
51flask of lillandril0 (general)
52talisman of hunding0 (general)
53izara’s journal open0 (general)
54canah feather0 (general)
55kithral’s journal0 (general)
56starsign’s book0 (general)
57izara’s journal closed0 (general)
58starstone0 (general)
59krisandra’s key0 (general)
60key to iszara’s lodge0 (general)
61necro book - view only0 (general)
62bar mug ..for thugs hands, etc0 (general)
63mariah’s watering can0 (general)
64glass bottle0 (general)
65glass bottle with water0 (general)
66glass bottle w/aloe and water0 (general)
67Strength Potion0 (general)
68bandage0 (general)
69bloody bandage0 (general)
70skeleton sword1 (weapon)
71keep out poster…mage’s guild0 (general)
72keep out poster…dwarven ruins0 (general)
73tobias mar mug0 (general)
74Bone Key0 (general)
75flaming sabre1 (weapon)
76goblin sword1 (weapon)
77ogre’s axe1 (weapon)
78dram’s sword1 (weapon)
79palace key0 (general)
80dram’s bow0 (general)
81dram’s arrow0 (general)
82silver locket that just says silver locker0 (general)
83island map0 (general)
84wanted poster0 (general)
85palace diagram0 (general)
86last0 (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 3 in the file, which the comment block does not define. The comment block describes type 2 as the hand-object type; type 3 appears 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 distinct game_object_file paths 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 add command, 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

KeyExample valueDescription
start_world0World index loaded on new game start.
start_marker0Spawn marker index within the start world.
test_map_delay1Delay (seconds) between worlds during test-map cycling.
test_map_order0,1,2,1,...,-2Comma-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

KeyTypeDescription
world_map[N]pathRGM scene file for this world. Present on every entry.
world_world[N]pathWLD terrain file. Only present for outdoor worlds with a heightmap terrain mesh.
world_redbook[N]integerCD audio track number for background music.
world_palette[N]pathCOL palette file.
world_shade[N]integerShade table index.
world_haze[N]integerHaze/distance-fog table index.
world_background[N]integerBackground fill color index. 0 = black, 2 = sky color, other values are palette indices.
world_compass[N]integerCompass heading offset (fixed-point). Omitted for most worlds; present where the player can see a compass.
world_flash_filename[N]pathGXA file used for screen-flash transitions when entering or leaving this world.

Lighting

KeyTypeDescription
world_sun[N]x, y, z, intensitySun direction vector and intensity. The three components are large fixed-point integers defining the light direction; intensity is a scalar.
world_ambient[N]integerGlobal ambient light level.
world_ambientfx[N]integerAmbient effect intensity (affects dynamic lighting).
world_ambientrgb[N]r, g, bAmbient light color as three 0..255 components.
world_sunangle[N]integerSun angle (fixed-point). Controls the horizontal rotation of the sun direction.
world_sunskew[N]integerSun skew (fixed-point). Controls the vertical tilt of the sun direction.
world_sunrgb[N]r, g, b, scaleSun color as three 0..255 components plus a scale factor.
world_fogrgb[N]r, g, bDistance fog color as three 0..255 components.

Sky

KeyTypeDescription
world_sky[N]pathGXA skybox texture. Only present for outdoor worlds.
world_skyfx[N]filenameBSI sky texture (scrolling sky layer).
world_skyscale[N]integerScale factor for the sky layer.
world_skylevel[N]integerVertical offset of the sky layer (negative = below horizon).
world_skyspeed[N]integerScroll speed of the sky layer.
world_sky_xrotate[N]integerSky rotation rate around the X axis.
world_sky_yrotate[N]integerSky rotation rate around the Y axis.

Sun Disc

KeyTypeDescription
world_sunimg[N]filenameBSI texture for the rendered sun disc.
world_sunimgrgb[N]r, g, bTint color for the sun disc texture.
world_sunscale[N]integerSize scale of the sun disc.

Water

KeyTypeDescription
world_wave[N]a, b, cWave 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

KeyTypeDescription
world_node_mapN[W]pathPVO .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.

KeyTypeDescription
world_rain_delay[N]integerFrames between rain drop spawns.
world_rain_drops[N]integerMaximum simultaneous rain drops.
world_rain_start[N]integerVertical start height for rain drops (negative = above ground).
world_rain_end[N]integerVertical end height where drops are removed.
world_rain_sphereN[W]x, y, z, rSphere 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.

IDRGM FileWLD FilePaletteLocation
0MAPS\start.rgmMAPS\hideout.WLD3DART\sunset.COLStarting hideout (exterior, sunset)
1MAPS\ISLAND.rgmMAPS\ISLAND.WLD3DART\island.COLStros M’Kai island (daytime)
2MAPS\catacomb.rgm3DART\catacomb.COLCatacombs
3MAPS\PALACE.rgm3DART\palace00.COLPalace interior
4MAPS\caverns.rgm3DART\REDcave.COLCaverns
5MAPS\observe.rgm3DART\observat.COLObservatory
6MAPS\necrisle.rgmMAPS\necrisle.WLD3DART\necro.COLNecromancer’s Isle (rain, rotating sky)
7MAPS\necrtowr.rgm3DART\necro.COLNecromancer’s Tower interior
8MAPS\drint.rgm3DART\observat.COLDwemer ruin interior
11MAPS\jailint.rgm3DART\necro.COLJail interior
12MAPS\temple.rgm3DART\island.COLTemple
13MAPS\mguild.rgm3DART\redcave.COLMages Guild
14MAPS\vile.rgm3DART\island.COLVile Lair
15MAPS\tavern.rgm3DART\island.COLTavern
17MAPS\hideint.rgmMAPS\hideout.WLD3DART\hideout.COLHideout interior (shares hideout WLD)
18maps\silver1.rgm3DART\island.COLSilversmith (area 1)
19maps\silver2.rgm3DART\island.COLSilversmith (area 2)
20maps\belltowr.rgm3DART\island.COLBell Tower
21maps\harbtowr.rgm3DART\island.COLHarbor Tower
22maps\gerricks.rgm3DART\island.COLGerrick’s
23maps\cartogr.rgm3DART\island.COLCartographer’s
24maps\smden.rgm3DART\island.COLSmuggler’s Den
25maps\rollos.rgm3DART\island.COLRollo’s
26maps\jffers.rgm3DART\island.COLJeffers’
27MAPS\island.rgmMAPS\ISLand.WLD3DART\nightsky.COLIsland (night variant)
28MAPS\ISLAND.rgmMAPS\ISLAND.WLD3DART\sunset.COLIsland (sunset variant)
29maps\brennans.rgm3DART\island.COLBrennan’s Farm
30MAPS\extpalac.rgmMAPS\ISLAND.WLD3DART\sunset.COLPalace exterior (sunset, uses island WLD)
99maps\brennans.rgm3DART\island.COLBrennan’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

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:

KeyDefaultDescription
preload_sprites1Preload menu sprite textures at startup.
hi_res_menu1Use high-resolution menu rendering.
ignore_autosave0Skip auto-save slot handling.
force_movies0Force 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

PageMenu Screen
0Main Menu (Save, Load, New, Options, Movies, Continue, Quit)
1Save Game
2Load Game
3Movies (cinematic list with Smacker playback)
4Options (Sound Volume, Music Volume, Text toggle, Speech toggle)
5Confirm New Game
6Confirm Overwrite Save
7Key Binding Options

Texture fields

Each page uses up to 2 background textures sourced from TEXBSI archives:

KeyDescription
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.):

KeyDescription
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:

KeyDescription
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:

RangeMeaning
-1Continue / resume game.
-2Quit game.
1–10Main menu navigation (Save=1, Load=2, Movies=3, Options=4, New=10).
100–102Save game page actions.
200–202Load game page actions.
300–302Movie page actions.
400–411Options page actions (volume sliders, toggles).
500–501Confirm new game actions.
600–601Confirm overwrite actions.
700–724Key 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).

KeyDefaultDescription
next_key52 (period)Next item in inventory.
prev_key51 (comma)Previous item in inventory.
inventory_key23 (I)Open inventory screen.
log_key38 (L)Open logbook.
quick_sword_key16 (Q)Quick-draw sword.
quick_health_key35 (H)Quick-use health potion.
map_key50 (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]138Move forward (joystick).
key_down[1]139Move backward (joystick).
key_left[1]136Turn left (joystick).
key_right[1]137Turn right (joystick).
key_a[1]135Action / Use (joystick button 4).
key_b[1]133Jump (joystick button 2).
key_c[1]134View / Look (joystick button 3).
key_d[1]132Walk toggle (joystick button 1).
user_key_up0User-remapped forward key.
user_key_down0User-remapped backward key.
user_key_left0User-remapped left key.
user_key_right0User-remapped right key.
user_key_a0User-remapped action key.
user_key_b0User-remapped jump key.
user_key_c0User-remapped view key.
user_key_d0User-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]

KeyDefaultDescription
waiting_messageHIT KEYText 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):

RangeKeys
0empty (no key)
1escape
2–111 through 0
12–13minus, plus
14backspace
15tab
16–25q through p
26–27l bracket, r bracket
28enter
29ctrl
30–38a through l
39–41:, ", tildy
42l shift
43\
44–50z through m
51–53comma, period, slash
54r shift
55prt scr
56alt
57space
58caps lock
59–68f1 through f10
69–70num lock, scrl lock
71–73home, kbd up, page up
74num minus
75kbd left
77kbd right
78num plus
79–81end, kbd down, page down
82–83insert, delete
87–88f11, f12
76, 84–86, 89–127unkn NN (unused scancodes)

Scancodes 128–139 are mouse/joystick inputs:

ScancodeName
128left mouse
129right mouse
130mouse x (axis)
131mouse y (axis)
132–135button 1 through button 4 (joystick)
136–137joy left, joy right
138–139joy 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:

KeyDefaultDescription
use_registryfalseEnable the Windows registry path lookup subsystem.
use_32bittrueUse 32-bit file I/O (DOS/4GW extended file access).
archive_file_firstfalseCheck .ZAP archives before loose files.
local_pathtrueUse local (relative) file paths.
ignore_registry_errorsfalseSuppress errors from registry path lookup failures.
registry_logfalseEnable logging of registry system operations.
advanced_existtrueUse advanced file-existence checking.

External References

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:

  1. [surfaces] section: texture-set/index to surface-type assignments
  2. Sound remap sections: [unknown], [water], [deepwater], [scapewater], [scapedeepwater], [lava], [sand], [wood], [tile], [scape], [rock], [gloop]

Surface Assignment Records ([surfaces])

  • set, index, type
  • set, all, type

Where:

  • set: texture set id, documented range 0..511
  • index: texture index inside the set
  • type: 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/scapewater and deepwater/scapedeepwater sections are expected.
  • Empty sections are valid (for example [unknown], [lava], [scape] in shipped sample).
  • The file is parser-tolerant regarding token case (WOOD and wood both 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 typeBehavior
None (default)Tile rendered as fully opaque; no transition blending.
FullTile is always fully blended (alpha = 256). Used for index 0 (default/empty tile).
GradientAlpha ramps linearly across the tile based on pixel position. Used for indices 6 and 52.
Hard edgeSharp alpha cutoff at a fixed distance into the tile. Used for index 31.
Custom patternPer-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..63 use 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

IndexValueDescription
00No fog at close range.
331Fog begins at index 33.
34–482–255Rapid ramp from barely visible to fully opaque.
63End 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.

TopicDescriptionDocs
Cheat System13 XOR-obfuscated cheat codes built into the enginecheats.md
Item AttachmentVertex-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 ScriptingSOUP386 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 RendererTwo-layer sky system: static GXA skybox + scrolling BSI texture, sun disc billboard, per-world configuration, and runtime console commands.sky.md
Water WavesPer-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

#NameEffect
0oracleLists all 13 cheats and their current on/off state in the console
1nodemarkerDebug 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.
2moonrakerFor the player actor, skips several combat/movement processing calls and flips a player state bit used in combat flow
3taskToggles 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).
4animationToggles 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).
5magiccarpetFly mode — disables gravity and falling, allowing the player to walk horizontally through the air at current altitude
6savecheatsWrites all cheat states to REDGUARD.CHT so they persist across sessions
7drevilPrevents player death — suppresses death handling when health reaches zero and bypasses some player damage application paths
8drnoDisables part of the non-player actor update path, reducing/skipping some NPC behavior processing
9goldfingerGod 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.
10neversaydieDead code. The flag can be toggled and persisted in REDGUARD.CHT, but it does not change gameplay behavior.
11oddjobDead code. The flag can be toggled and persisted in REDGUARD.CHT, but it does not change gameplay behavior.
12yeahbabyVertical 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 add console 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.

OpcodeUESP NameBit LayoutParametersPlayback Behavior
0ShowFrame10 + 10handle_index, vertex_indexAdvance frame; set attachment handle + vertex. The only opcode that drives item positioning.
1EndAnimation20-bit(unused, always 0)Set animation handle to −1; stop associated sound; call playback recursively for next animation.
2GoToPrevious20-bittarget_frameJump backward to an earlier frame in this group. Used for walk/run loops.
3GoToFuture20-bittarget_frameJump forward to a later frame. Conditional — checks animation state flags and pending transitions.
4PlaySound10 + 10sound_param, volume_shiftPlay 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.
5BreakPoint20-bit(unused, always 0)Set vertex-enable flag at anim control +0x0B. Often the target of GoToFuture jumps.
6SetRotationXYZ6 + 6 + 6rot_x, rot_y, rot_zSet 3-axis rotation (each param × 256). Actor orientation override.
7SetRotationAxis2 + 18axis (0=X, 1=Y, 2=Z), valueSet rotation on a single axis. Finer precision than opcode 6.
8SetPositionXYZ6 + 6 + 6pos_x, pos_y, pos_zSet 3-axis position offset (each param × 8).
9SetPositionAxis2 + 18axis (0=X, 1=Y, 2=Z), valueSet position offset on a single axis.
10ChangeAnimGroup10 + 10target_group, target_frameJump to a different animation group and frame. Same bit layout as opcode 0, but writes to anim control fields, not attachment.
11Rumble/SFX20-biteffect_bitmask5-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).
12DelayCounter20-bitcounter_valueSet frame delay counter; pause animation until counter expires.
13ConditionalDelay20-bitcounter_valueSet conditional delay; direction-dependent counter.
14LoopControl20-bittarget_frameDecrement loop counter; jump to target frame if counter > 0.
15Transition6 + 7 + 7trigger_mask, start_frame, target_groupMid-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)

OpcodeTotal CmdsMapsActorsNotes
0 (ShowFrame)93,75027280Every animated actor
1 (EndAnimation)8,51327280Every animated actor
2 (GoToPrevious)3,25827275Walk/run loops
3 (GoToFuture)7,12127276Conditional jumps
4 (PlaySound)6,9132798Combat actors
5 (BreakPoint)13,16727105Combat actors
11 (ActorSound)164272Cyrus + Golem only
15 (Transition)4612722Guards only
6–10, 12–14000Dead code in shipped game

Bit Layout Summary

LayoutOpcodesExtraction
10 + 100, 4, 10(packed >> 4) & 0x3FF, (packed >> 14) & 0x3FF (both sign-extended)
6 + 6 + 66, 86-bit signed at positions 4, 10, 16
2 + 187, 92-bit selector at 4, 18-bit signed at 6
6 + 7 + 7156-bit at 4, 7-bit signed at 10, 7-bit signed at 17
20-bit1, 2, 3, 5, 11, 12, 13, 1420-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:

  1. Entry point — resolves animation handle and reads the tracked vertex.
  2. 3D object manager — looks up handle in a table. For “virtual” animations (type 0x02), follows a parent handle chain recursively.
  3. Frame builder — reads (x, y, z) float position of the given vertex from the current animation frame. For frame 0: reads base_vertices[vertex_index × 12]. For animated frames: applies delta-compressed offsets (i8×3 or i16×3) from the base frame. Returns float[3].
  4. 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 OffsetSourceDescription
+0x10Item typeType discriminator: 1 = weapon/hand-object, 3 = general item
+0x4aROB handle3D model handle for the item
+0x77ROB handle (type 1 only)Hand model — the 3D model shown when weapon is drawn
+0x7bROB handle (type 1 only)Hilt model — the 3D model shown when weapon is sheathed
+0x7fROB segment dataLength/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:

  1. Read two vertex positions from the actor’s current animation frame (via the tracked vertex index).
  2. Add world position offsets.
  3. Rotate by the actor’s orientation matrix (actor struct +0x51).
  4. Compute heading and pitch from the direction between the two points.
  5. Build item rotation from the computed direction + actor roll.
  6. 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)ConditionAction
0x14 (drawing sword)Frame < draw thresholdPosition hilt model at scabbard
0x14 (drawing sword)Frame ≥ draw thresholdPosition hand model at hand; set drawn flag
0x15 (sheathing sword)Frame < sheath thresholdPosition hand model at hand
0x15 (sheathing sword)Frame ≥ sheath thresholdPosition 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:

FunctionPurpose
handitemAssign a held item to an actor
displayhandmodelShow the hand (drawn) model
displayhanditemShow the item in the hand
displayhiltmodelShow the hilt (sheathed) model
displayhiltitemShow the item at the hilt position
drawswordTrigger draw animation/state transition
sheathswordTrigger sheath animation/state transition
isholdingweaponQuery: is actor holding a weapon?
iscarryingweaponQuery: is actor carrying (has) a weapon?
isdrawingswordQuery: is actor in draw animation?
issheathingswordQuery: 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

SOUP Scripting

Bytecode scripting engine used by the Redguard runtime for actor behavior, dialogue, puzzle logic, and scene control.

Script Data Sources

SourceContainerNotes
Map script bytecodemaps/*.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 filessoup386/*.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 tableSOUP386/SOUP386.DEFText definition file loaded at runtime. Defines function/task names, flags, equates, and attributes used by script execution. See SOUP386.DEF.
EXE-embedded interpreterRuntime binaryThe 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 (RGM and .AI) and executed by the VM.
  • Function names and metadata come from SOUP386.DEF, while actor-local script bytes come from RASC/.AI content.

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:

ThreadStart AddressPurpose
Thread 0RAHD.script_pcMain execution (dialogue, activation, behavior)
Thread 1Offset 0x00Interrupt 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.

ModeContextEffect
MAINTop-level statementPerforms assignment (writes to flag/variable/property)
LHSLeft side of if comparisonReads value; consumes 1 extra operator byte
RHSRight side of if comparisonReads value; consumes 1 extra operator byte
PARAMETERArgument to a task/function callReads value; consumes mode-specific padding bytes
FORMULARight side of an assignment expressionReads 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.

OpcodeNameEncoding (after opcode byte)Description
0x00Tasku16 func_id, u8 param_count, params…Call task on self
0x01Multitasksame as 0x00Async task on self (non-blocking)
0x02Functionsame as 0x00Call function on self (returns value)
0x03Ifcondition chain, u32 end_offset, blockConditional branch
0x04Gotou32 targetUnconditional jump
0x05Endu32 targetHalt — terminates execution
0x06Flagu16 flag_id, mode-dependentGlobal flag read/write
0x07Numerici32 valueImmediate 32-bit integer
0x0ALocalVaru8 var_index, mode-dependentLocal variable read/write
0x0FObjDot++object encoding, u16 ref_idIncrement object property
0x10ObjDot–object encoding, u16 ref_idDecrement object property
0x11Gosubu32 targetSubroutine call (pushes return address)
0x12Return(none)Return from subroutine
0x13Endint(none)End secondary thread
0x14ObjectDotobject encoding, u16 ref_id, mode-dependentObject property read/write
0x15Stringu32 string_indexString literal from RASB/RAST table
0x16NumericAlti32 valueSame as 0x07; used for global flag function arguments
0x17Anchoru8 anchor_valueAnchor assignment
0x19ObjDotTaskobject encoding, task encodingTask call on named object
0x1AObjDotFuncobject encoding, task encodingFunction call on named object
0x1BTaskPauseu32 labelPause until task completes
0x1EScriptRVu8 expected, blockBranch on script return value

Bytes with no known opcode: 0x08, 0x09, 0x0B0x0E, 0x18, 0x1C0x1D, 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)

ModeBytes after u16 flag_id
MAINFormula (assignment to flag)
LHS, RHSu8 operator byte
PARAMETERu16 padding (always 0x0000)
FORMULA(none)

LocalVar (0x0A)

ModeBytes after u8 var_index
MAINFormula (assignment to variable)
LHS, RHSu8 operator byte
PARAMETER3 padding bytes (0x000000)
FORMULA(none)

ObjectDot (0x14)

ModeBytes after object encoding + u16 ref_id
MAINFormula (assignment to property)
LHSu8 operator byte
Other(none)

Object Name Encoding

Used by opcodes 0x0F, 0x10, 0x14, 0x19, 0x1A. A leading byte selects the object target:

ByteAdditionalObject
0x00u8 paddingMe — the script’s own actor
0x01u8 paddingPlayer — Cyrus
0x02u8 paddingCamera
0x04u8 string indexNamed object from per-script string table (RASB/RAST)
0x0Au8 var indexObject 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.

ByteOperatorArityEngine instruction
0; (end)(return result)
1+binaryadd eax, ebx
2-binarysub eax, ebx
3*binaryimul ebx (signed)
4/binarydiv ebx (unsigned, zero-check via CPU trap)
5<<binaryshl eax, cl
6>>binarysar eax, cl (arithmetic/signed)
7&binaryand eax, ebx
8|binaryor eax, ebx
9^binaryxor 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 to imul (multiply) and byte 4 maps to div (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.

ByteOperator
0= (equal)
1!=
2<
3>
4<=
5>=

Conjunctions

Multiple comparisons in a single if are chained with conjunction bytes:

ByteMeaning
0End of condition list
1and
2or

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:

TypeBehaviorScript syntax
TaskBlocking — script waits for completionFunctionName(...)
MultitaskAsynchronous — script continues immediately@FunctionName(...)
FunctionImmediate — returns a valueFunctionName(...) (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:

FunctionsTypeEncoding
ACTIVATE, AddLog, AmbientRtx, menuAddItem, RTX, rtxAnim, RTXp, RTXpAnim, TorchActivateDialogue key4-byte ASCII string (RTX lookup key)
LoadWorldMap IDi32 map identifier
ActiveItem, AddItem, DropItem, HandItem, HaveItem, SelectItem, ShowItem, ShowItemNoRtxItem IDi32 item identifier
All othersIntegeri32 signed integer

Function Table

SOUP386.DEF declares 367 callable functions (indices 1–367; index 0 is the synthetic NullFunction). Functions span the following categories:

CategoryExamplesCount
MovementMove, WalkForward, MoveToLocation, WanderToLocation~16
Rotation / FacingRotate, RotateByAxis, FacePlayer, FaceAngle, FaceObject~10
CamerashowObj, showPlayer, lookCyrus, showCyrusPan~13
Dialogue / RTXRTX, rtxAnim, menuNew, menuProc, menuAddItem, menuSelection~8
AnimationPlayAnimation, PushAnimation, WaitAnimFrame, SetAction~10
CombatbeginCombat, endCombat, isDead, adjustHealth, shoot, shootPlayer~16
Lighting / FXLight, LightRadius, LightFlicker, FxPhase, FxFlickerOnOff~14
Object controlEnableObject, DisableObject, HideMe, ShowMe, KillMe~12
InventoryAddItem, DropItem, HaveItem, HandItem, ShowItem, SelectItem~9
SoundSound, AmbientSound, EndSound, StopAllSounds~5
Weapon / Attachmenthanditem, displayhandmodel, drawsword, sheathsword~11
Spatial queriesInRectangle, InCircle, DistanceFromStart, AtPos~5
Flat (billboards)Flat, FlatSetTexture, FlatAnimate, FlatOff~7
AISetAiType, SetAiMode, Guard, Animal~5
Static objectsLoadStatic, UnLoadStatic, PointAt~3
Global flagsSetGlobalFlag, TestGlobalFlag, ResetGlobalFlag3
AttributesSetAttribute, GetAttribute, SetMyAttr, GetMyAttr4
DebugPrintParms, 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:

TypeSemantics
BOOLBinary state (0 or 1)
NUMBERInteger counter or timer
FLIPFLOPToggled 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

  • RAHD stores per-actor script pointers, variable counts, and function-table indices.
  • RASC stores compiled script bytecode as a contiguous blob.
  • RAST/RASB store script string literal data and offset tables.
  • RAVA stores initial local variable values (i32 array).
  • RAHK stores hook entry offsets rebased against script base addresses.
  • RAAT stores per-actor attribute tables (256 bytes each), addressed by names from SOUP386.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 Anchor opcode (0x17) has an encoding discrepancy between the disassembler (reads 0x17 then 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 RGM scripts; usage may be limited to .AI flows that were available during development but not included in the final release.

External References

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:

  1. Background fill — a solid color behind everything
  2. GXA skybox — a static panoramic texture
  3. 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

PhaseTriggerAction
LoadWorld openEngine reads per-world sky keys from WORLD.INI, then calls the sky system opener with the BSI texture filename
RenderEvery frame in the main loopSky layers are drawn before terrain and scene geometry
CloseSession closeSky textures are released and the system is shut down

Background Fill

The world_background[N] key sets what is drawn behind the sky layers:

ValueBehavior
0Black
2Sky color (derived from the palette)
OtherPalette 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:

KeyDescriptionDefault
world_skyscale[N]Size/scale of the scrolling texture0x3200 (12800) if 0 or omitted
world_skylevel[N]Vertical offset (negative = below horizon)0xFFFFF254 (−3500) if 0 or omitted
world_skyspeed[N]Scroll speed0
world_sky_xrotate[N]Rotation rate around the X axis0
world_sky_yrotate[N]Rotation rate around the Y axis0

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:

KeyDefaultDescription
sky_disable0Disable sky rendering entirely. 0 = enabled.
sky_move1Enable sky scrolling. 0 = frozen.
sky_xrotate3Global X-axis rotation speed
sky_yrotate40Global 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:

KeyDescription
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:

CommandAliasDescription
fxskyscale <value>skyscSet the BSI layer scale
fxskylevel <value>skylSet the BSI layer vertical offset
fxskyspeed <value>skyspSet 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.

WorldLocationSky Features
0Starting hideout (exterior)Sunset skybox
1Stros M’Kai island (daytime)Daytime skybox + scrolling clouds
6Necromancer’s IsleSkybox + rotating BSI layer + rain weather
27Island (night variant)Night skybox (nightsky.COL palette)
28Island (sunset variant)Sunset skybox (sunset.COL palette)
30Palace exteriorSunset 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

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:

ParameterINI positionConsole commandDescription
Amplitude1stfxwaveampVertical scale of wave displacement. Multiplies the sine-table value.
Speed2ndfxwavespeedRate of phase advance per frame. Higher values = faster ripple animation.
Spatial frequency3rdfxwavefreqControls 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:

ModeFormulaWhen used
Defaultheight[i] = -ABS(source[i])No water level specified
Water-relativeheight[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:

OffsetSizeTypeNameDescription
0x004f32xWorld X position (grid_x * 256.0)
0x044f32yHeight (from lookup table, or wave-displaced)
0x084f32zWorld Z position (grid_z * 256.0)
0x18–0x30face_normalsTwo face normals per cell (upper/lower triangle)
0x30–0x3Cvertex_normalAveraged vertex normal for lighting
0x484u32texture_indexTerrain 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:

CommandSyntaxEffect
fxwaveampfxwaveamp <value>Set wave amplitude
fxwavespeedfxwavespeed <value>Set wave speed
fxwavefreqfxwavefreq <value>Set spatial frequency

These modify the same globals as the INI parameters and take effect on the next frame.

External References