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

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