Redguard Preservation
Reference documentation for The Elder Scrolls Adventures: Redguard (1998) file formats, engine behavior, and undocumented features. The documentation covers the GOG release’s Glide renderer and notes known original-CD differences.
Reference sections
- Engine behavior: scripting, actor animation, movement, rendering, audio, menus, and hidden cheats.
- File formats: binary layouts for models, maps, textures, sound, saved state, and bundled support files.
- Configuration: INI settings for worlds, combat, items, input, rendering, and menus.
Look and feel
Reproducing the look and feel explains how terrain and model lighting, fog, sky, water, texture animation, camera projection, and on-screen presentation combine, with links to each subsystem.
Starting points
| Task | References |
|---|---|
| Load a world | RGM scenes, WLD terrain, WORLD.INI, and PVO visibility |
| Reproduce actor behavior | SOUP scripting, animation playback, player movement, and item attachment |
| Reproduce the scene renderer | Start with the Glide shading overview, then follow its references to lighting, normals, textures, palettes, fog, sky and water. |
| Reproduce menus and presentation | Book menu, HUD, subtitles, and cinematics |
| Compare the 1998 demo | Demo release |
| Inspect or convert assets | CLI usage and library API |
Related projects
- Cyrus is a Rust and Bevy rebuild of Redguard that uses this documentation and the
redguard-preservationparsers. Its asset viewer opens the formats described here in the browser. - Maiko (site) runs the original game natively on Windows and Linux by translating the original executable.
Engine lineage
Redguard runs on Bethesda’s XnGine, a DOS-based 3D engine. Battlespire used its own build of the engine, and Redguard added 3dfx Glide support.
| Game | Year | SOUP usage | Assessment |
|---|---|---|---|
| The Terminator: Future Shock | 1995 | Unverified | A community port drives map logic from an action-id handler table |
| The Terminator: SkyNET | 1996 | Unverified | A community port drives map logic from an action-id handler table |
| The Elder Scrolls II: Daggerfall | 1996 | Not established | Quests use a separately documented table-driven format |
| XCar: Experimental Racing | 1997 | Unverified | No evidence identified |
| An Elder Scrolls Legend: Battlespire | 1997 | Unverified | No script format documented; candidate for checking |
| Burnout: Championship Drag Racing | 1998 | Unverified | No evidence identified |
| The Elder Scrolls Adventures: Redguard | 1998 | Confirmed | Uses SOUP386 |
| NIRA Intense Import Drag Racing | 1999 | Unverified | No evidence identified |
Terminator and Daggerfall files share structure with Redguard’s:
| Format | Shared structure |
|---|---|
| WLD terrain | The Terminator games use the same section layout and the same 128-entry height lookup table. Daggerfall’s WOODS.WLD uses the same container: width and height at 0x04 and 0x08, an offset list at 0x90, and a 22-byte header on each offset’s block. It stores 1000×500 cells and a 1000×500 elevation map. |
| 3D models | Terminator and Daggerfall .3D files place the vertex block at 0x30 and the face array at 0x3C. In v2.6 and v2.7 files the face texture field splits into an archive number (value >> 7) and a record number (value & 0x7F) in all three games. |
| Software texture banks | Terminator TEXTURE.NNN banks use the same 20-byte directory starting at byte 26. Image width, height, data offset, frame count, and frame delay sit at the same offsets, and animated frames use the same transparent-run and literal-run row encoding. |
Terminator scenes use a cell grid with linked entity lists, not RGM sections.
Related community work:
- XnGine: game list and engine history.
- skynet-godot
wld_terrain.gd: TerminatorWLDlayout and height table. - skynet-godot
mesh_3d.gd: Terminator.3Dheader offsets and texture ids. - skynet-godot
texture_nnn.gd: TerminatorTEXTURE.NNNlayout. - skynet-godot
map_file.gd: TerminatorMAPlayout. - UESP: Daggerfall Mod:WOODS.WLD format and Daggerfall Unity
WoodsFile.cs: Daggerfall terrain file layout. - Daggerfall Unity
Arch3dFile.cs: Daggerfall.3Dheader and texture ids. - Daggerfall Unity and terminator-unity: a Daggerfall reimplementation, and an effort to extend it to the Terminator games.
Installation contents
Main Files
| File | Size | Description |
|---|---|---|
REDGUARD.EXE | 980 K | Main game executable |
RGFX.EXE | 1.9 M | Glide (3dfx) renderer executable |
DOS4GW.EXE | 260 K | DOS/4GW protected-mode extender |
3DfxSpl2.dll | 1.1 M | 3dfx Splash/Glide support library |
glide2x.dll | 1.3 M | Glide 2.x runtime |
ENGLISH.RTX | 177 M | Dialogue text + voice audio container (largest file in the game) |
REDGUARD.SWG / redguard.swx | 18,610 bytes each | SideWinder Gamepad and Freestyle Pro controller profiles |
OBJECT.SAV | 4 K | Object state persistence |
*.INI | — | Configuration: COMBAT, ITEM, KEYS, MENU, REGISTRY, surface, SYSTEM, WORLD |
*.LOG | — | Runtime log files (BITMAP, CAMERA, COMBAT, ERROR, EXIT, GENERAL, GRID, MAINLOOP, MENU, OBJECT, PATH, RAI, SAVEFILE, STARTUP, TESTMAPS) |
*.TXT (3 files) | — | BETHESDA.TXT, CREDITS.TXT, ReadMe.TXT |
Asset Directories
| Directory | Size | Files | Contents |
|---|---|---|---|
fonts/ | 604 K | 29 | 29 .FNT bitmap font files (Arial variants, HI/LO menu fonts, Redguard-styled fonts) |
fxart/ | 102 M | 755 | Glide-version 3D assets: 204 .3DC, 65 .3D, 31 .ROB, 27 .COL, 415 TEXBSI.xxx textures, FOG.INI, more model information report |
maps/ | 12 M | 45 | 27 .RGM scene files, 5 .PVO visibility octrees, 4 .WLD terrain files, 9 .TSG trigger-state files |
sound/ | 5.3 M | 49 | MAIN.SFX (all 118 sound effects), R212.WAV, Miles Sound System drivers (.mdi, .dig), OPL and AD instrument banks, PIF program settings, RST editor state, audio configs |
soup386/ | 48 K | 1 | SOUP386.DEF — script function/flag definitions for the SOUP engine |
system/ | 18 M | 69 | 65 .GXA UI graphics (menus, inventory, compass, maps, skies), gui.anm, gui.lbm, pointers.bmp, SKY_61.PCX |
SAVEGAME/ | 79 M | 594 | 17 save slots (SAVEGAME.000–016), each containing per-map .TSG trigger-state snapshots + LOGBOOK.TXT |
Asset and support-file roles
Models, textures, fonts, UI images, maps, dialogue, sound effects, movie data, and CD music supply the content of a faithful reimplementation. Hardware configuration, audio-utility launch settings, SideWinder presets, and tool reports describe the original environment. Their layouts remain part of the format references in Bundled support resources.
The cursor file system/pointers.bmp contains a GXA archive despite its
extension. fxart/fxart.txt and fxart/fxart3dc.txt are historical directory
listings, and fxart/LOGFILE.TXT records collision-sphere generation.
The support-file reference describes these
files and the Miles setup catalog sound/MSSDRVR.LST.
Save-game containers describe SAVEGAME.SAV and the
per-world .TSG snapshots. A slot’s LOGBOOK.TXT is journal content.
GOG vs original CD differences
The English CD installer contains software and Glide resources. GOG selects the Glide distribution and omits the software-specific asset directory and executable. The game data, renderer choice, configuration defaults, and host packaging are separate parts of edition compatibility.
Executables and installation layout
| Resource | CD installer payload | GOG distribution |
|---|---|---|
| Software engine | RG.EXE beside the CD installer; uses 3dart/ | Omitted |
| Glide engine | RGFX.EXE beside the CD installer; uses fxart/ | Redguard/RGFX.EXE |
| Windows launcher | REDGUARD.EXE in the installer cabinet | Redguard/REDGUARD.EXE |
| Installed resources | DATA1.CAB, including both renderer asset sets | Expanded Redguard/ directory with Glide assets |
| Cinematics | .SMK files in the CD data directory | Data track of game.gog, described by game.ins |
| Music | Red Book audio tracks on the CD | Audio tracks in the same game.gog image |
| Host runtime | DOS/4GW and the selected renderer’s hardware support | DOSBox configuration and Glide support libraries |
| Controllers | PC gameport; SideWinder profiles for Microsoft’s SideWinder software | DOSBox gameport emulation, disabled in the 2.0.0.5 installer’s launch configuration (joysticktype=none); see KEYS.INI |
RGFX.EXE is byte-identical between the English CD payload and GOG. The Windows
REDGUARD.EXE launchers differ; they are not the DOS rendering executables.
The CD places the DOS executables outside DATA1.CAB, so extracting that cabinet
alone does not produce a runnable installation.
Demo executable
The 1998 demo’s RGFX.EXE is build 5.0 of game version 1.0; the CD and GOG
executable is build 1.0. It runs the retail data and adds bug fixes, dialogue
fallbacks and a lock to the island. See Demo release for the
executable and data differences.
Renderer-specific assets
| Directory | Renderer | Model versions | Resources |
|---|---|---|---|
3dart/ | Software | v2.6, v2.7 | 120 v2.6 .3DC, 52 v2.7 .3D, and 27 v2.7 .3DC; ROB archives, TEXTURE.### textures, COL palettes, and SHADE.### / HAZE.### tables. |
fxart/ | Glide | v4.0, v5.0 | 204 v4.0 .3DC, 26 v4.0 .3D, and 39 v5.0 .3D; ROB archives, TEXBSI.### textures, COL palettes, and standalone BSI images. |
GOG’s fxart/ contents are byte-identical to the English CD installer payload.
GOG does not include 3dart/.
The directories overlap in model names, but are not interchangeable copies.
Glide includes additional standalone models; software includes additional ROB
archives. Even matching names can have different geometry and animation data:
TIRBA001.3DC has different face and frame counts, and NCROCK.3D has different
face counts. Resolve resources within the chosen renderer’s asset set rather
than substituting a same-named file from the other set.
Both model generations use renderer-specific headers and texture references.
See 3D model versions.
Software texture banks are named TEXTURE.###, not TEXBSI.###; changing the
filename prefix does not establish format compatibility. See software texture banks and software palette lookup tables.
3dart/ contains 18 COL palettes, including art_pal.col, world palettes,
and sky palettes. It does not use art_pal.col exclusively. Matching palette
names can differ between renderers: NECRO2.COL and REDGUARD.COL have different
contents. The Glide set contains 27 populated COL palettes and an empty
CLDTEST.COL placeholder. See COL palettes.
Shared content
The English CD installer payload and GOG share byte-identical:
- RGM scenes and their embedded scripts, WLD terrain, and PVO visibility data;
SOUP386.DEF,WORLD.INI,COMBAT.INI,ITEM.INI,MENU.INI,KEYS.INI,REGISTRY.INI, andsurface.ini;ENGLISH.RTXdialogue and speech,MAIN.SFX, and the other sound resources except the configureddig.iniandmdi.inidevice selections;- fonts and system artwork, including title, menu, HUD, and sky images.
Renderer-specific model and texture differences therefore coexist with shared authored scenes, scripts, and presentation resources.
Configuration defaults
GOG changes these values relative to the English CD installer’s SYSTEM.INI:
| Section and key | CD installer default | GOG default |
|---|---|---|
[screen] smk_interlace | 1 | 0 |
[system] fast_sound | 0 | 1 |
[3dmanager] cache_lifetime | 32 | 64 |
[dialog] dialog_print_text | 0 | 1 |
[3dfx] resolution | 7 | 12 |
These are stored settings, not differences in the shared game scripts.
animation_drive differs only in drive-letter capitalization, d:\ versus
D:\. The software [screen] resolution remains 1 in both configurations.
See SYSTEM.INI for field meanings and renderer overrides.
The CD cabinet’s sound/dig.ini and sound/mdi.ini contain setup comments
without device selections. GOG supplies Sound Blaster device and driver settings.
Hardware configuration belongs to the installation environment; it does not
replace the game’s Red Book music tracks.
Cinematics, disc audio, and support files
The CD Smacker movies are byte-identical to those in GOG’s data track,
including their embedded audio. GOG’s virtual disc omits the CD’s installer
cabinet, DOS engines, setup files, and bundled hardware-driver installers.
Its ISO9660 filenames FINDRE~1.EXE and REDGUA~1.ICO refer to the same
contents as the CD’s FindRedguard.exe and RedguardCD.ICO.
An extracted CD data directory contains movies and installation files, but not the Red Book audio tracks. Mounting that directory as a CD-ROM cannot reconstruct the missing music tracks. A music-capable disc source must retain the audio tracks and their numbering; separately distributed soundtrack files are not a substitute for the disc’s track layout. See background music.
GOG adds host Glide libraries and DOSBox launch configuration. The selected Glide implementation determines the splash variant independently of the shared Glide executable. See startup presentation.
Demo release
The November 1998 demo restricts play to the island of Stros M’Kai. It ships a later engine build than the retail release and a reduced data set.
Package
The downloadable demo (rgdemo.zip) contains an InstallShield installer. Its
RGFX.EXE sits beside the installer, outside data1.cab. The cabinet installs
the Glide data set: SYSTEM.INI, WORLD.INI and the other configuration files,
ENGDEMO.RTX, and the fxart, fonts, maps, sound, soup386 and system
directories. It contains no software-renderer assets and no cinematics.
DEMOMOD.TXT describes the demo for players.
Data
| Data | Demo content |
|---|---|
| Maps | ISLAND.RGM, ISLAND.WLD and ISLAND.PVO only, identical to the retail files. Every interior, the catacombs and N’Gasta’s island are absent. |
WORLD.INI | World 1 (the island by day) and world 99 (maps\brennans.rgm, whose map is not shipped). start_world=1, start_marker=1. |
fxart | 259 of the 755 retail files, identical to their retail versions. |
system | 37 of the 69 retail files, identical to their retail versions. |
| Dialogue | ENGDEMO.RTX: the same 4,866 entries as ENGLISH.RTX with the same text except entry $133, but only 1,224 voiced entries instead of 3,933. The other entries are string-only. |
| Unchanged | ITEM.INI, MENU.INI, KEYS.INI, COMBAT.INI, REGISTRY.INI, SURFACE.INI and SOUP386.DEF are identical to the retail files. |
The island scripts are the retail ones, including the door and boat transitions to interiors and other worlds. Their target maps are not part of the demo; the demo lock prevents those transitions.
SYSTEM.INI differs from the retail file in these values:
| Section | Key | Demo | Retail |
|---|---|---|---|
[screen] | smk_interlace | 1 | 0 |
[system] | animation_drive | h:\ | D:\ |
[system] | fast_sound | 0 | 1 |
[system] | rtx_filename | ENGDEMO.RTX | ENGLISH.RTX |
[debug] | demo_version | 1 | absent |
[3dmanager] | cache_lifetime | 32 | 64 |
[dialog] | dialog_print_text | 0 | 1 |
[3dfx] | resolution | 7 (640×480) | 12 (1024×768) |
[rtx_patch] | bad[0], good[0] | D2DC, D12C | absent |
d2dc is referenced by the retail TAVERN.RGM and exists in neither RTX file.
d12c is “I THOUGHT I MIGHT LEND A HAND.”
Executable
The demo RGFX.EXE has version 1.0, build 5.0 in its
version stamp; the CD and GOG executable is
build 1.0. Build 5.0 contains all of build 1.0 and runs the retail data as
well. Its change notes read:
- “Fixed rope death-reattach bug.”
- “Fixed texture binding crash bug on certain object flats.”
- “Fixed run of edge fall ‘stutter’ bug.”
The build 1.0 note, “Fixed camera reset bug, (see leaving the catacombs).”, is not repeated.
Overview
| Change | Kind | In change notes | Applies to retail data |
|---|---|---|---|
| Rope attachment while dying | Bug fix | Yes | Yes |
| Ledge-fall stutter | Bug fix | Yes | Yes |
| Object-flat textures | Bug fix | Yes | Yes |
| Animation slots | Bug fix | No | Yes |
| String-only dialogue with speech on | Bug fix | No | Yes |
| Missing dialogue entries | Improvement | No | Yes |
| Inventory model textures | Improvement | No | Yes |
| String-only dialogue with subtitles off | Demo support | No | Yes |
[RTX_PATCH] substitutions | Demo support | No | Only when configured |
| Demo lock | Demo restriction | No | Only when demo_version is set |
| Diagnostics | Diagnostic | No | Yes |
Bug fixes correct build 1.0 behavior that is wrong in the retail game. Improvements change working behavior. Demo support exists for the demo’s text-only dialogue file, and the demo restriction confines play to the island.
Bug fixes
Rope attachment while dying
The rope and hook attachment test also requires that the player is not dead. The flag it reads is set when health reaches zero and the death animation starts. In build 1.0 a dying player can still attach to a rope.
Ledge-fall stutter
When a fall off a ledge begins, the actor is also moved up to 4096 position units in its last movement direction, away from the edge.
Object-flat textures
A flat’s texture ID is resolved when the flat is created: bits 7–15 select the
TEXBSI file and bits 0–6 the image. An ID that cannot be
resolved is a fatal error. A texture group that is not yet loaded is loaded at
full resolution. Build 1.0 stores only the raw ID and leaves the resolved index
at zero; its flat renderer stops with the fatal error Bad Texture Index %d when
the resolved index is zero or above 30000. Build 5.0 removes that check.
Animation slots
Animation selection scans the actor’s animation group table. In the ordered scan, a slot whose group byte is zero no longer matches, so a request for group zero selects nothing. The random pick skips a slot whose entry pointer is null instead of reading through it.
String-only dialogue with speech on
An RTX entry without audio is marked as having no voice when it is played.
With [dialog] dialog_use_speech=1, such an entry is then shown for
characters × 15 units, the same timing as a line with speech off. Build 1.0
neither draws nor frees a string-only entry while speech is on (see
subtitle timing). ENGLISH.RTX contains 933
string-only entries.
Improvements
Missing dialogue entries
A missing RTX entry logs ERROR: GetRtx( %c%c%c%c ), RTX NOT found to
GENERAL.LOG and plays entry cbu1 (“DON’T SUPPOSE YOU KNOW WHAT’S GOING
ON.”) instead. Build 1.0 stops with a fatal error. A missing cbu1 is still
fatal.
Inventory model textures
Item models load their textures at full resolution:
[3dfx] text_scale is overridden with 1, 1
while the item system loads its models and restored afterwards.
Demo support
String-only dialogue with subtitles off
A string-only entry is drawn even with dialog_print_text=0, so its text
remains readable when it has no voice. Voiced entries follow the setting.
[RTX_PATCH] substitutions
[RTX_PATCH] substitutes up to 15
entries by name. The section is absent from the retail SYSTEM.INI.
Demo lock
[debug] demo_version enables the
lock. Without the key it is off, so the demo executable plays the full game with
the retail SYSTEM.INI. With it on:
- every world load loads world 1 at marker 1, and a load while world 1 is loaded does nothing;
- pending world changes from doors, boats and the console
loadcommand are discarded; - the player-state update that follows the first world load at startup is skipped.
Diagnostics
- F11 toggles an overlay that shows the current dialogue entry as
(%d) RTX %c%c%c%c. Build 1.0 contains the overlay but never calls it. [3DFX] warningsenables two texture-manager warnings.- A missing texture archive prints
Missing Texture File: FXART\TEXBSI.%03dbefore the fatal error. COMBAT.LOGis reset at startup with the other log files. No code writes to it.
External References
Engine behavior
Runtime rules for scripts, actors, world rendering, sound, and presentation. The file format references describe the stored data; the configuration reference describes the INI settings.
Scripting and actors
| Topic | Behavior |
|---|---|
| SOUP scripting | Virtual machine, bytecode, value modes, tasks, function dispatch, and global flags. |
| SOUP386.DEF | Function, attribute, reference, and flag definitions used by scripts. |
| SOUP property descriptors | Actor-record offset and width of every script property. |
| Actor animation | Model frames, animation requests, loop counts, and placed actor eligibility. |
| Player movement | Coordinate units, turning, collision, and frame timing. |
| Item attachment | Vertex tracking for held weapons and shields. |
| Developer console | All 82 registered commands, aliases, subcommands, numeric parsing, and command quirks. |
| Hidden cheats | Cheat entry, effects, persistence, and developer controls. |
World rendering
Look and feel connects the visible result to the rendering and presentation rules below.
Start with the Glide shading overview for flat and smooth shading, the color pipeline, and links to the detailed rules.
| Topic | Behavior |
|---|---|
| Glide interface | Library binding, called entry points, window parameters, and initial render state. |
| Model lighting | Face brightness, directional and local lighting, and retained instance colors. |
| Actor prelighting | Retained map-light colors on eligible static-model actors. |
| Terrain lighting | Ambient and directional RGB lighting and terrain normals. |
| View projection | Horizontal field of view, viewport scaling, and camera coordinates. |
| Fog and clipping | Scene and sky ranges, fog tables, and color blending. |
| Sky | Sky planes, background color, scrolling, and sun settings. |
| Texture animation | Shared texture frames, authored delays, automatic and explicit selection. |
| Water waves | Terrain vertex displacement and wave settings. |
Audio and presentation
| Topic | Behavior |
|---|---|
| Positional sound | Distance attenuation, stereo pan, channels, and surface sound remapping. |
| Music | CD tracks, world selection, switching, and repetition. |
| Subtitles | Dialogue, taunt, and movie text layout and timing. |
| In-game HUD | Health, items, activation prompts, compass, and draw order. |
| Book menu | Menu models, text placement, fonts, selection, and actions. |
| Cinematic playback | Movie selection, skip points, subtitles, and display rules. |
| Startup presentation | Splash assets, projection, lighting, and screen sequence. |
| Exit quotes | Console banner and the random film quote printed on quit. |
SOUP Scripting
Bytecode scripting engine used by the Redguard runtime for actor behavior, dialogue, puzzle logic, and scene control.
Function overview
SOUP386.DEF declares callable functions in installed order. Index 0 is the
synthetic NullFunction; authored entries start at index 1. See
SOUP386.DEF for function IDs and parameter declarations, and
function dispatch for argument evaluation and task lifetime.
Shared actor setup, navigation, family transforms, and model admission are specified alongside the functions that use them below.
Parameter diagnostics
PrintParms and LogParms take eight integer arguments and return 0. Both
include the receiver’s associated script name, using Unknown script when its
script handle cannot be resolved. They format all eight arguments as signed
decimal integers in argument order, separated by a comma and a space.
PrintParms separates the script name from the first integer with a space. It
copies the formatted line into the first empty diagnostic message slot, scanning
66 slots in order with a copy bound of 255 bytes. If every slot is occupied, it
adds no message and still returns 0.
LogParms sends its line to the internal diagnostic log. Its format starts with
two spaces and places |3 between the script name and the first integer.
Neither function changes the receiver’s gameplay state.
IsWithinMap
IsWithinMap() takes no script arguments and always returns 1. It does not
read map bounds or actor position, and changes no actor or world state.
TrueFunction
TrueFunction() takes no script arguments and returns 1. It does not change
world or actor state.
Animal
Animal is declared as a task with two script arguments. Its handler ignores
their values, returns zero, and changes no actor or world state.
Script Data Sources
| Source | Container | Notes |
|---|---|---|
| Map script bytecode | maps/*.RGM (RASC section) | Main per-actor compiled script payload. Offsets and lengths are stored per actor in RAHD (script_data_offset, script_length, script_pc). |
| Standalone AI script files | soup386/*.AI (for example CAMERA.AI, sword.ai, projtle.ai) | Loaded separately from map RGM files. Uses the same SOUP bytecode model as map scripts. No .AI files are included in the shipped game; the soup386/ directory contains only SOUP386.DEF in both the GOG and original CD releases. |
| VM definition table | SOUP386/SOUP386.DEF | Text definition file loaded at runtime. Defines function/task names, flags, equates, and attributes used by script execution. See SOUP386.DEF. |
| EXE-embedded interpreter | Runtime binary | The executable contains the SOUP VM interpreter and native function handlers; script payload bytes are file-sourced (RGM sections and .AI files). |
What Is In the Runtime vs Data Files
- Runtime contains the SOUP VM/interpreter and native handlers for script-callable operations.
- Script payload data is loaded from map/script assets (
RGMand.AI) and executed by the VM. - Function names and metadata come from
SOUP386.DEF, while actor-local script bytes come fromRASC/.AIcontent.
VM Architecture
The SOUP386 VM is a register-free, program-counter-driven bytecode interpreter. There is no operand stack — values flow through function return values and are consumed directly by the calling instruction. Local variables (RAVA section, per-actor int[]) and global flags (369 entries shared across all scripts) provide persistent state.
Threading Model
An actor retains a main continuation and an optional entry continuation in the same bytecode buffer:
| Continuation | Start Address | Purpose |
|---|---|---|
| Main | RAHD.script_pc | Main execution (dialogue, activation, behavior) |
| Entry | Offset 0x00 | Separate entry work, present only when script_pc != 0 |
The main and entry invocations share bytecode and local variables. Entry processing temporarily replaces the active cursor, then restores the main continuation. The actor has one subroutine return continuation, shared by these invocations, rather than a stack of return addresses.
Execution is cooperative. The original runtime consumes non-blocking instructions until a blocking task or an explicit yield returns control. Existing tasks are revisited before the main script body can continue.
End (opcode 0x05) stores its authored target and ends the current invocation.
It does not permanently terminate the actor’s main script. An eligible later
invocation resumes at the stored target. Endint (opcode 0x13) resets the active
cursor to the script base and returns from the current invocation.
World shell-script preparation
After loading map objects, world loading prepares the available DEBUG,
COMMON, and map-specific shell scripts in that order. The debug-shell
suppression switch skips DEBUG; disabling shell loading prevents shell-record
creation. This preparation precedes the separate CYRUS, GREMLIN, and CAMERA
initialization.
Each shell record takes its script continuations and string-table reference from its map definition and receives its own copy of the initial local variables. When the initial script cursor is absent or equals the script base, no separate entry cursor is stored. New shell records are prepended to the shell list.
The map-specific shell script is the script named after the world’s map file
(START for START.RGM); a name with no installed script creates no record.
SYSTEM.INI [debug] final_version set to a nonzero value skips DEBUG.
Shell script execution
Every world-loop iteration, before the ordinary actor update, each shell record
runs in list order, so the newest record runs first. A record runs as an actor
without a map object: its attributes and control state are zeroed for the run,
while its locals, continuation and task slots persist between runs. Execution is
in shell context, in which the weapon, item and activation predicates report no
ordinary animation state. KillMe marks the record finished and the engine
frees it after that run; a script that never calls it runs every iteration for
the rest of the world.
OpenShell(name) and item use create a record from the installed script of that
name, copying its initial local variables; names compare without case. A world
without the script reports an error. A record created during an iteration first
runs in the next one.
The OpenShell native forwards the supplied name unchanged to shell-record
creation. Its receiver is used in diagnostic arguments and is not passed to
that creation service. Entry diagnostics precede the native’s availability
check. The unavailable-system path records its source information and requests
fatal error 192 before attempting creation. Within shell-record creation,
name resolution precedes the check that shell records are enabled, so disabling
shell records does not bypass name resolution.
Name resolution removes one configured script-directory prefix when the name
starts with that prefix, comparing ASCII letters without case. It performs no
separator conversion. It copies the remaining name into shared scratch storage
and converts ASCII lowercase letters to uppercase. If the resulting name is
longer than three bytes and ends in .AI, it removes that one suffix. The
three-byte name .AI itself remains unchanged.
The resolver searches the current world’s script records in order and returns the first name matching without ASCII case. Duplicate names select the first record. An empty name can match an empty record name. A nonpositive script count performs no comparisons. If no record matches, the resolver records the normalized and original names, emits a diagnostic and requests fatal error 107. The supplied name and script records are unchanged; normalization uses the shared scratch string.
Task start with no free slot
An actor has six task slots. Starting a task scans them in order for a free one; when none is free the start returns without starting the task, running its native or changing any slot, and the calling script continues with the next instruction. A script that starts more tasks on one actor than it has slots (for example one rtxAnim per dialogue line on the same actor while earlier ones still run) therefore loses the later ones silently.
Ordinary update gates
SYSTEM.INI’s [system] task_system enables the ordinary entry, task, and main
script stages when its numeric value is one. The per-actor RAHK native hook is a separate call record, not the
entry continuation. With the task system enabled, the hook is dispatched only
when no unpaused task slot exists. With the task system disabled, task-slot
occupancy does not suppress that hook. Its own suppression flag still applies
in both paths. Actor exclusions and script-name filters apply before these stages.
The actor’s runtime script-control byte has independent effects:
| Mask | Effect in the ordinary actor update |
|---|---|
0x01 | Suppress the ordinary main stage, including its entry and task work |
0x02 | Suppress the RAHK native hook |
0x04 | Use entry and main-body processing without the existing-task pass |
0x08 | Exclude the actor from the ordinary actor-update body |
The 0x04 path is distinct from the usual entry, existing-task, then main-body
order. It retains the entry continuation’s blocking-operand gate, but bypasses
both the existing-task pass and its blocking-task gate before the main body.
An existing unpaused blocking slot therefore does not by itself suppress the
main body on this path. These runtime flags are not RAHD attribute indices. See
actor update order.
The zero-argument control natives modify only their mask in the receiver’s script-control byte. Each disable function sets its mask; each enable function clears its mask. All return zero and preserve the remaining bits.
| Disable function | Enable function | Mask |
|---|---|---|
DisableScript | EnableScript | 0x01 |
DisableTasks | EnableTasks | 0x04 |
DisableObject | EnableObject | 0x08 |
DisableCombat | EnableCombat | 0x10 |
DisableScript sets mask 0x01; EnableScript clears only that mask. Both
return zero and preserve the other bits. They do not terminate the native call’s
current interpreter invocation or clear existing tasks. The wrapper checks the
main-stage suppression flag after the native hook and combat processing, so a
hook can change eligibility for the same update. Once the wrapper enters either
main-stage path, it does not recheck those flags between entry work and the main
body.
The native hook precedes the ordinary entry/task/main sequence. When the task
system is enabled and no unpaused task exists, combat processing follows the
hook for actors with combat state outside shell execution. This includes the
selected player; the wrapper does not restrict combat processing to map actors
or exclude the selected actor. Combat maintenance applies its own player and
nonplayer branches. The entry continuation
is processed later inside the enabled main stage. Mask 0x01 does not suppress
the separate native hook.
HeaderHook diagnostics
HeaderHook() takes no script arguments, writes receiver information to the
diagnostic output, and returns 1. It reports the receiver’s identifier,
optional name, and script name when its script handle resolves in the script
registry. An absent or unresolved handle uses the no-script diagnostic.
All output goes through the console text writer that PrintParms uses. The three lines are the
banner Header Hook: (preceded by a line break), one description line, and the AI line. The
description line is Unnamed object (id N) at P no script attached, Unnamed object (id N) at P using "script", Object "name"(id N) at P no script attached or Object "name"(id N) at P using "script", chosen by whether the receiver’s name field is null and whether its script handle
resolves; N is the signed high word of the identifier field and P the receiver’s address. The
AI line is Node n, dnode d, ai_type t, ai_mode m, ai_delay y, ai_length l, ai_var1 v1, ai_var2 v2.
The following diagnostic reports the receiver’s node, dnode, AI type,
mode, delay, length, and two AI variables. The AI variables are printed as signed
16-bit values. The function does not change the receiver or execute its RAHK
native hook. The ordinary update’s native-hook stage is a separate operation.
PlayerStand query
PlayerStand(receiver) compares the receiver identity with the selected player’s
retained supporting-actor reference. It returns one for equality and zero
otherwise. The query performs no collision search and does not update either
actor or the retained reference.
PlayerArc query
PlayerArc(offset, tolerance) tests the selected player’s horizontal direction
relative to the receiver’s heading. Angles use 2048 units per turn. The query uses
the receiver’s attached position when attached, otherwise its linked query position
when present, otherwise its local position. It reads the selected player’s local
position. Vertical separation does not affect the result.
The receiver heading comes from its attached pose when attached and its local pose
otherwise. A linked query position alone does not replace that heading. The query
forms the horizontal vector from the player to the receiver with wrapping signed
32-bit subtraction, computes its quantized bearing, and negates that bearing modulo
2048. It subtracts offset from the receiver heading modulo 2048, then compares the
absolute shortest wrapped angular difference with the signed tolerance.
Equality succeeds; negative tolerance fails.
The query returns one for success and zero otherwise. It performs no visibility or distance test and does not update either actor.
Fighting query
isFighting searches the retained ordinary actor traversal for another actor
whose combat target is the receiver. A candidate must have a combat record,
its retained attacker-selection state must equal exactly one, and IsDead
must return false. Combat construction and reinitialization clear that selection
state to zero. This state is distinct from the combat action, maintenance phase
and weapon-drawn flag.
The search chooses the first candidate with the smallest rounded three-dimensional distance. Its initial distance bound is 65535 in Q8 coordinates, and replacement requires a strictly smaller distance. Candidates at that bound are excluded; equal distances preserve the earlier actor in traversal order. Position lookup uses each actor’s effective query position, including its normal attachment or linked-position handling.
When a candidate is selected, isFighting returns one only when the rounded
horizontal distance to that candidate is strictly less than [misc] in_combat_threshold from COMBAT.INI, shifted left eight
bits with 32-bit wrapping. Otherwise it returns zero. The horizontal comparison
uses the candidate chosen by three-dimensional distance; it does not perform a
second nearest-candidate search. Both distance calculations use the shared
mixed-precision integer-distance calculation.
Combat maintenance
A newly constructed combat record has no target, an action selector of -1 and a zero cooldown. Combat construction clears the record before assigning its attribute-derived counters and attack metadata. First allocation assigns the current combat-record allocation counter as its retained allocation index, then increments the counter with 32-bit wrapping. Reinitializing an existing record preserves its allocation index across the clear. Opening a world opens a new object-system session, whose combat-record allocation counter starts at zero. Closing that session releases its object pool.
Fresh map loading initializes type-one placement models in MPOB file order. Cell-owned and separately allocated placements share this traversal; spatial cell order does not reorder their model construction. These map models initialize before the two standalone player models. The initially selected player model is constructed before the alternate player model.
Combat construction copies actor attribute 2 into the record’s strength value and attribute 4 into its
armor value (the values Strength, Armor, setStrength and setArmor use). It stores five
counters as 129 minus attribute 58, 62, 63, 87 and 88 respectively, and four more as copies of attributes
59, 64, 65 and 66. The nonplayer decision stage uses the counters from attributes 58, 62 and 63 as divisors
of its tick-based pacing tests. The action, pending-move and defence selectors start at -1, the aggression
budget at 50, the attack state at 0, and combat flag mask 0x08 is cleared.
Combat construction copies RAEX v_vertex as a signed model-vertex index and
converts signed v_size to runtime Q8 contact radius by shifting left eight bits.
An absent actor-extra record supplies zero for both values.
Combat construction derives three spacing ranges from the nine attack slots. Every populated slot contributes to the divisor. Only slots whose first motion has kind 1 contribute range values. The near range is the smallest attack minimum. The far range starts at zero; when a qualifying first motion’s maximum exceeds the retained far range, the attack maximum replaces that range. The comparison uses the motion maximum, while the assignment uses the attack maximum. For each qualifying slot, the constructor adds the attack minimum plus half of the difference between its maximum and minimum to a signed midpoint sum. The subtraction and additions wrap at 32 bits; division truncates toward zero. The third spacing range is that sum divided by the number of populated slots, including slots whose first motion has another kind. If no slot qualifies, the near, far and third ranges are 10000, 30000 and 20000 respectively.
The motion kind this test reads is the first word of the runtime motion record,
which is not the COMBAT.INI type. The type value is stored further into the
record. The loader writes 1 in the first word of every attack motion: single
attacks of type 0 (melee) and type 3 (stab) and the first moves of combos all
carry it, so every populated slot of the installed actors qualifies. For example
the three slots of the Vermai fighters (attacks 75, 76 and 77, each with
minimum 0, maximum 55000, types 3, 3 and 0) give near 0, far 55000 and third
27500, and the five slots of the START pirates (two combos of maximum 45000
and three singles of maximum 35000, all minimum 0) give near 0, far 45000 and
third 19500. The first word of a type 4 (finishing) motion has not been
read from the original. The installed COMBAT.INI contains attacks of type
0, 2, 3 and 4 only.
Fight spacing runs only when the action selector is -1. With the combat-stance predicate set, it uses the constructor’s spacing ranges directly. Both stance paths turn toward the target after preparing their ranges and period, and continue only when the absolute returned angle is strictly below 128. Below the near range it tries backward, right and left movement admission in that order, taking the first result equal to 1. Above three times the far range it requests forward walking only if animation group 31 exists. Otherwise, above the third range plus half the difference between the far and third ranges, it requires group 92 and queries forward movement for one update. It requests forward movement only if the target distance minus that amount is at least the third range. All these movement requests use duration 1.
The facing wrapper uses both actors’ local X/Z positions. It first runs the facing-arrival test, using the speed selected by the receiver’s current animation group. Arrival copies the target’s local X/Z to the receiver, leaves Y and heading unchanged, registers the receiver for a deferred map update and returns zero. Otherwise, the quantized actor heading of the local target displacement supplies the desired heading for the ordinary turn helper. Positive X selects 1536, negative X selects 512, positive Z selects zero and negative Z selects 1024. The lower-level bearing result has the opposite sign; that result is negated once to obtain the actor heading. Its result is zero on a snap and otherwise the signed difference from before the turn step; spacing applies its strict angle gate to that result.
The disengaged decision branch aligns toward the retained target point rather
than reading the target’s current position. If alignment returns zero, it tries
a separate three-dimensional point-arrival test. That test shifts each target
and local coordinate right by eight before subtraction. Its Y difference is the
target Y minus the sum of local Y and the actor’s positive Y bounds extent, all
in integer map units. It uses the same current-group movement attribute as facing,
but admits only when rounded distance is strictly less than (speed * 384) >> 8;
frame scale does not multiply this threshold. Missing animation or attributes
supply speed zero. Admission copies all three target coordinates after shifting
right and then left by eight, preserving heading, and refreshes spatial/model
registration. Rejection leaves position unchanged. This helper snaps on arrival;
it does not move along a path. If neither alignment nor point arrival handles the
update, the disengaged branch requests MoveForward(1) when the shared target
distance is below 500001, and WalkForward(1) otherwise.
When the combat-stance predicate is zero, spacing replaces the near range with 30000 and triples the far range. It consumes two inclusive 0..32767 random draws. The third range becomes three times its original value plus the allocation index times the first draw scaled by 100 and divided by 32767. The spacing period becomes twice its original value plus the allocation index times the second draw scaled by 10 and divided by 32767. Each random scaling division precedes the allocation-index multiplication. Range, period and index arithmetic wrap at 32 bits.
When execution reaches the remaining spacing branch, it first runs the multiple-attacker spacing helper. If that helper requests no sidestep, it adds the receiver’s allocation index to the shared tick counter with signed 32-bit wrapping. Only a signed remainder of exactly one after division by the effective spacing period requests a random movement. The division truncates toward zero; a zero period or a quotient outside the signed 32-bit range causes a division fault.
The movement choice is an inclusive random draw from 0..32767 multiplied by 20 and divided by 32767. Values below 16 choose backward movement, values 16 and 17 choose right movement, and values 18 through 20 choose left movement. Only the chosen direction’s admission is tested. A result equal to one requests that movement with duration one; any other result leaves movement unchanged, without trying another direction. A successful multiple-attacker sidestep skips both the pacing division and this random draw.
A spacing pass that passes the action-selector and turning gates registers the receiver in the deferred map-update list. This registration also occurs when movement admission rejects every direction, the required animation group is absent, or no movement is requested. Repeated registration retains the first occurrence of each actor. The deferred pass processes registrations in that order, clears the consumed entries and resets the list count.
The combat coordination selector starts at zero, separately from the consecutive close-distance counter. Assigning the coordination selector first clears that counter, then stores the requested selector value.
Combat coordination
The coordination selector is also the attacker-selection state used by attacker iteration. Exactly one admits an attacker. The waiting-state lookup scans the ordinary actor list from its head, excludes the supplied target actor, and chooses the first actor with a combat record, a nonnull combat target, lifecycle selector other than eight and coordination state one. It does not require that actor’s target to equal the supplied target.
The active-state lookup starts after the receiver in ordinary list order and
wraps at the list end, stopping before the receiver. It chooses the first other
combat actor with the same combat target, lifecycle selector other than eight,
and either a held weapon or combat flag mask 0x04. The selected player is
excluded.
After successful engagement and both cleanup executors reporting inactive,
coordination state one selects the active branch. With negative aggression and
an active-state candidate, the candidate’s three-dimensional distance to the
selected player must be strictly below three times its far spacing range to
send addressed message 0xd002. The range multiplication wraps at 32 bits.
That send precedes a kind-nine timed taunt request, then assignment of coordination
state zero. Spacing, counter-attack and defense selection still follow this
originally selected active branch.
Other coordination values select the waiting branch. An absent waiting-state
candidate assigns state one. With a candidate and more than ten consecutive
close-distance passes, the receiver sends 0xd003, then requests a kind-eight
timed taunt. Spacing follows the waiting branch without counter-attack or defense
selection on that pass.
Without engagement, state one runs the active-state lookup and sends 0xd002
when a candidate exists, then assigns state zero. Other states remain unchanged.
The retained-target movement helper follows in either case.
Counter-attack timing
Counter selection subtracts the target’s current signed animation command
cursor from its current attack motion’s first_collide_frame and
first_defend_frame, producing two signed wrapping gaps. It does not use the
frame-wait progress byte or the displayed model frame. The target’s retained
attack-slot selector and current motion selector choose the motion.
The target motion’s one-based defense reference, minus one with signed
wrapping, selects one of the receiver’s four defense slots. That record’s
first_collide_frame and collide_duration supply the counter’s timing start
and duration. These values are distinct from spatial min_range and max_range.
Counter selection first requires a held weapon, distance strictly inside the
target’s spacing window, and an absolute wrapped heading difference greater than
768. The target action must be neither minus one nor nine, the receiver action
must be minus one, at least one bound defense must be available, and the target’s
attacked flag 0x10 must be clear. Timing and counter-reference queries follow
these gates.
An accepted counter preserves the low two intent bits while setting bit 0x02,
stores the pending counter index, invokes the synchronous attack starter, then
sets the target’s attacked flag. The target mark follows the starter’s return.
A deterministic timing miss steps right for an even pacing sum or left for an
odd sum when the target’s retained attack selector is three or four; otherwise
it steps backward. These movements use duration one and call the movement
primitive directly, without the spacing helper’s additional obstacle admission.
The attack starter first rejects drawing or sheathing, then requires action minus one. For a nonplayer it retains the pending index, assigns action nine and invokes the attack executor. The selected player’s pending-index selection is separate.
Attack executor
The attack executor resolves its pending move through the four defense slots bound from attributes 79 through 82, rather than the nine attack slots. An idle cleanup request does not inspect move metadata. An idle non-cleanup request compares the current animation group with the pending move’s group. If they differ, it makes a forced animation request with modifier zero, sets the finite loop countdown to one and clears the animation’s pending group. Only then does it set combat attack state to two and subtract ten from the aggression budget with signed 32-bit wrapping. An already-current group leaves the record unchanged.
With nonzero attack state and a pending move, leaving that move’s animation group clears attack state and the pending move. The action selector also clears when it is nine. The executor reports active exactly when attack state is nonzero and a move remains pending.
Multiple-attacker spacing
The multiple-attacker spacing helper traverses the current target’s attacker
list in ordinary actor-list order. First and next queries exclude the target itself
and require a combat record targeting it, retained attacker-selection state exactly
one, and a false IsDead result. Its initial primary neighbor is the first attacker, unless that attacker
is the receiver. It starts a retained distance at 65535. For each later attacker
other than the receiver, a horizontal query distance strictly below the retained
distance replaces the primary neighbor and moves the former primary into the
secondary slot. Equal distances retain the existing pair. The initial attacker
is not distance-tested, and the secondary slot is the preceding primary rather
than a separately selected second-nearest attacker.
The neighbor-distance query uses horizontal Euclidean distance in runtime Q8
units. For each actor, a nonzero attr_slave selects attached position; otherwise
a valid linked positioned record supplies position, with ordinary local position
as the fallback. It subtracts the receiver’s X/Z from the candidate’s X/Z with
signed 32-bit wrapping and supplies zero for the Y difference. Each difference is
stored as single precision. The sum of squares passes through a double-precision
store before an extended-precision square root; the stored single-precision root
converts to an integer with nearest-even rounding. This position selection differs
from the local-position vectors used by the separation query below.
For each retained neighbor, the helper forms receiver-minus-target and neighbor-minus-target vectors from local Q8 positions. Each subtraction wraps at signed 32 bits, then each component shifts arithmetically right eight before vector-to-angle conversion. The separation is the receiver vector’s Y angle minus the neighbor vector’s Y angle: both angles are masked to 2047 and the difference wraps into the inclusive range -1024 through 1024. The two exact half-turn signs remain distinct. An absent neighbor supplies 1024 without converting either vector. Vector-to-angle conversion first normalizes all three vector components to length 65536. Each signed component is stored as single precision. Their sum of squares passes through a double-precision store before an extended-precision square root. The root multiplied by 1/65536 is stored as a single-precision divisor. A zero divisor produces a zero vector. Otherwise each component divided by that divisor is stored as single precision, then converted to a signed integer with nearest-even rounding. The angle conversion consumes this normalized vector. Its Y angle is the negative of the quantized actor heading from normalized X/Z, minus 1024, wrapped modulo 2048. Normalized Y influences this result through the shared three-dimensional normalization, even though the heading query itself uses only X/Z. The zero vector produces Y angle 1024. Normalization can round a small horizontal component to zero, so taking the heading directly from the unnormalized displacement is not equivalent.
The threshold is [misc] non_engaged_spacing from COMBAT.INI, stored without
scaling. Absolute values use signed wrapping negation. With primary and secondary
separations a and b, and threshold s:
- If
abs(a) < s, it requests a sidestep only whenabs(b) > s / 3, using the sign ofa. - Otherwise, if
abs(b) < s, it requests a sidestep only whenabs(a) > s / 2, using the sign ofb.
Division truncates toward zero. A negative chosen separation requests right; zero or positive requests left. Both use duration one. The helper returns one after making that request, without using the movement routine’s numeric return, and zero when it makes no request.
Selected-player maintenance is skipped when its retained entry-inhibition value is nonzero. Player initialization clears this value. Player activation replaces it with the activation operation’s result, so initialization does not make it permanently zero. This gate does not inhibit nonplayer maintenance.
The target/perception prepass returns false for the selected player before nonplayer perception processing. For a nonplayer actor, it returns false when combat flag mask 0x04 is clear or the target is absent. When maintenance reaches a combat record, it restores attribute 1 from attribute 119 before processing messages. The nonplayer decision stage runs only for death/state selector 6 and an actor that is not dead. This stage can select targets and consume random values.
After walk-map or free-node admission, the nonplayer perception prepass samples
visibility at twice the combat far range. The combat record retains a signed
lost-sight timer and a lost-sight routing flag, both cleared by combat construction.
The delay is [misc] node_timer from COMBAT.INI, stored without scaling.
When visibility is absent, the timer is compared with the delay before any increment. Only a timer strictly greater than the delay enters routed pursuit. At or below the delay, the timer adds the shared nominal tick count with 32-bit wrapping and the prepass returns false. Crossing the delay during that addition therefore takes effect on a later pass.
Above the delay, a zero lost-sight flag permits a coordination-peer lookup. A successful lookup sends message 0xD002 to that peer and sets the actor’s coordination selection state to zero. The lost-sight flag is then set to one, point navigation is requested toward the target’s current query position with zero tolerance, and the prepass returns true. This path does not increment or clear the timer. A nonzero flag skips the peer lookup on later pursuit passes.
Visible targets reset the timer to zero. If the lost-sight flag is nonzero, reacquisition restores lifecycle selector six, clears the flag, releases held next/current nodes, sets coordination selection state to one, and clears all walk-node reservation bytes, in that order. The prepass then returns false.
Selected-player combat processing uses the attack/defence executor state as its
phase value. This state is separate from both the cooldown and the retained
combat allocation index. After message processing, phase zero with a nonzero action selector
resets the action selector and both retained action indices to -1. An action
selector of zero leaves all three values unchanged at this step.
It then calls the first maintenance helper followed by the second, both with
action -1. With phase zero and argument -1, the first helper returns false
without invoking other services or changing actor or combat state. With the
combat record in its freshly initialized state, the second helper also returns
false without invoking other services or changing actor or combat state.
The selected player’s drawing or sheathing state suspends action and cooldown maintenance. Otherwise a nonzero cooldown decreases by the shared nominal tick count, saturating at zero. With zero cooldown, action -1 does no attack work; actions 0 through 6 enter attack processing. Other action values reset to -1.
Attack-processing admission
Attack processing requires a combat record and an action other than -1 or nine.
The animation admission query reads the animation state’s unsigned reverse-playback
byte. It must return zero, and combat flag 0x01 must be set. Only then does processing query the current attack’s active collision
window. A true result skips the extended-window query. A false result consults
the extended window instead. If either window admits the attack, processing
sets combat flag 0x20 before traversing either collision-candidate list.
Empty candidate lists leave that flag set.
The active collision window uses the signed animation command cursor and the
current motion’s first_collide_frame and collide_duration. It includes the
start and excludes the end: start <= cursor < start + duration. The end uses
signed 32-bit wrapping addition. A cursor before the start rejects the window
without querying duration.
The extended window first requires a nonzero duration. It then requires
cursor >= start + duration, using the same wrapping end calculation, and
combat flag 0x20 clear. It reads that flag only after the cursor comparison
succeeds. Negative durations receive no separate rejection. Both windows use
the command cursor, not the displayed model frame or frame-progress byte.
Combat candidate dispatch
Attack processing scans its ordered actor candidates, skipping empty entries. For a candidate with a combat record, it first runs that candidate’s defence query against the attacker. A nonzero defence result skips the contact query. Otherwise it queries contact from the attacker to the candidate. A zero contact result continues the scan.
On the first nonzero contact result, it clears the attacker’s combat flag masks
0x01 and 0x02, emits message 0xd001 from the attacker to the candidate,
and requests the attacker’s combat taunt category 3, in that order. It then
returns without scanning later actor or static candidates.
A candidate without a combat record skips the defence query. The contact query
from the attacker to it must succeed and the candidate must not be dead (IsDead).
An attacker with nonzero attribute 97 that neither holds a weapon nor has combat
flag four then returns without scanning later candidates and without any response.
Otherwise the same impact response as for an actor-owned face runs: the attacker’s
attribute-86 effect at volume zero and pitch offset rnd(128) * 16 - 1024 Hz, then,
for a candidate with nonzero health, negative attack damage and a death dispatch
with selector zero below one. Attack processing then clears the attacker’s combat
flag masks 0x01 and 0x02 and returns without scanning later candidates.
Unowned static weapon contact
After actor candidates, attack processing visits retained face-query models in candidate order. Each model receives the retained weapon tip followed by the base as segment endpoints, with query option zero. Results zero and minus one are misses; every other result admits the unowned-model impact response.
Query mode zero scans authored faces after model and face-group admission. Mode one returns zero when a face-group bound admits the segment, without scanning its faces. Mode two returns zero after model-radius admission, without visiting face groups. These modes are separate from the shared polygon-sidedness flag. Its initial stored value is enabled, and initialization also enables it. The temporary query scopes which disable it restore it to enabled before returning. The weapon contact caller does not assign the flag. With sidedness enabled, the face query rejects a segment whose start lies more than four map units behind the authored face plane.
A model without an actor owner uses installed [misc] sword_clank_1, with volume
zero and pitch offset rnd(128) * 16 - 1024 Hz, positioned on the attacker.
Candidate type exactly one repeats the impact sound with a separate random draw.
Other candidate types request one sound. The scan continues to later candidates
and leaves attack intent set. Models with actor owners use the actor-response
path separately from this unowned-model response.
Actor-owned face contact
An actor-owned face candidate still receives the model segment query, but the result does not gate its actor contact test. A successful actor contact must also pass the target rejection check. For attackers with attribute 97 equal to zero, this admits the impact response directly. Other attackers require either the weapon-holding predicate or combat flag four.
The response requests the attacker’s attribute-86 effect at volume zero and
pitch offset rnd(128) * 16 - 1024 Hz, then flushes the sound request. A target
with nonzero health receives negative attack damage, followed by a health read.
A result below one dispatches its death response with selector zero. Zero health
skips damage and death dispatch. Processing continues to later face candidates
and retains attack intent, including after a successful response.
Contact damage and nonplayer health
Contact damage reads the receiver’s retained attack slot and motion selector.
A negative attack slot returns zero; otherwise the selected motion’s signed
damage value is returned without additional arithmetic.
The nonplayer additive health routine first rejects a nonzero attribute 120. If attribute 97 is nonzero, it also requires either the weapon-holding predicate or combat flag four. These checks precede the health write. An admitted change adds the signed change to the unsigned health byte with signed 32-bit wrapping, then clamps the result to zero through 255. This additive routine does not dispatch death itself; the contact caller reads health and owns that dispatch.
Combat hit-reaction admission and animation
A nonplayer hit receiver requires either attribute 102 equal to zero or its value equal to the sender’s attribute 23. A mismatch skips displacement, animation and damage. The selected player bypasses this outer admission test.
An admitted reaction displaces the receiver relative to the sender using the same 3000-unit horizontal translation as the block-message response. It leaves the sender’s position unchanged. For the selected player, a nonzero retained death-animation override then returns before weapon queries, animation or damage.
A nonplayer reaction starts animation group 117 without querying weapon state. The selected player first tests weapon holding, then combat stance only when not holding a weapon. Either predicate selects group 117 on an even world-loop counter or 118 on an odd counter. Otherwise it queries availability of group 119. When available, the receiver-to-sender forward-orientation check selects 119 for a hit in front or 120 otherwise. When unavailable, world-loop parity selects group 132 or 133 instead. All these starters receive loop argument zero.
After animation, damage still requires attribute 102 to be zero or to match the sender’s attribute 23, including for the selected player. A selected-player reaction admitted only by identity therefore animates without applying this damage response. After admitted damage returns, sender attribute 23 equal to 75 requests the sender’s ordinary weapon-restoration service.
Combat direct-damage response
The 0xd005 response helper first displaces the receiver relative to the sender
using the 3000-unit horizontal translation. For the selected player, a nonzero
retained death-animation override then returns before weapon queries, animation
or damage.
A nonplayer receiver starts animation group 117. The selected player tests weapon holding, then combat stance only when not holding a weapon. Either predicate selects group 117 on even world-loop parity or 118 on odd parity; otherwise parity selects group 132 or 133. All starters receive loop argument zero. This response does not query group 119 or forward orientation.
After animation, an enabled selected-player damage-suppression flag skips the health response for the selected player. Other receivers continue regardless of that flag. The response negates the sender’s unsigned attribute 96 and passes that signed change to generic additive health with mode zero, then reads the receiver’s health. Health below one requests receiver taunt category six only for a nonplayer receiver, then invokes the combat death service with selector zero for either identity. Nonfatal health skips taunt and death dispatch.
Combat hit-response damage scaling
The admitted combat hit response reads the attacker’s retained strength and
current-motion damage, then the receiver’s retained armor. Compute the product
(strength + 100) * damage, then divide it by 100 using signed truncation
toward zero. Call that quotient scaled. The health change is
-(scaled - (scaled * armor) / 100), with the armor division also truncating
toward zero. Integer additions, multiplications, subtractions and negation wrap
at 32 bits. No separate strength, damage or armor clamp occurs here.
The response passes this signed change to generic additive health with mode zero, then reads the receiver’s health. Health below one requests receiver taunt category six, followed by attacker taunt category seven. It then reads the attacker’s current motion kind and invokes the receiver’s combat death service with selector zero for kind zero, or selector one for any nonzero kind. Nonfatal health skips these taunts, the motion-kind read and death dispatch.
Health native routing
SetHealth(receiver, value) first raises the selected player’s shared HUD
feedback timer to 25 when it is below 26. It computes value - current_health
with signed 32-bit wrapping and passes that delta through the generic
health-with-death admission service, including for the selected player.
AdjustHealth(receiver, change, mode) performs the same early selected-player
feedback update. For the selected player, mode zero directly invokes additive
health; every nonzero mode directly invokes death-owning health. These direct
selected-player calls bypass generic attribute admission. Their source actor is
the retained native-command source described below, except that a source equal
to the health receiver is passed as absent.
For other receivers, only mode one selects generic health-with-death admission; all other mode values select generic additive-health admission. These generic services apply their attribute gates before forwarding selected receivers or changing nonplayer health. The early native feedback update remains visible when a later admission rejects the health change.
Both generic services reject nonzero attribute 120 before any weapon or combat query. With nonzero attribute 97, they first test weapon holding. Only when that is false do they read combat flag four. A missing combat record supplies a clear flag. The receiver is admitted when either test succeeds; otherwise it is left unchanged. An admitted selected receiver forwards to the corresponding selected health service with no source actor.
Retained health source context
The ordinary entry/task/main interpreter entry establishes its executing actor as the shared native-command source before entry continuation or the existing-task gate. It establishes this source even when an unpaused task prevents the main body. The separate task-actor update uses the same interpreter entry.
In the ordinary actor update, and in the separate-record update, the per-actor RAHK native hook runs before the interpreter entry of the same actor. The hook therefore reads the actor of the previous interpreter entry as the shared native-command source, and the entry replaces it. Nothing else writes the shared source; it keeps its value across the rest of the frame, so natives called from a task polled later still read the actor that last entered the interpreter.
The alternate entry/main path selected by script-control mask 0x04 does not
replace that source actor. It retains the source from the previous assigning
entry while bypassing the existing-task pass. Both entry paths capture the
current shell-execution context before interpreting entry work.
AdjustHealth reads this retained source independently of the receiving actor.
For a selected-player receiver, identity equality converts the source to null;
a different retained actor remains the source. The source is therefore not
recomputed from an object-targeted health receiver or from the actor entering
the alternate main path.
Selected-player additive health
Selected-player initialization clears hit inhibition, sets session maximum health to 100 and copies the actor’s current health byte into retained displayed health.
The selected-player additive routine returns immediately when hit inhibition is active. Otherwise it sets a feedback timer below 50 to 25, adds the signed change to current health with signed 32-bit wrapping, and clamps the result to zero through the session maximum health. It writes the health byte first. A negative change requests ordered taunt category two followed by the damage-reaction service, even when the resulting health is zero. Retained displayed health is written after these services return; both services observe the previous displayed-health value. Category two uses the in-order taunt scan and bypasses target-distance and clock-period gates.
The reaction service suppresses its work when either current or pending animation is group 73, 74, 117, 118, 132 or 133. Otherwise it draws random parity and tests the weapon-holding predicate. With that predicate false, even parity selects group 133 and odd parity selects 132. With it true, even selects 118 and odd selects 117. The selected animation starts before the reaction’s action and motion follow-up services.
The action follow-up runs only for the selected player in living state 15 with its inhibition flag clear. Before its transition service, it sets living state 17 and clears its retained ledge-support actor reference, including when the transition is subsequently blocked. If the transition leaves living state nonzero, it clears the three directional state bytes afterward. Other receivers and living states leave this state unchanged. The motion follow-up calls its destruction service only when the receiver has a rope attachment. Without that attachment it does nothing.
Player initialization clears the separate action-inhibition byte. Ledge entry from living state zero requests state 19 and runs the transition. If a nonzero living state remains, it clears the three directional bytes and sets action inhibition to 16. This byte is separate from hit rejection and combat-decision inhibition.
The state-17 transition requests group 56 and clears living state when the receiver is the selected player, attribute 105 is not one, the torch-burning counter is zero, and neither current group 149 nor 150 blocks it. Those groups block only while animation is enabled and shell context is clear. The animation request precedes the living-state clear. Other combinations retain state 17.
The admitted state-17 transition sets the retained action-transition counter to six. Ordinary actor updates leave a zero counter unchanged. For a nonzero counter, if either the queued-motion byte or falling-motion byte is zero and the signed counter is less than three, the counter clears immediately. This test does not depend on living state. Otherwise the update clears the raised-arms bit of both motion bytes, subtracts the low byte of the shared tick count with byte wrapping, and clamps a negative signed-byte result to zero. The zero-counter and early-clear paths retain both motion bytes. Directional action words are not changed by this counter stage.
Replenishing health
ReplenishHealth takes no arguments. If the receiver’s health byte is at least
100, it returns zero without changing health or feedback. Otherwise it sets the
health byte to 100 and returns one. For the selected player, the change first
raises the shared HUD health-feedback timer to 25 when it is at most 25; larger
timers remain unchanged. This command does not write retained displayed health
or invoke additive-health inhibition, hurt reactions or death services.
Rope construction and swing
Loading a map builds one rope per MPRP record, only when SYSTEM.INI ropes_load is
set. At most 255 ropes exist; another is a fatal error. A rope retains a link array and
an attachment list and joins the head of the rope list.
Constructor inputs, from the record: the link model is rope_model and the optional end model
static_model (an empty name is none); the base position; length links (an i16); the
swing maximum amplitude; the speed rate, where a value below 1 becomes 199; and the type
byte. An end model sets bit 0x10 of the type byte. Bit 0x20 starts the live amplitude at the maximum
(otherwise 0). Heading angle_y and the three swing sound effects at 0x34–0x3C are stored on the rope. A nonzero
word at 0x44 lowers the base Y by the link length times length, plus 256 times an engine constant. A link’s length is a fixed-point value
taken from its model record. Link i records its position along the rope as
(link length / 2 + i × link length) >> 8 and its extent as link length >> 8.
Each update, for ropes whose active word (set to 1 at construction) is 1, the amplitude is clamped to 0..maximum. With
type bits 0x4 and 0x8 clear and a nonzero amplitude, the phase advances by
speed × 0x40 × frame scale >> 8 and the step angle is
sine[(phase >> 8) & 0x7FF] × amplitude >> 9. A zero amplitude gives step angle 0. Attached
actors playing animation 0x52 move one node toward the base, and 0x53 one node down while the
limit routine allows it; each such step also sets the amplitude to 2 and the phase to 0 when the amplitude and the rope’s word at offset 0x62 are zero.
The integer sine table holds, for i in 0..1023, trunc(sin(i × π / 1024) × 65536), with the
entry at 2048 − i negated and the entry at 1024 zero.
Placement runs from the map render list. Link 0 sits at the rope position, yawed by
−angle_y & 0x7FF and pitched by −(amplitude >> 8) & 0x7FF. Link i ≥ 1 sits at the previous
link position plus the previous link’s matrix applied to (0, link length, 0), and is yawed the same
way and pitched by −(sum of the step angles of links 1..i−1 >> 8) & 0x7FF, so link 1 has pitch 0 and an
unswinging rope hangs straight. Positions are stored as single-precision values divided
by 256. An actor attached to node i is placed on that link position.
Each placement then offsets the actor by its hand: the vector (0, −hand Y, −hand Z) of the same grip
vertex, rotated by the actor’s own orientation (heading, pitch, roll 0), is added to the actor’s position,
so the hand rests on the link. The Y component is skipped while the actor plays group 0x54 with the animation
record byte +0x11 below 3.
Rope attachment
Each ordinary actor update of an actor whose movement profile enables the rope check runs an
attachment test before ordinary movement. When the test reports true the ordinary movement and
combat stages are skipped for that update. The test is false for every actor except the selected
player, and false while the player is holding a weapon (see IsHoldingWeapon).
The ordinary update applies displacement from the retained motion before the player’s control hook, then runs attachment admission after that hook. A new jump or rope release queued by the hook participates in admission on that update. The rope countdown therefore advances on the release update. A directional release retains its new jump-off speed for the next update’s carried displacement; it does not apply that speed in the earlier displacement pass.
For the selected player the test returns true without further work when it already has an attachment. It returns false when its directional state bytes 0x1B2 and 0x1B3 are both zero, and when byte 0xC1 is zero while the actor has no animation record or that record’s word at offset 0x161 is zero. A nonzero rope countdown (byte 0x1B0) then decrements by the low byte of the shared tick count; when it would reach zero, the stored rope identifier (byte 0x1AF) and the countdown both clear. The rope identifier is the byte at rope offset 0xC, so a rope the player just left is skipped until the countdown expires. The countdown does not advance while both directional state bytes are zero, including idle after a release, or while another earlier admission condition rejects the player.
Ropes are tried in list order, head first. A rope is a candidate when it is not skipped by the identifier rule, it has a link array and more than one link, and it has a contact sphere list. Contact is decided by one of two tests:
- With type bit 0x1 clear, any overlap between the player’s body spheres and the rope’s contact spheres.
- With type bit 0x1 set, the absolute heading difference (the smaller of the differences from the
rope heading and from the rope heading plus 0x400, over the 0..0x7FF circle) must not exceed SYSTEM.INI
rope_attach_anglemasked with 0x3FF (512 as shipped), and the same sphere overlap must hold.
The player’s spheres are its optional RAFS records (flag 2 always, flag 4 only while the actor’s current
animation group pair matches the record) followed, unless attribute 56 is nonzero, by its ordinary
collision shape spheres; each center is rotated by the actor orientation and added to its position.
At most 128 take part. The rope’s spheres are the records of its own sphere list (11 bytes each:
flags, a 16-bit position triple and radius, each >> 16 << 8), translated by the rope position, at
most 128. Two spheres overlap when the rounded three-axis distance between their centers is less than
the sum of their radii. Contact also records the lowest rope sphere index that touched.
On contact the actor requests animation group 0x54, then attaches (below). For the selected player the rope’s type bit 0x4 clears afterwards and a retained flag is set to one.
Admission does not place the actor on its chosen link. Link and hand-offset placement run in the rope render-list service on a subsequent world update, using the animation group and frame progress current at placement time.
Attach removes any attachment the actor already has (the removal above), allocates a 20-byte node,
links it at the head of the rope’s attachment list, stores the actor in the node and the node in the
actor, copies the rope identifier into the actor and clears the countdown. The requested link index
is the lowest touching sphere index, clamped to [0, link count − 1], then replaced by the link
nearest to the actor’s hand height: starting at index − 8 (floor 0), the link whose stored Y (a
single-precision value) is at or below the target height and closest to it wins. The hand point is
grip vertex 0 (the RAEX grip0 vertex) of the first ShowFrame of animation group 79, as the
integer Q8 model vertex. The target is (hand Y + actor Y) >> 8, with both inputs in Q8. When its
distance from the floor probe is less than the actor height limit plus the vertex’s absolute Y, it becomes the floor probe minus that limit. With no group 79 frame the
requested index stands. The node stores that chosen link index.
Then, with type bit 0x1 clear the rope heading becomes the actor heading; with it set the actor
heading adds the signed heading difference selected above. When either of the actor’s directional
bytes 0x1B2 and 0x1B3 exceeds one, the rope word at offset 0x6A becomes zero while actor byte 0x1CB is
3 or 7 and 0x40000 otherwise, and the rope amplitude becomes (d / 1000) × (link count / link index) rounded, with d the rounded planar distance of the actor’s last step in Q8 and the integer division of d by 1000 truncated, and the rope word at offset 0x62 clears. Finally bytes 0x1B1 through
0x1B4 of the actor clear, and animation group 0x54 is requested when it is not already current.
Rope control while attached
An actor attached to a rope skips the ordinary movement and combat stages (see
Rope attachment). The selected player’s movement dispatch has a rope branch that
runs while it is attached. Keys are the KEYS.INI bindings: Up, Down, Left, Right, A (key_a), B (key_b,
the jump key) and C (key_c, the view key).
- Idle. When the actor holds a weapon, or neither Up nor Down is held, the branch requests group 6 (the idle-rope entry of the named group table).
- Jump release. B is latched on attach: the latch stays set while B is held and clears on an update with B up. With the latch clear and B held, the branch removes the attachment (the removal with its detach motion), runs the ordinary jump entry (which, with the rope countdown now nonzero, requests group 85 and sets the second directional-state byte to 24), clears the heading-turn rate below and sets the latch. The ordinary movement and collision admission reads the attachment after this control branch. A jump release therefore admits the newly queued motion on the same ordinary update; it does not retain the attached movement bypass for that update. The jump latch remains set after release. While falling with B held, that latch prevents the controller from clearing the rope regrab countdown. Damage and a world change also remove the attachment.
- Climb. Without a held weapon and with C up, Up runs the climb-up helper and otherwise Down runs
the climb-down helper. Climb up does nothing unless the attachment’s node index is at least 2; it requests
group 82 when the current group is not 82 and returns true, otherwise it requests group 82 again while
the animation record’s byte
+0x0ais zero and returns whether byte+0x0bis nonzero. Climb down asks the hand-height link choice for node index + 1; it does nothing unless the result is greater than the current index, requests group 83 and returns true when the group was not already 83, and otherwise returns whether byte+0x0bis nonzero. Each update the rope owner reads the current group: group 82 moves the attachment one node toward the base when the shared tick count is nonzero, and group 83 one node down while the limit routine allows it, and either step sets the amplitude to 2 and the phase to zero when both the amplitude and the pending push (the word at rope offset 0x62) are zero. The selected player’s ordinary update also requests group 83 for Down and else group 82 for Up while attached and C is up. - Turn. A nonzero heading-turn rate adds
(rate >> 4) × tick scale >> 8to the actor heading (mod 2048) each update and accumulates the step; the rate clears once the accumulated total exceeds the rate in magnitude. With a zero rate and A held, a rope whose amplitude is below 5 either (type bit 0x1 set) starts a turn of rate 1024 for Left or −1024 for Right with the accumulated total cleared, or (bit clear) addsRight × tick scale >> 8 − Left × tick scale >> 8to the heading. - Pump. With C and A up, the pump service runs on the attached rope unless its type bit 0x8 is set. With neither
Left nor Right held it clears the pending push. Otherwise, while the amplitude is zero it adds 3 to the pending push,
sets the amplitude to 2 and, with
dthe absolute heading difference between the actor and the rope heading, sets the phase (rope offset 0x6A) to 0x40000 for Left or zero for Right whendis below 16, and the opposite when it is not. While the amplitude is nonzero it takes the rope’s direction byte (offset 0x38), flipped when type bit 0x1 is set anddexceeds 16, and adds 3 to the pending push for Left when the byte is 1 and for Right when it is 0; the other key subtracts 3 while the amplitude exceeds 2.
While an attachment is placed on its node during the chain update, a selected player whose pending group is unset (or is group 79) and who holds neither Up nor Down requests group 80 when the rope’s direction byte is nonzero. With that byte zero it requests group 81 when its pitch is nonzero.
Rope amplitude updates
Each chain update ends by passing the sum t of the link step angles to the amplitude service, which keeps the previous sum
t'. A zero crossing (t ≥ 0 with t' < 0, t < 0 with t' ≥ 0, or both zero) runs two things. First, when the rope has
attachments and L = maximum / 4 exceeds 2, it plays a swing sound by amplitude a: sound 3 when a ≥ 3L, else sound 2 when
a ≥ 2L, else sound 1 when a ≥ L, each only when its effect number is nonzero. Second, with u = maximum / 5 and p the pending
push (the word at offset 0x62), it adjusts the amplitude and the push by one step:
p > 0: the step is 5 whenp ≥ 5andp > 4u, else 4 whenp ≥ 4andp > 3u, else 2 whenp ≥ 2andp > 2u, else 1. The amplitude gains the step and the push loses it.p < 0, withn = −p: the step is 1 whenn ≤ 3uand (n > 2orn ≤ 2u), 2 whenn ≤ 3uotherwise, 4 whenn > 3uandn ≤ 4u, and 5 whenn > 4u. The amplitude loses the step and the push gains it.p = 0andtort'nonzero: the amplitude falls by 5 when it exceeds4u, else 4 above3u, else 2 above2u, else 1 while nonzero.
After either branch the direction byte (offset 0x38, copied to 0x39) is 0 when t ≠ 0 and t' < t, and 1 otherwise, and t' becomes t. The
amplitude is then clamped to 0..maximum by the next update.
Rope update order
Each world-loop iteration builds the map render lists first. Only ropes in the retained active transient list participate in link and terminal placement, amplitude service, swing updates and attachment climbs. Ropes outside that list retain their dynamic state until admitted by a later window rebuild. Camera movement within the same starting cell does not change this participation. The list build places the admitted ropes’ links and attached actors and runs each admitted rope’s amplitude service, then runs the rope update (swing step and attachment climbs), then the shell scripts, then the ordinary actors, whose update runs the attachment test and the attached controls.
The link and terminal model transforms are published before the amplitude service and oscillator advance. Collision later in that iteration reads those published transforms, including the terminal’s accumulated pitch from the incoming step angle. Advancing the oscillator does not rebuild the model transforms. The next active geometry pass publishes the new swing pose. Rendered links, terminal collision bounds and face transforms use this same publication; querying them does not advance or republish a rope.
Rope attachment removal
Ropes own doubly linked lists of attachment nodes. Each node retains its actor, rope position and motion value. The actor retains the node identity. Owner lookup scans the rope list and each attachment list by node identity; an absent owner prevents removal.
Removal reconnects both neighboring nodes and replaces the rope’s head when the removed node is the head. Removing the only attachment leaves the owner with an empty attachment list. With a retained actor whose attachment is either this node or null, removal applies detach motion, clears the actor’s attachment and pitch, and sets its rope countdown to 24 before releasing the node. If the actor instead names another attachment, those actor writes are skipped. The removed node is released after the list and eligible actor updates.
The detach motion is the actor’s forward speed word, which queued motion 24 turns into a horizontal move of |speed| × frame scale >> 8 Q8 units per update, forward
at the actor heading for a nonnegative word and backward (heading plus 1024) for a negative one. With j the SYSTEM.INI rope_jump_add shifted left by eight (4608 as
shipped) and m the node’s motion value (the planar speed the chain update measured between the node’s previous and new link position, in Q8, negated when the rope’s
direction byte is zero): when the rope amplitude is below 6 and |m| is below frame scale × 0x1800 >> 8, the word is zero, except that for the selected player Up sets it to j and Down
to −j. Otherwise, with the direction byte flipped when type bit 0x1 is set and the actor’s heading differs from the rope heading by more than 16, a byte of 1 sets the word to
j + (|m| << 8) / frame scale and a byte of 0 sets it to −j − (|m| << 8) / frame scale.
The rope-countdown jump entry queues motion 24 and forces animation group 85 before
storing movement classification 2 in last_direction. During the later motion-24
displacement, attachment to a rope refuses the update before classification changes.
Otherwise, a negative speed word stores classification 3 and marks backward movement
bits 0xc0; a nonnegative word stores classification 2 and marks forward movement
bits 0x30. These writes precede translation suppression and the positive-distance
test, so zero speed and suppressed translation retain them.
Destroying an entire rope uses a separate path. It unlinks the rope from the rope list, walks and releases its attachment nodes, and clears each retained actor’s attachment link, even when that link names a different attachment. It does not invoke attachment detach motion or change actor pitch and rope countdown. Attachment nodes are released in list order, followed by the optional auxiliary record, optional end-model record and the rope itself. A null rope does nothing.
For the detach direction branch, the rope’s angle flag enables a heading test. The signed heading difference wraps over 2048 units. An absolute difference strictly greater than 16 flips the rope’s direction bit for that detach.
Selected-player death-owning health mode
The death-owning health service is separate from selected additive health. Hit inhibition rejects it before any service. Otherwise, a health-feedback timer below 50 is set to 25 before action cleanup or the health write. For a negative change, action reset and rope detach run before the actor health write. The service then clamps the wrapping sum to the session maximum and zero, and publishes actor and retained displayed health before later notification.
When health becomes zero and the session death-suppression flag is clear, it requests taunt category six, enables hit inhibition, clears the retained death-progress word, captures the selected actor’s local position and sends death notification, which sets its death selector to eight without changing other actor state. An existing controller animation override is preserved. If no override is set, a nonnull source actor’s nonzero attribute 109 supplies it before ordinary death animation selection.
When neither an existing override nor the source supplies a group, underwater
state two or four sets the retained death mode to two. Attribute 105 equal to
one skips the remaining animation choice. Otherwise, a nonzero fall_flag or
current group 114, 116, 130 or 131 sets the override to group 74. With none of
those conditions, an unarmed receiver force-starts group 134. A receiver holding
a weapon starts group 122 when present, otherwise group 121, then stores death
default 124 or 123 respectively and releases its held walk nodes, in that order.
The nonfatal or death-suppressed path instead clears the override, hit inhibition and the retained death-progress word. This word is separate from the controller’s pending action flag. A negative change then requests taunt category two and the damage reaction after health publication. These service orders differ from selected additive health and are not interchangeable.
Selected-player death admission state
IsDead returns true for a selected player with nonzero hit inhibition, or for
any receiver whose death selector is eight. The retained death-animation group
does not participate in this predicate. The same hit-inhibition flag gates
selected-controller death execution, ordinary defence sampling and dead-receiver
taunt rejection. It is separate from the retained death-animation group.
Selected-player death animation override
The selected player’s retained controller override stores an animation group identity, not only an active flag. When that override and selected hit inhibition are both nonzero, default-animation selection copies the override group into the actor’s default animation. If the current group already matches, it cancels the pending group, clears the current playback exit marker, and sets its finite loop countdown to 32. This replaces any previous loop countdown without restarting the current frame or resetting its command cursor. Otherwise it force-starts the override group. This branch bypasses ordinary default-animation selection.
Selected-player death controller
Hit inhibition admits the selected death controller after the ordinary preliminary controller countdowns. If the retained animation override is zero, the controller assigns group 122 while the actor holds a weapon, or group 134 otherwise. A nonzero retained group bypasses the weapon query.
Every admitted call force-requests the retained group with modifier zero before adding the nominal-tick increment to the retained death-progress word. The addition wraps at 32 bits; the starter observes the previous progress value. Only death modes one and two perform environmental effect updates. Zero and other mode values return after progress accumulation. The player initializer clears both death mode and progress to zero. These words are separate from the controller pending-action flag and preliminary countdowns.
For a selected player with hit inhibition, attribute 105 equal to one and death mode two, the world loop exits with outcome 5000 only when signed death progress is strictly greater than 12. Progress 12 does not exit. This branch bypasses the ordinary retained-group animation-completion comparison.
Otherwise, inhibited selected-player death completes only while the current
animation group equals the retained override. For override groups 13 and 74,
the signed command cursor must be less than or equal to the unsigned wrapping
frame-progress byte, anim.real_frame. For other override groups, completion
requires that cursor and progress byte differ. This compares command progress
with frame progress, not the displayed model frame or finite loop countdown.
Completion returns world-loop outcome 5000.
World-loop death exit ordering
The world loop checks death exits after map and separate-object updates, effects, scene and HUD presentation, and fade handling. A pending world transition returns its destination before the falling timeout or selected inhibition checks. The win request is checked after those death exits.
A selected player whose falling byte is zero clears the retained falling elapsed counter. A nonzero falling byte adds the shared nominal-tick increment to that counter, wrapping at 32 bits. When the resulting signed value is strictly greater than the configured falling-death limit, the loop returns outcome 5000. Equality does not exit. This check precedes, and does not require, selected-player hit inhibition. The player initializer clears this counter independently of selected death mode and progress.
The limit is max(player_fall_dead_time, 3) * 12, using signed configuration
comparison and 32-bit multiplication. It is measured in nominal ticks, not
rendered frames. A pending world transition bypasses the counter update as well
as the death checks.
Death completion dispatch
After world-loop outcome 5000, the main dispatcher checks the retained last-save slot. An absent slot opens the book menu with entry context three. An available slot presents a Yes/No request to load the last game, with the retained save description appended to the prompt. No opens the same book-menu context. Yes copies that slot into the pending load selection and selects saved-game loading for the next dispatcher pass.
The dispatcher clears selected-player hit inhibition after the confirmation or book-menu call returns. The saved-game loading pass receives the retained slot, then clears the pending selection and returns the dispatcher to world execution.
Selected death effect admission
Death-controller environmental effects read progress after its nominal-tick
increment. Mode one always emits its first effect. It then makes two independent
bounded random requests, each with bound progress / 6 + 1 using signed
progress and division truncating toward zero; a zero result emits
the corresponding additional effect. These random requests remain ordered and
independent. A final effect is emitted only when signed progress exceeds 12.
Mode two consumes one raw engine random value. It emits only when the bitwise
AND of that value and wrapping progress + 1 is zero. An admitted emission uses
one effect variant before signed progress 12 and another from 12 onward. It
then requests actor sound 71 before 12 or sound 70 from 12 onward, after a
bounded random request of 64 whose result is reduced by 32 for the sound’s
signed variation argument. No effect or sound request occurs when the initial
bitwise test rejects the call.
The packed descriptors passed to effect construction are:
| Emission | Descriptor |
|---|---|
| Mode one, unconditional first effect | 0xb081 |
| Mode one, first bounded-random effect | 0x22480 |
| Mode one, second bounded-random effect | 0x2580 |
| Mode one, final effect after progress 12 | 0x32480 |
| Mode two, before progress 12 | 0x2500 |
| Mode two, from progress 12 onward | 0x12500 |
The low sixteen bits identify the texture; the upper sixteen bits are effect flags. Every selected-death construction supplies zero spread and speed one.
Each emission first computes the camera-relative point described below, then
consumes two bounded random requests in X-then-Z order. The unconditional mode-one
image uses bound 32 and adds draw * 256 - 4096 to each horizontal coordinate.
Every other selected-death image uses bound eight and adds draw * 256 - 1024.
Y is the transformed point’s Y plus the receiver’s retained positive Y bounds
extent multiplied by 256, plus 1536. These coordinate operations wrap at 32 bits.
The extent is the stored bounds record’s positive Y extent, not the actor’s
current origin, animation image height or effect texture height. These point and
random operations still run when effect construction later finds a disabled or
full pool.
The mode-two sound is positioned from the receiver’s ordinary sound query and uses zero volume offset. Its signed variation is passed directly as the playback-rate offset in Hz.
The effect-position helper copies the supplied integer point and preserves its
Y coordinate. It subtracts the published current camera X/Z coordinates from
the point using signed wrapping 32-bit subtraction, then computes the shared
quantized horizontal bearing in 2048 units per turn. Camera height does not
participate. With index = (bearing - 1024) & 2047, it adds
offset * sine[index] to X and offset * sine[index + 512] to Z. The death
controller supplies offset 2048.
Each original X/Z coordinate is converted to single precision before the displacement is added. Each sum is stored in single precision, then converted to an integer by truncating toward zero. The helper returns a shared scratch point and leaves the supplied point unchanged.
Environmental effect animation
World initialization tears down the previous environmental effect pool before
creating a pool with [system] max_effects from SYSTEM.INI. Negative configured
capacity is clamped to zero. A zero capacity or a nonzero disable_effects
setting leaves the pool disabled. An enabled pool starts with every slot free.
Allocation scans the retained pool list from its head and takes the first free slot. It marks that slot active and decrements the free count without moving the slot in the list. Release marks an active slot free and restores the free count, without exceeding capacity. Releasing an already free slot has no effect. Update and drawing traverse the same retained list order.
The type-one effect constructor returns no effect when the pool is disabled or allocation fails. It separates the supplied packed descriptor into upper-16-bit flags and a lower-16-bit texture identity. The resolved texture supplies the target frame as its runtime frame count minus one. Current and reference frames start at zero, and the constructor retains the supplied animation speed.
For zero spread, the constructor subtracts texture_height * 128 from the
supplied fixed-point Y coordinate with wrapping 32-bit arithmetic. Position
publication then arithmetic-shifts each signed coordinate right by eight bits.
This places the effect half its texture height above the supplied point in
the engine’s downward-positive Y convention.
The world loop submits environmental effect billboards while building map draw lists, after actor model-position processing and before the object and code-object systems run. It updates effect lifetimes after those systems, before the rain-drop update and rendering the queued scene. Effects created by those later object updates miss the current loop’s billboard submission but participate in its lifetime update. Surviving effects can submit on the next loop. An effect already submitted can expire during the subsequent lifetime update; releasing its slot does not remove the submitted billboard.
The type-one environmental effect updater rejects a disabled effect pool, a
null effect, a different effect type or a zero texture descriptor. Otherwise
it leaves the current frame unchanged when that frame is -1 or the retained
reference frame equals the target frame.
When the reference frame is below the target, the updater adds nominal ticks multiplied by animation speed to the current frame. It keeps the effect alive through equality with the target and expires it only above the target. When the reference frame is above the target, it subtracts the same product and expires only below the target. Multiplication and frame arithmetic wrap at 32 bits; reference, target and expiration comparisons are signed. The retained reference, rather than the current frame, chooses the direction.
Environmental effect frame submission
The enabled pool’s draw traversal runs only when at least one slot is active and
walks the retained list in order. Before checking a record’s type or texture,
packed flag 0x20000 subtracts nominal_ticks + 2 from its retained whole-map Y
coordinate with wrapping 32-bit arithmetic. This changes retained state even
when that record does not submit a draw. Disabled or entirely free pools skip
the traversal and this position change.
For type-one records, a positive signed texture descriptor admits the draw
wrapper. Construction zero-extends the lower sixteen-bit texture identity, so
identities with bit fifteen set remain positive. Identity zero skips the wrapper;
identity 65535 reaches the wrapper but submits no billboard. Packed flag 0x10000 selects caller scale 90; otherwise the caller
scale is 256. The alternate drawing path retains its own fixed geometry scale.
Ordinary type-one effect drawing submits the resolved texture with automatic
frame selection, independently of the effect’s retained animation frame. The
retained frame still controls effect lifetime. Packed effect flag 0x20000
selects the alternate drawing path, which submits that retained frame explicitly
instead. Both paths submit the retained integer position converted to single
precision. The alternate path bypasses the ordinary effect metadata lookup.
The ordinary path submits face brightness 255 and geometry scale equal to the
draw caller’s integer scale divided by 256. Its default constant alpha is 255.
When texture metadata exists, metadata byte two plus one supplies alpha, and
metadata byte one is ORed into drawing flags initially set to one. The alternate
path submits geometry scale one half, constant alpha 140 and drawing flags
0x201.
The alternate path takes face brightness from the current world’s
world_ambientfx value in WORLD.INI. World preparation stores that integer in
sixteen bits and replaces a stored zero with 128. World entry publishes it as a
signed sixteen-bit value before effect drawing. This scalar is separate from
world_ambientrgb and the ordinary effect path’s fixed brightness 255.
The ordinary metadata lookup uses the current map’s
FLAT texture attributes, shared with placed flats.
It splits the texture identity into the archive and seven-bit image IDs and
uses the first matching attribute record. Missing records leave the defaults
unchanged.
Both paths use a centered camera-facing quad. Its half extents are the source image width and height multiplied by half the texture scale and geometry scale. Texture-coordinate scale is one. Alpha, face brightness and frame selection remain separate inputs to the shared billboard submission. The source bound uses the same quantized source-image diagonal as centered ordinary flats, multiplied by half the texture scale and geometry scale. Reduced texture allocation dimensions do not determine the bound.
Shared billboard submission transforms the retained center into camera space and tests its source bound before requesting a texture frame. A rejected bound neither requests the image nor appends a draw packet. An accepted bound requests the automatic or explicit frame, binds that frame’s face and vertex sources, and appends the packet in submission order. This image request precedes later polygon processing; it does not require raster coverage or the placed-flat spatial-cell selection.
Scene drawing submits ordinary spatial-list billboards before traversing the environmental effect pool. Accepted effect packets append to the same queue in retained pool order and participate in its shared alpha partition.
For centered billboard geometry, drawing flag 0x20 negates the local X half
extent and flag 0x40 negates the local Y half extent before corner generation.
These flags reverse the corresponding geometry axis while retaining the same
source-image bound. Drawing flag 0x100 becomes queued packet flag 0x10.
Billboard drawing flag 0x200 becomes queued packet flag 0x20, which disables
depth writes. Packets without that flag enable depth writes. The alternate
effect path therefore tests depth without updating the depth buffer. Its depth
comparison remains less-than, as on the ordinary path. Packet flag 0x10
selects an always-pass depth comparison instead.
Both effect paths select point texture filtering and constant alpha. Alpha values at least 254 select source-one/destination-zero blending; lower values select source-alpha/one-minus-source-alpha blending. The constant-color alpha channel retains the low byte of the converted alpha value.
Contact death animation dispatch
A null receiver does nothing. Selector zero first checks for animation group 122 and starts it when available. Otherwise the dispatcher checks group 121 and starts it when available. A nonzero selector goes directly to the group-121 check. Missing groups do not prevent the final death-selector assignment.
After a group starts, a receiver with combat state requests walk-node release and stores its death default as 124 for group 122 or 123 for group 121. Receivers without combat state skip those writes. Every nonnull receiver then receives death selector eight.
Combat death service
The direct-damage and scaled hit responses end a fatal hit with this service; the weapon-contact caller uses the contact death animation dispatch instead. A null receiver does nothing. A selected-player receiver does nothing while the session death-suppression flag is set or hit inhibition is active. The service takes a selector: zero prefers death group 122 when the receiver has it, any other selector or a missing group 121.
With attribute 69 equal to zero, the selected player stores the chosen group as the death-animation
override when none is set; it does not start the animation here, the
selected death controller does. A nonplayer receiver instead
force-starts the group, releases its held walk nodes while storing death default 124 (group 122) or
123 (group 121), sends coordination message 0xd002 from itself to the
active-state coordinator when one exists, and clears its close-distance
pass counter and coordination selection state. A nonplayer without a combat record faults.
Every receiver then resets its combat action selector to -1. A receiver with death selector eight
releases its held walk nodes; any other receiver takes the combat reset: lifecycle selector one
(with its response byte cleared when the selector was not already one) and, when the killed flag
is set, the script-control hook enabled. The service sets death selector eight, clears combat
flag 0x04 and zeroes the close-distance pass counter and coordination selection state. The
selected player finally applies health -256 through the
death-owning health with no source actor, which
enables hit inhibition and ends the world loop once the death animation completes. The
death-owning armed branch and the nonplayer death admission also reset the combat action
selector to -1 after storing the death default.
Hit-defence response
The candidate’s hit-defence query requires the attacker to have combat state and
an action other than -1 or nine. The defender’s pending defence index, the word the
attack executor resolves through the four defense slots (written by an ordinary
counter and by the selected player’s defence stance), must
equal the attacker’s current-motion counter-attack reference. The defender’s attack-slot selector
does not take part. The attacker’s defence
window must admit processing, and the defender must have combat flag 0x02 set.
Only then does the query test contact from attacker to defender.
An admitted contact miss clears defender flag 0x08 and returns zero. Earlier
admission failures leave this flag unchanged. A contact hit sets the attacker’s
reverse-playback byte to one before playing the defender’s impact sound. It then
emits message 0xd000 from defender to attacker, requests defender taunt category
4, clears intent masks 0x01 and 0x02 on the defender followed by the attacker,
and toggles defender flag 0x08. It returns one. Other combat flag bits survive.
The impact cue comes from COMBAT.INI section [misc]: sword_clank_1
on an even world-loop counter and sword_clank_2 on an odd counter. It uses
one random draw with bound 128 and pitch offset draw * 16 - 1024 Hz.
The defender supplies the sound position. Sound dispatch and flushing precede
the response message.
Defence contact window
The defence window compares the signed animation command cursor against the
current motion’s first_defend_frame and defend_duration. It includes the
start and excludes the end: start <= cursor < start + duration. The end uses signed 32-bit
wrapping addition. A cursor before the start returns zero without reading the
duration. Otherwise the query reads start again, then duration, then the cursor
again before comparing the end. Zero and negative durations have no separate
admission rule. This query does not read the attack-contact mark or playback
direction.
Motion contact selection
A motion whose col_sphere_size is zero uses the range/arc contact path. A
nonzero value selects sphere contact. Sphere contact resolves the current
motion’s col_vertex through the actor’s model-point query, then subtracts the
candidate actor’s local position component by component with signed 32-bit
wrapping. The three differences feed the distance query. Contact succeeds only
when its signed result is strictly less than the current motion’s
col_sphere_size. Equality rejects contact.
Contact orientation
Contact orientation subtracts receiver local position from the supplied point
with signed 32-bit wrapping, then shifts each component arithmetically right
eight bits. The normalized vector’s Y angle is converted to heading by
(1024 - angle) & 2047. The absolute wrapped difference from receiver yaw must
be strictly less than the current attack motion’s arc + arc_center, with
signed 32-bit wrapping in the sum. The current retained attack slot and motion
selector supply both fields. Equality rejects orientation. This threshold differs
from the first-motion defence-selection arc interval.
Range/arc motion contact
For a candidate with no combat record or a zero combat radius, the range/arc contact path checks orientation, then range, then elevation. A failed check skips the later checks. Orientation and range accept a nonzero result; elevation must return exactly one. Range uses three-dimensional query-position distance. Range and elevation use the receiver’s retained attack-slot selector.
For a candidate with nonzero combat radius and attribute 126 equal to zero, the path first resolves the candidate’s retained model vertex. It tests orientation against that point before querying horizontal query-position distance. The distance must be strictly between the minimum and maximum ranges of the retained attack slot’s first motion, and strictly less than the candidate’s combat radius. Equality at any of these boundaries rejects contact.
For a candidate with nonzero combat radius and attribute 126 nonzero, the range/arc contact path resolves its retained model vertex, then calls the sphere/segment query. The point coordinates and candidate radius shift arithmetically right eight bits for this query. Both segment endpoints use the same retained combat point of the receiver. The contact path returns the query’s result without converting it to a boolean.
With identical segment endpoints, the sphere/segment query tests the endpoint against the sphere. Each endpoint-minus-center coordinate wraps at signed 32 bits and stores as single precision. The squared components add in extended precision, then their sum stores as single precision and truncates to a signed 32-bit integer. An invalid integer conversion produces the signed minimum value. The radius square and its subtraction of that integer both wrap at 32 bits. A nonnegative signed residual returns one, including equality. A negative residual returns zero because the segment has zero length. Radius sign is not separately rejected.
Equipped-weapon combat endpoints
Drawn-weapon attachment requests use the signed RAEX grip0 and grip1 source
vertex indices. Stowed-weapon attachment requests use scabbard0 and scabbard1.
Both requests use the actor’s current animation handle and frame. The loader
copies these four signed words into the actor before attachment queries.
After selecting an equipped-weapon attachment, an actor with combat state retains
two endpoints and the attachment record. The base endpoint is the attachment’s
floating position truncated component by component to signed integers. For the
tip, the local vector (0, -(weapon_length << 8), 0) uses signed 32-bit wrapping
and transforms through the attachment matrix. Each transformed integer component
shifts arithmetically right eight bits. That integer adds to the corresponding
floating attachment position in extended precision, then truncates to a signed
integer without an intermediate single-precision sum store.
An absent attachment clears the retained attachment record and leaves both previous endpoints unchanged; both endpoints start at zero, so an actor that never selected an attachment tests the segment from the origin to the origin. An actor without combat state receives none of these writes. The attribute-126 contact path uses the retained tip for both of its segment endpoints.
Combat messages use 256 queues selected by attribute 125. Queue initialization clears each queue; startup and combat reset both allocate eight entries per queue. New Game and save restoration reset these queues. Actors retain separate byte read cursors. A message contains sender, receiver and code. Reading skips messages addressed to another actor, advancing and wrapping the read cursor. A receiver-zero message is accepted by any actor in that class. Sending a nonzero code writes at the class write cursor, advances and wraps it, then clears the next message code. A zero code does not send a message. The combat message stage consumes at most one addressed message per invocation.
Coordination message 0xd002 uses the receiver’s current combat target; 0xd003
uses the sender’s current combat target. Starting at the ordinary list head,
they clear coordination state for each living actor whose target equals that
target and whose coordination state is exactly one, excluding the target itself.
Traversal continues from each matched actor’s successor without wrapping. A null
target skips this scan. Clearing or assigning coordination state also resets the
actor’s consecutive close-distance counter. After the scan, 0xd002 assigns the
receiver coordination state one and aggression 50; 0xd003 assigns those values
to the sender. These assignments still occur with a null target. Neither message
changes the selected actor’s combat target. Unrecognized nonzero message codes
are consumed without combat effects.
Message 0xd000 clears intent bits 0x01 and 0x02 on the receiver,
then on the sender. It next displaces the sender horizontally relative to the
receiver with a movement amount of 3000. This operation writes position, not
actor heading. A null actor skips the displacement.
The bearing uses the moving actor’s local X/Z minus the other actor’s local
X/Z, with signed 32-bit wrapping. The translation heading is zero on positive Z, 1536 on positive X,
1024 on negative Z and 512 on negative X. The translation uses the original sine and
cosine tables, stores the old coordinate and the final sum as single precision,
and truncates the final sum toward zero. Y, actor angles and transform matrices
are unchanged; this helper does not register a deferred map update.
After displacement, handoff runs only when the receiver’s coordination state
is zero, the receiver is not the selected player, and the receiver’s death
query returns zero. It searches from the ordinary list head for the first
actor other than the sender that has combat state, a nonnull target, death
selector other than eight and coordination state exactly one. A match receives
message 0xd003 from the receiver. Without a match, the receiver’s coordination
state becomes one, resetting its consecutive close-distance counter. This
fallback does not assign aggression 50.
Message 0xd001 clears receiver intent bits before sender intent bits. If
receiver attribute 97 is nonzero, it tests weapon holding, then combat stance
only when not holding a weapon. When both tests are false, response processing
returns after the intent clears.
An admitted response draws a random value with bound 128 and requests the
receiver’s attribute-86 positional effect with volume zero and pitch
draw * 16 - 1024. It completes the positional sound request, requests receiver taunt category
two, and reads the sender’s combat action. Sender action eight requests receiver
weapon cleanup before the hit-reaction service runs with receiver then sender.
After the reaction returns, receiver action other than minus one or nine resets
the sender’s action to minus one. The receiver’s coordination state is then
read. State zero, nonselected identity and a zero death query admit the same
ordered coordinator search and fallback as the block response. The search
excludes the sender. A match receives 0xd003 from the receiver; without a match,
the receiver receives coordination state one without assigning aggression 50.
The ordinary 0xd005 response draws with bound 128, requests the attribute-86 positional effect
with volume zero and pitch draw * 16 - 1024, completes the positional sound request, requests
receiver taunt category two, then invokes the direct-damage response with
receiver followed by sender. After that returns, coordination state zero and
nonselected identity admit the ordinary coordinator search excluding the sender.
Unlike block and hit messages, this tail does not query death. A match receives
0xd003 from the receiver; no match assigns receiver coordination one without
assigning aggression 50. This message does not clear either actor’s intent bits.
Messages 0xd000, 0xd001 and 0xd005 consume their addressed message before
performing response work. Message 0xd001 also clears the receiver’s intent
bits before the sender’s. Message 0xd005 does not perform those intent clears.
Ordinary entry processing is suppressed by an unpaused blocking slot whose call operand address is at or before the saved entry cursor. This test is separate from the later main-body blocking test and from direct interrupt invocation.
Value Modes
The same opcode byte is interpreted differently depending on the calling context. There is no separate addressing-mode byte — the calling instruction determines how trailing bytes are consumed.
| Mode | Context | Effect |
|---|---|---|
| MAIN | Top-level statement | Performs assignment (writes to flag/variable/property) |
| LHS | Left side of if comparison | Reads value; consumes 1 extra operator byte |
| RHS | Right side of if comparison | Reads value; consumes 1 extra operator byte |
| PARAMETER | Argument to a task/function call | Reads value; consumes mode-specific padding bytes |
| FORMULA | Right side of an assignment expression | Reads value only, no extra bytes |
The number of trailing bytes consumed after an opcode depends on both the opcode and the current value mode. See Operand Encoding for per-opcode details.
Bytecode Encoding
Opcode Table
All integers are little-endian. The VM reads a leading opcode byte and dispatches. Unrecognized bytes are fatal.
| Opcode | Name | Encoding (after opcode byte) | Description |
|---|---|---|---|
0x00 | Task | u16 func_id, u8 param_count, params… | Call task on self |
0x01 | Multitask | same as 0x00 | Async task on self (non-blocking) |
0x02 | Function | same as 0x00 | Call function on self (returns value) |
0x03 | If | condition chain, u32 end_offset, block | Conditional branch |
0x04 | Goto | u32 target | Unconditional jump |
0x05 | End | u32 target | Store target and return from the current invocation |
0x06 | Flag | u16 flag_id, mode-dependent | Global flag read/write |
0x07 | Numeric | i32 value | Immediate 32-bit integer |
0x0A | LocalVar | u8 var_index, mode-dependent | Local variable read/write |
0x0F | ObjDot++ | object encoding, u16 ref_id | Increment object property |
0x10 | ObjDot– | object encoding, u16 ref_id | Decrement object property |
0x11 | Gosub | u32 target | Subroutine call (overwrites actor return continuation) |
0x12 | Return | (none) | Return from subroutine |
0x13 | Endint | (none) | End secondary thread |
0x14 | ObjectDot | object encoding, u16 ref_id, mode-dependent | Object property read/write |
0x15 | String | u32 string_index | String literal from RASB/RAST table |
0x16 | NumericAlt | i32 value | Same as 0x07; used for global flag function arguments |
0x17 | Anchor | u8 anchor_value | Anchor assignment |
0x19 | ObjDotTask | object encoding, task encoding | Task call on named object |
0x1A | ObjDotFunc | object encoding, task encoding | Function call on named object |
0x18 | Yield | none | Save the following cursor and end the invocation |
0x1B | TaskStop | u32 label | Remove matching actor task slots |
0x1C | TaskPause | u32 label | Pause matching actor task slots |
0x1D | TaskResume | u32 label | Resume matching actor task slots |
0x1E | ScriptRV | u8 expected, block | Branch on script return value |
The typed decoder leaves unimplemented opcode encodings explicit rather than interpreting them as successful operations.
Operand Encoding
Flag (0x06) and LocalVar (0x0A) consume different trailing bytes depending on value mode:
Flag (0x06)
| Mode | Bytes after u16 flag_id |
|---|---|
| MAIN | Formula (assignment to flag) |
| LHS, RHS | u8 update byte: zero reads, 10 increments, 11 decrements |
| PARAMETER | u16 padding (always 0x0000) |
| FORMULA | (none) |
LocalVar (0x0A)
| Mode | Bytes after u8 var_index |
|---|---|
| MAIN | Formula (assignment to variable) |
| LHS, RHS | u8 update byte: zero reads, 10 increments, 11 decrements |
| PARAMETER | 3 padding bytes (0x000000) |
| FORMULA | (none) |
ObjectDot (0x14)
| Mode | Bytes after object encoding + u16 ref_id |
|---|---|
| MAIN | Formula (assignment to property) |
| LHS, RHS | u8 update byte: zero reads, 10 increments, 11 decrements |
| PARAMETER, FORMULA | (none) |
Conditional updates write the adjusted value before the comparison and compare that adjusted value. Both sides of every comparison are evaluated, including their updates and function calls.
In PARAMETER mode, NumericAlt consumes four bytes but returns the unsigned low 16 bits. A String parameter consumes four bytes but uses the low byte as its actor-local string index.
Native parameters use numeric, global flag, local variable, string, or object-property operands. Native calls are value expressions outside PARAMETER mode; PARAMETER dispatch does not evaluate nested native calls.
Object-targeted native calls evaluate local-variable and property operands against
the receiving actor. Native string parameters instead use the retained
native-command source described under Retained health source context.
Normal interpreter entry establishes that source before entry continuation and
existing-task admission. The script-control mask 0x04 alternate path retains the
previous source, including its string table. A string parameter therefore need
not use either the native receiver’s table or the alternate path’s executing
actor’s table.
A routed initial call preserves the retained source’s string table. A later normal interpreter entry on the receiving actor establishes that receiver as the source before reevaluating its active task’s string indices. The stored task operand location remains in the calling actor’s bytecode.
Object Name Encoding
Used by opcodes 0x0F, 0x10, 0x14, 0x19, 0x1A. A leading byte selects the object target:
| Byte | Additional | Object |
|---|---|---|
0x00 | u8 padding | Me — the script’s own actor |
0x01 | u8 padding | Player — Cyrus |
0x02 | u8 padding | Camera |
0x04 | u8 string index | Named object from per-script string table (RASB/RAST) |
0x0A | u8 var index | Actor reference from local variable |
The local-variable form uses the selected local’s value directly as an actor reference. It does not resolve a runtime identifier through the namespace registry.
Named object lookup compares bytes with ASCII letters A–Z folded to lowercase. It searches registered object namespace names, rather than actor script names. The registry orders entries by their signed 16-bit runtime object identifier; lookup returns the first matching name. Registering an identifier already in that registry does not add another entry.
Removing a killed post-load object leaves its namespace registration in place. Named lookup returns the registered object reference without checking the live actor table or the object’s killed flag. Resetting the namespace registry clears all registrations.
Runtime object identifiers
MyId() returns the receiving actor’s stored runtime identifier as a signed
16-bit value extended to the script integer. The query does not allocate an
identifier or add the pool-slot bias.
Runtime identifiers are separate from the MPOB record’s object id. The reusable actor pool has capacity max(10, SYSTEM.INI [system] max_active_objects). An allocation uses the first free pool slot and initially assigns its zero-based index plus three as the actor’s 16-bit identifier.
World object loading resets its identifier counter to the pool capacity plus three. It visits MPOB records in file order. Types two and six use separate object paths. Each other record creates an ordinary actor. A nonzero activation flag allocates from the reusable pool and increments the counter once; a zero flag allocates outside that pool. The ordinary actor path then increments the counter and assigns its low 16 bits as the actor identifier before loading its script and registering its namespace.
Objects created after world loading by shoot, shootPlayer or DragonBreath are allocated as standalone records rather than from the reusable actor pool. Their runtime identifier is zero. If the installed script supplies an object namespace, creation registers that namespace under identifier zero; repeated registrations of the same identifier retain the first entry.
CYRUS and GREMLIN player instances load independently of MPOB placements. Each uses identifier one; the initial player selector chooses CYRUS. The camera instance loads CAMERA independently of MPOB and uses identifier two. Fresh world loading creates and registers ordinary MPOB actors first, followed by CYRUS, GREMLIN and CAMERA. Each player role is temporarily selected during its own initialization. After GREMLIN initialization, world setup marks it excluded from ordinary object updates and restores CYRUS as the selected player.
Separate map object script state
MPOB types two and six use separate object records rather than ordinary actor records. Script binding uses the same authored instance allocator as ordinary objects: each matching load advances the instance counter and, after the first instance, advances the local-variable pointer by the variable count times four. These objects therefore consume their authored local-variable allocation even though they do not consume the ordinary actor identifier counter.
The separate-object update converts each record into a cleared temporary actor and clears its attribute table. It restores the object’s position, script continuations, local-variable and string pointers, and native task-slot state. After processing, it copies position, continuations and task state back to the separate record. Attribute writes in that temporary actor are not copied back. The separate-object traversal runs after ordinary actor traversal and retains its own list membership. Outside editor mode, it maintains the optional light record after script/task dispatch and before copying the temporary actor back to its separate record. This light maintenance also runs when task-system advancement is disabled. Separate records always belong to their position’s 512-unit map cell, regardless of the MPOB activation byte. Loading prepends each record to its cell’s separate list. The cached view-window rebuild appends those lists in the same cell order as ordinary traversal, skipping the four corners. It does not apply the ordinary outer-border attribute filter to separate records.
Initial ordinary motion state
Ordinary actor allocation clears the complete actor record before applying
loader defaults. The reusable pool clears each selected free slot on every
allocation, including reuse; standalone allocation clears the newly allocated
record too. CYRUS and GREMLIN use the same pool allocator. Their last_direction
byte therefore starts at zero at allocation, as it does for ordinary actors,
regardless of the previous contents of the storage.
Its sliding state, falling state, queued-motion state, body response flags and collision-transition flags therefore start at zero. These runtime bytes are separate from the installed attributes and current animation group. The ordinary update invokes queued motion when its state is nonzero, using that state before the falling state when both are present.
Reference Name Encoding
The address reference returns the resolved actor reference itself. It does not
read a field from the actor or convert the reference to a runtime identifier.
Used directly as a native parameter, an object-property read passes that
reference value to the native handler. WaitLineUp can therefore receive an
actor reference through an address property operand.
Object-property operands carry a little-endian u16 reference ID after the object encoding. Property reads use the full reference ID to index the global references table loaded from the [refs] section of SOUP386.DEF. The high byte is not discarded. With native validation enabled, a reference outside the table enters the invalid-reference error path.
Local pose properties
pos.vx, pos.vy and pos.vz access the resolved actor’s local position.
angle.vx, angle.vy and angle.vz access its local angles. Each property
reads or writes the complete 32-bit field selected by its installed descriptor.
Player resolves the currently selected player before accessing the field.
These accesses do not substitute an attached pose, normalize angles, recompute matrices or update other actor fields. Each write changes only its selected field; subsequent actor processing performs the usual transform work. Ordinary heading values use 2048 units per turn.
Current animation property
anim.current reads the resolved actor’s current animation group as a signed
16-bit value, extended to the script integer width. It preserves negative
values, including the no-current-group value of -1. If the actor has no
animation association, the read returns zero. Reading this property neither
requests nor advances an animation and does not change actor or animation state.
Torch counter property
torch_timer accesses the resolved actor’s torch-burning counter. Reads return
the unsigned byte as a script integer. Writes retain only the low eight bits of
the assigned value and change no other actor field. The property access itself
does not run torch effects or decrement the counter.
Operators
Arithmetic Operators
Assignments use a terminated list of (value, operator) pairs. The formula loop reads a value (any value-producing opcode in FORMULA mode), then an operator byte, repeating until the terminator.
| Byte | Operator | Arity | Operation |
|---|---|---|---|
| 0 | ; (end) | — | Return result |
| 1 | + | binary | Addition |
| 2 | - | binary | Subtraction |
| 3 | * | binary | Signed multiplication |
| 4 | / | binary | Unsigned division |
| 5 | << | binary | Left shift |
| 6 | >> | binary | Arithmetic right shift |
| 7 | & | binary | Bitwise AND |
| 8 | | | binary | Bitwise OR |
| 9 | ^ | binary | Bitwise XOR |
The formula evaluator accepts only zero and binary operator bytes 1–9. Other operator bytes enter the fatal error path. Division by zero is not checked in software — it raises a CPU divide-by-zero exception caught by the C runtime signal handler.
The ScriptReader and ScriptParser tools interchange multiplication and division bytes. Engine bytecode uses 3 for multiplication and 4 for division.
Comparison Operators
Used in if conditions. Each comparison pairs a LHS value, comparison byte, and RHS value. Ordered comparisons use signed 32-bit integers.
| Byte | Operator |
|---|---|
| 0 | = (equal) |
| 1 | != |
| 2 | < |
| 3 | > |
| 4 | <= |
| 5 | >= |
Conjunctions
Multiple comparisons in a single if are chained with conjunction bytes:
| Byte | Meaning |
|---|---|
| 0 | End of condition list |
| 1 | and |
| 2 | or |
Conditions are evaluated left-to-right with no operator precedence — each conjunction folds the running boolean with the next comparison result.
Control Flow
Conditional Branch (If — 0x03)
Evaluates one or more comparisons chained with and/or. If the condition is false, the PC jumps to end_offset (absolute). If true, execution falls through into the inline block.
[0x03]
repeat:
[value: LHS mode]
[u8 comparison]
[value: RHS mode]
[u8 conjunction] // 0 = end, 1 = and, 2 = or
until conjunction == 0
[u32 end_offset LE] // false-branch target (absolute)
[block of instructions] // executed when true; ends at end_offset
Goto (0x04)
Unconditional jump. Sets PC to the u32 target address (absolute).
End (0x05)
Reads a u32 offset relative to the actor’s script base, stores that address as
the main continuation, and returns from the current invocation. It also clears
pending synchronization state for the actor’s configured group and user channels
and releases the current activation reference when it refers to that actor.
These notifications do not clear the stored continuation or disable the actor.
When the actor’s script remains eligible and has no blocking task, a later main script stage starts at that continuation. Target zero therefore restarts at the script base on that later invocation, rather than executing again immediately or stopping permanently.
The installed ISLAND FLAG2 script is 61 bytes: four blocking PushAnimation
calls request groups/counts (1, 25), (10, 1), (1, 3), (10, 2), followed
by End with target zero. Its finite task counts do not make the whole script
finite. The authored sequence can repeat while the actor remains eligible.
Gosub / Return (0x11, 0x12)
Gosub stores the address following its target operand in the actor’s single return continuation, then jumps to the target. A later Gosub overwrites that continuation. Return resumes at the stored continuation and restores the actor’s saved script-return byte. It does not pop a stack or clear the saved continuation. Subroutines share actor-local variables.
A fresh actor’s return continuation is null. A Return executed while it is null (installed scripts reach this by entering a subroutine body with End, for example the VILEDOG actor) behaves like Endint: the invocation ends and the actor’s main continuation becomes the script base. The same holds when the Return runs from the entry continuation; the main continuation is reset to the script base there too, unlike Endint, which preserves it. The game continues running; nothing faults.
Endint (0x13)
Resets the active cursor to the script base and returns from the current invocation. Script-entry processing preserves the main continuation while advancing its separate entry cursor.
DistanceFromLocation location selection and distance-helper boundary
DistanceFromLocation(location) has one argument. It selects a coordinate
triple from the receiving actor’s location table. The table begins with a
signed upper bound followed by triples of 32-bit coordinates. A nonnegative
location at or below that bound selects the corresponding triple, including
location zero and the upper-bound entry. An absent table, a negative location,
or a location above the signed upper bound selects the actor’s saved start
coordinates instead.
The native subtracts the actor’s current local X, Y and Z coordinates from the selected coordinates with 32-bit wrapping subtraction. It passes those three differences to the distance helper and returns the helper’s result unchanged. The location selector and native wrapper do not write actor or table state.
Function Dispatch
DistanceFromStart
DistanceFromStart(axis) returns a signed displacement from the receiving
actor’s current local coordinate to its saved start coordinate. Axis 0 selects
X, 1 selects Y, and 2 selects Z. Every other axis value returns zero before
reading actor coordinates.
For a supported axis, it subtracts the current local coordinate from the saved start coordinate with 32-bit wrapping, then shifts the signed result right by eight bits. The result is in world units; negative fractional differences round toward negative infinity. It reads the receiver’s stored coordinates directly, makes no calls, and does not modify actor or shared state.
GetMovementSpeed
GetMovementSpeed() takes no script arguments and queries the receiving actor.
It returns zero when the receiver has no animation record or attribute table.
Otherwise its current unsigned 16-bit animation group selects an attribute
byte, returned as an unsigned integer without scaling:
| Animation groups | Attribute index |
|---|---|
| 18, 30, 142 | 8 |
| 19, 143 | 9 |
| 20, 21 | 12 |
| 22, 23 | 13 |
| 24 through 27 | 10 |
| 31 | 19 |
| 92, 93 | 17 |
Every other group returns zero. The query does not require a model, inspect the selected player, call another service, or modify actor or shared state.
IsBouncing
IsBouncing() takes no script arguments. It returns one when the receiving
actor’s motion-state byte is from 27 through 32 inclusive, and zero for every
other byte value. It reads the receiver directly, without looking up the
selected player or calling another service. The query does not modify actor
or shared state.
IsShellScript
IsShellScript() takes no script arguments. It returns zero when the shell
system is disabled, the shell list is empty, or the receiving actor pointer
is null. Otherwise it returns the shared shell execution-context word
unchanged, without converting it to a Boolean.
The query does not dereference the receiver or inspect its script identity or shell-list membership. It makes no calls and does not modify actor or shared shell state.
MainloopExit
MainloopExit(code) compares its argument as a signed 32-bit integer. Values
below 5000 leave the shared main-loop exit request unchanged. Values at least
5000 replace the request with the supplied code, including when a different
request is already pending. The function returns zero in either case and does
not access the receiver.
At the main loop’s late exit-request check, a nonzero request becomes the loop’s return value. That check does not clear the request. A zero request continues through the remaining loop work.
TestTask
TestTask(value) takes one script argument and ignores it. It resolves the
receiver’s and selected player’s query positions, subtracts the player’s
three coordinates from the receiver’s with 32-bit wrapping, and passes the
difference to the shared integer-distance calculation. A signed result
greater than 131072 requests movement of the receiver toward the player’s
resolved position, with the movement request’s final argument set to zero.
Equality does not issue the movement request.
The handler returns one regardless of whether it issues that request or of the movement request’s return value. It does not report task completion; ordinary existing-task processing therefore retains its task slot until another operation removes it.
Spare1Parm7 diagnostic and argument count
SOUP386.DEF declares eight arguments for Spare1Parm7. Its native handler
emits a diagnostic using 0x%p Spare function with the receiver pointer and
returns zero without dereferencing the receiver or changing actor state.
It does not use the incoming argument values.
The handler receives the receiver and three argument values in registers and removes four argument slots from the stack on return. This accounts for seven arguments, despite the declaration’s eight. If five argument slots are supplied on the stack after those three register arguments, the final slot remains on the caller’s stack. Preserve the declaration’s eight-argument count separately from the handler’s four-slot stack cleanup.
The native dispatcher uses the argument-count byte in the encoded call record.
An eight-argument record evaluates all eight operands from last to first, places
three argument values in registers, and pushes five argument words. The dispatcher
restores its saved registers immediately after the native returns; it does not
repair a discrepancy between the pushed words and the native’s stack cleanup.
For an eight-argument record targeting Spare1Parm7, the handler leaves the eighth
argument in front of those saved registers, so the dispatcher restores and returns
from shifted stack positions. A seven-argument record pushes four words and is
balanced by this handler’s four-word cleanup.
Diagnostic writer
The diagnostic writer can suppress a request before formatting. A separate presentation setting can suppress storage after formatting. When storage is enabled, the writer limits the formatted message to 255 characters and copies it into the first empty slot of a 66-slot diagnostic buffer. A full buffer discards the message. These paths return normally; issuing a diagnostic does not guarantee that the message enters the buffer.
Spare1Parm6 diagnostic
Spare1Parm6(first, second, third, fourth, fifth, sixth) takes six script
arguments and ignores all six. It sends a diagnostic containing the receiving
actor pointer to the diagnostic writer and returns zero. The handler does not
dereference the receiver or modify actor state.
The native receives three argument values in registers and removes three argument slots from the stack on return, matching its six-argument declaration.
Spare1Parm5 diagnostic
Spare1Parm5(first, second, third, fourth, fifth) takes five script arguments
and ignores all five. It sends a diagnostic containing the receiving actor
pointer to the diagnostic writer and returns zero. The handler does not
dereference the receiver or modify actor state.
Spare1Parm4 diagnostic
Spare1Parm4(first, second, third, fourth) takes four script arguments and
ignores all four. It sends a diagnostic containing the receiving actor
pointer to the diagnostic writer and returns zero. The handler does not
dereference the receiver or modify actor state.
Spare1Parm3 diagnostic
Spare1Parm3(first, second, third) takes three script arguments and ignores
all three. It sends a diagnostic containing the receiving actor pointer to
the diagnostic writer and returns zero. The handler does not dereference the
receiver or modify actor state.
Spare1Parm2 diagnostic
Spare1Parm2(first, second) takes two script arguments and ignores both. It
sends a diagnostic containing the receiving actor pointer to the diagnostic
writer and returns zero. The handler does not dereference the receiver or
modify actor state.
Spare1Parm1 diagnostic
Spare1Parm1(argument) takes one script argument and ignores it. It sends a
diagnostic containing the receiving actor pointer to the diagnostic writer and
returns zero. The handler does not dereference the receiver or modify actor
state.
Spare1Parm0 diagnostic
Spare1Parm0() takes no script arguments. It sends a diagnostic containing
the receiving actor pointer to the diagnostic writer and returns zero. The
handler does not dereference the receiver or modify actor state.
Spare0Parm7 diagnostic
Spare0Parm7(first, second, third, fourth, fifth, sixth, seventh) takes seven
script arguments and ignores all seven. It sends a diagnostic containing the
receiving actor pointer to the diagnostic writer and returns zero. The handler
does not dereference the receiver or modify actor state.
Spare0Parm6 diagnostic
Spare0Parm6(first, second, third, fourth, fifth, sixth) takes six script
arguments and ignores all six. It sends a diagnostic containing the receiving
actor pointer to the diagnostic writer and returns zero. The handler does not
dereference the receiver or modify actor state.
Spare0Parm5 diagnostic
Spare0Parm5(first, second, third, fourth, fifth) takes five script arguments
and ignores all five. It sends a diagnostic containing the receiving actor
pointer to the diagnostic writer and returns zero. The handler does not
dereference the receiver or modify actor state.
Spare0Parm3 diagnostic
Spare0Parm3(first, second, third) takes three script arguments and ignores
all three. It sends a diagnostic containing the receiving actor pointer to
the diagnostic writer and returns zero. The handler does not dereference the
receiver or modify actor state.
Spare0Parm2 diagnostic
Spare0Parm2(first, second) takes two script arguments and ignores both. It
sends a diagnostic containing the receiving actor pointer to the diagnostic
writer and returns zero. The handler does not dereference the receiver or
modify actor state.
Spare0Parm1 diagnostic
Spare0Parm1(argument) takes one script argument and ignores it. It sends a
diagnostic containing the receiving actor pointer to the diagnostic writer and
returns zero. The handler does not dereference the receiver or modify actor
state.
Spare0Parm4 diagnostic
Spare0Parm4(first, second, third, fourth) takes four script arguments and
ignores all four. It sends a diagnostic containing the receiving actor pointer
to the diagnostic writer and returns zero. The handler does not dereference
the receiver or modify actor state.
Spare0Parm0 diagnostic
Spare0Parm0() takes no script arguments. It sends a diagnostic containing the
receiving actor pointer to the diagnostic writer and returns zero. The handler
does not dereference the receiver or modify actor state. Its diagnostic output
is a side effect even though it makes no gameplay state change.
Call Types
Active tasks reevaluate their encoded operands on every task invocation. A task therefore observes changes to locals, global flags, and queried values while it is active. Literal arguments are a special case of this rule.
Native calls evaluate their parameters from the last authored argument to the first. The handler receives the resulting values in authored argument order. This evaluation order also applies when starting or revisiting a task; nested function side effects are observable by earlier arguments.
SOUP386 distinguishes three call types, encoded in the opcode byte:
| Type | Behavior | Script syntax |
|---|---|---|
Task | Blocking — script waits for completion | FunctionName(...) |
Multitask | Asynchronous — script continues immediately | @FunctionName(...) |
Function | Immediate — returns a value | FunctionName(...) (context determines) |
Self-calls use opcodes 0x00/0x01/0x02. Object-targeted calls use 0x19/0x1A with an additional dispatch-type byte (0x00 = task, 0x01 = multitask, 0x02 = function) when used as a top-level statement. In non-MAIN modes, the dispatch-type byte is absent and the call is always treated as a function.
The runtime function table keeps only each function’s handler; the task, function or multitask keyword in SOUP386.DEF is parsed and discarded. A call therefore runs the handler of whatever function it names, whichever call type the script uses. Existing-task processing removes a task when its handler returns zero. Initial self-task registration is a separate step and does not use that native result as an immediate-completion test. Shipped scripts use FlatAnimate and FlatSound in multitask form even though both handlers return zero.
Object-targeted task dispatch differs from a self task statement. The receiver owns the task slot, and the calling body continues immediately. The self task statement checks the active result and yields when blocking. Both encodings must remain distinguishable even when the explicit object selector resolves to the current actor.
For a self task statement, the task-start wrapper first searches the receiver’s six task slots for the address of that statement’s function-index operand. An existing match skips the encoded operands without evaluating them or calling the native handler, advances to the following statement, and returns inactive from this start attempt. It does not remove the existing slot.
With no match and a non-null receiver, the wrapper calls the native and advances over the encoded operands. The statement helper restores the receiver as its result, so the initial start attempts registration even when the native returned zero. Registration searches for the first empty slot. The slot stores the function-index operand address, starts its invocation counter at one, clears its pause flag, and records the call’s blocking flag and saved native context. A full slot array leaves all slot entries unchanged; the start wrapper still reports active.
ResetAllAction in self task form with two numeric operands evaluates both
operands and clears both action words on an initial invocation. This path
reports active and attempts task registration, even though the reset handler
has no defined function result. An already registered call skips that initial
reset and argument evaluation. Its task encoding is therefore distinct from
a direct function statement that simply discards the result.
When an existing ResetAllAction task is unpaused, the task pass reevaluates
its encoded arguments and invokes the reset again. The handler leaves the
receiving actor in the native result register, so a non-null receiver retains
the slot. This task does not complete merely because the reset effect has
finished. Its invocation counter advances with unsigned 16-bit wrapping, and
a blocking slot continues to contribute to the task pass’s blocking result.
A paused slot does not evaluate arguments, reset action words, or advance its
counter. The action timer remains unchanged by these reset invocations.
The later task pass tests the native result directly. A zero result removes the slot and its blocking contribution; a nonzero result retains the slot and saves its native context. This test is separate from initial task registration.
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 NullFunction, a synthetic parser entry absent from
SOUP386.DEF. The native dispatch table uses four-byte handler-pointer entries.
Its loader clears the allocation and fills authored entries starting at index 1,
leaving slot zero null.
Index-zero native dispatch returns zero without calling a handler, evaluating parameters, or dereferencing the receiver. It records the receiver as the current native-call receiver before checking the table. The argument-preparation entry also returns zero for this null slot before reading the parameter count or writing the argument buffer. These are null-slot dispatch paths, rather than a named zero-argument native handler.
For a non-null handler, the dispatcher takes the argument count from the call
record, without comparing it to the declaration in SOUP386.DEF. Its dispatch
paths cover counts zero through eight. Supplied operands are evaluated from
last to first; the handler receives the first three arguments in registers
and any remaining arguments on the stack, in authored order. A count of zero
evaluates no operands and does not initialize argument registers to zero.
This dispatch rule does not define a value for an argument the handler expects
but the call does not supply.
After a direct function statement returns, the statement reader advances past the two-byte function index and one-byte argument count, then skips five bytes per encoded operand. It uses the encoded count for this cursor advance, rather than the declaration’s parameter count. The statement discards the native function’s result.
For a direct function statement invoking ResetAllAction with two numeric
operands, both operands are evaluated, both action words are cleared, the timer
is unchanged, and execution continues after both encoded operands. The reset
handler ignores these values and supplies no defined function result.
Parameter Type Overrides
When a Numeric (0x07) or NumericAlt (0x16) opcode appears in PARAMETER mode, the 4-byte value may be reinterpreted based on the calling function:
| Functions | Type | Encoding |
|---|---|---|
ACTIVATE, AddLog, AmbientRtx, menuAddItem, RTX, rtxAnim, RTXp, RTXpAnim, TorchActivate | Dialogue key | 4-byte ASCII string (RTX lookup key) |
LoadWorld | Map ID | i32 map identifier |
AddItem, DropItem, HandItem, HaveItem, SelectItem, ShowItem, ShowItemNoRtx | Item ID | i32 item identifier |
| All others | Integer | i32 signed integer |
ACTIVATE and TorchActivate receive the dialogue key as a packed 32-bit
numeric value, not a string-table index. Its least significant byte is the
first RTX key character. RTX lookup folds ASCII uppercase bytes to lowercase.
Ordinary entry gate
Ordinary entry processing uses the actor’s saved entry continuation, distinct from its main continuation. When the initial main entry offset is zero, the separate entry continuation is absent. A nonzero initial main offset starts main there and initializes the separate entry continuation at the script base. An absent entry continuation makes ordinary entry processing return immediately. Before running it, the engine checks occupied, unpaused blocking task slots. A slot suppresses entry when its stored operand address is less than or equal to the saved entry address. This is an address comparison, not a comparison of offsets relative to each task’s own script. For programs within the same RASC allocation, adding each script’s RASC offset to its local cursor preserves that ordering.
When entry runs, its saved cursor temporarily becomes the interpreter’s active
cursor. The resulting cursor is saved back as the entry continuation, and the
main continuation is restored. A yielding entry therefore resumes from its
saved position. Endint resets that entry continuation to the script base.
TaskStop, TaskPause, and TaskResume
An actor has six task slots shared by its entry and main invocations. Existing-task processing traverses those slots in allocation order, skips paused slots, reevaluates encoded operands, and removes a slot when its handler reports completion. Unpaused blocking slots suppress the subsequent main-body invocation; multitask slots do not. Reissuing a call whose operand address is already in a slot does not execute its handler again. Task labels resolve against the receiving actor’s script base and match the byte immediately after the labelled dispatch opcode. Self task calls store the operand address immediately after their Task or Multitask opcode. Object task calls store the operand address immediately after the embedded dispatch byte following the two-byte object selector. Routing retains that encoded operand location; it does not rebase foreign bytecode into the receiver’s script. TaskStop removes every matching slot and clears its blocking and pause state. TaskPause and TaskResume set and clear the matching slots’ pause state.
The KillMyTasks function takes no arguments and returns zero. It clears all six of the receiver’s task slots at
once, whatever their operand addresses, including their blocking and pause state and retained handler scratch,
without invoking any handler. It acts immediately, so a task started by a later statement of the same invocation
occupies a fresh slot.
The initial task call invokes its native handler, then allocates a slot independently of that handler’s return value. A self blocking task yields after this allocation, even when its first native invocation reports completion. Completion is applied by a later existing-task pass. An object-targeted task allocates on the receiver and the caller continues, including when the selector resolves to the caller itself.
Each active slot has a 16-bit invocation counter. Allocation sets it to one. An eligible existing-task pass computes its elapsed input as the low 32 bits of counter times the current frame scale, shifted right by eight, then increments the counter with 16-bit wrapping. Paused slots do not advance. The initial native call receives elapsed input zero.
Wait, WaitOnTasks, and WaitNonMulti
Wait(duration) stays active while the elapsed input is less than the wrapping 32-bit value duration - 1, compared as signed integers. WaitOnTasks stays active when more than one unpaused slot is occupied. WaitNonMulti stays active when more than one unpaused blocking slot is occupied. These count tests include the current wait slot when it has already been allocated.
Anchor, Yield, and ScriptRV
Anchor stores its operand byte as the actor’s anchor state. Yield retains the following cursor without issuing the End notifications.
ScriptRV reads an expected byte. On mismatch it skips the next five bytes. On a match, a following Return byte advances the cursor by four bytes; a following Gosub saves the current script-return byte before the control instruction runs. Other following instructions remain eligible. Return restores that saved byte.
Group synchronization channels
Actor data byte 29 names the actor’s group channel (1 through 255; zero means none), byte 30 its user
channel, and a nonzero byte 43 excludes the actor from its group channel’s rendezvous. Actors join their
channel’s member list when they are created. World initialization enables the system and clears every
channel’s released value; with the system disabled, or for an actor without a channel or member entry,
SyncWithGroup completes immediately.
Each member holds a posted value, initially zero. SyncWithGroup(group) is a task. Each invocation stores
group | 0x80 (the group’s low eight bits with the posted mark) as the actor’s posted value, then compares the
channel’s released value with that stored byte. When they are equal it clears the mark, keeping the group, and
completes; otherwise it keeps waiting. ResetGroupSync and the End operation set the actor’s posted value to
zero.
Once per world-loop iteration, after the family system update, every channel is updated. The channel carries the mark when every non-excluded member’s posted value has its mark. The channel then releases a group: it scans cyclically over 0 through 127 for a value that some non-excluded member holds (its posted value without the mark; a member that has not posted holds zero, so zero counts), starting at the previous released byte itself. Because a marked released byte is at least 0x80, a marked release starts the scan just after its group, while an unmarked release starts at its own group. The first value found, combined with the mark, becomes the released byte; when none is found the released byte is just the mark. A member that completed keeps its group but loses its mark, so the channel advances to the next posted group unmarked until that member posts again or ends.
User synchronization channels
User channels have separate member lists and released bytes from group channels. Actors register in the channel named by data byte 30 at creation, with a posted value of zero. Lookups use the actor’s current byte 30; changing that byte does not register a new membership. World initialization clears both channel systems and enables them. World teardown disables them, frees their member lists and clears their released bytes.
SyncWithUser(value) completes immediately when synchronization is disabled,
the current user channel is zero, or the actor has no member entry in that
channel. Otherwise each invocation stores the low byte of value OR 0x80
in its member entry. It then stays pending if any member of that user channel
has data byte 43 equal to zero. If every member has nonzero byte 43, it compares
the channel’s released byte, zero extended, with the full operand value. Equality
clears bit 7 of the caller’s posted value and completes; inequality stays pending.
The world-loop channel update processes each user channel immediately after its
group channel. It builds a presence table indexed by each full posted byte of
members whose data byte 43 is zero. Starting at the previous released byte, it
checks up to 256 candidates. After each unsuccessful check the candidate becomes
(candidate + 1) & 0x7f. The first present candidate becomes the released byte;
if no candidate is present, the released byte becomes zero. It does not add a
mark or require all members to have posted. In particular, the initial candidate
can be above 127, while every subsequent candidate is at most 127.
ResetUserSync and End clear the caller’s posted user value to zero without
changing the released byte or removing membership. They use the current byte 30
and do nothing when synchronization is disabled, that channel is zero, or the
caller has no member entry there. ResetGroupSync uses the equivalent guards
and current byte 29 for its group member entry.
ResetGroupSync and ResetUserSync take no arguments. They read bytes 29
and 30 respectively through the receiver’s control record before testing
whether synchronization is enabled. Neither validates the receiver or control
pointer or uses the selected player. Each member lookup searches its own
current channel’s list in link order and stops at the first matching actor
identity. The user reset uses the user-channel heads independently of the
group-channel heads. If duplicate entries exist, only the first matching
member’s posted byte is cleared. The other member fields, links, channel
heads, enable word and actor data remain unchanged. Neither native assigns
a defined success result. Both preserve the incoming non-result general
registers and leave argument-stack cleanup to their callers.
PushAnimation and TaskStop: animation ownership
Each task slot saves and restores its native handler state independently.
Multiple PushAnimation slots therefore retain separate not-started/started
phases while requesting and observing the same receiving actor’s animation.
Each invocation sees animation changes made by earlier calls; the animation is
not copied into the task slot.
TaskStop clears matching operand slots and their blocking and pause flags.
It does not invoke the native handler or restore earlier animation state.
Stopping a PushAnimation task therefore leaves its animation request and
current playback state in place.
PushControlAnimation(group, count) is a task with the same two phases as
PushAnimation. Not started, it requests the group through the ordinary
request unless the receiver’s current group already equals it; in that case it
sets the loop count and becomes started. Both outcomes keep the task pending.
Started, it completes when the current group no longer equals the group.
Otherwise it turns the receiver and stays pending: the heading becomes the
heading minus the scaled right turn amount plus the scaled left turn amount,
modulo 2048, using the ground-turn accumulators
and frame scale. The accumulators update every input update, whether or not
the player controller runs.
Player alignment ownership and targets
PlayerLineUp(angle, x_offset, z_offset) moves the selected player relative to
its receiving actor. Both distance modes move the player; neither moves the
receiver. Its return value is also stored in the shared player-lineup result
used by activation admission.
The first invocation in a native handler state selects the distance mode.
It compares the shared integer three-axis distance between the effective player
and receiver positions against SYSTEM.INI lineup_distance * 256. A strictly
greater distance selects approach mode; equality selects close alignment.
Subsequent invocations retain that choice in the native state. The initialized
marker, mode, fallback flag and retained close-alignment reference belong to
that handler state. Active task slots save and restore them independently.
They are separate from the shared activation-admission result.
For the normal target calculation, add angle to the reference actor’s heading
and wrap to 2048. Rotate the supplied horizontal offsets by that heading and
add the reference actor’s local X/Z. The final player heading is the rotated
heading plus 1024, wrapped to 2048. Approach mode uses the current receiver as
reference. Close alignment retains its first reference in native state and
uses that actor on later invocations. The target position is recalculated on
each invocation; it is not a captured destination.
In the normal path, approach mode faces the destination before forward movement. Close alignment
faces the final heading first, then chooses forward, sideways or backward
movement from the relative destination bearing. Reaching the destination does
not by itself finish alignment. The player must finish turning to the final
heading and request its default animation. A completed alignment clears the
player’s alignment flag and returns zero; ongoing alignment returns one.
In approach mode, a retained fallback flag or a nonzero packed collision-response
word masked by 0x01e2 switches to facing the receiver’s current local X/Z.
Completion in that branch also sets the native return-state byte to 6. Close
alignment’s fallback instead turns to the final heading derived from its
retained reference, requests the default animation, and sets that byte to 6.
Both completion branches clear the player’s alignment flag.
Approach mode’s movement step runs after the player has finished facing the
destination and the movement-distance check permits movement. It is one call of
the player’s WalkForward helper with duration 1, preceded by a saved copy of the
player’s position, step counter and animation state. If the bearing from the
new position to the destination, relative to the player’s heading, then lies from
256 through 1792 (the destination is no longer ahead), the step is undone, the
player’s X/Z are set to the destination and the retained fallback flag is set.
After the movement, a nonzero collision-response word masked by 0x01e2 together
with a collision counter above 8 also sets the fallback flag. Approach mode
returns one on every call that does not complete.
Close alignment’s movement step runs when the player has finished turning to the
final heading and the movement-distance check permits movement. Let b be the
player’s heading minus the heading of the displacement to the target (the heading
the facing step turns to), wrapped to 2048 (the same quantity approach mode tests). The helper is
chosen by b: right for 257 through 768, backward for 769 through 1280, left for
1281 through 1792, forward otherwise. Each helper first saves the player’s
position, packed step counter, animation state and the two animation words that
follow it, then calls its stepper with duration 1: MoveRight, MoveLeft,
WalkForward or the walking backward step. It keeps the step when its test
passes, otherwise it restores everything it saved, clears that direction’s shared
called flag, sets the player’s X/Z to the target and reports failure, which sets
the retained fallback flag. With b' the bearing measured again after the step,
the keep tests are: right, b' from 257 through 767; left, b' from 1281
through 1791; forward, b' above 1792 or below 256; backward, the horizontal
distance to the target no greater than before the step. After the step, a nonzero
collision-response word masked by 0x01e2 also sets the fallback flag. Close
alignment returns one on every call that does not complete.
PlayerPickupLineUp and PlayerParallelLineUp
PlayerPickupLineUp(angle, x_offset, z_offset) and PlayerParallelLineUp(angle, x_offset, z_offset) are
tasks that move the selected player relative to the receiving actor; ObjectPickupLineUp and
ObjectParallelLineUp run the same two movers on another actor. They do not store the shared
player-lineup result. Each invocation first pushes a collision query frame for the player (position, a
radius from the player’s model record scaled by a constant, or 200 when it has none,
height word) and pops it afterwards. Unlike PlayerLineUp the offsets are added to the receiver’s local X/Z
unrotated: the target is the receiver’s position plus the offsets (Y from the receiver), and the target
heading is the receiver’s heading plus angle, wrapped to 2048.
The pickup mover returns zero when done and one while continuing. On its first invocation (elapsed time zero)
a blocked straight path to the target (a segment of at most 0x20000 units, tested against objects but not
the receiver or the player) selects the parallel mover for the rest of the task; the choice is retained in
the task’s native state. Otherwise each invocation, in order:
- A nonzero packed collision-response word with an age above 8 clears nothing but abandons the task: return state 6, return zero.
- The player’s roll is leveled and the player faces the target; while still turning it returns one.
- The facing arrival test snaps onto the target: all four called flags are cleared, the default animation is requested and the task returns zero.
- When the movement-distance check permits movement, one forward close step toward the target runs (as in close alignment); a failed step also clears the called flags and completes as in 3.
- Otherwise a stale collision response abandons the task as in 1, clearing the player’s alignment flag, and the task returns one.
The parallel mover works from the final heading angle + 1024 (wrapped): when the player’s heading is more than
768 from it the player turns to face angle + 2048, backs toward the target with a backward close step while the
movement-distance check permits it, and completes once the backward step fails and the turn to the final heading
has finished (called flags set); otherwise it turns to the final heading (returning one while turning), snaps on
arrival (called flags set, default animation requested, return zero) and otherwise takes the close step that the
relative bearing selects, as in close alignment, returning one unless the collision response is stale.
Shell execution context transitions
The shell-context enter operation does nothing when the shell system is disabled
or the context word is already one. Otherwise it writes one and saves three
shared execution words. The leave operation does nothing when the shell system
is disabled or the context word is already zero. Otherwise it writes zero,
clears the current-shell reference and restores those three execution words.
These transition guards are distinct from IsShellScript returning the context
word unchanged when its enabled, nonempty-list and nonnull-receiver gates pass.
ObjectParallelLineUp target binding
ObjectParallelLineUp(angle, x_offset, z_offset) moves its receiving actor
relative to the current task-target actor. It reads that target through the
position and orientation accessors. The destination is target X plus x_offset,
target Y unchanged, and target Z plus z_offset. The destination heading is the
target heading plus angle, wrapped to 2048. The coordinate additions use
32-bit wrapping arithmetic. The wrapper forwards these values to the parallel
mover and returns its result unchanged. It does not update the shared
player-lineup result. The wrapper provides no unavailable-target fallback.
Alignment distance thresholds
Alignment position tests use local Q8 X/Z and the shared integer distance function with Y set to zero. They select a speed byte from the current animation group and actor attributes. Missing animation or attributes selects zero. Other groups also select zero.
| Current animation group | Speed attribute index |
|---|---|
| 18, 30, 142 | 8 |
| 19, 143 | 9 |
| 20, 21 | 12 |
| 22, 23 | 13 |
| 24 through 27 | 10 |
| 31 | 19 |
| 92, 93 | 17 |
Let speed be that byte and scale the Q8 frame scale. The alignment arrival
test uses (((2 * speed - 1) << 8) * scale) >> 8. The facing helper’s arrival
test instead uses (((speed + 1) << 8) * scale) >> 8. Products wrap to signed
32 bits and the final shifts are arithmetic. A horizontal distance less than
or equal to the threshold snaps local X/Z to the target and registers a map
update. Y remains unchanged. With speed zero and a positive ordinary scale,
the first threshold is negative and cannot admit a nonnegative distance.
The facing helper’s test can still snap in that case.
The movement-distance check is a third test. It uses
((speed * 512 - 1) * scale) >> 8, with the same wrapping and signed shift.
It permits a movement attempt for actor state 7, speed zero, a nonpositive
threshold, or a distance strictly greater than the threshold. It does not
snap or register the actor.
Alignment turning uses the support-response turn rules, including selected-player camera updates. Returning to the default animation reports success on every normal return; it does not wait for playback to finish.
Player movement, turn, speed, arctangent and location-distance queries
PlayerArctan() returns the arctangent angle (0 through 2047) of the horizontal displacement from the
selected player to the receiver (or to the stored fallback position when no player is selected), the
negation modulo 2048 of the bearing PlayerLooking uses.
PlayerMoved() compares the selected player’s current local X, Y and Z position with its stored
previous position. It returns 1 if any component differs and 0 otherwise. It does not require a model
handle. The receiver is not the actor queried.
PlayerTurned() returns 1 when any of the selected player’s three angles differs from the angles saved
at the ordinary update’s entry, otherwise 0. PlayerTurnedDirection() returns the sign of the change of
the player’s negated yaw ((2048 - yaw) & 2047) since the saved one: 1 when it grew, -1 when it shrank
and 0 when unchanged, with the sign inverted when the change exceeds 1024. Both return 0 for an actor
without a model handle.
PlayerMovementSpeed() returns an attribute byte of the selected player selected by its current animation
group: groups 18 and 30 attribute 8, 19 attribute 9, 20 and 21 attribute 12, 22 and 23 attribute 13, 24
through 27 attribute 10. The selected attribute is an unsigned byte, returned without scaling. The query
uses the selected player rather than its receiver and does not inspect position changes or require a
model handle. Other groups, and a player without animation state or attributes, return zero.
DistanceToLocation(location) returns the rounded Euclidean length, in Q8 units, from the receiver’s
position (its linked position when it has one) to the location, selected as in MoveToLocation.
PlayerOnScape
PlayerOnScape() returns 1 when the selected player’s last ground-response record is a terrain contact and
0 otherwise. The ground update clears the record’s kind byte, sets it to 0x40 for a terrain contact and to
0x20 for a model or actor face, and retains the record for the selected player after its update.
PlayerStatus
PlayerStatus() classifies the selected player and returns the class of the first matching rule, or
zero. The rules test, in order: the queued-motion byte (1 gives 1, 2 gives 2, 8 gives 3, 3 gives 4),
the falling byte (2 gives 5, 8 gives 6, 3 gives 7), a nonzero sliding byte (8), the living-state
byte (10 gives 9, 11 gives 10, 20 gives 11, 21 gives 12), the current animation group (18 or 30 gives
15, 19 or 23 gives 16, 24 or 25 gives 17, 26 or 27 gives 18, 20 or 21 gives 19, 22 gives 20), and last
whether the player’s local position differs from its position saved at the update’s entry (21).
Classes 13 and 14 repeat the living-state tests of 9 and 10 and are never returned.
BounceObject
BounceObject(power, speed, steer) is a task that launches its object (the selected player in
installed content: BounceObject(160, 40, 1)) up and forward, then leaves the descent to the ordinary
falling controller. It reports one while it runs and zero when done. The lifecycle byte moves through
30 (entry requested), 31 (rising) and 32 (falling). The first invocation refuses, ending the task with no
effect, when the item system is not ready, the lifecycle byte is 30 or 31 or in 10 to 19, the surface
is deep water (2 or 4), or power is zero. Otherwise it clears the falling, queued-motion and sliding
bytes and the first two motion-vector words, sets lifecycle 30 and requests animation group 114 with a
weapon held, 66 with a burning torch, otherwise 65.
Every invocation then runs, in order:
- While the lifecycle byte is 30: if the current animation group is not 114, 66 or 65, clear the same motion bytes and words and report one. Otherwise set lifecycle 31 and set the lift to attribute 32 (jump height in map units) times 256.
- If the object is the selected player and
steeris nonzero, subtract the ground-turn delta (scaled right amount minus scaled left amount) from the heading, modulo 2048. - Move forward along the heading by
((128 - speed) << 8) / 3(truncating division) times the frame scale, shifted right by 8, in Q8 units. - While the lifecycle byte is 31: subtract
lift * scale >> 8from Y and((256 - power) << 4) * scale >> 8from the lift. A lift above zero requests group 112 with a weapon held or the stance bit0x04set, 38 with a burning torch, otherwise 39, and clears the motion bytes and words as above. A lift of zero or less starts an ordinary fall (fall origin Y plus attribute 32 times 256, timer 30, falling byte 1), requests group 113 (forced) with a weapon held or the stance bit set unless the current group is already 115, otherwise group 57 with a burning torch or 56 (forced), and sets lifecycle 32. Either way the call reports one. - Otherwise, when the falling byte is zero set lifecycle 0 and report zero; else report one.
The lift shares the fall-origin word. “Weapon held” is the ordinary holding-weapon predicate.
The ordinary update’s ground response begins with a handler for the actor’s lifecycle states 27 through 32. For states 30 and 31 it reports the update handled, so the ground response does nothing (no support query result is applied, no fall starts). State 32 is not handled: the ordinary falling and landing response runs, and the falling byte it clears ends the task as above.
PlayerFaceObj
PlayerFaceObj is a task that turns the selected player to face its receiving actor. It
reports one while the player is still turning and zero when done, and each invocation sets or
clears the player’s alignment flag accordingly. The alignment flag is the hook-disabled bit that
DisableHook sets, so the player’s native hook (its controller) does not run while the player is
being aligned.
An invocation first checks the player’s packed collision-response word (the body-response byte, the transition byte and the two auxiliary bytes). When the word is nonzero and the collision-response age is above 8, it sets the native return-state byte to 6, clears the alignment flag and reports zero without turning.
Otherwise it applies the alignment roll leveling, then the facing step. Using the player’s
alignment speed, a horizontal distance (Y zero, local Q8 X/Z)
to the receiver at or below the facing arrival threshold snaps the player’s X/Z to the
receiver’s, registers a map update and reports zero. Farther away, the target heading is the
negated arctangent angle of the horizontal displacement from the player to the receiver
(the heading the PlayerLooking bearing reports), wrapped to 2048, and the player turns to
it by the support-response turn rules: zero when the
heading already matches or the turn snaps to it, otherwise nonzero. A zero result clears the
alignment flag and a nonzero one sets it, and the task reports that result.
IsOnEdge receiver query
IsOnEdge() is a zero-argument function. A null actor receiver returns zero.
Otherwise it reads the receiving actor’s unsigned state byte and returns one
for values 10 through 19 inclusive, zero for every other byte value. It reads
the receiver directly rather than selecting the player, consulting a control
record or querying collision geometry. The query leaves actor state unchanged
and has no ordinary script/task enable gate.
Receiver AI type query
MyAiType() returns the receiving actor’s lifecycle selector as an unsigned byte
value from 0 through 255. It does not query the selected player or the adjacent
AI mode, and does not change the actor’s state.
Receiver AI type setter
SetAiType(value) is a one-argument function that replaces the receiving
actor’s lifecycle-selector byte with the low eight bits of value and returns
zero. It writes the same byte queried by MyAiType, leaving the adjacent AI
mode and other actor fields unchanged. The native writes directly into the
receiver; it does not require or read a separate control-record pointer, invoke
an AI service or test ordinary script/task enable state. It does not validate
the receiver pointer.
Receiver AI mode query
MyAiMode() returns the receiving actor’s AI mode as an unsigned byte value
from 0 through 255. It reads the mode rather than the adjacent lifecycle
selector, does not query the selected player and leaves actor state unchanged.
Receiver AI mode setter
SetAiMode(value) is a one-argument function that replaces the receiving
actor’s AI-mode byte with the low eight bits of value and returns zero. It
writes the byte queried by MyAiMode, leaving the adjacent lifecycle selector
and other actor fields unchanged. The native writes directly into the receiver
without reading a separate control-record pointer, invoking an AI service or
testing ordinary script/task enable state. It does not validate the receiver
pointer.
GetAttribute, SetAttribute, GetMyAttr, and SetMyAttr
GetAttribute reads an unsigned byte from the receiving actor’s attribute table and returns zero when the table is absent. SetAttribute returns zero. When the table exists, it writes the low byte of the supplied value. Writing attribute 6 also copies that byte to attribute 115; writing attribute 1 also copies it to attribute 119. These two handlers do not check the attribute index range.
GetMyAttr reads an unsigned byte from the receiving actor’s attribute table. It returns zero when the table is absent or the index is outside 0–255. SetMyAttr returns zero and ignores an absent table or an out-of-range index. A valid write truncates the supplied value to its low byte. Attribute 44 also stores that truncated byte as the combat death-default group when the actor has a combat record and its object-update exclusion is nonzero. No other index has a write-time effect: attachment, rotation, family and model attributes are read where they are used, and a write neither rebinds the family nor refreshes the model.
Initial attribute copy and defaults
Loading an actor creates an independent 256-byte copy of its definition’s RAAT attributes, then applies these adjustments before registering its groups:
| Attribute | Initial adjustment |
|---|---|
31, attr_jump_time | A signed byte value at or below zero is replaced by the low byte of the effective system jump time. Thus zero and byte values 128–255 use the fallback. |
32, attr_jump_height | Zero is replaced by bits 8–15 of the effective system jump height. The shipped value, 80, therefore leaves the attribute at zero. |
35, attr_draw_frame | Zero becomes 2. |
34, attr_sheath_frame | Zero becomes 10. |
25 and 26, attr_master and attr_slave | Both become zero when [debug] disable_family is exactly 1. |
115, attr_reserved_weight | Copies attribute 6, attr_weight. |
119, attr_reserved_collide | Copies attribute 1, attr_collide. |
The effective jump settings come from [system] jump_time and jump_height in
SYSTEM.INI. Values below 1 use 6 and 80 respectively.
These load-time defaults do not run again on ordinary attribute writes.
Actors loaded through the animation model-table path also replace attribute 125,
attr_mess_group, with the next value from a shared counter. The counter returns
its current value, then increments modulo 256. Opening an actor-definition map
resets this shared counter to zero before reading the map records. The counter
is shared across actor definitions and instances, rather than restarting for
each definition. The static-model shortcut and actors without models do not
take this assignment path.
A type-1 MPOB placement has a subsequent model fallback. It runs only when
actor-definition loading left both the animation model table and the static
model absent, and the placement’s model name is nonempty. If the placement’s
is_static byte is exactly one, it loads a static model without assigning a
message group. Any other byte value builds an animation model table, using
that byte as its frame count. When the actor has an attribute table, this
fallback replaces attribute 125 with the next shared counter value. A zero
frame count still takes the animation-table branch.
SetAttribute and SetMyAttr: default-group side effects
SetAttribute first writes the low byte to attribute 44. Only when IsDead
returns true does it release the actor’s two held walk-node reservations and
store the full supplied value as the combat death-default group. A node’s ownership byte is cleared only when it matches
the low byte of the actor identifier; both actor-held node indices are then
cleared. This operation does not reset the animation cursor, current group,
or loop countdown.
SetMyAttr writes the combat death-default group only when a combat record
exists and the object-update exclusion is nonzero. It stores the truncated
attribute byte and does not release walk-node reservations. This exclusion
check is distinct from the IsDead predicate used by SetAttribute.
The combat death-default group and the animation playback default are separate state. Ordinary actor updates select the playback default using actor state; these attribute writes do not directly replace that selection step. See ordinary default selection.
FaceObject task entry
FaceObject(target) is a one-argument task. It first reads the receiver’s
attribute byte 15. A zero byte returns zero without reading the target or
calling the shared facing wrapper. A nonzero byte forwards the receiver and
target to that wrapper and converts its result to a Boolean: zero stays zero,
and every nonzero result becomes one.
The wrapper first passes the target’s local X/Z coordinates to the facing-arrival test. A nonzero arrival result returns zero without calculating a bearing or invoking the turn helper. Otherwise it subtracts the receiver’s local X/Z from the target’s local X/Z, with 32-bit wrapping, and passes the differences to the bearing helper. It negates the returned bearing and masks it to 2048 heading units before calling the ordinary turn helper for the receiver. The wrapper forwards that helper’s result unchanged; the task entry applies the Boolean conversion.
FaceLocation timed facing
FaceLocation(location, deadline) is a two-argument task. The receiver’s location
table begins with a signed upper index bound followed by three-word position
records. A nonnegative index at or below that bound selects its record. A null
table, negative index or index above the bound selects the receiver’s inline
location instead.
The task subtracts the receiver’s local X/Z from the selected location’s X/Z
with 32-bit wrapping and calculates a planar bearing. It computes
delta = (-heading - bearing) & 2047. A zero delta returns zero immediately.
For delta at most 1024, the turn distance is delta and the update adds a step
to the heading. For a larger delta, the distance is (-delta - 1024) & 1023
and the update subtracts the step. The exact half turn uses the addition branch.
The divisor is the signed magnitude of the wrapping difference between the
shared tick counter and deadline, with a zero divisor replaced by one.
Magnitude uses wrapping 32-bit negation, so the minimum signed value retains its
bit pattern. The step is the signed quotient of distance << 8 by that divisor,
truncated toward zero, then arithmetically shifted right by eight. A zero step
returns zero without writing the heading. Otherwise, heading arithmetic wraps
at 32 bits and the stored heading is not masked back to 2048 units.
After a nonzero step, the task returns one only when wrapping deadline - 1
is greater than the current tick counter as a signed 32-bit value. Every other
return is zero. The task changes the heading directly rather than invoking the
ordinary turn helper.
PlayerDistance and PlayerFacing
PlayerDistance() returns the sum of the absolute X, Y and Z differences
between the selected player’s current position and the receiver’s effective
position. Positions and the result use the runtime coordinate scale of 256
units per world unit. Without a selected player, it uses the global fallback
position. A global override selects the fixed result 9999999 instead of
calculating distance. The override is the nonzero neversaydie cheat state.
Cheat initialization clears it before loading the optional REDGUARD.CHT file;
without that file it starts disabled.
Fresh ordinary actor allocation starts without a linked-position record. This record is separate from the attribute-based master/slave attachment.
Effective receiver position follows the same selection used by
positional sound: a nonzero attr_slave
selects attached position; otherwise a valid linked positioned record supplies
position, with the receiver’s ordinary position as the final fallback.
AcuratePlayerDistance() is PlayerDistance with the rounded Euclidean length of
the same three-axis difference instead of the sum of absolute differences. It
uses the same positions, the same fallback position without a selected player,
and the same fixed 9999999 result under the neversaydie override. The length
follows the node-marker distance arithmetic.
PlayerFacing(offset, tolerance) compares orientations. It adds offset to
the receiver’s effective yaw, wraps the sum modulo 2048, and compares it with
the selected player’s current yaw. The signed difference is wrapped into
[-1024, 1024]; the result is one when its absolute value is less than or equal
to the signed tolerance, otherwise zero. The offset and tolerance are full
signed 32-bit arguments. The receiver’s attached yaw is used when attr_slave
is nonzero; otherwise its ordinary yaw is used. This function requires a
selected player. See coordinate and heading units.
PlayerLooking(tolerance) tests the selected player’s horizontal orientation
relative to the receiver. Subtract the receiver’s effective X/Z position from
the selected player’s ordinary X/Z position, retaining signed 32-bit differences.
Convert that displacement to the quantized actor heading.
This query retains signed 32-bit wrapping absolute values and signed comparisons
during bearing reduction. The minimum signed coordinate difference therefore
does not behave like a widened positive magnitude.
Compare it with the selected player’s current yaw plus 1024, wrapped modulo 2048.
The result is one when the absolute wrapped angular difference is less than or
equal to the signed 32-bit tolerance, otherwise zero. A negative tolerance always
rejects. The query requires a selected player and uses the same effective receiver
position selection described above. It does not perform a visibility or line-of-sight
query.
PrintSingleParm(value) formats the receiving script’s name (or Unknown script
when its script handle cannot be resolved) and the signed integer as
<name> <value>. It writes the text to the first free of the console’s 66 text
lines with a copy bound of 255 bytes, drops the text when no line is free, and
returns zero.
PrintStringParm(text) uses the same script-name prefix, message slots, copy
bound, full-queue behavior and zero return. It formats the supplied string as
<name> <text> instead of interpreting its value as an integer. The handler uses
the string value already supplied by native-parameter evaluation; it does not
perform another string-table lookup. See native string parameters
for their source context.
PlayerInSight() is true when the selected player is within 262144 units of the
receiver and nothing blocks the line between them. The distance is the rounded
Euclidean length of the difference between the two actors’ local positions, with
no effective or attached position substitution; a longer distance is false and
exactly 262144 passes. The segment between the two local positions, in whole map
units, is then tested against each entry of the retained candidate model list, in
order, exactly as the combat “In sight” test described under the combat decision rules does. An entry that is not
hit, or whose owner is the selected player, does not block; any other hit makes
the result false.
The candidate model list is one engine-wide list that every candidate gather replaces. A receiver that gathers no candidates of its own (an actor without collision) tests against the list the most recent gather by any actor left, which is empty before the first gather.
AtNodeLocation
AtNodeLocation(location, distance) reads the receiver’s indexed location (the same location table the other
Location functions use) and returns one when the receiver is at it, otherwise zero. The height test comes
first: the location’s Y minus the receiver’s local Y, shifted right by eight as an absolute value, must not exceed
half of the receiver’s total Y extent (halved toward zero); otherwise the result is zero. Then the horizontal (X and Z) distance between the receiver’s local position and the location, by
the two-component integer distance routine, is compared with the signed distance argument with an inclusive
bound, in the same 256-per-map-unit scale.
AtNodeMarker: node-marker distance
AtNodeMarker(marker, distance) returns one when the receiver’s ordinary local
position is within the supplied distance of the indexed marker, otherwise zero.
It uses all three axes and does not select attached or linked positions.
Marker coordinates are converted from map units to runtime units by a left
shift of eight bits; coordinate subtraction retains the low 32 bits.
The distance is Euclidean. Each signed difference is first stored as a
single-precision float. The sum of squares passes through a double-precision
store before an extended-precision square root; the root is stored at single
precision and converted to a signed integer with nearest-even rounding. Rounding
the root to double precision before the single-precision store can change the
result at a single-precision midpoint. The result is compared with the
signed distance argument using an inclusive bound. The distance argument
therefore uses 256 units per map unit.
Marker lookup is fatal for an out-of-range or absent marker. While a world is loaded, the lookup reads that map’s own marker table: the index must be below the map’s marker count and the presence byte must equal one. A fixed 128-slot table that accepts any nonzero presence byte is read only when no loaded map is active. The marker argument is an unscaled signed index, distinct from the distance units.
Navigation map validation
Walk-map construction clears its runtime disabled byte, so newly loaded maps are enabled. This byte is separate from installed node reservation bytes.
DisableNodeMaps() and EnableNodeMaps() are zero-argument functions that
traverse the loaded walk-map list. Disable writes one to each map’s runtime
disabled byte; enable writes zero. They leave list links, node reservations and
actor navigation records unchanged. An empty list is allowed. Both return zero.
Common actor initialization clears the current walk map and sets the held current-node, next-node and final-destination indices to -1. Later actor loading or navigation can assign a map and node; raw allocation zeroes do not describe these initialized navigation indices. The previous-next record starts at allocation zero; common initialization does not overwrite it.
Map placement loading supplies the authored local position before loading the actor. Actor loading then assigns its current node and map through the general nearest-node query, using each local coordinate shifted right by eight. This initial association does not add the actor’s Y extent and does not filter node reservations. It is separate from the combat nearest-free-node search.
An actor without a current walk map clears its current-node, next-node and final-destination indices to -1. An enabled map remains valid when each of those indices is below its node count; the -1 unset sentinel is allowed.
An index at or above the node count invalidates the map. Validation releases held current/next reservations, clears all three indices, and removes the map. A disabled map also releases held current/next reservations and removes the map, but retains the final-destination index until the next validation observes the absent map.
Reservation release clears a node’s ownership byte only when it matches the low byte of the actor’s runtime identifier. It clears the actor’s current and next indices even when another actor owns the node. An index at or above the node count does not address a node during release.
Current-node assignment releases the held next node before the held current node. When a next node was held, it first copies that index to the actor’s previous-next record. Next-node assignment instead releases current before next and leaves the previous-next record unchanged. Both operations clear the released indices even when another actor owns their nodes.
After releasing, assignment claims the requested node only when its ownership byte is zero or matches the low byte of the actor identifier. A successful claim writes that low byte and the corresponding current or next index, and returns one. A rejected claim returns zero with both held indices cleared. With no map, both held indices are cleared and assignment returns zero without changing the previous-next record. Neither assignment changes the final destination.
Validation and held-node release preserve the previous-next record, including when they invalidate or remove the map.
Nearest free walk node
Combat perception can search all loaded walk maps for a free node before navigation begins. This differs from the general node-selection query because only nodes with a zero reservation byte qualify. The search reads the actor’s query position, shifts each coordinate right by eight, and adds the actor’s positive Y extent to the shifted Y coordinate. Node X/Z are unsigned 16-bit coordinates and node Y is signed 16-bit.
Maps and nodes retain their loaded order. The search minimizes Manhattan distance with an initial exclusive bound of 4096. Equal distances retain the first candidate. An improving candidate below distance 12 returns immediately, even if a later node would be closer. The shared winning map is cleared before searching. A miss leaves it absent and returns node index -1.
Nearest node within one walk map
Point navigation can search its current walk map for the destination node. The search takes map-unit coordinates, ignores reservation bytes, and rejects an absent or disabled map. It minimizes Manhattan distance with an initial exclusive bound of 4096. Equal distances retain the first node, and an improving candidate below distance 12 returns immediately. The shared selected map is cleared first. A hit sets it to the supplied map; a miss leaves it absent and returns -1. This search does not select a different map.
Walk-route search and next-hop selection
Walk-map loading sizes the shared route queue and visited array to the largest loaded map’s node count plus eight. Search starts with the current node at depth one. An unset current node returns -1; an equal current and destination node returns that node before testing map availability. Other searches reject an absent or disabled map.
The forward search processes its queue in order and visits each node’s links in authored order. A link qualifies when its unsigned cost is at most 2048, its target passes node admission and has not been visited, and the expanded node’s reservation byte is either zero or equal to the requesting identifier. The reservation test belongs to the expanded node, not the linked destination. Qualifying targets are marked visited and queued with the parent’s depth plus one. Finding the destination starts backtracking immediately. Exhausting the queue or its capacity returns -1. Costs filter forward traversal; they are not accumulated as a shortest-path distance.
Backtracking does not retain the parent that discovered a node. For each node, it scans earlier queue entries in reverse order for depth one lower. It looks up the first authored link from each candidate to the current node. Zero cost is not a predecessor. Among nonzero links, the smallest low fourteen cost bits wins; equal costs retain the first reverse-scan candidate. Backtracking repeats until reaching the initial queue entry and returns the next node after it. Failure to find a predecessor returns -1. The forward admission filters are not reapplied during this predecessor scan. Duplicate links can therefore affect backtracking through their first authored cost even when another duplicate caused forward admission.
Each predecessor scan clears two shared link-result flags. An improving link sets the first flag when cost bit 14 is set and the second when bit 15 is set. These flags are not cleared by a later improving link within that same scan. The final scan’s flags accompany the selected next hop. A shared backtracking-inhibition word equal to one returns -1 and clears both flags before scanning. Other values allow backtracking.
When the shared player-proximity exclusion switch is zero, node admission returns true without reading the player. When enabled, a valid node in a present map is rejected only when its rounded three-dimensional distance is strictly below the shared signed threshold. Node X/Z are unsigned and node Y is signed. The player coordinates are the local actor position shifted right by eight. An absent map or an index outside its node range returns true without a distance calculation.
Point-navigation command state
The point-navigation wrapper copies four retained actor values into shared command scratch before the navigation core runs and copies them back afterwards: a 32-bit command value, a 16-bit command word, a phase byte and a counter byte. The copy-back occurs for either navigation-core return value. Alternating actors therefore retain each actor’s own four values rather than inheriting the last actor’s scratch contents.
After copying command state back, the wrapper measures rounded horizontal Q8 distance between the local position and the retained previous local position. It compares that distance with the frame scale shifted left by 13 with 32-bit wrapping, interpreted as signed and arithmetically shifted right by eight. A strictly greater distance restores all three local coordinates from the previous position. Equality retains the new position. This rollback does not undo command-state copy-back or change the navigation core’s return value.
Common actor initialization sets the command word to 0xFFFF. It leaves the 32-bit command value, phase and counter unchanged; fresh allocation clears those fields before common initialization. A separate shared 32-bit pause delay is outside these copies. Loading or saving an actor’s command values does not reset that delay.
Native task resumption separately loads command scratch from the actor’s six
native task slots. Each slot retains four 32-bit values, including the pause
delay and packed command word/phase/counter. Saving a task copies those values
back to the same slot. Two of these words hold general native continuation
values; FxPhase uses them for its accumulator and direction. The point command’s
separate 32-bit value is outside these four saved words. Active task resumption
replaces that separate value with the task call count times frame scale, shifted
right by eight, before evaluating its operands. The point-navigation actor fields
and the native task continuation are therefore distinct retained owners of values
used during the same command invocation.
Before evaluating native operands, a statement call clears all four retained scratch words. A direct function expression also clears all four. An object-targeted function expression clears the first two words and the packed word/phase/counter, while retaining the point command’s 32-bit value. This encoding distinction applies even when the explicit object target is Me. The direct native-hook dispatcher performs neither setup reset. A newly allocated task slot saves the scratch resulting from its first native invocation.
The six-slot resume pass visits slots in order. An empty or paused slot saves the current shared scratch into that slot without invoking its native call. An active slot loads its retained scratch before invoking the native call. A still-pending call saves the resulting scratch. A completed call clears its call pointer and blocking flags without saving the resulting scratch back to its slot. Its resulting shared scratch remains available to later slots and actors. Each invoked slot increments its unsigned 16-bit call count once. The pass sums the blocking flags of still-pending calls with byte wrapping.
The ordinary actor main stage performs native-hook and combat maintenance before its body/task processing. Task resumption can therefore replace command scratch for a later actor even when that later actor has no active task of its own. The direct native-call dispatcher does not clear the shared pause delay.
Point navigation validates the actor’s map and held-node indices before checking the pause delay. A nonzero unsigned 32-bit delay consumes the shared nominal tick count with saturation at zero and returns pending for that invocation, including when the subtraction reaches zero. Routing resumes on a later invocation. The delay is shared across actor invocations, so another actor can consume the remaining delay. This delay is distinct from the signed combat lost-sight timer.
Point-navigation completion stores the low byte of its result code in the shared navigation result byte and releases the actor’s held current and next walk-node reservations. The map and final destination remain retained. It changes lifecycle selector seven to zero, leaves other lifecycle selectors unchanged, assigns attribute 1 to 13, and returns zero. This completion helper leaves the actor’s four retained point-command fields and shared command words unchanged; the enclosing wrapper still performs its command copy-back.
Before ordinary point movement, the blocked-route counter increments with unsigned byte wrapping when the collision-response auxiliary byte has bit 7 set, a map is present, and a next node is held. A resulting counter greater than eight selects phase 24. Otherwise the counter resets to zero.
Before the blocked-route counter, a body-response mark of bit 2 (a contact with another actor) whose overlap contact identifier is not one, outside phase 27, draws one raw engine random value. Its low two bits select: 1, the held next node becomes the previous next node and phase 27 is selected; 2, the shared pause delay is set to 36 and the call returns pending; otherwise nothing changes.
Collision response marks are then read. When the collision-transition byte’s bit 0 is clear, any of auxiliary-byte bits 1 or 7, or body-response bits 1 or 3, releases the actor’s held walk nodes and selects phase zero, whatever phase the blocked-route counter selected. When the transition bit is set, the held nodes are released too; a collision-response age below 16 then restores the three local coordinates from the actor’s retained stable position, and an older response selects phase 24 unless the phase is already 24 or 25.
Phase 24 is the back-off after a blocked route. It releases the actor’s held walk nodes, then
steps backward for one update with WalkBackward when attribute 93 is zero and with MoveBackward
otherwise (both with a frame count of one, and WalkBackward is MoveBackward in deep water),
selects phase 25 and stays pending. Phase 25 changes nothing until the actor’s current animation
group equals its default group, then selects phase zero. Both phases stay pending (lifecycle
selector seven, attribute 1 unchanged) and register the actor once for a deferred map update.
The shared player-exclusion switch starts disabled during player setup and nothing in the executable ever enables it, so walk-node admission and held-target admission never reject a node on its account. Point movement consults this switch before falling/queued-motion handling. It is the same switch used by walk-node admission; held-target admission has its own position quantization and angular checks.
A falling actor or one with queued motion assigns attribute 1 to 13, selects phase one when a next node is held or phase zero otherwise, sets lifecycle selector seven, and returns pending. This path precedes immediate arrival and default-animation admission.
A motion direction of 15 takes the same path after first calling a ledge update that acts only on the selected player (and only while its attribute 105 is not one); for every other actor that call changes nothing.
Phases 11 and 22 skip the immediate-arrival test. For other phases, a successful immediate-arrival test copies the destination’s local X and Z to the actor while preserving local Y, admits the default animation, and completes with result zero. Default-animation admission always succeeds. It requests the signed default group with modifier zero through the forced animation-request path only when SYSTEM.INI animation processing is enabled, animation state exists, the actor is not excluded from object updates, and its current group differs from its default group. It does not change the actor’s default group.
After immediate destination arrival is rejected and collision response is resolved, point phases 1 and 27 process the held next node. With no next node, they release held reservations, select phase zero and remain pending. On arrival at the held next node, default-animation admission runs before current-node assignment. The phase then becomes eight regardless of whether the claim succeeds. These pending paths set lifecycle selector seven and register the actor once for a deferred map update. Attribute 1 remains 12.
When the held next node has not been reached, phases 1 and 27 face that node.
A nonzero facing result retains the phase. A zero result selects phase two.
Only after that zero result, nonzero attribute 90 calls Rnd(attribute90).
A zero random result selects phase nine and stores (draw & 127) + 16 in the
point-command word using another shared random draw. Other outcomes preserve
the command word. These paths retain held nodes and history, set lifecycle
selector seven and register one deferred map update.
Phase nine updates the point-command word using nominal ticks. It sign-extends the 16-bit word to 32 bits, compares that value as unsigned with the tick count, and clears the word when it is smaller. Otherwise subtraction wraps into the 16-bit word. A zero result selects phase two. A held weapon also selects phase two, preserving the updated word.
An unarmed actor with a nonzero remaining word stays in phase nine. If its current animation group equals its retained default group, it consumes one shared draw. When the low five bits are all set, it draws again and masks to two bits, rejecting three until a value from zero through two is obtained. It requests animation group one plus that value with modifier zero. Other animation groups consume no idle draws. These paths retain navigation indices, set lifecycle selector seven and register the actor for a deferred map update.
Animation-stop calls in point navigation return without changing playback, actor motion or navigation state.
Held-node arrival validates the actor’s navigation and tests local horizontal Q8
distance against ((speed + 1) * 256 * frame_scale) >> 8. Equality is accepted;
products wrap at 32 bits and the shift is signed. Speed is the current animation
group’s movement speed without the immediate-destination fallback. On success,
local X/Z snap to the node’s unsigned X/Z coordinates scaled by 256, local Y
is preserved, and the actor is registered for a deferred map update. Registration
deduplicates the actor, including when the enclosing pending path also registers
it. A missing map reports held-node arrival before reading a node.
Held-node facing reads the node’s unsigned X/Z coordinates scaled by 256, subtracts the actor’s local Q8 position and turns toward the resulting horizontal bearing. It does not perform the coordinate-facing arrival test or snap X/Z. Without a current navigation map it returns zero without turning.
Phase two releases held nodes and selects phase zero when no next node is held. Otherwise it faces the next node when the animation exit marker is clear, ignoring that facing result, then invokes coordinate walking toward the node with tolerance zero. A pending walk retains phase two. A completed walk performs default-animation admission again and the held-node arrival test with tolerance zero. A failed arrival selects phase zero without releasing held indices. Successful arrival assigns the next node as current and selects phase eight even when the claim fails. These pending paths set lifecycle selector seven, leave attribute 1 at 12 and register one deferred map update.
Point direct visibility
Point direct visibility reads the receiver’s query position and compares the rounded three-dimensional Q8 distance to the destination with an inclusive limit of 131072. A larger distance rejects before model tests. The segment endpoints are each shifted right by eight to integer map coordinates.
The query visits the prepared mixed-model candidates in order. A type-16 candidate is skipped before geometry when its actor is the receiver or selected player, or its model is the receiver’s own model instance. Other candidate types do not receive that early exclusion. Model segment tests disable early return after subobject admission; that argument does not set polygon sidedness. Point direct visibility leaves the shared polygon-sidedness flag unchanged. Scene initialization enables that flag, and temporary bounds queries restore it to enabled before returning. A zero or failure result does not block. A successful model result blocks unless the candidate actor is the receiver; that receiver exception occurs after the geometry call. The first blocking result rejects immediately.
Point route initialization
Phase zero releases held nodes and calls the animation-stop operation, which leaves playback unchanged. When the actor already holds a current node, it assigns that node as current again and selects phase eight. Otherwise route initialization releases held nodes, performs the ordinary nearest-free-node search and copies the shared selected map into the receiver, including an absent map on a miss. A missing map or node returns zero. Otherwise it assigns the found node as next and returns one after the assignment; the assignment’s claim result does not change that return value.
A nonzero initialization result selects phase one. A zero result retains phase zero. The pending tail sets lifecycle seven and attribute 1 to 12 and registers a deferred map update. Ordered release and assignment preserve previous-next history.
Phase-eight route monitoring
An unset current node releases held nodes and selects phase zero. Otherwise phase eight searches the current map for the destination’s XYZ coordinates, each shifted right by eight. A missing destination completes point navigation with result byte two. When the destination equals the current node, held nodes are released. Nonzero attribute 95 selects phase 21. With attribute 95 zero, a failed direct-visibility check completes with result byte four; an admitted check selects phase 22.
For a different destination, route search receives the current node, destination
node and low identifier byte, followed by attributes 92 and 91. A missing next
hop completes with result byte three. A next-node claim uses the ordinary
ordered release and reservation owner. A rejected claim completes with result
byte seven. An accepted claim selects phase one when the shared first-link
0x4000 flag is zero, or phase 12 when it is nonzero.
The pending tail sets lifecycle selector seven, leaves attribute 1 at 12 and registers a deferred map update. Completion stores its result byte, releases held nodes, changes lifecycle seven to zero, sets attribute 1 to 13 and returns zero before pending registration. Completion retains the phase-eight command phase and previous-next history.
Direct-route phases 11, 21 and 22
Phases 11 and 22 bypass the point entry’s immediate destination-arrival test. Phase 21 admits the default animation and completes with result byte zero on its ordinary return.
Phase 22 releases held nodes first. Nonzero attribute 95 selects phase 21. Otherwise it faces the original destination’s X/Z coordinates. A completed turn selects phase 11; a pending turn retains phase 22. The pending tail sets lifecycle seven and registers movement, with attribute 1 remaining at 12.
Phase 11 also releases held nodes first. Nonzero attribute 95 selects phase 21. Otherwise a failed direct-visibility check selects phase zero. An admitted check writes attribute 1 to 13, then invokes coordinate walking toward the original destination with the caller’s tolerance. Nonzero sliding state selects phase zero before that call but does not skip walking. Completed walking completes the point call with result byte zero.
Pending direct walking faces the original destination when the animation exit marker is clear, ignoring the facing return, then registers a map update and rechecks direct visibility. A failed recheck selects phase zero. The common pending tail sets lifecycle seven and repeats deduplicating registration; attribute 1 remains 13 on this path. The initial visibility rejection and the attribute-95 transition retain attribute 1 at 12 instead.
Phase twelve (leap link)
Phase twelve is selected when the claimed first link carries the 0x4000 flag.
An unset next node releases held nodes and selects phase zero. Otherwise the
stage runs the coordinate arrival test at the next node’s X/Z with the caller’s
tolerance. An arrival assigns that node as the current node through the
ordinary claim and selects phase eight. Without arrival, facing the node (the
face-node turn used by phase one) that returns zero starts the leap: the
receiver is given queued motion 2 and its animation requests group 32, or 33
with a burning torch. The pending tail is the common one, with attribute 1 at
12. Its start is refused, leaving the phase unchanged, while the receiver is
the selected player in its post-jump cooldown, its group is 114, 116, 130 or 131,
its falling, queued or sliding byte is nonzero, its stored surface is 2 or 4, it
is attached to a rope, or its living state is outside zero and 15. State 15 runs
the hang release instead of starting the leap. A started leap clears the
stable-position-valid flag, records the current Y as fall origin and zeroes the
fall timer and sliding distance.
Coordinate walking and rollback
Coordinate walking saves local position, the packed movement byte and retained
animation group/frame words before testing destination arrival. When arrival
fails, it also saves the animation receiver. Attribute 93 equal to zero selects
WalkForward; a nonzero value selects MoveForward, with duration one in both
cases. The position-query owner is selected before movement and its coordinates
are read after movement for destination bearing. That owner can supply alternate
or linked coordinates rather than local position.
If the masked difference between bearing and local heading lies from 256 through 1792 inclusive, walking clears the shared forward-movement-called flag and restores the saved local position, movement byte, animation receiver and retained group/frame words, then snaps local X/Z to the destination. Local Y remains the saved Y. Otherwise it retains the movement result and returns one.
An initial arrival also snaps local X/Z and retains local Y. After either snap, default-animation admission runs and the coordinate walker returns zero. Default-animation admission returns one on every ordinary return, whether it issues a forced request or skips it. Its result remains one after a forced request. The empty animation-stop call changes no playback state.
Walk-node arrival distance
Node arrival compares horizontal distance from the actor’s local position to the destination, using the same rounded Euclidean distance as node markers. It accepts equality in:
distance - tolerance <= (((speed + 2) * 256 * frame_scale) >> 8)
Coordinates and tolerance use runtime Q8 units. Multiplication and subtraction wrap at 32 bits, and the final right shift is signed. Vertical distance does not participate. A missing navigation map reports arrival before reading a node.
Speed comes from the actor’s current animation group when it provides a nonzero movement speed. Otherwise it is the minimum nonzero attribute among indices 8, 19, 17, 12, 9, 13 and 10. When all seven are zero, the fallback speed is 8.
Wander: destination selection and movement limits
Wander(radius, tolerance) validates the actor’s current navigation-map state
and sets attribute 94, attr_node_in_use, to one. With no current map, validation
resets the held current-node, next-node and final-destination indices to -1.
Ordinary map loading copies the authored runtime position into the stored home position before actor setup and attachment transforms. Actor setup selects its initial navigation map and current node through the global nearest-node query.
When the final destination is unset, Wander clears the actor’s saved navigation
scratch state and chooses a candidate around its stored home position. The
positive radius is converted from runtime units to map units by shifting right
eight bits. Home and effective current-position coordinates are converted by
integer division by 256, truncating toward zero. Candidate X and Z each equal
home + radius - Rnd(2 * radius); candidate Y is the home Y. It repeats both
random draws while the rounded horizontal Euclidean distance from the effective
current position is at most half the integer radius. Distance rounding follows
the node-marker distance arithmetic.
The candidate then goes through the global walk-node selection query. An empty loaded map list leaves the final destination unset. It does not bypass candidate selection or its random draws, and the next invocation tries again.
Wander finishes by measuring horizontal displacement from the actor’s saved
previous local position. Its limit in runtime coordinate units is
(frame_scale << 13) >> 8, with a wrapping 32-bit left shift followed by a
signed right shift. If the rounded displacement exceeds the limit, it restores
all three previous local coordinates. Equality retains the current position.
The function returns the measured displacement before any restoration.
MoveToLocation: authored locations
Ordinary actor setup creates location 0 from the saved original home position, then appends the actor header’s RALC entries relative to that home. These stored locations remain unchanged when the actor moves or becomes family-local. Negative selectors or selectors greater than the stored location count use the home position. A selector equal to the stored count falls immediately beyond the allocated entries; ordinary valid selectors run from zero through count minus one.
MoveToLocation(location, duration) compares the shared signed task elapsed value
with the signed duration. When elapsed is strictly greater than duration, it
assigns the stored target to all three local position coordinates, queues the
actor’s map update, and returns zero. Equality still performs a movement step.
Otherwise its source is the attached effective position when attr_slave is
nonzero, or the local position when that attribute is zero. It subtracts source
from target with wrapping 32-bit arithmetic and normalizes the resulting vector
to signed Q16 components. Normalization converts each component to single
precision, sums the squared components with extended intermediates, stores the
sum to double precision, and takes its square root. The divisor is that length
multiplied by 1/65536 and stored to single precision. A zero divisor produces
three zero components. Each other component is divided by the divisor, stored
to single precision, and converted to an integer with nearest-even rounding.
It separately measures the rounded three-dimensional distance from source to
target using the node-marker distance arithmetic.
Movement distance is signed integer division of that distance by
duration + 1 - elapsed, truncating toward zero. It then multiplies by
frame_scale with wrapping 32-bit arithmetic and shifts arithmetically right by
eight bits. Each displacement component is
((normalized_component >> 8) * movement_distance) >> 8, again with wrapping
multiplication and signed shifts. Displacements are added to the actor’s local
position with wrapping arithmetic. The operation queues a map update and returns
one, including when all three displacements are zero.
WanderToLocation(location, duration) selects its target and source as
MoveToLocation does and normalizes the direction the same way. Its first
invocation in a task slot sets a retained speed to the signed integer division of
the rounded source-to-target distance by duration, truncating toward zero; the
slot keeps that speed and ignores the elapsed input afterwards. Each invocation
computes the step as the speed times frame_scale shifted arithmetically right by
eight. It queues the actor’s map update. When the distance, measured again before
moving, is greater than the step, each displacement component is
((normalized_component >> 8) * step) >> 8, added to the local position, and the
task returns one. Otherwise the local position is assigned the target and the task
returns zero.
Direct point moves: MoveMarker, MoveLocation, MovePosition
MoveMarker(marker, tolerance), MoveLocation(location, tolerance) and MovePosition(x, y, z, tolerance) are
three front ends to one stepped walk toward a Q8 destination, called once per task update; the
MoveNodeMarker, MoveNodeLocation and MoveNodePosition natives are the walk-node counterparts and do not use
this routine. The position form passes its three Q8 arguments unchanged. MoveLocation passes the actor’s stored
location position unchanged: the location lookup returns the entry for a selector from zero through the stored count,
and the actor’s home location for a negative selector or one beyond the count. MoveMarker looks the marker up in the
world’s marker table (identifiers 0 through 127; an identifier outside that range or an unused slot raises a runtime
error) and shifts its X and Z left by eight. Its Y is the marker Y minus the actor’s positive Y extent, then shifted
left by eight, so the actor’s feet rather than its origin reach the marker height. The tolerance is passed on
unchanged.
Each step first releases the actor’s held walk nodes. It then returns one with these effects, without further processing, when the actor is falling or has queued motion: its first attribute byte becomes 13, the retained phase becomes 0 and the lifecycle selector becomes 7. An actor hanging from a ledge (living state 15) is released from the ledge and takes the same exit. Otherwise, unless the phase is 11 or 22, an arrival test against the destination and tolerance runs first; on arrival the actor requests its default animation group and the step completes by returning zero. When it has not arrived, the first attribute byte becomes 12 and the retained phase selects the step:
| Phase | Step |
|---|---|
| 0 | Select phase 22 (the routine the original calls here is empty). |
| 22 | Turn toward the destination; select phase 11 once the turn is finished. |
| 11 | Set the first attribute byte to 13. A sliding actor resets the phase to 0. Walk toward the destination; when the walk reports arrival the step completes with zero. Otherwise, while the walking animation has not reached its exit, keep facing the destination, and queue a map update. |
| 9 | The countdown shared with the node wrapper: the retained 16-bit counter becomes zero when it is smaller than the elapsed ticks and otherwise drops by them. A zero counter, or an actor holding a weapon, selects phase 2. Otherwise an actor in its default animation group draws a random number each step and, when the low five bits are all set, draws again until the low two bits are 0 to 2 and requests that group plus one. |
| 21 | Request the default animation group unless the actor already plays it, and complete with zero. |
| any other | Reset the phase to 0. |
Every step that stays pending also sets the lifecycle selector to 7 and returns one.
UnOffset
A linked-position record names an owner actor, a stored offset triple and a cached position. UnOffset does nothing
for an actor without a record or whose record names no owner. Otherwise it rotates the stored offset by the owner’s
orientation matrix, adds the owner’s effective position and the actor’s own local position (all with wrapping 32-bit
addition), stores the sum as the actor’s local position, publishes it right-shifted by eight to the cached model
position, and clears the owner identifier. The record, its offset and its cached position are kept.
Offset, PlayerOffset, PlayerOffsettask and CollidePlayerWeapon
Offset(x, y, z) attaches the calling actor to the actor it is called on (the owner), so that it follows the owner:
an actor with attr_slave set raises a runtime error. A caller already attached to this owner returns with no change;
one attached to another owner is first detached as UnOffset does. The caller’s linked-position record is created
when it has none. The stored offset is the caller’s local position minus the owner’s effective position plus the
three arguments (wrapping 32-bit arithmetic), rotated by the inverse of the owner’s orientation matrix. The record
takes the owner and that offset, and the caller’s local position becomes zero. The actor’s position is then the
owner’s effective position plus the stored offset rotated by the owner’s matrix plus the (zero) local position, as
UnOffset evaluates it.
PlayerOffset(x, y, z) sets the selected player’s local position to the receiver’s effective position plus the three
arguments rotated by the receiver’s orientation matrix, and returns zero.
PlayerOffsettask(x, y, z) does the same on every invocation and never completes (it returns one). Each invocation also
copies the receiver’s whole orientation matrix, its three angles and its three previous-angle values to the player. When the receiver’s
local position equals its previous-position copy and its angles equal its previous angles, the invocation additionally
adds the elapsed ticks to the pause-tick word of the shared native command (the second word of the four-word command
block). The handler itself reads the word nowhere else.
CollidePlayerWeapon(x, y, z, radius) returns zero unless the selected player holds a weapon, its attribute byte 105
is not 1 and the player’s attack state is exactly 1. Otherwise it evaluates the sphere/segment query described with the range/arc contact path
for a sphere centred on the receiver’s effective position plus the three arguments, shifted right by eight per
axis, with the radius shifted right by eight, against the weapon’s tip passed as both segment endpoints, and returns that query’s result.
PlayerMain
PlayerMain is the selected player’s native controller: the shipped player actor’s per-update hook is the zero-argument
call PlayerMain on itself, and its handler is the controller routine described in
Ordinary controller dispatch and the sections that follow it. It takes no
arguments and returns no value.
DetachMe
DetachMe() takes no arguments and returns zero. A receiver with zero
attr_slave is unchanged.
For an attached receiver with nonzero attr_master, the function first clears
attr_slave. It then uses the receiver-master lookup, which selects no node in
a finite, acyclic family tree. The function returns without unlinking the family
record, restoring position or angles, or requesting the actor refresh.
For an attached receiver with zero attr_master, the function searches ordinary
attached-actor lists for the receiver. It visits children before the current
node’s ordinary list, then visits siblings. A missing record leaves the receiver
unchanged. A found record is unlinked by updating either the owning list’s head
or the preceding record’s next pointer. The function clears attr_slave, copies
effective position and effective angles into the receiver’s local position and
angles, and requests the actor refresh. It does not free the unlinked membership
record or change the effective-position and effective-angle fields.
AttachMe
AttachMe(group) takes an integer family identity and returns zero. An argument
of zero, the receiver’s current attr_slave, or its current attr_master
returns immediately without changing the receiver.
Otherwise the function finds the receiver’s existing ordinary membership,
detaches the receiver, then searches the family tree for a master whose
byte-sized attr_master equals the full integer argument. A missing target
returns zero after the detach attempt; this operation does not roll back the
previous attachment. Target selection visits a node before its children and
siblings. The receiver-master lookup used separately by this operation does
not select a node in a finite, acyclic family tree.
An ordinary membership record is reused when one was found before detaching; otherwise a new record is allocated. The record is prepended to the target’s ordinary attached-actor list and references the target master’s matrix. The receiver’s slave identity becomes the group value.
Before converting the receiver to local coordinates, the function copies its
position and angles into the effective-position and effective-angle fields.
It subtracts the target group’s position from the local position and passes
the result through the target matrix’s transpose transform. When the receiver’s
attr_no_slave_rot is zero, each local angle becomes the difference from the
target group’s angle modulo 2048. These local angles also become the saved
previous local angles. The receiver’s matrix is rebuilt from the target matrix
with negative local yaw, pitch and roll rotations in that order. A nonzero
attr_no_slave_rot skips these angle and matrix changes; it still converts
position to the target frame.
FixSlaveAngle
FixSlaveAngle(axis, angle) returns zero. When the receiver’s attr_slave is
zero, it leaves the receiver unchanged. Otherwise it selects the angle-table
entry indexed by that slave group and writes one local angle component:
local_component = (angle - group_component) & 2047
Axis zero selects pitch, axis one selects yaw, and every other integer selects roll. The function does not update the other angle components, the effective angles, or the orientation matrix. It reads the existing group-angle table directly without a family-tree lookup.
The group-angle table holds one entry of three 32-bit angles (pitch, yaw,
roll) per group number, indexed by the group byte. The family update writes an
entry for each master, indexed by that master’s attr_master, so a slave’s
group names the entry its master wrote:
- Normally the entry is the entry of the master’s own
attr_slavegroup plus the master’s local angles, each masked to 11 bits. A master that is not itself a slave adds to entry zero. - When the master’s
attr_no_slave_rotis nonzero, the entry is the master’s local angles masked to 11 bits, without the parent group’s angles.
The entry therefore follows the master’s accumulated angles as of its latest update; a slave group no master has written reads whatever the table holds for that index.
DetachSlaves
DetachSlaves() takes no arguments and returns zero. It does not change
attachments for a finite, acyclic family tree. A receiver with zero
attr_master returns immediately. Otherwise its family lookup traverses child
and sibling lists without selecting a node and returns no result. The later
detach calls are consequently unreachable. Traversal updates an internal
scratch pointer without changing actors or the family tree.
HideSlaves and ShowSlaves
HideSlaves() and ShowSlaves() take no arguments and return zero. Neither
changes visibility for a finite, acyclic family tree. A receiver with zero
attr_master returns immediately. Otherwise both use a family lookup that
traverses child and sibling lists without selecting a node and returns no
result. Their later visibility writes are consequently unreachable. Traversal
updates an internal scratch pointer without changing actors or the family tree.
EnableSlaves and DisableSlaves
EnableSlaves() and DisableSlaves() take no arguments and return zero.
Neither changes actor state for a finite, acyclic family tree. A receiver with
zero attr_master returns immediately. Otherwise the handler searches the
engine’s family tree, but the shared lookup traverses child and sibling lists
without selecting a matching node and returns no result. The actor-flag changes
after that lookup are consequently unreachable.
The lookup updates an internal traversal scratch pointer while visiting nodes.
These functions do not change the global slave-scheduling settings or act as
aliases for the generic Enable and Disable commands.
IsSlave and IsMaster
IsSlave() returns one when the receiver’s byte-sized attr_slave is nonzero,
otherwise zero. IsMaster() applies the same test to attr_master. Both take
no arguments and leave the receiver unchanged. They test attribute values
directly without searching the family tree or requiring a matching membership
record. The other family attribute does not affect the result.
Actor families and attached transforms
attr_master and attr_slave are byte-sized group identities. A nonzero slave
identity attaches an actor to the matching master group. An actor can be both a
slave of one group and the master of another, forming a nested family.
Family initialization scans identities 1 through 255. For each identity it selects the first matching master without a slave identity from the actor traversal list. It collects attached child masters separately from attached actors without a master identity. Each collection prepends matching actors, so its traversal reverses their encounter order.
The initialization traversal visits allocated map-cell registry identities in increasing order, preserving each cell’s actor list, then appends the persistent actor list. Registry identities are assigned to cells as they are allocated; sorting cell coordinates does not define this traversal. Fresh actor allocations prepend to their cell or persistent list. Duplicate root masters are permitted: the first matching root is selected, and the other roots remain independent.
Initialization saves each master’s original position and angles before processing its descendants. Descendants are initialized before the child master’s own coordinates become local. For each attached actor:
- Subtract the parent’s saved original position from the actor’s position.
- Multiply that displacement by the transpose of the parent’s orientation matrix to obtain the local offset.
- If the parent’s
attr_no_slave_rotis zero, subtract the parent’s saved original angles from the actor’s angles, wrapping each component modulo 2048. - Initialize the attached-position storage with the resulting local offset.
The angle subtraction tests the parent’s attribute. The later composition branch tests the attached actor’s own attribute.
Family refresh
Each family refresh caches the root master’s current position and angles. It composes each child master before visiting that child’s descendants, then processes the remaining attached actors. Attached positions use the runtime coordinate scale of 256 units per world unit:
effective_position = parent_effective_position + parent_matrix * local_offset
Refresh preserves the previous attached position before storing the new one. Render position components use the effective integer coordinate shifted arithmetically right by eight bits, then converted to single precision.
When the attached actor’s attr_no_slave_rot is zero, refresh copies the parent
matrix and applies negative local yaw, pitch and roll in that order. Its
effective angle fields separately add the parent’s effective angles and local
angles, wrapping each sum modulo 2048. A child master publishes its effective
position and angles for descendants. Matrix composition and effective Euler
angle addition are separate operations.
When the attached actor’s attr_no_slave_rot is nonzero, position still uses
the parent matrix and translation. Its existing orientation matrix instead
receives negative local yaw, pitch and roll changes relative to the saved
previous local angles. Its effective angle fields use the local angles modulo
2048 without adding parent angles.
Ordinary axis locks
Near the end of an ordinary actor update, after script and movement handling,
nonzero position-lock attributes 40, 41 and 42 restore X, Y and Z respectively
from that update’s saved position. Rotation-lock attributes 38, 37 and 39
restore pitch, yaw and roll respectively from the saved angles. Each axis is
independent, and the current attribute values are read when this step runs.
Actors with nonzero attr_slave bypass all six restorations. The step restores
coordinate and angle fields; it does not rebuild the orientation matrix.
Integer matrix arithmetic
Both local-offset initialization and position composition convert each signed integer input to single precision first. Matrix elements are single precision. Products and additions use x87 intermediates; each completed component is stored to single precision before conversion to an integer. Single-precision stores and integer conversion use the ordinary nearest-even rounding mode, as in the forward-probe transform. Parent translation is integer addition after conversion.
For row-major matrix entries m00 through m22, ordinary composition evaluates:
X = (y * m01 + x * m00) + z * m02
Y = (x * m10 + y * m11) + z * m12
Z = (x * m20 + y * m21) + z * m22
The transpose used during initialization evaluates:
X = (y * m10 + x * m00) + z * m20
Y = (x * m01 + y * m11) + z * m21
Z = (x * m02 + y * m12) + z * m22
Matrix axis updates
Axis rotations use the shared rotation table.
For the supplied wrapped angle a, s is table entry a and c is entry
a + 512. For each existing matrix row (x, y, z), the helpers write:
| Axis operation | Temporary single-precision result | Other single-precision result | Unchanged component |
|---|---|---|---|
| Yaw | x_new = x * c - z * s | z_new = z * c + x * s | y |
| Pitch | y_new = y * c + z * s | z_new = z * c - y * s | x |
| Roll | x_new = x * c + y * s | y_new = y * c - x * s | z |
Both expressions use the row values from before that axis operation. The helper rounds the temporary result first, stores the other result in the matrix, then copies the temporary into the matrix. Each result is single precision before the next axis operation. In the ordinary unattached incremental refresh, each product also rounds to single precision before the addition or subtraction. Family composition applies these operations to the copied parent matrix with negative local yaw, negative local pitch and negative local roll, each wrapped modulo 2048.
Angle snapshots and script ordering
Explicit orientation assignment initializes the previous-angle fields from the assigned angles. The ordinary actor update copies current local angles into those fields before animation, movement and main script processing. Actors excluded from that update do not take this snapshot.
The ordinary family-composition branch reuses these fields for propagated
angle deltas. Each component becomes
(local_angle - saved_local_angle + parent_delta) & 2047 and supplies the delta
for descendant processing. The attr_no_slave_rot branch leaves these fields
unchanged and passes their stored values onward.
In the normal main loop, family refresh precedes map-list rendering and the
ordinary actor update. A separate ground-settling path also invokes family
refresh. A local-angle write does not itself refresh attached position, matrix
or effective angles. Consequently, an ordinary script’s RotateByAxis followed
by PlayerFacing reads the previously composed attached yaw until another
family refresh occurs.
Move, sMove, Rotate and sRotate: direct increments
Move, sMove, Rotate and sRotate are tasks taking (x, y, z, duration). Each handler treats an
operand of zero as no change for its axis and returns “continue” while duration - 1 is greater than the
signed task elapsed input, so the task completes on the invocation whose elapsed input reaches
duration - 1. None of them performs collision, matrix refresh or position restoration, and the
first invocation changes the pose.
Move multiplies each nonzero operand by the shared whole nominal-tick increment and adds the product
to the local position word of its axis, with wrapping 32-bit arithmetic. sMove instead adds
((operand << 8) / duration) >> 8 to each word, using signed division and an arithmetic shift, on every
invocation whatever the tick increment; a zero duration is a division fault.
Rotate and sRotate are the same routine. For each nonzero operand it computes
(((|operand| << 8) / duration) * elapsed) >> 8 with signed division and an arithmetic shift, gives the
result the operand’s sign, and adds it to the local angle word of the axis. The sum is stored as the
handler leaves it, without masking to 2047; later orientation builds mask the angle when they read it,
so the stored word can exceed one turn. The elapsed input is zero on the first invocation, which
therefore changes no angle. A zero duration is a division fault.
sMoveAlongAxis(axis, delta, duration) converts the duration to frames, (duration << 8) divided by the
shared frame scale, and adds ((delta << 8) / frames) >> 8 on the axis (zero X, one Y, any other value Z)
to the position after rotating that vector by the receiver’s angles. It returns “continue” while
duration - 1 is greater than the elapsed input. A zero frame count or step division is a fault.
RotateByAxis: relative rotation
RotateByAxis(axis, delta, duration) modifies the receiver’s local angles.
Axis zero selects X, axis one selects Y, and every other axis value selects Z.
Angles wrap modulo 2048. A zero delta or zero duration completes without
changing an angle.
The task keeps a target and accumulator with eight fractional bits. When the
saved target and the task elapsed input are both zero, it initializes the target
to ((current_angle + delta) & 2047) << 8 and the accumulator to
current_angle << 8.
Each invocation computes a fixed-point step by shifting delta left eight
bits, dividing by duration with signed truncation toward zero, multiplying by
the current shared frame scale, then arithmetic-shifting right eight bits.
The intermediate shifts and multiplication retain their low 32 bits. If this
step is zero and the shared whole nominal-tick increment is nonzero, that tick
increment becomes the step, negated only when the saved target is negative.
The task compares the absolute shortest wrapped angle difference between the
current angle and target >> 8 with abs(step >> 8). If the difference is
larger, it adds the step to the accumulator, writes
(accumulator >> 8) & 2047, and remains active. Otherwise it writes
target >> 8 and completes. The initial invocation can change the angle.
Arguments are reevaluated on later invocations, while the saved target and
accumulator persist unless the initialization condition is met again.
Each task slot owns its target and accumulator; the receiver’s angle is shared. Stopping the task removes its slot without restoring an earlier angle. Transform and attachment updates consume the changed local angles separately; the handler does not update their matrices itself. Frame scale and nominal ticks are distinct inputs; see animation timing.
sRotateByAxis: elapsed relative rotation
sRotateByAxis(axis, delta, duration) stores the selected angle as its origin
whenever elapsed input is zero. Only axes zero through two select and write an
angle. The origin remains task-local on later invocations.
For a nonnegative delta, the offset is
((delta << 8) / duration * elapsed) >> 8. For a negative delta, it computes
that expression using the absolute delta, then negates the shifted result.
Division truncates toward zero and the right shift is arithmetic. Intermediate
shifts, multiplication and negation retain their low 32 bits. The handler writes
(origin + offset) & 2047.
When elapsed is at least duration - 1, it overwrites that result with
(origin + delta) & 2047 and completes. Otherwise it remains active. Thus the
last invocation snaps to the full delta rather than the interpolated offset.
Duration one completes on the initial invocation. Duration zero produces a
division-by-zero fault before the completion check. This task uses elapsed input,
not shared frame scale or nominal ticks, and does not refresh composed transforms.
RotateToAxis: absolute rotation
RotateToAxis(axis, target, duration) modifies the receiver’s local angle.
Axis zero selects X, axis one selects Y, and every other value selects Z.
The target is masked to 2047. A zero duration completes without changing an
angle.
Let difference be the masked current angle minus the masked target. The
signed movement toward the target is -(difference & 2047) when the difference
is below -1024, -difference when it lies between -1024 and 1024 inclusive,
and 2048 - difference when it exceeds 1024. Thus the two half-turn ties
retain the direction determined by the subtraction.
Each task slot stores this movement with eight fractional bits and a separate
angle accumulator. When the saved movement and elapsed input are both zero,
it initializes them to movement << 8 and current_angle << 8. Later argument
reevaluation changes the destination used for completion, but does not
reinitialize a nonzero saved movement.
Each invocation divides the saved movement by the current duration with signed truncation toward zero, multiplies by shared frame scale, then arithmetic-shifts right eight bits. Shifts and multiplication retain their low 32 bits. If the result is zero and the whole nominal-tick increment is nonzero, the step is that increment shifted left eight bits, negated when the saved movement is negative.
The handler compares the absolute movement from the current angle to the
current target with abs(step >> 8). If it is larger, the handler adds the step
to its accumulator, writes (accumulator >> 8) & 2047, and remains active.
Otherwise it writes the masked target and completes. The initial invocation
can change the angle. The handler does not refresh the receiver’s composed
transform or attachment matrix.
sRotateToAxis: elapsed absolute rotation
sRotateToAxis(axis, target, duration) stores the selected angle as its origin
whenever the elapsed input is zero. Only axes zero through two select and
write an angle. The origin remains task-local on later invocations.
It subtracts the stored origin from the target before masking the target.
When this signed difference is below -1024, it replaces it with
difference & 2047. When it exceeds 1024, it replaces it with
-(difference & 1023). Other differences remain unchanged.
The interpolated angle is the origin plus
((difference << 8) / (duration - 1) * elapsed) >> 8, masked to 2047.
Division truncates toward zero, the right shift is arithmetic, and intermediate
arithmetic retains its low 32 bits. The interpolation executes before the
completion check. A duration of one therefore produces a division-by-zero
fault even at elapsed zero.
When elapsed is at least duration - 1, the handler overwrites the interpolated
angle with target & 2047 and completes. Otherwise it remains active. The
positive-difference mask can make the interpolated path approach a different
angle before this final snap. This task uses elapsed input rather than shared
frame scale or nominal ticks.
MoveObjectAxis: actor-local movement
MoveObjectAxis(axis, delta, duration) moves along the receiver’s orientation,
using its current stored matrix or the upright forward shortcut. Delta and
position use eight fractional bits. Zero delta or duration completes immediately.
At elapsed zero, the task stores delta as its target and resets its traveled
accumulator. Each invocation computes (delta / duration >> 8) * frame_scale
with signed division, arithmetic shift and wrapping 32-bit multiplication, then
adds that step to the wrapping accumulator. A negative step completes when the
accumulator is at or below the target; a positive step completes when it is at
or above the target. These completion paths perform no movement and do not
snap the receiver to the target.
Otherwise, axis 0, 1 or 2 selects a local X, Y or Z displacement; other axes select a zero vector. For axis 2, a receiver with zero slave attribute, pitch and roll uses the upright forward translation rule, including single-precision coordinate addition and truncation. Other cases transform the displacement through the stored actor matrix, round the transformed components to nearest-even integers, and add them to the receiver’s coordinates with wrapping arithmetic.
After movement, the receiver is registered once in the deferred map-update
list. The task remains active only while signed elapsed is less than the
wrapping value duration - 1. Later invocations reevaluate arguments while
retaining target and accumulator unless elapsed is zero again. The task does
not perform collision or restore position when stopped.
MoveByAxis and sMoveByAxis: relative movement
MoveByAxis(axis, delta, duration) changes a receiver’s local position in the
same eight-fractional-bit units as the position fields. Axis zero selects X,
one selects Y, and every other value selects Z. A zero delta or zero duration
completes without changing position.
Each task slot retains a target, initially zero. Whenever that saved target is
zero, the handler replaces it with current_position + delta; this test does
not depend on elapsed time. Each invocation divides delta by duration using
signed truncation toward zero, arithmetic-shifts that quotient right eight,
then multiplies by the shared frame scale, keeping the low 32 bits. This order
quantizes the step before multiplication.
For a negative step, a proposed position at or below the target snaps to the
target and completes. For a positive step, a proposed position at or above the
target does the same. Otherwise the step is added to the current position.
A zero step leaves position unchanged at this point. The handler then compares
the signed task elapsed input against duration - 1: reaching that value snaps
to the saved target and completes; otherwise the task remains active. Additions
and subtraction wrap to 32 bits. The initial invocation can move the actor.
sMoveByAxis(axis, delta, duration) is a separate elapsed-position task. Only
axes zero, one and two read or write a coordinate. Its saved origin starts at
zero; when both origin and elapsed input are zero, it captures the selected
current coordinate. Each invocation writes origin plus
(((delta << 8) / duration) * elapsed) >> 8, using signed division, an arithmetic
right shift and low-32-bit intermediate results. When signed elapsed is at
least duration - 1, it instead writes origin + delta and completes. Other
axis values still perform arithmetic and completion testing but do not write
position. This handler has no zero-duration shortcut.
MoveAlongAxis(axis, delta, duration) is a task that moves along an axis of the
receiver’s own orientation. A zero delta or zero duration completes without
change. The slot retains the delta and a running sum, both reset when the signed
elapsed input is zero. Each invocation computes the step as for MoveByAxis
(signed delta / duration, arithmetic shift right eight, times the frame scale,
low 32 bits) and adds it to the running sum. A negative step completes when the sum
is at or below the delta, and a positive step when it is at or above it; that
invocation does not move the actor. Otherwise the step becomes a vector on the
selected axis (zero X, one Y, any other value Z), is rotated and added to the local
position, and the actor registers a map update. With attribute 36 nonzero, or while
the receiver’s per-update matrix-use byte is zero (it is cleared at the start of
every ordinary update), the rotation is built from the receiver’s current pitch,
yaw and roll; that sense is the inverse of the stored matrix’s transform, so a
quarter turn of heading sends a positive X step onto negative Z. Only with
attribute 36 zero and the matrix-use byte set does the stored matrix rotate the
vector in the stored matrix’s own sense.
The task then completes when duration - 1 is not greater than the elapsed input.
It never snaps to a target position.
Both tasks reevaluate their arguments on subsequent invocations and retain
slot state under their respective initialization conditions. Neither handler
performs collision, matrix, or attachment refresh. Stopping a task does not
restore position. Signed division overflow, including division by zero in
sMoveByAxis, has no successful movement result.
Rnd and QuickRnd
Rnd(bound) and QuickRnd(mask) each consume one draw from the shared
engine random stream, also used by sky scroll initialization.
The generator updates an unsigned 32-bit state with wrapping arithmetic:
state = state * 1103515245 + 12345
draw = (state >> 16) & 32767
The executable’s stored initial state is 1. The state belongs to the shared generator, not to an individual script or actor.
QuickRnd(mask) returns draw & mask, using the complete 32-bit argument.
The argument is a bit mask rather than an exclusive upper bound. A zero mask
still consumes a draw; a negative mask is accepted.
For a positive bound, Rnd(bound) first masks the draw with the smallest
mask in 1, 3, 7, ..., 16383 whose range can contain bound values. Bounds
greater than 16384 leave the draw unchanged. It then repeatedly subtracts the
bound until the result is less than the bound. Equivalently, apply that mask
and take the remainder modulo the bound. The result is not uniformly
distributed for every bound. Zero and negative bounds do not define a valid
bounded draw.
HideMe and ShowMe
HideMe() sets the receiver’s hidden flag and clears its current presentation
record. ShowMe() clears that flag. For a static-model receiver, showing it also
restores its model presentation record and refreshes its transformed geometry.
Both calls return zero.
The hidden flag does not exclude the receiver from ordinary script or animation updates. It suppresses the ordinary actor’s optional sound and light updates. Player selection also uses this flag to hide the unselected player model.
Current model presentation
A retained model resource and an actor’s current model presentation record are distinct. For an actor admitted to the ordinary presentation traversal, the current record is cleared before hidden and update-excluded state are checked. Either exclusion therefore leaves that actor without a current model record. The PVO actor predicate runs earlier: an actor rejected there retains its prior current record as well as its stored coordinates. Stored floating-point model coordinates have a separate lifetime. Actor default initialization clears them to zero. Clearing the current record, hidden or update exclusion, and PVO rejection preserve those coordinates until model-position processing runs again. An absent animation frame or unavailable selected resource also preserves the previous coordinates when position processing is skipped. A static-model actor that passes those checks restores its retained resource and refreshes its model position and orientation, even when the retained resource is null. Drawing exclusion suppresses submission without clearing the restored record.
For an animation-table actor, an unavailable selected model or an absent frame
selection clears both the selected resource and the current presentation record.
Collision candidate construction consumes the current record and its stored
model transform. A logical position change does not itself refresh that transform.
In normal main-loop order, model presentation runs before the ordinary actor
update. ShowMe() provides the separate immediate static-model refresh described
above.
The ordinary model-position path uses the actor’s logical Y shifted right by
eight when the collision attribute mask 0x04 is clear. With that mask set,
water processing can adjust model Y for stored surface classes 1 through 5
and 11. Other stored surface classes retain the shifted logical Y. A nonzero
collision attribute by itself therefore does not establish water displacement.
The normal world loop enables water-position processing before its frame loop and clears that enable when it leaves the loop. A nonzero attribute 26 bypasses this model-position path before water processing; family refresh owns the attached actor’s model coordinates.
For other actors, a linked-position record with a nonzero owner reference selects a separate model-position calculation before water processing. Copy the record’s local offset, convert its components to single precision, and transform it by the owner’s stored orientation matrix. Each resulting component is converted to an integer using the active floating-point rounding mode. The default mode rounds to nearest, with ties to even.
Select the owner’s effective position, add the transformed offset and the actor’s own local logical position, then shift each signed component arithmetically right by eight and store it as a float. Both integer additions wrap at 32 bits before the shift. The actor’s logical position is unchanged. This linked calculation returns before water displacement, health prepasses or environmental effects. A missing linked record or zero owner reference instead falls through to ordinary model positioning. The linked record’s cached position used by effective-position queries is a separate value from this calculated model position.
For the selected player with attribute 105 equal to one, enabled water-position
processing and collision attribute mask 0x04 run an earlier surface prepass.
Stored surface classes 2 and 4 clear both directional-state words before a
health change of -255 through the selected death-owning health service, with no
source actor. Classes 1 and 3 clear those words without that health call. Other
surface classes do neither in this prepass. The later surface-position processing
still follows; this prepass does not replace the surface-5 and surface-11 route.
With water-position processing enabled and collision attribute mask 0x04
set, stored surface classes 5 and 11 place the model Y at the arithmetic-shifted
logical Y plus 64. For the selected player, the retained death-progress word is
also added. These integer additions wrap at 32 bits before conversion to the
floating-point model position; the logical position is unchanged.
Stored surface class 1 places model Y at the arithmetic-shifted logical Y plus 64. Surface classes 2, 3 and 4 first check the selected player’s retained hit inhibition. An inhibited selected player uses shifted logical Y plus 64 plus retained death progress, then returns without coverage sampling or the height-change effect check.
Otherwise, surface 2 uses coverage 256. Surfaces 3 and 4 sample terrain material blend coverage at the stored model X and Z, each truncated toward zero to integer map units. Coverage zero and 256 remain unchanged. Other coverage values are doubled and capped at 256. Model Y becomes shifted logical Y plus one quarter of that coverage, using an arithmetic right shift by two.
Animation groups 141 through 143 then add [game] swim_depth to the stored
model Y. This setting replaces zero with 40 during initialization; other values
are retained. The shifted-position result is stored as a float before the
swim-depth addition, whose result is also stored as a float. Group 140 does
not receive that offset.
For the non-inhibited surface-2/3/4 path, the prior stored model Y is truncated
toward zero. The new stored Y is subtracted, and the difference is truncated
toward zero before taking its integer absolute value. A magnitude strictly
greater than 16 requests environmental effect descriptor 0x2500; equality
does not. Surface 1 and the inhibited selected-player path skip this check.
This environmental effect starts from the actor’s current logical Q8 position. Its horizontal point is displaced 2048 Q8 units toward the camera using the camera-relative effect-point rules. Its vertical point is logical Y plus the actor’s positive vertical extent times 256 plus 1536, with signed 32-bit wrapping arithmetic. The effect constructor applies texture-height centering and publishes map-unit coordinates, descriptor flags, the texture’s last frame as the target frame, and speed one. This path consumes no random numbers. Disabled effects or refused allocation produce no effect.
The selected player’s surface-5 path sets retained death mode one after publishing model Y. Surface 11 leaves that mode unchanged. Both paths then test queued motion and the low two bits of each directional-state byte. A nonzero queued-motion state or either nonzero bit pair suppresses the health call; it does not undo the model position or death-mode write. Otherwise the receiver gets a health change of -255 through the death-owning health route. The selected player’s mode write therefore precedes any death initialization performed by that health service.
Model admission and support ownership
Both standalone player roles are allocated into the persistent actor list. The initially selected player is allocated first and the alternate second; insertion at the front leaves the alternate before the selected player in that list. Model-list assembly appends this persistent list after the collected cell actors, so standalone players participate without an MPOB placement. Player setup hides the alternate. It remains linked but hidden-state exclusion prevents its model position processing.
The current map’s PVO file supplies a camera-centered selected-object list.
An empty selected list leaves ordinary model admission unrestricted. With a
nonempty list, the ordinary presentation traversal excludes actors with
attr_non_moving set. This predicate also applies to standalone players.
With a nonempty selected list, animation-model admission also reads the actor’s
retained support owner. A
static-placement support must occur in the selected MPSO IDs. Support from an
actor with attr_non_moving set must occur in the selected secondary object
IDs. Other support cases pass this admission check. Rejected animation-model
admission still runs model-position processing when a frame is selected, then
clears the selected resource and current model record. An absent frame skips
position processing. Thus rejected support can still produce model-position
health changes and environmental effects without publishing geometry.
When loading a world with a PVO file, the secondary object table is built
from the complete map actor traversal. Map cells are scanned in increasing Z,
then increasing X, preserving each cell’s actor list. The persistent actor
list follows the cells. Only actors with attr_non_moving set are appended
to the secondary table, in that traversal order. This cell-coordinate scan
is distinct from the registry-identity traversal used for family initialization.
Ordinary actor updates clear the retained actor and static-placement support owners before ground processing. A selected model face assigns its candidate’s actor and static-placement owners. Model presentation precedes that ordinary update, so its admission reads support retained from earlier ground processing. Actor-following also consumes the retained actor support owner.
Global Flags
369 global flags (indices 0–368), shared across all scripts. Each flag has a declared type:
| Type | Semantics |
|---|---|
BOOL | Binary state (0 or 1) |
NUMBER | Integer counter or timer |
FLIPFLOP | Toggled state (doors, switches, puzzle elements) |
The loader maps BOOL, FLIPFLOP, COUNTER, and NUMBER to runtime kinds zero through three. Initializing a flag stores its authored numeric value and clears the latch. A BOOL set toggles its numeric value only when unlatched, then latches it. FLIPFLOP toggles on every set. COUNTER decrements a nonzero value and latches it; a zero value remains unchanged. NUMBER sets the latch without changing the numeric value. The SOUP SetGlobalFlag and ResetGlobalFlag wrappers return zero. TestGlobalFlag returns one or zero according to the latch.
The flag store is two arrays indexed by flag number and sized by the number of [flags] entries: 32-bit signed numeric values, and one byte per flag that holds the runtime kind and the latch. Both are allocated when the flags are loaded, zeroed, and then initialized entry by entry from SOUP386.DEF, so the store holds exactly one value per declared flag (369 on the installed definition). Bytecode reads a flag’s numeric value directly from the value array, and writing that array changes what scripts read without touching the latch.
Native flag functions maintain a separate latch in addition to the numeric flag value. TestGlobalFlag reads that latch; ResetGlobalFlag clears it. SetGlobalFlag dispatches according to the runtime type bits and can toggle, decrement, or latch a flag. It is not a generic assignment of one to the numeric flag.
Flags are declared in the [flags] section of SOUP386.DEF and accessed by bytecode via SetGlobalFlag, TestGlobalFlag, and ResetGlobalFlag (which use the NumericAlt opcode 0x16 for their flag-ID parameter).
Six flags have non-zero defaults: TimeOfDay (1), OB_TelV (1), OB_TelH (12), At_Shoals (1), Rock_1_Down (1), Rock_2_Down (1).
Flag categories span narrative progression (acts 1–8, for example After_Catacombs, After_League, Won_Game), inventory state (HaveAmulet, HaveGem, Equipped_Torch), NPC dialogue tracking (DreekiusTalk, TobiasTalk, SionaFriend), puzzle mechanics for catacombs (CTDoor*, CTWeight), caverns (CV_Lock*, CV_Pillar*), observatory (OB_TelV, OB_Platform), palace (PI_Door*, PI_Throne*), dwarven ruins (DR_Steam, DR_Boiler, DR_Pipe*), and the scarab (SCB_Position, SCB_ArmL, SCB_ArmR), as well as runtime control (Talking, MenuRet, StrengthTimer, MapTimer).
Relationship to RGM Sections
RAHDstores per-actor script pointers, variable counts, and function-table indices.RASCstores compiled script bytecode as a contiguous blob.RAST/RASBstore script string literal data and offset tables.RAVAstores initial local variable values (i32array).RAHKstores per-actor native call records addressed by RAHD section offsets.RAATstores per-actor attribute tables (256 bytes each), addressed by names fromSOUP386.DEF.
See RGM.md for record-level layouts and offsets.
External References
- UESP
Mod:RGM File Format - RGUnity/redguard-unity
RGRGMScriptStore.cs— SOUP VM interpreter with dispatch loop, threading model, and formula evaluator - RGUnity/redguard-unity
soupdeffcn_nimpl.cs— SOUP function ID-to-name table (367 functions) - Dillonn241/redguard-mod-manager
ScriptReader.java— RASC bytecode disassembler with full opcode and value-mode decoding - Dillonn241/redguard-mod-manager
ScriptParser.java— RASC bytecode assembler (script text to bytecode) - Dillonn241/redguard-mod-manager
MapHeader.java— RAHD record parser with field offsets - Dillonn241/redguard-mod-manager
MapDatabase.java— SOUP386.DEF parser (function, flag, reference, attribute definitions)
Selected-player location camera task
showCyrusLoc is a three-argument task. It tests IsHoldingWeapon on the
selected player, independently of the receiving actor. While that predicate is
true, the task returns zero without invoking its camera-location handler.
Otherwise it passes the selected player and all three full-width arguments to the camera-location handler and returns that handler’s result. The wrapper does not narrow or transform the arguments. Task-slot scheduling and operand reevaluation follow the ordinary native-task rules.
Player camera task state
showPlayer is a task with no arguments. Each invocation asks the player camera
selector for a slot, falling back to slot zero when the selector reports no
selection. It updates the shared camera selection to that slot.
The task increments its first private word with 32-bit wrapping. When the result
is one and elapsed input is zero, it sets bit 0x01 in the selected camera
record’s flags and returns one. Otherwise it clears bits 0x01 and 0x20 in
that record and returns zero. Other bits in the record’s flag byte are retained.
The private word belongs to the task continuation and is distinct from the
shared camera selection.
A nonzero task result keeps the task slot active and zero completes it. The
initial invocation therefore leaves showPlayer active with bit 0x01 set, and
the next task pass completes it after clearing bits 0x01 and 0x20.
Player camera slot selection
The slot requested for the selected player is the first that applies:
| Condition | Slot |
|---|---|
The selected player holds a weapon (IsHoldingWeapon) | 3 |
| The shared debug word is nonzero (zero at program start) | 8 |
| The selected player’s living state is 15 | 4 |
| The selected player has a rope association | 5 |
When none applies, no slot is requested.
A request for a different camera slot changes the selection only when the
shared transition word is zero or the current record has flag 0x01. Otherwise
the current slot remains selected. Requesting the current slot leaves selection
unchanged. The pose stage clears this word according to its correction and
movement-limiting results.
Actor camera offsets
Location-camera setup reads the actor’s attr_camera_target_offset_x,
attr_camera_target_offset_y, attr_camera_target_offset_z,
attr_camera_offset_x, attr_camera_offset_y, and attr_camera_offset_z
attributes as unsigned bytes. Each byte independently becomes zero when it is
zero, or (value - 127) * 500 otherwise. The signed results replace the selected
camera record’s target-offset and camera-offset vectors. This conversion does
not change the actor attributes or the other fields in the camera record.
Selected record and selected index
The engine keeps the selected camera record and a separate selected-index word. Camera record selection sets both. The movement limiter’s slot-one test, the orientation stage and the distance-height stage read the index word; every other stage reads the selected record. A pan leaves the index on the slot being panned while the selected record is record two.
showPlayerPan
showPlayerPan(duration) is a task with one argument. It increments the same
private word as showPlayer. A negative duration writes a diagnostic line and
the task returns zero before touching the private word or any camera state.
On the first invocation (private word one, elapsed zero) it:
- takes the slot the player camera selector requests, or zero, and makes that record the selected record directly, without the selection guard;
- begins a scripted camera;
- copies the requested slot’s record into record two and sets the selected index to the slot (the selected record stays whichever record the begin step left selected);
- binds the selected record to the selected player;
- records the camera’s current offset from the player,
published position - record position offset - bound position, as the start vector. The start distance is that vector’s rounded length shifted right eight. The start angles are the camera orientation conversion of the vector. The target is the record’s angle offset minus the bound angles (not wrapped), and the delta is the target minus the start angles with each component wrapped once into half a turn either side of zero (below -1024 add 2048; above 1024 subtract 2048).
Every invocation then runs the scripted-camera release step with duration.
While it reports running and duration is greater than one, the selected record’s
flags 0x04 and 0x08 are cleared and, with span = duration - 1:
- each angle offset component is the bound angle plus the start angle plus
delta * elapsed / span; - the distance is the start distance plus
(record distance - start distance) * elapsed / span;
products wrap as signed 32-bit values and division truncates toward zero. The task returns one.
Otherwise (the release step finished, or duration is one or less) the selected
record receives the target as its angle offset and its own distance, flags 0x01
and 0x20 are cleared, the showPlayer step runs (it increments the private word
again, selects the requested slot through the selection guard and clears flags
0x21 on it) and the task returns zero. Only record two is edited, so the slot’s
own record is unchanged afterwards.
Location camera request
The first invocation of the location-camera handler (private word one, elapsed
zero) sets flag 0x01 on the record that is selected at that moment, begins a
scripted camera (see below), binds the record that step selected (record two) to
the target actor, applies the actor camera offsets to
it, and then writes its request from the three task arguments. The flag set on
the first record stays set: after the handler, record zero (when it was the
selected record) keeps flag 0x01.
The binding selects one of two position and angle vector pairs on the actor, chosen by attribute byte 26 (zero or nonzero). The camera therefore orbits the target actor, not the player.
The arguments are shifted left eight bits. That vector and the record’s
position offset (as just replaced from the actor attributes) are each rotated
by the bound actor’s angles. Flag 0x01 is set on the bound record again, and
the actor’s attr_camera_move byte then selects:
attr_camera_move | Request | Flags |
|---|---|---|
| 0 | rotated argument vector + rotated offset + bound position | set 0x08, clear 0x04 |
| 1 | rotated argument vector + rotated offset | set 0x04, clear 0x08 |
| 2 | record distance = rounded length of the rotated argument vector >> 8; angle offset = angles of that vector (the camera orientation conversion) plus the bound actor’s angles, each wrapped to 2047 | clear 0x08 and 0x04, set 0x01 |
Other values write no request and leave the flags as the begin step set them.
The handler then runs one camera pose update with the bound record selected,
clears flag 0x01 on the selected record, and returns one. Later invocations run
the scripted-camera release step with argument zero and return zero.
showObj is the same handler with the argument vector used as given instead of
rotated by the bound actor’s angles (only the record’s position offset is
rotated); its requests are therefore vector + rotated offset + bound position
(attr_camera_move 0), vector + rotated offset (1) and the orbit of the unrotated
vector (2). The showCyrus wrappers of both handlers return zero without touching
the camera while the selected player is holding a weapon, and otherwise run the
handler with the selected player as the target actor.
Object pans
showObjPan and showObjPanLoc take a target actor, three vector arguments and a
duration, and move the camera from where it is towards a request over duration
ticks. The vector arguments are shifted left eight bits. A negative duration writes
a diagnostic line and the task returns zero before touching the private word or any
camera state. Both share the first invocation’s setup with the
location camera: begin a scripted camera, bind the
selected record (record two) to the target actor, apply the
actor camera offsets, and rotate the record’s position
offset by the bound actor’s angles. They differ in two ways:
showObjPan | showObjPanLoc | |
|---|---|---|
| Vector | used as given | rotated by the bound actor’s angles |
Absolute request (attr_camera_move 0) | vector + bound position | vector + rotated offset + bound position |
Flag 0x40 | set on the record selected at entry (before the negative-duration check) and on the selected record at completion | not touched |
The first invocation (private word one, elapsed zero) records the start state: the
published camera position minus the bound position, as the start position; that
position minus the rotated offset, whose rounded length shifted right eight is the
start distance and whose camera orientation conversion is the start angles; the
vector’s rounded length shifted right eight as the final distance; and the delta
from the start angles to the vector’s angles, each component wrapped once into half a
turn either side of zero. No flag 0x01 is set and no pose update runs; the normal
camera update publishes the result.
Every invocation runs the scripted-camera release step with duration. While it
reports running and duration is greater than one, with span = duration - 1, the
actor’s attr_camera_pan_type selects the interpolation:
- one: the selected record’s request is the start position plus
(vector + rotated offset - start position) * elapsed / spanper component, with flag0x04set and0x08cleared; - any other value: flags
0x04and0x08are cleared, each angle offset component is the bound angle plus the start angle plusdelta * elapsed / span, and the distance is the start distance plus(final distance - start distance) * elapsed / span.
Products wrap as signed 32-bit values and division truncates toward zero. The task returns one.
Otherwise the request attr_camera_move selects is written exactly as the location
camera writes it (with the absolute request of the table above, the relative request
being vector + rotated offset, and the orbit using the vector alone), flag 0x01 is
cleared on the selected record, and the task returns zero.
The showCyrusPan and showCyrusPanLoc wrappers apply the same holding-weapon
guard as the location wrappers and then run the handler
with the selected player as the target actor.
World-position camera requests
showObjWorld(x, y, z) is a location camera whose arguments are a world position. Its
vector is (argument - P) << 8 per component, where P is the position offset of the
record that is selected when the task starts (the arguments are not shifted before the
subtraction). The first invocation sets flags 0x20 and 0x01 on that record, begins a
scripted camera, binds record two to the target actor, applies the
actor camera offsets, sets flag 0x01 again and writes the request
attr_camera_move selects. Nothing is rotated and the offsets are not added:
attr_camera_move | Request | Flags |
|---|---|---|
| 0 | the vector | set 0x08, clear 0x04 |
| 1 | vector - bound position | set 0x04, clear 0x08 |
| 2 | orbit of vector - the actor’s own position (not the bound pose, which differs for actors with attribute byte 26 set), as for the location camera | clear 0x08 and 0x04, set 0x01 |
It then runs one camera pose update, clears flag 0x01 on the selected record and returns
one; later invocations run the release step with argument zero and return zero.
showObjWorldPan(x, y, z, duration) returns zero for a negative duration without touching
the camera. Otherwise it runs showObjPan with the arguments (argument << 8) - B per
component, where B is the bound position of the record selected at the call, and
showObjPan shifts them left eight bits again.
showObjRot is declared as a task with eight script arguments. Its handler
ignores their values, changes no camera or actor state, and returns zero.
lookObj
lookObj(duration) turns the camera in place to face the target actor. A negative
duration writes a diagnostic line and the task returns zero before touching the
private word or any camera state.
The first invocation (private word one, elapsed zero) begins a scripted camera, binds
the selected record (record two) to the target actor and applies the
actor camera offsets. It keeps the published camera angles as
the start angles and runs the camera’s orientation stage at the published position,
with the record’s target offset, to get the angles that face the target; the delta is
the difference from the start angles with each component wrapped once into half a turn
either side of zero. It then sets flags 0x08 and 0x02 (fixed orientation) on the
record, writes the published position as the record’s request, and returns one. The
release step does not run on this invocation.
Every later invocation runs the scripted-camera release step with duration. While it
reports running and duration is greater than one, the published camera angles are
written directly: start angle plus delta * elapsed / (duration - 1) per component
(products wrap as signed 32-bit values, division truncates toward zero), and the task
returns one. Otherwise flag 0x02 is cleared and the actor’s attr_camera_move
selects the request, using the published position P, the bound position B and the
record’s position offset O:
attr_camera_move | Request | Flags |
|---|---|---|
| 0 | P | set 0x08, clear 0x04 |
| 1 | P - B - O | set 0x04, clear 0x08 |
| 2 | orbit of P - B - O, as for the location camera | clear 0x08 and 0x04, set 0x01 |
Other values write no request. The task then returns zero.
Scripted camera begin and release
Beginning a scripted camera sets the scripted-camera active word to one,
requests slot two (the selection guard applies), clears the first twelve bytes
of the selected record twice, increments the nesting count, resets the record’s
flags to 0x40 and sets flag 0x20.
The release step takes a duration argument. It selects slot two’s record,
raising a shared reselection mark when the selected record changed. While
duration - 1 exceeds the task’s elapsed input it returns one. Otherwise it
decrements the nesting count, clears the active word when the count reaches
zero, and returns zero.
Camera marks and the render camera step
Besides the published (logical) pose the camera keeps a render pose: three floats holding the position in whole map units and three angles. Four marks decide each frame whether the render pose glides towards the published pose or cuts to it.
| Mark | Set by | Meaning |
|---|---|---|
| snap | camera setup; the first showPlayer call; the first showObj, showObjLoc and showObjWorld call; loading and pause paths | a cut is pending |
| hold | later showPlayer calls (so the showPlayer that ends a showPlayerPan too); the first showObjLoc call | forces a cut |
| forced glide | showObjPan and showObjPanLoc, on entry before the negative-duration test | forces a glide, and skips the pose update’s obstacle segment check for the frame |
| in frame | the main loop, at the start of each frame | the frame is a normal one |
Camera setup clears hold and forced glide and cuts the render pose to the first published
pose (position shifted right eight bits, truncating toward zero; the angles unchanged).
lookObj and showObjRot set no mark.
The render step runs once per frame after the pose update. First it clamps the render
position’s x and z floats into [0, 65535]. Then it glides when the following chain
reports a glide, the snap mark is clear and the frame mark is set:
- hold set: cut;
- forced glide set: glide;
- the selected player holds a weapon, or is associated with a rope: glide;
- the scripted-camera active word is nonzero: cut;
- snap clear: glide;
- the selected record has flag
0x20: cut; - selected index zero, three or four: glide; otherwise cut.
A glide uses the installed [camera] factors glide_x, glide_y, glide_z
(0.9, 0.2, 0.9) and glide_angle_x, glide_angle_y, glide_angle_z (0.7, 0.1, 0.7).
With p the published position shifted right eight bits as a single float and r the
render position float, each axis becomes r + (p - r) * factor evaluated in extended
precision and stored as a single. The result times 256, truncated toward zero, goes through
the camera terrain correction, is divided by 256 truncating toward zero, and becomes the new
render float. Each published angle is replaced by
(previous + trunc((published - previous) * factor)) & 2047, where previous is the
render angle, the difference is wrapped once into half a turn either side of zero, the
single-precision product is truncated toward zero, and the factor for the first (pitch) angle
is glide_angle_y and for the second (yaw) angle glide_angle_x; the third uses
glide_angle_z. The published angles are therefore smoothed in place.
A cut sets the render position to the published position shifted right eight bits (truncating toward zero) and clears snap and hold. Every pass clears forced glide, and the final published angles become the render angles for the next frame.
Follow camera observation
The debug overlay’s World (x, y, z)-(pitch, yaw, roll) line is the published camera pose
and its Player (x, y, z)-(pitch, yaw, roll) line the selected player’s, in whole map units
and 2048-unit angles. With the installed defaults (record zero position offset (0, -14000, 0),
distance 250, target offset (-6000, -14000, 0)), a player at (32464, -275, 33892) with
angles (0, 22, 0) gave the camera (32481, -330, 33642) with angles (0, 2027, 0): the
camera sits 250 units behind the player along the player’s heading, 55 units above, and looks
at the player’s height. The angle triple’s orientation matrix carries local +Z along the look
direction with the X axis mirrored.
Frame view angles and screen shake
After the render step the frame builds its view orientation from the published angles. A roll
tilt would be selected by a mode word that nothing writes (zero), so no tilt is applied. A
screen-shake level (0 to 32) wobbles the first angle: on even frames of the main loop’s
frame counter the level is subtracted from the angle and then halved, on odd frames it is added
and kept; each angle is then wrapped to 2047. One shake impulse raises the level by 32, capped
at 32. Impulses come from the actor-event service (operand
bit 0x10) and a debug key.
Ordinary camera selection inhibition
On the ordinary camera-update path, a nonzero scripted-camera active word or
bit 0x20 in the selected camera record suppresses automatic slot reselection.
The camera still runs its pose-processing stage. These guards precede the
holding-weapon and other player-state checks used to choose an automatic slot;
they do not suspend the whole camera update.
When neither guard applies, the ordinary camera-update path requests the slot from the player camera slot selection table and falls back to slot zero when no condition applies, so a player that sheathes its weapon returns to slot zero on the next update.
Camera request candidate position
The camera’s initial position-composition stage gives record flag 0x04
priority over 0x08. With 0x04, it adds the stored request vector to the
record’s bound actor-position vector, with wrapping signed 32-bit components.
Otherwise, with 0x08, it copies the stored request vector directly. These are
candidate positions supplied to subsequent camera processing, not final rendered
camera poses.
Before testing either request flag, this stage compares the stored distance shifted left by eight against the stored minimum, using signed comparison. If it is lower, the minimum replaces the distance without a compensating shift. Equality leaves the distance unchanged.
Without either explicit request flag, the stage rotates the record’s position
offset by the bound angles. It separately constructs a vector with zero X and Y
and negative distance shifted left by eight as Z. The angles for this second
rotation are the bound angles minus the record’s angle offsets, masked to one
revolution. Flag 0x80 replaces the first of these angles with the negated first
angle offset, ignoring that bound-angle component. The candidate is the bound
position plus both rotated vectors, with wrapping signed components.
Camera vector rotation builds an identity matrix, applies negative yaw, then positive pitch and roll, and transforms the supplied integer vector. These angle signs differ from the ordinary actor-matrix update convention.
Camera collision candidate radius
SYSTEM.INI [camera] obstacle_size supplies the candidate-size cutoff. Setup
stores it with eight fractional bits, and collection shifts it back before
comparing with each candidate model’s radius. The model radius must be strictly
greater than this cutoff. This is separate from the added bounds-test radius
computed from the queried segment.
Candidate collection subtracts the fixed-point endpoints before arithmetic-shifting the displacement components right by eight. It separately shifts the starting point by eight. Each shifted component is squared with a wrapping signed 32-bit multiply, made non-negative with a wrapping absolute value (the most negative value stays negative), and the three results are summed with 32-bit wrap. The added model-bound radius is twice the floor of the integer square root of that sum read as an unsigned 32-bit value, so a displacement long enough to wrap still yields a radius. The center-distance test accepts equality with the squared sum of the model radius and this added radius.
The bounds test stores each integer-origin minus model-center component at single precision, then stores their summed squared distance at single precision. It also stores the model radius plus added radius at single precision before squaring that combined radius for comparison. Equality admits the model.
Camera collision candidate eligibility
Candidate collection scans two linked object lists in order and appends model references to one candidate array. A model equal to the caller’s excluded model is skipped in both lists.
Entries of the first list carry a model and a bounds record. An entry is a candidate when its model and bounds are present, the bounds radius is strictly greater than the obstacle cutoff, and the segment bounds test accepts the model.
Entries of the second list are actors. An actor is a candidate when it has a model bounds record, is not excluded from ordinary object updates, and then passes the same model, radius and bounds tests. The bounds-record flag is set when the actor’s script and model load establishes its bounds; the exclusion is the flag the ordinary object-update loop tests.
The candidate array holds 64 entries. When it already holds 64, the engine writes a diagnostic line instead of appending and stops scanning the current list; the remaining entries of that list are dropped.
Camera collision result selection
The query arithmetic-shifts each fixed-point endpoint component right by eight before the geometry test. The integer wrapper converts returned hit positions with the active x87 integer rounding mode and scales normal components by 256 before converting them. Selected output positions are shifted left by eight; selected output normals retain the wrapper’s scale.
Polygon hits store their position along the tested segment as a rank: the segment parameter multiplied by 32768, converted with the active x87 integer rounding mode. The segment start has rank zero and the end has rank 32768.
The camera collision query counts all returned hit records independently of whether a record replaces either selected output. For segment ranks from zero through 32768, minimum selection starts at 32767 and requires a strictly lower rank. Maximum selection starts at zero and requires a strictly higher rank. Equal ranks retain the first selected record.
A result containing only ranks 32767 or 32768 therefore reports hits without replacing the shared minimum-position output. A result containing only rank zero leaves the shared maximum-position output unchanged. The query does not clear these output positions before scanning its candidates.
Camera collision response
The pose collision stage runs when the selected camera record has flag 0x40
and the shared collision-inhibition word is zero. It rotates the record’s target
offset using its bound angles, adds the bound position, and queries the segment
from that target to the candidate camera position. Candidate collection excludes
the bound actor’s model instance.
A nonzero query hit count replaces the candidate camera position with the shared minimum-position output and reports success. A zero hit count leaves the candidate unchanged. The response does not separately check whether the query replaced its minimum output. An endpoint-only hit can therefore copy the retained minimum position from before the query.
Camera movement limiting
Camera record flag 0x01 bypasses the movement-limiting stage. Otherwise the
stage uses both the last published camera position and the preceding position.
Its scalar timing helper floors the signed frame scale at 180, multiplies with
32-bit wrapping, and arithmetic-shifts the product right by eight.
The movement budget starts with the distance between those two retained positions plus the shared budget increment, capped by the record’s unsigned maximum. The timing helper scales that budget. When the candidate displacement exceeds it, the stage forms an integer fraction with eight fractional bits and scales displacement components with multiplication followed by an arithmetic right shift. It reports that limiting occurred even when integer quantization leaves a component unchanged.
A separate vertical limit precedes this distance test except for camera slot one and the selected-player special-state branch. The lower bound is the timed vertical parameter. A displacement above its negation is replaced with the negation of the unscaled parameter. The two sides therefore do not use the same timing conversion. The selected-player special-state branch also retains the candidate’s horizontal components during distance limiting while still applying the vertical component and reporting limiting.
The shared budget increment is SYSTEM.INI [camera] max_acc. The vertical
parameter is [camera] max_y_vel negated before storage, so it is the negative
lower bound described above. Both are stored without scaling. Setup stores them
with the other shared camera values, and the same values are reloaded when the
selected player’s camera defaults are reloaded.
Camera orientation retention
Camera record flag 0x02 makes the orientation stage copy the previously
published camera angles into the new pose. This branch skips target-direction
calculation, leaves the record’s bound-position history unchanged, and does not
refresh the two transformed camera vectors.
Without this flag, slots zero and one use only the target offset’s vertical component; other slots use the full vector. The stage rotates that offset by the bound angles, adds the bound actor position, and derives the camera angles from the direction between the published camera position and that target. It also refreshes the bound-position history and transforms both stored camera vectors using the derived angles.
Before angle conversion, the direction components are stored as single-precision values. Their Euclidean length divided by 65536 is stored at single precision. Each component is divided by that stored divisor, stored at single precision again, and converted to an integer with the active x87 rounding mode. A zero divisor produces a zero vector. Angle lookup consumes this quantized direction.
The direction is camera position minus the rotated target position. Using the signed bearing lookup whose positive X axis is a quarter turn and positive Z axis is zero, yaw is the X/Z bearing minus half a turn. Pitch is the bearing of the rounded horizontal length against Y, minus a quarter turn. Both wrap to 2048 units per revolution; roll is zero.
The two retained basis vectors use a direct angle matrix with positive pitch, negative yaw and positive roll. Each output component is a dot product with a matrix column. Vector inputs and dot products are stored at single precision; final integer components use the active x87 rounding mode.
Camera contact-normal response
The camera body-response stage converts the candidate fixed-point position to floating coordinates and gathers contact normals. It sums normal components after dividing their integer representation by 256. A nonzero sum is normalized. If the sum cancels to zero, it instead uses the first contact normal divided by 256, without normalizing that fallback.
The response truncates the camera body’s floating radius to an integer, multiplies it by the selected response direction, scales the result by 256, truncates each component toward zero, and adds it to the candidate position. No contacts leave the candidate unchanged. Opposing contacts can therefore produce a response in the first contact’s direction rather than no response.
Each accumulated normal component is stored at single precision after each addition. Normalization stores the reciprocal length at single precision, then stores each scaled direction component at single precision. Each product of the integer radius and selected direction is also stored at single precision before scaling by 256 and truncating toward zero.
Camera distance-height correction
Only slots zero and three run the distance-height correction. It subtracts the bound actor position from the candidate, divides each signed fixed-point delta by 256 with truncation toward zero, and computes the resulting distance. A zero distance is replaced with 0.1 for the following comparison and division.
When that distance is strictly below the record’s distance threshold, the stage scales the vertical delta by threshold divided by distance. It replaces candidate Y with bound actor Y minus the absolute scaled vertical delta converted back to fixed point with truncation toward zero. Candidate X and Z remain unchanged. Equality with the threshold leaves the candidate unchanged.
The divided deltas, summed squared distance, square root and threshold are stored as single-precision values. The vertical division and subsequent threshold multiplication each store a single-precision result before conversion back to fixed point.
Camera terrain-height clamp
When the terrain-height stage is enabled, both terrain queries receive candidate X and Z arithmetic-shifted right by eight. The floating height result is truncated toward zero before conversion back to fixed point. The camera body’s stored clearance parameter is multiplied by 1024 and truncated separately.
Only a candidate Y plus clearance strictly greater than the converted terrain height triggers correction. The replacement Y is terrain height minus clearance; X and Z remain unchanged, and the shared correction flag is set. Equality does not set the flag or replace the pose.
The terrain query converts horizontal coordinates to cells with the active x87
integer rounding mode after applying x / 256 - 0.5 and z / 256 + 0.5.
When both cell offsets from the retained terrain-window center are strictly
between -16 and 16, height comes from that retained grid. At either boundary or
outside the window, it comes from map samples through the height lookup table.
The camera query therefore does not always use the static map heights.
Each selected cell uses a triangle plane rather than bilinear interpolation.
Its diagonal joins (i, j) to (i + 1, j - 1). When the horizontal offset
x - i * 256 is greater than j * 256 - z, the third corner is (i + 1, j);
otherwise it is (i, j - 1). Equality uses the latter triangle. The query
publishes a normalized plane normal; the integer wrapper scales that normal
by 256 and converts it using the active x87 integer rounding mode.
For plane construction, the first vertex is (i, j). In the greater-offset
branch the next vertices are (i + 1, j - 1) and (i + 1, j); otherwise they
are (i, j - 1) and (i + 1, j - 1). The two edges are second minus first
and third minus second. Each edge component is stored at single precision
before their cross product is formed. Replacing these adjacent edges with
mathematically equivalent edges can change the sampled height through rounding.
Camera pose stage order
The pose stage composes a candidate and performs a segment collision check. A hit replaces the candidate with the retained minimum hit position, then runs distance-height correction, contact-normal response and terrain correction.
A second segment check follows on both paths. A hit again replaces the candidate with the retained minimum position. The stage then runs distance-height correction, contact-normal response, movement limiting and terrain correction, in that order, before publishing the pose.
Camera pose publication
The transition word clears when the pose stage reports a collision or terrain correction. Without such a correction, it clears only when its value is one and movement limiting did not occur. A limited update can retain value one; other nonzero values also remain unchanged without correction.
After position correction, the pose stage moves the previously published camera position and angles into their respective history vectors. It then publishes the new position to shared camera state and to both the camera actor and alternate camera actor. Camera setup stores the CAMERA instance as both, so the two writes address the same actor. Orientation processing follows position publication, and its result replaces the shared published angles.
Position history therefore contains the preceding published pose, not the new request or a temporary collision candidate. Retaining orientation still advances angle history to the preceding published angles.
Camera record initialization
Camera setup clears all nine records, initializes record zero, and copies that
record into slots one through eight before applying slot-specific configuration.
The copied fields include the selected-player binding, position and angle
bindings, position offsets, angle offsets, distance limits and maximum movement
rate. Record zero has collision flag 0x40 before these copies.
The ordinary target-offset configuration is assigned to record zero after the copies. It therefore does not initialize the other records’ target offsets. Combat target offsets are assigned separately to slot three. The remaining slots retain their cleared target-offset vectors at this initialization stage.
Camera initial state
Camera setup selects record zero. The shared transition word, the scripted-camera active word and its nesting count are zero. Setup then computes the first published pose with one pass of the pose stage that omits movement limiting, and stores it as both the current and the previous pose.
Camera update
The CAMERA actor’s script body ends immediately. Its native hook,
cameraController, takes no arguments and is the camera update itself: the pose
update for the selected record’s bound actor that the location camera natives
also run. The hook runs once per ordinary update, at the camera actor’s position
in the ordinary actor traversal.
ResetRotation
ResetRotation(x, y, z) acts on the receiver and returns zero. The actor keeps a
triple of the pitch, yaw and roll it was created with (the angles it has when it
is allocated; the player swap copies it between player records and
XResetCamera sets it to the current angles). For each argument
that is nonzero, the receiver’s pitch (x), yaw (y) or roll (z) is replaced
by that retained component.
The previous-angle history then becomes the receiver’s current angles, and its orientation matrix is rebuilt. When the receiver’s rotation mode (attribute 28) is one, an alternate construction builds the matrix. Otherwise the matrix starts as the identity and takes rotations of the negated yaw, then pitch, then roll.
ReloadRotation
ReloadRotation() takes no arguments, acts on the receiver and returns zero. It
performs the second half of ResetRotation without restoring
any angle: the orientation matrix is rebuilt from the receiver’s current angles
(by the rotation mode’s construction, as above) and the previous-angle history
becomes the current angles.
ResetPosition
ResetPosition(x, y, z) acts on the receiver and returns zero. The actor keeps
the world position it was created at. For each argument that is nonzero, the
receiver’s local position on that axis (x, y, z) is replaced by the
retained component, even for an attached actor. The previous-position history
then becomes the receiver’s current local position.
When the receiver is the selected player, the shared view origin moves by the change the reset made on each axis. The receiver’s transform is then rebuilt.
MovePlayerAboveMe
MovePlayerAboveMe(x, y, z, angle) returns zero and asks for the selected player to be placed relative to the receiver. The offset
(x, y, z) is rotated by the receiver’s orientation matrix and added to the receiver’s position (the world position of
an attached actor, otherwise its local position, taken through the parent when it has one). The requested player Y is that
sum minus the selected player’s positive Y extent shifted left eight bits, minus the [system] standing_height value from SYSTEM.INI
shifted left eight bits; X and Z are the sum itself. The call stores this position and angle as one pending placement; a later call in the same frame replaces it.
The main loop applies the pending placement once per frame and then clears it. The selected player’s
local position and its previous-position history both take the requested position, its yaw becomes angle & 0x7ff,
and the camera is marked to cut instead of glide. The call has no other effect on the receiver.
XResetCamera
XResetCamera takes no arguments, acts on no receiver state and returns zero. In
order, it:
- Runs the
LockoutPlayerroutine for the selected player with zero. - Runs the
KillMyTasksoperation on the selected player and clears five flag bits (0x01,0x02,0x04,0x08,0x10) of the actor byte that immediately precedes its task arrays. - Rebuilds the selected player’s orientation matrix from its current angles and stores those angles in both of its retained angle-history triples.
- Sets the scripted-camera active word and its nesting count to zero.
- Applies step 2 to the camera actor.
- Reloads record zero’s player-type defaults,
selects record zero, resets its flags to
0x40and recomputes the first published pose as setup does.
Camera record installed settings
Camera setup reads SYSTEM.INI [camera] integers and stores them without
scaling except where stated. Record zero receives, before the copy to slots one
through eight:
| Record field | [camera] keys |
|---|---|
| Position offset | offset_pos_x, offset_pos_y, offset_pos_z |
| Angle offset | offset_angle_x, offset_angle_y, offset_angle_z |
| Distance | camera_distance |
| Minimum distance | camera_min_distance |
| Maximum movement rate | max_vel |
| Left position limit | camera_left_pos |
| Right position limit | camera_right_pos, negated |
Record zero also gains flag 0x40. target_offset_x, target_offset_y and
target_offset_z are assigned to record zero after the copy, and to slot three
from the same keys.
Slot-specific settings follow the copy. Each replaces the slot’s angle offset and
distance; a nonzero *_max_vel replaces its maximum movement rate, and a zero
value keeps the copied record-zero rate.
| Slot | Mode | Angle offset keys | Distance key | Maximum movement key |
|---|---|---|---|---|
| 3 | combat | camera_combat_angle_offset_x/y/z | camera_combat_distance | camera_combat_max_vel |
| 4 | hanging | camera_hang_angle_offset_x/y/z | camera_hang_distance | camera_hang_max_vel |
| 5 | rope | camera_rope_angle_offset_x/y/z | camera_rope_distance | camera_rope_max_vel |
| 6 | rope above | camera_rope_above_angle_offset_x/y/z | camera_rope_above_distance | camera_rope_above_max_vel |
| 7 | rope below | camera_rope_below_angle_offset_x/y/z | camera_rope_below_distance | camera_rope_below_max_vel |
| 8 | debug | camera_debug_angle_offset_x/y/z (see below) | camera_debug_distance | camera_debug_max_vel |
The debug slot’s three angle-offset reads all store to the first angle-offset field; its second and third angle offsets keep the copied record-zero values.
Slot three gains flag 0x40 (already present from the copy). Slots six and seven
additionally load aim vectors from camera_rope_above_aim_x/y/z and
camera_rope_below_aim_x/y/z, bind the record’s aim reference to them, and set
their flags to exactly 0x40. Slots four and eight keep the copied flags. Slot five gains flag 0x80 in addition to 0x40, giving 0xc0.
Slots one, two and four through eight keep their cleared target-offset vectors.
obstacle_size and fudge_factor are shifted left eight bits and max_y_vel is
negated before storage in shared camera state, outside the records.
SheathSword
SheathSword takes no arguments and returns zero. It acts only when all of these
hold for the receiver: the task system is enabled, the receiver has animation state and
a combat record, the combat cooldown is zero, the lifecycle byte is not in 27 through 32,
the receiver is neither drawing nor sheathing a sword (the IsDrawingSword and
IsSheathingSword predicates), and IsHoldingWeapon is true. Otherwise it changes
nothing.
When it acts, the lifecycle byte becomes 21 (sheathing), extra_time is
cleared, bit 0x04 of the combat flags is cleared, and the combat cooldown becomes 24.
The independent rope_wait companion motion byte is retained.
For the selected player it additionally sets the HUD health-display timer to 25 and selects
camera slot zero.
endCombat
endCombat takes no arguments and returns zero. It first does what SheathSword does for
the receiver, including its eligibility tests. It then unconditionally sets the lifecycle selector
(MyAIType) to 1 and the AI mode (MyAIMode) to 0, re-enables the receiver’s native hook as
EnableHook does, clears combat flag 0x04, and zeroes the combat record’s close-distance pass counter and
coordination selection state. A receiver without a combat record faults in the original.
FacePlayer
FacePlayer(duration) is a task that turns the receiver’s local Y angle toward the
selected player. Each invocation computes the bearing from the receiver’s local position to
the selected player’s local position as the arctangent angle of the displacement, which
is the negation (modulo 2048) of the bearing the PlayerLooking query reports, then:
gap = (-heading - angle) & 2047, which is thePlayerLookingbearing minus the heading. A zero gap completes the task without a change.remaining = |elapsed - duration|, treated as 1 when zero.- If
gapis above 1024,gap = (-gap - 1024) & 1023and the step is subtracted from the heading; otherwise the step is added. step = ((gap << 8) / remaining) >> 8. A zero step completes the task without a change.- The heading wraps modulo 2048.
After a turn the task completes when elapsed >= duration - 1, and remains active
otherwise.
FacePlayerInertia
FacePlayerInertia(duration) follows FacePlayer except for the step. After the gap is
reduced as FacePlayer does (a gap above 1024 becomes (-gap - 1024) & 1023 and subtracts), the step is
(((gap << 8) / duration) - elapsed) >> 8 with signed division and an arithmetic shift; a duration of
zero is a division fault. A zero step, including a negative one that shifts to zero, completes the task
without a change. The turn, the wrap and the completion test are the same.
Potion counters and combat strength and armor
SetPotion(index, value) stores the full 32-bit value in a global counter table and
returns one. GetPotion(index) returns the stored value. The table is zero-filled when
the engine starts and is not cleared by world changes. The shipped scripts use indices
0 through 3 (blood, milk, ectoplasm and sap items).
setStrength(value) and setArmor(value) store the full 32-bit value in the strength and
armor fields of the receiver’s combat record and return nothing. They apply to a receiver
that has a combat record.
Nonplayer combat decision: target, distance and engagement
The nonplayer decision stage (death and state selector 6, actor not dead) begins as follows.
The decision entry caches the current 32-bit clock sample for combat pacing and timed
taunts. In normal clock mode this is the raw BIOS-clock sample, interpreted as signed.
It is distinct from nominal-tick increments and from the world-loop frame counter.
The world-loop counter resets to zero on loop entry and increments once at the beginning
of each iteration. In the waiting branch, after spacing, one random draw admits a
chance taunt when draw * 100 / 32767 < 2. The taunt type is 5 when the wrapping sum
of the receiver’s allocation index and world-loop counter is even, and 8 when odd.
Timed selection of that requested type still uses the cached clock sample. The
active branch ends the same way: after spacing, counter-attack selection and
defence selection, one random draw admits a chance taunt under the same
draw * 100 / 32767 < 2 test, and the requested type is always 5.
Valid target. An actor is a valid combat target when it exists, it is not both in lifecycle state 8 and killed, it has a combat record, and its own combat target is not itself.
Target selection. When the receiver’s combat target is not valid, the target becomes the selected player, whether or not the player is itself a valid target, and the target’s local position is copied into the receiver’s retained target position. A valid target leaves that position unchanged until engagement succeeds.
Distance and counter. The distance from the receiver to its target uses the query positions of both actors (attached, linked or local, as for the fighting query) and the three-dimensional rounded distance. While that distance is below 25000 a counter in the combat record increments once per pass with signed 32-bit wrapping; otherwise the counter is zero. Combat construction and reinitialization clear this counter and the retained target position. After engagement succeeds, the target’s local position replaces the retained target position before the defence and attack executors run, in that order, each with request -1. An active result from either executor ends this decision pass before spacing and further selections.
Engagement. The receiver is engaged when attr_combat_ignore_in_sight is nonzero.
Otherwise, with attr_combat_long_sight nonzero, it is engaged when the target is in sight
within a distance of 0x80000; with it zero, within 0x40000. Both distances use the
position’s eight fractional bits.
In sight. The test is false for an absent actor or target and when their distance exceeds the limit. Otherwise the segment between the two query positions, in whole map units, is tested against each entry of the engine’s retained candidate model list, in order. The test does not collect candidates itself: it reads the list collected for the receiver by ordinary actor traversal immediately before the receiver’s update (see Candidate collection for an ordinary update), so only models near the receiver can block the line. An entry that is not hit, or whose owner is the target, does not block; any other hit makes the result false. The result is true when no entry blocks.
Nonplayer defence selection
The defence selection runs only while the receiver is armed and the
selected-player hit-inhibition flag is clear. Standalone-player initialization clears
this flag; selected-player health death sets it. With no move selected, the target must lie inside the receiver’s
forward arc and its distance must be strictly below the receiver’s far window.
The forward-arc query subtracts the receiver’s local position from the target’s
local position with signed 32-bit wrapping arithmetic, then arithmetic-shifts each
coordinate right by eight. It obtains the normalized vector’s orientation and
compares the receiver’s local yaw with (1024 - vector_yaw) & 2047. The absolute
wrapped difference must be strictly below 256 heading units. Composed query poses
are not substituted for these local positions.
The defence counter becomes one when the headings differ by less than 256 heading
units or the target is outside combat stance. Otherwise it becomes 129 - attribute 58.
A target is in combat stance when it is armed or its combat flag mask 0x04 is set.
The flag query returns false without a combat record; the action query returns -1.
Heading differences mask both headings to 0 through 2047 and wrap the difference
into -1024 through 1024.
The pacing sum is the receiver’s per-actor combat value plus the shared tick counter, with signed 32-bit wrapping addition. When its signed remainder by the defence pacing divisor is zero:
- For a target move other than -1 or 9, the target’s signed animation command
cursor must exceed the wrapping sum of its current motion’s
first_collide_frameandcollide_duration. The current attack slot and motion selector choose that motion. This uses neither the displayed model frame nor the wrapping frame-progress byte. The receiver gains intent mask0x01, then asks the best-usable-defence picker for a kind. A successful pick starts only if its reach test succeeds. - For target move 9, the open-defence picker supplies a kind, which starts only if the pick and reach test succeed. This branch does not set the intent mask.
- For target move -1, neither of these picks runs.
After this branch, independently of its pacing remainder, a zero signed remainder
of the pacing sum by the defence counter sets intent mask 0x01, obtains the paced
pick and starts it if the reach test succeeds. Starts happen synchronously, before
any later picker reads the combat state.
With a move already selected, the selection uses only the defence counter remainder.
On zero remainder, defence selector zero requests kind one and selector one requests
kind two; either request first sets intent mask 0x01. Other selectors do nothing.
Intent updates preserve every other combat flag bit.
Best and open defence picks
Both pickers scan the receiver’s nine authored attack slots in ascending slot order. These are the attack slots bound from attributes 70 through 78, not the separate four defence records. The first motion supplies each slot’s distance window, near timing and counter-attack reference. Absent slots are skipped. A slot is usable only when the signed actor-to-target distance is strictly greater than its minimum range and strictly less than its maximum range. The best picker chooses the usable slot with the lowest signed near timing value, strictly below 65536. Equal values retain the earlier slot.
The open picker applies the same rules, but excludes a slot whose one-based counter-attack reference minus one equals the receiver’s current move’s counter-attack index. The subtraction wraps as signed 32-bit arithmetic. Best selection does not read counter-attack references. Open selection reads the candidate counter-attack reference, then queries the current move’s counter-attack index, only after that slot passes its distance window. An excluded slot does not read near timing. Each admitted slot repeats the current-move query. Both pickers map a winning slot of six, seven or eight to defence kind four. If no slot qualifies, they report failure without changing the caller’s output.
The current-move counter-attack query returns zero when the receiver has no combat record or its action selector is -1 or 9. These cases do not read attack or motion metadata. Otherwise, it resolves the receiver’s retained attack-slot selector and current motion selector, reads that motion’s authored one-based counter-attack reference and subtracts one with signed 32-bit wrapping. The action selector gates the query but is not the attack-table index.
Paced defence pick
The initial choice is zero. One random draw in the inclusive range zero through
32767 produces draw * 6 / 32767, with unsigned integer division. That slot
replaces the initial choice only if it exists and passes the strict distance
window. The maximum draw therefore probes slot six. This fallback does not
require arc or elevation admission and does not remap the slot.
The picker then scans slots zero through five in ascending order. A populated slot must pass distance, arc and elevation admission, in that order. A failed test prevents the later tests for that slot. The fallback reads only existence and the distance window. Neither path reads the counter-attack reference. If the receiver and target face alike, or the target is outside combat stance, a slot replaces the choice only when its signed accumulated damage exceeds the retained damage, initially zero, and the pacing sum has zero signed remainder by the defence pacing divisor. A successful replacement retains that damage. Equal damage totals retain the previous choice.
Otherwise, each admitted slot resets the defence counter to 129 - attribute 58
before reading its near timing. This branch does not read accumulated damage.
The picker tracks the lowest near timing strictly below 65536, with the same
zero-remainder gate, but this branch does not replace the chosen slot. The final
return is the fallback or the last priority-branch replacement. No qualifying
slot leaves the initial choice of zero intact.
Defence elevation and arc admission
Both tests read the first motion of the receiver’s selected attack slot.
Elevation admission uses the receiver and target’s bounds-query positions.
For each coordinate, that query computes the signed 32-bit wrapping expression
position - (negative_extent << 8) + (positive_extent << 8), then divides by two
with truncation toward zero. It does not substitute the actor origin.
Let d be target bounds-query y minus receiver bounds-query y, with signed
32-bit wrapping subtraction. Let w be the wrapping sum of the receiver’s
positive and negative x extents, each shifted left eight bits, and let h be
w / 2, truncating toward zero. The required elevation is:
- One when
-h > dand-(w + h) < d. - Otherwise two when
d > handw + h > d. - Otherwise zero.
All additions and negations in those comparisons wrap as signed 32-bit values. The motion passes only when its elevation equals the required value. Band edges belong to elevation zero; they do not pass either outer band.
Arc admission reads both actors’ ordinary local origins and the receiver’s local
heading, without attached or linked query-position substitution. It subtracts
receiver position from target position in each coordinate
with signed 32-bit wrapping subtraction and arithmetic-shifts each difference
right eight bits. The bearing input is ordered (delta_z, delta_y, delta_x)
and normalized to signed Q16 using the authored-location normalization.
The normalized vector uses the signed polar lookup described by
camera orientation: its positive first axis is
a quarter turn, its positive third axis is zero, and the lookup result is reduced
by half a turn and wrapped modulo 2048. A zero horizontal vector yields 1024.
Let a be the
absolute wrapped heading difference between the receiver heading and that bearing,
and let c be the motion’s arc centre plus 512. The test passes only when
c + arc > a and c - arc < a, using signed 32-bit wrapping arithmetic for the
bounds. It does not halve the configured arc or include either boundary.
Defence start admission
The starter dispatch admits kinds zero through six and kind eight only when the corresponding receiver attack slot is populated. Kind seven and values outside zero through eight do nothing. Admission differs by kind:
- Kind zero, three, five and six require no selected move.
- For a nonplayer receiver, kinds one and two do not require an idle selection. For the selected player, kind one requires selected move zero and kind two requires selected move one.
- Kinds four and eight require zero combat cooldown, neither drawing nor sheathing, and no selected move.
An admitted start first writes its kind into the defence selector, then selects
that move, then invokes the defence executor with request zero synchronously.
The executor can therefore change state before a later picker or starter reads it.
It resolves the defence selector through the receiver’s nine attack slots and
uses the move selector to choose that attack’s motion. When the actor leaves the
expected animation group, a populated following motion admits the next chain
step, up to the third motion. The test reads the following motion’s first word, not the
current one’s. Motion loading stores one in that first word for every motion it fills,
whatever the authored type is (the authored type lives in a separate field), and unfilled
motion slots keep their zero-initialized word. A combo therefore chains through all of its
referenced moves.
For selected-player kind four, the movement update precedes those writes and uses
attribute 16 shifted left eight bits with movement mode one. Its return value does
not gate the subsequent defence start. The nonplayer kind-four path omits that
movement update.
When the defence executor starts or advances to a new motion, it requests the
animation group, then sets the animation record’s finite loop countdown to one.
This is the same countdown used by PushAnimation and the animation interpreter.
The move selector is written before the animation request. After the request and
loop-count write, a start clears target mark 0x10, sets executor state one,
raises intent bit one, clears mask 0x20 and subtracts ten from aggression, then
synchronously requests a type-one timed taunt. A chain step clears the target
mark, raises intent and subtracts ten before clearing mask 0x20; it does not
request another type-one taunt. Animation services therefore run before these
combat-state changes, and the taunt service sees the committed start state.
DisableHook and EnableHook
DisableHook sets and EnableHook clears mask 0x02 of the receiver’s script-control byte
(the native-hook suppression flag above) and changes nothing else. Both take no arguments
and return zero.
GetFramePassed
GetFramePassed takes no arguments and returns the shared whole nominal-tick increment of
the current update (the same value the elapsed-time tasks consume).
ShowItem and ShowItemNoRtx
ShowItem(item, text, first, second) and ShowItemNoRtx(item, first, second)
return zero and only leave a request for the main loop’s item viewer. Each stores
the item identifier and the two trailing words and marks a request pending. ShowItem
ignores its second argument and leaves the viewer’s no-text flag as it was;
ShowItemNoRtx sets that flag. The flag stays set until the viewer consumes the
request. The handlers perform no display and read no inventory state.
Item viewer
After the world frame is drawn, the main loop runs the examine view in
mode 0 for a pending request with the item identifier and the two request words, then clears the
pending flag. The view does nothing for an identifier absent from the item list. The ShowItem text
argument is not used. Every installed script call passes zero for both request words. The model
flies in from the offset (first, second, 1490) from its resting place and, when the view closes,
flies to (second, first, 1490) in the same coordinates (the exit swaps the two words). Pose,
controls, fade and the close rule are those of the examine view, whose close requests are View,
Activate, Enter and Escape. When it closes the no-text flag is cleared.
Description lines are RTX records looked up by a four-character label (longer text is cut to four characters), played as speech with the label’s text drawn by the examine view unless the no-text flag is set. The first line depends on the item identifier:
| Item | First line | Later lines |
|---|---|---|
| 20, 22, 24, 26 | the item’s description[x] label | one more, cip and the decimal potion counter (GetPotion index (item - 20) / 2) plus one |
| 36 | cibe | cibl |
| 53 | iij2 | iij3 through iij8 |
| 55 | cikj | ny01 through ny11 |
| other | the item’s description[x] label | none |
With [system] sound on, each line plays as speech and the next line starts when that speech
ends; a line whose record has no speech shows its text and the sequence stops there until the
view returns. With sound off, no speech plays and each line lasts twice its text length in
updates. A label without an RTX record ends the sequence.
ViewGXA
ViewGXA(name, frame, count) is a task that shows one installed GXA image over the world
frame. name is a file name relative to the game directory (the shipped scripts name
system\stmap01.gxa through stmap17.gxa and system\mirror1.gxa through mirror3.gxa),
frame selects the image within the file and count is the number of task counter ticks.
The first invocation (counter zero, nothing loaded) loads the file. Every invocation whose
counter is below count - 1 places the selected frame centered on the 640 by 480 screen
(x is half of the 640 minus the frame width, y half of the 480 minus the frame height),
clears the second image slot and stays active. The invocation at counter count - 1 or later
discards the image, clears the drawn-image pointer and completes, so the image is drawn
for count - 1 frames (the same completion test as Wait). The main loop draws the pending image after the world frame.
DisplayHandItem
DisplayHandItem(item, x, y, z) is declared as a four-argument function. Its
native boundary receives an actor receiver, the item identifier and all three
offset words. The fourth argument is passed on the stack and removed on return.
The native uses the ordered item lookup: lookup disabled, an empty list, an
absent identifier or an earlier greater unsigned identifier produces no match.
A missing match requests a diagnostic before any hand-model operation.
For a matched item, the native reads its hand-model pointer and forwards the receiver, that pointer and the three offset words to the hand-model helper. The helper first clears the shared temporary-model pointer. A null receiver or null hand-model pointer then returns zero without allocating a temporary model or changing the item. With both pointers nonnull, the next dependency is the temporary-model allocator. The lookup and these initial helper branches leave the item records and actor contents unchanged.
DisplayHandModel
DisplayHandModel(name, x, y, z) is a four-argument function. It resolves
name before testing the actor receiver or entering the hand-model helper.
For ordinary single-byte names, lookup extracts at most eight characters of
the base filename without its directory, drive or extension, uppercases ASCII
letters and compares the resulting string with stored resource names. It
searches the first resource list in order, then the second; the first match
wins. Stored names are compared as stored rather than normalized during the
comparison.
A missing resource records the original name pointer and requests error 33.
A matched resource of type 2 records the name pointer and requests error 34.
For an accepted resource, the resolver returns its embedded model record.
The wrapper then clears the shared temporary-model pointer and forwards the
actor receiver, resolved model and all three offset words to the hand-model
helper used by DisplayHandItem. Name resolution also runs for a null actor
receiver. The fourth argument is passed on the stack.
Hand and hilt model helpers
The hand-model helper (DisplayHandModel, DisplayHandItem) and the hilt-model helper
(DisplayHiltModel, DisplayHiltItem) run the same routine over different actor words. Both
clear the shared temporary-model pointer, return zero for a null actor or model, and otherwise
allocate one record from the transient-model pool. The pool holds 900 records and is emptied
once per world frame by the view setup, so a display call lasts for the frame it is made in.
Allocation zero-fills the record; an allocation request with 900 records already in use
requests runtime error 35 and the helper returns zero without publishing a record. A successful
allocation is published as the shared temporary-model pointer and the record’s model field is
the supplied model.
The helper then queries two vertices of the actor’s current animated model: the signed grip vertex pair for the hand helper and the signed scabbard vertex pair for the hilt helper. The three offset arguments are added to both vertices before the actor’s orientation matrix transforms them. The record’s orientation is the pair’s pitch and heading with the actor’s roll, as for equipped weapons. The hand helper places the record at the midpoint of the transformed pair added to the actor’s map position; the hilt helper places it at the first transformed vertex added to the actor’s map position. Both also store the first transformed vertex plus the actor position as the actor’s retained attachment point and the record’s pitch, heading and roll as the actor’s retained attachment angles. Both return zero.
hand_pos and hand_angle properties
hand_pos.vx, hand_pos.vy and hand_pos.vz (offsets 0x201, 0x205, 0x209) and
hand_angle.vx, hand_angle.vy and hand_angle.vz (0x20d, 0x211, 0x215) are the six signed
dwords the hand and hilt model helpers store in the actor. The
equipped-weapon routines (the drawn, sheathing and stowed placements) call the same two helpers,
so they store the same words: hand_pos is the first transformed attachment vertex plus the actor’s
map position in Q8 and hand_angle is the placed model’s pitch, heading and roll in 1/2048 turns.
A property read returns the stored dword and a property write replaces it without masking, so a
script can store any 32-bit value. The words are plain actor storage: nothing clears them, the
helpers overwrite them on every call, and the actor keeps them across the frame.
DisplayHiltModel
DisplayHiltModel(name, x, y, z) is a four-argument function. It uses the same
resource-name resolver as DisplayHandModel, including the missing-resource
and type-2 error requests. Resolution runs even for a null actor receiver and
precedes clearing the shared temporary-model pointer.
After successful resolution, the native clears that pointer and forwards the
receiver, resolved embedded model record and all three offset words to the
distinct hilt-model helper used by DisplayHiltItem. It does not route to the
hand-model helper. The fourth argument is passed on the stack.
Item display models
Both item natives place the item record’s hand-model pointer, the model the equipped-weapon hand
placement uses (hand_object_file in ITEM.INI); an item without one has a null pointer.
DisplayHiltItem also takes this hand-model pointer and passes it to the hilt
helper, which places it at the actor’s scabbard vertex pair, rather than using the item’s
hilt_object_file model. A null pointer returns zero before any record is allocated.
DisplayHiltItem
DisplayHiltItem(item, x, y, z) is declared as a four-argument function. It
clears the shared temporary-model pointer before looking up item, including
when lookup is disabled or no item matches. The ordered lookup compares the
complete unsigned identifier and stops at an earlier greater identifier. A
missing match requests a diagnostic before entering the hilt-model helper.
For a match, the native reads the same item hand-model pointer used by
DisplayHandItem. It forwards that pointer, the actor receiver and all three
offset words to a distinct hilt-model helper. The helper clears the shared
temporary pointer again. A null receiver or null model pointer returns zero;
with both nonnull, its next dependency is the temporary-model allocator. These
initial branches leave item records and actor contents unchanged. The fourth
argument is passed on the stack and removed on return.
AnimateGXA
AnimateGXA(name, first, cycle, count) is a four-argument task. The native
handler does not use its actor receiver. Its image task counter, loaded
resource and animation progress are words of the running task slot’s native
state: the loaded resource is the first native continuation word and the
progress is the second, beside the counter word. They are
loaded before the handler runs and stored back after it, so each task slot
keeps its own and a saved task retains them. When the counter and cached
resource are both zero, the image loader is called with name and a zero
second parameter, its result becomes the cached resource and the progress
is set to zero. Otherwise the handler uses the cached resource. When
counter >= count - 1, using signed comparison, it releases the cached
resource, clears both image slot pointers and returns zero, which completes
the task; it publishes nothing on that path and leaves the progress word
as it was.
On the initialized active path, the handler publishes two image slots. Both
are centered on a 640 by 480 screen using signed resource width and height:
x = floor((640 - width) / 2) and
y = floor((480 - height) / 2). The first slot uses frame first. The second
uses first + progress + 1 when that index is below the signed resource frame
count, and first + 1 otherwise. The pointers come directly from the resource
frame-pointer table; the native does not decode or draw those frames.
After selecting both pointers, it adds the shared update increment to
animation progress. With a positive cycle, it repeatedly subtracts cycle
while progress is strictly greater than cycle; equality is retained. It
returns one on this active path. Slot selection uses progress before this
update. The native writes neither the actor receiver nor the cached resource
contents on this path.
The main loop draws the published slots after the world frame: slot 1, then
slot 2 when its pointer is nonzero, each as an ordinary image frame at its own
position, so slot 2 lies over slot 1. It then clears both slot pointers, so a
slot is drawn only for the update that published it and the active task
republishes both every update. No installed script calls AnimateGXA.
DropAllItems
DropAllItems takes no arguments and returns zero. When the task system is enabled it
applies the DropItem routine to every item id from zero up to the item count, each with the
count currently owned, in ascending order, and then clears the selected item. Ids with a
zero count are skipped. Each id must have an ITEM.INI definition; the shipped file
defines ids 0 through 86 without gaps.
InRectangle
InRectangle(x1, z1, x2, z2) returns one when the receiver’s query position lies inside the
rectangle on both horizontal axes, otherwise zero. The query position is the attached pose
for an attached receiver, the linked position when one exists, and the local position
otherwise (the same selection as the fighting query). Each pair of corners is ordered
independently, so the corners may be given in either order, and the comparison is inclusive.
The arguments use the position’s runtime units.
Camera initial state and player-type settings
At world load the camera actor and the alternate camera actor are both the CAMERA instance.
The shared selection starts at slot zero and record zero is the selected record. Each record’s
bound actor is the selected player. The stored bound-position history of record zero starts as
the bound actor’s current position. The shared state block (transition word, scripted-camera
active word and both published poses) starts zero until setup runs the full camera pose stage;
its result then becomes both the current and the preceding published pose.
Record zero’s distance, minimum distance, target offset and position offset take the
installed [camera] values listed in the record settings above when the selected player’s
attr_player_type is not one. When it is one, they take a reduced set derived from the same
keys instead:
| Record field | Value for player type one |
|---|---|
| Distance | camera_distance arithmetic-shifted right one bit |
| Minimum distance | camera_min_distance arithmetic-shifted right one bit |
| Target offset | target_offset_x >> 1, 6400, target_offset_z >> 1 |
| Position offset | offset_pos_x >> 1, 6400, offset_pos_z >> 1 |
Script light controls
Light, LightRadius, LightIntensity, LightOffset, LightFlicker, FlickerLight and
LightFlickerOff act on the receiver’s light record and return zero. When the receiver has no
record, the first call creates one: disabled and unlocked, offsets zero, intensity 256 and
radius 100 (each current value equal to its starting value), flicker fields zero. LightOff does
not create a record.
The explicit light-record field assignments cover the control flags, offsets, current and starting intensity and radius, and modulation fields. They do not include the RGB colour channels.
The backing heap’s existing-free-block allocation operation updates block and free-list metadata. It does not clear the light record’s RGB payload bytes, whether it splits the free block or consumes the whole block.
The allocation tracking context is requested separately as a 300-byte block. Its allocation service zero-fills its allocated block before optional guard registration. This context request is distinct from the 41-byte light-record allocation request.
Map loading gives a type-4 object a record of its own: locked and enabled, with the placement’s intensity and radius as both current and starting values and colour zero (the placement’s colour is not copied, unlike for a type-6 object), so those objects’ first light call updates an existing record.
| Function | Effect |
|---|---|
Light(radius, intensity) | Enable; set current and starting radius and intensity |
LightRadius(radius) | Set current and starting radius; enable; clear the inactive counter |
LightIntensity(intensity) | Set current and starting intensity; enable; clear the inactive counter |
LightOffset(x, y, z) | Set the three offsets |
LightFlicker(a, b) | Set flicker values a and b and clear the flicker phase word |
FlickerLight(a) | The same as LightFlicker(a, 1) |
LightFlickerOff | Set flicker value a to zero |
Every stored value keeps only its low sixteen bits.
Nonplayer combat taunt requests
The decision stage requests a taunt of a numbered type from
the receiver’s taunt list (each record carries a type, a four-byte RTX label, and an animation
value). A request is ignored when the receiver is dead or its list is empty, and when type 9 is
requested while combat flag 0x40 is set.
Distance gate. For types other than 0, 2 and 6, a request with a combat target is ignored when
the distance to the target is 0x40000 or more.
Busy gate. For types 10 and 11, the request is ignored while the receiver has a current taunt whose pending field is set.
Types 0, 2, 6, 10 and 11 use the in-order scan with the one-in-four random stop described in the taunt selection rules, including the death fallback to the hurt type here expressed as: when nothing matched for type 6, the request repeats as type 2.
All other types, including 1, 3, 4, 5, 7, 8 and 9, consume fifteen draws, each choosing a list index draw * count / 32767,
even after a record has been selected. A
probed record of the requested type with a nonzero label becomes the choice when no choice exists
yet, or otherwise replaces it when (combat value + clock sample) mod 2 is zero. The combat
value is the combat record’s retained allocation index. The decision stage caches the current
32-bit clock sample. In normal clock mode this is the raw BIOS-clock sample, interpreted as a
signed value, rather than a nominal-tick increment or accumulated nominal ticks. Their sum wraps
as a signed 32-bit value. The taunt is then presented only when (combat value + clock sample) is a
multiple of the combat record’s signed taunt period, initialized to 129 - attribute 87;
presenting a type 9 taunt also sets combat flag 0x40.
Presenting a taunt replaces the current taunt reference in the combat record.
PlayAnimation
PlayAnimation(group, modifier) in its function form requests an animation group for the receiver and returns zero
without waiting (unlike PushAnimation, which is a task). The request goes through the ordinary
request arbitration with the modifier narrowed to one byte and uses the shared
receiver eligibility gate.
The installed BAT and DOORBAT scripts call multitask PlayAnimation(10, 0).
In BAT, this call requests group 10 and retains its native task slot while the
main thread continues through rotation, sound, movement and waiting calls.
The slot remains pending when group 10 is current and after the movement calls
finish. End loops the main thread without clearing this slot. The next call
at the same script site reuses the existing slot rather than allocating another.
The native request routine does not assign a stable integer result on every return path. The function-form zero result therefore does not define a general task or multitask completion result for other operands or receiver states.
Candidate collection for an ordinary update
When ordinary traversal reaches an actor whose collection flag is set, it fills the engine’s retained candidate model list before running that actor’s update (native hook, combat decision and script stages included). The list holds the models inside a sphere centred on the actor’s local position, with the actor’s own model excluded. The sphere radius is twice the stored radius of the actor’s current model, truncated toward zero, or 200 when the actor has no model. A zero collection flag leaves the previous retained list unchanged and proceeds to the actor’s update. The collector keeps at most 64 entries. Queries made later in the same update (the in-sight test and the nonplayer movement probes) read this list without collecting again.
The face-model list stores static placements and ordinary live actors with type
0x20. Static entries have no actor owner; live entries retain their actor owner.
Transient model entries have type 0x80 and no actor owner. This collector does
not emit type 0x10, even though some consumers have special exclusions for
that type. Such exclusions do not change the types of entries this collector emits.
Nonplayer blocked-ahead probe
The movement helpers used by the combat decision ask whether the space ahead of the receiver is
blocked by distance. The probe applies only when the receiver has a combat record and its current
animation group has class one. It forms a local offset with
X zero, Y equal to (positive Y extent of the actor's body - SYSTEM.INI post_collide_height - distance)
in eight-fractional-bit units, and Z equal to -14336; rotates it by the actor’s matrix; and adds the
actor’s query position. The resulting single point (radius zero) is tested against each model in the
actor’s candidate list: any contact blocks. Candidates are
typed by source: static placements and ordinary live actors carry type 0x20, while transient models
carry type 0x80. While the actor’s queued-motion byte is 0x18 (24), transient candidates are
skipped; static placements and ordinary live actors remain eligible.
QueueMovie
QueueMovie(index) compares the index as an unsigned integer. An index from 5 through 10 plays that
cinematic at once from inside the script call: the movie’s table record is marked unlocked, the
movie plays, and the stored queued-movie value becomes -1. Any other index, including a negative one,
does not play: it replaces the stored queued-movie value (initially -1, meaning none), and the call returns
without further effect. The main loop plays a stored value other than -1 between
worlds (see movies).
chargeWeapon
chargeWeapon(charge) swaps the receiver’s weapon between item 1 and item 75 (the charged weapon). A signed
16-bit charge equal to 1 adds item 75 with count 1, sets the receiver’s attribute 23 (the weapon item) to 75,
empties item 1 and selects item 75. Any other charge does the same in the other direction: it adds item 1,
sets attribute 23 to 1, empties item 75 and selects item 1. The item add, empty and select operations are the
shared inventory routines, so their own admission rules (for example a closed item system) apply to each step.
menuNew and menuSelection
The scripted choice menu keeps an item count, a highlighted index, a committed index and a table of 30-byte item
records; each record holds the item’s text storage and, at offset 0x14, the 32-bit value that menuAddItem
stored. menuNew does nothing while the count is zero. Otherwise it releases every item’s text storage, sets
the count and the highlighted index to zero and clears two menu-state words, leaving the other record fields as
they were. menuSelection returns the value at offset 0x14 of the record at the committed index. It does not
test the count, so after menuNew it keeps returning the last stored value of that slot until a later
menuAddItem overwrites it.
Flat natives
The Flat functions control the receiver’s 20-byte flat sprite record (enable byte at offset 0,
pending byte at 1, offsets at 2, 4 and 6, texture identifier at 8, last-frame word at 0xc, counter at 0xe, frame at
0x10, animation byte at 0x13). Every one except FlatOff first creates the record when the receiver has none; the
new record has its enable and pending bytes clear. All return zero.
FlatLikeStatic(texture)sets the enable byte to one and the pending byte to zero, storestextureas the texture identifier, sets the last-frame word to0xffffand the counter, frame and animation byte to zero.FlatOffset(x, y, z)clears the pending byte and stores the three offsets.FlatStopcopies the last-frame word to the counter and the counter to the frame, sets the enable byte to one, and clears the pending byte and the animation byte.FlatOffdoes nothing for a receiver without a record; otherwise it clears the enable and pending bytes.FlatSetTexture(bank, image)forms the texture identifier((bank << 7) & 0xff80) | (image & 7). When the record already holds that identifier and has a cached image slot (offset 0xa), the playback state is kept. Otherwise the image is looked up by bank and image (and loaded when absent; a failed load is fatal), the slot and identifier are stored, the last-frame word becomes the image’s frame count minus one (at least zero) and the counter and frame become zero. Either way the enable byte becomes one and the pending and animation bytes zero.
In the Glide executable’s ordinary actor update, retained flat maintenance runs immediately after light maintenance and before animation advancement and ordinary SOUP dispatch. It applies to actors that reach that maintenance stage and whose hidden flag is clear. Standalone objects created after world loading use this same traversal and maintenance order. A flat created or disabled by ordinary script dispatch is therefore first maintained on the next eligible update.
PlayerHanging and updatePlayerViewAttrib
PlayerHanging() returns one when the selected player’s living state is 15 (hanging) and its retained
hang-model reference (see ledge entry) is the receiver’s model; otherwise
it returns zero. updatePlayerViewAttrib() is an empty routine: it takes no input, changes nothing and returns
no meaningful value.
Session requests: LoadWorld, WonGame, PushPlayer and PopPlayer
LoadWorld(world, marker, heading), WonGame, PushPlayer and PopPlayer return zero and only record a
request; the main loop acts on it after the current script step. LoadWorld retains its three integers.
The main loop then loads the world: the world index must be 0 through 99 and name a WORLD.INI entry
(otherwise loading ends with an error), the current-world index becomes that value, the selected start marker
(the second argument) supplies the player’s starting position triple, and the third argument, masked to 0
through 2047, supplies the starting heading. The three words are cleared when the load begins.
WonGame, PushPlayer and PopPlayer each set one request flag for the main loop, which consumes them.
WonGame ends the world loop and starts the ending.
PushPlayer and PopPlayer are consumed at the end of a world-loop iteration that does not leave the loop:
push first, then pop. Each clears its own flag when consumed, whether or not it acts. The engine keeps the
CYRUS and GREMLIN player records and one swap state, clear in a fresh world. While the swap state is clear
CYRUS is the selected player and GREMLIN is excluded from ordinary object updates; while it is set GREMLIN is
selected and CYRUS is excluded. The world loop re-asserts that selection and exclusion every iteration.
PushPlayeracts only while the swap state is clear. GREMLIN becomes the selected player, the swap state is set, and GREMLIN takes CYRUS’s player transfer (below). GREMLIN stops being excluded from ordinary updates and CYRUS becomes excluded.PopPlayeracts only while the swap state is set. CYRUS becomes the selected player again, the swap state is cleared, and CYRUS takes GREMLIN’s position, heading and retained movement state with the same exclusion exchange reversed. CYRUS then receives an ordinary animation group request for group 0, issued twice.
A second PushPlayer while swapped, or a PopPlayer while not swapped, only clears its flag.
The player transfer copies from the role that was selected to the role that becomes selected: the local position,
two further triples of 32-bit words at offsets 0xFB and 0x108, the three angles, which the orientation setter
masks to 0 through 2047 and also stores as the creation angles (start_angle, offset 0x144) and the previous-angle history (last_angle, offset 0x150) before it
rebuilds the orientation matrix, the four motion bytes at +0x1B1 through +0x1B4 (the last is the living
state), the 32-bit word at +0x1CD, the two words at +0x08 and +0x0C, and byte 3 of the attribute block (the
health attribute). It zeroes the words at +0xC7 and +0x25, re-registers the new role in its map cell, and clears the
new role’s object-update exclusion byte while setting the old role’s.
KillMe and Resurrect
KillMe and Resurrect take no arguments, act on the receiver and return zero. Both first release the
receiver’s two held walk-node reservations: a reserved node is cleared only when its recorded owner is the
receiver, and both reservation slots then hold no node.
KillMe then clears the receiver’s current model presentation record, releases its two retained per-actor
buffers, sets its killed flag and its death selector to 8, and stops every active sound owned by the receiver.
The two buffers are the receiver’s light record (41 bytes) and its 20-byte flat
sprite record, the record the Flat functions control: an enable byte, a pending byte, three 16-bit offsets, the
texture identifier, a frame cache and the playback counters. Each pointer is cleared after its buffer is freed,
so a later light or flat call creates a new record with the defaults.
Map loading creates a flat record for every type-3 placement, enabled, with zero offsets and the placement’s
texture value as its texture. While a record is enabled and its texture is nonzero, the ordinary actor drawing
traversal draws the actor’s flat as a camera-facing sprite at the actor’s position plus the offset resolved
through its orientation, using the texture’s attributes for its flags and alpha. The installed type-3
placements are the LAVABUB1 lava bubbles (DRINT, ISLAND, CATACOMB) and the LH_FIRE and CT_FIRE fires.
The drawing routine runs once per actor in the ordinary traversal, for actors whose draw-suppress byte is clear. It
draws nothing unless the record’s enable byte and texture identifier are nonzero. The three signed 16-bit offsets
are transformed by the actor’s current pose, and the integer result (the transformed point shifted right by eight)
is the sprite’s world position. The record caches the loaded image slot for its texture; while that cache is zero it
is filled from the texture identifier, and a zero slot or one above 30000 is a fatal error. The texture’s attribute
record, when present, gives the draw flags (the flag word is 1 plus the attribute’s flag byte) and the alpha (the
attribute’s alpha byte plus one); without it the alpha is 255. The sprite is then submitted through the same routine
as placed flats with the cached slot, the record’s frame word, the
position, scale 1 and those flags and alpha, with the world’s world_ambientfx value (see WORLD.INI) as the brightness. When flag
0x80 is set the flag word also gains 0x200 and the three inputs select the
special flat constant tint, with red, green and blue each
200.
Each update of an actor that is not hidden also steps its flat record, beside the light maintenance. A zero or
0xffff texture disables the record. A disabled record adds the elapsed ticks (low byte, wrapping) to its pending
byte and is freed once that byte exceeds 32. An enabled record first consumes a wrap marker (the high bit of its
loop byte): the marker clears and the frame returns to the record’s counter. A frame of 0xffff, or a counter equal
to the last frame, shows a static image. Otherwise the frame moves by the elapsed ticks times the speed byte, up when
the counter is below the last frame and down when it is above. Passing the last frame disables the record when the
loop byte is zero; otherwise it sets the wrap marker and restarts the frame at the counter plus the overshoot (clamped
to the counter if that still passes the last frame).
Resurrect acts only while the killed flag is set or the death selector is 8; otherwise nothing further
happens. It clears the killed flag. When the receiver has combat state and its death selector is 6, it first sets the
selector to 1 and the death response to 0, clears combat flag 0x04 and zeroes the close-distance pass counter and
the attacker selection state (the unconditional part of the end-of-combat reset); a receiver that KillMe just
killed has selector 8, so Resurrect leaves its selector unchanged. With combat state it then re-initializes that
state from the receiver’s attributes.
Removal of killed created objects
Each world-loop iteration, before the family update, removes every object that was created after the world loaded
(it has no map object) and whose killed flag is set: it leaves its map-cell list and the actor table, its KillMe
cleanup runs again and its record is freed. A killed map actor stays allocated; the ordinary actor update skips
killed actors.
projectileAI
projectileAI is a task without arguments; its update runs on the receiver, a created object whose parent is its
creator. The update returns one to keep flying and zero to end the task:
- The speed is the receiver’s attribute 8 shifted left by eight, multiplied by the frame scale and shifted right by
eight (signed). The previous local position is saved, then the offset
(0, 0, speed)rotated by the receiver’s pitch, yaw& 0x7ffand roll is added to its local position. - A distance from the parent of
0xbb800(Q8) or more ends the task, keeping the moved position. The distance is the single-precision magnitude of the X, Y and Z differences rounded to nearest-even; it is zero without a parent. - When the world has terrain and the receiver’s Y is strictly greater than the terrain height at its integer X/Z (the terrain support sample, times 256 and rounded), the previous position is restored and the task ends.
- Candidates are gathered around the receiver’s position with the radius attribute 8 (map units), excluding the receiver’s own model, by the live and transient candidate rules (static placements, ordinary actors, transient models). The segment runs from the previous to the new position, both shifted right by eight.
- Every face-model candidate not owned by the parent is queried with that segment, without polygon sidedness. A
segment that ends exactly on a face plane does not cross it. Over
all contacts, the one with the smallest rank (strictly less than
0x7fffand every earlier rank) is kept. When any contact exists, the receiver moves to that contact’s integer point shifted left by eight and the task ends. - Otherwise every actor-style candidate except the parent is tested in candidate order with the actor’s collision spheres. A sphere is hit when either segment endpoint lies within its radius (squared integer distance computed in single precision and rounded, compared with the squared radius). The spheres are the actor’s optional RAFS records (flag 2 always, flag 4 only while the actor’s current animation group pair matches the record), followed, unless attribute 56 is nonzero, by the actor’s selected body shape spheres; each center is rotated by the actor’s orientation and added to its position. At most 128 spheres take part. Vertex-bound RAVC records are a separate collision owner and do not replace this sphere list.
- On the first hit, an actor with combat state is sent combat message
0xd005from the parent; with or without combat state, the previous position is restored and the task ends. Without a hit the task continues.
InKey
InKey(code) returns one when a keyboard key is down and zero otherwise. The low byte of the argument
is a character; its key is found by matching the character against the keyboard table’s unshifted
and shifted characters, and a character with no table entry returns zero. The next three bytes each
name a required modifier by a character, either case: s Shift (either shift key), c Control and
a Alt; the result is zero when a named modifier is not held. Otherwise the result is one when the
key’s state word is nonzero or the key is the latest key event the keyboard handler recorded. The
debug shell reads it for the letters p, c and d and for Alt+q, and a few bridge and door
scripts read r.
InKey leaves the key state words and latest-event byte unchanged, including
on a successful query. Its receiver is unused. The character lookup receives
only the low argument byte and runs once before modifier checks; an unmapped
character returns zero before those checks. Unknown modifier bytes are ignored
and repeated modifier requirements have the same effect as one. The native
preserves the incoming general registers other than the result and argument
registers and leaves argument-stack cleanup to its caller.
Releasing an ordinary key clears its held-state word without clearing the latest-event byte. The normal world loop clears the latest event during its clock update, after the keyboard-text handler and before the shell, object and code-object stages. The latest event is therefore separate from a per-frame held-key sample.
SpacePressed
SpacePressed() is a function without arguments. It returns one when the
Space key’s 16-bit input state word is nonzero and zero when it is zero. Space
is keyboard code 57 in KEYS.INI. The function
does not consume or change the state word, inspect the latest-key-event byte,
or dereference the receiver. It makes no service calls and creates no task.
Repeated calls return the same result while that word remains unchanged.
PressKey
PressKey(code) takes one packed integer argument. Its low byte is a character,
resolved through the keyboard table’s unshifted and shifted characters. No
matching key returns zero before checking modifiers. Each of the next three
bytes can require c Control, s either Shift key or a Alt, in either case.
Other modifier bytes are ignored, and repeated requirements have the same
meaning as one. All named modifiers must have nonzero state words; either
Shift state word satisfies s. Modifier state uses the fixed keyboard codes
listed in KEYS.INI, not action bindings.
Once the modifiers pass, a nonzero state word for the resolved key or a matching latest-key-event byte returns one and clears that key’s state word. Otherwise it returns zero without clearing the word. It does not clear the latest-event byte: repeated calls can return one again while that byte still matches, even after the state word has been cleared. A failed modifier check does not consume the key state. The receiver is not dereferenced or changed. This is an ordinary function, not a task.
GolemSteam and DragonBreath
GolemSteam takes no arguments and returns zero. A receiver excluded from object updates does nothing. Otherwise
one Rnd(4) draw that is not zero ends the call. On zero a second Rnd(4) draw selects one of the receiver’s four RAEX
attachment vertices in the order grip0, scabbard1, grip1, scabbard0. The vertex, rotated by the receiver’s
orientation and added to its position, becomes the point of a steam effect: descriptor 0x32480 when the world-loop
frame counter is odd and 0x22480 when it is even, with speed one.
DragonBreath takes no arguments and is also the update of the multitask DragonBreathTask, which completes when the
update returns zero. It acts on the receiver, a dragon with combat state:
- A receiver whose death selector is 8 returns zero. First, when the player holds item
0x4bwith a nonzero count, one unit of item 1 is added, the player’s held-item attribute (23) becomes 1, item0x4bis emptied and item 1 becomes the selected item. - One
Rnd(4096 / frame scale)draw of zero, while a dialogue line owned by the receiver is active, spawns the smoke effect0x32480at an attachment vertex:scabbard0when a following draw masked with one is nonzero, elsescabbard1(rotated and offset as forGolemSteam). - A nonzero fireball cooldown, a process-wide counter that starts at zero, is reduced by the nominal ticks of the frame (to zero at the least) and the update returns one.
- Without combat flag
0x04or without a combat target the update returns one. A target whose rounded 3-D distance is less than0x19000(Q8) also returns one. - Otherwise the fireball is created: an object of the installed script
DRAGBALLwhose parent is the receiver, at the rotatedgrip0vertex plus the receiver’s position. Its orientation is the aim fromgrip0togrip1: yaw is the bearing of the horizontal difference plus 1024; the pitch is the elevation of the difference minus 512, then120 - pitch, all masked to 2047; roll is zero. The cooldown becomesRnd(32) + 12and the update returns one.
Nonplayer deep-water and edge probe
The strafing steppers (MoveLeft, MoveRight) and MoveForward call one probe before they move. It
applies only to receivers whose surface byte is 2 or 4 (the deep-water classes); every other
receiver passes. The probe point is the receiver’s position plus a step of 16384 (units of 1/256
map unit) along the direction chosen by the motion-direction byte (2 and 6 forward, 3 and 7
backward, 4 left, 5 right, any other value forward), rotated by the receiver’s heading. The point is
looked up in the terrain material rules of terrain material blend coverage.
The probe fails (blocks the move) only when the resolved surface is 10 with blend coverage 0 and the terrain slope class at the point is 2. A point with blend coverage 256, a point with partial coverage (surface 3), a point without terrain, and every other surface pass. The slope class is the classification of the terrain face normal as for ground support. A failing probe returns zero to its caller.
Nonplayer step hazard
The step hazard test answers whether moving a nonplayer receiver to a destination lands on a steep surface. It applies only to receivers whose attribute 1 has bit 1 set; others never report a hazard. The surface at a point is resolved with a vertical segment of the post-collide height about the point’s centre (the point’s Y plus the receiver’s positive Y extent) against the candidate models and the terrain. Among the contacts, only floor (class 4) and steep (class 2) surfaces compete, and the one whose rank is nearest 0x4000 wins; the terrain replaces that surface when the segment’s top is at or below the terrain height and the model surface is farther from the destination’s centre than the terrain is. A point with no surface counts as class 1, and when neither the destination nor the current position has a surface the result is no hazard. The class comes from the contact normal’s Y (scale 256): at most -161 is 4, at least 161 is 8, at most -42 is 2, otherwise 1. The test reports a hazard when the destination’s class is 2.
The selected player runs the same test, except that it reports no hazard while the player’s support-history counter is nonzero.
Directional step refusal marks
A refused directional destination check retains a mark in the receiver’s
collision-response word.
Forward refusal sets cf_check_forward_fail (0x80), left
cf_check_left_fail (0x20) and right cf_check_right_fail (0x40), all in
the word’s first byte; backward refusal sets cf_check_backward_fail
(0x100, body-response bit 0x01). The writes preserve the other bits.
These marks participate in the 0x01e2 mask used by player alignment’s
fallback decision. The response-record maintenance counts their age and clears
them with the other packed response bytes.
MoveForward
MoveForward(duration) is a task that steps the receiver forward along its heading. It shares
MoveBackward’s result and movement rules except as listed here.
Admission. The call completes without moving when the receiver’s rope attachment, sliding state,
falling state or queued-motion byte is nonzero. There is no lifecycle-byte test. While the two-bit step
counter in the receiver’s motion byte (bits 4 and 5) is below three, the forward step check must also
pass: it passes when the lifecycle byte is 7 and attribute 1 has bit 0 clear; otherwise it fails when the
receiver’s attribute 1 has bit 1 set and the step hazard
at the destination reports a hazard, where the destination is the receiver’s position moved forward by
half of attribute byte 8 (whole units, times the frame scale, arithmetic shift right eight). A failed check
completes the call. Admission then stores movement classification 2 in
last_direction, before combat selection and animation admission. A later
missing animation group or pending request does not undo this store.
Combat stance (the receiver holds a weapon or its combat flag 0x04 is set). The call completes
without moving when the receiver already has a selected combat move. Otherwise the group is 92 and the
step length is attribute byte 17.
Outside combat stance. In deep water (surface 2 or 4) the edge probe must pass, the group is 142 and the distance is halved; otherwise the group is 30 while a torch burns and 18 when not, with step length attribute byte 8.
Animation and movement. The call completes without moving when the receiver has no entry for the group.
Otherwise the group is requested as PlayAnimation(group, 0) does, and with the animation system enabled and
the current group not the requested one the call waits like MoveBackward. The movement heading is the receiver’s
local Y angle (no offset) and the distance is the step length in whole units times 256, scaled and translated as
for MoveBackward.
Forward movement amount query
Fight spacing’s forward amount query shares the forward startup counter and
step-called state with actual movement. It refuses nonzero rope attachment,
sliding, falling, queued-motion or living-state values. Below startup counter
three, it requires the forward step check to pass. It then stores movement
classification 2 in last_direction, before selecting or requesting the animation
group. A refusal before that store preserves the previous classification. The
store remains when the animation request leaves another group current; it does
not wait for the startup counter or forward-called flag to advance. This retained
classification is distinct from the later displacement direction.
A held weapon or combat flag 0x04 selects group 106 and attribute 16 for action
selector 4, or group 92 and attribute 17 otherwise. Outside combat stance it
selects attribute 8 and group 142 in deep water, group 30 with a burning torch,
or group 18 otherwise. It requests the group with frame zero. When animation is
enabled and the requested group is not current, it returns the ordinary duration
continuation result without marking forward called or advancing the counter.
Once animation permits the step, it marks forward called, multiplies the selected step byte by 256 and halves that distance in deep water. Below counter three it applies the ordinary startup adjustment and increments the shared counter. With translation suppression clear, its return value is the signed wrapping product of that adjusted amount and frame scale, shifted right eight. With suppression set, it instead returns the adjusted amount without frame scaling. It does not translate the receiver. A subsequent movement request therefore reads the counter and animation state changed by this query.
Movement continuation
Each ordinary actor update clears the four movement-called flags before its script and
decision stages. After those stages and the animation processing, an actor with no rope
attachment, no queued-motion or living-state value and a clear forward-called flag continues
moving when its current animation group is 18, 30, 67 through 70, 75, 77, 92, 106, 130, 131
or 142. The continuation is a forward step that requests no animation group: it stores
motion direction 2, sets the forward-called flag and, below startup counter three, requires the
forward step check. Its step length is attribute byte 17 with a held weapon or combat flag
0x04, otherwise attribute byte 8 (shifted left seven instead of eight for groups 75 and 77),
shifted left eight. The startup adjustment, the deep-water halving, the frame scaling and
the translation-suppression marker apply as in the forward movement amount query, and the
translation follows the actor’s heading.
The same continuation runs the other directions when their own called flag is clear: groups 19, 76, 78 and 93 step backward (motion direction 3; in combat stance the blocked-ahead probe at distance 30 stops it first; step length attribute byte 18 in combat stance, otherwise attribute byte 8 shifted left seven for groups 76 and 78 and attribute byte 9 shifted left eight for the others; startup counter at bits six and seven), and groups 94 and 95 circle left and right (step length attribute byte 83 shifted left eight, the heading turned by attribute byte 84 scaled by the frame scale, movement along the new heading plus or minus 512; the left circle stores motion direction 25, the right circle 5).
The walking groups continue the same way, each with its own step check: groups 20, 21 and 31 walk forward (motion direction 6, the walk-forward step check at any startup counter; step length attribute byte 19 in combat stance, otherwise attribute byte 12, both shifted left eight; a zero length is half of attribute byte 8, unshifted); groups 22 and 23 walk backward (motion direction 7, the backward step check; attribute byte 20 in combat stance, otherwise 13; a zero length is half of attribute byte 9); groups 24, 25 and 28 sidestep left and 26, 27 and 29 right (motion directions 4 and 5, the lateral step check; step length attribute byte 10 shifted left eight; movement along the heading plus or minus 512 without turning). These steps store their motion direction before the step check. A swimming actor (deep water) continues too: it needs the deep-water edge probe along its motion direction first and moves half the distance, except for the backward steps.
The retained movement classifications are distinct from the displacement-direction field. The forward continuation stores classification 2 after its startup support check and before the deep-water edge refusal. The backward continuation stores 3 after its combat blocked-ahead refusal and before the deep-water edge refusal. Walking stores 6 or 7 after any deep-water edge refusal but before its forward or backward support check. Ordinary sidesteps store 4 or 5 before both the deep-water edge refusal and lateral support check. Combat circling differs from its native task: the left or right lateral support check runs first, then stores classification 25 or 5 respectively. Circling then joins the final distance scaling directly, without a separate deep-water edge probe. A refusal before a store preserves the previous classification; a later refusal retains the newly stored value.
Because it requests no group, an idle group requested earlier in the same update (a spacing pass that requested no movement, for example) stays pending until the playing group finishes and the actor keeps walking forward until then. A spacing pass for an actor in combat stance above three times its far range, or without stance below the midpoint between its far and third ranges, requests no movement; the actor still walks on through the continuation.
WalkForward
WalkForward(duration) delegates to MoveForward(duration) in deep water before
checking its own admission state. For a nonplayer receiver outside deep water,
a rope attachment, sliding state, falling state or queued motion refuses the
call. The walk-forward step check must pass before movement classification 6 is
stored in last_direction. This precedes walking-group selection and the
animation request gate. Both classifications are distinct from the direction
assigned by directional ordinary displacement.
The walk-forward step check refuses a rope attachment. It passes immediately for
a receiver whose lifecycle selector is 7 and whose attribute 1 has bit zero clear.
Otherwise it builds a destination from the local offset
[0, 0, (attribute12 + positive_z_extent) * 256], with signed wrapping arithmetic,
rotated by the receiver’s stored matrix. It adds the rotated X/Z to the receiver’s
position and retains the receiver’s Y coordinate. It requires the destination
body-contact test to pass, the step-hazard result to differ from one, then the
additional destination-admission test to pass, in that order. A failed test
sets bit 0x80 in the receiver’s step-status byte and returns false. Early
rope refusal and lifecycle acceptance leave that byte unchanged.
The step’s destination support test passes whenever attribute 1 bit 1 is clear.
The additional destination-admission test runs the ordinary collision pass with
the receiver temporarily at the destination. It restores the original position
afterwards, while retaining other collision writes.
A held weapon or combat flag mask 0x04 selects animation group 31 and attribute
byte 19 as the step length. Outside combat stance, a burning torch selects group
21 and otherwise group 20; both use attribute byte 12. A missing group skips
animation requests and translation, while duration completion is still evaluated. An available group is requested
with frame zero. With animation enabled, a different current group waits without
advancing the step counter or translating.
The initial movement distance is the selected step length multiplied by 256.
When that length is zero, the initial distance is instead half of attribute byte
8, without the multiplication by 256. While the two-bit step counter is below
three, the existing step-distance adjustment runs and the counter increments.
Animation translation suppression prevents movement after that adjustment.
Otherwise, signed wrapping multiplication by frame scale followed by arithmetic
shift right eight gives the distance. Positive distances translate along the
receiver’s local Y heading, masked to 2047. Duration completion follows
MoveBackward.
MoveLeft and MoveRight
MoveLeft(duration) and MoveRight(duration) are tasks that strafe the receiver. They share
MoveBackward’s admission, result and movement rules except as listed here. Admission is
identical except that lifecycle byte 15 is also admitted. A call stores movement classification 4
(left) or 5 (right) once admitted, before the lateral or edge checks, current-group refusal, group
lookup and animation request. Later refusal retains this classification. The displacement direction
is a separate field.
Combat stance (the receiver holds a weapon or its combat flag 0x04 is set). The call completes
without moving when the receiver’s current group is already the strafe group (94 for left, 95 for right,
or either of the two) or when the receiver has no entry for the strafe group. Otherwise it requests the
group as PlayAnimation(group, 0) does, then turns the receiver’s heading: the left call subtracts, and the
right call adds, attribute byte 84 times the frame scale (arithmetic shift right eight), wrapped to 2047.
The movement heading is that new heading plus 512 (left) or minus 512 (right), wrapped, and the step length is
attribute byte 83. No safety check applies.
Outside combat stance. The call completes without moving when the lateral step check fails (the
destination of a step of attribute byte 10 plus the receiver’s retained step value, to the left or right of
the heading, fails the model contact test). In deep water (surface 2 or 4) the edge probe must also pass and the
group is 143; otherwise the group is 25 left or 27 right while a torch burns, else 24 left or 26 right.
When the animation system is enabled and the receiver’s current group is not the requested one the call
waits like MoveBackward: one while duration - 1 > elapsed, zero otherwise, without moving. With the
lifecycle byte 15 (hanging at an edge) the call instead runs the lateral edge test and requests group 28 left
or 29 right. The movement heading is the receiver’s heading plus 512 (left) or minus 512 (right) and the step
length is attribute byte 10.
Movement and result. The shared step counter, translation-suppression marker, frame scaling and result are those of MoveBackward.
Fight-spacing directional admission
The left, right and backward destination checks used by fight spacing refuse a rope attachment before reading geometry. They accept immediately when lifecycle selector 7 has attribute 1 bit zero clear. These early results leave the receiver’s step-status bytes unchanged.
Otherwise, each check builds its directional local offset with signed wrapping arithmetic:
| Direction | Local offset |
|---|---|
| Left | [-(attribute10 + negative_x_extent) * 256, 0, 0] |
| Right | [(attribute10 + positive_x_extent) * 256, 0, 0] |
| Backward | [0, 0, -(attribute13 + negative_z_extent) * 256] |
It rotates the offset by the receiver’s stored matrix, adds the rotated X/Z to
the local position and preserves the receiver’s Y. It requires a
nonzero destination body-contact result, a step-hazard result other than exactly
one, then a nonzero additional destination-admission result, in that order.
A failed stage skips the later stages and returns zero. Failure ORs 0x20 into
the step-status byte for left, 0x40 for right, or 0x01 into its companion
byte for backward. Success returns one without clearing retained status bits.
The checks themselves do not replace the receiver’s position.
MoveBackward
MoveBackward(duration) is a task that steps the receiver backward relative to its heading. Each
invocation returns zero (complete) or one (continue); it reads the task’s elapsed input.
Admission. The call does nothing, completing immediately, when the receiver’s rope attachment, sliding state, falling state or queued-motion byte is nonzero, or its lifecycle byte is not 0 (byte 15 is refused too, unlike the strafes). Otherwise it sets the receiver’s movement classification to 7, before the combat blocked-ahead probe, group lookup and animation request. Later refusal retains this classification. The displacement direction is a separate field.
Animation. The receiver is in the combat stance when it is holding a weapon or its combat flag
0x04 is set. In combat stance the blocked-ahead probe runs with
distance 30; a blocked probe completes the call. The group is then 93 and the step length is the
receiver’s attribute byte 18. Outside combat stance the group is 143 in deep water (surface value 2
or 4), 23 while a torch burns, and 19 otherwise, with step length attribute byte 9. When the receiver
has no entry for the group, the call skips the rest and uses the final duration test below. Otherwise
the group is requested as with PlayAnimation(group, 0). When the animation system is enabled and
the receiver’s current group is still not the requested one, the call returns one while
duration - 1 > elapsed and zero otherwise, without moving.
Movement. The heading is the receiver’s local Y angle plus 1024, wrapped to 2047. The distance is
the step length in whole units times 256. A two-bit step counter in the receiver’s motion byte
(bits 6 and 7) selects an initial-distance adjustment while below three, then increments.
The adjustment reads attribute byte 11 independently of animation translation suppression.
For input distance d, it follows this table. Products wrap as signed 32-bit integers,
right shifts are arithmetic, and division truncates toward zero.
| Attribute 11 | Counter 0 | Counter 1 | Counter 2 |
|---|---|---|---|
| 1 | d >> 2 | d / 3 | d >> 1 |
| 2 | 0 | (d * 85) >> 8 | (d * 170) >> 8 |
| 3 | 0 | 0 | d >> 1 |
| Other | d | d | d |
At counter three the distance stays unchanged. The packed byte holds left, right, forward/walk and backward counters in consecutive two-bit fields, from least to most significant. Each direction has a shared called flag, reset at ordinary wrapper entry. At ordinary update cleanup, a direction’s counter is cleared only if its called flag is zero; called directions retain their counters. Directional displacement and sliding mark this same packed byte, saturating the selected direction’s counter rather than writing a separate movement record. Collision rollback can separately clear the packed byte when restoring the previous position. Entering collision processing does not unconditionally clear it.
Unless the animation’s translation-suppression marker is set, the distance is multiplied by the frame scale (arithmetic shift right eight) and, when positive, the receiver’s X and Z move that far along the heading by the heading-based translation rule.
Result. One while duration - 1 > elapsed, otherwise zero.
WalkBackward
WalkBackward(duration) is a task. In deep water (surface value 2 or 4) it runs
MoveBackward unchanged. Otherwise it follows MoveBackward’s animation, movement and
result rules with these differences.
Admission. The call completes immediately when the receiver’s rope attachment, sliding, falling or queued-motion byte is nonzero or its lifecycle byte is not 0. For the selected player it also completes immediately while a retained player-mode word equals one and the queued-motion and falling bytes are both zero. It then runs the backward form of the ordinary directional step check, which uses attribute byte 13, and completes when that check refuses. Only then does it set the movement classification to 7, before group lookup and animation request. A rejected step check leaves the classification unchanged; a later missing group or pending request retains 7. The displacement direction is a separate field.
Animation. In combat stance the group is 93 and the step length is attribute byte 20. Otherwise the group is 23 while a torch burns and 22 otherwise, with step length attribute byte 13. There is no blocked-ahead probe.
Movement. When the step length is zero, the initial distance is attribute byte 9 shifted right by one, not multiplied by 256; the step-counter adjustment and frame scaling then apply as for MoveBackward.
MoveAboveMe
MoveAboveMe(offset_x, offset_y, offset_z, angle) is a four-argument function.
Its source actor comes from the shared source actor reference, independently
of its receiver. It resolves the source position using the alternate-position,
active-attachment, then current-local-position precedence. A source with the
alternate-position predicate set invokes diagnostic error 137 before the
transform. If the diagnostic services return, the function continues.
The source actor’s matrix transforms the three offsets in place. Each transformed offset is added to the corresponding resolved source coordinate. The target Y then subtracts the receiver’s vertical offset shifted left eight bits and adds the shared vertical adjustment. These integer operations wrap at 32 bits.
The shared source actor reference is the shared native-command source: the ordinary
entry, task and main interpreter entry writes its executing actor there before the entry
continuation, and nothing else writes it. The RAHK native hook and the alternate entry/main path
selected by script-control mask 0x04 do not, so a native they run reads the actor of the most
recent ordinary interpreter entry, and a call routed to another object keeps that source while
the named object is the receiver. The shared vertical adjustment is the [system]
standing_height value from SYSTEM.INI shifted left eight bits, read once when the settings
load. The alternate-position predicate tests the source’s slave attribute for a
nonzero value, the same attribute that makes position resolution select the alternate position.
For the selected-player receiver, the function sets the pending-position flag to one, stores target XYZ in the pending-player coordinates, and stores the angle argument unchanged in the pending angle. It does not directly write the receiver’s local coordinates or register the receiver in the actor list.
For another receiver, it writes target XYZ directly to current local
coordinates, stores angle & 2047 as the local Y angle, and registers that
receiver in the pending-position list. Both returning paths return zero.
GotoLocation
GotoLocation(location) is declared as a one-argument task. It selects a
coordinate triple from the receiver’s location table using an inclusive signed
upper bound, falling back to saved start coordinates for an absent table,
negative location, or location above the bound.
The arrival helper first shifts target and current local coordinates right arithmetically by eight bits. It adds the actor’s vertical offset to the shifted current Y, then queries the three-dimensional distance between those positions. Arrival requires that signed distance be strictly less than the receiver’s movement speed multiplied by 1.5 and rounded down to an integer. Equality does not count as arrival.
On arrival, the helper copies the quantized target coordinates back into the actor’s current local position, clearing their low eight bits, and registers the actor in the pending-position list. The task then sets movement state to zero and returns zero. It does not update the previous-position triple.
Otherwise the task sets movement state to four, stores the low 16 bits of the
location argument, and invokes the state-refresh helper. After that refresh,
a stored signed 16-bit location of -1 returns zero; every other stored value
returns one. This sentinel completion leaves movement state four. The refresh
updates the state-four action attribute and countdown before the sentinel
check. Countdown values zero and 255 are preserved; other values decrease by
the engine countdown step, clamping to zero when smaller than that step.
The movement-state writes are the ai_type lifecycle-selector byte, and the
retained location is a signed 16-bit word independent of ai_mode and
ai_delay. State four writes attr_collide to four after updating ai_delay
with the shared nominal tick count. This refresh does not request an animation
or invoke path selection. Arrival leaves the location word, delay, mode and
attr_collide unchanged.
AI destination clearance
Before its mode dispatch, the AI progress service tests direct destination
clearance only when the receiver’s AI mode is not 11 and ai_delay is zero.
A clear result releases
the receiver’s held walk nodes and sets AI mode to 11. It then continues the
same progress invocation; clearance does not itself complete the movement.
The clearance query resolves the receiver’s query position and measures rounded three-dimensional distance to the supplied Q8 destination. A distance greater than 131072 rejects the destination before collision queries. Equality proceeds. Both endpoints are then arithmetic-shifted right by eight into whole map units. The query consumes the retained candidate-model array in order without gathering a new array. Type-16 entries owned by the receiver or the selected player, and type-16 entries whose model is the receiver’s embedded model, are skipped.
Each remaining model receives the segment with polygon sidedness disabled. A null or failure-sentinel result does not block. A returned result blocks unless that candidate’s owner is the receiver. The first blocking result returns false; reaching the end of the array returns true. The clearance query does not change the receiver’s location, mode, delay or node reservations. Those state changes belong to the enclosing progress service.
AI mode eleven progress
Mode 11 of the AI progress service sets attr_collide to 15. If the receiver’s
slide_flag is nonzero, it sets ai_mode to zero and ai_delay to 255 before
continuing the same invocation.
The service steers toward the supplied destination’s X and Z coordinates.
Steering first applies its horizontal arrival test. Otherwise it turns toward
the destination and returns the signed heading difference from before that
turn. Only an absolute difference strictly less than 256 proceeds to the
MovePos predicate. A nonzero predicate result requests forward movement and
then refreshes the receiver’s spatial state. A difference of 256 or greater,
or a zero predicate result, skips both operations.
After steering and any movement, the service repeats the destination-clearance
query against the retained candidate array. It retains the current mode and
delay only when clearance succeeds and the receiver’s entire collision-response
word is zero. Refused clearance or any nonzero bit in that word sets ai_mode
to zero and ai_delay to 255. These outcomes return the progress result one;
none completes the enclosing movement task.
MovePos
MovePos(x, y, z) is a three-argument function that returns a horizontal
proximity result without writing actor state. It resolves the receiving
actor’s position before checking the actor’s movement-state byte. State 7
returns 1 without a speed or distance query.
For other states, the native queries GetMovementSpeed for that receiver.
Let s be the speed shifted left by eight bits. Zero s returns 1.
Otherwise the threshold is formed by multiplying (2 * s - 1) by the engine’s
movement-time factor, keeping the low 32 bits, interpreting them as signed,
and shifting right arithmetically by eight bits. A nonpositive threshold also
returns 1 without querying distance.
For a positive threshold, the distance helper receives target X minus resolved
X, zero, and target Z minus resolved Z, with 32-bit wrapping subtraction. The
Y argument does not affect the result. A signed distance result greater than
the threshold returns 1; an equal or smaller result returns 0. The native
performs no coordinate writes, relocation, or movement-task scheduling.
SetLocation
SetLocation(location) selects a coordinate triple from the receiving actor’s
location table. A nonnegative location at or below the table’s signed upper
bound selects that entry. An absent table, negative location, or location above
the bound selects the saved start coordinates.
The relocation helper writes all three selected coordinates unchanged into both the actor’s current local position and its previous-position triple. When the receiver is the selected player, it also translates the engine’s tracked reference-position vector by the same displacement. The subtraction and addition wrap at 32 bits, preserving the prior difference between the actor’s local position and that reference vector. Other receivers leave the vector unchanged.
After these writes, the helper registers the actor in the pending-position list. Existing entries suppress duplicate insertion. Empty-list insertion precedes the capacity check; a nonempty list appends an absent actor only when its signed count is below its signed capacity. A full nonempty list reports an error after the position writes. Normal completion returns zero.
OffsetLocation
OffsetLocation(source, x, y, z) transforms the supplied offset by the source
actor’s matrix, adds the result to the source’s resolved position with 32-bit
wrapping addition, and writes the three sums into the receiving actor’s current
local position. It returns zero and removes the fourth argument from the
caller’s stack. It makes no pending-position registration call.
The native resolves the source position first and the receiver position second. It discards the receiver’s resolved position. Source and receiver may be the same actor. Position resolution selects the actor’s alternate position when its alternate-position attribute is nonzero; otherwise it selects the attachment record’s position when that record names an attached actor, or the actor’s current local position when no such attachment is active. The matrix always belongs to the supplied source actor, regardless of which position storage is selected.
The matrix helper converts the three signed offsets to single precision before multiplying them by the source’s three matrix rows. Products and additions use x87 intermediates. Each completed row is stored in single precision, reloaded, and converted to a signed 32-bit integer using the incoming floating-point rounding mode. The matrix helper leaves that control word unchanged. The subsequent position additions wrap at 32 bits.
SetObjectLocation
SetObjectLocation(location, x, y, z) selects coordinates from the receiving
actor’s location table. A nonnegative location at or below the table’s signed
upper bound selects its coordinate triple. An absent table, negative location,
or location above that bound selects the actor’s saved start coordinates.
The three remaining arguments are independent flags. A nonzero flag copies
the selected X, Y or Z coordinate unchanged into the actor’s matching current
local-position component; zero leaves that component unchanged.
After the selected coordinate writes, the native registers the receiving actor in a shared pending-position list. It does so even when all three flags are zero. An existing occurrence suppresses a second insertion. An empty list receives its first entry before the helper checks the capacity field. A nonempty list appends an absent actor only while its signed count is below the signed capacity. Otherwise it reports an error after the coordinate writes. Normal completion returns zero. The native removes the fourth argument from the caller’s stack.
SetStartPosition
SetStartPosition(x, y, z) restores selected components of the receiving actor’s
current local position from its saved start position. Each argument is an
independent flag: zero leaves that component unchanged, and any nonzero 32-bit
value copies the corresponding saved coordinate unchanged. The arguments are
not coordinate values. The saved start coordinates remain unchanged.
The native returns zero. It performs no helper calls and writes only the selected current-position components. When all three flags are zero, it does not access the receiver’s coordinates.
DistanceFromLocation numerical distance helper
The numerical helper used by DistanceFromLocation takes three signed 32-bit
coordinate differences. It converts each difference to single precision,
forms the three squares with x87 intermediates, and adds X squared to Y
squared before adding Z squared. The sum is stored as a double-precision
argument to the square-root wrapper. For a nonnegative argument that wrapper
loads the double and executes the x87 square-root instruction.
The helper stores the square root in single precision, reloads it, and converts
it to a signed 32-bit integer. These conversions and intermediate arithmetic
use the incoming x87 control word. The helper does not replace that control
word or force truncation. With nearest-even rounding, the triple (3, 4, 0)
returns 5 and (1, 1, 0) returns 1. With rounding toward positive infinity,
(1, 1, 0) returns 2. Single-precision stores can change results before the
integer conversion, so an integer square root of the original differences is
not equivalent.
With masked floating-point exceptions, an out-of-range final integer conversion
produces the x87 integer-indefinite value, whose 32-bit pattern is 0x80000000.
For example, under nearest-even rounding, the triple (2147483647, 0, 0)
produces that value after its single-precision conversions.
CountAiShells list query
CountAiShells(name) is a one-argument function. It returns zero when the AI
shell list is disabled or its head is null. Otherwise it extracts the filename
stem from name into a shared nine-byte buffer, with an eight-character stem
limit, and uppercases that stem. The receiver is not dereferenced.
It traverses the AI shell list through each entry’s next pointer. For each entry it clears a second nine-byte shared buffer, copies nine bytes from the entry’s start into that buffer, and compares it case-insensitively with the query stem. Each equal comparison increments the result. Traversal counts every matching entry, including duplicates, without changing entries or links.
The next pointer follows the entry’s first eight name bytes, so the ninth copied byte is the low byte of that pointer. The copy does not append a terminator. The comparison therefore uses that ninth byte as well as the name bytes; this is not a fixed-width comparison of eight bytes. The list traversal has no cycle check or iteration bound.
AddFlatEffect
AddFlatEffect(x, y, z, spread, descriptor, speed) is a six-argument function returning zero. It
allocates a flat-effect record and returns without effect when the effect pool is disabled or has no
free record. The descriptor’s low sixteen bits select an installed image; an unknown image is a fatal
error. The record keeps the descriptor in its high word and the image’s frame count minus one, and
speed as its animation speed; its two retained motion words start at zero.
Placement depends on spread. With zero, the Y argument is lowered by the image height times 128
and each coordinate is stored shifted right by eight. With a nonzero spread no lowering happens, and each
coordinate (X, then Y, then Z) is stored as its argument shifted right by eight plus Rnd(spread >> 7)
minus spread >> 8, one draw of the shared random stream per axis, after the record and image were found.
A spread below 128 gives a bound of zero or less, which the reduction behind Rnd never leaves.
AddLightEffect
AddLightEffect(a, b, c, d, e, f, g, h) is an eight-argument function. Its
receiver and first three arguments do not affect the operation. It returns
zero when the shared effect system is disabled or no record is allocated.
Otherwise it returns the allocated record itself.
Allocation requests record type two. It requires an enabled effect system and a strictly positive signed free count, then scans the linked record list for the first zero type byte. A successful allocation decrements the free count, preserves the record’s next link and sets its type byte to two. A positive free count does not guarantee success when the list contains no free record. The allocation requests a four-byte zero fill at the record’s start, then restores the saved link; it does not clear the whole record.
The native then copies d into two record parameters and f into another
two, and stores h, g and e in three further parameters. Two control words
are set to one and two companion words to zero. These assignments do not
read the receiver or calculate a position from a, b or c. Record bytes
outside the assigned fields retain their previous values.
Type-two lifetime and modulation
The type-two update subtracts nominal ticks from the signed lifetime initialized
by h, using wrapping 32-bit arithmetic. A remaining lifetime at or below zero
ends the record before either modulation channel advances.
The f/g channel advances before the d/e channel. Each channel retains its
starting value, current value, modulation amplitude, period and phase. Creation
sets each period to one and each phase to zero. A zero amplitude leaves that
channel unchanged. For a positive amplitude, the phase adds nominal ticks with
wrapping 32-bit arithmetic. When the resulting signed phase is at least the
signed period, the phase resets to zero and the current value becomes
starting + Rnd(amplitude) - (amplitude >> 1), with wrapping 32-bit arithmetic.
Each triggered channel consumes its own shared random draw.
Type-two scene submission
The shared scene-draw pass visits retained records when the effect system is
enabled and its free count differs from its capacity. Its alternate-movement
flag applies the same nominal_ticks + 2 upward Y step before type admission,
including records whose type is no longer active.
A type-two record submits only when its current d-derived channel is strictly
positive as a signed 32-bit value. Submission copies the record’s retained XYZ
and the low sixteen bits of its current d- and f-derived channels into the
shared drawing descriptor. Other descriptor fields retain their previous values.
The call uses the ordinary drawing path. It does not derive a position from
AddLightEffect’s first three arguments or initialize the descriptor’s remaining
fields.
The descriptor is shared with actor-light submission and other local-light callers. Its three color words initially contain zero. Resetting the renderer’s accumulated light lists does not clear it. Actor-light submission replaces all three descriptor color words. Type-two submission preserves those words, so it inherits the descriptor’s current color, including across a light-list reset when no caller replaces that color.
The separate object-light drawing path traverses its retained spatial list before drawing the shared effect pool. A record without a light is skipped. Its actor-light submission is also skipped when the light is disabled or its signed radius is below one. Each admitted light replaces all three descriptor color words in traversal order. The last admitted light’s color therefore reaches subsequent type-two submissions unless another caller replaces it.
The ordinary local-light consumer converts retained XYZ from signed 32-bit
integers and the three color words from signed 16-bit integers to single
precision. It takes the low word of the d-derived channel as a signed radius.
The f-derived descriptor word does not participate in that local-light
submission. Neither channel supplies fresh RGB values.
ReloadPosition
ReloadPosition() is a zero-argument function. It copies the receiver’s
current local logical XYZ unchanged to previous-position history. For each
axis, it then shifts the signed logical component arithmetically right by
eight and stores that integer as a single-precision floating-point model
coordinate. Negative fractional coordinates therefore round down before
conversion rather than truncating toward zero.
The shifted signed 32-bit input lies between -8388608 and 8388607 inclusive. Every integer in that range is exactly representable in single precision, so the conversion itself does not discard additional precision. Current logical coordinates remain unchanged. The function returns zero and does not invoke position registration, attachment resolution or environmental model-position adjustments.
SetRotation
SetRotation(x, y, z) is a three-argument function. It masks each argument
with 2047, writes the results to the receiver’s current pitch, yaw and roll,
then copies that triple to its previous-angle history. Existing angles do not
supply any component of the new triple.
It then rebuilds the receiver’s orientation matrix. Rotation mode attribute 28 equal to exactly one selects the alternate matrix construction, passing all three masked angles unchanged. Every other byte value first resets the matrix to the identity, then applies negated yaw, negated pitch and negated roll in that order. Each negated angle is masked with 2047 before its matrix operation. All operations target the receiver’s own matrix.
The function returns zero after matrix construction returns. It does not register the actor in the pending-position list.
SetPosition
SetPosition(x, y, z) is a three-argument function. It copies the arguments
unchanged to the receiver’s current local coordinates, then copies that
triple to its previous local coordinates. It does not quantize the arguments
or apply the receiver’s vertical offset.
For the selected player, it also translates a retained shared reference
triple by the same displacement. For each axis the new reference is
old_reference + argument - old_local, with wrapping 32-bit arithmetic.
Other receivers leave this reference triple unchanged.
After these coordinate writes, it registers the receiver in the pending-position list. An empty list appends the receiver before checking capacity. A nonempty list first searches for the receiver and leaves an existing entry unchanged. An absent receiver appends when the signed count is below signed capacity. Otherwise the helper invokes diagnostic error 196 without appending. The native returns zero when the helper returns.
FindAiShell list query
FindAiShell(name) is a one-argument function returning a Boolean. A disabled
AI shell list or a null head returns zero before processing the name. Otherwise
it extracts and uppercases the filename stem into its shared query buffer, then
walks the list in link order. Each entry is copied into a second shared buffer
and compared case-insensitively with the query stem.
The first equal comparison returns one immediately, before reading that entry’s next pointer or visiting later entries. Reaching the null link without a match returns zero. The result is a presence flag, not an entry pointer or a count. The receiver is not dereferenced, and entries and links are not changed.
Its stem extraction uses a limit of eight, and each entry copy uses nine bytes
without appending a terminator. As in CountAiShells,
the ninth byte comes from the low byte of the following next pointer. Its two
shared scratch buffers are separate from those used by CountAiShells.
Traversal has no cycle check or iteration limit.
FxPhase
FxPhase(a, b, c, d, e, f, span) is declared as a seven-argument multitask.
Its native handler resolves the receiver’s auxiliary record before testing
span. A zero span returns zero without changing the shared phase state or
assigning the three output words. Record allocation and initialization can
therefore occur even on this return path.
A nonzero span adds the shared step value to a shared 32-bit accumulator.
If the signed accumulator is strictly greater than the signed span, the
accumulator resets to zero and the shared direction becomes
(direction + 1) & 1. Equality does not reset it. The accumulator and direction
are native execution scratch. Statement-call setup clears them before operand
evaluation. Each active task slot saves and restores its own pair, so concurrent
phase tasks advance independently. Direct native hooks inherit the current
scratch without the statement setup reset.
A nonzero direction interpolates from (a, b, c) toward (d, e, f); zero
interpolates in the reverse direction. For each pair of start and end values,
the handler subtracts their independently left-shifted-by-eight values with
32-bit wrapping arithmetic. It divides that signed difference by the signed
span, truncating toward zero, multiplies the quotient by the accumulator
with 32-bit wrapping, then arithmetically shifts right by eight and adds the
start value. The low 16 bits are stored in the receiver’s corresponding current
RGB channel. There is no output clamp. The handler returns one and removes four stack
arguments. Signed division overflow is not guarded.
The receiver resolver reuses an existing record. For a missing record it increments allocation counters, requests 41 bytes and initializes selected fields before the native proceeds. Allocation failure is not checked before record writes. This allocation does not reset the shared accumulator or direction.
FxLightSize
FxLightSize(first, second) configures the same radius-modulation channel as
LightSize(first, second). It does not enable the record or alter its lock flag.
It is declared as a two-argument multitask. Its
native handler resolves the receiver’s auxiliary record, writes the low
16 bits of first to one word and the low 16 bits of second to another,
and clears the intervening word. It returns zero. The arguments are not
clamped or scaled, and their upper 16 bits are discarded.
An existing receiver record is reused without reinitialization. A missing record increments the allocation count and byte total, requests 41 bytes and stores the returned pointer on the receiver. The resolver clears the three words before the native assigns them. Other record fields retain the resolver’s initialization or their previous values. Allocation failure is not checked before record writes.
FxFlickerOnOff
FxFlickerOnOff(bound) is declared as a one-argument multitask. Its native
handler resolves the receiver’s auxiliary record, then draws one bounded
random value. A nonzero draw leaves the record unchanged. A zero draw clears
a selected word when it is nonzero; when that word is zero, it copies the
paired word into it. Copying a zero paired word therefore leaves it zero.
The handler returns one. The selected word is the current radius and the paired
word is the starting radius. Toggling does not change the enabled or lock flags;
normal light maintenance can subsequently clamp an enabled record’s radius.
The bound is passed directly to the random generator, without an absolute
value conversion. For positive bounds the generator advances its shared
32-bit seed by multiplying by 1103515245 and adding 12345, with wrapping
arithmetic, and takes bits 16 through 30. For bounds at most 16384 it masks
the draw to the smallest low-bit mask covering bound - 1, with a minimum
mask of one. Larger positive bounds retain all 15 bits. Repeated subtraction
reduces the draw below the bound. A zero bound never exits this reduction.
An existing receiver record is reused. A missing record increments the allocation count and byte total, requests 41 bytes and stores the returned pointer on the receiver. The resolver initializes both words used by this native to 100. Thus a zero draw on a newly allocated record clears the selected word. Allocation failure is not checked before record writes.
FxFlickerDim
FxFlickerDim(amount) is declared as a one-argument multitask. Its native
handler obtains the receiver’s auxiliary record and takes the wrapping
32-bit absolute value of amount as a random bound. It draws one random
value and subtracts that same value from each of three unsigned base words.
Each result is truncated to 16 bits before being interpreted as signed.
Results below -255 are replaced by -255. The outputs are the receiver’s current
RGB channels. The three base words are its starting RGB channels, initialized
from the authored placement for a type-six object. Phase interpolation changes
current channels without changing these starting channels. The handler returns one.
The random generator advances its shared 32-bit seed by multiplying by
1103515245 and adding 12345, with wrapping arithmetic. It takes bits 16
through 30 of the new seed. For a positive bound at most 16384, it masks
this value to the smallest low-bit mask covering bound - 1, with a minimum
mask of one. Larger positive bounds use all 15 bits. Repeated subtraction
reduces the value below the bound. A zero amount or the minimum signed
32-bit amount does not terminate this reduction loop.
An existing receiver record is reused. A missing record increments the
allocation count and allocated-byte total, requests 41 bytes and stores the
returned pointer on the receiver. Initialization clears three leading bytes
and three following words, sets two words to 256 and two to 100, and clears
six further words. It does not initialize the three base words read by
FxFlickerDim. Allocation failure is not checked before record writes.
ChangeGroup membership update
ChangeGroup(group) is a one-argument function returning zero. It reads the
receiver’s current unsigned group byte before checking whether group-list
management is enabled. When disabled, it leaves that byte and the lists unchanged.
When enabled and the old group is nonzero, it searches that group’s list in link order for the first entry whose object pointer equals the receiver. If found, it unlinks and releases that entry. Removing the head updates the group head; removing a later entry updates its predecessor’s next link. A following entry receives the removed entry’s previous link. Failure to find the receiver does not prevent the group change.
It then stores the low eight bits of group as the receiver’s group and
registers the receiver in that group’s list. Group zero has no registration.
For a nonzero group, an existing matching receiver entry prevents insertion.
Otherwise registration requests a 13-byte entry, clears those 13 bytes, and
inserts it at the head. The entry contains next and previous pointers, the
receiver pointer, and a trailing zero byte. The previous link is null, and an
existing head’s previous link points to the new entry.
Changing to the same nonzero group still removes the first matching entry and performs registration again. With one matching entry this moves the receiver to the head; a remaining duplicate prevents a replacement allocation. Lookup stops at the first matching receiver and has no cycle check or traversal bound.
ScriptParticles
ScriptParticles(offset, count, tag, lifetime, mode, speed, resource, companion)
is an eight-argument function. It resolves a receiver position pointer and
adds offset to that position’s second 32-bit component in place, with
wrapping arithmetic. This changes the receiver’s selected position storage;
it is not a temporary position copy. The addition occurs before the particle
system’s enable check, including when the system is disabled or count is
zero or negative.
The position resolver first checks a receiver metadata byte. A nonzero byte selects the receiver’s alternate position. Otherwise, a nonnull receiver reference whose first word is nonzero selects the referenced position; all other cases select the receiver’s ordinary position. The reference is not consulted when the metadata byte selects the alternate position.
When enabled, the dispatcher resolves resource before testing the signed
count. A nonpositive count performs no record allocation. A positive count
repeatedly requests a free particle record, stopping at the requested count
or the first failed allocation. Allocation scans a contiguous array of
86-byte records up to its signed capacity, choosing the first zero tag word
and requesting an 86-byte zero fill.
For each allocated record, the dispatcher writes tag, the low 16 bits of
the resolved resource token and a copy of the three selected position
components. It clears a control word, stores lifetime, calls the velocity
setup with mode and speed, then stores 256 and -1 in two further control
words and writes companion. A zero tag leaves the record eligible for
selection again on the next allocation.
The wrapper preserves the dispatcher’s residual return register rather than
assigning a separate success value. A disabled dispatcher leaves count in
that register. An enabled nonpositive count leaves zero; completing the loop
leaves the requested count, while a failed allocation leaves zero even after
some records were initialized. These values do not report how many records
were successfully initialized before exhaustion.
ChangeUser membership update
ChangeUser(user) is a one-argument function returning zero. It reads the
receiver’s current unsigned user byte before checking whether list management
is enabled. When disabled, it leaves the user byte and lists unchanged.
When enabled and the old user is nonzero, it searches that user’s list for the first entry whose object pointer equals the receiver. A match is unlinked and released. Removing the head updates the user head; otherwise the predecessor’s next link is updated. A following entry receives the removed entry’s previous link. An absent receiver entry does not prevent the update.
The receiver’s user byte then receives the low eight bits of user.
Registration does nothing for user zero or when that user’s list already
contains the receiver. Otherwise it requests a 13-byte entry and inserts it at
the head, storing the old head as its next link, a null previous link, the
receiver pointer, and a zero trailing byte. An existing head’s previous link
points to the new entry. These stores initialize all 13 entry bytes; there is
no separate clearing call.
Changing to the same nonzero user still removes the first match before registration. With a single matching entry this moves the receiver to the head. A remaining duplicate prevents a replacement allocation. Lookup follows next links, stops at the first match and has no cycle check or iteration limit.
IsDrawingOrSheathing
IsDrawingOrSheathing is a zero-argument function. It calls
IsSheathingSword with the supplied receiver first. A nonzero result returns
one immediately. Otherwise it calls IsDrawingSword with that same receiver
and returns its result.
Both callees return zero when native execution is disabled, the receiver is null, the receiver’s animation/control pointer is null, or the shared mode word is nonzero. These checks precede their action and animation reads.
With those gates satisfied, IsSheathingSword returns one when the receiver’s
action byte is 21 or the animation/control record’s first word is 129.
IsDrawingSword returns one when the action byte is 20 or that word is 91.
The action comparison occurs first and a match skips the animation-word read.
The other returning paths return zero. The wrapper and both callees do not
modify receiver, animation/control, or global state.
IsCarryingWeapon
IsCarryingWeapon is a zero-argument function returning zero or one. It
uses the supplied receiver rather than replacing it with the global player.
It first reads the receiver’s metadata. A nonzero carrying-state byte in
that metadata returns one before the ordinary enable and mode checks.
When that byte is zero, the function returns zero if native execution is disabled, the receiver’s animation/control pointer is null, or the shared mode word is nonzero. Otherwise it reads the selected item identifier byte from the same metadata. A zero identifier returns zero.
A nonzero identifier is looked up in the shared ordered item list using unsigned identifiers. The first equal identifier wins; the first greater identifier stops the search. The function returns one exactly when that lookup finds an item of type one. Item quantity is not tested. All other returning paths return zero. The function and its item lookup do not modify receiver, metadata, item, or global state.
The initial receiver and metadata reads precede the later receiver-null check. That check does not make a null receiver a safe query input.
KillSlaves entry and family lookup
KillSlaves() is a zero-argument function returning zero for finite acyclic
runtime family structures. It first reads the receiver’s master byte. A zero
byte returns immediately without searching the family registry.
For a nonzero master byte, it calls the recursive family lookup with the
receiver and the registry head. That lookup visits each entry’s child chain
before its next sibling, recording each visited entry in shared scratch state.
It never compares an entry with the receiver or assigns a matching entry.
An exhausted chain returns null, so every completed recursive lookup returns
null and KillSlaves returns zero.
The later action calls and recursive KillSlaves call are unreachable through
this lookup result. The reachable path does not change actors or family links;
only the lookup scratch pointer changes. Lookup has no cycle check or traversal
bound.
CheckPlayerWeapon
CheckPlayerWeapon() is a zero-argument function. It does nothing when the
item system is disabled or the supplied receiver is null. Otherwise, it
replaces the receiver with the globally selected player; the supplied
nonnull receiver is not dereferenced.
The player’s selected item is retained only when the player has a nonnull animation pointer, a shared mode word is zero, the selected item byte is nonzero, and lookup finds an item whose type is one and signed quantity is strictly positive. Lookup scans the item list in order using unsigned item IDs. It returns the first equal ID or stops with no result at the first ID greater than the requested byte value. It does not search past that entry.
On any failed condition, the function clears the player’s selected item byte and runs the reset helper. For current animation words 129 or 91, that helper requests motion using the signed upper half of the player’s retained motion word and a zero third argument. Player action bytes 20 and 21 are cleared together with their companion byte; other action values are retained. A nonzero active byte is cleared, sets shared feedback to 25, and clears bit two of a player model byte while preserving its other bits.
The return register is residual state, not a Boolean result. The initial no-op paths retain the supplied receiver. A retained item leaves its item record pointer. Reset leaves the player pointer when the active byte was zero and the player’s model pointer when that byte was nonzero. The reset helper’s motion request result is overwritten before these returns.
UnLoadStatic
UnLoadStatic() is a zero-argument function. A separate receiver control
word gates the operation. When that word is nonzero, the native clears the
receiver’s static selection byte to zero. When the control word is zero,
it leaves that byte unchanged, including nonzero byte values.
The gate does not test the payload pointer assigned by LoadStatic.
UnLoadStatic retains that pointer on both paths. It returns the receiver,
changes no other receiver bytes and makes no calls to release or modify the
payload. Nonzero control words are treated alike, including values with only
the sign bit set.
The control word is the actor’s animation-group record pointer. On the file-based group-loading path, a missing frame table or a static selection byte equal to one skips record creation. Otherwise the loader releases any existing group record, stores a newly allocated 749-byte record in this word, zeros that record and initializes its first signed group word to minus one before reading the group data. A group request also requires this pointer and rejects a static selection byte equal to one. Other nonzero selection byte values do not satisfy that equality test.
The file-based loader installs and initializes the new group record before opening its group-data file. A zero open result records the input name as error context and requests system error 58. The loader performs no rollback of the group pointer or static selection byte before that request. If the error service returns, the loader passes the same zero file handle to the next section lookup; it does not return a recoverable load-failure result.
Startup reads debug.console_error and debug.attempt_recover from
SYSTEM.INI. A nonzero attempt_recover setting forces console_error to
one. The system-error recovery branch requires console_error to equal
one before notification and tests the stored attempt_recover word again
after notification returns. It does not use the notification routine’s
numeric return value for that test. These settings do not bypass the
other system-error phase and display-status gates.
GetBroadcastSenderId query
GetBroadcastSenderId() is a zero-argument function. It uses the receiver’s
unsigned broadcast-selection byte and the broadcast system’s no-selection byte.
Equal bytes return zero without reading a broadcast entry or sender.
For a selection different from the sentinel, it indexes the broadcast entry
array with a stride of 12 bytes, takes the entry’s first pointer as the sender,
and returns that actor’s stored runtime identifier as a signed 16-bit value,
using the same extraction as MyId(). The result is an identifier, not the
sender pointer or the entry index. The query does not consume the selection,
alter the entry or change the sender.
The native calls the broadcast-availability helper and then checks the selection against the sentinel again before indexing. The index is the full unsigned byte; there is no additional bounds or null-sender check on this path. An absent broadcast context requests a runtime error before the availability query. The availability helper also requests an error if the context is absent.
LoadStatic
LoadStatic(name) is a one-argument function. It resolves name to a record
payload, stores that pointer on the receiver, sets the receiver’s static
selection byte to one and returns the receiver itself. The existing pointer
and selection byte are overwritten; the native does not release the old
pointer or copy the payload.
The lookup obtains a search key through a shared name helper, copies it to a shared scratch buffer and uppercases it. It searches the first linked list in order and then the second list, comparing each entry name with that key. The first equal entry wins. Thus a match in the first list prevents any search of the second list, and later duplicate entries are ignored. A record with type byte two is rejected after selection; lookup does not continue to a later matching record of another type.
A missing name stores the requested name pointer in shared error context and requests error 33. Selecting a type-two record stores the same error context and requests error 34. For other selected types, lookup returns the record’s embedded payload pointer. The receiver assignments occur after lookup returns. List traversal has no cycle check or iteration bound, and the key scratch buffer is shared between calls.
HoldAnimation
HoldAnimation is a zero-argument function. A null receiver returns zero
without reading receiver state. Otherwise it reads the receiver’s
animation/control pointer. A null pointer returns zero. A nonnull pointer
causes a single control-record byte to be set to 255 and is also the
function’s residual return value.
The store replaces the entire byte, including when the previous value is already 255. Other control-record bytes and receiver state are unchanged. The handler makes no calls and does not read the ordinary native-enable or shared mode words.
PauseAnimation
PauseAnimation(value) is a one-argument function. A null receiver returns
zero without reading receiver state. Otherwise it reads the receiver’s
animation/control pointer. A null pointer returns zero. A nonnull pointer
causes the low byte of value to replace the same control-record byte
written by HoldAnimation. The upper argument bytes are ignored by the store.
On the store path, the residual return has the control pointer’s upper 24 bits and the argument’s low eight bits. The argument register is replaced by the control pointer. On the null receiver or null control paths, the argument register is unchanged. The other five saved general registers are preserved.
The handler makes no calls and does not read the ordinary native-enable or shared mode words. Apart from the selected byte, control-record and receiver state are unchanged.
PlayerArmed
PlayerArmed is a zero-argument function that calls IsHoldingWeapon on the
globally selected player and returns its Boolean result. The supplied
receiver is not used to select the queried actor. A null selected player
returns zero.
IsHoldingWeapon first rejects a null receiver. It then reads the receiver’s
metadata. A nonzero carrying-state byte in that metadata returns one before
the ordinary enable and mode checks.
When that byte is zero, the function returns zero if native execution is
disabled, the receiver’s animation/control pointer is null, or the shared
mode word is nonzero. Otherwise a nonnull receiver model pointer and a
nonzero receiver active byte are both required. The function then calls
IsSheathingSword on the same receiver and returns one only when that
predicate returns zero.
The model predicate reads the metadata carrying-state byte before its own
ordinary gates and tests the model pointer without dereferencing it. The
holding predicate and all helpers used by PlayerArmed leave receiver,
metadata, animation/control, and global state unchanged.
IsLightingOrUnlighting
IsLightingOrUnlighting is a zero-argument function. It calls
IsLightingTorch with the supplied receiver first. A nonzero result returns
one immediately. Otherwise it calls IsUnlightingTorch with that same
receiver and returns its result.
Both callees return zero when native execution is disabled, the receiver is null, the receiver’s animation/control pointer is null, or the shared mode word is nonzero. These checks precede their action and animation reads.
With those gates satisfied, IsLightingTorch returns one when the receiver’s
action byte is 33 or the animation/control record’s first word is 149.
IsUnlightingTorch returns one when the action byte is 34 or that word is 150.
The action comparison occurs first and a match skips the animation-word read.
The other returning paths return zero. The wrapper and both callees do not
modify receiver, animation/control, or global state.
SpeedScale
SpeedScale(value) is a one-argument function. It multiplies the argument by
a shared 32-bit scaling word and retains the low 32 bits of the product.
It interprets that truncated product as signed and shifts it right
arithmetically by eight bits. Thus its return is the signed truncated product
divided by 256, rounded toward negative infinity. Product truncation occurs
before the shift.
The receiver is not dereferenced, including when null. The handler makes no calls, modifies no receiver or global state, and does not read the ordinary native-enable or shared mode words. The argument register is unchanged.
ReleaseAnimation
ReleaseAnimation is a zero-argument function. A null receiver returns zero
without reading receiver state. Otherwise it reads the receiver’s
animation/control pointer. A null pointer returns zero. With a nonnull
pointer, it reads the same control-record byte used by HoldAnimation and
PauseAnimation. Only a value of 255 causes a store, which replaces it with
zero. Values from zero through 254 are retained without a write.
Every path with a nonnull control pointer leaves that pointer as the residual return value, whether or not a store occurs. The handler makes no calls and does not read the ordinary native-enable or shared mode words. Other control-record bytes and receiver state are unchanged.
GetMessageSenderId query
GetMessageSenderId() is a zero-argument function. It uses a channel byte
from the receiver’s animation/control subrecord and the receiver’s message
selection byte. Each channel has its own no-selection byte. Equal selection
and no-selection bytes return zero without reading a message entry or sender.
The function first calls the message-availability helper, which compares those bytes. If that helper returns zero, the query returns zero. Otherwise the query reads the channel and selection again and repeats the comparison.
For a selection different from the channel’s sentinel, the channel indexes a table of pointers to message-entry arrays. Each entry is 12 bytes. The selection indexes that array, and the entry’s first pointer supplies the sender. The result is the sender’s stored runtime identifier, extracted as a signed 16-bit value from the upper halfword of its identity word.
The query does not consume the selection or modify the receiver, channel tables, message entry, or sender. The helper and query both request the missing-context diagnostic when the message context is absent. Neither provides a null-sender or entry-bound check in the lookup path.
ItemUsed
ItemUsed(identifier) is a one-argument function. When native execution is
disabled, it returns zero without looking up the item. Otherwise it searches
the shared item list using the full unsigned 32-bit identifier. The first
equal identifier wins. The first greater identifier stops the search, even
if a later entry would match.
On success it returns the matched item’s stored used-state word unchanged, including its sign bit. It does not convert that word to a Boolean, check the item’s type or quantity, or change the item. The supplied receiver is not dereferenced.
New item records start with a zero used-state word. A completed UseItem
increments that word with 32-bit wrapping after the use script admission,
drop-use list, conditional self-drop and remove-after-use flag. Compass use
also reaches this common completion path. Refused attempts and the burning-
torch dousing path do not increment it. A script instance limit can suppress
a new shell without suppressing the remaining completion effects or increment.
A failed lookup updates the shared error context, copies the handler’s source-name text into the shared error buffer, and requests runtime error 179. The native contains a zero-result continuation after that request. The runtime error service determines whether that continuation is reached.
IsCombatCapable
IsCombatCapable is a zero-argument function and is the model predicate called
by IsHoldingWeapon. It queries the supplied receiver. It first reads the
receiver’s metadata carrying-state byte. A nonzero byte returns one before
the ordinary enable, animation/control, mode, and model-pointer checks.
When that byte is zero, the function returns zero if native execution is disabled, the receiver’s animation/control pointer is null, or the shared mode word is nonzero. Otherwise it returns one exactly when the receiver’s model pointer is nonnull. Neither the animation/control pointer nor the model pointer is dereferenced by this predicate. It makes no calls and changes no receiver, metadata, or global state.
The metadata reads precede the later receiver-null test. That test does not make a null receiver a safe query input.
IsInAir query
IsInAir() is a zero-argument function. A null receiver or a nonzero shared
mode word returns zero before reading receiver state. With a nonnull receiver
and shared mode zero, any nonzero sliding, falling or queued-motion byte
returns one. If all three bytes are zero, living-state values 27, 28 and 29
return one; every other living-state byte returns zero.
The result is Boolean. The function does not read the ordinary native-enable word, call other services, or modify receiver or shared state.
GetMessageSender query
GetMessageSender() is a zero-argument function returning the sender pointer
from the receiver’s selected message entry. It does not dereference that
pointer or extract the sender’s runtime identifier. A selected entry whose
sender pointer is null therefore returns zero.
The receiver’s animation/control subrecord supplies a channel byte. Each channel has its own no-selection byte, compared with the receiver’s message selection byte by the message-availability helper. An unavailable result returns zero before reading a message-entry array. Otherwise the query reads the channel and selection again and repeats the sentinel comparison.
When the selection differs from that channel’s sentinel, the channel indexes a table of pointers to message-entry arrays. The selection indexes the chosen array with a stride of 12 bytes. The entry’s first pointer is returned unchanged.
The query does not consume the selection or modify receiver, channel-table, message-entry, or sender state. Both the query and its availability helper request the missing-context diagnostic when the message context is absent. The lookup path does not check entry bounds or validate the returned pointer.
IsInLava query
IsInLava() is a zero-argument function. It returns one when the supplied
receiver’s liquid-state byte equals 5 and zero for every other byte value.
The result is Boolean. The receiver is dereferenced without a null check.
The function does not read the ordinary native-enable or shared mode words, call other services, or modify receiver or shared state.
IsInWater query
IsInWater() is a zero-argument function. It returns one when the supplied
receiver’s liquid-state byte is in the inclusive range 1 through 4 and zero
for every other byte value. The comparison is unsigned and the result is
Boolean. The receiver is dereferenced without a null check.
The function does not read the ordinary native-enable or shared mode words, call other services, or modify receiver or shared state.
IsMessageWaiting query
IsMessageWaiting() is a zero-argument Boolean function. It reads a channel
byte from the supplied receiver’s animation/control subrecord and compares
the receiver’s message-selection byte with that channel’s no-selection byte.
Equal bytes return zero; different bytes return one.
The function does not scan a queue, read a message-entry array, or inspect a sender. It does not dereference the message-context pointer. A nonzero context pointer is only tested for presence before the channel and selection reads. An absent context requests the missing-context diagnostic.
Receiver, control-subrecord, sentinel-table, message-entry, and sender state are unchanged. There is no null-receiver or null-control-subrecord gate before the receiver and channel reads.
InCircle query
InCircle(x, z, radius) queries the supplied receiver’s horizontal position.
A nonzero attached-position attribute selects its attached position. Otherwise
a linked positioned record with a nonnull first pointer supplies position;
if that record is absent or its first pointer is null, ordinary local position
is used. The attached-position choice takes precedence over the linked record.
The query subtracts x and z from the selected position’s X and Z with
32-bit wrapping, supplies zero for the Y difference, and calls the shared
distance helper. It returns one when that helper’s result, interpreted as a
signed 32-bit integer, is at most the signed radius; otherwise it returns
zero. The boundary is inclusive and selected position Y does not contribute.
The distance helper stores each signed difference at single precision, forms the squared sum using the active x87 precision and rounding settings, stores the sum at double precision, takes its square root, stores the root at single precision, and converts it to a signed integer using the active x87 rounding mode. The query does not establish a rounding mode of its own.
The query changes no receiver, metadata or linked-position state. It does not read the ordinary native-enable or shared mode words. Position selection reads receiver metadata before any receiver-null protection, so a null receiver is not a safe input.
Messaging context lifecycle
The messaging context contains 256 channel-array pointers. Each channel array contains the configured number of 12-byte entries and is cleared on allocation. Broadcast operations use channel zero. Each channel has a shared no-selection byte; receivers retain their own selection bytes separately.
The channel byte every message native reads for a receiver is attribute 125
(attr_mess_group), the same attribute that selects a combat message queue. The
engine has one messaging context: startup and combat reset each initialize it
with eight entries per channel, so the combat queues and the message natives share
channels, entries and the global entry limit. An entry holds sender, recipient and
value words; a combat message’s code is its value word. A receiver’s selection
byte is its retained read cursor, and a channel’s no-selection byte is the index
the next send writes.
Context initialization first releases the previous context. Releasing an active context clears all channel no-selection bytes, releases the channel arrays and then releases the pointer table. Releasing an absent context skips the channel loop. Both release paths leave the context absent and its capacity zero.
Separate map object updates copy the broadcast-selection byte from the separate record into the temporary actor before task processing and copy it back after processing. The selection therefore survives those temporary actor conversions.
Broadcast availability query
IsBroadcastWaiting() takes no arguments. It returns zero when the receiver’s
broadcast-selection byte equals the global broadcast no-selection byte, and one
otherwise. It does not consume the selection or modify messaging state.
The function requests the missing-context diagnostic when the broadcast context is absent. It checks only whether that context is present; it does not dereference the context or inspect a broadcast entry or sender. The receiver’s storage is read without pointer validation.
GetBroadcastSender query
GetBroadcastSender() is a zero-argument function returning the sender pointer
from the receiver’s selected broadcast entry. The pointer is returned unchanged
without dereferencing the sender or extracting its runtime identifier. A null
sender pointer in the selected entry therefore returns zero.
The query first calls the broadcast-availability helper, which compares the receiver’s broadcast-selection byte with the broadcast no-selection byte. Equal bytes return zero before reading an entry array. Otherwise the query reads the selection again and repeats that comparison.
For a selection different from the sentinel, the broadcast context’s first pointer supplies the entry array. Each entry is 12 bytes, indexed by the unsigned selection byte. The selected entry’s first pointer is the result.
The query does not consume the selection or modify receiver, entry-array, entry, or sender state. Both the query and its availability helper request the missing-context diagnostic when the broadcast context is absent. The lookup path does not check entry bounds or validate the returned pointer.
PipeHidden query
PipeHidden() is a zero-argument function returning a shared array of actor
pointers. It walks the shared actor list in list order, excludes the supplied
receiver by pointer identity, and appends each remaining actor whose hidden
byte is nonzero. The receiver is compared without being dereferenced.
For fewer than 64 selected actors, the array contains those pointers followed by a null pointer. The function returns the same shared buffer for an empty selection. It rewrites the selected prefix and terminator, leaving later slots unchanged. It does not change actor flags or list links.
After storing each selected pointer, the function increments the count. A count of 64 or greater requests runtime error 11 before following the next list link. The 64th pointer is therefore already stored when that request occurs. The handler contains a traversal continuation after the request; the error service determines whether that continuation is reached. The terminator is written only after traversal reaches the end of the list.
The query does not read the ordinary native-enable or shared mode words. It provides no cycle detection or malformed-pointer checks.
DeleteBroadcast selection advance
DeleteBroadcast() is a zero-argument function returning zero. It calls
the broadcast-availability helper before changing the receiver. When the
receiver’s broadcast-selection byte equals the no-selection byte, it returns
without changing the selection or reading the global broadcast limit.
Otherwise it increments the selection as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A new selection below the limit is retained; a selection greater than or equal to the limit is replaced with zero. The increment occurs before the comparison, including when a later reset stores zero again.
The function advances only the receiver’s selection. Neither the function nor its availability helper reads or modifies broadcast-entry or sender memory, dereferences the context pointer, or releases an entry. The helper requests the missing-context diagnostic when that pointer is zero. There is no receiver-null gate before the selection read.
PipeMasters query
PipeMasters() takes no script arguments and returns the shared actor-pointer
buffer. It first clears the shared output count and the buffer’s first slot.
If the supplied receiver’s attr_slave byte is zero, it returns that buffer.
Otherwise it searches the family tree for a slave entry naming the receiver.
The lookup visits a node’s child subtree before its own slave list, then continues through sibling nodes. The first matching slave entry wins. Shared scratch identifies the containing family node and the preceding slave entry. A missing match returns the buffer without appending anything further.
For a match, the query takes the containing node’s master actor, appends that
pointer, increments the shared count, and repeats from that actor. It stops
when the current actor’s attr_slave is zero or no slave entry names it. Actor,
metadata, family-node and slave-entry records are unchanged; lookup scratch and
the output count are shared mutable state.
The query checks the count before each append. If the count is already 64, it requests runtime error 11 before reading and storing the next master pointer. The error service determines whether the append continuation is reached. Exactly 64 pointers can be returned if the next iteration stops before append.
Only the initial first-slot clear is a null write. Appends do not write a null terminator, and unused later slots retain their previous contents. The query does not read the ordinary native-enable or shared mode words and provides no protection against cyclic family links or master chains.
PipeSlaves query
PipeSlaves() takes no script arguments. For a finite, acyclic family tree, it
returns the shared actor-pointer buffer with its first slot set to null and
the shared output count set to zero. Later buffer slots retain their previous
contents.
The native clears the count before calling its collector. The collector first
reads the supplied receiver’s attr_master byte. A zero byte returns without
family traversal. Otherwise it calls the receiver-master lookup, which walks
child and sibling links without selecting a matching node and returns no
result. The collector therefore returns before appending pointers or entering
its later diagnostic and recursive collection paths.
Traversal updates the shared visited-node scratch pointer. It leaves the preceding-entry scratch unchanged and does not change actor, metadata, family node or slave-entry records. After collection, the native writes the null terminator at output index zero and returns the shared buffer. No diagnostic is requested on these finite, acyclic paths.
The receiver metadata is dereferenced without a null check. The query does not read the ordinary native-enable or shared mode words.
DeleteMessage selection advance
DeleteMessage() takes no script arguments and returns zero. It first calls
the message-availability helper. The helper reads the receiver’s message
channel from its subrecord and uses that byte to select a no-selection byte.
If the receiver’s message-selection byte equals that channel’s no-selection
byte, the function returns without changing the selection or reading the
global message limit.
Otherwise it increments the selection as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A new selection below the limit is retained; a selection greater than or equal to the limit is replaced with zero. The increment occurs before the comparison, including when a later reset stores zero again.
Neither the function nor its availability helper reads or modifies message entries or sender memory, dereferences the context pointer, or releases an entry. The helper requests the missing-context diagnostic when that pointer is zero. There is no receiver-null or subrecord-null gate before the helper’s channel and selection reads.
PipeMyGroup query
PipeMyGroup() takes no script arguments and returns a shared array of actor
pointers. It first reads the supplied receiver’s unsigned group byte, then
walks the shared actor list. It excludes the receiver by pointer identity and
appends every remaining actor with the same group byte, preserving list order.
The receiver need not itself occur in that list. Group zero is compared like
every other byte value.
For fewer than 64 selected actors, the selected pointers are followed by a null pointer. An empty selection returns the same buffer with its first slot null. The selected prefix and terminator are rewritten; later slots retain their previous contents. Actor links, metadata and group bytes are unchanged.
After each selected pointer is stored, the count increments. A count of 64 or greater updates the shared source context, copies the handler’s source-name text including its terminator to the shared error buffer, and requests runtime error 11 before following the next actor link. The 64th pointer is already stored at that point. The error service determines whether the traversal continuation is reached. The output terminator is written only when traversal reaches the end of the list.
Receiver metadata is read even when the actor list is empty, without a null check. The query does not read the ordinary native-enable or shared mode words and provides no cycle detection.
PipeGroup query
PipeGroup(group) returns a shared array of actor pointers selected from the
shared actor list. It excludes the supplied receiver by pointer identity and
compares every other actor’s unsigned metadata group byte with the complete
32-bit argument. It does not truncate the argument to a byte. Arguments outside
zero through 255 select no actors. Matching actors retain list order, and the
receiver need not occur in the list. The receiver’s own group does not determine
the selection.
For fewer than 64 selected actors, a null pointer follows the selected prefix. An empty selection returns the same buffer with its first slot null. Later slots retain their previous contents. The query does not modify actor links, metadata or group bytes.
The selected pointer is stored before the count increments and the capacity check runs. A count of 64 or greater updates shared source context, copies the handler’s source-name text including its terminator to the shared error buffer, and requests runtime error 11 before following the next actor link. The 64th pointer is already stored at that request. The error service determines whether the traversal continuation is reached. The output terminator is written only when traversal reaches the end of the list.
The receiver is used only for pointer exclusion; its metadata is not read. There is no ordinary native-enable or shared-mode gate and no cycle detection.
ReceiveMessage entry selection
ReceiveMessage() takes no script arguments. It reads the receiver’s message
channel from its subrecord, then checks the receiver’s message-selection byte
against that channel’s no-selection byte. Equality returns zero before reading
the context’s entry-array pointer or any entry.
For an available selection, the channel selects an entry-array pointer from the messaging context. Entries have a 12-byte stride. A null recipient or a recipient equal to the receiver returns the entry’s 32-bit value unchanged. The matching entry remains selected, including when its value is zero. Recipient tokens are compared without dereferencing them; the sender field is not read.
An entry addressed to another receiver is skipped by incrementing the selection byte. The new byte wraps from 255 to zero, is zero-extended, and is compared with the global limit as a signed 32-bit integer. A value below the limit is retained; otherwise the selection is reset to zero. The function then repeats the sentinel and entry checks. The channel is captured once; the channel’s sentinel and entry-array pointer are read again during traversal.
Only the receiver’s selection changes while skipping entries. Entries and sender objects are unchanged, and no entry is released. A missing messaging context requests the missing-context diagnostic before the channel read. There is no receiver-null, subrecord-null, malformed-entry or iteration-limit gate in this function.
PipeUser query
PipeUser(user) returns a shared array of actor pointers selected from the
shared actor list. It excludes the supplied receiver by pointer identity and
compares every other actor’s unsigned metadata user byte with the complete
32-bit argument. It does not truncate the argument to a byte. Arguments outside
zero through 255 select no actors. Matching actors retain list order, and the
receiver need not occur in the list. The receiver’s own user byte does not
determine the selection.
For fewer than 64 selected actors, a null pointer follows the selected prefix. An empty selection returns the same buffer with its first slot null. Later slots retain their previous contents. The query does not modify actor links, metadata or user bytes.
The selected pointer is stored before the count increments and the capacity check runs. A count of 64 or greater updates shared source context, copies the handler’s source-name text including its terminator to the shared error buffer, and requests runtime error 11 before following the next actor link. The 64th pointer is already stored at that request. The error service determines whether the traversal continuation is reached. The output terminator is written only when traversal reaches the end of the list.
The receiver is used only for pointer exclusion; its metadata is not read. There is no ordinary native-enable or shared-mode gate and no cycle detection.
ReceiveBroadcast entry selection
ReceiveBroadcast() takes no script arguments. It compares the receiver’s
broadcast-selection byte with the broadcast no-selection byte. Equality
returns zero before reading the messaging context’s entry-array pointer or
any entry. This function does not read the receiver’s message-channel
subrecord or ordinary message-selection byte.
For an available selection, the first pointer in the messaging context gives the entry array. Entries have a 12-byte stride. A null recipient or a recipient equal to the receiver returns the entry’s 32-bit value unchanged. The matching entry remains selected, including when its value is zero. Recipient tokens are compared without dereferencing them; the sender field is not read.
An entry addressed to another receiver is skipped by incrementing the broadcast-selection byte. The new byte wraps from 255 to zero, is zero-extended, and is compared with the global limit as a signed 32-bit integer. A value below the limit is retained; otherwise the selection is reset to zero. The function then repeats the sentinel and entry checks. The sentinel and context’s first pointer are read again during traversal.
Only the receiver’s selection changes while skipping entries. Entries and sender objects are unchanged, and no entry is released. A missing messaging context requests the missing-context diagnostic before the selection read. There is no receiver-null, malformed-entry or iteration-limit gate in this function.
PipeAroundMe query
PipeAroundMe(distance) walks the shared actor list, excludes the supplied
receiver by pointer identity, and returns a shared array of selected actor
pointers in list order. For each other actor it subtracts the receiver’s three
position coordinates from that actor’s coordinates using wrapping 32-bit
subtraction. The signed differences pass through the numerical distance helper
described under DistanceFromLocation.
The helper’s signed integer result is compared with the complete signed 32-bit
argument. An actor is selected when that result is less than or equal to the
argument.
The query does not establish an x87 control word. Coordinate conversion, intermediate precision, square root and integer conversion follow the helper’s incoming floating-point settings. The comparison also treats an integer-indefinite result as a signed integer; it does not reject that result or negative arguments.
For fewer than 64 selected actors, a null pointer follows the selected prefix. An empty selection returns the same buffer with its first slot null. Later slots retain their previous contents. Actor links and positions are unchanged.
Each selected pointer is stored before the count increments and the capacity check runs. A count of 64 or greater requests runtime error 11 before following the next actor link. The 64th pointer is already stored at that request. This handler does not update source context or copy source-name text before the request. The error service determines whether traversal continues. The output terminator is written only when traversal reaches the end of the list.
The receiver need not occur in the list. Its position is read only when a candidate other than itself is visited. There is no ordinary native-enable or shared-mode gate and no cycle detection.
PipeAroundPos query
PipeAroundPos(x, y, z, distance) walks the shared actor list and returns a
shared array of selected actor pointers in list order. It excludes the supplied
receiver by pointer identity. For every other actor, it subtracts the three
explicit coordinate arguments from that actor’s position using wrapping 32-bit
subtraction. The receiver’s position does not participate in this calculation.
The signed differences pass through the numerical distance helper described
under DistanceFromLocation.
The helper’s signed integer result is compared with the complete signed 32-bit
distance argument. An actor is selected when the result is less than or equal
to that argument. The query does not establish an x87 control word or reject
negative arguments or an integer-indefinite result before this comparison.
For fewer than 64 selected actors, a null pointer follows the selected prefix. An empty selection returns the same buffer with its first slot null. Later slots retain their previous contents. Actor links and positions are unchanged.
Each selected pointer is stored before the count increments and the capacity check runs. A count of 64 or greater requests runtime error 11 before following the next actor link. The 64th pointer is already stored at that request. This handler does not update source context or copy source-name text before the request. The error service determines whether traversal continues. The output terminator is written only when traversal reaches the end of the list.
The receiver need not occur in the list and is used only for pointer exclusion. There is no ordinary native-enable or shared-mode gate and no cycle detection. At the native boundary the receiver and three coordinates enter in registers; the distance argument occupies one stack word, which the native removes on return.
AtPos position arrival
AtPos(x, y, z) takes three fixed-point coordinate arguments and returns zero
or one. It first arithmetic-shifts each argument right by eight bits. It also
arithmetic-shifts the receiver’s position coordinates right by eight bits, then
adds the receiver’s vertical adjustment to the shifted vertical coordinate.
These additions and subsequent coordinate differences use wrapping 32-bit
arithmetic.
The target-minus-adjusted-current differences pass through the numerical
helper described under DistanceFromLocation.
The arrival threshold is GetMovementSpeed() multiplied by 384, then
arithmetic-shifted right by eight bits. The speed query reads the supplied
receiver, including its animation-group and attribute guards. Arrival requires
the threshold to be strictly greater than the signed numerical distance result;
equality returns zero.
On arrival, all three receiver position coordinates are replaced with the shifted target arguments shifted left by eight bits. The low eight input bits are discarded, including for negative coordinates. The vertical position receives the target coordinate directly; the earlier vertical adjustment is not subtracted from that write. After the position stores, the receiver is registered in the shared changed-actor list, then the function returns one. The nonarrival path returns zero without changing the position or changed-actor list.
Registration appends immediately when the list count is zero, without checking capacity. For a nonzero count it first searches for the receiver by pointer identity. An existing entry leaves the list unchanged. An absent receiver is appended when the signed count is below the signed capacity. Otherwise the helper updates source context, copies source-name text including its terminator, and requests runtime error 196. The position stores have already occurred when that error is requested. The error service determines whether the function’s remaining continuation is reached.
The query does not establish an x87 control word or apply an ordinary native-enable or shared-mode gate. Its numerical result follows the helper’s incoming floating-point settings. It reads receiver position before the speed query’s missing-record guards.
Changed-actor list
The changed-actor list holds the actors whose map-cell membership must be refreshed before the
next regional selection. Its capacity is the [system] key max_remap_objects; the engine reads
a missing key as zero and raises any value below 32 to 32, so the capacity is at least 32. The
list is allocated and emptied when a world’s map structures are built, and again when they are
released.
Registration appends the actor unless the count is zero (an empty list appends without a capacity
test), the actor is already in the list (the list is searched by actor identity and left
unchanged), or the count has reached the capacity, which raises runtime error 196 after the
caller’s position stores. The runtime error service shuts the engine down unless the
attempt_recover setting is nonzero and the console-error gate permits recovery; with the
installed attempt_recover=no, the call never returns to its caller or to the interrupted script.
Entries stay in first-registration order.
Once per main-loop frame, after the actor updates and input and before the map list is built
around the viewer, the loop first removes actors flagged for deletion (their list slot is zeroed
and the actor leaves its cell) and then flushes the list. The flush visits the slots in order,
refreshes the cell membership of every nonzero entry, zeroes the slot and sets the count to zero.
A savegame write and read flush the list first, so a saved world never carries pending entries.
AtPos, GotoLocation, MoveAboveMe and the other natives that write an actor’s position
directly register it here.
ReplyBroadcastSender entry write
ReplyBroadcastSender(value) returns zero. It requests the missing-context
diagnostic before testing the argument when the messaging context is absent.
A zero argument returns without querying broadcast availability or the sender.
A nonzero argument also returns without changing entries when no broadcast is
selected, or when the selected sender is null or equals the receiver.
Otherwise the function uses the context’s first pointer as a 12-byte entry array. At the global broadcast no-selection index, it stores the receiver as sender, the selected broadcast’s sender as recipient, and the argument as the entry value, in that order. The sender token is compared and stored without dereferencing its object.
It then increments the global index as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A value below the limit is retained; otherwise the index is reset to zero. After that update, the function reads the context’s first pointer again and clears all three words of the newly indexed entry.
The entry stores precede the index increment, any reset, and the three clear stores. If the updated index names the slot just written, the clear stores erase that new entry. If it names the currently selected entry, that entry is cleared. The receiver’s own broadcast-selection byte is unchanged. The function does not inspect the destination entry before overwriting it, allocate an entry, or release one.
AtNodePosition coordinate-distance predicate
AtNodePosition(x, y, z, distance) returns one when the supplied coordinates
are within the supplied distance of the receiver, and zero otherwise. It
subtracts the receiver’s position from each corresponding coordinate argument
using wrapping 32-bit subtraction. These differences pass directly to the
numerical helper described under
DistanceFromLocation, without
shifting the coordinates or applying a vertical adjustment.
The helper’s signed integer result is compared with the complete signed 32-bit
distance argument. Equality is accepted. Negative arguments and an
integer-indefinite result are compared as signed integers without a separate
rejection. The function does not establish an x87 control word; its result
follows the helper’s incoming floating-point precision and rounding settings.
The predicate reads the receiver position directly, without a receiver-null gate. It does not look up a node, read node-map state, check an ordinary native-enable or shared-mode word, or modify actor or shared selection state. At the native boundary the receiver and three coordinates enter in registers; the distance argument occupies one stack word, which the native removes on return.
TurnToAngle native boundary
TurnToAngle(angle) first compares the complete argument with the receiver’s
stored yaw. Exact equality returns zero without querying weapon state or
changing yaw.
Otherwise the native queries IsHoldingWeapon for the supplied receiver. If
that query is false, it tests the receiver’s weapon-control flag. Either true
condition selects the unsigned armed-turn attribute byte; otherwise it selects
the unsigned ordinary-turn attribute byte. The selected byte is multiplied by
the shared movement scale with wrapping 32-bit arithmetic, arithmetic-shifted
right by eight bits, and clamped to a minimum step of eight.
The angular helper masks current and target yaw to 11 bits, subtracts target from current, and adjusts differences above 1024 or below -1024 by one full 2048-unit turn. The native negates this difference. The two exact half-turn signs remain distinct.
A zero difference or an absolute difference no greater than the step snaps stored yaw to the target masked to 11 bits and returns zero. For the selected player, the native requests the receiver’s orientation-matrix update after storing yaw and before returning.
For a larger difference, the step takes the difference’s sign. If the receiver has an animation record, the native requests a directional animation before changing yaw. A false holding query selects the ordinary directional pair, with the receiver’s style flag choosing its variant. A true holding query selects the armed directional pair. The animation request receives the supplied receiver and a zero third argument.
After that request returns, or immediately when no animation record exists, the native adds the signed step to current yaw and masks the result to 11 bits. The selected player then receives the orientation-matrix update request. The incremental path returns the original signed angular difference, rather than the step or the remaining difference after the update. Animation-selection and matrix-update services determine whether their callers’ continuations are reached.
The native does not apply an ordinary native-enable or shared-mode gate before the exact-yaw comparison. Its receiver-specific weapon predicates retain their own guards. Receiver metadata is required on paths that select a turn attribute.
ReplySender channel entry write
ReplySender(value) returns zero. It requests the missing-context diagnostic
before testing the argument when the messaging context is absent. A zero
argument returns without querying message availability or the sender. A
nonzero argument also returns without changing entries when no message is
selected, or when the selected sender is null or equals the receiver.
Otherwise the function reads the receiver’s message channel from its subrecord. That channel selects an entry-array pointer from the context and a global no-selection byte. Entries have a 12-byte stride. At that global index, the function stores the receiver as sender, the selected message’s sender as recipient, and the argument as value, in that order. The sender token is compared and stored without dereferencing its object.
It then increments that channel’s global index as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A value below the limit is retained; otherwise the index is reset to zero. The function reads the channel’s context-array pointer again and clears all three words of the newly indexed entry.
The entry stores precede the index increment, any reset, and the three clear stores. If the updated index names the slot just written, the clear stores erase that new entry. If it names the currently selected entry, that entry is cleared. The receiver’s own message-selection byte is unchanged. Other channels’ index bytes and arrays are unchanged. The function does not inspect the destination entry before overwriting it, allocate an entry, or release one.
SendBroadcast entry write
SendBroadcast(value) returns zero. When the messaging context is absent, it
requests the missing-context diagnostic before testing the argument. A zero
argument returns without reading the entry-array pointer or global limit and
without changing entries or the global broadcast index.
For a nonzero argument, the context’s first pointer supplies a 12-byte entry array. At the global broadcast no-selection index, the function stores the receiver token as sender, zero as recipient, and the argument as value, in that order. It does not dereference the receiver or inspect the destination entry’s previous contents.
It then increments the global index as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A value below the limit is retained; otherwise the index is reset to zero. The function reads the context’s first pointer again and clears all three words of the newly indexed entry.
The entry stores precede the index increment, any reset, and the three clear stores. If the updated index names the slot just written, those clear stores erase the new entry. No receiver selection is read or changed. The function does not allocate an entry or release one.
TurnToPos native boundary
TurnToPos(x, z) takes two script integers in the receiver’s stored coordinate
units. It first measures horizontal distance using the wrapped differences
current_x - x and current_z - z, with a zero vertical component. Its proximity
threshold is computed from GetMovementSpeed(receiver) + 1: shift left eight
bits, multiply by the shared movement scale with 32-bit wrapping, then shift
arithmetically right eight bits. The distance and threshold compare as signed
integers, including equality.
Within that threshold, the native writes the exact supplied X and Z coordinates, leaves Y unchanged, registers the receiver for position processing, and returns zero. Registration follows the shared position-processing queue rules, including its capacity diagnostic after the coordinate writes.
Outside the threshold, it computes the wrapped differences x - current_x and
z - current_z, converts them to a planar angle, negates that angle, masks it to
2047, and delegates to TurnToAngle on the same receiver. Its return is the
turning operation’s return.
For signed planar components other than the minimum signed integer, conversion
records their signs and takes their magnitudes. While either magnitude exceeds
511, it shifts both right by one bit. The positive Z axis is angle zero, positive
X is 512, negative Z is 1024, and negative X is 1536. Equal nonzero normalized
magnitudes produce a first-quadrant angle of 256. Otherwise it indexes the
arctangent table with floor(512 * smaller / larger) and complements about 512
when X is larger. Sign adjustment selects the quadrant, and the result is masked
to 2047. The zero vector produces zero.
The hardware arctangent initialization path fills 513 table entries for indices
zero through 512. Entry i is the integer truncation of
atan(i / 512) * 65536 / 3.141592654, shifted arithmetically right six bits.
PointAt native boundary
PointAt() takes no script arguments. It reads the shared pointing target and
subtracts the receiver’s floating-point X and Z coordinates from the target’s
corresponding coordinates. Each difference is truncated toward zero and stored
as a signed integer before planar angle conversion. For finite differences
within the signed conversion range, this discards the fractional component
before selecting the angle.
The shared pointing target is the shared source actor reference described under MoveAboveMe: the actor of the most recent ordinary interpreter entry. A call made from that actor’s own script on itself therefore yields zero differences, and a call routed to another object turns that object toward the source. The floating-point coordinates are the actors’ stored model coordinates (map units); their lifetime is described with the model presentation record. The planar angle is the original’s bearing helper on the integer X and Z differences, negated and masked to 2047.
The native negates the planar angle, masks it to 2047, writes it directly to the
receiver’s yaw field, and returns zero. It does not delegate to TurnToAngle,
request animation, or update a transform matrix. Existing yaw does not contribute
to the result. The shared pointing target is distinct from a script argument.
SendMessage channel entry write
SendMessage(value) returns zero. When the messaging context is absent, it
requests the missing-context diagnostic before testing the argument. A zero
argument returns without reading the recipient’s channel, the entry-array
pointer or global limit, and without changing entries or channel indices.
For a nonzero argument, the recipient’s subrecord supplies an unsigned channel byte. That channel selects both a pointer in the context table and a global no-selection index byte. Entries contain three 32-bit words. At the selected channel index, the function stores the receiver token as sender, the recipient token and the argument as value, in that order. It does not dereference the sender token or inspect the destination entry’s previous contents.
It increments the channel index as an 8-bit value, wrapping 255 to zero. The new byte is zero-extended and compared with the global limit as signed 32-bit integers. A value below the limit is retained; otherwise the index is reset to zero. The function reads the channel’s context-table pointer again. At the updated index it stores zero as sender, the recipient token as recipient and zero as value, in that order.
The initial entry stores precede the index increment, any reset and the final entry stores. If both indices name the same slot, the final stores replace the new sender and value with zero while retaining the recipient. The recipient’s message selection and other channels are unchanged. The function does not allocate an entry or release one.
The native takes the receiver, then a recipient actor, then the value: a call that
supplies two arguments, the recipient and the value, writes sender equal to the
receiver, the recipient argument and the value argument on the channel of the
recipient’s attribute 125. The definition declares only one argument, and the
value word is read from the third operand, so a call that supplies one argument
passes it as the recipient and leaves the value word at the receiver token.
That call also reads the channel byte through the recipient operand, so an
integer there selects an arbitrary channel. A zero value, which can only be
supplied explicitly, returns before the recipient is read. Observed in the original with a
synthetic statement in a live actor: two arguments (own token, 777) wrote the
sender, recipient and value on the receiver’s own channel and advanced its index; one
argument (1234) wrote the receiver token as sender and as value and 1234 as
recipient on an unrelated channel. No installed script calls SendMessage.
LineUp task entry and phase boundaries
LineUp(angle, x_offset, z_offset) is declared as a task with three arguments.
It uses a retained shared lineup target. If that target is null, it copies the
shared pointing target into the retained target before examining the lineup
phase.
When the phase is exactly one, it adds the angle argument to the target’s yaw,
masks to 2047, adds a half-turn of 1024, and masks again. It calls TurnToAngle
for the receiver with this heading. A nonzero turning result makes LineUp
return one. A zero turning result reaches the receiver’s animation-completion
service before the task can finish.
Other phase values first compare the target pointer with the receiver. Equality requests the self-target diagnostic before normal alignment work. For a distinct target, the task first adjusts receiver pitch toward zero using the shared pitch step, then sets bit one of the receiver’s alignment flag byte while preserving its other bits. It next queries receiver state before constructing the offset position. The X and Z offset arguments have not been consumed at this boundary.
Pitch adjustment treats the stored pitch as signed when choosing its branch.
For pitch above 1024, it compares the wrapped value pitch + 2048 with the step
as unsigned integers. A smaller value snaps pitch to zero; otherwise it writes
(pitch + step) & 2047. For positive pitch at most 1024, an unsigned comparison
of pitch with the step selects either zero or (pitch - step) & 2047. Nonpositive
pitch is left unchanged. The pitch helper changes no transform matrix itself.
LineUp mover
LineUp is the close-alignment mover that PlayerLineUp runs in close mode,
registered directly as the native handler, so its moving actor is the receiver
rather than the selected player. It reads the retained lineup target (copied
from the shared source actor on the first invocation, then kept in the task’s
native state) as the reference actor. The handler performs none of
PlayerLineUp’s mode selection or debug marker work and does not store its
result in the shared player-lineup result. A reference equal to the receiver
requests the self-target diagnostic.
With the retained fallback flag set, it turns the receiver to the reference
heading plus angle plus a half turn; a nonzero turn returns one, a zero turn
with an incomplete default-animation request also returns one, and otherwise it
writes six to the shared lineup phase byte, clears mask 2 of the alignment flag
and returns zero. Without the flag it levels roll, sets mask 2, rotates the
offsets by the reference heading plus angle, adds the reference X/Z, and runs
the same arrival, turning, bearing-selected step and fallback rules as
PlayerLineUp close alignment: completion clears mask 2 without writing
phase six, and a rejected step or a collision-response mark sets the fallback
flag.
WaitLineUp argument actor flag
WaitLineUp(actor) is declared as a task with one argument. It tests mask 2
in the argument actor’s alignment flag byte and returns one when that bit is
set, otherwise zero. Other bits do not affect the result. The supplied receiver
is not dereferenced, and the task does not change the argument actor, receiver,
or shared lineup state. It calls no helper or waiting service itself. The
argument must identify an actor whose flag byte can be read.
ObjectLineUp entry and abort boundary
ObjectLineUp(angle, x_offset, z_offset) is a task with three arguments. It
loads the shared pointing target directly. Unlike LineUp, its entry does not
initialize or select a retained lineup target.
Before pitch adjustment or target-coordinate access, it checks the receiver’s
condition word and unsigned state byte. A nonzero condition word together with
a state byte above eight aborts the task: it writes six to the shared lineup
phase byte, clears mask 2 in the receiver’s alignment flag byte while retaining
the other bits, and returns zero. The target pointer and all three arguments do
not affect this entry abort decision.
Otherwise it runs the pitch adjustment described for LineUp, sets mask 2 in
the alignment flag byte, and queries the receiver’s state before querying the
target or constructing the offset position. This entry uses the shared pointing
target even when a separate retained lineup target exists. The shared pointing
target is the shared source actor reference described under PointAt: the actor
whose script last ran an interpreter entry. It stays that actor while the receiver’s
own task stage later polls the task, so a receiver-owned task keeps aligning to the
actor that called it (the object-targeted call returns to its caller at once).
Observed in the original with a synthetic object-targeted call in a live actor: the
selected player took the task slot and set its alignment flag, and the shared
reference kept changing between actor pointers as other actors’ entries ran.
After the receiver and target queries, the task computes the heading
(target yaw + angle) & 2047 and rotates the X and Z arguments by that heading
with the offset-rotation service. The destination is the rotated offsets added to
the target’s X and Z; Y is not used. The destination test is the alignment
arrival test on the receiver. The final heading is (heading + 1024) & 2047.
When the arrival test returns one, the task turns the receiver to the final heading
with TurnToAngle. A nonzero turning result returns one. A zero result calls the
receiver’s animation-completion service: a nonzero result clears mask 2 of the
alignment flag byte and returns zero, a zero result returns one.
When the arrival test does not return one, the facing helper runs on the destination;
a nonzero result returns one. Otherwise the movement-admission helper runs with the
destination X, zero and the destination Z. When it returns exactly one, the step helper
runs on the destination; a zero result from the step helper takes the same final-heading
turn and completion sequence as arrival. In every other case the entry abort test runs
again: a nonzero condition word with a state byte above eight writes six to the shared
lineup phase byte, clears mask 2 and returns zero; otherwise the task returns one.
PlayerForceTalk positioning
PlayerForceTalk(distance) is a function with one argument. It builds the
integer vector (0, 0, distance) and transforms it through the receiver’s stored
three-by-three floating-point orientation matrix. It adds the resulting X and Z
components to the receiver’s stored X and Z with 32-bit wrapping arithmetic,
then writes those coordinates directly to the selected player’s X and Z. The
player’s Y and yaw are left unchanged. The receiver may itself be the selected
player; both coordinate additions use the corresponding receiver coordinate.
The matrix helper first converts each integer input component to a stored single-precision float, computes each matrix row’s dot product, and stores each result as a single-precision float. It then converts the stored results to signed integers using the current floating-point rounding mode. For the supplied zero-X, zero-Y vector and finite matrix entries, the X and Z contributions come from the third element of the matrix’s first and third rows respectively.
The native returns zero. It makes no dialogue, animation, matrix-update, collision-query or position-processing registration call. Its coordinate writes are direct; the selected-player pointer and receiver matrix must already exist.
PlayerFaceTalk
PlayerFaceTalk(distance) is a task with one argument that walks the selected player in front of the
receiving actor and turns it to face the receiver. The receiver and the selected player may be the same actor.
It returns zero when done and one while continuing.
The target is built from (0, 0, distance): the vector is transformed through the receiver’s stored
orientation matrix using the integer-to-float matrix helper described for PlayerForceTalk, and the
receiver’s X and Z are added to the transformed X and Z with wrapping integer arithmetic. The target stays
local; nothing is written to the player directly. The final heading is the receiver’s yaw masked to 2047
turned by a half turn (+ 1024, masked again).
Each invocation, in order:
- The selected player’s roll is leveled, as for
LineUp. A nonzero player attribute 26 then starts the actor diagnostic path (error 137); if the diagnostic services return, the invocation continues. - The facing-arrival test runs against the target. On arrival the player turns to the final heading with the support-response turn rules; while it is still turning the task returns one. Otherwise all four called flags are set, the default animation is requested, and on completion (the request always reports it) the player’s alignment flag is cleared and the task returns zero.
- Away from the target, the player first turns to the final heading; while it is still turning the task returns one.
- A nonzero packed collision-response word with an age above 8 abandons the task: return state 6, the alignment flag cleared, return zero.
- The relative bearing (the player’s heading minus the heading of the displacement to the target, wrapped to 2048) selects the close step as in close alignment: right from 257 through 768, backward from 769 through 1280, left from 1281 through 1792, forward otherwise. When the movement-distance check does not permit movement, or the step is kept, the task returns one.
- A rejected step has restored the saved state and put the player’s X/Z on the target. The task then turns to the final heading (returning one while turning), requests the default animation and, on completion, clears the alignment flag and returns zero.
The task never sets the player’s alignment flag, unlike PlayerLineUp; it only clears it. It does not store
the shared player-lineup result.
Guard native request boundary
Guard(a, b, c) is declared as a task with three arguments. Its native stores
the supplied arguments but does not use their values. It clears the receiver’s
guard-control byte, then calls its movement helper with that receiver, a fixed
radius of 262144 and a zero mode argument. If the helper returns, Guard returns
zero regardless of the helper’s result.
The helper measures the receiver’s displacement from its stored start position. A signed distance above the supplied radius selects a lookup of the start coordinates after arithmetic shifts right by eight. Distance equal to the radius uses the within-radius path. Within the radius, a clear activity byte together with both retained selection words equal to minus one requests random selection with a bound twice the radius before constructing candidate coordinates.
When a retained movement record or selected node is unavailable, the helper clears the activity byte and requests a receiver-state reset. For a valid selected node, it reads X and Z as unsigned 16-bit coordinates and Y as a signed 16-bit coordinate, shifts each left eight bits, and passes the receiver and coordinates to the movement service. Guard’s zero mode is forwarded in the movement request. Lookup, random selection, reset and movement services determine progress beyond these request boundaries.
ZapObjectNoKill receiver adjustment
ZapObjectNoKill(amount) is a function with one argument. A nonzero receiver
metadata exclusion byte prevents the operation. When the receiver’s weapon
requirement byte is nonzero, the operation also requires either the holding
weapon predicate or the model weapon flag. These gates precede player selection
and all adjustment writes.
For a receiver other than the selected player, the native reads the stored
unsigned adjustment byte, adds amount with 32-bit wrapping arithmetic, and
interprets the sum as signed. A sum above 255 stores 255, a positive sum at most
255 stores its low byte, and a nonpositive sum stores zero. The native does not
request a kill, animation or movement service on this path.
The native does not normalize its return value. The exclusion gate leaves the receiver metadata pointer as the result. A failed weapon requirement leaves the weapon predicate’s residual result: zero without a model, or the model pointer’s upper 16 bits with its lower 16 bits zero. An upper clamp returns 255. A positive unclamped sum replaces only the low byte of the receiver pointer in the result. A nonpositive sum leaves the receiver metadata pointer as the result.
For the selected player, after the same gates, the native calls the player
adjustment service with a zero mode and the original amount. That service
controls subsequent effects and the returned value.
ZapObject adjustment and follow-up boundary
ZapObject(amount) is a function with one argument. Its receiver metadata
exclusion byte prevents adjustment when nonzero. A nonzero weapon requirement
byte additionally requires the holding weapon predicate or model weapon flag.
Both gates precede player selection and adjustment writes.
For a receiver other than the selected player, it adds amount to the stored
unsigned adjustment byte with 32-bit wrapping arithmetic and interprets the sum
as signed. A sum above 255 stores 255. A positive sum at most 255 stores its low
byte. A nonpositive sum stores zero and replaces the local sum with zero.
A positive stored result returns immediately. The upper clamp returns 255; the positive unclamped path returns the receiver pointer with only its low byte replaced by the sum’s low byte. The exclusion gate leaves the metadata pointer as the result. A failed weapon requirement leaves zero without a model, or the model pointer’s upper 16 bits with its lower 16 bits zero. The native does not normalize these return values.
After a nonpositive result, the native queries the holding weapon predicate again. A true result requests an animation condition query for the receiver with animation identifier 122. A false result requests a separate receiver-state predicate. These queries precede further animation and finalization services. The adjustment byte has already been written when either query is requested. Their results determine subsequent behavior.
For the selected player, after the initial gates, the native instead requests
the player adjustment service with zero mode and the original amount. This is
a distinct service from ZapObjectNoKill’s player request. It controls later
effects and the returned value.
ZapPlayerNoKill selected-player adjustment
ZapPlayerNoKill(amount) is a function with one argument. It saves its receiver
but selects the player through the shared player pointer. A nonzero shared
adjustment gate bypasses all updates and leaves the supplied receiver as the
return value. This gate is tested before the selected player is read.
With the gate clear, a shared threshold-controlled value below 50 in signed
comparison is replaced with 25; values at least 50 are retained. The native
reads the selected player’s unsigned adjustment byte, adds amount with 32-bit
wrapping arithmetic, and treats the sum as signed.
It first compares the sum with the shared signed adjustment ceiling. A sum above the ceiling stores the ceiling’s low byte and uses that unsigned byte as the local result. Otherwise, a positive sum stores its low byte while retaining the full signed sum as the local result. Otherwise it stores zero and uses zero as the local result. The ceiling comparison precedes the comparison with zero, so the ceiling is not implicitly restricted to the byte range.
A nonnegative amount publishes the local result to the shared adjustment-result
word and returns that same result. The published result can differ from the
stored byte when a positive sum is within a ceiling above 255. The receiver’s
identity does not select the adjusted actor.
A negative amount instead requests a service for the selected player with mode
2 after the threshold and adjustment-byte updates, before publishing the local
result. Further service processing controls progress beyond this request.
ZapPlayer adjustment and zero-result request
ZapPlayer(amount) is a function with one argument. It reads the selected
player’s unsigned adjustment byte before testing the shared adjustment gate.
A nonzero gate bypasses all writes and returns that byte. The receiver does not
select the player being read.
With the gate clear, the native adds amount to the byte with 32-bit wrapping
arithmetic. A shared threshold-controlled value below 50 in signed comparison
is replaced with 25. A negative amount then requests a service for the selected
player before writing the adjustment byte or publishing the local result.
For a nonnegative amount, the signed wrapped sum is compared with the shared
signed ceiling before comparison with zero. A sum above the ceiling stores the
ceiling’s low byte and uses that unsigned byte as the local result. Otherwise a
positive sum stores its low byte and retains the full sum as the local result.
Otherwise the native stores zero and uses zero. It publishes the local result
to the shared adjustment-result word.
A nonzero local result, or a nonzero shared zero-result override, clears the
retained animation selection, adjustment gate and adjustment clock. For a
nonnegative amount, this path returns the local result. A zero local result
with a clear override instead requests the selected-player service with mode 6.
The byte and shared result have already been written at this request boundary;
the retained selection, gate and clock have not yet been cleared by this path.
Further service processing controls progress beyond that request.
Zap natives and the selected-player health services
The selected-player services the Zap natives call are the two AdjustHealth
uses. ZapPlayerNoKill and the selected-receiver branch of ZapObjectNoKill are the additive
service; ZapPlayer and the selected-receiver branch of ZapObject are the death-owning service.
The service takes a damage source actor. ZapPlayer passes its own receiver, which the service
treats as absent when it is the selected player, so the receiver’s attribute 109 selects the death
animation only when a different actor calls it; ZapObject’s selected branch passes none. The
receiver gates of the object natives are those of the generic health services: a nonzero attribute
120 returns first, then a nonzero attribute 97 needs a held weapon or combat flag four. None of
the four natives applies the early selected-player feedback update that AdjustHealth performs.
The generic nonplayer services behind SetHealth and AdjustHealth are these same two bodies: the
health-with-death admission service of SetHealth and of AdjustHealth mode one is the ZapObject
body, and the additive service of every other mode the ZapObjectNoKill body.
When ZapObject leaves a nonplayer receiver at zero health it ends the receiver with a death
dispatch. A receiver holding a weapon force-starts group 122 when it has that group and group 121
otherwise, releases its held walk nodes, stores death default 124 or 123, resets its combat action
selector to -1 and takes death selector eight; the dispatch dereferences the combat record, so a
receiver without one faults. Any other receiver force-starts group 74 when its current group is
114, 116, 130 or 131 or its falling byte is nonzero, group 134 otherwise, and takes death selector
eight with no death default or selector reset.
lookCyrus target forwarding boundary
lookCyrus(frames) is declared as a task with one argument. Its wrapper saves
its receiver but forwards the shared selected-player pointer and frames to
the exact native handler bound to lookObj. It returns that handler’s result
unchanged if the handler returns. The wrapper does not use its supplied receiver
to select the target.
The shared handler saves the target and frame argument, then requests formatting
of the literal label lookObj into the shared camera diagnostic buffer. This
request precedes its frame-sign check, camera counter updates and target
accesses. Neither the wrapper nor this prefix dereferences the selected player
or writes actor or camera state before the formatter request. The formatter and
subsequent camera processing determine progress beyond that boundary.
IsWithinView horizontal distance test
IsWithinView() is a function with no arguments. A clear shared view-test gate
returns zero before reading the receiver. With the gate enabled, the receiver’s
position-selection metadata byte chooses between its ordinary stored position
and its alternate stored position. Any nonzero selection byte chooses the
alternate position.
The native arithmetic-shifts the selected X and Z coordinates and the shared camera X and Z coordinates right by eight individually. It subtracts the shifted receiver coordinates from the shifted camera coordinates with 32-bit wrapping arithmetic. It passes the resulting horizontal X and Z differences and a zero vertical difference to the integer distance helper. Receiver and camera Y do not contribute.
The distance helper converts signed integer components to stored single-precision
floats, computes the sum of their squares, takes the square root, stores that
result as single precision, and converts it to an integer using the current
floating-point rounding mode. IsWithinView compares the resulting integer with
the shared view-distance threshold as signed values. It returns one for distance
at or below the threshold, and zero for a greater distance. Equality is admitted.
It writes no actor or shared camera state and makes no direction, visibility or
occlusion query.
The receiver’s position-selection byte is its attr_slave: a nonzero value
selects the attached (effective) position, so an attached receiver is measured
at its composed position and a free one at its local position. The view-distance
threshold is written once, when the actor system is initialized, as the absolute
difference of the [xngine] back_plane and front_plane settings of
SYSTEM.INI, each held shifted left by eight bits, shifted back right by eight
after the subtraction. With the documented defaults (3800 and 7) it is 3793.
swordAI initial terrain request
swordAI() is declared as a task with no arguments. Its native retains the
supplied receiver, reads the receiver’s stored X and Z coordinates, and
arithmetic-shifts each signed coordinate right by eight. It passes the shifted
X and Z to its first terrain-sampling helper. Negative fractional coordinates
therefore round downward at this boundary. Receiver Y, yaw and incoming argument
register values do not contribute to this initial request.
Before the terrain-sampling request, the native writes only its own stack. It has not changed receiver coordinates or shared state. The terrain-sampling helper and later movement processing determine progress beyond this boundary.
PopItem saved-value restoration
PopItem(id) is a function with one argument. When item processing is disabled,
it returns zero without searching the item list. Otherwise it searches the
shared ordered item list for the supplied unsigned identifier. An exact match
selects the first matching record; encountering an identifier above the supplied
identifier stops the search as a miss. The receiver does not select the list.
On a matching item whose pushed-value flag is nonzero, the native clears that flag and restores the saved 32-bit value into the item’s current-value field. With a clear pushed-value flag, it retains the current value. It returns the resulting current value in either case. The saved value itself is retained. A second call after restoration therefore returns the current value without repeating the restoration, unless another operation sets the pushed flag again.
A missing item records a diagnostic location and requests copying of the item error text into the shared error buffer before invoking the error service. This path has not changed any item record at the copy request boundary. The copy and error services determine progress beyond that request.
PushItem saved-value replacement
PushItem(id, value) is a function with two arguments. Disabled item processing
returns zero without searching the shared item list. With processing enabled,
it uses the same unsigned ordered lookup described for PopItem. The supplied
receiver does not choose the list.
If the selected item’s pushed-value flag is clear, the native sets the flag to
one and copies the current 32-bit value into the saved-value field. If the flag
is already nonzero, it retains both the flag byte and the saved value. It then
writes the supplied value into the current-value field and returns that value.
The write and return retain all 32 bits. Repeated pushes replace the current
value while preserving the first saved value until another operation clears
the pushed flag.
A missing item records its diagnostic location and requests copying of its error text into the shared error buffer before invoking the error service. No item record has changed at that copy request boundary. The copy and error services govern progress beyond the request.
PushAllItems indexed iteration
PushAllItems(value) is a function with one argument. Disabled item processing
returns zero without invoking PushItem. With processing enabled, it starts a
local index at zero and repeats while that signed index is less than the shared
signed item count. Each iteration invokes the exact PushItem native with the
original receiver, the index as the item identifier and the supplied value.
It increments the index after the call returns and rereads the shared count for
the next comparison. A zero or negative count executes no iterations.
The loop returns zero when it finishes, independently of the per-item return
values. It enumerates integer identifiers rather than traversing each linked
item record. A missing identifier therefore reaches PushItem’s diagnostic
path after any earlier items have already changed. There is no wrapper-level
rollback before that diagnostic request. The missing-item copy and error
services determine whether iteration can continue.
PopAllItems restoration and selection
PopAllItems() is a function with no arguments. Disabled item processing
returns zero without restoring items or selecting an item. With processing
enabled, it starts an index at zero and invokes PopItem(index) with the
original receiver while the signed index is less than the shared signed item
count. It increments the index after each return and rereads the count. A zero
or negative count skips restoration. The identifiers are consecutive integers,
not the identifiers obtained by traversing the item list. A missing identifier
therefore enters PopItem’s diagnostic path after earlier restorations.
After the loop finishes, the command ensures an item selection and returns zero. An existing nonnull selection is retained without changing the player’s selection byte. Otherwise, the ordered item lookup first requests identifier one. A matching record becomes selected and the player’s selection byte becomes one, regardless of that record’s current value or kind.
If that lookup fails, selection traverses the linked item list. Records with zero current value are skipped. The first record with a nonzero current value and kind one wins. If none has kind one, the first record with a nonzero current value wins. Selecting a record stores its pointer and copies the low byte of its identifier to the player’s selection byte. If no record qualifies, both fields remain unchanged.
SubItem signed subtraction
SubItem(id, amount) is a function with two arguments. Disabled item processing
returns zero without looking up an item or changing item state. Otherwise,
id uses the unsigned ordered lookup described for PopItem. A missing record
enters the item diagnostic path before changing item state.
For a matching record, a negative signed 32-bit amount is negated with 32-bit
wrapping arithmetic. The minimum signed value therefore remains negative. The
normalized amount is compared with the record’s current value as signed 32-bit
integers. If it is greater, the current value replaces the amount. The command
subtracts that amount from the current value with 32-bit wrapping arithmetic,
stores the result and returns zero. It does not change the pushed-value flag or
saved value, and does not invoke item selection.
For a nonnegative current value and an amount other than the minimum signed value, subtraction reduces the value by at most its current value. The signed minimum amount follows the same wrapping and signed comparison rules rather than being rejected or converted to a positive magnitude.
RemoveItem current-value clearing
RemoveItem(id) is a function with one argument. Disabled item processing
returns zero without looking up an item or changing item state. Otherwise, it
uses the unsigned ordered lookup described for PopItem: the first exact
identifier match wins, and a greater identifier ends the search. A missing
record enters the item diagnostic path before changing item state.
For a matching record, the command stores zero in the current-value word and returns zero, including when the word is already zero. It retains the saved value and pushed-value flag. The native does not invoke item selection, dependent-item removal or a presentation service. The receiver does not alter the lookup or clearing operation.
PlayerSnap local-position copy
PlayerSnap(angle, x_offset, z_offset) is a function with three arguments. It
forms a temporary heading from the receiver’s heading plus angle, masked to
2048 heading units, and passes temporary copies of the two offsets to the
planar rotation helper. It adds the receiver’s local X/Z to those temporaries.
The selected player’s stored local X/Z are then copied directly from the receiver’s local X/Z. The computed temporary coordinates are discarded. The three arguments therefore do not supply a positional offset to the player. The function returns zero, retains both actors’ Y and heading fields, and does not invoke the ordinary position-update or collision services. When the receiver is the selected player, the two assignments retain the existing X/Z.
The rotation helper still executes before the copy, including its table reads and floating-point operations. Discarding its temporary results does not remove that dependency or its floating-point exception behavior.
PlayerTilt receiver orientation
PlayerTilt(angle, strength, divisor) is a three-argument multitask. A nonzero
receiver attribute 26 starts the actor diagnostic path before tilt calculation.
The task uses half the larger receiver total X/Z extent, converted to the local
coordinate scale by a left shift of eight followed by division by two toward
zero, as its distance limit.
The retained actor association is the supporting-actor reference also read by
PlayerStand, distinct from the retained ledge-support reference. When this
reference on the selected player is not the receiver, the target pitch and roll
are both zero. Otherwise, the task computes a bearing
from the player’s local X/Z to the receiver’s effective X/Z. Effective position
on the ordinary attribute-zero path uses the linked record’s position when the receiver has an active linked record,
and the receiver’s local position otherwise. The bearing, receiver heading and
angle are added and masked to 2048 units, producing a. The unscaled targets
are:
- Pitch:
512 - awhena < 1024, otherwisea - 1536, then negated. - Roll:
awhena < 512,1024 - awhen512 <= a < 1536, otherwisea - 2048.
The distance used for strength is the horizontal integer distance between the
receiver’s local position and the player’s local position. This is distinct
from the effective position used for bearing. Distance is capped at the limit.
The associated-receiver branch divides by the limit without a zero guard.
The fixed-point distance factor is
(trunc(16777216 / limit) * capped_distance) >> 16. The supplied strength is
multiplied by that factor and arithmetically shifted right by eight. Each
unscaled target is multiplied by the resulting strength, arithmetically shifted
right by eight and masked to 2048 units. Multiplication and intermediate integer
arithmetic wrap at 32 bits.
If both current angles already equal their targets, the task returns one
without requesting an orientation refresh. Otherwise, each axis uses the
shortest signed wrapped difference from its current angle to its target. Both
inputs are masked to 2048 units before subtraction. The exact half-turn
difference retains the sign of target minus current. With a
zero divisor, the step starts as that difference. With a nonzero divisor, it
is the signed quotient truncated toward zero, clamped to a minimum of one.
The shared integer update multiplier is the whole nominal-tick increment of
the current update, including zero-tick updates. It multiplies the step with
32-bit wrapping.
For each axis, a signed difference greater than its step adds the step to the current angle without masking the stored sum; otherwise the target is stored directly. After these stores, the task requests the receiver’s orientation refresh. If that service returns, the task returns one. Orientation refresh rebuilds the local matrix from identity with negated yaw, pitch and roll, each masked to 2048 units, except that rotation mode one selects the alternate Euler builder. It then copies all three current angles to angle history. The diagnostic and orientation-refresh services are separate dependencies of the native.
PlayerTiltRevx orientation targets
PlayerTiltRevx(angle, strength, divisor) is a three-argument multitask. A
nonzero receiver attribute 26 starts its actor diagnostic path. When the
selected player’s retained actor association is not the receiver, target pitch
and roll are both zero.
For an associated receiver, the task computes the bearing from the player’s
local X/Z to the receiver’s effective X/Z and sets a = (bearing + angle) & 2047.
The receiver’s heading is not added. On the ordinary attribute-zero path, an
active linked record supplies effective position; otherwise local position is
used. The unscaled targets are:
- Pitch:
512 - awhena < 1024, otherwisea - 1536. - Roll:
-awhena < 512,a - 1024when512 <= a < 1536, otherwise2048 - a.
Horizontal distance for strength uses the receiver’s local position rather than its effective position. The distance limit, fixed-point strength scaling, signed wrapped angle differences, divisor handling, shared integer update multiplier and per-axis snap-or-add rules are those described for PlayerTilt. These rules act on the targets above, including the negated roll component.
Both angles already at target return one without refreshing orientation. Otherwise the task stores its axis updates and requests orientation refresh; if that service returns, the task returns one. Diagnostic processing and orientation refresh remain separate service dependencies.
Selected-player health query
PlayerHealth takes no script arguments. It reads attribute 3 as an unsigned
byte through the selected player’s control record and returns that value as
a zero-extended integer. The script receiver is unused. The native performs
no selected-player or control-record null check, makes no service calls, and
does not change actor or control-record memory. It preserves the incoming
non-result general registers and leaves argument-stack cleanup to its caller.
Actor animation playback
Actor animation selects polygon-mesh frames through RAAN references and RAGR command groups. Model frames contain vertex animation, not skeletal animation.
Model and frame selection
A file ShowFrame command uses an unsigned twenty-bit reference into the
actor instance’s frame table. Each RAAN entry contributes the indicated
number of model frames, starting at zero, in entry order. The loader resolves
the reference to a runtime model handle and frame index and patches those two
values into ten-bit command fields. Playback stores that pair as the actor’s
current pose. It does not interpret the second value as an attachment vertex.
Placed-object frame sources
RAAN is not the only source of an instance’s frame table. After initializing
a type-1 MPOB object, the loader uses
its model_name when the instance has no frame table, its static-model flag
is clear, and the name is nonempty.
- An MPOB
is_staticvalue of 1 selects the static model path and leaves no animation frame table. - Other values select the animated model path. The byte is the number of frame-table entries, each referring to that model and successive frame indices starting at zero. A zero value allocates an empty table.
The group loader installs the actor definition’s RAGR groups only when the instance has a frame table and its static-model flag is not set. It resolves ShowFrame operands against that instance’s table and rejects references outside the table. RAGR commands can therefore exist in a definition without becoming runtime animation groups for a particular placement. Instances sharing one definition can obtain their frames from different placed models.
Animation tasks
PushAnimation(group, count) requests the group if it is not yet current.
When the group becomes current, the task sets its byte loop countdown to count.
It remains active while that group is current and completes when the group
changes. Thus count is a finite loop input, not a speed or wall-clock duration.
The animation request made by this task has a speed modifier of zero.
The current group is read as a signed sixteen-bit value and compared with the complete thirty-two-bit group argument. This comparison does not first narrow the argument to sixteen bits. The loop count stores only the low eight bits of its argument. Changing the count operand after the task enters its started phase does not rewrite the countdown; that phase only compares the current group with the reevaluated group operand.
The first invocation has a separate not-started phase. If the requested group is not current, it makes an ordinary request and remains active. Even when that request starts the group immediately, this invocation does not set the loop count. A subsequent invocation that finds the group current sets the byte count and enters the started phase, still returning active. Once started, it completes only when the current group differs; it does not re-request the group. An initial invocation that already finds its group current sets the count immediately and remains active. Each invocation evaluates the call’s operands again. Task creation and later task invocations are separate scheduling stages.
The task does not test whether the requested group exists. Ordinary request fallback still applies, but selecting the default does not change the task’s requested group. While that requested group is not current, the task remains in its not-started phase and makes another ordinary request on each invocation. A missing group therefore does not by itself complete the task.
For forward playback, GoToPrevious with a nonzero loop countdown decrements
that countdown. If it becomes zero, playback continues after the command;
otherwise it jumps to the command’s target. With a zero countdown it jumps
without decrementing. GoToFuture continues past itself while a finite loop
countdown is nonzero. With no countdown, a pending transition or the animation’s
exit flag can make it take its target instead.
The group header currently called anim_id supplies the animation’s byte
frame countdown. An animation request adds its speed modifier to that value.
This is separate from the task’s loop countdown.
Frame waits
WaitAnimFrame(group, frame) inspects the receiver’s animation state.
WaitPlayerAnimFrame(group, frame) applies the same test to the currently
selected player, independently of the task’s receiver. Either task completes
when its inspected actor or animation association is absent.
If neither the current nor pending group matches group, the task completes.
If only the pending group matches, it keeps waiting. When the current group
matches, it completes once the unsigned byte anim.real_frame is greater than
or equal to frame; otherwise it keeps waiting. This counter is distinct from
the anim.frame command cursor and the selected model-file frame.
Selecting a new group resets anim.real_frame to zero. Ordinary frame
advancement increments it when the frame countdown expires, and each skipped
ShowFrame increments it again. The byte wraps from 255 to zero. A zero-tick
update or a countdown decrement that retains the same frame leaves it unchanged.
Command processing has a separate signed sixteen-bit cursor. It indexes the
packed command table and remains on the command displaying the current frame.
Selecting a new group resets it to zero. BreakPoint and DelayCounter
advance that cursor without incrementing anim.real_frame; a GoToPrevious
backward jump assigns its command target without resetting the counter.
Combat timing queries read this signed cursor, rather than anim.real_frame.
The animation record keeps one pointer per group at offset 0x25 + 4 × group;
a group without a table has a zero pointer. The upper sixteen bits of the first
word of a group’s table are its command count, and its commands take three bytes
each. anim.stop_flag is the animation’s exit flag: a forward GoToFuture with
no loop countdown takes its target when the flag is set, and taking the exit sets it.
A forced group request for the group that is already current only cancels the
pending request while the flag is clear. With the flag set it restarts the group,
resetting the command cursor and clearing the flag. A taken selected-player mask-8
transition sets the flag before its request, so that request restarts a current
group of the same number.
Current and pending group values are signed sixteen-bit values compared with the full script integer argument. The frame threshold also remains a full script integer; neither argument is narrowed before comparison. These tasks read the current state on each invocation without requesting or advancing animation, changing actor state, or calling another native handler.
Ordinary requests and default groups
Receiver eligibility
Animation group requests do nothing when the animation system is disabled, when the receiver has no animation state, or when the receiver is excluded from ordinary object updates.
Current, pending and default groups
An actor keeps a current group, one pending group, and a default group. Ordinary requests do not form a queue. When the current group is the default, requesting the default again returns without restarting it. Requesting another group clears the current group so that the new request can start immediately.
Otherwise, an ordinary request replaces the pending group when there is no current group, no pending group, the pending group number is less than or equal to the requested number, or the pending group is the current group. A lower number cannot replace a higher pending number outside those cases.
An absent requested group falls back to the actor’s default group if that group exists. If neither exists, the pending request is cleared. The accepted group’s low header byte plus the request modifier becomes its byte countdown, with byte wrapping. A request with no current group invokes animation advancement immediately using the existing shared tick input; it does not invent a tick.
When neither a current nor pending group exists, advancement requests the
default group with modifier zero. EndAnimation clears the current group and
invokes advancement again, so a pending request or the default can start in
the same update. Treating End as a permanently stopped actor loses this behavior.
Forced requests, group-type interruption rules, actor eligibility, and the number of advancement calls made by an actor update use additional rules. Request arbitration does not activate placed actors automatically.
Placed actor initialization and eligibility
New actor animation state initializes its default group to zero. For a placed object with MPOB type 1 and an animation model table, group setup builds the RAGR table and makes two ordinary requests for group zero with modifier zero. These requests reuse the shared nominal ticks.
The actor loader chooses its model path from the RAHD record. A zero
raan_count means no model. Otherwise, the static-model shortcut applies when
raan_count is one, the complete signed anim_control_prefix field is −1,
and the signed anim_frame_data field is less than two. All other nonzero-model
cases build the animation model table. This classification precedes group
requests and the model-path message-group attribute assignment. Exported actor
metadata records the result as model_load_path: none, static, or
animation_table. The complete signed anim_control_prefix field is exported as anim_control_prefix; its low byte is the
anim.max_group value of the actor’s animation record.
The MPOB activation byte selects storage and update-list membership. Zero stores an ordinary placed actor in its spatial map cell; a nonzero value uses the persistent actor list. The active update list combines nearby cell actors with that persistent list. A zero activation byte therefore does not mean that an actor can never animate.
Cell assignment uses the decoded MPOB X/Z coordinates directly, with 512 coordinate units per cell. The runtime position has eight fractional bits; shifting that position by eight recovers the decoded file coordinate. The three-byte MPOB coordinate itself is not divided by 256 again.
The cell window starts nine 512-unit cells before the supplied horizontal center, clamped at the map edge. The main loop supplies the current view coordinates, not the actor body position. It spans eighteen cells per axis, omitting its four corners. The inner sixteen by sixteen cells include ordinary actors. On the outer border, actors with a nonzero attribute byte 113 are excluded. Debug and global object-processing switches can further restrict this list. Membership alone does not make an actor eligible for an update; its runtime exclusion, pause, state and script checks still apply.
Cells are visited in increasing X within increasing Z rows. Eligible actors retain their current per-cell linked-list order; the persistent actor list is appended after the cell actors. New cell actors are inserted at the front of their cell list, and new persistent actors at the front of the persistent list. Consequently, actor creation order is not ordinary update order.
For each view coordinate, the window origin is zero below 4608, 56319 when the coordinate plus 4608 exceeds 65535, and otherwise the coordinate minus 4608. Dividing this origin by 512 gives the first cell. The regional update compares that starting cell with its cached entry and rebuilds the lists when they differ.
Moved actors enter a deduplicated deferred list in first-registration order. The frame loop consumes this list before regional actor selection, clearing its entries and count after the pass. Map-excluded script context suppresses relocation; pooled persistent actors also retain their existing membership. For a cell actor, relocation uses its linked position when present, otherwise its local position. Each X/Z coordinate is shifted right eight bits and masked to 16 bits before selecting its 512-unit cell.
An unchanged cell retains list order. A changed cell unlinks the actor from its previous cell and prepends it to the destination cell. After a cell change, the rounded horizontal distance from the camera to the actor’s local position, shifted right eight bits, invalidates the cached regional list when it exceeds 3584. Nearby relocation retains the cached traversal until another rebuild condition applies. Removing a queued actor clears its pending entry.
Ordinary default selection can use attribute byte 44, but combat, injury, attachment, movement and player-specific states can select other groups. Changing the default can force an immediate replacement when the old default was current and had not reached an exit. It is not safe to select attribute byte 44 unconditionally for every actor.
The installed ISLAND map has three FLAG2 placements, all ordinary objects with a zero activation byte and no static model override. Their FLAG2 attribute record is all zero, including byte 44. These values define their initial state.
Living actor default selection
For an actor that does not satisfy IsDead, ordinary default selection uses
these branches in order. The first matching branch wins.
| Condition | Default group |
|---|---|
Selected player satisfies IsInDeepWater | 141 |
| Actor is attached to a rope | 79 |
| Actor state is 15 | 7 |
IsSheathingSword is true | Attribute byte 44 |
IsDrawingSword or IsHoldingWeapon is true | 8 |
| Actor’s torch-burning counter is nonzero | 5 |
| Otherwise | Attribute byte 44 |
The selected-player deep-water branch also invokes weapon-state cleanup; selecting group 141 alone does not perform that transition.
IsSheathingSword and IsDrawingSword require the item system to be loaded,
an actor with animation state, and ordinary rather than shell execution.
IsSheathingSword accepts actor state 21 or current group 129;
IsDrawingSword accepts actor state 20 or current group 91.
IsHoldingWeapon returns false for an absent actor. For a present actor,
nonzero attribute byte 110 returns true before the item-system, animation-state
and shell checks. Otherwise it requires those three gates, combat capability,
a set weapon-drawn flag, and a false IsSheathingSword result.
After selection, when animation state exists and execution is outside the shell context, a changed default replaces the current group immediately if the current group was the old default and its exit marker is clear. A pending group equal to the old default is separately replaced by an ordinary request for the new default. These checks use the old default captured before selection.
Combat entry
beginCombat uses the lifecycle selector read by IsDead, independently of the
motion state used by weapon and living-default selection. On a nonplayer actor,
it first sets script-control bit 0x02, including when the lifecycle selector
already equals 6. A selector already equal to 6 skips the remaining entry work.
Otherwise entry writes selector 6, handles weapon setup, sets combat flag 0x04,
and requests the begin-combat taunt type 0, in that order.
Weapon setup checks attribute byte 23. When it is nonzero and animation group 91 exists, an actor whose weapon-drawn flag is clear runs weapon-draw admission and then immediately requests group 91 with modifier zero. An already drawn weapon skips both operations. When group 91 is absent, entry runs weapon cleanup, then sets the weapon-drawn flag and clears motion state to zero. A zero attribute byte 23 directly sets that flag and clears motion state, without weapon cleanup. These motion-state writes do not replace the lifecycle selector.
Weapon draw admission
Weapon-draw admission requires the item system, an animation state, no shell execution, no rope attachment, no motion state from 27 through 32, neither a draw nor sheath transition, no held-weapon predicate, no falling or queued motion, and motion state other than 15. For the selected player, attribute byte 105 equal to 1 also prevents admission.
After those gates, a nonzero held-item byte runs weapon cleanup. With a zero
held-item byte, absence of assigned weapon state also runs cleanup. Otherwise edge
motion states 10 through 19 and stored surfaces 2 or 4 prevent the draw. A
successful draw sets motion state 20. For the selected player it also sets
combat flag 0x04. DrawSword runs this admission for its receiver; the repositioning step that
follows it in the routine is inactive in the shipped game, so a draw never moves
the actor. This admission does not itself set the weapon-drawn flag.
Equipped-weapon transition
The equipped-weapon drawing pass requires the item system, an actor with animation state, ordinary execution outside the shell, and an assigned weapon. The actor’s weapon attribute selects an item of weapon type. Without combat state, this pass clears the drawn flag before processing motion state.
For motion state 20, the pass requests group 91 with modifier zero. While
that group is current, its wrapping frame-progress byte is compared with attribute
byte 35, attr_draw_frame. Before the threshold, the sheathed model is drawn.
At or beyond the threshold, the held model is drawn, the weapon-drawn flag is
set and motion state becomes zero. If group 91 is not current, the sheathed
model is drawn and the transition remains pending.
For motion state 21, current group 129 uses attribute byte 34,
attr_sheath_frame, as its frame-progress threshold. Before the threshold,
the held model is drawn. At or beyond it, the sheathed model is drawn, the
drawn flag clears and motion state becomes zero. A pending group 129 is
cancelled when that transition completes. If group 129 is not current, the
pass requests it with modifier zero and draws the held model. That invocation
does not test the sheath threshold again, even if the request immediately makes
group 129 current. Threshold processing resumes on a later drawing pass.
These transitions do not clear the companion motion byte. Outside these two motion states, a set drawn flag selects the held model. A clear flag selects the sheathed model, using the attached drawing path while group 129 remains current.
Weapon cleanup
Weapon cleanup restores the selected player’s stored playback-default group,
with modifier zero, when its current group is 91 or 129. Other actors and other
current groups do not receive that request. Motion state 20 or 21 is reset to
zero together with extra_time, independently of the drawn flag.
This clears the extra-time counter, not the independent rope_wait companion
motion byte or the motion-direction byte.
When the weapon-drawn flag is set, cleanup clears it and clears combat flag
0x04. For the selected player it also sets the
health-candle show timer to 25 ticks. If the drawn flag was
already clear, cleanup leaves the candle timer and combat flag unchanged.
Dead actor default selection
IsDead reads a runtime state selector rather than the health attribute. It
succeeds when that selector is 8, or when the selected player’s death override
is active. This selector is distinct from the actor state used by the living
default-selection branches.
When ordinary default selection reaches the dead-actor branch, an actor with combat state takes its default group from that state’s death-animation selection. Without combat state, it selects group 123 when that group exists, otherwise group 124. Group existence requires an animation state with a populated entry for the requested group.
Animation-table setup clears the actor’s combat-state association and creates combat state when attribute byte 14 is nonzero. This state is separate from the animation group’s playback cursor and from the actor’s attribute table. The combat record’s death-animation selection starts at zero; constructing the record does not copy attribute byte 44 into that selection. A newly allocated combat record receives the next combat index. Reinitializing an existing record retains its index. The target starts absent, the action selector starts at -1, and the cooldown starts at zero. Move bindings follow COMBAT.INI actor move selection.
Ordinary cell and persistent actor allocation clears the actor record before common initialization. The death selector and object-update exclusion therefore start at zero, and the combat-state association starts absent. These are allocation values, separate from later player-role setup and gameplay changes.
Both standalone player roles use the same cleared object allocator before player-specific initialization. Their collision metadata therefore also starts at zero.
A fresh ordinary actor’s collision metadata byte starts at zero. When its initial model uses the static load path, the loader copies the collision byte from the matching named ROB segment when that segment exists. The MPOB static-model fallback performs the same copy. Loading geometry and retaining this per-actor collision metadata are separate operations; a standalone model resource does not itself supply the ROB byte. Live static face candidates retain the actor’s stored byte as support metadata.
The nonplayer death-animation paths pair a forced request for group 122 with stored death default 124, and a forced request for group 121 with stored death default 123. The requested death animation and the subsequent ordinary default are separate values; the default selector reads the stored combat-state value. Storing the combat death default first releases the actor’s held walk-node reservations. Each node’s ownership byte is cleared only when it matches the actor identifier’s low byte; both actor-side reservation indices are reset.
Forced requests
A forced request for the current group, before its exit marker is set, clears the pending request without restarting the cursor, countdown or finite loop count. Otherwise it clears the current group, replaces the pending group, and resets cursor, direction, delays, markers and finite loop state. A missing group falls back to the existing default, using the same byte speed-modifier addition as an ordinary request. With an accepted group it immediately advances using the existing shared nominal ticks. Ordinary numeric pending priority does not apply to a forced request.
These rules do not make an ineligible actor eligible. The request and advance boundaries reject globally disabled animation, an absent animation state, and actors using the static model shortcut.
Ordinary actor update order
After the actor passes the update-list exclusions, its state-dependent default group is selected. For an actor whose animation-update exclusion is clear, the update then advances the current animation and makes an ordinary request for the selected default. The actor’s main script stage comes later in that same update, after this advancement and the intervening movement handling.
Within the enabled main script stage, the engine processes script entry work, then existing tasks. Task processing can leave the main script body eligible to continue in that update. Actor flags and global script switches can suppress these stages independently; nearby-cell membership does not bypass them.
Consequently, a script animation request can follow an animation advancement in the same actor update. If the request clears the current default or starts an actor with no current group, its immediate advancement reuses the shared tick increment. A renderer that advances each actor exactly once before or after all script execution loses this ordering.
Ordinary collision stage
The ordinary collision branch follows main-script processing and the periodic
actor update. In its admitted nonplayer path, a nonzero collision attribute byte
1 runs the support query before the movement and ground response selected by
bit 0x04. Body-contact collection and response follow that work. Bit 0x01
enables those contacts only for living actor state zero, state 15, or states 27
through 32 inclusive.
Bit 0x04 dispatches states 10 through 19 to a selected-player-only response
handler. For a nonplayer actor that handler returns without changing actor
state. Outside that range, states 20 and 21 run
weapon-draw admission, including its cleanup branch.
They do not also run the ordinary motion and ground response. Other states
select that ordinary response. This dispatch does not
suppress the preceding support query or subsequent eligible body contacts.
Before support, ordinary nonplayer collision runs the forward obstacle wrapper,
except for living states 10 through 19, then the overlap stage when attribute 1
has bit 0x08. Slope prevalidation precedes the primary support query. Ground
response selected by bit 0x04 precedes stable-position retention and eligible
body response. Conditional recovery and water rollback follow body response.
Before recovery the actor’s weight attribute 6 is restored from reserved weight
attribute 115.
After collision handling, support-following runs before attribute-controlled axis restoration. These stages can change the result of earlier script movement; updating the animation or local transform alone does not perform them.
Supporting actor motion
The ordinary nonplayer support-following stage uses the retained supporting actor. A missing actor owner or a supporting actor with attribute 24 zero leaves the rider unchanged. Static-placement ownership alone does not carry a rider.
For an unattached support the stage reads its current local position and update-entry local position. The latter is separate from its stable grounded position. A support whose linked-position record names an owner supplies the record’s cached position and its position before the latest refresh instead; angles always come from the support’s own local angles and angle history. It first adds the support’s position delta to the rider’s local position and adds the support’s heading delta to the rider’s local heading, wrapping heading to 2048 units.
It then subtracts the support’s current position from the translated rider. It builds an identity matrix rotated in yaw, pitch, roll order using the negative previous support angles, and applies the column transform to that offset. It builds another matrix with the negative current support angles and applies the row transform. Both transforms store input coordinates as single precision and convert each result to an integer with nearest-even rounding. The final rider position is the resulting offset plus the current support position. Pitch and roll affect position but do not change the rider’s pitch or roll.
An attached support uses current and previous effective positions and angles
(each family refresh stores the effective values it replaces as the previous ones);
this path does nothing during the first two family generations. Living states
10 through 19 follow the ledge-support actor
instead of the retained ground support, with the same attribute 24 and
attachment rules. The retained ledge point receives the support’s translation
and the same rotation about the support, and the ledge heading receives the
support’s heading delta, wrapped to 2048 units, in the same pass as the rider.
The selected player in those states is not subject to the destination
collision queries below. Otherwise, for the selected player with collision
attribute bit 0x01 set, the support’s translation and heading delta are
applied first and the candidate models are gathered again at that translated
position. The rotated destination is then rejected when the player’s body
spheres, placed there with the current angles, meet a candidate (candidate type
0x80 is skipped while queued motion is 24), or when the segment from its
update-entry position to the destination meets a candidate. Polygon sidedness is
off for both tests. A rejected destination leaves the translated position and the
turned heading in place; an accepted one stores the rotated position. These
cases are distinct from the ordinary unattached nonplayer path.
Ordinary actor eligibility
The ordinary actor update traverses the actor update list. It skips the actor
body for the unselected member of the two designated player actors, a matching
active script-name exclusion, a nonzero runtime object-update exclusion, or
script-control mask 0x08. This suppresses the ordinary animation stage and
later main-script stage together. Update-list membership alone does not make
an actor eligible.
The runtime animation-update exclusion is separate. A nonzero value skips the ordinary animation stage, including current-group advancement and its default request, without itself skipping the later main-script stage. Direct animation requests retain their own boundary checks. At each ordinary update, the engine clears this exclusion on the selected player actor and sets it on the other player actor.
SYSTEM.INI’s [debug] disable_slaves sets the object-update exclusion for
actors with attr_slave set and attr_master clear. disable_master_slaves
sets it for actors with both attributes set. These tests set the runtime
exclusion; turning off the option does not clear an already-set exclusion in
this update branch.
Timing boundary
The normal world loop calculates the shared FPS-derived timing before sampling gameplay input and building the view-centered actor lists. Ordinary actor traversal follows map-list presentation and the rope and shell systems. It runs before the scene-rendering call. Timing is calculated for the loop, not separately for each actor in that traversal.
Animation consumes the shared whole nominal-tick increment. The normal timing path accumulates eight-fractional-bit frame scales and takes the difference between consecutive whole accumulated values. It can therefore supply zero, one, or several ticks during an update. The first twelve updates after a timing reset use one tick. See player timing.
Selected blocking operations, including model resource loading, rebase the FPS measurement baseline so that suspended time does not enter the next update. The player timing section documents the save, restore, reset, and startup rules.
Shipped FLAG2 sequence
The shipped island FLAG2 program contains finite PushAnimation requests.
It requests groups/counts (1, 25), (10, 1), (1, 3), (10, 2) and then
ends the invocation with target zero. A later eligible script stage can restart
that authored sequence. Replacing it with a single endless idle group loses
the group changes and task boundaries. See SOUP End.
Forward frame countdown
For a newly selected group, the command cursor starts at zero and the byte countdown starts at the group’s low header byte plus the request’s speed modifier. With zero input ticks, neither cursor nor countdown advances, but control commands are still interpreted until a frame is selected.
With nonzero input ticks and a zero countdown, the cursor advances one command, the countdown reloads, and the remaining tick count minus one becomes a frame skip count. The countdown then decrements once, with byte wrapping. With a nonzero countdown, it only decrements once and the current frame remains selected. Thus the countdown and multi-tick frame skipping are separate inputs.
Control commands do not consume that frame skip count. Each encountered
ShowFrame consumes one skip while a skip remains. The interpreter advances
past those frames and selects the next one when no skips remain. It does not
interpolate vertex positions between model frames. These rules describe a
forward current group; reverse playback, pending-group eligibility, delays,
and event dispatch have additional state.
Reverse frame countdown
A reverse-playback byte equal to one changes the ordinary command step to minus
one. Frame-countdown expiration decrements the signed command cursor and the
wrapping frame-progress byte. Skipped ShowFrame commands decrement both again.
A nonzero countdown only decrements once per nonzero-tick update, as in forward
playback; zero ticks leave the countdown and cursor unchanged.
Before each reverse command is decoded, a cursor less than one ends the current group. Command zero is therefore not displayed by reverse traversal. Playback clears the current group, stops its remembered sound if present, and invokes ordinary advancement again with the same tick input. With no pending group, that advancement requests the default group.
Delay processing still precedes reverse advancement. An unexpired or indefinite delay yields without decrementing the command cursor. An expired delay clears and lets reverse advancement use the call’s complete tick input.
Reverse control commands
With no finite loop countdown, reverse GoToPrevious and GoToFuture decrement
the command cursor rather than taking their targets. A nonzero finite countdown
changes this behavior. GoToPrevious decrements it; reaching zero advances the
cursor by one, while a remaining count takes the authored target. GoToFuture
with a nonzero finite countdown advances the cursor by one without changing the
countdown.
Each reverse command iteration clears the exit marker, movement-suppression
marker, and retained translation and rotation deltas before testing the cursor
boundary. BreakPoint sets movement suppression and steps backward, so the next
iteration clears that marker again. Translation, rotation, group-change,
loop-marker and native commands step backward without applying their forward
operations. Loop-marker commands leave their retained loop counter unchanged.
Reverse DelayCounter stores the narrowed byte delay and decrements the cursor.
A nonzero delay yields immediately; zero continues command processing.
EndAnimation still ends the group and invokes ordinary advancement.
PlaySound dispatches the ordinary animation sound services and retains the
returned sound slot before decrementing the cursor. Opcode eleven still invokes
the actor-event service with the authored operand and decrements the cursor.
Actor events
Opcode eleven dispatches its operand to the actor-event service at the command, in forward and reverse playback alike, and then moves the cursor one step in the playback direction. The service runs synchronously, in command order with sound commands, and never selects a frame.
The operand is a bit set. The service tests its bits in this order against the actor’s stored sound surface:
| Bit | Required surface | Effect descriptor |
|---|---|---|
0x01 | lava (5) | 0x2580 |
0x02 | water (1) | 0x2500 |
0x04 | water (1) | 0x2500 |
0x08 | sand (6) | 0x2700 |
A set bit whose surface does not match does nothing, and bits 0x02 and 0x04
together request two effects. Each effect starts at the actor’s current logical
Q8 position, displaced 2048 Q8 units toward the camera, with the vertical point
logical Y plus the actor’s positive vertical extent times 256 plus 1536. This is
the point the landing effect uses, with the camera-relative displacement of the
selected death effect admission rules.
Bit 0x10 then adds one screen-shake impulse.
No other bit has an effect.
Action words
Each actor has unsigned 32-bit pending and accepted action words and an unsigned byte timer. The directional-state bytes used by jump-key arbitration, walk-off recovery and surface assignment are the low bytes of these same words.
SetAction(mask) acts only for mask 1 or 2, and only when neither word already contains
the requested bit. A current group from 65 through 74, nonzero queued motion, or nonzero
falling motion clears the low two bits of both words and ends the call. Sliding motion is
not tested at this stage.
Otherwise, the step-hold condition is true when attribute 11 is zero or the motion byte’s step field, bits 4 and 5, equals three. With that condition true, a current group of 18 or 30 and a clear animation exit marker sets both low bits of the pending word and clears the timer. Other admitted states invoke script Jump. The checks for groups 67, 68, 69 and 70 occur after the 65-through-74 rejection and cannot be reached. The handler has no defined function return value.
The action update runs after the actor’s script stage and before ordinary movement. It adds elapsed nominal ticks to the timer with unsigned byte wrapping. A resulting timer strictly greater than 36 ORs the entire pending word into the accepted word, clears the pending word, and resets the timer. Nonzero queued motion, or a current group other than 18 or 30, clears the low two bits of both words. Higher bits survive this clearing.
Accepted bit zero clears the low two bits of both words before requesting action entry 8. Only if accepted bit one remains set does it perform the same clearing before requesting entry 9. Thus bit zero wins when both bits are set, including when admission refuses. The ordinary jump entry owns these requests. Group 18 maps entry 8 to queued motion 8 and group 34, and entry 9 to queued motion 9 and group 36. Group 30 instead requests groups 35 and 37. Other groups select ordinary jump motion 1 with group 32, or group 33 with a burning torch. A nonzero runtime jump selector overrides these rows with queued motion 24 and forced group 85. Non-forced requests use reverse zero, and successful initialization sets motion direction to two.
ResetAction(mask) clears every bit selected by its 32-bit mask in both words.
ResetAllAction clears both words. Neither reset changes the timer. These handlers have no
defined function return value. The words are independent of the selected player’s controller
pending-action flag.
Transition command
Opcode fifteen carries a trigger mask, a start frame and a target group. Reverse playback only moves the cursor one command back. Forward playback asks a decision routine, keyed on the mask, whether to take the transition:
| Mask | Condition for taking the transition |
|---|---|
| 1 and 2 | Either of the two low bits of the actor’s pending action word, the word SetAction fills, is set. Taking mask 1 clears bit 1 of the pending and accepted action words, and taking mask 2 clears bit 0 of both; the accepted word then gains the pending word’s remaining bits. |
| 4 | The actor’s falling byte (the motion byte at +0x1b2, which every actor has and the ground turn rules read) is nonzero. |
| 8 | The actor is the selected player and the forward movement binding (key_up, either of its two codes) is pressed. |
| any other | Never taken. |
A command that is not taken advances the cursor by one. A missing actor or animation record is never taken either.
Taking mask 8 sets byte +0x0a of the animation record to one, then makes a group
request on the actor through the shared group-force routine: group 31 for a selected
player holding a weapon, otherwise group 30 with a burning torch or group 18 without
one. Each path sets bits 0x30 of the actor’s combat-state byte +0x1cc and runs one
further update routine for the actor before the decision reports success. That routine is
a ground step with a step budget of one: the ordinary forward step on the unarmed path
(described next) and the walking step on the weapon path. The walking step skips itself in
deep water, where the ordinary forward step runs instead; otherwise it checks the forward
step, forces group 31 when the actor holds a weapon (group 21 with a burning torch, 20
without), and moves by the model record’s walking step byte (+0x13 armed, +0xc otherwise)
under the same ramp. Masks 1 and 2
report a distinct success value from mask 8 and mask 4.
The ordinary forward step does nothing while the 32-bit value at +0x1ab or any of the bytes
at +0x1b1, +0x1b2 and +0x1b3 is nonzero. Otherwise it forces group 18 (group 30 when
byte +0x1fe is nonzero) when that group exists, and moves the actor along its heading by
the step byte of its model record (+8, shifted left eight bits and halved while the
terrain-state byte +0x1dc is 2 or 4) times the movement scale. Bits 4–5 of +0x1cc hold a
ramp counter. While it is below three, the step is first scaled by a factor chosen by the
model record’s ramp byte (+0xb) and the counter, and the counter then increments:
| Ramp byte | Counter 0 | Counter 1 | Counter 2 |
|---|---|---|---|
| 0 | 1 | 1 | 1 |
| 1 | ¼ | ⅓ | ½ |
| 2 | 0 | ⅓ | ⅔ |
| 3 | 0 | 0 | ½ |
The decision result selects the command’s outcome. Not taken advances the cursor by one.
A mask 8 transition leaves the cursor on the command and ends the update with the group
request already made. A mask 1 or 2 transition sets the cursor to the command’s second
operand less two (the start frame field) and, like a taken GoToFuture, sets the exit marker and clears
the two six-byte delta records. A mask 4 transition does the same with the third operand,
as described next.
Taking mask 4 jumps the cursor to the command’s third operand (target_group, a
command index below 128) and continues interpreting there, exactly as a taken
GoToFuture does: it sets the exit marker, clears the movement-suppression
marker and clears the two six-byte delta records. The decision for mask 4 has no
other effect.
Forward type-one transitions
A forward type-one group uses the ordinary frame countdown and control commands.
With no finite loop countdown, a pending group different from the current group
makes GoToFuture take its exit target. The jump sets the exit marker and clears
the movement-suppression marker before interpreting the target commands.
A pending default does not immediately replace a type-one group, even after the
exit marker is set. Playback continues through the exit tail until
EndAnimation, which selects the pending default in the same advancement with
the same nominal ticks. With a pending non-default group, an already-set exit
marker instead permits replacement at the beginning of a subsequent advance,
before its ordinary countdown update. The pending group number may equal the
current number. The ordinary request priority rules still determine which
request becomes pending.
Delay processing precedes that replacement test. An unexpired finite delay or an indefinite delay yields before replacement eligibility is checked. When a finite delay expires, replacement receives the call’s complete tick input. Selecting the replacement resets its cursor, countdown, loop state and markers.
Zero-tick updates retain the currently selected frame. The walk group’s PlaySound command in the exit tail does not end the group or consume a frame skip. See animation sound requests.
Forward type-two groups
A forward type-two group (ledge-hang loops, and the player’s and pirates’ idle and movement groups) uses the same exit-marker replacement rule as type one: once the exit marker is set, a pending group that exists and differs from the actor’s default group replaces the current group at the start of the next advancement.
GoToFuture differs. With no finite loop countdown and either the exit marker set or a
pending group differing from the current one, a type-two group whose pending group is
the actor’s default group does not take the exit target: the cursor advances by one
command, the exit marker is not set, and playback continues past the jump. Any other
pending group takes the exit target as in type one.
Forward animation delays
DelayCounter, opcode twelve, narrows its operand to a byte, advances the
command cursor, and yields without selecting a new model frame when that byte
is nonzero. Zero continues interpreting commands in the same invocation.
The previous displayed pose remains selected during the wait.
At the start of subsequent advancements, byte value 255 yields indefinitely. For another nonzero value, the engine subtracts the shared whole nominal ticks with wrapping 32-bit arithmetic and tests the signed result. A positive result is narrowed back to the delay byte and yields. Otherwise the delay clears and ordinary advancement continues in that same call, using the original tick input in full. The delay does not consume a portion of the frame-skip input. A zero-tick advancement therefore preserves a finite delay.
Waiting neither ends the current group nor completes a task waiting for a group
change. Selecting a replacement group clears its delay state. The cursor has
already moved past the delay command; the usual countdown and frame-skip rules
still apply when the delay expires. ConditionalDelay and LoopControl use
separate state from DelayCounter.
Translation, rotation and repeat commands
Opcodes 6 and 7 store a translation and opcodes 8 and 9 a rotation in the animation record. The
record keeps three 16-bit position words and three 16-bit rotation words. Opcode 6 stores its three
operands shifted left by eight in the position words; opcode 8 stores its three operands shifted
left by three in the rotation words. Opcodes 7 and 9 store one unshifted value in the word their
axis operand selects: zero the first, one the second and any other value the third. Each command
then moves the cursor on. Every cursor step that clears the BreakPoint marker also zeroes all six
words, so they hold their values only until the next frame is selected.
Opcode 13 stores its operand, narrowed to a byte, in the record’s repeat byte and moves on. Opcode 14 moves on when the repeat byte is zero; otherwise it decrements the byte and sets the cursor to its operand. A group that stores N and then reaches the loop command N times therefore runs the block between them N more times. Selecting a group clears the repeat byte. In reverse playback these commands only step the cursor back. The shipped game contains none of them.
Opcode 10 changes the group in the middle of playback. Its operands name a group and a command
index. When that group has no command table, or no command at that index, the engine reports a
fatal animation error. Otherwise the record’s current-group word becomes the group and the cursor
the index, the command table in use becomes the new group’s, and interpretation continues in the same
call. Every other byte of the record keeps its value: the countdown and its reload byte, the loop and
repeat bytes, the wait byte, the frame-progress byte and the exit and BreakPoint markers. In reverse
playback the command only steps the cursor back.
Breakpoints and walking translation
BreakPoint, opcode five, sets a marker in the current animation state and
continues interpreting commands. It does not pause the game or the animation.
Ordinary forward walking checks this marker after advancing its startup phase;
a set marker suppresses that update’s translation.
Queued and falling vertical writers read the same marker after their preceding
animation request. A set marker suppresses their vertical arithmetic and timer
updates. Static models and absent animation state do not apply this suppression.
For forward playback, advancing the command cursor when its frame countdown
expires clears the marker. Skipping a ShowFrame, taking a GoToPrevious
backward jump, or taking a GoToFuture exit jump also clears it. A zero-tick
update or a countdown decrement that keeps the same cursor does not clear it.
A later BreakPoint can set it again before the selected frame is returned.
Selecting a new group resets the marker before interpreting its commands.
Ordinary collision rollback snapshot
After ordinary animation advancement, the update saves the current animation receiver and the two selected model-frame words, together with the directional movement flags. Late collision rollback restores those saved values. The receiver snapshot includes the current group, pending request, command cursor, countdowns, loop state, exit and motion markers, and remembered animation sample slot. The separately stored default animation selection is not restored.
Restoring a remembered sample slot does not stop channels started after the snapshot or restart a channel that has already stopped. A later animation stop uses the restored reusable slot, including a replacement sound occupying it.
Initialization with the animation system off
The request routines return before touching the animation record while the animation switch is zero. A placed animation-table model initialized in that state therefore keeps the record built from the group tables with no current or pending group and no selected model frame. The per-update model refresh draws an animation-table actor only when it has a selected frame, so such an actor is not drawn; scripts and the other stages still run. Turning the switch on later lets the ordinary default request select a group and frame.
Standalone player construction also obeys the animation switch. With the switch zero, the initial requests leave the player animation record without a current group or selected frame; controller and combat state construction still takes place.
Group type byte
The group header’s type byte is tested only for the values one and two, by the advance routine’s
replacement and GoToFuture rules above. Every other value, including values above two, plays as
type zero. Only the low byte of the header’s type field is read.
Animation group classes
The engine keeps a table of one class value per animation group number (0 through 177), consulted by combat and movement code to decide whether a group is a fighting group or a reaction group. Groups not listed have class zero.
| Class | Meaning | Groups |
|---|---|---|
| 1 | Fighting group | 8, 31, 91 through 120, 129 through 133, 135 through 140, 175, 176, 177 |
| 2 | Hit-reaction or death group | 74, 121 through 128, 134 |
Player movement
Player movement combines held input, actor attributes, animation state, and a shared frame scale.
Coordinate and heading units
Runtime positions have eight fractional bits. A marker coordinate of 40816
becomes a runtime coordinate of 10448896. The developer console’s object coord
command displays both representations. Its start position retains the spawn
position while the current position changes with movement and ground contact.
Headings wrap after 2048 units per revolution. For ordinary ground movement with the sword sheathed, forward at heading zero increases Z. Right turning decreases heading; left turning increases it. Combat and special movement states use different rules.
Marker-based world entry
A marker-based world load selects the requested map marker and uses its position
for the initial player position. The marker coordinates are multiplied by 256
for runtime storage. The requested heading is wrapped modulo 2048; the marker’s
auxiliary byte does not supply a heading. The player actor uses the map’s CYRUS
actor definition, including its attributes and animation data.
The two standalone player actors use the shared actor allocator. Their motion state, rope attachment, torch-burning counter and weapon-drawn flag start at zero. Model and animation setup are separate from this allocation state.
Initial placement and ground settling are separate. Ground settling can change the player’s Y coordinate, so the marker’s Y coordinate is not a permanent ground height.
Startup ground settling
Startup ground settling is separate from the ordinary grounded movement update. It prepares the spatial window around the actor X/Z, using the static collision list order. It queries nearby model candidates with radius 1000 map units, excluding the actor’s current model. The candidate list is the one the ordinary collision queries build, so it holds more than the static placements: after them come the models of the ordinary actors that have a model, are not the excluded player role, are not excluded from object updates, have attribute 55 equal to zero and pass the model-distance test, and only an actor whose model kind byte is one contributes to the face-model list the contacts are taken from. An actor-owned deck, such as a ship, therefore competes with the terrain. The vertical segment keeps the prepared actor X/Z and extends 500 map units in each direction from the prepared actor Y plus its positive Y extent.
When terrain is enabled, its sampled height is truncated toward zero to an
integer map height. The terrain query does not test water cells: a water cell
supplies the same interpolated height as a dry cell. Terrain is eligible only strictly between the segment
endpoints. Its rank is abs(16384 - ((abs(height - start_y) * 8388) >> 8)).
The initial rank without eligible terrain is 65536.
Model contacts are visited in candidate and contact order. Only surface
classifications 2 and 4 participate. Their rank is
abs(16384 - contact_rank). A model contact replaces the current selection
only when its rank is strictly smaller. Equal ranks retain the earlier
selection, including terrain when it was selected first. This differs from
ordinary support selection’s later-equal replacement.
If a support is selected, the actor Y becomes
(selected_height - positive_y_extent + standing_height) * 256, where
standing_height is the configured map-unit value from SYSTEM.INI.
Without a selection, the actor Y is unchanged. This pass does not translate X/Z.
Ordinary idle animation default
For the ordinary grounded player at rest with the weapon sheathed and no special
action override, default animation selection reads attr_panic_group from the
actor’s attributes. This is a group number, not a boolean or a hardcoded request
for group zero. The installed ISLAND CYRUS definition supplies zero.
This selection precedes the ordinary animation advancement and default request. Combat, rope and special-action states use separate selection branches.
Upright actor orientation
Actor orientation stores pitch, heading and roll modulo 2048. Assigning an orientation rebuilds the actor matrix from identity, applying negative heading, negative pitch and negative roll in that order. The rotations use the floating point shared rotation table.
For zero pitch and roll, let s be the table sample at negative heading modulo
2048, and c the sample 512 entries later. The matrix rows are (c, 0, s),
(0, 1, 0) and (-s, 0, c). It maps local forward onto (s, 0, c) in original
world axes. Use the stored table samples even at quarter and half turns;
replacing their small residual values with exact mathematical zeros changes
integer collision conversion at some distances.
Incremental orientation refresh
Assigning an orientation and refreshing an existing actor matrix are distinct
operations. In the ordinary unattached refresh branch, changed angles with
attr_rotation = 0 rotate the existing matrix by the negated differences between
current and previous heading, pitch and roll, in that order. Each difference is
wrapped modulo 2048 for the shared rotation-table lookup. Unchanged angles leave
the matrix intact. The stored matrix therefore retains its accumulated
single-precision rotation history; it is not reconstructed from the current
heading on every refresh.
Each axis operation rounds its products to single precision before adding or
subtracting them, then rounds the resulting component to single precision.
These rounding points apply to the ordinary unattached incremental refresh;
coordinate transforms and collision-matrix construction are separate operations.
The ordinary actor update copies current pitch, heading and roll into their
previous-angle fields before selecting the default animation and running the
ordinary animation block. Movement later in that update can change the current
angles. The admitted incremental refresh does not advance this snapshot.
In the normal main-loop order, map-list presentation, which can refresh actor
matrices, precedes the ordinary actor update. The ordinary update restores
position and angle axis locks after support following, without an unconditional
refresh of an unattached actor matrix. A heading changed during ordinary
nonplayer movement therefore remains separate from its stored matrix until the
next presentation refresh, unless that operation explicitly assigns an
orientation. Explicit orientation assignment and attached-actor composition
remain separate operations.
In that branch, attr_rotation = 2 instead rebuilds from identity using the
negated current angles. A selected player with nonzero roll takes a separate
rebuild branch. These rules do not replace attached-actor matrix composition.
Ground-turn arithmetic
Left and right input have independent turn accumulators. Releasing one direction
resets its accumulator to zero. For ordinary movement, a held direction adds
SYSTEM.INI turn_speed while its accumulator is below turn_max_speed.
The addition is not clamped, so settings whose increment does not divide the
limit can overshoot it. The installed values 12 and 48 produce held-input
accumulators 12, 24, 36, 48, 48 on consecutive input updates.
Each direction is multiplied by the current frame scale and shifted right by eight bits separately. Ordinary ground turning adds the scaled left amount and subtracts the scaled right amount, then wraps the heading modulo 2048. At scale 256, starting from heading zero, five right-input updates produce headings 2036, 2012, 1976, 1928, 1880. These are arithmetic examples, not a claim that input updates occur at a fixed wall-clock frequency.
Animation and player state control whether this heading update is allowed. The arithmetic alone is insufficient to implement the player state machine. Each input update recomputes both accumulators. A direction that is not held resets to zero; with no selected player a held direction is one. Otherwise the accumulator is incremented by the ordinary settings, or by the selected player’s fight-turn attributes when either of these holds:
- The player holds a weapon (attribute 110 is nonzero, or the item system is ready and the player has an assigned, drawn, not sheathing weapon) and its current animation group is neither 18 nor 31.
- Its
hand_itemis 51.
The fight-turn increment is attr_fight_turn_accel (attribute 22) and the limit
is attr_fight_turn_speed (attribute 21): while the accumulator is below the
limit the increment is added, with the same overshoot as the ordinary settings.
CYRUS carries accel 12 and speed 64, against the ordinary 12 and 48; the
attr_turn_speed attribute (15, value 32) is not read by this update.
Ground-turn eligibility and animation
Keyboard turning runs inside the selected player’s controller, after the view, lockout, dialogue and topic-menu returns and before forward and backward dispatch, so movement in the same update uses the turned heading. The controller also returns before turning while the current animation group is 129 or 91 and the item system is ready and not paused. The rules below cover a sheathed, non-swimming player that holds no item. While the player holds item 51 the accumulators use the fight-turn attributes, the ordinary turn animation is suppressed and the keyboard movement dispatch after turning is skipped.
Without the walk key held, turning applies when the living state is 0 or 27 and
the actor’s motion bytes at +0x1b2 and +0x1b3 (two of the sliding, falling
and queued-motion bytes) are both zero. The heading becomes the previous heading
minus the scaled right amount plus the scaled left amount, modulo 2048. A
nonzero net amount then makes an ordinary animation request with modifier zero:
| Net turn | Torch not burning | Torch burning |
|---|---|---|
| Left | Group 14 | Group 16 |
| Right | Group 15 | Group 17 |
No request is made while the weapon-holding predicate is true or the actor
stands on deep-water surface 2 or 4. A nonzero attr_holding_weapon makes the
weapon-holding predicate true before its ordinary gates. Equal left and right
amounts request nothing.
With the walk key held, turning applies only while forward or backward is held,
the motion bytes at +0x1b2 and +0x1b3 are zero and the living state is outside 10 through
19. It requests no animation and does not bank.
These rules hold with the installed [cyrus] mouse_turn=0. A nonzero value adds a
mouse-driven turn block that plays the same four turn groups from the sign of
the horizontal mouse movement.
After turning and before forward and backward dispatch, a sidestep block runs
when the walk key is held or the living state is 15. With neither forward nor
backward held, left alone calls MoveLeft and right alone calls MoveRight
with duration 1, using the same stepper as the
script natives; the controller ignores their
result. Holding forward or backward skips the sidestep.
The third actor angle is a roll that banks while running. It changes only in these rules, all modulo 2048:
- When the current group is 18 or 30 and turning applies without the walk key, a left turn adds 4 (8 while roll exceeds 1024) when roll is below 32 or above 1024; a right turn subtracts 4 (8 while roll is below 1024) when roll is above 2016 or below 1024. Other rolls are unchanged. With no net turn the roll relaxes.
- When the current group is neither 18 nor 30, the roll relaxes once per update, and twice when turning applies without the walk key and the net turn is zero.
- Relaxing: above 1024 the roll becomes 0 when it exceeds 2040 and otherwise gains 8; from 1 through 1024 it becomes 0 below 8 and otherwise loses 8; zero stays zero.
- Any other update in groups 18 and 30 leaves the roll unchanged.
Movement data and timing
Forward running and walking use the actor’s attr_forward_speed and
attr_forward_walk. They are actor data, not universal controller constants.
Startup movement also depends on attr_step_type and animation progress.
The frame scale uses eight fractional bits, with 256 representing one nominal update. The first twelve updates after a timing reset use 256. Subsequent timing can use an FPS-derived scale with the configured normal, minimum and maximum frame rates and smoothing settings. A nominal frame rate of 12 does not mean the game runs a fixed 12 Hz simulation.
Heading-based translation
Horizontal translation uses the floating-point shared rotation table, not the separate integer sine table. Negate the actor heading and mask it to 2047 to select the sine sample; cosine uses the sample 512 entries later. Multiply the integer movement distance by each sample and add it to the current X or Z position converted to single precision. Store the sum at single precision, then truncate it to the runtime integer coordinate. Rounding the sum before integer conversion is observable at ordinary world coordinates.
For heading 2036, distance 680, and initial X/Z (10446802, 10818615), the result
is (10446827, 10819295). Keeping the sum at double precision until truncation
would give a different Z result.
Direction recovered from displacement
Collision response derives a movement heading from the integer X/Z displacement.
Take the absolute components and repeatedly halve both, discarding remainders,
until neither exceeds 511. Axis-aligned vectors select the corresponding quarter
revolution directly. For two nonzero components, equal magnitudes select 256;
otherwise index a first-octant table with
floor(512 × smaller_component / larger_component). When X is larger, subtract
the table result from 512. Reflect this first-quadrant angle according to the
original component signs, negate it for the actor-heading convention, and mask
to 2047.
The 512 used first-octant entries are
truncate(atan(index / 512) × 65536 / 3.141592654) >> 6.
The diagonal case uses its explicit value rather than indexing an extra
endpoint. Directly rounding a continuous atan2 angle produces different
results.
Ordinary controller dispatch
The selected player’s native controller runs within the ordinary script stage. Default selection, ordinary animation advancement, and the ordinary default request precede that stage. Queued and falling motion also precede the script stage. Animation-based movement continuation and ground response follow it. Returning from the controller does not skip those later ordinary stages.
A freshly loaded executable initializes the retained LockoutPlayer word to
zero. Any nonzero retained word returns from the controller before
keyboard movement dispatch. After that check, either the dialogue-state word or
the topic-menu-state word being exactly one also returns before movement
dispatch. These controller tests are separate from the attribute assignment
performed by LockoutPlayer.
The controller combat predicate is false when the actor has no combat record.
With a combat record, it is true exactly when combat flags mask 0x04 is set.
This is the same flag set by
beginCombat, rather than a separate controller
assist field. Combat-record allocation and reinitialization explicitly clear
the flags byte before the remaining combat setup.
For an ordinary grounded player with the sword sheathed, combat flag 0x04
clear or no combat record, no rope attachment, and no jump or view input, the
held walk/run key inverts the retained walk-mode choice:
| Walk mode | Walk/run key held | Forward helper |
|---|---|---|
| Zero | No | Run |
| Zero | Yes | Walk |
| Nonzero | No | Walk |
| Nonzero | Yes | Run |
Walk mode comes from [CYRUS] WALK_MODE in SYSTEM.INI; Tab toggles it.
Forward input takes priority over backward input when both are held. With no
forward input, backward input takes its separate dispatch branch.
The view key returns before this ordinary non-combat movement dispatch.
The run helper is the script native MoveForward and the walk helper is
WalkForward, each called with duration 1 and an ignored result. Both use the
shared step counter, animation request and translation-suppression rules of
MoveForward and WalkForward.
With no forward input and backward input held, the same walk rule selects WalkBackward or
MoveBackward, each called with duration 1: the walk helper when the walk mode and the held
walk/run key differ and the run helper otherwise. In a lifecycle state of 0x14, 0x15, 0x21 or 0x22
forward and backward dispatch is unchanged, while left and right turning does not run.
A player that holds a weapon (the weapon-holding predicate) or has combat flag 0x04 set
ignores the retained walk mode in both forward and backward dispatch: the held walk/run key
alone selects the walk helper, and its absence the run helper. Without the walk/run key, a held
jump key starts the jump entry in place of the helper for a player that holds a weapon only while the
action key is not held, and for any other player only while the current group is not a running
group (forward) or one of the walking groups 19 and 23 (backward).
The helper selection does not bypass the helper’s motion, support, animation, or movement-marker tests. Water states, drawn weapons, rope motion, jumping, and special living states have separate dispatch branches.
Armed attack input
The input sampler keeps a tap latch per key (up, left, right, the walk key, jump, look and action).
Once per world-loop iteration a latch whose key is not held counts frames since release and clears
its press count when that timer exceeds the tap window; a latch whose key is held and was not held
the previous iteration counts a press: the count rises when the release timer is above zero and
below the window, and restarts at zero otherwise, and the timer restarts either way. The window is
[cyrus] tap_time shifted left eight bits and divided by the frame scale (signed), with a floor
of six. A latch query names a key, a count and whether to consume: it matches when the press count
equals the count, and a consuming match clears count, timer and held flag.
After movement dispatch, and only while the jump key is not held, a player holding a weapon runs the action-key handler. It does nothing in lifecycle states 20 and 21, without a combat record, without the action key, or unless the combat action selector is -1, 0 or 1. While the queued byte is nonzero or the falling byte is one, it records a pending jump attack, which starts the jump attack once falling is one; otherwise the record clears. With the first directional-state byte’s low two bits set it only consumes the action latch. Otherwise it selects one move:
| Condition (in order) | Move |
|---|---|
| Up key latch consumed at count 1 (a double tap) | Lunge, move 4 |
| Up key held | 3 |
| Action latch consumed at count 2 | 2 |
| Action latch matches count 1 | 1 |
| Action latch matches count 0 | 0 |
With none of these the action latch is consumed. A move starts only when its authored attack slot is populated and the pending move word allows it: moves 0 and 3 need it to be -1, move 1 needs 0 and move 2 needs 1. Starting writes the move to the action selector and the pending move word and runs the executor with request 0: the move number selects the attack slot, the executor starts that slot’s first motion (animation group, motion selector zero), sets combat intent bit one, sets the attack state to one and lowers aggression by ten. A hit on a target then follows the ordinary attack-processing rules for the current slot and motion. The jump attack is move 6 started once the falling byte is one: it needs its authored slot and an empty pending word, and consumes the action latch.
The lunge (move 4) first moves the player: when its lifecycle byte is zero and no rope, sliding, falling
or queued motion is set, it takes one MoveForward step of attribute byte 16
(whole units times 256) along the local Y heading, with the same forward step check below startup
counter three, the same startup ramp and frame scaling, but no animation request. After admission and
the startup step check pass, the step stores movement classification 2 in last_direction before
marking forward movement called or advancing the startup counter. Earlier refusal preserves the
previous classification; translation suppression does not undo the store. The move also needs
its authored slot, a zero cooldown and no drawing or sheathing state before this step is taken.
Before any of these moves (0 to 4 and 6) starts, a reach test runs over the ordinary actor list, with
the move number selecting the attack slot and the slot’s first motion supplying arc, arc_center,
min_range and max_range. An actor is in reach when its distance from the player (the rounded
three-dimensional distance between query positions) is strictly between min_range and max_range and
the absolute wrapped difference between the player’s heading and the heading toward the actor is strictly
within arc_center + 512 plus or minus arc. The receiver itself is not excluded from the list. The start
proceeds when either:
- an actor with a combat record is in reach and is either attackable (attribute 97 zero) or in lifecycle selector 6; or
- no actor is protected: a protected actor is in reach, is not in lifecycle selector 6 or 8 and has
attribute 97 nonzero. For the forward move (3) the arc half-width used for this protected-actor test is
six times
arc, evaluated in single precision.
Otherwise the move does not start and the player requests an opponent-unarmed taunt: type 11 when the protected actor’s attribute 118 is nonzero, else type 10. Those taunt types use ordered selection like begin-combat taunts, and are not requested while the retained taunt cache entry still remembers a sample.
Armed look-key handling
The look key leaves the movement dispatch, but the armed handler described above still runs, ahead of the attack keys and only while the jump key is not held. In order:
- With both the look and action keys held, a timer adds the nominal ticks of the iteration. Once it is
greater than
[cyrus] sheath_sword_delay(6 when the configured value is zero) the handler runsSheathSwordfor the player. The iteration ends either way. Any other state of the two keys clears the timer. - A drawing or sheathing sword ends the iteration.
- With the action key held the attack selection above follows.
- With the action key released, a counter that adds the nominal ticks of every iteration is compared with
COMBAT.INI
[misc] cyrus_defend_interval. When the look key is held and the counter is greater, the defence stance starts (below) with[cyrus] auto_defendoff, or only when an automatic defence is found with it on. A started stance sets the low two bits of the combat flags to 2 and clears the counter. - A jump attack still pending while the falling byte is one then starts as in the attack selection, and the action latch is consumed.
The defence stance does nothing unless no move is selected. It sets the pending defence index to zero, or to the counter-attack reference of the first attacker when that attacker is in the player’s forward arc (strictly under 256 heading units) and its attack state is one. An attacker is an actor in the ordinary list other than the player with a combat record whose target is the player, whose attacker-selection state is one and that is not dead. The stance then selects move nine and runs the defence executor, which requests the selected defence’s animation group, sets attack state two and lowers aggression by ten.
The automatic defence visits the attackers in list order. An attacker qualifies when it is in the forward
arc, has combat flag 0x01 and attack state one, and the strike window of its current motion lies inside the
active window of the player’s defence record for that motion’s counter-attack reference. With
gap_near = first_collide_frame - cursor and gap_far = first_defend_frame - cursor (the cursor is the
attacker’s animation command cursor) and the defence’s first_collide_frame as start and
first_collide_frame + collide_duration as end, the window is inside when start < gap_far or gap_far <= 0,
and end > gap_far, and start < gap_near, and end > gap_near. The first qualifying attacker starts the stance.
Controller initialization and preliminary state
Creating the standalone CYRUS and GREMLIN actors runs the same player
initializer for each actor, then restores CYRUS as the selected player. Each
initializer clears selected hit inhibition, the death-animation override group,
the pending action flag,
three preliminary countdowns and sound-maintenance inhibition.
These resets precede actor model/script setup and the initial animation request.
The preliminary countdowns are unsigned 32-bit values in whole nominal-tick units. One countdown advances only while both queued-motion and falling states are zero. The other two advance on every controller call. Each nonzero countdown subtracts the nominal-tick increment with saturation at zero. These three countdowns are separate from the selected-player support-history counter, which advances earlier in the ordinary actor update.
The conditional countdown is processed before the pending action flag. A pending action with sliding state zero selects action 74 and invokes its action helper: it stores group 74 as the death-animation override and applies health -255 through the death-owning selected-player health, naming the player as the source. The flag is not cleared by the helper, so each controller call with sliding state zero repeats it. The two unconditional countdowns are processed afterward, before the selected hit-inhibition test. Nonzero hit inhibition invokes the death controller and returns before idle-counter reset, lockout and ordinary keyboard dispatch.
Idle animation requests
The ordinary idle counter starts at zero in a freshly loaded executable. The controller resets it to zero while attached to a rope or while the current animation group is nonzero. This reset precedes the lockout test. With the normal BIOS clock, the clock measurement advances the counter once per accumulated 18 BIOS timer ticks, independently of the nominal animation tick increment.
After the lockout check, an unattached player with attr_health < 40 requests
group 4 when the signed idle counter is greater than 4. At health 40 or above,
a counter greater than 12 requests one of groups 1, 2, or 3. Selection draws the
shared random generator’s low two bits, retries value 3, and adds one to the
accepted value. Either request first sets the idle counter to -1.
These requests precede the dialogue and topic-menu checks. They use the normal animation request operation and do not replace the actor’s default group.
Controller under queued, falling and sliding motion
The selected player’s controller reads the queued, falling and sliding bytes in only the places below; all other branches ignore them. The rows assume living state 0, no rope attachment and a sheathed sword. Rope attachment, living states other than 0 and a held weapon take separate branches.
| Controller part | Queued or falling nonzero | Sliding nonzero, queued and falling zero |
|---|---|---|
| Preliminary resets, countdowns, hit inhibition, lockout, dialogue and topic-menu returns | Run as without motion; the conditional countdown stays frozen | Run |
| Queued value exactly 3 | Cleared when the current group equals the actor’s default group, before the lockout test | Not applicable |
| Pending action 74 | Runs when sliding is zero | Skipped |
| Idle requests, view-key return, drawing and sheathing returns | Unchanged | Unchanged |
| Falling nonzero with the jump key held | Clears the companion motion byte unless either retained rope-release flag is nonzero; queued motion alone does not | Not applicable |
| Mouse turning, keyboard turning with or without the walk key | Skipped: heading, turn animation request and banking do not run | Run |
| Roll relaxation after the turning block | Runs once, unless the current group is 18 or 30 | Same |
| Sidestep, forward and backward dispatch | Selected by the same key tests; each helper refuses, so no animation request, motion write or translation | Same refusal |
| Forward-with-jump and backward-with-jump dispatch | The jump entry refuses | Refuses |
| Jump-key arbitration | Clears the low directional-state bits | Reaches the ordinary jump entry, which refuses |
| Forward steering while queued | See below | Not applicable |
| Action key and prompt activation | No queued, falling or sliding test in the controller | Same |
Living state 0 is not required for the preliminary rows. Banking is not touched when the turning block is skipped, but the single post-block roll relaxation still runs.
With the forward key held, a queued byte whose low seven bits equal 1 and a current group of 32 or 33 becomes exactly 2, clearing its high bit. This runs after movement dispatch in the same update, also when the player holds item 51, and has no weapon or walk-key test. Queued state 2 then selects the arbitration animations of queued motion arbitration. Other directional input does not alter queued or falling motion in the controller.
Forward-walking startup
Before startup acceleration, an ordinary forward walk requests its selected
walking animation. With [system] animation_system = 1 in SYSTEM.INI, if
that request leaves a different group current, the walk returns before marking
forward movement accepted, advancing the startup phase, or translating the actor.
A pending walking group therefore does not by itself permit movement. The walk
movement classification is 6. It is assigned after successful forward-support
admission and before the walking-group lookup and request gate. The shared
forward-update indicator is set only after the animation request passes that
gate. Ordinary forward running uses movement classification 2.
These classifications are distinct from the direction and packed movement
flags assigned by directional ordinary displacement.
Both classifications select forward direction 2 and movement mask 0x30 in
that later stage; the forward-walk helper does not itself assign that mask.
This differs from the animation movement marker gate below, which suppresses translation after startup has advanced.
Ordinary forward walking keeps a startup phase from zero through three.
The selected walking speed is converted to runtime position units by multiplying
it by 256. For attr_step_type = 1, phases zero, one, and two use one quarter,
one third, and one half of that speed respectively, with integer division.
Phase three uses the full speed. A walking speed of 8 therefore gives startup
amounts 512, 682, 1024, and then 2048.
When the selected walking-speed attribute is zero, the forward-walk helper
instead uses attr_forward_speed >> 1 as its runtime-unit startup amount.
This fallback is not multiplied by 256. Startup division and frame scaling
still apply to that amount.
For ordinary sheathed, non-combat forward walking, the helper selects animation group 20. It checks that the group exists before requesting it or advancing startup. The ordinary request precedes the movement-marker test, so a request that advances animation immediately can change whether this walk translates.
An eligible walking update advances the startup phase before testing whether the current animation permits translation. Suppressed translation does not undo that phase advance. Consequently the first visible displacement need not use phase zero. Resetting the phase whenever translation is suppressed changes the startup behavior.
When animation permits translation, the startup amount is multiplied by the frame scale and shifted right by eight bits before heading-based displacement. Walking groups can use animation type 1; a player animation adapter restricted to type 0 cannot reproduce ordinary walking.
Ending a walking update
The actor update tracks whether it accepted forward movement. At the end of an update without accepted forward movement, it resets the forward startup phase to zero. This differs from suppressing translation through animation: an accepted walking update can advance startup while producing no displacement.
Continuation after input release
Accepted movement is not equivalent to a held input. The actor update can continue movement from its current animation group after the input has cleared. The run-group continuation still passes through the animation marker gate and counts as accepted forward movement when that marker suppresses translation.
For an admitted ordinary sheathed actor whose current group is 20, the update continues forward walking if earlier movement in that update did not already accept forward motion. This continuation repeats the forward support probe and uses the walking speed, startup phase, frame scale, and movement marker. It does not request the walking animation again, so the pending default-group request survives. Marker-suppressed continuation still counts as accepted forward motion and preserves the startup phase. Once the current group becomes idle, this walking continuation no longer applies; an update without other accepted forward motion resets the phase.
For ordinary type-1 walking, a pending default-group request does not immediately
replace the current group. A GoToFuture command takes its exit target when a
different pending group exists, or when exit processing is already active. The
taken jump marks exit processing active and clears the movement marker; a
BreakPoint at the target can set the marker again. A pending default remains
pending through these exit frames until EndAnimation starts it in the same
advancement call, using that call’s tick input.
Ordinary forward-walk support checks
Before an ordinary ground walk is accepted, the engine probes ahead of the
actor. The local forward probe distance is the actor’s forward extent plus
attr_forward_walk, converted to runtime position units. Actor orientation
transforms the offset; the probe retains the actor’s current Y coordinate.
The local offset has zero X and Y components. Its Z component is the sum of
forward extent and attr_forward_walk, multiplied by 256. The integer matrix
transform converts its input components to single precision, computes each
rotated component, and stores that component as single precision before
converting it back to an integer with nearest-even rounding. The transformed
integer X and Z offsets are added to the actor’s runtime X and Z coordinates;
the transformed Y offset is discarded.
The probe must pass three checks, in order:
- The actor’s collision spheres at the probe must not intersect nearby model faces.
- The surface check must not reject the probe as a steep surface.
- A ground-support query at the probe must succeed.
The body check builds the actor’s working sphere shape at the proposed position. Each Q8 position component is shifted right by eight before conversion to single precision; the collision orientation remains the actor’s current orientation. Prepared model candidates are visited in their retained order. Queued motion 24 excludes candidates of type 128. The first blocking model rejects the attempt without querying later models. Terrain does not participate in this body check, and the actor’s stored position is unchanged.
The ground-support query temporarily uses the probe position and then restores the actor’s position. It considers terrain and nearby model faces. This is not a permanent Y clamp and is not equivalent to accepting every horizontal step.
Surface classification uses the signed Y component of a normal scaled by 256:
| Normal Y | Classification |
|---|---|
| Less than −160 | Ordinary ground (4) |
| −160 through −42 | Steep ground (2) |
| −41 through 160 | Side surface (1) |
| Greater than 160 | Underside (8) |
Ground selection accepts classifications 2 and 4; the separate slope rules control whether movement onto that support is allowed. Surface classification alone does not describe slope sliding or collision response.
An unsuccessful sphere, steep-surface, or ground-support probe marks the forward attempt blocked and returns failure. Subsequent checks in that probe are skipped. The blocked-attempt marker is transient; it is not a permanent collision state.
Forward slope surface selection
The forward steep-surface check uses post_collide_height as the half-length
of its vertical segment, centered on integer actor Y plus the positive Y extent.
It is bypassed when collision attribute bit 2 is clear, or when the designated
player’s support-history counter is nonzero.
For the proposed position, face-model contacts of classification 2 or 4 compete
by abs(16384 - segment_rank). Strictly smaller values replace the current
contact, preserving the first equal candidate. The initial comparison value is
999999 and the initial model height is 9999999 map units.
Terrain participates when its height is no lower than the segment’s lower end. Its height difference from the segment center is truncated toward zero before taking the absolute value. That value must be strictly less than the absolute difference of the current model height from that center. A winning terrain sample replaces the normal and height. When the proposed position has a selected surface, classification 2 rejects the attempt and other classifications allow this slope check. This selection is separate from the ordinary ground-support rank comparison.
Current-position slope selection compares X and Z before map-unit conversion, using their representation with eight fractional bits. Equal X/Z skips that probe, regardless of Y. A difference of 1/256 map unit on either horizontal axis can therefore run a second probe even when the integer map coordinates match.
When neither the proposed-position nor the current-position slope probe selects a surface, the slope check allows the attempt to continue. The separate support query still runs and can accept the designated player’s fallback sphere contact or reject absent support. A missing slope contact does not itself reject motion.
When the proposed position has no selected slope surface but the current position does, classification reads the uninitialized proposed normal. The result is not defined. When the proposed position has a selected surface, its classification determines rejection even if the current-position probe selects no surface.
Queued and falling vertical motion
Queued and falling vertical motion skip their arithmetic when an animated model has active model-controlled vertical motion. Static models, absent animation state and animations without that control use the following rules. All positions and the retained fall origin have eight fractional bits. The retained timer is a byte; falling tests it as signed.
The vertical factor sequence at indices 0 through 29 is exactly represented by
integer division of 256 * (900 - (29 - index)^2) by 900. Indices 30 through 69
have factor 256. The sequence is constant initialized data that no code writes. The next entries belong to adjacent engine state. A queued
update reads the entry at its current timer before advancing, then reads the
entry at the advanced timer to decide whether it transfers to falling.
A falling update uses entry zero when the signed timer is nonpositive.
The additional increment is a constant zero: nothing ever writes it. The fine-tick
increment is the finer counter increment of the update.
While an animated-model actor’s falling byte is nonzero, the ordinary support query
instead uses a reduced probe distance chosen by the measured FPS that the frame scale was
derived from (twelve during the startup countdown): at 10 or more it is
(post_collide_height >> 2) * 3, below 10 it is post_collide_height.
Every other ordinary support query, including every static-model one, uses
post_collide_height.
Queued motion writes origin_Y - attribute_32 * factor[timer], then advances
the timer by the sum of the fine-tick increment and its additional increment,
wrapping as a byte. After that advance, a factor greater than 255 transfers the
queued byte to falling. A transferred high-bit motion forces animation group 58
when the torch-burning counter is zero; queued motion clears after that request.
The vertical writer retains motion classification.
Falling limits its vertical value by post_collide_height * 512 - 256, with
signed comparison. With a positive signed timer, it writes origin Y plus the
smaller of that limit and -attribute_32 * factor[timer], then subtracts the
same fine-tick increment sum from the byte timer. With a nonpositive timer,
it adds the smaller of that limit and
attribute_32 * factor[0] * (fine_tick_increment + additional_increment + 2)
to current Y. Products and position arithmetic retain 32-bit wrapping.
The queued-motion clearance probe runs from the current effective integer Y
to that Y plus the positive Y extent, retaining X and Z. It visits the prepared
face candidates without the queued-motion type exclusion. Contacts of normal
classification 2 or 4 compete by their integer world Y; strictly smaller Y
replaces the current height, initialized to 999999. With no selected height,
clearance is 256000 Q8 units. A selected height returns
height * 256 - current_effective_Q8_Y.
The terrain query uses current integer map X/Z and truncates its interpolated
height toward zero. That height replaces the model result only when it is
strictly greater than current integer map Y and strictly less than the retained
model height. It does not use the ordinary support-segment rank or lower end.
The terrain query runs whenever the world has terrain. It does not test water
cells: a water cell supplies the same interpolated height as a dry cell. The
probe applies no height-grid bounds check of its own: the height array is addressed linearly (row index times
the grid width plus column), so a column past the east or west edge continues into the adjacent row of the array,
and the cell interpolation then runs between the last column of one row and the first columns of the next.
Engine cell row 256 is inside the height array: its upper corners use source row zero
and its lower corners use source row one. The ordinary triangle interpolation runs
there without a special refusal of the last row. The two triangles retain their
usual diagonal selection; cell-row numbering alone does not indicate an
out-of-array access.
More generally, adding 256 to a cell column and one to its engine row preserves
all selected source indices: (256 - (j + 1)) * 256 + (i + 256) equals
(256 - j) * 256 + i. A cell row outside the rectangular grid can therefore
still address height-array elements through a column carry. The combined linear
source index determines whether a corner belongs to the array.
A row past the north end of the array reads memory outside the array. Swimming straight north from the NECRISLE
start (west of the island) keeps the swimming state through the last array row and the row beyond it, and the player
enters animation group 18 with surface 0 within a few map units of crossing Z 0; the height then drops about 835 map
units in four seconds while Z keeps decreasing, so the player falls out of the world. The south end of the same map
instead reads level ground at Y -64.
Swimming west in the same map stops at X 261 and stays there with the forward input held. The interpolated
terrain height there, using the same linear addressing, is level (zero) at X 260 and rises steeply to the west
(-45 at X 240, -157 at X 200, -720 at X 0). The stop coincides with the start of that rise.
Directional ordinary displacement
Directional displacement masks the input motion byte with 127. Values 2, 6,
8, 9 and 24 select forward direction 2 at the actor heading and mark movement
bits 0x30. Values 3 and 7 select backward direction 3 at heading plus 1024
and mark 0xc0. Value 4 selects direction 4 at heading plus 512 and marks
0x03; value 5 selects direction 5 at heading minus 512 and marks 0x0c.
Other values return without these writes. Selected headings wrap to 2048.
The selected direction and movement bits are written even when the current animation suppresses displacement. Otherwise, stored surface state 2 or 4 halves the signed distance by arithmetic right shift. A positive resulting distance translates local X/Z using the heading-based translation rule. The corresponding directional update indicator is also set.
For an initial ordinary animated fall, current groups 41, 42 and 43 apply this displacement using the retained motion direction and Q8 distance 4096 before selecting their initial fall animation.
Pre-script queued and falling stage
Every actor update runs one stage between the ordinary animation stages and the script stage. Its only content is directional displacement. It has no test for actor kind, selected player, living state or collision attribute mask, so the selected player and every nonplayer take it alike. After the action-transition counter bookkeeping, a nonzero queued byte supplies the motion value; otherwise a nonzero falling byte does. Neither nonzero skips the stage. The sliding byte never selects it. Directional displacement returns without effect while the actor is attached to a rope.
For queued or falling motion 8 or 9 the displacement is forward at the actor heading. It sets motion
direction 2, the forward update indicator and movement bits 0x30, even when the current animation
suppresses displacement. Otherwise, while the accumulated sliding distance is below 0x18000, each
update adds (0x10000 / attribute 31) * frame_scale >> 8, limited so the accumulated distance
does not exceed 0x18000, to that distance and translates by the same amount; at or above the
cap nothing moves. Motion 2 (a jump steered forward) does the same with 0xb400 and a cap of
0x10000. Deep water halves neither. Other motion values add no displacement here. A zero attribute
31 divides by zero in the original.
Queued arbitration, the queued and falling vertical writers, the edge service, landing and fall start are not part of this stage. They run only inside the ground response, which an animated actor reaches only when its collision attribute mask has bit 4 set. An actor whose ground response does not run therefore still receives this displacement every update while its queued or falling byte is nonzero, and never advances vertically, lands or starts a fall.
Ground stage by model kind
A static-model actor runs its ground stage only when attribute 108 is nonzero and attribute 107 is zero, regardless of the collision attribute mask: the support query, then the ground response. The animated-model ground stage is selected by the collision attribute mask instead: a nonzero mask runs the obstacle pass unless the selected player is in its alternate collision mode, mask bit 8 runs the overlap pass, the support query follows, and mask bit 4 runs the ground response. Attribute 107 nonzero also ends a fall at the first support miss: the falling byte is cleared and nothing else changes.
Ordinary landing decision
On support regained while falling, clear the runtime jump selector and compare the absolute wrapping difference between current local Q8 Y and retained fall origin. Resolve and store the landing surface before this comparison; surfaces 1 through 4 replace the compared distance with zero.
The death threshold is SYSTEM.INI [game] fall_death_height * 256, and the
hurt threshold is fall_hurt_height * 256, retaining 32-bit arithmetic.
Distance strictly above the death threshold requests fatal landing and applies
health change -256. Otherwise distance strictly above the hurt threshold
requests group 73 and applies -fall_hurt_zap, except when the actor’s separate
hurt-exclusion predicate suppresses both. That predicate is true for stored
surface states 1 through 5 and 11. These threshold branches precede the
ordinary landing animation selection. They clear the falling byte.
For the ordinary nonplayer fatal path, group 74 is an ordinary request. The ordinary hurt group 73 also uses ordinary request arbitration.
The selected player takes the same threshold tests and the same hurt response:
group 73, then the generic health-with-death admission with -fall_hurt_zap,
which forwards the selected receiver to its death-owning health with no source.
Its fatal landing makes no animation request. It stores group 74 as the
death-animation override, applies health -256 through that same admission and
then clears the falling byte. A fatal landing on a support whose stored
classification is steep (2) instead raises the controller’s pending action flag
and does nothing else: no override, no health change, and falling stays set.
Ordinary landing animation selection
After threshold handling admits an ordinary landing, low falling state 8 requests group 67 without a burning torch or 68 with one when the selected player is not holding a weapon; otherwise it requests 130. Low state 9 similarly requests 69 or 70 without that selected-player weapon predicate, or 131 with it. These branches read the selected player’s weapon state, which is distinct from the landing actor’s weapon state.
Other falling states request 65 without a burning torch or 66 with one when the landing actor is unarmed. A weapon-holding actor requests 114 unless its current group is 115 or 116. All these landing requests use ordinary animation arbitration and clear falling after dispatch. The retained death-response byte is set to 8 by the actor death notification service. Ordinary landing retains motion classification.
Nonplayer health mutation
The shared health service returns without mutation when attribute 120 is nonzero. With attribute 97 nonzero, it also returns when the actor is neither holding a weapon nor in combat mode with packed bit 2 set.
Nonplayer health is attribute 3. Add the signed health change using wrapping 32-bit arithmetic, then clamp the signed result to 0 through 255. A result of zero dispatches death animation and notification. For an unarmed actor, force group 74 when falling is nonzero or current group is 114, 116, 130 or 131; otherwise force 134. Death notification sets the retained response byte to 8. Armed death has separate combat animation and state services. Selected-player health uses a separate service rather than mutating this attribute byte.
Stable recovery probe
The late stable-position recovery probe translates the actor’s query position
forward by 6144 Q8 units at its local heading, then converts all coordinates
to integer map units. Its vertical segment starts at that Y plus the actor’s
positive Y extent minus post_collide_height and ends at that Y plus the extent
plus post_collide_height.
Recovery prepares its own ordered static, live-actor and transient candidate lists. A current model supplies a search radius of twice its header radius, truncated toward zero. With a current model, admission uses the effective query position. Without a current model the search radius is 200 map units and admission uses local position. The actor’s own current model instance is excluded.
It visits its prepared face-model candidates in order with sidedness disabled. The first contact with normal classification 4 accepts recovery and writes local position to the probe’s integer X/Z multiplied by 256 and contact integer Y minus the positive extent, also multiplied by 256. It does not add standing height or rank competing faces.
When no model contact accepts, terrain height truncated toward zero accepts recovery if it lies inclusively between both segment endpoints. Terrain acceptance does not write the actor position. With no animation receiver the probe accepts before those segment tests. Candidate preparation precedes that receiver check and is distinct from the ordinary retained collision list.
Collision-response word (last_collide)
An actor’s collision responses are marks in a 32-bit word, the last_collide
field (its property descriptor reads the low byte). The low byte contains the
primary body-response marks, including wall slide and wall hit. The following
byte contains collision-transition marks. The remaining bytes are retained
independently and participate in the packed response test and clear. The byte
last_collide_count that follows is the collision-response age. Bits carry the
SOUP386.DEF cf_* names:
| Bit | Name | Set by |
|---|---|---|
0x1 | cf_slide_off_wall | Primary body response, projected wall contact with heading adjustment (result bit 1) |
0x2 | cf_hit_wall | Primary body response, other projected contact or rollback (result bit 2) |
0x4 | cf_polygon_push_out | Residual face-contact separation (result bit 4) |
0x8 | cf_last_valid_set | Not set |
0x10 | cf_validate_slope_fail | Not set |
0x20 | cf_check_left_fail | Left step refusal |
0x40 | cf_check_right_fail | Right step refusal |
0x80 | cf_check_forward_fail | Forward step refusal |
0x100 | cf_check_backward_fail | Backward step refusal |
0x200 | cf_check_run_forward_fail | Forward-speed destination refusal |
0x400 | cf_managed_collision | Overlap mark (body-response 0x04) |
0x800 | cf_validate_forward_failed | Hit mark (body-response 0x08) |
0x1000 | cf_premeture_jump_end | Queued-motion transition (body-response 0x10) |
0x2000 | cf_premeture_fall | Falling transition (body-response 0x20) |
0x4000 | cf_cross_polygon_push | Bounds response (body-response 0x40) |
0x8000 | Landing on another actor (body-response 0x80) | |
0x10000 | Stable recovery and water rollback (collision-transition bit 0x01) |
Stable recovery response
The ordinary script wrapper retains a byte collision-response age. After its admitted script work, an all-zero packed response record clears the age to zero. Otherwise, an age below 200 adds the shared nominal tick increment with byte wrapping; an age at least 200 is unchanged. The wrapper then clears the packed response record and overlap contact identifier. Body-response and collision transition marks occupy separate bytes of that packed record.
A nonplayer stable recovery request returns without work when the stable-position record is invalid. It prepares the separate recovery query and returns when that probe accepts. On rejection, it restores the stable local position, clears attribute 6, falling and sliding, restores the saved directional movement flags and animation collision snapshot, and marks collision transition bit 1. The restored animation snapshot does not replace the ordinary default group.
The nonplayer then adds Rnd(attribute_15 * 4) - attribute_15 * 2 to local heading,
wrapped to 2048. A collision-response age strictly greater than 16 also runs the
bounds-segment obstacle response. The recovery marks the shared position-changed
flag. The selected player takes neither the heading change nor the bounds response:
after the shared restore (stable position, attribute 6, falling and sliding cleared,
movement flags and animation snapshot, collision transition bit 1) it runs the
ordinary ground response once more. When that response reports no support, the
attempted position is written back, the stable-position record becomes invalid and
the falling byte returns to its value before recovery.
After reserved weight is restored, ordinary collision invokes stable recovery when attribute 89 is nonzero and either sliding or falling is nonzero. Otherwise, a captured landing group of 67 through 70, 130 or 131 invokes recovery only when sliding, falling or the shared surface veto is nonzero. These checks precede the separate water rollback.
Ordinary water rollback
A nonplayer actor with no falling or queued motion rolls back when its stored surface is water, codes 1 through 4. This restores a separately retained local position and clears attribute 6. At the end of an ordinary update, a nonwater surface or active falling state replaces this record with the current local position. A water surface without falling leaves it unchanged. The record is distinct from the position captured at update entry. It also restores the directional movement flags and the animation collision snapshot, marks collision transition bit 1, and sets the shared changed flag. Selected-player actors do not take this branch.
Stable ordinary position
After collision attribute bit 0x04 ground response, an actor with queued
motion, falling and sliding all zero marks its stable-position record valid
and copies all three current local Q8 coordinates into that record. An update
with any of those motion states nonzero retains the existing stable record and
validity. The caller of the ground-support probe ignores
the probe’s answer and applies this rule after it returns on every path,
including the early exits for a handled bounce update or queued motion. This
record is distinct from the position saved at update entry.
Jump admission clears its validity before initializing queued motion.
Ordinary slope response
Slope response reads [game] slide_speed from SYSTEM.INI, clamped to at
least 8 during settings loading. It uses the current primary support result.
With no support or a classification other than 2, it requests ordinary group
75, or 77 with a burning torch, unless the current group is already 75 through
78. This fallback does not change sliding state, distance or position.
For class 2, the horizontal support normal supplies the quantized movement
heading. The comparison heading is local for an unattached actor and effective
for a family actor. A signed wrapped heading difference with magnitude below
512 selects a forward slide, groups 75 or 77, and movement bits 0x30.
Other differences select a backward-facing slide, groups 76 or 78, and bits
0xc0. Backward-facing actors turn toward movement heading plus 1024; both
branches translate along the movement heading.
Turning precedes the slide animation request. It uses attribute 15 when neither holding a weapon nor in combat mode, and attribute 21 otherwise. The turn rate is the signed wrapping product of that byte and frame scale, shifted right by 8, then clamped to at least 8. Differences within that rate snap to the target without requesting a turn animation. Larger differences turn by the rate and request an ordinary turn group when an animation receiver exists. Without a held weapon these are positive 14 or torch 16, and negative 15 or torch 17. With a held weapon they are positive 96 and negative 97. Combat mode affects rate selection independently of the held-weapon animation branch. Heading writes wrap to 2048. The turn helper returns zero when the desired heading already matches or the turn snaps to it. Otherwise it returns the signed heading difference before applying the turn step. For the selected player the turn also rebuilds the actor’s orientation matrix from its angles (identity, then negated yaw, pitch and roll) and copies the angles to the angle history; it changes no camera state.
The first slide forces its slide group; subsequent slides request it ordinarily.
The slope response sets the packed direction counters without setting the shared
direction-called flags. The ordinary update clears counters for directions whose
movement natives did not run, so slope motion alone does not retain a saturated
startup counter for the next update.
Distance is the signed wrapping product slide_speed * 128 * frame_scale,
shifted right by 8. Only a positive distance translates X/Z. Primary support
is queried again with the original response probe distance; an available result
replaces local actor-origin Y. The first slide clears accumulated sliding
distance, then every class-2 response adds the computed distance, including
nonpositive values, and sets sliding state to one.
Slide-group movement
Every actor’s update, the selected player’s included, runs a group-driven movement stage after its script and the movement natives. The stage reads the current animation group and, when no rope node is attached and the sliding byte is clear, requests the directional displacement the group implies unless a displacement of that direction was already requested earlier in the same update. Slide groups 75 and 77 request forward displacement (direction 2) and groups 76 and 78 backward displacement (direction 3). The displacement uses the ordinary startup counters in the movement bits (forward bits 4-5, backward bits 6-7) and the slide-group speeds of the movement natives (attribute 8 shifted left seven).
The slope response sets the sliding byte on every class-2 update, so the stage is idle while an actor slides. It resumes in the update after the ground response finds non-steep support and clears the sliding byte: the slide group keeps playing until its animation ends and moves the actor along its heading (or heading plus 1024 for groups 76 and 78) for those updates, without the slope response, changes to the accumulated sliding distance or support-height changes. An actor that lands on a flat surface while sliding therefore keeps travelling for the rest of the slide animation.
Slide termination
When the ground response finds support and the sliding-surface result is clear (or the native
return-state byte is nonzero), a nonzero sliding byte ends the slide. If the accumulated sliding
distance exceeds SYSTEM.INI [game] slide_hurt_height * 256, the actor first takes directional
displacement of Q8 distance 0x800 (direction 2 in groups 75 and 77, direction 3 otherwise), then
requests group 73 and changes health by -slide_hurt_zap; the selected player instead stores death
override group 74 (no animation request) and takes the health change through its death-owning health.
The sliding byte then clears. The selected player also takes the hurt branch whenever its
steep-landing flag (the controller’s pending action flag) is set, whatever the slide distance.
The slide and turn animation requests use the shared animation request gate. Disabling the animation system leaves the current group unchanged while slope displacement, orientation and sliding bookkeeping continue. A declined hurt-animation request does not skip the final displacement, health change or clearing of the sliding byte. Static-model actors have no animation receiver and likewise complete this slide termination without requesting a group.
Late collision stage in the ledge states
An animated actor in living states 10 through 19 skips the forward obstacle wrapper and, at the ground response, runs the ledge transition instead of the ordinary support response; the overlap pass and the support query still run, and the support query takes the falling probe distance when the falling byte is nonzero. A nonzero falling byte in these states therefore has no rule of its own: the ledge transition leaves it unchanged for an actor other than the selected player. The primary body response runs in living states 0, 15 and 27 through 32 only.
Support response in the ledge states
The ordinary support response starts no fall while the actor is in living states 10 through 19 (turning to or climbing a ledge, catching, hanging, climbing from a hang, dropping, finishing a climb, entering a ledge). It only ends its query; a missed support, a falling byte and queued motion are left as they are, for static-model and animated actors alike. The bounce states 27 through 32 have no branch of their own in this response: a support miss in them takes the ordinary fall start below. Jump entry in state 15 dispatches the hang release for the selected player only; any other actor in state 15 clears its stable-position-valid flag and changes nothing else.
Initial falling bookkeeping
When the ordinary support response misses and starts a fall, it records local
Q8 Y plus 256 * attribute 32 as the fall origin and sets the byte timer to
30. The ordinary falling byte becomes one. Static-model actors then run the
vertical writer without an animation request. An animated actor’s initial
unarmed request is forced group 56 without a burning torch or 57 with one;
a weapon-holding actor forces 113 unless current group is 115.
Animated entry also has separate selector, stable-position recovery and horizontal-displacement branches before this initialization. When either directional-state byte has a low bit set, the recovery branch clears those two bits in both bytes, restores all three coordinates from the stable position record, runs the ordinary jump entry with requested motion 8, forces group 35 with a burning torch or 34 without, runs the queued-motion arbitration once and ends the update’s fall handling. Current groups 18 and 30 have an exit-marker rollback branch and otherwise force 59 or 60. For the exit-marker branch, restore all three coordinates from the stable position record and force the current default group, then return without starting a fall. Without that marker, record the ordinary fall origin and timer, set falling to one and force 59 without a burning torch or 60 with one. These entry branches are distinct from continuation of an existing fall.
Jump selector at a support miss
An animated actor that misses support without falling first tests the runtime jump selector. A selector whose distance from 24 is below four (and nonzero) starts the ordinary jump entry for it, which selects queued motion 24 and forces group 85, and the support miss ends there: the walk-off recovery, the fall start and the edge service do not run. Other selectors leave the miss to those services.
Falling animation continuation
For an animated actor already falling without support, the edge service runs before the continuation request. When it does not handle the actor, low falling state 3 requests group 63 without a burning torch or 64 with one. A set high bit with no burning torch otherwise requests group 58. These cases precede the short-distance motion variants.
Falling byte 8 with absolute local Q8 Y distance from the fall origin strictly below 32768 requests group 59 without a burning torch or 60 with one. Byte 9 under the same condition requests 61 or 62. Other ordinary unarmed continuation requests group 56 or 57 according to the torch-burning counter. An actor holding a weapon instead forces group 113 unless its current group is 115. The vertical writer runs after this request, reading the resulting animation’s model-controlled vertical-motion marker.
Selected-player ledge entry
Ledge entry lets the selected player catch a ledge while falling or during queued motion, hang from it, move along it and climb onto it. It uses the living-state byte, the sliding, falling and queued-motion bytes, the retained ledge-support actor reference, a retained ledge point and ledge heading, and the global action-inhibition countdown. Ledge living states are 10 through 19. The edge service, ledge transition, hang release and climb request retain motion classification, including when they clear sliding, falling or queued motion. Coordinates use Y increasing downward. The positive Y extent reaches from the actor origin down to its feet; the negative Y extent reaches up to its head.
Edge service
The edge service returns zero unless the actor is the selected player, attribute 105 is not one, the torch-burning counter is zero, and neither living state 33 or 34 nor current group 149 or 150 blocks it. Those block only while animation is enabled and shell context is clear. Then:
- Stored surface 2 or 4 returns zero. In living state 15 it first runs the hang release and then the ledge transition once.
- Living states 10 through 19 return one without other work.
- Any other nonzero living state returns zero.
- Living state zero becomes 19, the ledge-support reference is cleared and the
ledge transition runs once. A nonzero resulting living state clears the
sliding, falling and queued-motion bytes and sets the action-inhibition
countdown to 16. A zero resulting state with a nonzero action-transition
counter clears bit
0x80of the queued-motion and falling bytes. The service returns the transition’s result.
The ground response calls the service for an animated actor without support. An actor already falling calls it before the continuation request; an actor starting to fall calls it after its initial fall request. A nonzero result clears the falling byte and ends that update’s fall handling. Queued motion arbitration also calls it; a nonzero result clears queued motion and returns handled.
Player initialization clears the action-inhibition countdown. Each update subtracts the whole nominal ticks; a countdown that would not stay positive becomes zero.
Per-update ledge dispatch
An actor whose ground-response bit 0x04 is set and whose living state is 10 through 19 runs the
ledge transition once per update in place of the ordinary motion and ground
response; weapon-draw states 20 and 21 keep their own branch. For an actor other than the selected
player the transition changes nothing. When the sliding, falling and queued-motion bytes are all zero
afterwards, the stable-position-valid flag becomes one and the current position is retained as the
stable position, including when the transition ended in state 18 or cleared the state. Nonzero
bytes leave the stable record unchanged. The update does not run the body-contact response of states
other than 0, 15 and 27 through 32.
No ledge candidate
When the ledge scan finds no record, entry from living state zero
sets living state 19, selects nothing, sets the living state back to zero and
returns zero. Sliding, falling, queued motion and the action-inhibition countdown
are unchanged, except for the bit 0x80 clear above when the action-transition
counter is nonzero. The ledge-support reference stays cleared. The scan changes
no actor state, so the caller continues its ordinary fall or queued-motion
handling exactly as if the service had not run. A weapon-holding actor or a
nonzero action-transition counter always takes this path.
Ledge transition
The transition returns zero without changes unless the actor is the selected player, attribute 105 is not one and the living state is 10 through 19. It clears the global hang-model reference. A nonzero torch-burning counter or the state-33/34 or group-149/150 block runs the hang release and returns zero. Otherwise states are handled in the table’s order within one call; a state set by one row is handled by later rows in the same call.
| State | Meaning | Handling |
|---|---|---|
| 15 | Hanging | Scan and select the hand-nearest class 3 record. None runs the hang release and returns 0. Otherwise set the hang-model reference to the record’s top model. Only when a left or right sidestep started this update, add 256 × sine[256] to the record’s X and 256 × sine[768] to its Z, nearest rounding, and, if the current animation’s BreakPoint marker is clear, copy the record point to the ledge point and the record heading to both the ledge heading and the actor heading. Without a sidestep, retain the existing ledge point and headings. Measure the support below the origin. A distance below (positive Y extent + 24) × 256 returns the climb request’s result. Otherwise apply the hang pose, set the ledge-support reference to the record’s actor and return 1. |
| 19 | Entering | Scan. Result 1 selects the first class 1 record and state 12. Otherwise the first class 2 record selects state 10, else the hand-nearest class 3 record selects state 14. Without a record, set state 0 and return 0. Otherwise copy the record point to the ledge point, its heading to the ledge heading and its actor to the ledge-support reference. |
| 12 | Turning to a low ledge | Run the slope turn helper toward the ledge heading. A nonzero result returns 1 when its magnitude is at most 256; otherwise set state 0 and return 0. When facing, rescan; a result other than 1 sets state 0 and returns 0. Otherwise set X/Z to the ledge point plus low_forward[0] × 256 at ledge heading plus 1024, leave Y, force group 90 and set state 13. |
| 14 | Catching a hang | A wrapped difference between actor heading and ledge heading above 256 in magnitude sets state 0 and returns 0. Select the hand-nearest class 3 record from the existing table without rescanning; none sets state 0 and returns 0. Set X/Z to the ledge point plus high_forward[0] × 256 at ledge heading plus 1024, leave Y, force group 88 when record byte 0 is zero or 89 otherwise, set state 15, the actor heading to the record heading and the ledge-support reference to the record’s actor, and return 1. |
| 10 | Turning to a high ledge | As state 12, but the rescan must return 2. Set X/Z to the ledge point plus high_forward[0] × 256, set Y to the ledge point’s Y, request group 86 with speed modifier 0 and frame threshold 2, and set state 11. |
| 13 | Low climb | While the current group is 90, with f the animation’s real_frame, set X/Z to the ledge point plus low_forward[f] × 256 − lag at ledge heading plus 1024; when f exceeds 6 set Y to ledge_Y + (low_height[f] + standing_height) × 256. Return 1. Otherwise set state 18, clear sliding, falling and queued motion, and return 1. |
| 16 | Climb from hang | Force group 86, set state 11 and real_frame to zero. |
| 11 | High climb | As state 13 with group 86, high_forward and high_height, writing Y on every frame. |
| 17 | Dropping | Force group 56, set the action-transition counter to 6, clear the living state and the ledge-support reference, and return 0. |
| 18 | Climb finished | Returns 0. The ordinary actor update clears the pending group while the current group is 90 or 86; otherwise it forces the default group and clears the living state. |
Other ledge states return zero. A ledge reached from a fall or queued motion can only produce class 3 records (see classification), so a successful catch passes states 19, 14 and 15 in one call and returns 1.
An offset d at heading h adds round(d × sine[u]) to X and
round(d × sine[u + 512]) to Z, with u = (−h) & 2047 and the shared
floating-point sine table. Ledge heading plus 1024 points from the wall back
toward open space. lag is max(0, command_cursor − command_count / 2) × 256
for the current group’s signed command cursor and command count, and zero while
animation is disabled. The climb tables are indexed by real_frame without a
bound check:
| Table | Entries |
|---|---|
low_height | 35, 35, 35, 35, 38, 29, 15, 0, −7, −21, −42, −56, −58, −60, −59, −61, −61, −61, −61, −61, −61, −61 |
low_forward | 28, 28, 27, 27, 28, 24, 21, 16, 8, 7, 6, 5, 4, 4, 4, 5, 4, 3, 2, 2, 2, 2 |
high_height | 81, 67, 45, 24, 11, −1, −19, −28, −29, −29, −29, −29, −45, −53, −58, −60, −61, −61, −60, −60, −60, −60, −60, −60, −60, −60 |
high_forward | 30, 30, 30, 26, 20, 16, 8, 3, −6, −6, −4, 0, −6, −3, 1, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 |
Hanging sidestep
A left or right sidestep in living state 15 is admitted when the ledge scan, run with
the actor shifted along its own X axis, still finds a class 3 record for the hand. The shift is the
actor’s X extent on that side (the negative extent to the left, the positive to the right) plus
attribute 10, in whole map units, rotated by the actor matrix; X and Z change and Y stays. The
original position is restored whatever the result. An admitted step requests animation group 28
(left) or 29 (right) and, with the animation system enabled, continues until that group is current.
It then sets the left or right update indicator, which the hanging transition reads, and moves the actor along its
own X axis by attribute 10 times the frame-scaled step ramp, with no translation while the current
animation’s BreakPoint marker is set.
Ledge hang release
For the selected player in living state 15 with a zero action-inhibition countdown, set living state 17, clear the ledge-support reference and run the transition. A nonzero resulting state clears the sliding, falling and queued-motion bytes. Other states, and a nonzero countdown, change nothing.
Ledge climb request
After jump-key handling, the selected-player controller calls the climb request while forward is held and the living state is 15. It then calls the hang release while backward is held and the living state is still 15. These directional calls do not require the walk key.
For the selected player in living state 15, scan and select the hand-nearest class 3 record. Without one, or with record byte 2 zero, return 1 and keep hanging. Otherwise set the ledge-support reference to the record’s actor, set state 16 and run the transition; a nonzero resulting state clears the sliding, falling and queued-motion bytes. Return the transition’s result.
Ledge hang pose
Let g be the grip-0 vertex of the first ShowFrame of the actor’s animation
group 7, from the animation-vertex query
in Q8 integers. Set X/Z to the ledge point plus high_forward[0] × 256 at ledge
heading plus 1024, and Y to ledge_Y + standing_height × 256 + abs(g.y) + 512.
Without a group 7 frame nothing is written.
Ledge support below the origin
A vertical model segment runs at integer current X/Z from Y plus the positive Y
extent up to Y. The smallest contact Y among classes 4 and 2 is kept; with
terrain enabled, a terrain height strictly between Y and that value replaces it.
The result is selected × 256 − current_Y, or 256000 without a selection.
Ledge scan
The scan clears the four-record ledge table and disables polygon sidedness for
its duration, re-enabling it afterward. It finds nothing when the
action-transition counter is nonzero or the actor holds a weapon. Otherwise it
runs one pass at forward offset p = 0 and, when that returns zero, a second at
p = 6144 (Q8). The result is 1 when a class 1 record exists, else 2 for
class 2, else 3 for class 3, else 0.
Each pass:
- Build a segment from actor-local Q8
(0, 40960, 9728 + p)to(0, −40960, 9728 + p), rotated by the actor’s orientation matrix, added to the current position and shifted right eight. - Query each prepared model candidate with the integer ground-probe adapter,
classifying contacts with the
surface classification. Keep the
class 4 contact with the strictly greatest fraction as that candidate’s top
(zero fractions never qualify). Class 8 contacts from candidates whose type
lacks bit
0x08append their Y to an underside list shared by the pass. - For a candidate with a top, while fewer than four records exist, build a
horizontal wall segment from local
(0, 0, 9728 + p)to(0, 0, p), rotated and positioned as above, at Y = top Y + 16. Over all candidates keep the class 1 contact with the strictly greatest fraction whose normal’s facing heading (its displacement heading plus 1024) is within 256 of the actor heading, differences wrapped by 2047. No such wall adds no record. - Repeat the wall segment 24 units lower: any such facing class 1 contact sets
record byte 0 to one, otherwise zero. Append a record with byte 0, ledge point
(wall_X, top_Y, wall_Z) × 256, the top’s model, the record heading (the wall normal’s facing heading) and the ledge-support actor (the wall candidate’s actor, zero when it has none). A fifth record is not guarded against.
Ledge record classification
Let Y be the integer current Y, neg and pos the Y extents,
reach = max(14, fall_step >> 9) with fall_step the nonpositive-timer
falling step
min(post_collide_height × 512 − 256, attribute_32 × factor[0] × (fine_tick_increment + 2)),
and hand_Y = Y + (g.y >> 8) for the group 7 grip vertex g, or Y − 1000
without that frame. For each record with top Y t:
- With falling or queued motion nonzero, or living state 14 through 17: class 3
in living state 15 or when
abs(hand_Y − t) < reach, else 0. - Otherwise: class 2 when
Y − neg − reach <= t < Y − (neg >> 2); class 1 whenY − (neg >> 2) <= t < Y + pos − post_collide_height; else 0.
Then an underside Y with abs(t − underside) < 8 sets class 0. For a nonzero
class:
- Outside living state 15, a wrapped heading difference above 256 between actor and record heading sets class 0 and skips the fit test.
- The fit test places the actor’s collision spheres, with its current angles, at
the wall point plus offset 4096 at the record heading and Y
(t − pos − standing_height) × 256. Candidates are tested in order; a body contact fails the body test and ends the loop. Until then each candidate is also tested with one sphere of radiuspost_collide_height >> 1centeredpos − (post_collide_height >> 1)below that Y; a class 8 contact fails the head test. - A failed body test sets classes 1 and 2 to 0. For class 3, record byte 2 is 0 when the head test failed, otherwise 1 when the body test passed and 0 when it failed.
The hand-nearest class 3 record minimizes abs(t × 256 − (local_Y + g.y)),
earliest on ties, and does not exist without a group 7 frame. Other selections
take the first record of the class.
Ordinary jump entry
Jump entry requires no active native motion task, queued motion, falling or sliding, and actor state zero or 15. Selected-player edge and water gates run separately. State 15 dispatches its separate exit service instead of this ordinary initialization.
For ordinary initialization, clear the stable-position-valid flag, record current local Q8 Y as fall origin, set the byte fall timer to zero and clear accumulated sliding distance. With the runtime jump selector zero, current group 18 selects queued 9 and request 36 when the requested motion is 9, otherwise queued 8 and request 34. Current group 30 similarly requests 37 or 35. For the selected player’s jump key, current group 19 (or 23 with a burning torch) instead selects queued 3 and request 49 (50), copies the heading into the motion vector and sets the controller countdown to 6, as the script Jump rows do; the walking backward groups jump standing. Other current groups select queued 1 and request 32 without a burning torch, or 33 with a burning torch. A nonzero runtime jump selector instead selects queued 24 and forces group 85. Set motion classification to 2 after the request.
The controller itself reads no surface state: water and lava surfaces change its behavior only through the movement helpers and the support response it calls.
Script Jump
The zero-argument SOUP function Jump returns zero immediately, including when
admission refuses or an edge service handles the call. It does not wait for the
jump animation or landing.
Admission refuses a selected player with a nonzero conditional controller countdown. It also refuses surface 2 or 4, any active native task, nonzero queued, falling or sliding motion, and living states other than 0 or 15. Groups 114, 116, 130 and 131 refuse admission. For the selected player with attribute 105 equal to one, all water surfaces 1 through 4 refuse admission.
After admission, clear the stable-position-valid flag. Living state 15 runs hang release and returns. Otherwise run selected-player edge entry; a handled entry returns before initializing queued motion. For a nonselected actor in living state 15, hang release does nothing. The native still returns zero after clearing stable-position validity, without changing motion state, position or animation.
Ordinary initialization records current local Q8 Y as fall origin, clears the fall timer and accumulated sliding distance, then chooses the queued motion and animation request below. A nonzero runtime jump selector overrides all rows, selecting queued motion 24 and forcing group 85.
| Current group | Queued motion | Requested group | Additional writes |
|---|---|---|---|
| 18 | 8 | 34 | None |
| 30 | 8 | 35 | None |
| 19 | 3 | 49 | Copy heading into ov.vy; set the selected-player conditional countdown to 6 |
| 23 | 3 | 50 | Copy heading into ov.vy; set the selected-player conditional countdown to 6 |
| 31 | 8 | 34 | Copy heading into ov.vy |
Other, holding a weapon or with combat flag 0x04 | 1 | 111 | None |
| Other, no held weapon or combat flag, torch burning | 1 | 33 | None |
| Other, no held weapon or combat flag, no burning torch | 1 | 32 | None |
All non-forced rows use the ordinary animation request with reverse zero. After the request, set motion classification to 2. The script native does not select the backward queued state 9 used by the parameterized ordinary jump entry.
Jump-key arbitration
While the jump key is held, every ordinary controller update after movement dispatch runs one arbitration with mask 1; the rope branch has its own jump-off service. The arbitration does nothing when either directional-state byte already has the mask bit set. Otherwise:
- A current group from 65 through 74, a nonzero queued byte or a nonzero falling byte clears the low two bits of both directional-state bytes and ends the arbitration. The sliding byte is not tested here.
- Otherwise a step-hold condition is true when attribute 11 is zero or the motion byte’s step field (bits 4 and 5) equals 3. With the step-hold condition true and either current group 18 or 30, a clear exit marker sets both low bits of the pending action word and clears the jump-window timer. Any other state calls the ordinary jump entry. This jump start refuses, without any state change, while the actor stands on surface 2 or 4 (deep water), while its current group is 114, 116, 130 or 131, and while its falling byte is nonzero.
This is the only writer that sets the directional-state low bits. The ordinary jump entry also refuses the selected player while the conditional preliminary countdown is nonzero, so a nonzero queued or falling byte, or a nonzero sliding byte, ends the call without effect.
Jump window
Every actor update, after its script stage and before its ordinary movement, adds the whole nominal ticks to a byte counter. When the counter exceeds 36 the second directional-state byte receives the first byte’s bits, the first byte clears and the counter returns to zero. A nonzero queued byte, or a current group other than 18 and 30, clears the low two bits of both bytes. Then a set low bit of the second byte clears the low two bits of both bytes and starts the ordinary jump entry with requested motion 8; otherwise a set second bit does the same with requested motion 9. The pending first byte therefore survives for up to 36 ticks while a run or walk jump group loops, which is the window in which a support miss takes the walk-off recovery.
For the selected player the entry runs the following before the ordinary initialization, in order: it refuses while the conditional preliminary countdown is nonzero, while a rope is attached, while the queued, falling or sliding byte is nonzero, outside living states zero and 15, and in water or lava surfaces when attribute 105 is one. It clears the stable-position-valid flag. Living state 15 then runs the hang release instead of starting a jump, and living state zero runs the ledge entry (the edge service without the water exit, the action-inhibition countdown and the fall-bit clear); a ledge held by either ends the entry.
Queued motion arbitration
A zero queued byte returns unhandled. Low queued state 3 requires group 49 when the torch-burning counter is zero, or group 50 when it is nonzero. An unavailable required group clears queued motion and returns unhandled. A different current group returns unhandled without a burning torch, but handled with a burning torch. These branches do not read the living-state byte.
Pending groups 32 through 37 and 111 defer queued motion and return unhandled. A successful selected-player edge service clears queued motion and returns handled. Other admitted queued states make an ordinary animation request before vertical advancement:
| Low queued state | No torch, high bit clear | No torch, high bit set | Burning torch |
|---|---|---|---|
| 1 | 39, or 112 when holding a weapon | 40 | 38 |
| 2 | 42 | 43 | 41 |
| 3 | 49 | 49 | 50 |
| 8 or 24 | 44 | 48 | 45 |
| 9 | 46 | 48 | 47 |
Other nonzero queued states advance vertically without this animation request.
After vertical advancement, a cleared queued byte returns handled. Otherwise,
clearance strictly below (positive_Y_extent - 4) * 256 clears queued motion
and restores all previous local position coordinates; the result remains handled.
Ordinary ground prevalidation
Ground prevalidation uses a shared guard reset at ordinary-wrapper entry. The first call marks the guard used before checking collision attributes. Later calls return without changing the retained position rollback veto or sliding-surface result, even when collision candidates or actor attributes have changed.
The first call bypasses geometry queries when collision attribute bit 2 is clear, or for the designated player while its support-history counter is nonzero. This bypass clears both retained results. Other actors are not excluded by the counter.
It queries current and previous effective positions with the post-collision height as vertical half-length. Face contacts use the forward steep-surface midpoint ranking above, followed by terrain height comparison. This scan does not apply the queued-motion candidate-type exclusion. When local X and Z have not changed, the current result also serves as the previous result.
If both queries miss, both results clear. When exactly one query misses, the classification of the missing side is read from uninitialised stack memory and is not defined. Classification 4 at both positions also clears both. Classification 2 at both positions permits motion and marks the sliding-surface result. For a current class 2 after another previous class, subtract the motion heading from the current normal heading minus 1024 and wrap to 2048. Values below 512 or above 1535 request rollback and clear sliding. For other directions, motion permits sliding only when attribute 89 is zero and the current animation group is neither the retained default group nor any of groups 20 through 27. Otherwise it requests rollback. Other current classes clear both results.
The wrapper applies rollback by restoring all three previous local position coordinates, clearing the retained movement flags, and running the ordinary obstacle transition before the main support query.
The ground response reads the sliding-surface result after it has assigned the support height to the actor’s Y. A set result with a zero native return-state byte clears the falling byte, runs the ordinary slope response and ends the ground response without its remaining handling. With a nonzero native return-state byte the result is ignored. The rollback result is read only by the wrapper.
Forward ground-support admission
The forward ground check saves the actor position, queries support at the proposed position, then restores all three position coordinates before deciding whether the attempt succeeds. The support query retains its other state changes.
The attempt fails when collision attribute bit 2 is set and the resulting support classification is 2. It also fails when support is absent or the shared surface veto equals one. Bypassing the earlier slope check does not bypass this final classification check.
When the surface-state updater reaches its ordinary assignment path, it sets the shared veto to one for actor surface states 5 and 11, and zero for other states. Its early-return paths do not perform that assignment. The forward check reads the shared veto after the support query; it does not recompute it solely from the actor’s current surface state.
Surface-query state retention
The surface query has separate return, actor-surface and shared-veto results. An excluded query returns zero without replacing the actor’s surface or the shared veto. An admitted query can also run without assigning actor state: it returns the resolved model or terrain surface, but retains both the actor’s surface and the shared veto. For other support kinds, this path returns zero.
Queued motion, either directional-state byte with either low bit set, or the selected-player surface exclusion cause the excluded-query result. With those gates clear, falling, sliding, or living states 27 through 29 select the return-without-assignment path. Other ordinary actors take the assignment path.
The ordinary assignment path stores the resolved surface in the actor, then sets the shared veto according to that stored value. Model contacts use the packed face texture assignment; terrain contacts use the actor’s integer map X/Z position. Other support kinds assign surface zero. A zero return alone therefore does not establish that the actor’s surface or shared veto is zero.
The ordinary support wrapper captures the actor’s previous surface before
querying support. It commits the designated-player support-history result, then
invokes the surface updater only when collision attribute mask 0x04 is set.
This invocation is not conditional on finding support: a miss supplies the
cleared source kind to the updater. With that attribute mask clear, the wrapper
does not refresh the actor’s surface or shared veto.
Surface-transition handling compares the updater’s return value with the saved actor surface, rather than checking only whether assignment changed the actor. Water surfaces are values 1 through 4; lava is value 5. When both the previous and current actor surfaces are outside water and lava, this wrapper does not produce water/lava transition effects.
Ledge point storage
The entering state’s scan selects one record by class (class 1, 2 or 3 starts living states 12, 10 and 14). The
selection copies the record’s ledge point into the actor’s ov.vx, ov.vy and ov.vz, the record’s heading
into the second word of the motion triple (move.vy), and the record’s ledge-support actor into the actor’s
linked field. The later ledge states read the point and heading from these fields, not from a rescanned record,
so a script reading ov or move.vy while the player holds a ledge sees the held point and heading.
Water and lava transition effects
The effects below run only for the selected player, only when the surface updater ran (collision
attribute mask 0x04) and its returned value differs from the saved previous surface. Let P be the
saved previous surface and S the actor’s stored surface after the update; the predicates read S.
An update that takes the excluded-query result returns zero without storing, so with a nonzero P the
comparison fires and the predicates read the unchanged stored surface; the return-without-assignment
path returns the resolved surface, which equals P when the surface did not change.
- S is outside 1 through 4 and P is 0 or above 4 (not in water before or now): when S is 5 or P is
5, one effect of identifier
0x2580is created at the actor; otherwise nothing happens. Each effect is theAddFlatEffectnative with zero spread and speed one, placed at the actor’s X and Z moved eight map units (0x800 in Q8) toward the camera, along the bearing from the camera to the actor, at the support height truncated toward zero plusstanding_height(map units, scaled to Q8) plus 0x600. - Otherwise (in water now or before):
- Animation request. S not 2 or 4 and P equal to 2 or 4 (leaving deep water): the actor’s current animation motion byte 2 or 6 requests group 18, or group 30 while the torch-burning counter is nonzero; any other motion byte requests group 19, or 23 with the torch. S equal to 2 or 4 (in deep water): motion byte 2 or 6 requests group 142, any other motion byte group 143. Shallow-to-shallow changes request nothing.
- One effect of identifier
0x2500is created, placed as above. - A positional sound at the actor with volume zero and a rate offset equal to a random value below 64 minus 32: effect 70 when S is outside 1 through 4 (left the water); otherwise effect 71 when P is 0 or above 4 or S is 2 or 4 (entered water, or in deep water); otherwise effect 70.
SOUP IsInDeepWater() reads the receiving actor’s stored surface state.
It returns one for states 2 and 4, and zero for every other state. It does not
perform a new terrain or support query.
Converting support height to actor position
The selected ground surface height is not the actor origin height. For ordinary
support, truncate the selected surface height toward zero in map units, subtract
the actor’s positive Y extent, multiply by 256, and add the configured
standing_height converted to runtime units. Ground-query eligibility and the
later movement response remain separate from this conversion.
Applying ordinary ground support
In an ordinary grounded update, the actor retains its previous position before walking changes X and Z. The subsequent obstacle pass and support query precede the ground response. When ordinary ground support is available and no airborne or special movement state diverts the response, the response assigns the returned actor height to Y. It preserves the translated X/Z and the saved previous position. This is a discrete support-height assignment, not vertical interpolation toward the terrain surface.
Terrain sink recovery
The ground-support probe of an actor begins with a terrain check, before the
bounce update and the queued-motion resolution. It applies in a world with
terrain to an actor in an airborne state (a nonzero falling, sliding or
queued-motion byte, or living state 27, 28 or 29) while the shared camera
transition word is zero. The actor’s query position supplies X, Y and Z: the
attached position when attr_slave is nonzero, otherwise the linked
position when the actor has one, otherwise the local position. The check
samples the terrain at the integer X and Z,
truncating the height toward zero, and compares it with the query Y shifted
right by eight plus the actor’s positive Y extent. When the sample is smaller
and the difference exceeds half of post_collide_height, the actor’s local
position takes its retained stable position (whether or not the
stable position is marked valid, leaving the mark
unchanged), move_counter clears and the fall start runs:
- Living state 15 leaves the actor unchanged.
- Living states 27 to 32, or queued motion 24, first set queued motion to one,
clear the living state and zero
move.vx,move.vyandmove.vz. - With queued motion nonzero, bit
0x10of byte0x1ceis set, the falling byte becomes one with the queued motion’s high bit kept, queued motion clears, the fall origin becomes the local Y plusattribute_32 × factor[30]and the fall timer becomes 30. The actor forces group 58 when the kept high bit is set, otherwise group 113 with a weapon held (unless the current group is 115), otherwise group 56. - With queued motion zero and a nonzero falling byte, bit
0x20of byte0x1ceis set and the falling byte becomes one with its high bit kept.
The bounce update follows the terrain check; an actor with no bounce state then runs the queued-motion resolution, which reports zero without queued motion. When the bounce update or the queued-motion resolution reports handled, the probe ends before the cheat bypasses and before any support query, so a handled queued update applies no support result. An unhandled result continues into the support probing with the actor as the resolution left it.
Terrain support sampling
The ground query converts actor coordinates to integer map units by arithmetic right shift by eight before sampling terrain. Fractional runtime coordinates therefore do not reach the terrain interpolator. This coordinate conversion is separate from truncating the resulting surface height toward zero.
Terrain cell selection rounds map_x / 256 - 0.5 and
map_z / 256 + 0.5 to the nearest integer, with ties going to the even integer.
Let the selected cell coordinates be i and j. Its four horizontal corners
are (256i, 256j), (256(i+1), 256j), (256i, 256(j-1)) and
(256(i+1), 256(j-1)). The diagonal joins the first and last corners.
If map_x - 256i is strictly greater than 256j - map_z, use the triangle
containing the second corner; otherwise use the triangle containing the third.
The surface height comes from that triangle’s plane. Its normalized face normal
is multiplied by 256 and rounded to nearest-even for the integer contact normal;
it is not an interpolated shading normal.
For interior terrain cells, runtime grid row j corresponds to WLD source row
256-j.
Near-camera queries use the current terrain vertex cache; queries more than
15 grid cells away on either axis use decoded heightmap values directly.
Ordinary actor collision-shape initialization
The ordinary actor loader copies the primary MPSZ record when an animation
model table and a nonnegative primary bounds index are present. A static-model
actor with an MPOB model_id other than -1 also takes the primary-record path.
The copied bounds supply the sphere builder with the actor’s signed RAEX radius
scale. The builder replaces the record’s orientation byte according to its
dominant-axis comparisons.
A static-model actor with model_id -1 derives its bounds from the loaded
model’s vertices. Positive and negative extents start at zero. Vertices are
visited in stored order; each component that extends the current signed extent
replaces it after nearest-even integer conversion. Negative extents then become
nonnegative magnitudes. The positive and negative X/Z extents are multiplied by
SYSTEM.INI sphere_object_scale and shifted right by eight; Y extents are not
scaled. A nonpositive system scale uses 256. Each total is the sum of its two
extents, and the center offsets are zero. The usual sphere builder then applies
the separate RAEX radius scale.
An actor with neither that static model nor an applicable primary-record path receives positive and negative extents of 12, totals of 24, zero center offsets, and one sphere centered at zero with radius 12. This bypasses the usual sphere builder. This initialization precedes the type-one placed-model fallback; loading a placement model afterward does not rerun collision-shape initialization.
Bounds record fields
The bounds record lives inside the actor record: an orientation byte followed by
the total, center-offset, positive and negative extents, each three signed
32-bit values in X, Y, Z order. The size.* script properties address these
fields: size.orientation, size.size.v* (totals), size.pos.v* (center
offsets), size.psize.v* (positive extents) and size.msize.v* (negative
extents). Actor creation fills the record from the model bounds and the sphere
builder then replaces the orientation byte.
Queries read the positive and negative extents from the record each time they run: the ground and hazard probes, the combat bounds and the actor box center (per axis, the position plus the positive extent minus the negative extent in map units, halved). A script write to an extent therefore changes every such query on the next evaluation. The sphere list is rebuilt only by the sphere builder at actor creation and model changes, so a write does not alter spheres already built.
The center offset is read by two routines. The residual body response sets the origin of the
first working record to the actor’s query position plus the offset, unrotated; the primary contact
collection reads the selected shape record’s own origin and applies no offset. The selected player’s
fallback support sphere adds the offset to its center. Sphere construction and the vertical ground
probe ignore it. Shipped files carry a zero offset, so only a script write to size.pos moves it.
The orientation byte’s only reader is the per-update shape copy, which fills two working records and
gives the second one extra sphere when the byte is zero (upright). No stage reads the second record:
its pointer is used only to allocate, free and fill it. A script write to size.orientation
therefore changes no query.
Query position
Ground, prevalidation, terrain-check, stable-recovery, residual-body and obstacle queries take their
position from one selector: the composed attached position when attr_slave is nonzero, otherwise the
linked record’s cached position while the actor’s link record names an owner, otherwise the local
position. The obstacle query’s start point uses the matching previous-position selector (previous
attached position, the link record’s previous cached position, previous local position). The
queued-motion vertical clearance and the blocked-ahead probe also start from the selector’s position.
The primary contact collection does not: the shape preparation sets the selected shape record’s origin
to the local position and the angles to the negated local angles, and the query reads that record.
Armed nonplayer wall avoidance
After the late collision passes, a nonplayer actor that holds a weapon or is in combat stance and has
a combat record runs a wall-avoidance pass. The pass is skipped unless a hold timer is running or the
collision-response word has one of the bits 0x10bef. A corner probe returns a four-bit mask: bit 1
the front point, 4 the back, 2 the right and 8 the left. Each point is the body’s corner at
(0, 0, positive_z - half), (0, 0, -(negative_z - half)), (positive_x - half, 0, 0) and
(-(negative_x - half), 0, 0), rotated by the actor matrix and added to the actor’s map-unit X and Z,
where the radius is (total_y - post_collide_height) / 4 and half is that radius halved. Each point
holds four stacked spheres of that radius, centered at -(negative_y - radius / 3) plus multiples of
the radius, tested with an identity orientation at the actor’s integer map Y. A nonzero mask is stored
and restarts the hold timer at 16; a zero mask lets the timer fall by the nominal ticks (clearing the
stored mask at zero) and the stored mask keeps steering meanwhile.
The pass starts a zero side at 2. Each set bit moves X and Z by frame_scale * 0xa00 >> 8 along the
heading (the translate-forward rule): bit 2 along heading plus 0x200 and sets the side to 8; bit 8
along heading minus 0x200 and sets the side to 2; bits 1 and 4, when neither bit 2 nor bit 8 is set,
first move along heading plus 0x200 when the side is 8 and minus 0x200 otherwise, then bit 1 along
heading plus 0x400 and bit 4 along the heading itself.
Living animation bounds selection
An ordinary actor with combat and living-animation records refreshes its spatial bounds before its default-state update. The current living animation’s group class selects the bounds. Class 2 uses the tertiary MPSZ bounds when the combat record has a tertiary shape. Class 1 uses the secondary MPSZ bounds when it has a secondary shape. Other classes, and missing shapes for the selected class, use the retained primary bounds.
The animated actor loader initializes all three retained bounds from the actor’s initial bounds, then replaces secondary and tertiary bounds for nonnegative second and third RAHD MPSZ indices. An index of -1 leaves the corresponding shape absent.
The refresh runs at the start of every ordinary update, before the scripts, and overwrites the whole
bounds record (extents, totals, center offset), so a script write to a size.* field of an actor with
combat and animation records lasts only until its next update; an actor without those records keeps the
written value.
Refreshing spatial bounds copies the bounds record without rebuilding the ordinary collision spheres. Support height and forward clearance therefore use the current animation’s extents while ordinary body contacts retain their initialized spheres, subject to the separate contact selector below.
Contact body-shape selection
Contact queries use the actor’s initialized ordinary body shape unless its current living animation belongs to class 2, a combat record exists, and that record has a tertiary MPSZ shape. That combination selects the tertiary shape. Fighting class 1 does not select the tertiary shape. An absent tertiary shape falls back to the ordinary shape.
The animated actor loader builds the tertiary shape from the third RAHD MPSZ index using the signed RAEX radius scale. A third index of -1 leaves that shape absent. Combat-record reinitialization preserves this shape. The selector updates the chosen shape’s whole-map translation and wrapped inverse angles from the actor’s current effective pose before returning it.
Upright actor collision spheres
For bounds whose X and Z totals are not strictly greater than both other totals,
the ordinary collision shape is a vertical series of spheres. Its unscaled
radius is half the larger X/Z total, truncated, with a minimum of 4 map units.
The sphere spacing is three times half that radius, with the division performed
first. The available height is total Y minus SYSTEM.INI post_collide_height;
a negative available height is replaced by post_collide_height. The sphere
count is the available height divided by the spacing, truncated, with a minimum
of two.
The radius scale comes from the signed RAEX field at 0x12. Nonpositive scales
select 256; the scaled radius is the integer product of radius and scale shifted
right by eight. All spheres have X and Z centers zero. All except the last start
at Y = radius minus the negative Y extent, increasing by the spacing. The last
center is positive Y extent minus post_collide_height minus scaled radius.
For ISLAND Cyrus, totals (48, 128, 49), positive Y extent 62, negative Y extent
66, post-collision height 50 and the default radius scale produce two radius-24
spheres at local Y −42 and −12. An additional support sphere is stored separately
and is excluded from the ordinary obstacle-query count. X- and Z-dominant shapes
use different construction rules.
Horizontal actor collision spheres
An X total strictly greater than both Y and Z selects the X-dominant shape. Otherwise, a Z total strictly greater than both X and Y selects the Z-dominant shape. Every tie selects the upright shape.
The horizontal shape starts with radius max(4, max(other totals) / 2), using
signed integer division. If post_collide_height < 2 * radius, subtract half
post_collide_height from the radius, clamp it to at least 4, and set the Y
center to that radius minus the negative Y extent. Otherwise retain the radius
and set the Y center to negative post_collide_height.
The spacing is (3 * radius) >> 1. The ordinary sphere count is the dominant
total divided by spacing, with a minimum of one. The stored radius is
(radius * scale) >> 8, using the same scale default as the upright shape.
For X-dominant bounds, every ordinary sphere has the same center
(radius - negative X extent, Y center, 0). The loop advances a separate
accumulator without applying it to the stored X centers. For Z-dominant bounds,
centers start at (0, Y center, radius - negative Z extent) and advance in Z by
the spacing. Both horizontal shapes store one additional sphere copied from the
first ordinary sphere; their ordinary query count excludes this copy.
The upright shape’s additional sphere has center
(0, positive Y extent - scaled radius - (standing_height << 8) - 2, 0) and the
same scaled radius. This expression uses the runtime-scaled standing-height
value directly, while the stored sphere center is a map-coordinate value.
Per-update collision shape preparation
An actor’s selected collision shape receives its current position before the
ordinary update copies the shape into working buffers. Each signed runtime
coordinate is arithmetic-shifted right eight bits, then stored as a
single-precision map coordinate. Each orientation component stores the integer
(-angle) & 2047; these angle fields are not floating-point values.
Collision queries rebuild their orientation from these angle fields, rather
than using the actor’s retained presentation matrix. Let sp, sy, and sr
be shared-table sine samples at the wrapped negative pitch, yaw, and roll;
cp, cy, and cr are the samples 512 entries later. The collision matrix
has these rows:
| Row | X | Y | Z |
|---|---|---|---|
| 0 | sp * sr * sy + cr * cy | -sr * cp | sr * sp * cy - sy * cr |
| 1 | sr * cy - sp * sy * cr | cr * cp | -sr * sy - cr * sp * cy |
| 2 | cp * sy | sp | cp * cy |
Each resulting component is stored at single precision. Multiplications and
additions retain extended intermediates until that store. For an upright
actor this matrix maps local forward to (-sy, 0, cy), unlike the retained
actor matrix’s (sy, 0, cy) mapping. The primary body query uses the selected
shape’s current local coordinates and angles. Residual response uses the first
working record’s copied angles, with its origin refreshed from the actor’s
current effective map position.
An actor whose definition carries vertex-bound collision records (RAVC) selects a shape
rebuilt from them instead of its initial shape: one sphere per record, in record order. Each center
axis is the record’s model vertex divided by 256, truncating toward zero, plus the record’s signed
offset on that axis, and the radius is the record’s radius. The vertex is read in model units from the
actor’s current animation frame when the actor has an animated model source, otherwise from its
base model. The rebuilt spheres then follow the position and orientation preparation above.
The ordinary update copies the selected shape into two working records. For upright bounds, the second record’s sphere count is increased by one, including the additional stored support sphere. The first record retains the ordinary obstacle count. X- and Z-dominant bounds retain the same count in both records. If the actor has no selected shape, this preparation uses the saved selected-player shape and the selected player’s bounds orientation instead.
Static collision list order
Static models use the same eighteen-cell spatial window as the
placed-actor update list.
The four corner cells are omitted. Cells are visited by increasing X within
increasing Z rows, and each cell’s static list retains its existing order.
For nonnegative map coordinates, static placements are assigned to cells by
their X and Z coordinates divided by 512 with integer truncation. Within a
cell, placements occur in reverse
MPSO record order. The resulting lists are concatenated in that order before
model-distance selection. Actor-only border eligibility does not remove static models.
For each horizontal center coordinate, the starting map coordinate is zero
below 4,608; it is 56,319 when the center plus 4,608 exceeds 65,535; otherwise
it is the center minus 4,608. Divide that starting coordinate by 512 to obtain
the first cell. The upper-edge start therefore occupies cell 109.
Model-distance candidate test
An ordinary actor’s collision query converts its runtime origin to integer map units by arithmetic right shift by eight. If the actor has a model, the search radius is twice the model radius, truncated to an integer. Without a model, the search radius is 200 map units. The actor’s own model is excluded.
A model passes the distance test when the squared distance from the query origin to the model placement is no greater than the square of the sum of the query radius and the model radius. Model placement coordinates and radii are floating point. The coordinate differences, squared-distance sum, and combined radius are stored at single precision before the final comparison. Equality passes.
Terrain material lookup
Terrain material queries use integer map X/Z converted to single precision.
Multiply each coordinate by 1/256, subtract 0.5 from X and add 0.5 to Z,
store both results at single precision, and round each to the nearest integer
with ties to even. Valid grid columns are 0 through 254; valid rows are 1
through 255. Outside that range the lookup returns -1.
For valid cells, runtime row j reads WLD Map 3 source row 256-j.
The returned byte retains the low-six-bit texture index and high-two-bit
rotation. Surface lookup uses texture set 302 and that texture index before
applying terrain blend-mask and water overrides, as described in
sound surface selection.
Terrain material blend coverage
Material lookup derives local mask coordinates from integer map coordinates:
u = (X >> 1) & 127, v = 127 - ((Z >> 1) & 127). The tile’s rotation
selects (u,v), (127-v,u), (127-u,127-v), or (v,127-u) for rotations
zero through three respectively. Call the rotated coordinates a,b.
The generated 256-by-256 mask has value
clamp(2 × (x + y - 254), 0, 255) at (x,y). Terrain image indices select
these coverage rules:
| Image | Coverage |
|---|---|
| 0 | 256 |
| 5 | Mask at (a+109,b+109) |
| 6, 52 | Zero for a <= 28; ((a-28) × 455) >> 7 for 29 <= a < 100; 256 otherwise |
| 7 | Mask at (a+32,b+32) |
| 30 | Mask at (a+128,b+128) |
| 31 | Zero for a < 5, otherwise 256 |
| 32 | Zero when mask at (a+64,b+64) is zero, otherwise 256 |
| Other indices | Zero |
Start with the surface assignment for texture set 302 and the image index. Dry pixels on images 6 and 52 select sand, and dry pixels on images 31 and 32 select rock. On the other mask-backed images, zero coverage replaces a water assignment with sand; a deepwater assignment also becomes sand before the water override. Other image indices retain their base assignment.
After these rules, nonzero coverage selects surface 3, except coverage 256 selects surface 4. Coverage 255 is therefore distinct from full coverage.
Static candidate list separation
Static collision flag 2 excludes a placement before distance selection. Flag 1 routes an admitted placement to the actor-style list; other admitted placements go to the face-model list. Each list holds at most 64 entries. A full actor-style list skips further actor-style entries and continues the static scan. Encountering another admitted face model when the face-model list is full ends the static scan, including any later actor-style placements.
Ground support reads the face-model list, both for its primary segment query and its selected-player fallback sphere query. It does not query the separate actor-style list. This separation does not exclude actor-style contacts from other collision responses. The ordinary forward obstacle probe also reads only the face-model list and reports blocked when any queried model produces a sphere contact.
Each static face candidate retains its collision flag byte as support metadata. When primary or fallback support selects that candidate, the selected support receives that byte unchanged. Terrain selection instead sets support metadata to zero.
Live and transient collision candidates
Candidate construction visits prepared static placements first, ordinary live actors second, and transient models last. The face-model and actor-style lists each retain their separate limit of 64 entries across these phases.
The ordinary actor exclusion test rejects the unselected member of the two player roles. It admits the selected player and actors outside those roles. Selection zero chooses the first role; every nonzero selection chooses the second. The remaining candidate gates still apply to the selected player.
A live actor is admitted only when its ordinary actor exclusion test passes, its runtime animation-update exclusion is zero, its loaded model exists and differs from the querying actor’s model, attribute 55 is zero, and the model distance test passes. A live actor using the static model load path enters the face-model list with its actor owner and collision flags. Other admitted live actors enter the actor-style list with their actor owner. A full destination list skips that entry. When both lists are full, traversal returns before processing later live actors or transient models.
Whole-map preload links all rope records into the active transient list. When the clamped starting cell of the ordinary map window changes, the engine rebuilds that list. It admits a rope when the sum of the absolute X, Y, and Z differences between the published camera position and the rope base is strictly less than 4096 map units. The comparison uses signed Q8 positions and does not use the swung terminal position. Camera movement that leaves the starting cell unchanged retains the list.
An active transient record with a loaded model and flag 0x10 enters the
face-model list until that list is full. These entries have candidate type
0x80, zero collision metadata, and neither a static-placement owner nor an
actor owner. They do not run the static-placement distance filter.
Actor sphere overlap
An actor does not overlap itself. Actor overlap uses each actor’s selected runtime shape, or one model-radius sphere for an actor using the static model path. At most the first 32 spheres of each shape participate. Local sphere centers receive the actor matrix transform and its shape origin truncated to integer map coordinates.
Sphere pairs are visited in shape order. The actor-to-actor query truncates the radius sum and each center difference toward zero. The distance calculation stores integer differences as single-precision values, sums their squares, computes the square root, stores that result at single precision, and converts it to an integer with nearest-even rounding. A pair overlaps only when this three-dimensional distance is strictly smaller than the truncated radius sum. The first overlapping pair returns that sum minus the rounded horizontal distance, with the vertical difference set to zero for that second distance. When no pair overlaps, the query returns zero.
An actor-style static placement contributes one sphere at its stored placement position, with the loaded model radius. The actor-to-placement query truncates each sphere radius separately toward zero before adding them. Both queries truncate center differences before the distance calculation and return horizontal separation from the first overlapping pair.
Weighted actor overlap response
For an ordinary actor, a nonzero attr_weight supplies mobility
256 - attr_weight; weight zero supplies mobility zero. Actor overlap
response excludes actors with nonzero attribute 26.
The selected player has zero overlap mobility while either the shared player lineup result or the player lockout word is nonzero, regardless of attribute 6. This override applies both when the player owns the pass and when another actor queries it. It does not change the stored weight attribute.
When an immovable response would restore the updating actor’s previous position, a selected player with a nonzero shared lineup result keeps its current position instead. Lockout alone does not suppress that restoration. These rules apply to both actor-pair and actor-style static-placement responses; small actor-pair contacts still mark contact and return before weighted response.
Grounded living states use the same weighted actor and static-placement overlap response as ordinary standing, including weapon drawing and sheathing, states 20 and 21. They do not bypass overlap queries. The grounded state exception is 27 through 32: an actor-pair response skips the updating actor’s current support owner after the small-contact check. Other actor pairs still use weighted separation, and this exception does not change static-placement response.
An actor-to-actor separation below three map units marks contact on both
actors and ends the overlap pass without displacement. For larger separation,
when both mobilities are zero the updating actor returns to its previous
position. Otherwise the divisor is the sum of mobilities when both are
nonzero, or the nonzero mobility plus 256 when one is zero. The integer factor
is 131072 / divisor. Each movable actor’s Q8 displacement distance is
(factor * mobility * separation >> 9) + 255, with wrapping signed 32-bit
multiplications and an arithmetic right shift. The actors move in opposite
horizontal directions. A zero-mobility actor is not displaced.
An actor-style static placement does not share this divisor. An updating
actor with zero mobility returns to its previous position. Otherwise the
factor is 131072 / mobility and its Q8 displacement distance uses the same
multiply, shift and addition expression. Static-placement separation below
three still takes this response; the small-contact early return applies only
to actor pairs.
For a positive actor-pair overlap, collision-attribute bit 3 on the other
actor marks that actor’s body-response bit 0x04 before displacement handling.
A separation below three marks body-response bit 0x04 on both actors and
stores each actor’s low-word runtime identifier on the other. Weighted actor
separation marks the updating actor’s body-response bit 0x04 and stores the
other actor’s identifier. Both-immovable rollback does not add that mark to the
updating actor. Existing result bits are preserved.
Weighted static-placement separation marks body-response bit 0x04 on the
updating actor and stores contact identifier zero. Its immovable rollback does
not add a result mark. Separation translates only X and Z, preserving Y. The
updating actor moves away from the other origin; a movable other actor moves
in the opposite direction. Coordinate addition is stored at single precision
and then truncated toward zero, using the shared heading-translation rule.
Overlap marks share the body-response byte with the bounds and forward responses. The primary and residual responses write their result bits to the step-status byte instead (see the collision-response word). None of them write the collision-transition byte.
An actor-pair overlap with a separation of three or more takes a landing branch
before any weighted separation when the updating actor’s falling state is
nonzero, the other actor’s attribute 98 (attr_bounce_power) is nonzero, and
the absolute difference between the actor’s Q8 Y and its retained fall origin
is at least SYSTEM.INI [game] fall_bounce_height times 256 and the actor’s
positive Y extent is greater than zero. The branch
attempts to land the actor on the other actor and ends the overlap pass with
body-response bit 0x80 set whether or not the landing is admitted; without
these conditions the weighted response applies. The landing is not admitted when
the actor is in living states 10 through 19, 27 or 28, or its surface is
deepwater or scapedeepwater, or the other actor’s model link is empty, and then
changes nothing else. An admitted landing makes the other actor the
actor’s support, clears the actor’s sliding, falling and queued-motion bytes
and its two retained motion words, sets living state 27, requests animation
group 10 on the other actor, and requests the landing animation group on the
actor: 114 when the actor holds a weapon (attribute 110,
attr_holding_weapon, nonzero, or the drawn-weapon test), otherwise 66 when its
burning-torch byte is nonzero, else 65. The support’s group 10 need not exist
for landing admission. Its request follows the ordinary animation fallback
rules; catalogue membership does not replace the live model-link check.
A landed actor then bounces off its support through living states 27, 28 and 29. Each update returns zero without moving the actor when the support reference is empty, the support has no animation or model link, or the support’s attribute 98 is zero; it then clears the support reference and the living state. Otherwise:
- In state 27, while the support’s current group is 10, its
anim.stop_flagis zero and itsanim.framecommand cursor is below half of the group’s command count (halved and truncated toward zero), the actor’s sliding, falling and queued-motion bytes and its two retained motion words are cleared and the update reports handled. After that half the state becomes 28 with a lift ofattribute_32 × factor[0] × 16, kept in the fall-origin dword. - In states 27 to 29, once past the wait above, an actor that is the selected
player with a support whose attribute 100 (
attr_bounce_turn) is nonzero first subtracts the ground-turn delta ofBounceObjectfrom its heading, modulo 2048. The actor then moves forward along its heading by(128 − support attribute 99) × 256 / 3, frame-scaled. - In state 28 the lift, frame-scaled, is subtracted from Y and decays by
(256 − support attribute 98) × 16, frame-scaled. While lift remains the actor requests group 112 with a weapon held or the combat stance bit0x04set, 38 with a burning torch, otherwise 39, clears its sliding, falling and queued-motion bytes and the update reports handled. When the lift is spent the fall origin becomesY + attribute_32 × factor[30], the fall timer 30 and the falling byte 1; the actor forces group 113 with a weapon held or the combat stance bit0x04set (unless the current group is 115), 57 with a burning torch, otherwise 56, and the state becomes 29. - A zero falling byte after these steps (the actor has landed) clears the living state and the support reference.
States 30 and 31 belong to BounceObject and report
handled here; state 32 reports unhandled.
Living states
The living-state byte (extra_flag) holds the actor’s current special
action. Zero is ordinary movement. The engine writes the values below; a
script can write any byte through extra_flag.
| State | Meaning | Owner |
|---|---|---|
| 0 | Ordinary | |
| 10 | Turning to a high ledge | Ledge transition |
| 11 | High climb | Ledge transition |
| 12 | Turning to a low ledge | Ledge transition |
| 13 | Low climb | Ledge transition |
| 14 | Catching a hang | Ledge transition |
| 15 | Hanging | Ledge transition |
| 16 | Climb from hang | Ledge transition |
| 17 | Dropping | Ledge transition |
| 18 | Climb finished | Ledge transition |
| 19 | Entering a ledge | Ledge transition |
| 20 | Drawing a sword | Animation |
| 21 | Sheathing a sword | Animation |
| 27 | Landed on a bouncing actor | Actor bounce, above |
| 28 | Bouncing up from an actor | Actor bounce |
| 29 | Falling from an actor bounce | Actor bounce |
| 30 | Bounce entry requested | BounceObject |
| 31 | Bounce rising | BounceObject |
| 32 | Bounce falling | BounceObject |
| 33 | Lighting a torch | HUD |
| 34 | Dousing a torch | HUD |
Jump motion values
The queued-motion byte (jump_flag) and the falling byte (fall_flag) share
one code. Its low seven bits name the jump; bit 0x80 selects the
raised-arms groups in queued motion arbitration
and forces group 58 when queued motion transfers to falling. The engine copies
the queued byte to the falling byte when the jump peaks, and starts a plain
fall as falling 1. It preserves bit 0x80 in these transfers but never sets
it; a script can write any byte through jump_flag and fall_flag.
| Value | Meaning | Written by |
|---|---|---|
| 0 | None | |
| 1 | Standing jump, or a plain fall | Ordinary jump entry, fall starts |
| 2 | Forward jump | Leap links (SOUP) |
| 3 | Backward jump | Jump key while moving backward |
| 8 | Running jump | Ordinary jump entry from group 18 or 30 |
| 9 | Running jump, left lead | Ordinary jump entry from group 18 or 30 for requested motion 9 |
| 24 | Rope jump | Nonzero runtime jump selector |
The overlap pass visits its retained actor-style candidate list in order. Later queries read the positions left by earlier responses. If any weighted separation occurred and the loop completes, the forward obstacle response runs again. A small actor-pair contact returns immediately and skips this repeat, even when an earlier candidate caused weighted separation.
Ordinary forward segment response
The ordinary motion response probes a segment from the previous effective
origin to the current effective origin. Both endpoints are shifted from Q8
to integer map units and receive the same caller-supplied vertical offset.
It visits retained face-model candidates in order. The ordinary segment path
uses the enabled polygon-sidedness flag. The model-query argument selects
early return after subobject admission; it does not set polygon sidedness.
Queued motion 24 excludes candidate type 0x80.
World contact points are converted to integers with nearest-even rounding. World normals are multiplied by 256, stored at single precision, and converted to integers with nearest-even rounding. Neither conversion uses the radius truncation rule. The stored contact rank is unchanged.
The motion heading comes from the horizontal current-minus-previous effective position, using wrapping integer differences and the shared quantized heading lookup. A normal’s facing heading is its horizontal displacement heading plus 1024, wrapped to 2048. Subtract the motion heading; differences below -1024 add 2047, and differences above 1024 subtract 2047. Only differences in the inclusive range -511 through 511 accept the contact.
Accepted facing contacts contribute their integer world normals in query order. An empty normal accumulator takes the next normal unchanged. Otherwise, the two normals are added, normalized to Q16, and shifted right by eight to produce the new accumulated normal. This ordered combination is not an arithmetic average of all contacts.
The chosen contact point comes from the smallest rank & 0x7fff among accepted
contacts. Equal ranks retain the earlier point. Normals from later accepted
contacts still contribute even when their point does not replace it. A
response-disabled query reports contact without changing position. A
response-enabled query truncates the first selected sphere radius toward zero
and writes X and Z as point * 256 + accumulated_normal * radius, preserving Y.
Forward obstacle wrapper and motion transition
The wrapper first runs a responding forward query at vertical offset zero.
A hit marks body-response bit 0x08, then runs the motion transition unless the
enabled selected-player auto-grab path handles it. If queued motion or falling
is nonzero, it next runs a nonresponding query at
positive_y - (post_collide_height >> 2) and transitions on a hit. Finally it
runs a responding query at positive_y - post_collide_height; a hit marks
0x08 and uses the same auto-grab exception before transitioning.
Living state 15 bypasses the motion transition. Living states 27 through 32,
or queued motion 24, first replace queued motion with 1, living state with 0,
and clear the retained integer motion vector. With queued motion zero, a
nonzero falling byte marks 0x20 and becomes (falling & 0x80) | 1.
Nonzero queued motion marks 0x10, sets falling to (queued & 0x80) | 1, and
clears queued motion. It records fall-origin Y as the current logical Q8 Y plus
256 * attribute 32, and sets the byte fall timer to 30. The high falling bit
selects forced animation group 58. Otherwise, holding a weapon selects group
113 unless that group is already current; not holding selects group 56.
These are forced requests with modifier zero, not changes to the default group.
The animation-system and static-model gates still apply to the request.
The 0x08, 0x10, and 0x20 marks belong to the body-response flags,
together with overlap mark 0x04, independently of rollback transition flags.
The wrapper’s auto-grab switch is enabled only when [cyrus] auto_grab equals
- It delegates to the selected-player grabbing path before the normal motion transition; it is not a general obstacle rollback switch.
State-fifteen bounds response
Living state 15 replaces each forward motion probe with two bounds segments
and returns no forward-motion hit. These segments use the retained actor matrix
and current effective origin shifted to integer map units. The local endpoints
are (positive_x, positive_y + 2, positive_z) to
(negative_x, positive_y - 2, negative_z), and
(negative_x, positive_y + 2, positive_z) to
(positive_x, positive_y - 2, negative_z). Both bound records contain the stored
nonnegative extent magnitudes; this query does not negate the negative extents.
The query disables polygon sidedness, visits face-model candidates in retained
order, and tests both segments for each candidate before visiting the next.
Queued motion 24 excludes candidate type 0x80. Integer world contacts qualify
only when their normal Y component is between -41 and 160 inclusive.
Each segment tracks its own greatest negative rank and smallest positive rank,
initialized to -999999 and 999999. Rank zero does not contribute. A qualifying
contact contributes its full integer normal only when it strictly improves one
of those bounds. Earlier contributions remain in the sum when later contacts
improve a bound. The sum is not normalized. A nonzero sum adds directly to all
three logical Q8 position components and marks body-response bit 0x40.
The polygon-sidedness flag is restored to enabled after the query.
Sphere model admission
A sphere query first tests each body sphere against the model radius. Express the displacement from the actor origin to the model placement in actor-local axes, then subtract it from the local sphere center. The sphere passes when this squared displacement is no greater than the sum of the squared model radius and squared sphere radius. This is not the square of the sum of the radii. Only admitted spheres proceed through the actor/world/model transforms to the subgroup and polygon tests.
Collision subobject admission
The model runtime loader retains authored subobjects only for version 5 models. For other model versions, including version 4, it clears both the subobject list and its count. Version 4 subobjects therefore do not participate in these queries; the loader does not synthesize extent factors for them.
For a sphere center in model coordinates, let d be its displacement from a
subobject center, R the subobject radius, e its three extent factors, and r
the sphere radius. For nonzero length(d), form each component of
d × R / length(d) × e and store it at single precision. The subobject passes
when the squared displacement is no greater than the sum of those component
squares plus r². This is not a test against the subobject radius alone.
Segment model admission
Before visiting subobjects, transform both segment endpoints into model-local coordinates and test the model radius around the model origin. An endpoint inside or on the radius passes. When both endpoints are outside, use the same bounded segment/radius test described below, without extent scaling. Here the projection may be zero: it must be nonnegative and no greater than the squared segment length. The minimum squared length is the single-precision representation of 0.1, with equality accepted.
Segment subobject admission
For segment queries, subtract the subobject center from each model-local
endpoint and multiply each component by the reciprocal of its corresponding
extent factor. The reciprocals and scaled endpoint components are stored at
single precision. Let these scaled endpoints be a and b, and let r be
the subobject radius.
An endpoint inside or on the radius admits the subobject. When both endpoints
are outside, set d = a - b, L = d · d, and P = a · d. Admission requires
all of the following:
Lis at least the single-precision representation of 0.1.Pis greater than zero and no greater thanL.(a · a) × L - P × Pis no greater thanr² × L.
The squared start distance, squared radius, L, and P are stored at single
precision before their later comparisons. This test differs from sphere-query
subobject admission.
Admitted subobjects visit their face references in authored order. Segment queries retain repeated successful face references; the successful-face suppression used by sphere queries does not apply here. Each model query stops after its first 32 successful contacts, including repeated references.
Collision face traversal
A sphere-to-model query uses the model’s authored subobject face lists. It visits actor spheres in order. For each admitted sphere, it visits admitted subobjects in their stored order and then their face references in stored order. Faces omitted from those lists do not participate in this query.
The query tracks faces that have produced a contact. Once a face succeeds, later references to that face are skipped for the remainder of the model query, including references reached by later spheres. A failed test does not mark the face: another subobject reference or another sphere may test it again. Clearing the accepted-face set between spheres, or marking every attempted face, changes this behavior. Returned contacts retain the order of first successful tests.
Selected-player fallback support
When the primary terrain/model segment query finds no support, only the designated player receives a secondary sphere query. Its world-space center is the actor’s position shifted right by eight bits plus the collision center offset. Add the positive Y extent and subtract three from the center’s Y. The radius is 10 map units when the shared support-history counter is zero, and 14 when that counter is nonzero. The query uses an identity actor rotation.
During the designated player’s admitted update, before ordinary animation and
movement handling, the support-history counter decreases by the supplied nominal
tick increment, saturating at zero. Other actors do not perform this decrement.
After a designated-player support query, support metadata with mask 0x04 set
resets the counter to 12. This also applies to support queries made while probing
a temporary forward position; restoring the position does not undo the counter.
After the per-candidate actor-state exclusion, this fallback visits face-model candidates in order, then each model’s returned sphere contacts in order. It accepts the first contact whose normal classification is not side surface (1), including underside (8). It retains that contact’s model, normal, texture identity and support metadata, and returns the actor’s existing runtime Y unchanged. It does not move the actor to the contact’s projected height or rank the fallback contacts by distance. If no such contact exists, support remains absent.
For both primary and fallback model support, a steep classification (2) becomes ordinary ground (4) in the stored support state when collision-attribute bit 2 is clear. The geometric normal itself is unchanged.
Support-query misses
A support-query miss clears the selected model, source kind and classification, and sets the returned actor height to the no-support sentinel. It retains the previous contact normal, texture identity and support metadata byte. These retained fields do not constitute a valid contact when the source kind is zero.
The designated-player wrapper copies the result and tests its support metadata
for the support-history reset even when the query returned no support. A retained
metadata byte with mask 0x04 therefore resets the counter to 12 after a miss.
Ground-support ranking
The ordinary vertical support segment is centered on the actor’s integer map
Y plus its positive Y extent, extending the supplied search distance R in both
directions. Selection begins with rank 65,536 and no support. Terrain below
the lower end of this segment is not a candidate. Terrain above the upper end
receives rank 65,535. Terrain within the segment receives
(truncate(16777216 / (2 × R)) × (truncate(terrain_Y) − segment_start_Y)) >> 8.
Queued motion 24 excludes candidate type 128 before the primary support model query. The selected-player fallback applies the same exclusion.
Model contacts with normal classification 2 or 4 compete in candidate and contact order. A contact replaces the current support when its stored segment rank is less than or equal to the current rank, so a later equal rank replaces an earlier one. Model segment ranks use the 32,768 scale described below; terrain’s rank calculation is retained separately.
Segment contact with model faces
Ground probes intersect the original model polygons. In model coordinates,
let A and B be segment endpoints, V a face vertex, and N the stored face normal.
The signed endpoint distances are (V − A) · N and (V − B) · N.
When collision sidedness is enabled, an A distance greater than 4 rejects the
face. A crossing requires one endpoint distance to be negative and the other
nonnegative. A segment lying entirely in the plane is rejected.
The intersection fraction is the A distance divided by the difference between A and B distances. The intersection must pass the same oriented polygon-edge inside test as a sphere’s projected contact. The model placement transform returns the intersection and normal to world coordinates. The stored fraction uses a scale of 32,768 and nearest-integer conversion. The integer ground-probe adapter rounds world coordinates and normals multiplied by 256; it retains the stored fraction unchanged.
Sphere contact with model faces
The forward obstacle query converts the proposed actor origin to integer map units by arithmetic right shift by eight. Actor orientation transforms each local collision-sphere center; the model’s inverse placement transform then expresses the center in model coordinates. The face test uses the model’s stored face normal and polygon vertices, rather than a rendered triangle mesh.
For a sphere center C, radius r, a face vertex V, and face normal N, the signed
plane distance is (V − C) · N. When collision sidedness is enabled, a distance
greater than 4 map units rejects the face. Independently, a squared distance
greater than r² rejects it. The candidate contact point is the perpendicular
projection C + N × distance onto the face plane.
A face passes the remaining test when any polygon vertex is within the sphere,
any polygon edge segment intersects the sphere, or the projected contact lies
inside every oriented polygon edge. Boundary equality counts as contact.
For the inside test, each consecutive vertex pair A/B must satisfy
((A − contact) × (B − contact)) · N >= 0. A face that passed for one sphere
is not added again for another sphere in the same model query.
The model body-query adapter rejects working shapes with more than 24 spheres. During a model query, a subobject face scan stops when the successful-contact count reaches 32. Reaching that count is not a query failure: the returned contacts still pass through the world-coordinate integer adapter and the primary collector’s separate sixteen-contact limit.
The reported contact remains the plane projection even when a vertex or edge intersection accepted the face. The model placement transform returns the contact point and normal to world coordinates. The obstacle probe needs only whether any model reported a contact; it does not move the actor to that point.
Body-contact integer conversion
After transforming sphere contacts to world coordinates, the adapter converts contact positions to nearest integers in map units. Each world normal component is multiplied by 256, stored at single precision, then converted to a nearest integer under the default nearest-even rounding mode. It does not normalize the transformed normal again.
The ordinary body query copies these integer normals into its ordered contact list and classifies their integer Y components using the ground-normal thresholds above. Classification therefore follows world transformation and integer conversion; it is not the source polygon’s local classification.
Ordinary candidate lifetime
An admitted ordinary actor prepares its collision candidate lists before the main script wrapper. Later support, primary body and residual face queries reuse those lists. Movement and script changes do not rerun spatial admission between these stages. Query origins still follow the owning stage’s current actor position; retained candidate membership is separate from that position.
Primary body-contact collection
The primary body query retains contacts in candidate order and each model’s returned contact order. Collection returns immediately after its sixteenth model contact. Later candidates and the optional terrain contact are then omitted. When fewer than sixteen model contacts were collected, an available terrain contact is appended after them. This limit belongs to the primary body query; the residual separation query is separate.
The ordinary actor update clears the shared extra-contact append flag before animation-default and movement processing. The primary collector appends the retained normal only when that flag is set. Heightmap presence alone does not admit this extra contact. Ordinary terrain height/support queries are separate from this retained body-contact normal.
Body-contact facing admission
The primary body-response loop visits contacts in stored order. Contacts whose stored classification is ordinary ground (4) do not enter wall response. For other contacts, it computes the opposing normal heading by adding 1024 to the normal’s quantized actor heading and masking to 2047. Subtract the movement heading. Differences below −1024 add 2047; differences above 1024 subtract 2047. A contact faces movement only when the corrected difference is strictly greater than −512 and strictly less than 512. The 2047 correction makes geometric quarter-turn boundaries asymmetric: an uncorrected difference of 1536 becomes −511 and passes, whereas 512 remains 512 and fails.
The movement heading comes from the current-minus-previous X/Z displacement. When both horizontal coordinates are unchanged, the actor’s current heading is used instead. The response retains this heading across the contact loop; position or facing changes from earlier contacts do not recompute it.
Horizontal body-contact response
Body contacts are processed after the movement and ground-support response. The ordinary update runs this response only for living states 0, 15 and 27 through 32; other living states skip primary body response. The response uses the displacement from the previous position to the attempted position, including its vertical component. It normalizes that displacement to length 65536. The length divided by 65536 is stored at single precision before dividing the displacement components. Each quotient is stored at single precision before nearest-integer conversion.
For an eligible facing wall contact, the horizontal dot product is
(movement_x × normal_x + movement_z × normal_z) >> 8, using the normalized
movement and the contact’s integer normal. The comparison threshold is
SYSTEM.INI slide_range, which is −60000 in the installed configuration.
When the dot product is strictly greater than the threshold, the response
subtracts the normal component from the normalized horizontal movement. Each
projected component is shifted arithmetically right by four and added to the
previous X or Z position. Otherwise, an actor outside the sliding state restores
its previous X and Z. This step leaves Y unchanged.
Body-contact motion states
Before the dot product, each contact that passes facing admission first checks
the enabled auto-grab path. When the [cyrus] auto_grab switch equals 1 and the
selected-player grabbing path handles the contact, the whole primary body
response ends at once: later contacts, residual separation and the rest of the
response are skipped. Otherwise the contact runs the
motion transition and then
the dot-product test. The transition runs once per admitted facing contact,
whether that contact then projects or rolls back:
- A falling actor keeps falling as
(falling & 0x80) | 1and receives the0x20mark. - Queued motion becomes falling, with the
0x10mark, fall origin, timer 30 and forced animation group of that transition. - Living states 27 through 32, and queued motion 24, first become queued motion 1 with living state 0, then start falling the same way.
- Living state 15 is unchanged.
The response never changes Y, the fall origin or the fall timer itself. The heading-adjustment branch tests the motion bytes after this transition.
While the sliding byte is nonzero, contacts whose candidate type is 64 are
skipped before classification and facing admission. A sliding actor still
projects a contact above slide_range, but a contact at or below it leaves the
position unchanged and sets no body-result bit.
The response counts the retained contacts minus the type-64 contacts it skipped while sliding. After the response stage, the ordinary update clears a nonzero sliding byte when that count is nonzero. The update performs this test even when the response did not run for this actor; the count then remains from the most recent primary body response.
Grounded wall-facing adjustment
After a projected wall response, an actor adjusts its heading only when its queued, falling, living-state and sliding bytes are all zero and its current X, Y or Z position differs from its previous position. The starting heading for this calculation is the attempted displacement’s quantized heading, not the actor’s current facing. Convert the contact normal to the opposing actor-facing heading by adding half a turn to its negated quantized heading and masking to 2047.
Subtract the movement heading from that normal-facing heading. Differences below −1024 add 2047; differences above 1024 subtract 2047. A positive difference subtracts up to 32 from the movement heading; a negative difference adds up to 32. Zero leaves the movement heading unchanged. This difference correction uses 2047, unlike the ordinary 2048-unit turn wrap.
Separately compare the actor’s pre-response heading with the movement heading, masking both to 2047 and wrapping their difference to the interval −1024 through 1024 using a full 2048-unit turn. When the absolute difference is greater than 512, add 1024 to the adjusted heading and mask the result to 2047.
The response stores body-result bits independently of collision attributes. A projected wall contact sets bit 1 when the ordinary heading-adjustment branch runs; otherwise it sets bit 2. Horizontal rollback also sets bit 2. Each mark preserves the other result bits. Ordinary state 15 still projects or rolls back horizontal movement, but does not enter the state-zero heading adjustment. A falling actor, or one that the motion transition has just made falling, also sets bit 2 for a projected contact and never adjusts its heading.
Residual face-contact separation
After the primary body response and the later overlap stage, the actor queries
the prepared face-model list again, including admitted live models.
This residual query has no primary sixteen-contact cap and does not append a
terrain contact. It retains each model query’s thirty-two-contact buffer limit.
Queued-motion state 24 excludes candidate type 128 from this scan.
The separation distance is the current horizontal displacement length shifted
right by eight, with a minimum from SYSTEM.INI poly_push_units. To obtain
that length, first store each integer X/Z displacement component at single
precision. Sum their squares at extended precision, store the sum at double
precision, take its square root, and store the result at single precision.
Convert that result to an integer using the default nearest-even rounding mode
before shifting. The displacement is measured after primary body response. The installed
minimum is 16. For remaining contacts, sum their integer normals, divide each
component by the contact count with truncation toward zero, and normalize the
result to length 65536 using the body-response normalization above. Shift each
normalized component arithmetically right by eight. Add its X and Z components
multiplied by the separation distance to the actor position. Y is unchanged.
A nonempty contact set marks the residual-separation result even when its
averaged normal has no horizontal component.
The residual query uses the first collision working record, retaining its
ordinary sphere count. It rounds the first sphere’s radius to an integer and
assigns a common radius to every working sphere. State 15 uses half that radius
through an arithmetic right shift. Other states retain the original radius when
neither falling nor queued motion is active. Queued motion or falling normally
uses radius * 200 >> 8; falling with movement direction 3 uses that reduction
even when its low seven state bits are zero. These working radii do not replace
the actor’s retained initial shape. A nonempty residual result sets body-result
bit 4 while preserving the other bits.
FPS-derived frame scale
The normal FPS measurement path counts one frame per update and accumulates elapsed BIOS ticks. A measurement window contains 18 BIOS ticks. Once at least one complete window has elapsed, measured FPS becomes the frame count divided by the number of complete windows, using integer division. The frame count resets to zero; the tick remainder is retained. A negative accumulated tick count is clamped to zero. This uses the BIOS clock, not exactly one wall-clock second; see the water clock for its frequency.
Selected blocking operations, including model resource loading, save the timing state. Restoring that state reads the current BIOS clock and uses it as the next measurement baseline, so suspended time is excluded. The restore keeps the saved frame count, measured FPS, next scale, and fractional accumulators. The FPS tick remainder, smoothed FPS, and startup countdown are not part of the saved state.
The book menu uses this save/restore pair. While it is open, each menu iteration waits for a changed BIOS tick, updates the book, advances the shared clock, and then reads input. Leaving the menu restores the saved measurement frame count, measured FPS, next scale, and nominal/fine accumulators. The measurement remainder, smoothed FPS, and remaining startup countdown retain their values from menu processing. The currently exposed movement scale and nominal/fine increments are not restored; the next clock update replaces them.
The saved baseline is relative: the last sampled BIOS tick minus the measurement baseline. Restoration subtracts that saved difference from the current BIOS tick. On the normal BIOS measurement path, measurement finishes by making its baseline equal to its sample, so a save after that measurement has a zero difference.
The FPS-derived timing path keeps a measured FPS and a smoothed FPS. On each update, the smoothed value moves toward the measured value by one. If their initial difference is at least five, it first moves by half that difference, rounded down. Thus a measured change from 19 to 17 produces smoothed values 18, then 17 on successive updates. At the acceleration boundary:
| Smoothed FPS before update | Measured FPS | Smoothed FPS after update |
|---|---|---|
| 10 | 14 | 11 |
| 10 | 15 | 13 |
| 14 | 10 | 13 |
| 15 | 10 | 12 |
The half-difference adjustment applies to a difference of five in both directions.
use_smooth_fps selects the smoothed value instead of the measured value.
The selected FPS is clamped to min_frame_rate and max_frame_rate. The target
frame scale is the integer quotient of normal_frame_rate * 256 divided by
that FPS. A zero configured normal rate is replaced with 12 during loading.
With use_smooth_divisor disabled, the scale becomes the target immediately.
With it enabled, the scale first moves one unit toward the target. If it is
still above the target, it becomes the target immediately. If it is below the
target, the remaining difference determines a second increase:
| Remaining difference | Additional increase |
|---|---|
| 1–5 | 1 |
| 6–10 | 5 |
| 11–20 | 10 |
| 21 or more | 20 |
An exact match receives no additional increase. This smoothing is asymmetric: a falling target can take effect immediately, while a rising target can take several updates.
The 3dfx executable also contains an alternate path driven by a reprogrammed PIT interrupt, but nothing installs that interrupt (the installer has no caller or relocation), so the BIOS-clock path above is the one that runs.
Whole nominal ticks
The computed frame scale is accumulated in units of 1/256 nominal tick. Each update’s whole-tick increment is the difference between the accumulated whole ticks after and before adding the new scale. The fractional remainder carries to the next update. Accumulation precedes publication of the effective movement scale, and movement follows that publication. At the publication boundary, the accumulator already includes the forthcoming movement update. A clock origin for completed movement must exclude that pending increment. During startup, the accumulated calculated scale and exposed movement scale can differ. A movement update can therefore receive zero animation and script ticks while still having a nonzero movement scale. Rounding each update’s scale independently, or forcing at least one animation tick per movement update, changes behavior.
A separate finer counter adds the same scale to an accumulator and shifts the total right by five bits. It therefore advances eight units per nominal tick, with five fractional bits carried between updates. Its update increment is the difference of whole counter values.
A clock reset clears the measurement frame count, tick remainder, BIOS baseline, and fractional nominal accumulator. Measured and smoothed FPS start at 12, and both the used and next scales start at 256. The reset does not seed the baseline with the current BIOS time.
The reset also starts a twelve-update startup countdown. During that countdown, the exposed movement scale is 256, the whole nominal-tick increment is 1, and the finer counter increment is 8. The underlying FPS calculation and accumulation continue. Measured FPS replaces smoothed FPS for scale selection, and divisor smoothing is bypassed. After the countdown, the exposed values use the calculated scale and accumulated increments.
Entering the world loop starts the twelve-update countdown again without resetting the measurement or accumulation state.
Related documentation
Initial player animation setup
A newly allocated player actor starts with animation default group zero and stored surface state zero. Player definition and animation-group setup retain that surface value; subsequent support updates or restored world state can replace it. Player entry assigns the supplied map position with eight fractional bits and sets pitch and roll to zero and heading modulo 2048 before binding the actor definition and building its animation groups.
For an animated player definition, group setup makes two ordinary requests for group zero with modifier zero. It then clears the current model reference while retaining the selected animation model and frame identifiers. For an admitted animated actor, presentation resolves the model reference from those identifiers; a cleared reference alone does not mean that no animation frame is selected. Player entry follows that setup with a forced request for the actor’s default group, again with modifier zero. These requests use the existing shared nominal ticks; they do not establish a new clock interval.
The final request follows the ordinary forced-request rules. Requesting an already-current group before its exit marker is set only clears the pending request; it does not necessarily restart or advance the animation. The initial default group is distinct from the state-dependent default selected during subsequent ordinary player updates.
A world reload can restore temporary per-world saved state after player
initialization. The restored player record replaces position and animation state;
passing through marker initialization does not establish that the final loaded
player is a fresh spawn. Temporary world snapshots use the map’s .TSG file.
A world change saves the departing world’s temporary snapshot before the next
world loads. Entering a world whose snapshot exists restores it and then places
the player records at the destination marker; entering a world with no snapshot
starts from its authored placement. New Game deletes every snapshot.
Player contact lists
The selected player’s update clears two contact lists at its start. Its body
contact pass (run for motion states 0, 15 and 27 through 32) appends each object
whose collision model the body touches to both lists once: the first list accepts
an append while it holds at most sixteen entries and the second while it holds at
most thirty-two. The selected player’s primary ground-support query also appends
to the first list each model that becomes its best support candidate, under the
same rule, and the last such model is recorded as the support owner. PlayerCollide is true when the selected player is supported by
the receiver or the receiver is in the first list; PlayerCollideStatic is true
when the receiver is in the second list. Hazard objects such as the swinging
swords and hooks of the Drinal dungeon poll PlayerCollideStatic and damage the
player on contact; the Mages Guild doors poll PlayerCollide to open.
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.
Animation commands select the character’s model and frame. Attachment vertex
selection is separate from that frame selection. File ShowFrame commands
must not be interpreted as vertex indices. RAEX retains the grip and scabbard
vertex inputs.
Animation command stream
Groups contain packed three-byte commands. File ShowFrame commands use a
twenty-bit reference into the concatenated RAAN model/frame table. The loader
patches them into a runtime model handle and frame index. See
animation playback and RGM.
Complete Opcode Table
Names follow UESP where available.
| Opcode | UESP Name | Bit Layout | Parameters | Playback Behavior |
|---|---|---|---|---|
| 0 | ShowFrame | 20-bit file reference | frame_reference | Select a model/frame through RAAN; runtime representation is a patched 10 + 10 model handle and frame index. |
| 1 | EndAnimation | 20-bit | (unused, always 0) | Set animation handle to −1; stop associated sound; call playback recursively for next animation. |
| 2 | GoToPrevious | 20-bit | target_frame | Jump backward to an earlier frame in this group. Used for walk/run loops. |
| 3 | GoToFuture | 20-bit | target_frame | Jump forward to a later frame. Conditional — checks animation state flags and pending transitions. |
| 4 | PlaySound | 10 + 10 | effect, rate_offset | Play SFX positioned at the animated object. Both fields are signed. effect is the effect index after SURFACE.INI remapping; rate_offset × 64 Hz is added to the effect’s sample rate (result clamped to 512..44000 Hz). Volume and pan come from the positional sound rules, not from this command. Same bit layout as opcode 0 but params are NOT handle/vertex. |
| 5 | BreakPoint | 20-bit | (unused, always 0) | Set the animation breakpoint flag. Often the target of GoToFuture jumps. |
| 6 | SetPositionXYZ | 6 + 6 + 6 | pos_x, pos_y, pos_z | Store the three operands, each shifted left by 8, in the animation record’s position words. |
| 7 | SetPositionAxis | 2 + 18 | axis (0=X, 1=Y, any other value Z), value | Store the value, unshifted, in one position word. |
| 8 | SetRotationXYZ | 6 + 6 + 6 | rot_x, rot_y, rot_z | Store the three operands, each shifted left by 3, in the animation record’s rotation words. |
| 9 | SetRotationAxis | 2 + 18 | axis (0=X, 1=Y, any other value Z), value | Store the value, unshifted, in one rotation word. |
| 10 | ChangeAnimGroup | 10 + 10 | target_group, target_frame | Make the target group current at the given command index, keeping the other record bytes, and continue in the same call; a missing group or index is a fatal error. Reverse playback steps over it. |
| 11 | ActorEvent (exported as Rumble) | 20-bit | event_bitmask | Bit set passed to the actor-event service: bits 0 to 3 request the environmental effects 0x2580, 0x2500, 0x2500 and 0x2700 when the actor’s stored sound surface is lava, water, water and sand, and bit 4 adds a screen-shake impulse. Cyrus’s commands use 0; the Golem’s two stomp groups in DRINT use 16. |
| 12 | DelayCounter | 20-bit | counter_value | Set frame delay counter; pause animation until counter expires. |
| 13 | ConditionalDelay | 20-bit | counter_value | Store the operand in the animation record’s repeat byte and continue; reverse playback steps over it. |
| 14 | LoopControl | 20-bit | target_frame | With a zero repeat byte continue with the next command; otherwise decrement the repeat byte and continue at the target command. Reverse playback steps over it. |
| 15 | Transition | 6 + 7 + 7 | trigger_mask, start_frame, target_group | Mid-animation transition to another group. trigger_mask bits: 0=jump right, 1=jump left, 2=anim trigger (0x2500), 3=anim trigger (0x2700), 4=counter increment, 5=unused. Used by 8 combat actors. |
Opcodes 6–10 and 12–14 are implemented in the engine but do not appear in any of the 27 shipped RGM files.
Opcode Usage Census (all 27 shipped maps)
| Opcode | Total Cmds | Maps | Actors | Notes |
|---|---|---|---|---|
| 0 (ShowFrame) | 93,750 | 27 | 280 | Every animated actor |
| 1 (EndAnimation) | 8,513 | 27 | 280 | Every animated actor |
| 2 (GoToPrevious) | 3,258 | 27 | 275 | Walk/run loops |
| 3 (GoToFuture) | 7,121 | 27 | 276 | Conditional jumps |
| 4 (PlaySound) | 6,913 | 27 | 98 | Combat actors |
| 5 (BreakPoint) | 13,167 | 27 | 105 | Combat actors |
| 11 (ActorEvent) | 164 | 27 | 2 | Cyrus + Golem only |
| 15 (Transition) | 461 | 27 | 22 | Guards only |
| 6–10, 12–14 | 0 | 0 | 0 | Dead code in shipped game |
Bit Layout Summary
| Layout | Opcodes | Extraction |
|---|---|---|
| 10 + 10 | 0, 4, 10 | (packed >> 4) & 0x3FF, (packed >> 14) & 0x3FF (both sign-extended) |
| 6 + 6 + 6 | 6, 8 | 6-bit signed at positions 4, 10, 16 |
| 2 + 18 | 7, 9 | 2-bit selector at 4, 18-bit signed at 6 |
| 6 + 7 + 7 | 15 | 6-bit at 4, 7-bit signed at 10, 7-bit signed at 17 |
| 20-bit | 1, 2, 3, 5, 11, 12, 13, 14 | 20-bit signed at 4 |
Frame reference patching
The loader resolves the entire twenty-bit file reference through RAAN. It rewrites the command with a runtime model handle and frame index. Neither field is an attachment vertex index.
Vertex Position Lookup
At each frame, the engine reads the tracked vertex position through this call chain:
- Entry point — resolves animation handle and reads the tracked vertex.
- 3D object manager — looks up handle in a table. For “virtual” animations (type
0x02), follows a parent handle chain recursively. - Frame builder — reads the requested vertices from the selected model frame. Frame encoding is described in 3DC.
- Result is scaled by a global constant and rounded to integer world coordinates.
Actor model-point conversion
The runtime model-frame vertex query reads frame zero directly from its stored
floating-point base vertices. Later frames with encoding nibble zero convert
signed 32-bit coordinates to floating point with a factor of 1/256. Encoding
nibble one subtracts signed byte deltas from the base vertex. Encoding nibble
two subtracts signed 16-bit deltas from the base vertex, using a factor of
1/256 unless runtime flag 0x10 selects unscaled deltas. Each resulting
coordinate is stored as single precision.
The animation-vertex query’s disable gate is the dummy 3D manager state.
Initialization reads [3dmanager] dummy_manager from SYSTEM.INI, while the
dm command-line option forces that state to one. Static-angle conversion,
selected by [debug] convert_static_angles or the convert option, also
forces dummy-manager state to one when enabled. The query’s disable comparison
accepts exactly one as disabled.
A successful animation-frame vertex lookup supplies floating-point coordinates. The animation-vertex wrapper multiplies each coordinate by 256 and stores each product as single precision before converting the stored values to integers using the active floating-point rounding mode. With nearest-even rounding, half-integer ties select the even integer. It forwards the selected frame and vertex index to the model lookup. A nonpositive animation handle or an animation query-disable value equal to one returns the zero integer vector without model lookup.
The actor model-point query reads an integer vertex from the selected animation frame or the actor’s static model. It converts each coordinate to single precision before applying the actor matrix. For each transformed coordinate, it adds the actor’s local integer position before storing the sum as single precision. The integer position is not rounded to single precision separately before addition. The stored sums then convert to integer coordinates using the active floating-point rounding mode. With nearest-even rounding, half-integer ties select the even integer.
Item model data
Item definitions come from the ITEMS section of the configured item INI. Model names resolve against the loaded model registries, searching the primary registry before the secondary registry and selecting the first case-insensitive name match. The item initialization function iterates all items and populates per-item runtime fields:
World loading replaces the selected world-map filename’s extension with ROB
and loads that archive into the primary model registry before initializing item
geometry. Model filename resolution discards authored directory components and
uses the configured art directory, whose empty-setting default is FXART\\.
The inventory-item display loads INVENTRY.ROB into the secondary
registry; menu presentation loads MENU.ROB into that same secondary registry.
Loading a registry replaces its previous contents.
| Item Struct Offset | Source | Description |
|---|---|---|
+0x10 | Item type | ITEM.INI type: 1 = weapon, 3 = torch |
+0x4a | ROB handle | 3D model handle for the item |
+0x77 | ROB handle (type 1 only) | Hand model — the 3D model shown when weapon is drawn |
+0x7b | ROB handle (type 1 only) | Hilt model — the 3D model shown when weapon is sheathed |
+0x7f | ROB segment data | Length/offset — read from the ROB segment’s internal metadata. Used to offset the weapon collision sphere along the item axis. |
For weapon items, hand_object_file selects the hand model and
hilt_object_file selects the stowed model. The hand model’s ROB segment
bbox_extent_y supplies weapon length. The runtime reads that word as a signed
32-bit value before preparing the weapon’s collision spheres. The X and Z
extents do not supply this length.
Attachment Transform
The drawn-weapon routine queries the signed RAEX grip vertex pair from the actor’s current animation handle and frame. It adds the supplied local offsets to each vertex with signed 32-bit wrapping, then transforms both points through the actor matrix. The transformed points supply the direction calculation.
The two-point direction subtracts the second point from the first with signed
32-bit wrapping. It computes horizontal distance from the X/Z difference using
the integer distance query. With the signed bearing convention whose positive
X axis is 512 and positive Z axis is zero, direction pitch is the bearing of
(horizontal_distance, delta_y) minus 512. Direction heading is 1024 minus the
bearing of (delta_x, delta_z). Both results are masked to 2047; direction roll
is zero. Coincident points produce pitch 1536 and heading 1024.
The attachment matrix starts as identity. Its successive rotations use the negated direction heading, one quarter-turn minus the direction pitch, and the negated actor roll. Each angle is masked to 2047. The actor retains pitch plus one quarter-turn, heading, and roll, each masked to the same range.
For each attachment-position component, subtract the first transformed vertex from the second with signed 32-bit wrapping, shift the difference arithmetically right one bit, add the first vertex with wrapping, and shift that result arithmetically right eight bits. Add this integer midpoint to the actor’s floating position and store the sum as single precision. Separately, the actor retains the first transformed vertex plus its floating position multiplied by 256, converted to a signed integer by truncation.
If attachment allocation fails, the routine performs no vertex queries or transform publication.
The stowed-weapon routine uses the signed RAEX scabbard vertex pair with the same local-offset, actor-matrix and direction calculation. It builds the same matrix and retains the same angle fields. Its attachment position uses the first transformed vertex rather than the pair’s midpoint. Add that vertex to the actor’s floating position multiplied by 256 and truncate to a signed integer. Shift this integer arithmetically right eight bits and store it as single precision. An out-of-range integer conversion produces the signed minimum value before the shift.
Weapon State Machine
The weapon state machine selects which attachment routine and which model (hand vs hilt) to use based on the actor’s weapon state:
State (actor +0x1b4) | Condition | Action |
|---|---|---|
0x14 (drawing sword) | Frame < draw threshold | Position hilt model at scabbard |
0x14 (drawing sword) | Frame ≥ draw threshold | Position hand model at hand; set drawn flag |
0x15 (sheathing sword) | Frame < sheath threshold | Position hand model at hand |
0x15 (sheathing sword) | Frame ≥ sheath threshold | Position hilt model using grip geometry; clear drawn flag |
0x00 (idle, sheathed) | Current animation group is 129 | Position hilt model using grip geometry |
0x00 (idle, sheathed) | Other animation group | Position hilt model at scabbard |
0x00 (idle, drawn) | — | Position hand model at hand |
The draw/sheath frame thresholds are read from the actor’s attribute data (offsets +0x22 and +0x23 from an attribute block pointer at actor +0x272).
The collision sphere tip is offset from the grip point by item.length × -0x100, positioning it along the weapon axis for combat hit detection.
SOUP Script Interface
Scripts drive weapon state transitions through these SOUP functions:
| Function | Purpose |
|---|---|
handitem | Assign a held item to an actor |
displayhandmodel | Show the hand (drawn) model |
displayhanditem | Show the item in the hand |
displayhiltmodel | Show the hilt (sheathed) model |
displayhiltitem | Show the item at the hilt position |
drawsword | Trigger draw animation/state transition |
sheathsword | Trigger sheath animation/state transition |
isholdingweapon | Query: is actor holding a weapon? |
iscarryingweapon | Query: is actor carrying (has) a weapon? |
isdrawingsword | Query: is actor in draw animation? |
issheathingsword | Query: is actor in sheath animation? |
Additional runtime state tracked per actor: hand_pos.vx/vy/vz, hand_angle.vx/vy/vz, hand_type, hand_length, weapon_drawn, hand_item.
Held-item assignment
HandItem(id) is separate from inventory selection and weapon drawing. It rejects
assignment while the receiver is in deep water, on an edge, in the air, or holding
a weapon. It also requires an open item system, a receiver, and execution outside
the shell context. Deep water means stored surface 2 or 4; edge motion states are
10 through 19. The in-air predicate includes nonzero falling, sliding, or queued
motion, and motion states 27 through 29.
After admission, the requested ID must resolve to an item record. No owned-count, visibility, or item-type check is made. Success stores the low eight bits of the ID in the actor’s held-item field. It does not change the selected inventory item, assign the weapon attribute, or immediately position or draw an attachment. The function returns zero both after assignment and after a rejected admission.
Data Flow Summary
Animation playback resolves the model and frame first. Item attachment then reads selected vertices from that model frame and computes the item transform. The frame reference and the vertex selection are distinct inputs.
External References
- UESP: Mod:RGM File Format § RAEX — RAEX field names from in-game console
- RGUnity/redguard-unity
RGRGMFile.cs— RGMRAEXItem struct definition
Developer console
The Glide console accepts 82 registered commands with short aliases, plus 13 cheat names handled separately.
Command dispatch and syntax
Press F12 to open the console. Command names are case-insensitive. Each registered command has exactly the long name and short alias listed below. Other abbreviations are not accepted. The commands command, also spelled ?, lists the 82 command pairs; oracle lists the 13 cheat names and their raw state values. There is no literal cheat prefix command.
The engine also has a programmatic command entry point. It accepts the long registered names and the cheat names, but does not look up short aliases.
Input is lowercased and normalized before dispatch. A semicolon ends the input; it does not separate commands to execute in sequence. Arguments are parsed by each handler. Spaces and commas are not interchangeable: for example, use load 1,0,0, item add,ID, setflag NAME,VALUE, and object attr attr_health = 100. Square brackets in this reference indicate optional arguments and are not typed.
Most numeric handlers accept decimal integers, negative decimal integers, 0x hexadecimal values, named equates, and named SOUP flag values. An unresolved symbolic value prints a diagnostic and substitutes 1. The percent-prefixed binary-input path returns zero for ASCII binary digits. Quoted numeric input reads the first four bytes following the quote as a little-endian 32-bit value; it is not general string evaluation. delay uses decimal conversion directly.
Decimal and hexadecimal conversion consume the numeric prefix and stop at the
first non-digit for that radix. Arithmetic wraps to 32 bits. A leading minus
selects decimal conversion even when no digits follow, yielding zero in that
case. 0x without hexadecimal digits also yields zero. A leading plus does not
select numeric conversion through the console reader.
Symbol lookup checks equates, console variables, then SOUP flags. Both definition loading paths release the temporary equate table before returning. The normal console therefore has no retained definition equates, and its console-variable table is empty. SOUP flag lookup reads the current raw value, without invoking the flag’s latch operation. Flag names are matched case-insensitively; the name list is built in reverse definition order, so the last duplicate name wins.
The console keeps a selected object, a monitored object, a selected rope and a selected light. These are separate selections. An object selected for inspection does not become the monitor target until object monitor is used.
Interactive presentation and editor
The interactive console covers the game viewport with SYSTEM/PAPER.GXA.
At widths of at least 640 pixels, its standard font is the FNT named by
SYSTEM.INI [system] gui_low_font. The console draws that font with a single
ink color rather than the glyph palette colors. Its header and left text margin
start at the scaled equivalent of eight pixels in a 640 by 480 viewport.
The header identifies the development console and reports memory usage; a
horizontal separator precedes retained output and the command prompt.
Standard settings use palette entry zero for editing and header text. Report
text uses the GUI color nearest to six-bit RGB (0, 42, 0) in the active palette.
Palette matching examines entries 1 through 255 in six-bit RGB. An exact match
wins immediately. White entries whose component sum is 189 are otherwise skipped.
For each component, the difference is target minus candidate when nonnegative,
and candidate minus target minus one otherwise. Candidates with all three
differences below nine are compared by the absolute difference of their weighted
RGB sums, using weights 19661, 38666 and 7209. Equal scores select the later entry.
If this pass finds no candidate or its score exceeds 24, a second pass minimizes
the sum of the three component differences, again preferring later ties.
The sixteen console GUI color targets, in index order, are palette entry zero,
then six-bit RGB (62,62,62), (42,42,42), (32,32,32), (22,22,22),
(12,12,12), (32,51,63), (19,33,44), (10,17,26), (53,3,23),
(49,0,0), (60,0,0), (0,30,0), (0,42,0), (0,0,0) and (24,0,28).
The first index uses palette entry zero directly; the others use the matching
procedure above, including black at index fourteen.
The standard GUI font’s linked font points to itself after startup. The alternate console font is a copy of that same font, with its link pointing to the copy. At widths of at least 640, font zero selects the standard font and font one selects its linked font. Below 640, font zero selects the linked font and font one selects the alternate copy. Changing the console font selector therefore does not load a different FNT file.
The console has 512 retained lines, with at most 100 characters in each editable
command line. A > prompt distinguishes command lines from report lines.
F12 and Escape leave the interactive console. Left and Right move the cursor; Backspace removes the preceding character and Delete removes the following character. Typing inserts printable characters at the cursor. Up and Down select retained lines, including report lines; report lines are not editable. Tab copies a preceding command into the current editable line. Home moves to the start of the line, then to the oldest retained line when already at the start. Page Up and Page Down move through retained output. Enter dispatches an editable command. The interactive console runs its own input and drawing loop before returning to the caller’s game loop.
Console presentation and interactive command entry are separate modes. During world gameplay, an F12 request without Alt selects interactive entry; holding Alt selects the nonmodal overlay. The overlay returns to the world loop without running the command editor.
Appending a report while the console is closed also requests the nonmodal overlay and sets its timeout to 24 engine steps. This report path selects overlay mode even if an interactive request is pending. Reports submitted while the console is already active redraw it instead. A visible console background or report line therefore does not establish that the command editor is accepting input.
With shadows enabled, the Glide glyph path draws a black glyph copy before the
main glyph. The console selects a two-pixel offset in each axis at widths of at
least 640 and one pixel below that threshold. The standard console font uses
main alpha 255; the shadow alpha is (255 * 200) >> 8, or 199.
The cursor uses two report-colored vertical-bar glyphs, four scaled pixels above and below the insertion position. It is visible while a key is being handled or when the low bit of the console clock counter is set. On the normal BIOS-clock path, the counter increments once whenever accumulated elapsed time reaches 18 BIOS ticks, retaining the remainder modulo 18. A delayed update increments it once even when several such intervals elapsed. An input iteration without a key clears the forced-visible state.
Console preferences are stored in REDGUARD.CPF.
Registered commands
Each row is one command. The alias invokes the same handler from the interactive console.
Console and diagnostics
| Command | Alias | Arguments and behavior |
|---|---|---|
commands | ? | commands. Lists the 82 registered commands and their aliases. The 13 cheat names are listed separately by oracle. |
show | sh | show <topic>. Reports engine, world, player, object, memory, hardware, sound, and navigation information. Some topics toggle debug displays or write GRID.LOG. See subcommands. |
console | con | console <option>. Changes console appearance, clears history, exports output, and saves console preferences. |
print | lpt | print page / all. Sends the visible console page or retained output to a DOS printer. This is printer output, not an on-screen print command. |
hex | x | hex [address / last]. Displays memory as hexadecimal bytes and characters; omission continues the previous dump. |
recover | r | recover. Requests recovery from the recoverable error state. Refuses when there is no error or system integrity is considered bad. |
break | b | break. Enters the engine error path with the critical-drive-error code. Not a general-purpose script breakpoint. |
poke | p | poke address,byte[,byte...]. Validates an address and byte values, then displays one line of memory. Contains no memory write. Values outside -128 through 255 are rejected. |
restart | rs | restart video / xngine. Restarts the video subsystem or the rendering engine. Does not start a new game. |
delay | dl | delay [number]. Sets a wait of that many milliseconds, taken once per main-loop pass while the value is nonzero, as a busy wait calibrated at startup against the system clock. The number is read as decimal digits up to the first other byte, so omission, zero or a non-number clears it. |
State and scripts
Name patterns are case-insensitive. An asterisk accepts the remainder immediately, including any following pattern text. A question mark accepts one character; extra trailing question marks also match after a nonempty name ends. Empty patterns match in the shared matcher, although flag commands require a nonempty pattern.
| Command | Alias | Arguments and behavior |
|---|---|---|
get | g | get pattern. Looks up console variables by name pattern. The Glide build has an empty console-variable table, so no variables match. |
set | s | set pattern,value[,value...]. Parses assignments to console variables. The same empty table prevents assignments in this build. |
getflag | gf | getflag pattern. Lists matching SOUP flags, their types, values, and set status. |
setflag | sf | setflag pattern,value[,status]. Changes matching SOUP flag values and optionally their status. A nonzero status sets the set bit. Zero XORs the metadata byte with 239; it does not clear the set bit. |
script | sc | script log / disable [name] / block. Writes a script log, suppresses updates for actors using a named script, or advances the script block list. disable without a name clears the suppression; this is not an arbitrary SOUP-expression evaluator. |
Objects and inventory
| Command | Alias | Arguments and behavior |
|---|---|---|
object | o | object <operation>. Selects, inspects, edits, monitors, or teleports to objects; also provides an interactive kill mode. Includes attribute assignment and subcommands missing from help. See subcommands. |
logplayer | pl | logplayer off / console / screen. Disables player diagnostics or toggles their console/screen output. |
logmonitor | ml | logmonitor off / console / screen. Disables monitored-object diagnostics or toggles their console/screen output. |
rope | rp | rope heap / select / general. Lists ropes, interactively selects one, or reports the selected rope and its attached objects. |
item | i | item <operation>. Lists inventory definitions or player items, adds or removes one item, or drops a specified amount. |
playerdist | pd | playerdist. Reports the distance between the selected console object and the player. |
cameradist | cd | cameradist. Reports the distance between the selected console object and the camera. |
World and navigation
| Command | Alias | Arguments and behavior |
|---|---|---|
load | l | load world[,marker][,angle]. Requests a world load with optional marker and angle. Omitted marker and angle default to zero; without a world the command prints a usage message and requests nothing. The request is held while the console is open: a later load replaces the pending world, marker and angle, F12 closes the console and the world loop then leaves for the requested world, and Escape closes the console and discards the request. The angle is the entry heading modulo 2048, as in marker-based world entry. |
setmarker | sm | setmarker number. Saves the current player/camera placement as a user marker in REDGUARD.MRK. |
getmarker | gm | getmarker number. Restores a user marker for the current world from REDGUARD.MRK. |
setnetmark | snm | setnetmark number. Saves a user marker in the configured shared marker file. Uses a file path from SYSTEM.INI; it is not a multiplayer command. |
getnetmark | gnm | getnetmark number. Restores a user marker from that configured shared file. |
openputnode | op | openputnode [filename]. Opens the PUT-node authoring system, optionally with a file. |
saveputnode | sp | saveputnode [filename]. Writes the PUT nodes to the supplied filename or the filename used when opened. |
closeputnode | cp | closeputnode. Closes the PUT-node authoring system. |
getmapmarker | gmm | getmapmarker number. Moves the player to a marker authored in the current map. |
walk | w | walk [filename]. Generates a walk map from PUT nodes. Requires at least two nodes and a filename; enters the generation workflow and then exits through the engine shutdown path. |
node | n | node <operation>. Inspects navigation maps/nodes, toggles diagnostic displays, logs node data, and clears node occupancy. The help-only spelling showinuse is not accepted. |
Developer tests
| Command | Alias | Arguments and behavior |
|---|---|---|
test | t | test. Writes and reloads the player object through OBJECT.SAV, then compares its state. This changes live object state and writes files. |
test2 | t2 | test2. Writes GRID1.LOG and GRID2.LOG around an internal world save/load test. Also uses GRID.LOG and the internal save/load workflow. |
test3 | t3 | test3. Writes and reloads the monitored object through OBJECT.SAV, then compares its state. Requires a monitored object; changes live object state and writes files. |
Rendering and visibility
| Command | Alias | Arguments and behavior |
|---|---|---|
render | rd | render on / off / static. on and off clear and set a render-disable flag that starts clear; while it is set the main render pass is skipped. static toggles the static-object rendering flag, which starts set, and clears a related redraw variable. Any other word prints usage. |
scene | sn | scene. Enters an interactive scene inspection mode with information under the pointer. |
pvotreeinfo | pvoi | pvotreeinfo. Reports the loaded potentially-visible-object tree, lists, and patch count. |
pvoaddpatch | pvoa | pvoaddpatch. Interactively picks an object to add to the PVO patch list. |
pvodeletepatch | pvod | pvodeletepatch. Interactively picks an object to remove from the PVO patch list. |
pvoonoff | pvo | pvoonoff. Toggles the PVO visibility-system switch. Requires a loaded PVO tree. |
pvopatchsave | pvos | pvopatchsave. Saves the current PVO patches. |
pvotreeload | pvol | pvotreeload filename. Loads another PVO tree. The command first requires a PVO tree to be loaded already. |
Lighting editor
| Command | Alias | Arguments and behavior |
|---|---|---|
fxlightflag | lf | fxlightflag. Activates the light-editing controls and selects a light, or clears/restores the light selection. |
fxsavelights | sl | fxsavelights. Writes the current world light definitions to its world light file. |
fxgolight | fxgl | fxgolight number. Moves the player to a numbered light and selects it. Requires lights and the light-editing mode. |
fxselect | fxsel | fxselect. Interactively selects a light. |
fxeditred | fxer | fxeditred value. Sets the selected light’s red channel. Clamped to -255 through 255. |
fxeditgreen | fxeg | fxeditgreen value. Sets the selected light’s green channel. Clamped to -255 through 255. |
fxeditblue | fxeb | fxeditblue value. Sets the selected light’s blue channel. Clamped to -255 through 255. |
fxeditradius | fxerad | fxeditradius value. Sets the selected light’s radius. Negative input becomes zero; storage is 16-bit. |
fxlightmove | fxlm | fxlightmove value. Sets the light editor’s movement-step value. |
fxincstep | fxis | fxincstep value. Sets the light editor’s increment-step value. |
fxmanualx | fxmx | fxmanualx value. Sets the selected light’s X coordinate. Requires an active light editor and selected light. |
fxmanualy | fxmy | fxmanualy value. Sets the selected light’s Y coordinate. Requires an active light editor and selected light. |
fxmanualz | fxmz | fxmanualz value. Sets the selected light’s Z coordinate. Requires an active light editor and selected light. |
fxscriptname | fxsn | fxscriptname name. Assigns the selected light’s script name. Retains at most eight characters; requires an active light editor and selected light. |
Sky, lighting and water
Sky scale and level read signed 32-bit console numbers and convert them to float for rendering. Wave amplitude, speed and frequency do the same conversion. These five commands do not clamp their inputs. Sky speed uses the full signed value divided by sixteen for immediate scrolling, stores its low byte in the world configuration, and chooses a new random scrolling direction.
| Command | Alias | Arguments and behavior |
|---|---|---|
fxtexturelist | fxl | fxtexturelist start. Lists up to 40 texture entries starting at an index. A starting index is required despite the brackets in the built-in usage text. |
fxshowtexture | fxsh | fxshowtexture [number]. Shows a texture and its metadata; omission disables the texture preview. |
fxsunangle | sang | fxsunangle value. Sets the sun angle. Clamped to 0 through 2048. |
fxsunskew | sskew | fxsunskew value. Sets the sun skew. Clamped to 0 through 2048. |
fxsunscale | sscale | fxsunscale value. Sets the sun-image scale. |
fxsunred | sred | fxsunred value. Sets the sun lighting red channel. Clamped to -255 through 255. |
fxsungreen | sgre | fxsungreen value. Sets the sun lighting green channel. Clamped to -255 through 255. |
fxsunblue | sblu | fxsunblue value. Sets the sun lighting blue channel. Clamped to -255 through 255. |
fxsunbrightness | sbri | fxsunbrightness value. Sets sun-light brightness. Stored as a signed 16-bit value. |
fxsunimgred | sired | fxsunimgred value. Sets the sun image red channel. Clamped to -255 through 255. |
fxsunimggreen | sigre | fxsunimggreen value. Sets the sun image green channel. Clamped to -255 through 255. |
fxsunimgblue | siblu | fxsunimgblue value. Sets the sun image blue channel. Clamped to -255 through 255. |
fxskyscale | skysc | fxskyscale value. Sets the current world’s sky scale. |
fxskylevel | skyl | fxskylevel value. Sets the current world’s sky level. |
fxskyspeed | skysp | fxskyspeed value. Sets the current world’s sky scrolling speed. |
fxfogred | fred | fxfogred value. Sets the fog red channel. Clamped to -255 through 255. |
fxfoggreen | fgre | fxfoggreen value. Sets the fog green channel. Clamped to -255 through 255. |
fxfogblue | fblu | fxfogblue value. Sets the fog blue channel. Clamped to -255 through 255. |
fxambientred | ared | fxambientred value. Sets the ambient light red channel. Clamped to -255 through 255. |
fxambientgreen | agre | fxambientgreen value. Sets the ambient light green channel. Clamped to -255 through 255. |
fxambientblue | ablu | fxambientblue value. Sets the ambient light blue channel. Clamped to -255 through 255. |
fxwaveamp | wamp | fxwaveamp value. Sets the current world’s wave amplitude. |
fxwavespeed | wsp | fxwavespeed value. Sets the current world’s wave speed. |
fxwavefreq | wfrq | fxwavefreq value. Sets the current world’s wave frequency. |
Marker file
setmarker and getmarker keep user markers in REDGUARD.MRK, opened by that bare name in the working directory. The marker number is a console expression, so a hexadecimal, decimal, binary or character literal, a console variable or a name from the installed tables are all accepted.
| Offset | Size | Contents |
|---|---|---|
| 0 | 4 | Magic UMRK. |
| 4 | 4 | Size field, little-endian: 4 plus 56 per record. It counts the tag and the records, not the magic, the size field or the trailer. |
| 8 | 4 | Format tag, always zero. |
| 12 | 56 each | Records, in insertion order. |
| end - 4 | 4 | Trailer END . |
A record is fourteen little-endian 32-bit values:
| Index | Contents |
|---|---|
| 0 | Current world number. |
| 1 | Marker number. |
| 2-4 | Player position X, Y, Z in runtime units (256 per map unit), unscaled. |
| 5-7 | Player rotation angles about X, Y and Z, in the engine’s 0 through 2047 angle units. |
| 8-10 | Camera position X, Y, Z in runtime units, from the camera position variables. |
| 11-13 | Camera angles in the same 0 through 2047 units, from the camera angle variables. |
Both commands leave the file untouched without a player. setmarker creates the file when it is missing, and also rewrites it from scratch when its magic or tag does not match, losing any records it held. An existing record with the same world and marker number is overwritten in place. Otherwise the new record replaces the trailer, the trailer is written again after it and the size field grows by 56. getmarker reads the file and scans its records in order for the first one whose world and number both match the current world and requested number. A wrong magic or tag reports a file error and restores nothing, and a missing record restores nothing. A match sets the player position, copies it to the previous-position fields, sets the player rotation angles (each masked to 0 through 2047, copied to the previous-angle fields, and the rotation matrix rebuilt), writes the camera position and angle values, and refreshes the view. The camera values are the same position and angle variables that the camera update rewrites each frame: before any camera has published, the position starts at 10000 map units on every axis and the angles at zero, and one camera mode publishes the player position plus a rotated offset together with the negated player angles masked to 0 through 2047.
getmapmarker number reads the loaded map’s own marker table instead of the file. A marker is valid when its index is below the table’s count and its valid byte is 1; positions are stored in map units and multiplied by 256 to give the runtime position, which is also copied to the previous-position fields. An index outside the table or an invalid marker restores nothing. When no map table is loaded, the built-in table of 128 slots is used in the same way; it starts with every slot unused and is filled only by the legacy marker registration routine, which stores three position values for one index at a time.
Subcommands
Comma-separated names in the first column denote separate alternatives unless the row shows an argument form such as add,id.
show
| Subcommand or argument form | Behavior |
|---|---|
memory, free | Memory and heap reports. |
world | Current world configuration, including sky, lighting and wave settings. |
video | Video subsystem report. |
objects | Object and model totals. |
player | Player position, animation, combat and item state. |
xngine | Camera and renderer settings. |
ai | Loaded AI script files. |
3d | Loaded ROB models. |
library | Object-library entries. |
registry | Resource registry, archives and open-file information. |
grid [number / all] | Grid contents; the all form also writes GRID.LOG. |
map | Grid/object information with GRID.LOG output. |
sound | Sound-system state. |
group number, user number | Members of a group or user group, numbered 1 through 255. |
active, list, activelist | Object-chain reports. |
manager [handle] | Model-cache report, optionally for a handle. |
machine | Machine and DOS environment information. |
rain | Rain-system state. |
remap | Objects in the remap list. |
marker [flats] | Toggles marker display; flats selects the flat-marker display. |
all | Runs the main collection of diagnostic reports. |
console
| Subcommand or argument form | Behavior |
|---|---|
clear | Clears console history. |
write | Writes retained output to CONSOLE.LOG. |
font | Switches the console font choice. |
shadow | Toggles text shadow. |
save | Saves console preferences to REDGUARD.CPF. |
col0, col1, col2, col3 | Separate subcommands that cycle edit, report, on-screen and paper colours respectively. col3 requires paper to be off. |
hex,number | Sets the number of lines in a hex dump. |
limit,number | Sets the screen limit, from 0 through 300. |
paper | Toggles the paper background. |
show | Reports console preferences. |
object
| Subcommand or argument form | Behavior |
|---|---|
select | Interactively picks an object. |
closest | Selects the closest eligible object to the player. |
player | Selects the player. |
master number | Selects a master object, numbered 1 through 255. |
search script-pattern | Finds objects by script name; one match is selected automatically. |
address, "name" | Alternative selection forms using a numeric address or a library object name. |
general, quick, coord | General state, a short status report, or coordinates. |
anim | Reports the current animation frame. |
size | Reports object size information. |
combat, vcollide | Combat state or vertex collision spheres. |
matrix, locals, locations, strings, script, family | Separate reports for the corresponding object data. |
attr | Lists named attributes and their values. |
attr name = value | Assigns an attribute byte; the equals sign is optional. |
monitor | Makes the selected object the monitor target, or clears it if already monitored. |
goto | Moves the player to the selected object. |
die | Enters interactive object-killing mode. |
zap amount | The supplied-amount form reports a missing argument. The handler has a reversed presence check. |
item
| Subcommand or argument form | Behavior |
|---|---|
player | Lists item types with a nonzero player count. |
show[,id] | Lists all item definitions or one item. |
add,id | Adds one item to the player. |
sub,id | Removes one item from the player. |
drop,id,amount | Drops the requested amount. |
node
| Subcommand or argument form | Behavior |
|---|---|
maps | Lists loaded node maps. |
monitor | Toggles node-marker display for the monitored object. |
monitor dnode | Reports the monitored object’s destination node. |
monitor node | Reports the monitored object’s current node. |
monitor log | Toggles node logging for the monitored object. |
shownode | Toggles object-node markers. |
showmap | Toggles node-map display. |
player | Toggles player-node and route display. |
log map-id node-number | Writes the chosen node report to NODE.LOG. |
inuse | Reports repeated node-occupancy IDs. |
sweep | Clears node occupancy throughout the loaded maps. |
script
| Subcommand or argument form | Behavior |
|---|---|
log | Opens SCRIPT.LOG, enables script logging, and requests a return from the console. |
disable [name] | Suppresses the actor update path for the matching script basename. Omission clears it; basenames must be one through eight characters. |
block | Advances to the next entry in the engine’s script-block list. |
rope
| Subcommand or argument form | Behavior |
|---|---|
heap | Reports all ropes. |
select | Interactively picks a rope. |
general | Reports the selected rope and its attached objects. |
restart
| Subcommand or argument form | Behavior |
|---|---|
video | Restarts the video subsystem. |
xngine | Restarts the rendering engine. |
render
| Subcommand or argument form | Behavior |
|---|---|
on, off | Enables or disables rendering. |
static | Toggles static-object rendering. |
logplayer
| Subcommand or argument form | Behavior |
|---|---|
off | Clears both logging destinations. |
console, screen | Separately toggles each output destination. |
logmonitor
| Subcommand or argument form | Behavior |
|---|---|
off | Clears both logging destinations. |
console, screen | Separately toggles each output destination. |
Cheat names
These 13 names are also accepted as console commands: oracle, nodemarker, moonraker, task, animation, magiccarpet, savecheats, drevil, drno, goldfinger, neversaydie, oddjob, and yeahbaby.
oracle and savecheats perform actions. The remaining names accept a toggle or name = value. Their numeric activation conditions differ. See Cheat system for the effects and per-cheat rules.
Differences between help and command behavior
showacceptsrain,free,map,listandactivelisteven though its usage list omits them.object animandobject sizeare accepted but omitted from the object usage list.- The node help lists
node showinuse, but the handler does not accept that spelling.node inuseis accepted. fxtexturelistrequires an index despite the brackets in its usage message.fxshowtexturewithout an index turns its preview off.pokehas no memory-write operation. Successful byte validation is followed by a one-line hex display.getandsetuse an empty console-variable table in the Glide build. SOUP flag access throughgetflagandsetflagis separate and uses names loaded from SOUP386.DEF.object zaptests argument presence backwards. Supplying an amount produces its missing-argument message; omitting it reaches decimal conversion of the empty token.- The optional zero status in
setflagXORs the flag metadata with239, leaving the set bit unchanged and flipping the other bits. A nonzero status sets the set bit.
External references
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).
The developer console reference lists all other console commands and their aliases.
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.
Toggle rules
Cheats normally store 0 for off or 1 for on. Typing a cheat name again switches between those two values, whether entered during gameplay or in the console.
The console also accepts other numbers, such as oddjob = 2. These are raw values, not strength levels. Their gameplay meaning depends on the cheat: some checks accept any nonzero value, while others require exactly one.
Entering the name again handles these unusual values differently:
- During gameplay, any nonzero value becomes zero.
- In the console, an even value becomes one and an odd value becomes zero.
For example, after oddjob = 2, entering oddjob in the console stores one; entering it during gameplay stores zero. The console rule is (value + 1) & 1.
oracle and savecheats run dedicated actions. oddjob follows the ordinary state-handling rules above.
Cheat List
| # | Name | Effect |
|---|---|---|
| 0 | oracle | Lists all 13 cheats and their raw integer state values in the console |
| 1 | nodemarker | Recognized state slot: accepts toggles and explicit console values, appears in oracle, persists in REDGUARD.CHT and is replaced at startup by the node_marker key of SYSTEM.INI [debug]. No code reads the slot after startup, so it changes no display or movement behavior; the node-map and node displays follow the separate display_node_map and display_nodes keys of the same section. |
| 2 | moonraker | While the state is nonzero the selected player skips two collision passes of its ordinary update: the three forward obstacle probes (the one at the actor’s position and the two at the extent-derived heights, each also run again after an overlap repair) and the actor overlap pass (candidate gathering and resolution). Other actors are unaffected. Each update also sets bit 0 of the first flag byte of the selected player’s control record when the state is zero and clears it otherwise; that bit is the enable of the overlap pass for that record. |
| 3 | task | Toggles the task scheduling system — a high-level bytecode-driven behavior scheduler that runs on top of SOUP scripts. SOUP is the low-level scripting VM (“do X right now”); the task system is the higher-level sequencer (“do X, then Y, then Z over time”). Each actor has 6 independent task slots with their own bytecode pointers and frame counters. When enabled, a state machine interpreter processes ~30 task opcodes (0x00–0x1e) per actor per frame, handling action queuing, behavior transitions, AI/combat processing (pathfinding, combat stances, NPC-vs-player logic), and actor destruction. When disabled, only SOUP script execution runs — actors lose multi-step behaviors, combat AI, and pathfinding. Corresponds to task_system in SYSTEM.INI [system] (default: yes). |
| 4 | animation | Toggles the animation state machine — the system that manages animation playback, transitions, blending, and frame timing for all actors. When enabled, actors play animations (walk, fight, idle, etc.) with managed transitions and priority blending, and combat actions are gated by animation timing (a new attack cannot start until the current animation allows it). When disabled, all animation management early-returns: actors freeze on their current frame, combat action timing checks are bypassed (attacks fire without animation constraints), and animation progress always reports zero. The zero value also suppresses the ordinary animated-player draw; it does not retain a visible frozen player pose. Corresponds to animation_system in SYSTEM.INI [system] (default: yes). |
| 5 | magiccarpet | Fly mode — disables gravity and falling, allowing the player to walk horizontally through the air at current altitude |
| 6 | savecheats | Writes all cheat states to REDGUARD.CHT so they persist across sessions |
| 7 | drevil | Prevents player death — suppresses death handling when health reaches zero and bypasses some player damage application paths. While the state is nonzero, a hit message or a direct-damage message to the selected player applies no health change and starts no death response; the hit reaction animation request and the restoring of the sender’s charged weapon still run. Other actors take damage normally. The kill routine for the selected player also returns without effect. |
| 8 | drno | Disables part of the non-player actor update path, reducing/skipping some NPC behavior processing. While the state is nonzero, the per-actor update returns early for every actor except the selected player and the actor the camera setup was given, skipping the rest of that update. |
| 9 | goldfinger | Retains a burning torch. During ordinary actor updates, a nonzero selected-player torch counter becomes 100 when the world frame is divisible by eight and living state is not 34. A zero counter remains zero. The refresh takes the place of the burn rules on that update: the counter is set to 100 without the water branches and without the countdown. |
| 10 | neversaydie | Makes SOUP PlayerDistance() return 9999999 instead of calculating distance. |
| 11 | oddjob | Recognized state slot: accepts toggles and explicit console values, appears in oracle, and persists in REDGUARD.CHT. |
| 12 | yeahbaby | Vertical surface bypass — while active, Page Up moves the player upward through surfaces and Page Down clips below the current surface. The selected player’s ground-support probe is skipped, clearing its living-state byte, the sliding, falling and queued-motion bytes, the rope node reference (+0x1ab) and the last_direction byte (+0x1cb), whenever Page Up or Page Down is held or a flag toggled by Home is set (the player’s per-update handler consumes the Home key state and flips that flag once per press, and only while the handler is enabled); otherwise normal surface collision applies. Horizontal collision is unaffected. In the player’s per-update handler, each held vertical key moves the player’s Y position by 4096 (16 map units) per pass, Page Up subtracting and Page Down adding. While Insert is held the same handler also moves the player forward along its heading by 128 Q8 units times the frame’s step value and sets the camera record flag that suspends camera movement limiting, clearing it when Insert is released. The flag is set once, on the first held pass, on the camera record selected at that moment; the release test runs on every pass of the handler, whether or not the cheat is on, and clears the flag on the record selected at that moment, so turning the cheat off while Insert is held also restores the limiting. The handler and the bypass are enabled by this cheat or, in developer mode ([debug] final_version=0), regardless of it. |
The cheat names are themed after James Bond films (Moonraker, Dr. No, Goldfinger, Oddjob, Never Say Never Again) and Austin Powers (“Yeah Baby”).
Values other than zero and one
The Glide engine does not apply one shared on/off rule to every cheat. The ordinary gameplay checks use these conditions:
| Cheats | Condition used | Meaning of 2 |
|---|---|---|
task, magiccarpet | Value equals 1 | Does not enable the corresponding check |
nodemarker, oddjob | No gameplay check reads the slot | Any value is only stored |
moonraker, drevil, drno, goldfinger, neversaydie, yeahbaby | Value is nonzero | Enables the check, just like 1 |
animation | The movement natives’ wait for their requested group to become current requires 1; the animation requests (including the default request and forced requests), advancement and the progress readout accept any nonzero value | Runs the animation system but skips the movement natives’ wait: the natives that request a walking, backing or side-stepping group then step without waiting for it, so it is not equivalent to either ordinary on or off |
With animation at zero the selected player still moves: holding the walk key and forward advances it by the walking startup distance each update (16 units per update at frame scale 512 once the startup ramp ends), holding forward alone runs it at the run speed (40 units per update at 512), and the current animation group stays at its value (group 0 when idle) because the request routines do nothing. Nothing stops or faults.
oracle and savecheats execute commands instead of using these gameplay conditions. oddjob accepts and stores arbitrary values through the shared cheat-state handling described above.
For example, neversaydie = 2 still makes PlayerDistance() return 9999999, but magiccarpet = 2 does not enable its fly-mode check. Values above one are not additional strength levels. Use 0 and 1 for consistent on/off control.
The CHT parser preserves each raw integer. Its is_on() helper reports whether that integer is nonzero; it does not reproduce the per-cheat conditions in this table. Callers implementing gameplay must use the raw value and the corresponding condition.
Persistence — REDGUARD.CHT
The savecheats command (cheat #6) writes the 64-slot state array, including the 13 named cheat slots, to a file called REDGUARD.CHT in the game directory. It clears its own slot before writing, then sets that slot to one after closing the file. On startup, the engine checks for this file and restores any previously saved cheat states. Delete the file to discard the saved cheat values. See CHT format specification for the binary layout.
Startup first clears the cheat slots, then loads the optional CHT file. Later, SYSTEM.INI supplies task_system, animation_system, and node_marker, replacing the corresponding task, animation, and nodemarker values. ODDJOB has no such configuration replacement; saved values such as 0, 1, and 2 remain unchanged through system initialization.
Relationship to SOUP flags
Cheat slots and SOUP global flags use separate storage. SOUP’s SetGlobalFlag, TestGlobalFlag, and ResetGlobalFlag operate on the flags declared in SOUP386.DEF; a flag index is not a cheat index. The installed native function bindings do not provide a generic cheat getter/setter or console-command executor.
Native functions can still consult specific cheats internally. For example, PlayerDistance and AcuratePlayerDistance check neversaydie. The console’s getflag and setflag commands access SOUP flags, while cheat names access cheat state directly.
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
Look and feel
The rendered scene combines authored palettes and textures with separate rules for terrain, models, sky, and on-screen presentation. Matching those rules keeps lighting, distance, motion, and framing consistent with the original engine.
Camera framing and distance
View projection fixes the horizontal field of view. Changing the display aspect ratio changes the vertical field of view, so a wider viewport does not simply reveal more of the world on either side.
Fog and clipping determine how geometry disappears into the distance. Scene fog depends on camera-space depth, using a lookup table and the world’s fog color. The sky has its own fog table and clipping distances. Its distant texture blends toward the same color used to fill uncovered background pixels. The RGB565 framebuffer and dithering also affect the final color, especially in gradual fog transitions.
Terrain color and shading
WLD terrain supplies heights, texture choices, and texture rotation. Terrain lighting combines ambient RGB with a directional contribution at each vertex. The resulting color depends on the terrain normals, then passes through polygon clipping, screen-space color interpolation, and texture combination.
The terrain and model lighting paths have different inputs. Terrain does not use the model renderer’s local-light list. World settings for ambient color, sun direction, and directional RGB are described in WORLD.INI.
Models, palettes, and local lights
Model lighting combines face brightness, ambient and directional light, local lights, and retained instance colors. World-entry prelighting stores map-light contributions for static placements and eligible static-model actors. These stored colors continue to matter when the world is drawn.
COL palettes supply the colors of indexed artwork. Palette ownership and transparency rules determine how texture edges and camera-facing flats appear. Model textures, terrain textures, and placed flats have distinct drawing rules; the model reference covers chroma-key rejection, texture filtering, flat blending, and depth writes.
Water shape and changing light
Water waves displace selected terrain vertices. Their phase uses squared distance from the whole terrain midpoint and the BIOS clock. World wave settings control amplitude, speed, and spatial frequency. Moving the camera changes the terrain window, but does not move the ripple origin.
Displacement also changes normals and lighting. The engine updates marked entries and retains the other entries’ earlier values until a later update or grid rebuild. Those rules affect water-edge shading as well as the wave shape. Terrain lighting describes how the changed normals feed back into vertex color.
Sky, sun, and texture motion
The sky is a finite, tilted plane with scrolling texture coordinates. It is visible from either side and can be crossed vertically. Its background fill, fog, scrolling direction, and sun-disc blending each contribute to the visible horizon.
Texture animation supplies motion within textured surfaces. Playback state belongs to a loaded image, so objects sharing that image also share its current frame. Authored delays and explicit frame selection determine animation timing independently of water displacement.
Movement, interface, and sound
Player movement and actor animation determine the pace of movement, turning, and animation. Their coordinate and timing rules connect the camera view to what the player does.
The HUD, book menu, and subtitles define artwork placement, fonts, draw order, and text timing. The book menu also uses 3D models and animated camera paths. Positional sound supplies distance attenuation and stereo pan; music supplies world-specific CD tracks and their switching rules.
Renderer-specific resources
Choose the renderer’s matching model, texture, and palette resources before applying these rules. The asset directory and edition reference describes the available sets and their differences. Software texture banks and shade and haze tables have their own formats. The Glide rendering pages describe that renderer’s behavior.
Actor prelighting
World entry can retain map-light RGB on eligible static-model actors.
The world-entry prelighting pass requires prelighting to be enabled, a nonempty
static-placement list and a nonempty admitted map-light list. The npl
command-line option disables this pass. The same list requirements gate both
static placements and actors; actors alone do not enable it.
An actor qualifies when attribute 122, attr_non_moving, is nonzero and its
runtime model kind selects the static-model path. This includes the
actor-definition shortcut
and the placed actor’s explicit static-model fallback; eligibility does not
depend on which loader selected that runtime kind. The static model must exist.
Actors using an animation model table do not qualify, even when they have only
one frame or do not currently move.
The pass prepares an instance for the actor’s static model and uses the same face-vertex prelighting operation as static placements. Its retained color buffer is registered for world-level release together with the static-placement buffers. The attribute is tested at prelighting time; it is not a test of observed movement or current animation playback.
Placed nonmoving actors initialize their model-instance translation from each signed Q8 position component shifted arithmetically right by eight bits, then converted to floating point. Fractional map units are discarded before the conversion. Placed orientation initializes the same instance matrix from the authored angles. The prelighting pass reuses this prepared translation and matrix when binding the static model; it does not rebuild the transform from the actor’s current position or angles.
Entry and restoration order
Prelighting runs after world-entry loading and before ordinary actor updates. On fresh entry, the actor loader’s attribute defaults, model-path selection, placed orientation and initial group requests have already run. Ordinary script entry, task and main continuations have not advanced for the new entry.
Revisiting a retained world restores its actor records before prelighting. Eligibility therefore uses the restored attribute and model kind, and the operation consumes the restored embedded model-instance transform. The saved script and animation continuations are installed before this pass resumes ordinary updates. Restoring a saved game follows the same ordering: restore world and actor state, run prelighting, then continue ordinary updates. Retained colors are rebuilt by the pass rather than treated as saved animation state.
Texture animation
Glide texture binding advances animated TEXBSI images on demand.
Each loaded image retains a current frame and a next-update deadline. The frame starts unset and the deadline starts at zero. Frames belonging to one image share this state wherever the image is drawn.
An automatic frame request selects frame zero for a single-frame image. For an animated image, it advances by one frame when the BIOS tick count has reached the deadline, or when the current frame is unset. Advancing past the final frame wraps to zero. The next deadline becomes the current tick count plus the image’s frame delay. The first automatic request therefore selects frame zero, even if its previous deadline lies in the future.
The delay comes from the TEXBSI header’s anim_delay: milliseconds multiplied
by 18.2 and divided by 1000, truncated toward zero to an integer tick count. A result of zero
becomes one tick. See the TEXBSI header.
A late request advances only once. It does not catch up through all frames whose durations elapsed. An image that is not bound does not advance merely because time passes. Repeated bindings before the next deadline retain the same frame.
An explicit frame request selects that frame and replaces the retained current frame without changing the deadline. A later automatic request continues from that selected frame. This state belongs to the loaded texture image, not to an individual mesh instance.
Loaded-image lifetime
The loaded-image cache identifies names with a six-digit base-40 key. Its digit
alphabet, in ascending order, is 0123456789abcdefghijklmnopqrstuvwxyz~_#%.
ASCII uppercase letters are converted to lowercase. Conversion stops at the
first null byte, period, character outside that alphabet, or after six
characters. Missing trailing digits are filled with %. The first character
is the most significant digit. Characters after the stopping point do not
distinguish cache entries, and the source bank path is not part of the key.
Looking up an already loaded image reuses its texture record and increments its reference count. It does not reset the current frame or animation deadline, and does not add the active resource group’s ownership bit.
Loading a TEXBSI bank visits every image record in the bank. For each already loaded image, it increments the reference count and adds the selected group’s ownership bit without resetting animation state. Newly decoded images receive the selected group’s ownership bit. Bank ownership therefore includes images that have not been requested by a model or terrain polygon. Repeated records increment references again while retaining the same ownership bit.
Images can belong to multiple resource groups. Releasing a group clears that group’s ownership bit and releases its reference. A record retained by another group keeps its animation state. When no ownership bits remain, group cleanup forces final release even if repeated lookups within that group increased the reference count. Final release frees every frame and clears the image record. A subsequently loaded image receives the initial frame and deadline state.
Book-menu setup saves the selected resource group and selects group 30 for menu loading. Menu cleanup restores the saved selection. Its release argument controls group release: zero retains group 30, and any nonzero value releases it. Closing the book menu requests release; preparing its contents first runs cleanup without release. Releasing menu ownership preserves an image’s animation state when another resource group still owns it.
The banks referenced by MENU.ROB, INVENTRY.ROB and the configured book-page
surfaces contain only single-frame texture images. Their image keys do not
match any animated image in the shipped TEXBSI banks. Shipped TEXBSI image
names consist of six ASCII letters or digits.
World-resource cleanup releases group 31 when world resources are loaded, then clears the loaded state. Repeating cleanup with that state already clear does not release the group again. World transitions perform this cleanup before loading the destination world’s resources.
Model polygon admission
The ordinary actor dispatcher requires both the runtime animation-update exclusion and object-update exclusion to be clear before preparing an actor’s model. Its hidden flag must also be clear before model submission. A static-model actor needs a loaded model. These actor gates precede polygon admission; hiding an actor or excluding it from drawing prevents its model from requesting texture frames.
With PVO disabled, the actor dispatcher’s static and animated PVO eligibility helpers both admit the actor. The pose-preparation helper returns success; it does not add a visibility rejection. The animation exclusions and hidden flag have separate update and presentation effects.
Before model polygons are prepared, draw submission classifies a bound around
the model origin transformed into scaled camera space. Let (x, y, z) be that
origin, r the model header radius in engine units, and (sx, sy) the
projection scales. Its depth interval is [z-r, z+r]; its
horizontal and vertical intervals are [x-hx, x+hx] and [y-hy, y+hy], where
hx = 1.415 * (r*sx) and hy = 1.415 * (r*sy). The decimal multiplier is
1.415. The scaled radii, multiplied side radii and interval endpoints are
stored in single precision.
Submission rejects a depth interval whose upper endpoint is at or before the
near plane, or whose lower endpoint is at or beyond the far plane. It rejects
a side interval whose upper endpoint is at or below -z, or whose lower
endpoint is at or above z. Otherwise it records each plane crossed by an
interval. An endpoint exactly on a plane does not count as crossing it.
No crossed planes selects the unclipped polygon preparation path; any crossed
plane selects the clipped path. A rejected bound does not enter the draw list.
Ordinary model drawing visits the authored polygons in order. A polygon passes its facing test only when the vector from the local-space camera position to its first authored vertex has a strictly negative dot product with the selected frame’s face normal. A zero result rejects the polygon. Both shaded and unshaded drawing use this test. Solid-color polygons do not bind a texture.
Queued-model draw flag 0x04 selects a separate explicit-frame path. This path
prepares only the model’s first polygon, bypasses the ordinary facing test and
uses the instance’s explicit texture-frame selection. Ordinary drawing requests
automatic frame selection. Shipped ROB segment attribute bytes use only
0x00, 0x02 and 0x40; none selects the explicit-frame path when copied
unchanged into the queued-model draw flags.
Both paths prepare a polygon before binding its texture. They bind only when preparation returns at least three vertices. Earlier object rejection still applies.
The clipped preparation path transforms vertices into camera space with the
horizontal and vertical projection scales already applied.
It classifies each vertex against the near and far depth planes and the four
side planes x = -z, x = z, y = -z, and y = z. Points exactly on a
plane are inside. A polygon whose vertices are all outside the same plane is
rejected immediately. Otherwise, clipping visits the near, far, left, right,
negative-Y and positive-Y planes in that order, as needed.
Preparation returns the remaining vertex count. It does not require projected area or covered pixels: coincident vertices can still satisfy the count check. The unclipped preparation path retains the input vertex count. Thus texture binding admission is distinct from both an object’s visibility bound and the eventual rasterized coverage.
Placed flat admission
Ordinary MPSF flats enter the same bounded draw queue as models, using their generated sprite bound and camera-facing transform. Their drawing flags select the first-polygon path described above, so they bypass the ordinary face-normal test. Their instance frame selection is automatic. After polygon preparation returns at least three vertices, they request a frame from the shared image timeline before drawing.
The generated bound radius uses the source image width and height before texture
scale. Form the single-precision squared diagonal width² + height², apply the
quantized square-root table, and halve that
result with a single-precision store. Multiply by the texture scale and the
submitted geometry scale, then store the radius as single precision. The
ordinary MPSF geometry scale is 1. The bound uses the same projection-scaled
sphere classification as queued models; it is not the exact Euclidean diagonal
of the final quad.
A flat rejected by its bound or polygon clipping does not request a frame. A flat that survives preparation can request a frame even if it covers no pixels. Using the first-polygon path does not make the request an explicit frame override when the instance selects automatic playback.
Terrain triangle admission
Terrain submits the cells in its camera window. Each cell tests its two triangles separately. The dot product of the vector from the camera to the cell’s anchor vertex with that triangle’s stored face normal must be strictly negative. A zero result rejects the triangle.
An admitted triangle copies its three prepared camera-space vertices and classifies them against the same six clipping planes used for model polygons. A common outside plane rejects it. Otherwise it clips as needed and requires at least three remaining vertices before binding the cell’s texture with an automatic frame request. The texture request precedes polygon drawing. Clipping boundaries are inclusive, and coincident vertices can still produce an automatic request. Rejected or back-facing terrain triangles do not advance their texture image.
Glide interface
RGFX.EXE, the Glide build of the engine, renders through the 3dfx Glide 2
library and contains no Glide implementation of its own.
Library binding
The executable reaches Glide through the DOS overlay interface. It carries one
five-byte stub per Glide 2 entry point, 124 in all, and a parallel table of the
decorated entry-point names, such as _GRBUFFERSWAP@4. The first call through
any stub loads the glide2x overlay; each stub binds its entry point by name
when it is first called. The 124 names are the complete public Glide 2.4x
interface.
The game never asks Glide for its version. Any Glide 2 overlay that exports the called entry points can run it. The game CD contains no Glide implementation, so the player’s installed 3dfx driver supplied the overlay.
Implementations in the GOG release
| File | Implementation |
|---|---|
DOSBOX/glide2x_emu.ovl | 3dfx Glide 2.42 DOS overlay with Voodoo Graphics (SST-1) and Voodoo Rush (SST-96) initialization. It drives the hardware directly and runs on emulated Voodoo hardware. |
DOSBOX/glide2x.ovl | Small forwarding overlay that passes Glide calls to the host through DOSBox’s Glide port. |
DOSBOX/Glide2x.dll | OpenGLide 0.09rc9 host wrapper, configured by DOSBOX/glide.ini. |
Redguard/glide2x.dll | nGlide host wrapper. |
Startup presentation describes which path the GOG launch uses and which splash each one shows.
Called entry points
The executable calls 38 of the 124 entry points.
| Group | Entry points |
|---|---|
| Startup and shutdown | grGlideInit, grSstQueryHardware, grSstSelect, grSstWinOpen, grGammaCorrectionValue, grGlideShutdown |
| Buffers | grRenderBuffer, grBufferClear, grBufferSwap, grColorMask, grLfbLock, grLfbUnlock |
| Depth | grDepthBufferMode, grDepthBufferFunction, grDepthMask, grDepthBiasLevel |
| Textures | grTexSource, grTexDownloadMipMap, grTexDownloadTable, grTexMinAddress, grTexMaxAddress, grTexFilterMode, grTexClampMode, grTexMipMapMode, guTexCombineFunction |
| Color and alpha | guColorCombineFunction, guAlphaSource, grConstantColorValue, grAlphaBlendFunction, grChromakeyMode, grChromakeyValue, grDitherMode |
| Fog | grFogMode, grFogTable, grFogColorValue |
| Drawing | grDrawTriangle, grDrawLine, grDrawPoint |
The game draws only independent triangles, lines and points. It uses none of
Glide’s antialiased, polygon or clipping draw calls, and none of the
guFogGenerate* helpers: it builds its own fog tables.
It sets texture and color combination only through the gu convenience
functions, never through grTexCombine, grColorCombine or grAlphaCombine.
Glide’s texture memory manager, 3DF loader, culling, clip window and alpha test
are unused. Linear framebuffer access uses grLfbLock and grLfbUnlock, never the region
read and write calls.
Window and initial state
Opening the Glide window requires grSstQueryHardware to report at least one
board; otherwise Glide startup fails. The engine selects board 0 and opens the
window at the configured resolution with these parameters:
| Parameter | Value |
|---|---|
| Refresh | 60 Hz |
| Color format | ARGB |
| Origin | Upper left |
| Color buffers | 2 |
| Auxiliary buffers | 1 |
It then sets this state:
| State | Value |
|---|---|
| Render buffer | Back buffer |
| Depth buffer | W-buffer, GR_CMP_LESS, depth writes enabled |
| Texture clamp (TMU 0) | Wrap in S and T |
| Mipmapping (TMU 0) | Disabled, no LOD blending |
| Texture filter (TMU 0) | Bilinear minification and magnification |
| Texture combine (TMU 0) | Decal |
| Color combine | Texture times iterated RGB |
| Alpha source | Constant-color alpha |
| Dither | 4 by 4 |
| Fog | Disabled |
Each drawing path changes this state for its own draws. The shading overview links the per-path rules, and the HUD lists the state its overlay sets and restores.
External References
- Glide 2 API constants
- Glide 2 video modes and refresh rates
- Glide 2 state functions
- Glide 2 convenience functions
Glide shading
The world renderer calculates lighting at polygon corners and combines the interpolated lighting colors with textures, fog and framebuffer color precision.
Flat and smooth shading
Lighting produces an RGB color at each source corner before clipping and rasterization. The rasterizer interpolates these colors across each submitted triangle, a process called Gouraud shading. It interpolates lighting colors rather than normals; it does not evaluate the lighting formula anew per pixel.
Normal selection is a separate step. A face normal shared by all corners gives the same directional-light contribution at each corner. Different corner normals can give different contributions and a smoother appearance across neighboring faces. Local lights also depend on corner positions, so a shared face normal does not require identical corner colors or a uniformly lit face.
Terrain constructs a shading normal at each grid vertex from the normalized normals of its incident triangles, then normalizes the sum. These operations use the engine’s quantized reciprocal square root. Models instead consume supplied normals, retaining their magnitude. The model format describes face normals, vertex normals, corner indirection and face-normal fallback.
Color pipeline
- Prepare corner lighting from ambient RGB, directional light and, for models, face brightness and admitted local lights. Retained instance prelighting can replace part of the model calculation; actor eligibility depends on the runtime model kind and attributes, not simply whether an actor is stationary.
- Clip polygons while carrying their corner RGB and texture coordinates, then project and submit the surviving polygons as triangle fans.
- Interpolate lighting RGB linearly in screen space. Texture coordinates use perspective correction. Sampling uses the selected palette, allocation and address rules, and the filtering mode selected by the drawing path.
- Modulate sampled texture RGB by the interpolated lighting. Solid-color model faces incorporate their resolved palette color into corner lighting instead. Texture filtering and shading are independent: a smoothly lit surface can still use point-sampled textures.
- Apply fog on encoded RGB channels and the framebuffer’s RGB565 quantization and dithering rules. These are separate from the lighting calculation. The world lighting path has no scene exposure or filmic tone-mapping stage.
This describes the ordinary model and terrain color paths. Placed flats have their own constant-alpha blending and ordering rules. Sky and sun use separate drawing and blending paths. Water changes terrain geometry and shading normals; its displacement and normal-update rules are specified separately.
Texture residency
The Glide renderer defers texture-residency rebuilding behind a pending flag. With no rebuild pending, the routine leaves texture assignments and the cached last-bound texture and frame unchanged. A pending rebuild clears that flag, invalidates both binding-cache entries and the active textures’ residency assignments, then assigns texture-memory slots and uploads eligible resources. Inactive resource records are left unchanged.
This rebuild changes texture residency and binding caches without selecting a renderer palette. Inventory opening invokes it after its text and item-list setup, before the first inventory draw. It retains the palette selected by the caller rather than selecting an inventory palette.
Detailed rules
| Topic | Reference |
|---|---|
| Model normals and hard edges | Model shading selection |
| Model brightness, directional light and local lights | Model lighting |
| Retained map-light colors | World prelighting and actor prelighting |
| Terrain lighting and averaged normals | Terrain vertex calculation and shading normals |
| Clipping, projection and color gradients | Model clipping and projection, terrain preparation, screen-space color setup and view projection |
| Texture coordinates, filtering and modulation | Model texture addressing, model draw flags, terrain texture setup and texture combination |
| World palette conversion | COL palette precision and model palette ownership |
| Fog, framebuffer precision and dithering | Fog lookup and integer precision |
| Camera-facing flats and transparency | Placed flat drawing and framebuffer blending |
| Sky and sun | Sky plane and sun framebuffer blending |
| Animated terrain and textures | Water waves and texture animation |
Glide model lighting
Model lighting supplies encoded RGB at each polygon vertex before rasterization.
See the shading overview for flat and smooth shading, the color pipeline, and links to the related terrain, palette and framebuffer rules.
Texture transparency
The queued-model draw flags come from the ROB segment’s attribute byte for ordinary archive model submission. Bit 0 selects point filtering instead of bilinear; bit 1 enables chroma-key rejection for textured faces. See ROB segment attributes.
The software Voodoo 1 pipeline compares the filtered texture RGB with the chroma-key RGB before applying vertex-light modulation. A matching RGB is rejected. Filtering uses the encoded palette colors, including the key color; it does not interpolate a transparent alpha mask and apply an alpha cutoff.
Display initialization selects indexed palette texture mode. Each texture’s chroma key is the selected palette’s entry-zero RGB, copied without channel quantization. Palette selection uploads the complete 256-entry RGB table to texture unit zero. Palette-zero texels retain their RGB through filtering even when the material enables chroma rejection.
Palette ownership
Indexed textures use the palette table selected for the draw, including textures
with an embedded BSI CMAP. Reading that record does not select or upload a new
palette. World rendering uses the converted world COL table,
so its six-bit channel precision applies before model texture filtering as well
as terrain filtering. The book menu selects a separate interface palette and
restores the world palette when closing. Sky drawing temporarily selects its
own palette and restores the previously selected table afterward.
The world-palette adjustment path adds a separate signed offset to each input
channel, then multiplies by that channel’s fixed-point scale. Addition and
multiplication retain the low 32 bits. The signed product is shifted right by
eight bits with sign extension, then clamped to 0..63. A scale of 256 is unity
when the arithmetic does not overflow. The adjusted channels are expanded by
multiplication by four; palette index zero is forced to black. The resulting
table is installed and selected before the adjustment routine returns.
Startup initializes all three palette channel offsets to zero. World entry resets all three fixed-point scales to 256 before installing its destination palette.
World fade opacity is separate from the indexed palette table. The fade setter
negates its signed fade input, clamps the result to 0..256, and stores the
black-overlay opacity without reading the supplied palette or selecting a table.
When the initial world-view fade completes, the world loop applies the
world-palette adjustment again before clearing the overlay. Intermediate fade
opacity values therefore do not specify different palette RGB values.
Indexed solid-color model faces resolve their color index through the selected palette when the runtime model is prepared. The resolved RGB is stored in the runtime face, rather than looked up again for every draw. Direct packed-color faces follow their separate decoding path.
Actor texture-set substitution preserves this prepared solid RGB. It changes eligible texture-bank references, not solid colors. ROB draw flags select sampling and chroma rejection independently of the palette that supplied the retained solid RGB.
World entry installs and selects the converted destination palette before preparing its ROB archive. Indexed solid faces in that archive therefore retain the converted world RGB, including black palette index zero, before model lighting. Loading-progress drawing temporarily selects the interface palette and restores the renderer-selected palette before model preparation continues.
Inventory model preparation is deferred. Loading INVENTRY.ROB for an examined
item suppresses the archive loader’s automatic model preparation. After loading,
the item model named by ITEM.INI is resolved and explicitly prepared. That
preparation resolves indexed solid colors through the palette selected at that
point; the inventory model-loading helper does not select a palette itself.
The inventory screen draws before handling View. From that draw’s return through
Examine’s item lookup and clock pause to model preparation, the selected palette
is unchanged.
Placed flat drawing
Ordinary MPSF flats use point-sampled textures and palette chroma-key rejection. They use constant alpha, independent of texture alpha. An alpha input below 254 selects source-alpha and one-minus-source-alpha RGB blending; an input of 254 or greater selects source replacement. The constant alpha channel is the low byte of the rounded alpha input. Alpha-channel blend factors select source replacement in both cases. The alpha input and its default come from the FLAT attributes.
Their generated local face normal is (0, 0, -1). The camera-facing instance
matrix also transforms local-light positions into the generated quad’s local
coordinates before applying the ordinary vertex-light calculation.
Flat submission flag 0x400 instead makes the instance upright. It sets the
local up axis to world up, removes the vertical components from the other two
axes, and normalizes their horizontal projections independently. Ordinary MPSF
submission cannot select this flag: its base flag is 1, combined with the
byte-sized drawing flags from a matching FLAT attribute.
Flat submission does not test drawing flags 0x02, 0x04, 0x08 or 0x10.
A FLAT attribute that sets any of them draws the same as one that clears them.
These flats retain depth writes and the less-than depth test. Textured RGB uses the ordinary vertex-light modulation path. Their submitted face brightness is 255 when the alpha input is below 254; otherwise it comes from the current flat-brightness input. This is a face-brightness contribution, not a replacement for the subsequent lighting calculation.
Flat image dimensions
A centered ordinary flat spans the source image width and height multiplied by its texture scale and submission geometry scale. Its half extents use half of those dimensions. Its bounding radius is the quantized source-image diagonal multiplied by half the same scale. Neither geometry nor bound uses the reduced texture allocation dimensions, and flat submission has no 64-texel size cutoff.
The texture descriptor rounds allocation dimensions up to powers of two, capped
at 256. Allocation sizes of 128 and 256 use the corresponding reduction factors
from SYSTEM.INI [3dfx] text_scale. The pair lists the 256-size factor first
and the 128-size factor second. Missing settings default to 4,2. A factor of
one disables reduction.
Source dimensions and the image’s texture scale remain separate from these
allocation and sampling parameters.
Texture allocation and indexed upload
The Glide allocation descriptor supports power-of-two dimensions from 1 through
256. Its aspect ratio is limited to 8:1: when the requested pair is more
elongated, the smaller allocation dimension increases to one eighth of the
larger. For maximum allocation dimension M, both fixed sampling levels are
8 - log2(M).
With unit horizontal and vertical sampling strides, the descriptor’s two
coordinate scales are both 256 / M. Setting source dimensions smaller than
the allocation preserves those scales, including non-power-of-two source
sizes. Source-image dimensions and allocation dimensions remain distinct.
The indexed upload path with unit strides copies source texels row by row. Unused columns to the right and unused rows below are filled with palette index zero. It does not stretch or repeat the source image into that padding. A supplied source row pitch controls the input row stride; the default pitch is the source width.
For BSI loading, each source axis is rounded up to a power of two before
applying its configured 128- or 256-size reduction factor. Each axis retains
that factor as its sampling stride. Aspect-ratio padding follows reduction.
For source dimension D, sampling stride S, and maximum reduced allocation
dimension M, the coordinate scale is 256 * floor(D / S) / (M * D), stored
as a 32-bit float. Source dimensions themselves are retained without reduction.
The fixed sampling level is 8 - log2(M).
Indexed reduction samples source columns and rows starting at zero and stepping by their respective strides. It does not average neighboring texels. Remaining allocation columns and rows are still filled with palette index zero.
Texture address selection
Glide initialization sets texture unit zero to wrap in both directions. Ordinary model binding retains that address mode. Wrapping uses the allocation dimensions, including zero-filled padding, rather than the original source-image dimensions.
In the DOSBox-X Voodoo path, point filtering selects an integer texel from each
signed perspective coordinate Q by arithmetic shift right by level + 18,
then masks with the corresponding allocation dimension minus one. It does not
round to the nearest integer. Bilinear filtering instead selects the neighboring
pair and four-bit fractions using the
fixed texture address rule.
The texture’s fixed level is the allocation level described above.
Flat submission order
Ordinary placed flats prepend to the spatial list selected by the low sixteen bits of their integer X/Z placement coordinates, shifted right by nine bits. Their active list appends those cell lists in the normal runtime spatial selection order. Within each cell, this reverses file insertion order. Static models are submitted before these flats. Bound rejection happens before queue insertion; polygon clipping happens later, while drawing the queue.
The normal queue’s alpha partition does not compare depth. For ordinary flats,
alpha below 254 selects the partitioned category. When that category is present,
a tail cursor starts at the final queue entry and skips selected entries while
it is greater than zero. A forward cursor visits indices strictly below
queue_length - 1 - selected_count. Whenever the forward entry is selected,
it swaps with the tail entry, then decrements the tail and again skips selected
entries while greater than zero. The resulting order is not stable and need not
place every selected entry after every other entry. With an opaque model prefix,
partitioning the flat suffix independently gives the same flat order.
Flat framebuffer blending
Ordinary flat drawing enables four-by-four framebuffer dithering and alpha dither subtraction. In the software Voodoo 1 renderer, constant-alpha blending uses the same integer destination reconstruction and blend arithmetic as the sun, with the flat’s constant eight-bit alpha in place of filtered texture alpha. Source RGB is the encoded, lit scene color. The separately truncated source and destination terms are added before final RGB565 framebuffer quantization. Depth testing and depth writes remain enabled for these blended fragments.
Draw-flag ownership
A newly initialized render instance has zero draw flags and constant alpha 255. Animated actor group loading checks the first group’s name against the loaded ROB model lists. When that lookup succeeds, it copies the segment attribute byte into the actor’s render instance. It also replaces constant alpha when the segment alpha is between 1 and 253 inclusive. A missing archive entry leaves those initialized values intact. Later groups do not repeat this assignment. These are group-loading assignments, not per-frame filtering defaults.
Ordinary static-model submission refreshes the instance’s draw flags and constant alpha from the selected archive model’s prepared metadata before queueing it. The archive loader copies the segment attribute byte into that metadata and maps segment alpha 0, 254 and 255 to constant alpha 255. Other segment alpha values are retained. Static-model name lookup searches the loaded ROB model lists and reports an error when no entry exists; it does not use the standalone animated-model file loader as a fallback.
Special flat lighting bypass
Flat submission flag 0x80 selects a constant-color drawing path. The three
inputs otherwise used for face brightness, constant alpha and geometry size
instead supply red, green and blue. They are truncated to integers and packed
as 0x00RRGGBB. The queued instance uses draw flags 0x0c, constant alpha
255 and face brightness 255. Submission flags 0x100 and 0x200 still add
their corresponding depth flags.
This path clears the retained face-vertex RGB source and cursor, the selected local-light count and the transformed directional-light vector before polygon preparation. It does not shade the flat through the ordinary retained-color, directional or local-light calculation. Its texture-coordinate extents use twice the source dimensions times the texture coordinate scales, independently of the three color inputs.
Actor-flat drawing starts with submission flags 1 and selects special mode from
bit 0x80 of the matching texture attribute in the map’s FLAT table.
A missing attribute leaves the ordinary path selected. The selected image frame
is passed separately and does not add submission flags.
Actor-flat drawing adds submission flag 0x200 to this special path and supplies
red, green and blue values of 200. The queued polygon draw selects constant RGB
without multiplying it by sampled texture RGB. It still binds the image and
passes its texture coordinates, but the color combiner uses the supplied RGB
rather than texture or interpolated vertex RGB. The ordinary texture-times-vertex
color mode is restored after the queued draw completes.
The special queued draw selects texture alpha rather than the instance’s
constant alpha. RGB blending uses source alpha and one minus source alpha;
the alpha-channel blend factors select source replacement. Binding the image
with draw flags 0x0c disables chroma-key rejection, including when the image
and frame already match the texture-binding cache. Draw flag 0x02 enables
chroma rejection and supplies the image’s chroma-key RGB instead. Texture-alpha
selection is independent of the constant-RGB color selection.
The indexed image path prepares and binds eight-bit palette textures. In the software Voodoo 1 renderer, palette lookup supplies alpha 255 for every index, including index zero. The indexed byte selects a palette entry; it does not supply an alpha value. Bilinear filtering of these palette samples preserves their constant alpha. This palette format is separate from the alpha-palette format available on Voodoo 2.
The three color inputs do not request a geometry scale or constant alpha.
The special path keeps the ordinary flat’s vertex positions at the caller’s geometry scale. Horizontal and vertical flip flags still reverse its local axes; the three constant-color inputs do not resize or reposition its vertices.
Ambient and directional contribution
Models share the ambient RGB, directional RGB and normalized world direction specified in terrain-lighting.md. The model transform maps the world direction into model coordinates using the transpose of its linear matrix. Shading consumes the supplied model normal without normalizing it again.
Each face adds its unsigned brightness byte to all three ambient channels.
For ambient A, face brightness B, directional strength S, transformed
direction D and supplied normal N, the contribution in each channel is
A + B + max(0, -dot(D, N)) * S.
Local lights add to this contribution before each channel is clamped to
0..255. For a solid-color face, the clamped channels are then multiplied
by the corresponding palette RGB channel divided by 256. Textured faces
use the resulting RGB to modulate the texture. RGB interpolates in screen
space, while texture coordinates use perspective correction. There is no
physical material response, scene exposure or filmic tone mapping.
The runtime face’s brightness is the byte following its vertex count. Model
preparation shifts the stored face byte left by two bits and retains the low
eight bits. In v4.0/v5.0 model files the stored byte is the field named tex_hi
in the model format, so the runtime contribution is
(tex_hi * 4) & 255. Generated runtime faces supply their brightness directly
and do not pass through this file-model conversion. The packed runtime material
marks solid colors separately from texture references.
A model instance may retain precomputed per-face-vertex RGB. That RGB replaces the ambient, face-brightness and directional contribution, including its solid color tint. Local lights still add to it, followed by channel clamping; the solid tint is not applied a second time.
Polygon clipping
The clipped model preparation path computes each source corner’s RGB before clipping. It transforms positions into projection-scaled camera coordinates and uses the same six-plane volume and plane order as terrain polygon preparation.
At a crossing edge, RGB and texture coordinates use the same linear edge parameter as camera position. Generated components are stored as 32-bit floats. RGB is not multiplied by depth or reciprocal depth during clipping. Projection and screen-space RGB interpolation occur after this preparation. Consequently, clipping depth-weighted RGB and dividing it by the new depth is not equivalent to the model preparation rule.
Polygon projection
The ordinary model submission path projects the surviving clipped polygon as an ordered triangle fan. The first corner remains the anchor; each subsequent pair of corners forms the next triangle.
For each corner, reciprocal camera depth is stored as a 32-bit float before
projection. Screen X and Y use that stored reciprocal, the viewport center
and the corresponding viewport scale. Screen X has a 32-bit float store before
adding the bias 786432; Y retains the wider intermediate until the biased
store. Both biased screen coordinates are stored as 32-bit floats. This is the
same 1/16-pixel quantization used by
terrain projection.
The submitted corner retains its clipped RGB. Its vertex alpha is 255.
Each texture coordinate is multiplied by the stored reciprocal depth, and
that product is stored as a 32-bit float for Glide. Reciprocal depth is also
submitted separately for perspective correction.
Screen-space color setup
Ordinary model fans use the same Glide triangle color-gradient setup as terrain. The submitted RGB values become separate starting-color and screen-gradient registers after screen-Y ordering. Each fan triangle receives its own setup.
The installed Glide driver’s iterated-color and texture-times-iterated-color modes enable subpixel adjustment whenever the color-combine state is set. This does not depend on the prior subpixel-enable bit. Window initialization clears the RGB/depth clamp state; color-combine changes preserve that state. The software Voodoo renderer’s signed color conversion and subpixel adjustment therefore apply to the default model path as well as terrain.
Ordinary textured model drawing selects texture-times-iterated-color. Its encoded texture-byte modulation uses the same integer texture combination as terrain. Solid-color submission uses iterated RGB without a texture product.
Local-light model selection
A local light carries a world position, signed RGB strengths and a radius in
engine coordinates. Registration retains its squared radius and an attenuation
numerator (radius * radius + 3 * radius) / 256.
For model origin O, model radius R, light position L and light radius r,
the light is selected when dot(L - O, L - O) - R * R <= r * r.
The boundary is inclusive. This differs from testing whether two spheres overlap.
The model radius is the stored model bound; the selection test does not scale it
by the instance transform.
Selected light positions are transformed into model coordinates by multiplying
L - O by the transpose of the model’s linear matrix. This uses the matrix
entries directly, including any scale.
Local-light vertex contribution
For a selected light at transformed model-space position L, let
V = vertex_position - L and q = dot(V, V). Normalize V with the
quantized reciprocal square root described in
terrain lighting, retaining the
supplied model normal’s magnitude. Only a negative dot product between that
normalized vector and the supplied normal contributes light.
The distance calculation uses a separate quantized square-root table. For
unbiased exponent e of q and its upper seven fraction bits m, the index is
m + 128 * (e & 1). Table entry i contains the upper seven fraction bits of
the single-precision square root of (1 + (i % 128) / 128) * 2^(i / 128),
where division of the index by 128 is integer division. Combine those bits
with exponent floor(e / 2) + 127 to obtain distance d.
The attenuation is (radius * radius + 3 * radius) / (256 * (q + 3 * d)).
A negative attenuation contributes nothing. Multiply a nonnegative attenuation
by the negated normal dot product, then multiply each signed RGB strength by
that weight and add it to the current channel. There is no per-vertex cutoff
at the light radius. A vertex coincident with the light contributes nothing
because its normalized delta and dot product are zero.
Accumulate local lights before clamping and solid-color tinting. Retained
instance RGB receives local contributions and clamping without a second tint.
The distance-table initializer selects single-precision x87 arithmetic with
round-to-nearest; intermediate arithmetic rounds to single precision. The
attenuation denominator evaluates as (2 * d + q) + d; its numerator evaluates
as ((2 * radius + radius * radius) + radius) / 256.
Retained instance colors
Prelighting traverses faces in authored order and vertices in each face’s
order. It uses each face’s brightness and material together with the vertex
position and supplied normal, through the same model-lighting calculation.
The resulting clamped, tinted RGB channels are rounded to integers with
round-to-nearest, ties to even, then packed as 0x00RRGGBB. The retained
array contains one packed entry per face vertex, including repeated mesh
vertices shared by different faces. An existing instance buffer is reused
when prelighting is recalculated.
World prelighting
World prelighting is enabled by default. The npl command-line option disables
it. The world-entry pass requires both a nonempty static-object list and a
nonempty admitted map-light list. It registers the map lights and bakes retained
face-vertex colors for the static instances. With either list empty, this pass
does not bake those instances.
For world-entry prelighting, spatial cells are allocated in first-occurrence
order: static placements first, then admitted map lights. The cell key is
((z & 0xfe00) >> 2) + ((x & 0xffff) >> 9) using the low coordinate bits.
Lights prepend to their cell’s list. Collection concatenates cells in ascending
allocation order, retaining reverse insertion order within each cell. Light
registration and model selection preserve this order for accumulation.
Ordinary world drawing registers the map-light list only in editor mode. Outside that mode, the retained colors carry the baked map-light contributions; the map lights are not reapplied by that draw loop. Other registered local lights can still contribute through the retained-color shading path.
Local-light records
MPOB object kind 6 defines a runtime local light. With objects_load enabled, its
signed world ID must match the current world; its activation byte does not
gate this branch. The loader initializes the light as locked and enabled,
with zero positional offset and no intensity or radius modulation. Current
and starting intensity, radius and signed RGB come from the authored record.
These objects enter spatial-cell lists separately from MPSL map lights.
Outside editor mode, world drawing submits their enabled, positive-radius
lights from the active object list before drawing static models. Their current
signed RGB and radius contribute through the local-light calculation, including
when a static model already has retained colors. They are not part of the
world-entry map-light bake.
RgmFile::object_lights(world_id) returns these admitted authored records in
file order, before runtime script changes and active-region selection.
Script light state and cleanup
Actor-attached light records retain a lock flag, an enabled flag and an unsigned byte inactivity counter. During light maintenance, a nonzero lock forces the enabled flag on.
LightOff returns zero. If the actor has no light record, it does not allocate
one. Otherwise it clears the lock, enabled flag and inactivity counter without
changing the retained offsets, intensity, radius, colors or modulation settings.
It does not immediately release the record.
When an actor light is submitted for drawing, the engine resolves its retained offset through the actor’s current orientation and uses the resulting position for the local-light record. The light therefore follows the actor’s current pose; a map placement position is not a permanent substitute for the actor’s runtime position.
While a record is disabled, maintenance adds the low byte of the elapsed tick count to its inactivity counter with byte wrapping. It releases the record and clears the actor’s association only when the resulting unsigned counter exceeds 32. Enabled records do not accumulate this inactivity counter.
Light(radius, intensity) enables the record and replaces both the starting and
current radius and intensity. LightRadius and LightIntensity each replace
both copies of their respective value, enable the record and clear its inactivity
counter. These setters store the low sixteen bits of their arguments.
Modulation maintenance
The Glide executable maintains light records on both eligible ordinary actors and active spatial objects during its normal world update. Ordinary actor maintenance precedes that actor’s ordinary SOUP dispatch. Spatial-object maintenance follows its SOUP dispatch, before the temporary actor state is written back to the object. Actor eligibility and maintenance bypasses still apply. Ordinary actor light state advances even when the drawing pass does not submit it as a local light.
LightFlicker(amplitude, period) configures intensity modulation;
LightSize(amplitude, period) configures radius modulation. Each stores its
arguments as sixteen-bit words and resets its phase timer to zero. Neither
setter enables a disabled light record. A zero amplitude disables that channel’s
phase advancement and random draws.
Enabled records process intensity before radius. Each active channel adds the shared whole nominal-tick increment to its sixteen-bit phase with wrapping. The resulting phase and period are compared as signed sixteen-bit values. When the phase is at least the period, it resets to zero and the channel takes one random sample. Excess ticks do not carry forward or produce additional samples in that maintenance pass. These are the same nominal ticks used by animation, rather than render frames or milliseconds.
For positive amplitude A, the new value is the channel’s starting value plus
bounded_random(A) - floor(A / 2), stored with sixteen-bit wrapping. Intensity
has no lower clamp. After both channels, the current radius is interpreted as a
signed sixteen-bit value and raised to one if it is smaller. This radius clamp
also applies when neither channel takes a sample. Disabled records perform
inactivity cleanup instead of modulation.
The shared random generator advances a wrapping unsigned 32-bit state with
state = state * 1103515245 + 12345 and returns (state >> 16) & 32767.
The bounded helper consumes one such result. For positive bounds at most
16384, it masks the result with one less than the smallest power of two that
is at least the bound, with a minimum mask of one. Larger positive bounds use
the unmasked result. It then subtracts the bound repeatedly until the result
is smaller than the bound. This produces a biased distribution for many
bounds; it does not redraw rejected values. Actor order and intensity-before-
radius order therefore determine which samples each channel consumes.
Glide submission boundary
The normal Glide world draw keeps ordinary model actors and lightweight spatial objects in separate active lists. It registers light descriptors from the active spatial-object list. Owning a light record does not add an ordinary model actor to that list, and the ordinary model-actor draw traversal does not register its light record. Script-created light state on such an actor is therefore not an additional local-light emitter in this drawing pass.
Local-light registration forwards the current signed RGB strengths and radius. The retained intensity word is not an input to this registration and does not scale RGB during submission.
Runtime object-light region
The normal world loop selects spatial lists using the target camera’s integer
X/Z coordinates before camera smoothing. Each axis starts at target - 4608.
If that value is negative, the start is zero; otherwise, when target + 4608
exceeds 65535, the start is 56319. Shift each start right by nine bits to obtain
the first cell coordinate.
Selection visits an 18-by-18 cell square in row order, omitting its four corners. The grid has 128 cells per row. Within a cell, object lights follow reverse insertion order. Lists are reused while the first selected cell remains the same. The final rendered camera position can differ from the target used for this selection.
Map-light records
RgmFile::prelight_lights(world_id) returns the world-entry accumulation order
when static loading is enabled. RgmFile::local_lights(world_id) returns
admitted authored records in file order with signed
RGB, radius and positions in engine coordinates. RGM inspection metadata retains
these values and the activation and world fields. Placement extraction does not
convert them to standard punctual lights: their signed contribution and custom
falloff require engine shading.
Glide view projection
The Glide renderer uses a fixed horizontal half-field tangent of 0.788 for
world and book-menu rendering. The viewport is initialized to the full display.
For viewport width w, height h, and top-left corner (left, top), the
normalized horizontal and vertical projection scales are:
sx = 1 / 0.788
sy = (w / h) / 0.788
The viewport center is (left + w/2, top + h/2). For camera-space coordinates
(x, y, z), with positive Y down and positive Z forward, screen coordinates are:
screen_x = left + w/2 + (w/2) * x / (0.788 * z)
screen_y = top + h/2 + (w/2) * y / (0.788 * z)
The equivalent vertical field of view is 2 * atan(0.788 * h / w), about
61.166 degrees at 640 by 480. The horizontal field of view stays fixed when
the viewport aspect ratio changes.
The renderer subtracts its view position from world coordinates before applying the view matrix. Horizontal and vertical projection scales multiply the first and second rows of that matrix; the depth row remains unscaled.
See fog and clipping for depth ranges and book menu for its camera paths.
Glide terrain lighting
Terrain receives an RGB lighting value at each vertex, after its normals are computed and before rasterization.
See the shading overview for flat and smooth shading, the color pipeline, and links to the related model, palette and framebuffer rules.
World inputs
World loading sets the Glide ambient channels from
world_ambientrgb[N]. If that entry is absent, the channels default to
128,128,128.
The directional-light channel strengths are the first three components of
world_sunrgb[N], defaulting to 128,128,100 when the entry is absent.
The fourth component does not multiply those channels in the terrain lighting
calculation. World loading constructs the direction using world_sunangle[N]
and world_sunskew[N] and the engine’s trigonometric tables. The direction
setter normalizes a nonzero vector. A zero vector disables directional lighting.
These are the Glide terrain inputs. The world_sun[N] vector and intensity,
world_ambient[N], and world_ambientfx[N] are not operands of this terrain
vertex calculation. The visible sun image has separate tint inputs described
in sky.md.
The installed ISLAND day entry uses ambient RGB 64,64,64 and directional RGB
255,255,255. Its world_sunrgb fourth component is 2, which does not double
those directional RGB values.
Vertex calculation
Let A be the ambient RGB vector, S the directional RGB
strength, D the normalized light direction, and N the supplied terrain
vertex normal, expressed in the same renderer coordinate system.
The directional weight is max(0, -dot(D, N)). With directional lighting
disabled, the weight is zero. Each output channel is
clamp(A[channel] + weight * S[channel], 0, 255).
The routine consumes the supplied normal without normalizing it again. It retains floating-point RGB values at this stage. It does not apply material roughness, specular lighting, inverse-square attenuation, or the model renderer’s local-light list to this terrain calculation.
Terrain updates
Rebuilding the camera’s terrain grid recomputes face normals, then vertex normals, then vertex lighting. The wave update repeats these stages for dirty terrain entries after changing heights. Unmarked entries retain their earlier normals and RGB, including the omitted diagonal at water edges. The admission set and rebuild lifetime are specified in water.md.
Direction and exported coordinates
The direction before normalization is formed from the shared
float rotation table. Its components are sin(angle) * 16000,
sin(angle + 512) * 16000, and sin(skew) * 16000, each truncated toward zero
to an integer. The table sample at index n is
sin(n * 6.2831852 / 2048), stored as a 32-bit float. Missing angles are zero.
For ISLAND day, angle 256 and skew 512 produce the integer vector
11313,11313,16000, whose normalized direction is approximately
0.499984324,0.499984324,0.707128882.
For the interior terrain rows, source row r corresponds to engine grid
row 256-r. The GLTF terrain position uses negative source X, positive
height, and negative source row, with 20 engine units per exported unit.
Consequently direction and normal components transform as [-X,-Y,+Z].
The Z origin translation does not affect directions or normals.
Shading normals
Each terrain cell is split along the diagonal joining its negative-X, positive-Z corner to its positive-X, negative-Z corner. Each triangle’s cross product is normalized separately. A vertex normal is the normalized sum of the six incident triangle normals. Triangle areas therefore do not weight the sum.
Both normalization stages use a quantized reciprocal square root. For a
positive squared length stored as a 32-bit float, let e be its unbiased
binary exponent and m the upper seven fraction bits. Select index
i = m + 128 * (e & 1). The 256-entry table stores the upper seven fraction
bits of the 32-bit float reciprocal square root of
(1 + m/128) * 2^(i/128). Lower fraction bits are zero.
Combine these table bits with exponent floor((253-e)/2) when the table
entry is nonzero, or floor((254-e)/2) when it is zero. Multiply the vector
by the resulting float. These exponents are biased float exponents.
A flat face normal is 0,-1,0; the sum of six such faces normalizes to
0,-0.99609375,0. That supplied magnitude is retained by the lighting
calculation. With ISLAND day inputs, the resulting flat-terrain RGB is
approximately 190.99797,190.99797,190.99797.
Polygon preparation
Terrain triangles are clipped before projection and Glide submission. In the
projection-scaled camera coordinates used by the terrain path, the accepted
volume is near <= z <= far, -z <= x <= z, and -z <= y <= z.
Points on a plane are inside. Planes are processed in near, far, left, right,
top, bottom order.
At a crossing edge, camera position, RGB and the two texture coordinates use the same linear edge parameter. Each generated component is stored as a 32-bit float. Near/far intersections set depth to the corresponding plane. Left/right intersections derive depth from the stored X coordinate; top/bottom derive it from Y. RGB is not perspective-weighted during this clipping step.
The surviving polygon is submitted as a triangle fan, anchored at its first
vertex. Projection follows clipping. Each projected screen coordinate is
biased by 786432 and stored as a 32-bit float before Glide receives it.
The unbiased X coordinate has an additional 32-bit float store before this
addition; Y remains in the wider intermediate precision until the biased store.
The float spacing at that magnitude is 1/16 pixel. The common bias cancels
from the gradient differences and is discarded by the software rasterizer’s
signed 16-bit fixed-coordinate conversion. Triangle color gradients therefore
use these quantized projected positions.
Each fan triangle receives its own Glide color-gradient setup. Clipping a previously established screen-space color plane does not reproduce this camera-space RGB interpolation and subsequent per-triangle setup.
Screen-space color interpolation
Glide triangle setup orders vertices by screen Y. For input vertices 0, 1,
and 2, the following table specifies the order, including equal-Y cases:
| Condition | Ordered vertices |
|---|---|
y0 < y1 and y1 <= y2 | 0, 1, 2 |
y0 < y2 < y1 | 0, 2, 1 |
y2 <= y0 < y1 | 2, 0, 1 |
y2 <= y1 <= y0 | 2, 1, 0 |
y1 <= y0 < y2 | 1, 0, 2 |
y1 < y2 <= y0 | 1, 2, 0 |
This is not a stable sort. In particular, a top horizontal edge selects the later of its two input vertices as the color anchor. The anchor matters after fixed-point conversion, even when an unquantized affine plane is unchanged.
For ordered vertices A, B, and C, define screen-coordinate differences
AB = A - B and
BC = B - C, and determinant D = AB.x * BC.y - BC.x * AB.y.
A zero determinant produces no triangle. The four coordinate differences
divided by D are stored as 32-bit floats. For each color channel, setup
writes the channel at A and these gradients, also stored as 32-bit floats:
dLdx = (LA - LB) * (BC.y / D) - (LB - LC) * (AB.y / D)
dLdy = (LB - LC) * (AB.x / D) - (LA - LB) * (BC.x / D)
In the software Voodoo 1 renderer, screen coordinates are converted to signed 16-bit fixed point with four fractional bits. Starting color and each gradient are converted separately to signed 24-bit fixed point with twelve fractional bits. These conversions truncate toward zero before narrowing.
The terrain color path enables subpixel adjustment. Let ax and ay be the
fixed-point coordinates of A, and let S, Gx, and Gy be a channel’s
fixed-point start and gradients. Before rasterization, the start becomes:
S = S + floor(((8 - (ax & 15)) * Gx + (8 - (ay & 15)) * Gy) / 16)
For integer screen pixel coordinates (x, y), the channel accumulator is:
I = S + (x - floor(ax / 16)) * Gx + (y - floor(ay / 16)) * Gy
The channel integer is obtained by arithmetic right shift of I by twelve
bits. The Voodoo 1 color conversion keeps the low twelve bits, maps 4095
to zero and 256 to 255, and otherwise keeps the low eight bits.
A constant channel has zero gradients and remains constant across the triangle.
Quantizing an interpolated floating-point channel only at the fragment does
not reproduce the separately quantized start, gradients and subpixel origin.
Texture coordinates and setup
Terrain tile corners use Glide texture coordinates 1 and 254, rather than
0 and 256. The four rotation states select pairs of these endpoints for
the two triangles of each cell. Coordinates interpolate during camera-space
clipping before perspective weighting. The installed terrain bank contains
square 64-pixel textures; the Glide coordinate range is 256 units, so the
normalized endpoints are 1/256 and 254/256.
For each projected vertex, reciprocal depth is stored as a 32-bit float.
The two clipped texture coordinates multiplied by that reciprocal are also
stored as 32-bit floats. Triangle setup uses these three attributes with the
same vertex ordering, normalized screen differences and gradient formula as
the color channels. The texture unit uses the vertex’s common reciprocal
depth for W.
The software Voodoo 1 float-register path converts each texture start and gradient separately to signed fixed point with 32 fractional bits, truncating toward zero. Subpixel correction and integer pixel stepping use the same anchor and formulas as color interpolation, with 64-bit texture accumulators. Perspective division follows pixel stepping.
Software Voodoo perspective division
The DOSBox-X software Voodoo 1 path uses a table-interpolated reciprocal for perspective texture coordinates. This describes the software renderer, not physical Voodoo hardware.
Let S, T and W be the integer texture-plane accumulators at a pixel.
For 0 < W < 2^32, let n be the number of leading zero bits in the
32-bit representation of W, and define:
N = W << n
i = (N >> 22) & 511
f = (N >> 14) & 255
R[j] = floor(2^31 / (512 + j)), for j = 0 through 512
q = floor((R[i] * (256 - f) + R[i + 1] * f) / 256)
Scale q by 2^(n-6). A negative exponent uses a right shift that discards
the low bits. Multiply this reciprocal separately by S and T, retaining
each signed 64-bit product, then arithmetic-shift right by 29. The low signed
32 bits of each result are texture coordinates with 18 fractional bits.
This approximation can select different bilinear fractions from exact division.
Software texture sample selection
The 64-pixel terrain texture uses the software texture unit’s level 2, with both minimum and maximum levels fixed to that value. The base width and height masks are 255; shifting them by two selects the 64-pixel level. Bilinear fraction masking retains the upper four bits of an eight-bit fraction.
For a signed perspective coordinate Q with 18 fractional bits, the bilinear
path first arithmetic-shifts by level + 10, then subtracts 128 for the
half-texel center. The resulting value has eight fractional bits. Its integer
part selects the first texel, and bits 4 through 7 select the bilinear weight.
The neighboring texel is selected before address wrapping or clamping.
At terrain level 2, this is equivalent to computing K = (Q >> 16) - 8.
The first texel is K >> 4 and its four-bit fraction is K & 15, using
arithmetic right shifts. Converting Q to a single-precision float before
this integer selection can round across a sample boundary.
Texture combination
The terrain draw selects Glide’s texture-times-iterated-RGB color-combine mode. Lighting therefore modulates texture colors before scene fog. Vertex RGB is interpolated across screen space; texture coordinates use perspective correction. Terrain does not pass through a scene exposure or filmic tone-mapping stage.
The ordinary terrain pass uses bilinear minification and magnification.
Filtering follows the encoded-byte, four-bit, staged interpolation described in
sky filtering. The fractional weights
are quantized in the original texture orientation. For a vertically flipped
image with transformed coordinate v_export = 1 - v_original, the equivalent
vertical fractional weight rounds upward to a multiple of 1/16; horizontal
weights still round downward. Exact multiples are unchanged. The two row
interpolations and final vertical interpolation still round their results down.
Terrain texture binding disables chroma-key rejection. Palette index zero therefore remains an opaque color on terrain.
In the software Voodoo 1 renderer, texture modulation operates on encoded
8-bit RGB channels. The interpolated lighting channel is converted to an
integer before combination. For texture channel T and lighting channel L,
the result is floor(T * (L + 1) / 256), before fog.
Glide fog and clipping
Scene visibility and sky blending use separate clipping ranges and fog tables.
Clipping distances
The scene uses SYSTEM.INI’s [xngine] front_plane and
back_plane, in engine units. A nonzero world_back_plane[N] replaces the
system far distance on world load. The installed GOG configuration uses 7 and
3800, and none of its 29 worlds declares a world override.
The sky plane independently uses near/far 1 and 65000.
Fog lookup
The sky supplies reciprocal positive camera-space depth to Glide.
The sky table is active during that draw; the scene table is restored afterwards.
Fog color is world_fogrgb[N]. Scene densities come from FOG.INI.
The Glide table’s entry i corresponds to camera depth
2^(3 + floor(i / 4)) / (8 - (i modulo 4)) in engine units.
For positive depth d, let e = floor(log2(d)). The continuous table coordinate
is 4*e + 8*(1 - 2^e/d), clamped to the interval 0 through 63.
Interpolation between adjacent entries is linear in reciprocal depth, rather
than distance. This is a camera-depth curve, rather than radial fog.
Glide uploads each density byte with a delta byte equal to
4*(next density - current density) modulo 256. The last delta is zero.
The interpolated density is the current byte plus one quarter of the delta
byte times the fractional coordinate. The hardware quantizes that fraction
and result, and can dither them. For monotonic adjacent differences below 64,
this has the usual adjacent-value interpolation curve. The shipped scene
table’s transition from entry 62 to 63 wraps its delta; this lies beyond the
installed scene far plane.
The RGB blend weight is
(interpolated density + 1) / 256. Blending operates on encoded RGB channel
values toward the fog color, before framebuffer quantization. Density 255
replaces RGB completely. Density zero has a 1/256 blend weight. Fog does not
replace alpha. This channel operation differs from mixing linear-light RGB.
Fixed-point rounding, fog dither and framebuffer RGB565 precision can change
individual pixels around the continuous curve.
| Depth | Table index | Scene density | Sky density |
|---|---|---|---|
| 256 | 32 | 0 | 0 |
| 512 | 36 | 6 | 32 |
| 1024 | 40 | 14 | 64 |
| 2048 | 44 | 38 | 96 |
| 4096 | 48 | 255 | 128 |
| 32768 | 60 | 255 | 224 |
| 52428.8 | 63 | 0 | 255 |
The sky table is zero through entry 32, then rises by eight per entry through 240 at entry 62, followed by 255 at entry 63. Its density reaches 255 while approaching the finite sky boundary, so the texture blends into the same fog RGB that fills uncovered pixels.
Integer precision and framebuffer dithering
Table fog uses an unsigned 16-bit encoding of the pixel reciprocal-depth accumulator. Terrain supplies the same reciprocal depth to fog and texture perspective correction. Both use the terrain fixed-point plane setup, including its subpixel adjustment and integer pixel stepping. Reconstructing reciprocal depth from an interpolated floating-point camera depth can select a different fog fraction.
For constant depth, a supplied positive 32-bit float reciprocal depth w
produces accumulator t = trunc(w * 2^32). For varying depth, t is the
stepped fixed-point accumulator instead. Accumulators at or above 2^32
select zero. Values below 65536 select 65535. Otherwise,
with e the count of leading zero bits in the 32-bit t, the encoding is
min(65535, ((e << 12) | ((~t >> (19-e)) & 4095)) + 1).
The table index is the encoding divided by 1024; its interpolation fraction
is bits 2 through 9.
The density is the table byte plus
floor(delta_byte * fraction_byte / 1024). Voodoo 1 does not apply fog-density
dithering; the fog-dither switch is a Voodoo 2 feature. For incoming encoded
8-bit channel c, fog channel f and density d, the resulting channel is
clamp(c + floor((f-c)*(d+1)/256), 0, 255).
Glide initializes framebuffer dithering to 4 by 4, and the shipped game’s graphics setup explicitly selects that mode. The threshold matrix, indexed by framebuffer Y then X modulo four, is:
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
For threshold b and encoded channel c, the stored red/blue five-bit value
is floor((2*c - floor(c/16) + floor(c/128) + b)/16). The green six-bit value
is floor((4*c - floor(c/16) + floor(c/64) + b)/16). Display conversion repeats
high bits into the low bits: five-bit v becomes (v << 3) | (v >> 2), and
six-bit v becomes (v << 2) | (v >> 4).
External References
Sky Renderer
Outdoor sky configuration and the Glide renderer’s BSI sky plane and sun disc.
Overview
In the shipped Glide build, the sky uses world_skyfx[N] and a
matching .COL file. The texture is projected onto a horizontal quad before
scene geometry. A separate billboard draws the sun. The GXA file named by
world_sky[N] is not loaded by this renderer’s sky setup.
Lifecycle
| Phase | Trigger | Action |
|---|---|---|
| Load | World open | Engine reads per-world sky keys from WORLD.INI, then calls the sky system opener with the BSI texture filename |
| Render | Every frame in the main loop | Sky layers are drawn before terrain and scene geometry |
| Close | Session close | Sky textures are released and the system is shut down |
Background Fill
The shipped Glide build selects world_fogrgb[N] as the RGB color used to
clear the next back buffer after presenting a normal game frame. An omitted
value defaults to (0, 0, 0). The finite sky plane draws over this fill;
uncovered regions retain the fog color, including when the sky is disabled.
For example, world 0 uses (200, 100, 100), world 1 uses
(200, 190, 250), world 6 uses (57, 49, 49), world 27 uses (0, 0, 10),
and world 28 uses (200, 100, 100).
The shipped Glide world-settings reader does not read world_background[N].
Sky fog
The outdoor frame enables Glide table fog for the sky draw and restores the scene fog table afterwards. The sky draw does not disable fog. Its 64-entry table contains zero in entries 0 through 32, increases by eight from 8 through 240 in entries 33 through 62, and ends at 255 in entry 63.
The sky raster supplies reciprocal camera-space depth to Glide. The Glide fog lookup describes table interpolation and RGB blending.
GXA assets
world_sky[N] names a GXA asset in system/. The Glide renderer does not load
this asset; its sky comes from the BSI asset instead.
Glide sky plane
The shipped Glide build uses the following geometry and texture mapping. Coordinates use engine units, with positive Y pointing down.
- The plane is centered horizontally on the camera. Its four relative X/Z
corners are
(-65000, 65000),(65000, 65000),(65000, -65000), and(-65000, -65000). - Its absolute Y is
world_skylevel[N]. Zero or omitted selects-3500. world_skyscale[N]is the half-span of texture coordinates in texels. Zero or omitted selects2000. This is not the sun disc’s default scale.- For camera position
(cx, cy, cz), the plane’s relative height isworld_skylevel[N] - cy. Initial texture-coordinate centers are(0.009 * cx, -0.009 * cz). - For relative point
(x, z)on the plane and scroll offset(su, sv), the texture coordinates in texels are(scale * x / 65000 + 0.009 * cx + su, -scale * z / 65000 - 0.009 * cz + sv). Divide by image width and height for normalized coordinates and repeat the texture. The sky draw uses near/far distances of1and65000. - The BSI file is a standalone chunk stream, not a TEXBSI archive record.
Its matching
.COLfile supplies the colors independently of the world’s terrain/model palette. Sky sampling is opaque, including palette index 0.
Glide renderer initialization selects bilinear minification and magnification on texture unit 0. The sky texture binding and draw path do not change the filter mode. The initial boat sky uses bilinear minification and magnification, an eight-bit paletted texture, wrapping in both directions, and the base mip level. Palette lookup precedes color filtering.
For the software Voodoo 1 renderer, bilinear filtering operates on encoded
eight-bit channels, without an sRGB-to-linear conversion. Texel centers are at
half-integer coordinates. Each fractional coordinate is rounded down to a
multiple of 1/16. Each row is interpolated first, rounding its channel result
down to an integer; those two results are then interpolated vertically and
rounded down again. For channel endpoints a, b and fractional weight w,
each interpolation is a + floor((b-a)*w). For example, the midpoint between
encoded channel values 0 and 255 is 127.
The installed files SKY888.BSI, SKY899.BSI, SKYNIT.BSI, and
SKYNEC.BSI each contain one 256 by 256 frame and have matching COL files.
The seven outdoor world entries are 0, 1, 6, 14, 27, 28, and 30.
Scrolling and rotation
Raw world_skyspeed[N] is an unsigned byte converted to texels
per BIOS timer tick by dividing by 16. The timer runs at
1193182 / 65536 ticks per second. The initial scrolling direction is selected
randomly from 2048 angle steps. Scroll offsets start at zero, advance with the
shared engine sine/cosine table, and wrap at plus/minus 256 texels. A frame
processes at most 36 elapsed ticks. After each tick, both offsets subtract 256
at or above 256 and add 256 at or below -256. A stationary offset of exactly
-256 therefore becomes zero on the next processed tick. The plane tilts around
a camera-relative axis as described below.
The fxskyspeed console command interprets its value as a signed integer. It
sets the current sky speed to that full value divided by 16 and stores the low
eight bits in the current world’s speed setting. The current speed can therefore
differ from the byte-derived speed used when the world is loaded again.
Each successful command consumes one random value and selects a new direction
from its low 11 bits, including when the requested speed is zero. It retains
the accumulated texture offsets and the last processed timer tick.
Plane tilt
The sky is a plane that can be crossed vertically and is visible from either side. It is not an enclosing cube or sphere.
The initial tilt is 24 of the engine’s 2048 angle units, approximately 4.21875 degrees. It is a fixed angle, not an animation rate. The tilt setter masks its input to 11 bits.
Let h be the normalized horizontal camera-forward direction in engine
coordinates. For an unrotated plane point with horizontal position p, let
q = dot(p, h) and a = 24 * 6.2831852 / 2048. Its tilted horizontal position
is p + (cos(a) - 1) * q * h, and its relative vertical position is
sin(a) * q + world_skylevel - camera_y. Texture coordinates stay attached to
the unrotated corners. The camera-relative tilt is applied before adding the
height, not around the world origin.
Initial scroll direction
The sky consumes one value from the engine’s shared random stream and keeps its
low 11 bits as the angle. The stream updates its unsigned 32-bit state with
state = state * 1103515245 + 12345, wrapping at 32 bits; the returned value is
(state >> 16) & 32767. Therefore the sky direction is
(updated_state >> 16) & 2047. Reproducing a particular session requires the
state immediately before sky initialization or the resulting direction, not
just a world identifier. Other engine users also consume this stream.
Global Engine Toggles
The [xngine] section of SYSTEM.INI provides one master control that applies to all worlds:
| Key | Default | Description |
|---|---|---|
sky_disable | 0 | 1 disables the sky in every world. Any other value leaves it enabled. |
The same section also holds sky_move, sky_xrotate, and sky_yrotate. They have no effect.
The Glide settings reader stores sky_move, but the Glide sky producer does
not consult it. A value of zero does not freeze the BSI sky: scrolling continues
on elapsed BIOS ticks using the current direction and speed.
Sun disc
A separate billboard renders the sun as a textured sprite in the sky, independent of the BSI sky plane:
| Key | Description |
|---|---|
world_sunimg[N] | BSI texture for the sun disc |
world_sunimgrgb[N] | Tint color (r, g, b) applied to the sun texture |
world_sunscale[N] | Size scale of the sun disc |
The billboard center, relative to the camera, is the negative of
(truncate(16000*sin(angle)), truncate(16000*cos(angle)), truncate(16000*sin(skew))), using world_sunangle and world_sunskew in the
2048-step engine angle table. It does not use world_sun as its position.
The sprite faces the camera and remains the same angular size during camera
translation. Its near/far range is 1/65535 engine units.
Each billboard half-size is the corresponding BSI image dimension multiplied
by 0.5 * (tex_scale / 256) * (world_sunscale / 100). A zero or omitted sun
scale defaults to 12800. The installed SUN001.BSI is a single 256 by 256
frame with texture scale 256, so the installed sun scale 4000 gives half-size
5120 engine units. Missing sun image names suppress the disc.
The sun texture is bound as eight-bit alpha: its indexed pixel bytes supply
opacity, independently of its palette. The sun draw explicitly selects bilinear
minification and magnification on texture unit 0. Constant encoded RGB comes from
world_sunimgrgb, defaulting to (255,255,200). The draw uses source-alpha /
one-minus-source-alpha blending and disables fog for the sun, restoring scene
fog afterwards. Scene geometry drawn later can cover it.
In the software Voodoo 1 renderer, the sun’s alpha channel uses the same
four-bit fractional bilinear interpolation described for sky colors. Texel
centers are at half-integer coordinates; each fractional coordinate is rounded
down to a multiple of 1/16. Horizontal alpha interpolation truncates to an
eight-bit integer before vertical interpolation, which truncates again. The
resulting integer alpha feeds framebuffer blending. Sun texture coordinates
wrap in both directions.
Sun framebuffer blending
Sky texture filtering and fog produce colors before framebuffer encoding. The RGB565 conversion and dither arithmetic in this section describe the software Voodoo 1 framebuffer backend; they are not additional stages in the BSI texture or sky fog calculation.
The sun draw enables four-by-four framebuffer dithering and alpha dither subtraction. In the software Voodoo 1 renderer, blending reads the existing RGB565 destination by shifting the stored red and blue fields left by three bits and the green field left by two bits. It does not replicate the high bits into the vacated low bits.
Before blending, the destination channels undergo dither subtraction using the
pixel’s four-by-four dither value d. Red and blue become
floor((2 * channel + 15 - d) / 2); green becomes
floor((4 * channel + 15 - d) / 4).
For an encoded source channel s, corrected destination channel t, and
filtered eight-bit sun alpha a, the blended channel is
floor(s * (a + 1) / 256) + floor(t * (256 - a) / 256).
The source and destination terms truncate separately before addition.
The result is clamped to an eight-bit channel and then passes through the
normal framebuffer quantization.
Console Commands
The developer console (F12) exposes runtime sky adjustment:
| Command | Alias | Description |
|---|---|---|
fxskyscale <value> | skysc | Set the BSI layer scale |
fxskylevel <value> | skyl | Set the BSI layer vertical offset |
fxskyspeed <value> | skysp | Set the BSI layer scroll speed |
The show world console command displays current sky parameters in the on-screen debug overlay, including the sky texture name, scale, level, and speed.
World Sky Assignments
Seven outdoor world entries define sky parameters, including world 14. All others are indoor/dungeon locations with no sky.
| World | Location | Sky Features |
|---|---|---|
| 0 | Starting hideout (exterior) | Sunset BSI sky |
| 1 | Stros M’Kai island (daytime) | Daytime BSI sky |
| 6 | Necromancer’s Isle | Necromancer BSI sky and rain configuration |
| 14 | Island alternate entry | Daytime sky texture |
| 27 | Island (night variant) | Night BSI sky (SKYNIT.COL palette) |
| 28 | Island (sunset variant) | Sunset BSI sky (sunset.COL palette) |
| 30 | Palace exterior | Sunset BSI sky (shares island WLD) |
Worlds 1, 27, and 28 share the same ISLAND.WLD terrain and PVO node maps. Different palettes, BSI sky textures, and lighting parameters implement the time-of-day variants as separate world entries rather than dynamic sky transitions.
External References
- WORLD.INI documentation — per-world sky, sun disc, and background fill keys
- SYSTEM.INI documentation — global sky engine toggles in the
[xngine]section - UESP: Redguard Console —
show worldcommand displays sky parameters at runtime
Water Wave Animation
Per-frame vertex displacement system that animates water surfaces on the terrain grid. Selected terrain vertices receive a shared rotation-table displacement producing radial concentric ripples.
Water vertex selection
Wave displacement tests a vertex and the three corners toward negative grid X and positive engine Z. The selected vertex must have texture index {0, 30, 31}; the other three may have {0, 5, 30, 31}. Each index is masked to its lower six bits. Only the selected vertex moves. Texture 5 can border an animated vertex but is never itself selected for displacement.
The engine reverses WLD source rows when loading the terrain. In the raw
row-major grid returned by WldFile::combined_map, the selected vertex is
(x, row) and its neighbours are (x-1, row), (x, row-1) and
(x-1, row-1). The first raw row and first column have no such complete cell.
Wave parameters and enablement
world_wave[N] contains amplitude, speed and spatial frequency,
in that order. The world loader parses integers and converts them to floats.
Terrain setup truncates speed and frequency toward zero into integers.
Amplitude remains floating point. There is no additional speed multiplier.
A zero amplitude disables displacement; world_scapeshift does not enable it.
| Parameter | Console command | Meaning |
|---|---|---|
| Amplitude | fxwaveamp | Vertical displacement scale in engine units. |
| Speed | fxwavespeed | Phase advance per BIOS timer tick. |
| Spatial frequency | fxwavefreq | Multiplier for squared grid distance. |
Displacement and clock
For selected engine-grid vertex (x, z), width W, height H,
integer speed S, integer frequency F, amplitude A, and BIOS tick count T:
distance_sq = (x - floor(W/2))² + (z - floor(H/2))²
phase = (distance_sq * F + T * S) & 2047
height = base_height + wave_table[phase] * A - A * 0.5
The half-amplitude bias is stored as a 32-bit float before the vertex loop. For each selected vertex, the table-sample multiplication, base-height addition and bias subtraction retain register precision. Only the final height is stored as a 32-bit float. Rounding each intermediate operation to 32 bits can change the stored height.
The BIOS timer count is independent of rendered frames. The standard PC BIOS clock advances approximately 18.2065 times per second. The absolute phase depends on that clock’s initial value, not on scene loading. The spatial origin is the whole terrain midpoint, not the camera or the visible-window center.
base_height is the height-table
value, world_scapeshift - abs(source_height), in engine coordinates with
positive Y down. Exporters using positive Y up must negate the displacement
and apply their engine-unit scale. The half-amplitude subtraction is a vertical
bias; the range for positive amplitude is approximately [-1.5*A, 0.5*A].
It does not center the oscillation on the base height.
Wave lookup table
The shared rotation table contains samples of
sin(f32(n * 6.2831852 / 2048)), stored as 32-bit floats. Here f32
rounds the angle to single precision before evaluating the sine. The initialized
table contains 2,561 entries,
including the extended quarter revolution and its endpoint. Water addresses the table
starting at sample 512, so wave_table[p] is the float sample
sin(f32((p + 512) * 6.2831852 / 2048)). The 2048 wave phases therefore start near
one, zero, minus one, and zero at phases 0, 512, 1024, and 1536 respectively.
The decimal full-period constant is part of the table construction.
Terrain window
The engine fills a 34-by-34 storage grid around its terrain
camera-grid coordinates, from camera-17 through camera+16. The wave loop
selects the inner 32-by-32 vertices, from camera-16 through camera+15 on
each axis, and reads the four corners described above from that storage.
The center on each axis is the camera’s engine coordinate divided by 256,
rounded to single precision and then to the nearest integer, with ties to even.
A change on either axis reloads the storage grid before displacement.
Terrain projection transforms the inner 32-by-32 vertices. Triangle submission uses only those vertices, covering 31-by-31 cells with two triangles per cell. The outer storage ring supplies neighboring heights for selection and normals; it is not drawn. This is an original rendering window, not a moving ripple origin.
Dirty normals and retained lighting
Each wave update clears the storage grid’s dirty flags. Every selected vertex marks six storage entries, even when its replacement height equals its current height. In engine-grid coordinates, the marks relative to the selected vertex are:
| Relative X | Relative Z | Flags added |
|---|---|---|
| 0 | 0 | 3 |
| -1 | 0 | 3 |
| -1 | +1 | 3 |
| 0 | +1 | 3 |
| +1 | 0 | 2 |
| 0 | -1 | 2 |
Overlapping marks combine with bitwise OR. Positive engine Z corresponds to decreasing raw WLD row, so negate the relative Z column when using source rows.
Face-normal, vertex-normal and vertex-RGB updates all admit an entry when its whole dirty value is nonzero. They do not select different work from individual flag bits. The face pass replaces both triangle normals of an admitted cell. The vertex pass sums the six stored incident face normals using the terrain normalization rules; the RGB pass then uses the retained or replaced vertex normal.
Within the 34-by-34 storage grid, face entries cover X indices 0 through 32
and Z indices 1 through 33. A face entry at (x,z) stores the two triangles
of the cell with corners (x,z-1), (x+1,z-1), (x,z) and (x+1,z).
Vertex-normal and RGB entries cover indices 1 through 32 on both axes.
An entry outside its pass’s range is not updated by that pass.
The diagonal neighbor at relative (+1,-1) is not marked by the selected
vertex, although one of its incident faces can change. Unless another selected
vertex marks it, its vertex normal and RGB retain their previous values.
Recomputing every vertex normal from the displaced grid therefore produces
different water-edge lighting.
When the camera’s terrain-grid coordinates change, the storage grid reloads base heights and all three passes run over their full ranges without dirty admission. Subsequent wave updates resume selective replacement. Retention lasts across wave updates until an entry is admitted or a full rebuild replaces it.
Rendering Pipeline
The wave renderer runs each frame as part of the terrain update:
1. Build vertex grid
└─ 34×34 storage 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 the selected corner {0, 30, 31} and its three neighbours {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)
└─ Vertex RGB (lighting from retained or replaced vertex normals)
3. Lighting
└─ Per-vertex RGB from ambient + directional dot-product, clamped to [0, 255]
4. Rasterize
The RGB inputs and directional calculation are specified in terrain-lighting.md.
Admitted normals tilt with the displaced geometry. Unmarked water-edge entries retain their earlier normals and lighting as specified above.
Terrain Vertex Layout
Each storage vertex occupies 76 bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | f32 | x | World X position (grid_x * 256.0) |
| 0x04 | 4 | f32 | y | Height (from lookup table, or wave-displaced) |
| 0x08 | 4 | f32 | z | World Z position (grid_z * 256.0) |
| 0x18–0x30 | face_normals | Two face normals per cell (upper/lower triangle) | ||
| 0x30–0x3C | vertex_normal | Averaged vertex normal for lighting | ||
| 0x48 | 4 | u32 | texture_index | Terrain texture ID (lower 6 bits used for water detection) |
Grid stride: 76 bytes between adjacent X-axis vertices, 2584 bytes (34 × 76) between rows.
Console Commands
Three runtime console commands allow tuning wave parameters without restarting:
| Command | Syntax | Effect |
|---|---|---|
fxwaveamp | fxwaveamp <value> | Set wave amplitude |
fxwavespeed | fxwavespeed <value> | Set wave speed |
fxwavefreq | fxwavefreq <value> | Set spatial frequency |
These modify the same globals as the INI parameters and take effect on the next frame.
External References
-
PC BIOS timer ticks — standard BIOS timer clock
-
WLD § Water Tiles — texture-index detection criteria for water cells
-
WLD § Height Lookup Table — the 128-entry height table shared by terrain and water base heights
-
WORLD.INI § Water —
world_wave[N]INI parameter format -
SURFACE.INI — surface-type definitions including
[water],[deepwater],[scapewater]sound sections -
Redguard:Glide Differences — software vs Glide renderer behavior
Positional Sound
How sound effects from MAIN.SFX and speech from RTX get their volume, stereo pan and playback rate.
Units
- Engine positions are stored in world units × 256 (eight fractional bits).
World units are the units
SYSTEM.INIdistances use. - Angles use 2048 units per turn, wrapped modulo 2048.
- Volumes and pans are Miles Sound System sample values: volume 0–127, pan 0–127 with 64 at the centre (Miles: 0 = left, 127 = right).
Listener
The listener uses the camera’s published position in signed Q8 coordinates and its published yaw. In third person, distances are therefore measured from behind Cyrus. Only yaw enters the calculation; camera pitch and roll do not.
Rendering retains a separate floating-point position for camera gliding. Sound distance and pan read the published position, not that gliding position. A camera pose update replaces these listener coordinates immediately. Render preparation smooths the published angles before object updates; a later camera pose update replaces those angles again.
Radius and divisor
SYSTEM.INI [system] sound_distance (shipped 64) is read once at startup
into two values, which the engine’s debug sound page labels “Sound radius” and
“Sound divisor”:
| Value | Formula | Shipped |
|---|---|---|
Radius R | sound_distance × 36 world units | 2304 |
Divisor D | floor(65536 / floor(R / 128)) | 3640 |
A sound_distance of 3 or less makes floor(R / 128) zero, a division by
zero in the original; such values are unsupported.
Distance
For emitter E and listener L in engine units (world units × 256):
d = floor(round(sqrt(dx² + dy² + dz²)) / 256) (dx, dy, dz = E − L)
The distance is three-dimensional (height counts) and floored to whole world units. The square root runs in floating point and is rounded to the nearest integer before the shift; the 32-bit float intermediate only matters beyond about 65536 world units.
The emitter is the emitting object’s position, with two exceptions:
- An object whose
attr_slaveattribute (index 26 inSOUP386.DEF) is set emits from its attached position: its master’s position plus its own offset rotated by the master. - Otherwise, an object that links to another positioned record emits from that record’s position.
Animation PlaySound always uses the object’s own position.
Volume curve
With a signed per-call volume offset off:
if d > R: v = 0
else: a = clamp((d × D) >> 16, 0, 127)
v = clamp(127 − a + off, 0, 127)
Volume falls linearly from 127 at the listener. With the shipped constants the
attenuation reaches 127 at d = 2287, so a sound with off = 0 is already
silent there, slightly before the radius. A positive offset cannot make a
sound beyond R audible.
The addition of off wraps as a signed 32-bit sum before the final signed
clamp. An overflowing positive offset can therefore produce zero volume.
Pan
Pan uses only the horizontal plane (x, z):
a= the engine’s integer heading from listener to emitter: 0 when the emitter lies along +Z, 512 along +X, 1024 along −Z, 1536 along −X.- Take
|dx|and|dz|; while either exceeds 511, halve both (arithmetic shift). - If
|dx|is now 0:a= 0 whendz ≥ 0, else 1024. Otherwise, if|dz|is 0:a= 512 whendx ≥ 0, else 1536. - Otherwise, with
s = min(|dx|, |dz|)andb = max(|dx|, |dz|):t = T[floor(s × 512 / b)]. The base angle istwhen|dx| < |dz|,512 − twhen|dx| > |dz|, and 256 when equal. - Quadrant: only
dx < 0gives−base; onlydz < 0gives1024 − base; both negative givebase + 1024. The result is taken modulo 2048. T[i] = trunc(atan(i / 512) × 65536 / π) >> 6fori= 0..512, built at startup (truncation toward zero, then the shift).
- Take
u = (a − yaw) mod 2048.- Triangle wave:
u | Offset p |
|---|---|
| 0–511 | u >> 3 |
| 512–1023 | ((1024 − u) & 511) >> 3 |
| 1024–1535 | −((u − 1024) >> 3) |
| 1536–2047 | −(((2048 − u) & 511) >> 3) |
pan = 64 + p, range 1–127. Sounds straight ahead or straight behind are
centred; a quarter turn puts them fully to one side. At exactly u = 512 and
u = 1536 the & 511 yields zero, so a sound exactly abeam is centred for
that single angle value. Pan does not depend on distance.
Starting a sound
Every effect start takes an effect index, a playback-rate offset in Hz, a volume and a pan.
- Playback rate:
clamp(effect sample_rate + rate_offset, 512, 44000)Hz. The addition wraps at 32 bits before the signed clamp. - Looping: see SFX loop behaviour.
- Effect range: a negative effect index or an index at or beyond the number
of effects in
MAIN.SFX(118 shipped) starts nothing. - Channels: 16 sample channels. A start takes the first idle channel at or above a start index. When none is idle the sound is dropped; playing sounds are never stolen. The start index is 0 for the player’s own animation sounds and for speech, 4 for the combat message responses, and 6 otherwise (0 at boot until the first sound that resets it).
- Ownership: each channel records its owner object and the requested effect index (before any SURFACE.INI remap).
- Handles: a remembered sample handle identifies a reusable channel slot, not a particular start. Stopping by handle acts on the slot’s current sample without comparing its owner or requested effect.
- Master volume: every volume set, at start and on later updates, sends
(v × master) >> 8to Miles, wheremasterisSYSTEM.INI [system] volume(shipped 255, engine default 256 before the file is read) or the menu Sound slider (see menu). There is no other global effect gain. A changed master reaches a playing sound only at its next volume set; one-shots keep their start volume.
Animation PlaySound
Animation opcode 4 (attachment) starts a positional sound:
effect= first signed 10-bit field, remapped through SURFACE.INI by the animated object’s surface.rate_offset= second signed 10-bit field × 64 Hz.- Volume from the curve with
off = 0, pan from the animated object. - The sound starts even when
v = 0; it then occupies a channel silently. - The handle is stored on the animation; EndAnimation (opcode 1) stops it. No later volume or pan update was found for these sounds.
Volume and pan use the actor and camera positions when the PlaySound command executes. They do not use the actor’s later position at the end of the update. During ordinary player walking, animation advancement can start a sound before forward movement and ground response change the player’s position.
Each encountered PlaySound advances to the next command without consuming a frame skip. A second PlaySound replaces the animation’s remembered handle without stopping the previous channel. Selecting a replacement group clears that handle without issuing a stop. EndAnimation stops only the handle still remembered by that animation. Channel validity and completion are handled by the audio mixer independently.
Animation advancement checks the remembered sample before interpreting commands. If the sample is no longer playing, the remembered slot is cleared. EndAnimation stops a sound only while its slot is still remembered; a completed sample does not receive a later stop through that animation.
The metadata field historically named volume_shift contains the second
signed ten-bit operand. It is a rate offset in units of 64 Hz, not a volume
adjustment.
SOUP functions
SOUP386.DEF declares these sound functions:
| Function | Kind | Parameters | Behaviour |
|---|---|---|---|
Sound | function | effect, volume offset, rate offset | Positional start: volume from the curve with off = volume offset, pan at start. Starts even at v = 0. No later updates. |
FlatSound | function | effect, volume, rate offset | Non-positional: pan 64, v = clamp(base_volume + volume, 0, 127), where base_volume is SFX header byte 0x0C and is read from each effect. FlatSound(e, 0, 0) plays at that base volume. |
AmbientSound | multitask | effect, volume offset, rate offset | Each run: compute volume and pan. If v = 0, or the runtime object-update exclusion is set, stop this task’s sound and end the multitask. Otherwise start the sound if it is not playing (never started, or a one-shot finished), then set the current volume and pan. |
AmbientRTX | multitask | RTX label, volume offset, unused | First run: start the label’s RTX clip at pan 64 and its header volume, start index 0. Each run: compute volume and pan; if v = 0 or the object has been killed, stop the clip and end the multitask; otherwise set the volume and pan. The multitask ends once the clip has finished; it is not restarted. No dialogue line or subtitle is created. |
EndSound | function | effect | Stop every playing channel whose requested effect is effect, whatever its owner. |
EndMySound | function | effect | Stop every playing channel whose requested effect is effect and whose owner is this object. |
EndMySounds | function | — | Stop every playing channel owned by this object. |
StopAllSounds | function | — | Stop all 16 channels. |
FlatSound adds its volume offset to the header base volume with 32-bit
wrapping arithmetic before clamping the signed result to 0–127.
FlatSound, EndSound, EndMySound and EndMySounds return zero,
including when they start or stop no channel.
Sound returns zero as its SOUP scalar result, including when no sample starts.
StopAllSounds does not assign an independent scalar result. With sound disabled,
it leaves the incoming native value unchanged. With sound enabled, the final
channel-stop path supplies the residual value. This is neither a count of stopped
channels nor a Boolean success result.
Each AmbientSound task retains its own sample handle in native task state.
Whenever its elapsed input is zero, or the shared sample-reset flag is set,
it discards that remembered handle before querying playback status. Discarding
the handle does not itself stop a channel. The engine’s periodic sound-reset
path stops all sample channels and sets this flag, allowing active ambient
tasks to acquire replacement channels when they next run.
The RTX dialogue task instead completes immediately when this reset flag is set,
before checking its distance or loading an entry.
Sound maintenance runs after clock sampling and before ordinary object updates. Dialogue-record updates run after ordinary and code-object updates. Periodic RTX cache updates run after the dialogue-record stage. It clears the shared reset flag each invocation. Its accumulator starts at zero when the world loop starts and advances by the shared whole nominal ticks when the runtime inhibition flag is clear. A signed accumulator value greater than 1400 triggers the reset and clears the accumulator; exactly 1400 does not.
RTX initialization creates twenty cache slots with no text or audio payload,
cleared occupancy, and no remembered sample. Dialogue initialization allocates
max(dialog_max_dialog_lines, 16) records from SYSTEM.INI [dialog] and clears
their stored state. The topic-menu capacity is max(dialog_max_menu_items, 300),
and text width is max(dialog_max_text_width, 150). A configured
menu_traverse_delay of zero becomes one BIOS tick. WaitOnDialog scans every record and remains active while
any record has its active word equal to one. It does not filter by the caller.
The separate dialogue-state word starts at zero in a freshly loaded executable.
DialogBegin writes one to that word; DialogEnd writes zero. These writes do
not replace the per-record active-state scan performed by WaitOnDialog, and
the dialogue-state word is separate from the topic-menu-state word.
Dialogue creation selects the first inactive record starting at index one. If no record is available, it returns no record. An admitted record stores the speaker, formatted text, active state and the RTX entry’s remembered sample handle. Clearing a record removes its active state and text and replaces the remembered sample handle with the absent-handle value. It leaves the remaining record fields intact. Querying an absent record reports inactive.
RTXp uses record zero and assigns the selected player as its speaker,
regardless of the calling actor. It replaces that record directly rather than
searching for an inactive record. Clearing dialogue by an RTX cache entry’s
remembered sample scans active records starting at one and leaves record zero
alone. Matching uses the remembered sample slot, not speaker identity.
The RTXp task completes when the shared sample-reset flag is set. Otherwise,
its first zero-elapsed invocation loads the label once and replaces record zero
when lookup succeeds. Later invocations query that record’s activity without
reloading the label. The handler has no speaker-distance gate.
The RTX task completes when the shared sample-reset flag is set. Otherwise it measures the scripted actor’s
distance to the selected player: the rounded single-precision Euclidean length of the position difference, using
the same receiver position as PlayerDistance, shifted right eight bits. neversaydie does not replace this
distance. A distance greater than dialog_max_distance completes the task. When the task already created a
line, that line’s remembered sample is first stopped and the active ordinary records using that sample slot are
cleared; the reserved player record is untouched. Within range, only an invocation with zero elapsed input and no
retained record loads the label, once, and creates an ordinary record owned by the scripted actor. Later
invocations query the retained record’s activity without reloading. A full record table retains -1, which reads
as inactive. A failed lookup retains record zero, so the task then waits on the player’s line.
rtxAnim(label, a, b, c) runs the RTX steps and also cycles three animation groups on the scripted actor.
The cycle visits the operands in the order b, c, a. The invocation that creates the line requests the first
group (an ordinary request with speed modifier zero) and sets the animation’s loop countdown to one. On every
invocation, including that one, when the actor’s current group differs from the group last requested and the line
is active, it requests the next group in the cycle, wrapping after the third, and again sets the loop countdown to
one. The task keeps running while the line is active or the actor’s current group equals the group last requested,
and otherwise completes. While it keeps running it clears the topic-menu pending flag, and when it completes it
clears the topic-menu delay counter.
RTXpAnim(label, a, b, c) follows the same steps with the selected player as the animated actor and the speaker.
Its record is an ordinary record owned by the player rather than the reserved player record, its distance gate
measures the player’s own position, and it does not change the topic-menu state.
Dialogue lifetime is initialized from the loaded audio duration when a sample
handle is remembered. Duration uses integer units of 1/256 second:
(PCM_byte_count << 8) / (channels * sample_rate * bytes_per_sample).
When no sample is remembered, lifetime is fifteen times the original text byte
count before display formatting.
RTX lookup first reuses a cached entry with the same label, marking it used and resetting its age to eight nominal ticks. Otherwise it selects the first entry whose used flag is clear, even if that entry still retains a different label and payload. Loading a new entry replaces its retained text and audio payloads and marks it used with age eight. If every entry is marked used, lookup reports a cache-exhaustion error rather than evicting a used entry. File lookup lowercases ASCII uppercase letters in the four-byte label. A voiced entry with no remembered sample starts playback at sample start index zero, then restores the shared start index to six.
Periodic cache updates retain an entry unchanged while its remembered sample has playing status four. Otherwise they clear the remembered sample. An entry used since the preceding cache update clears its used flag and resets its age to eight nominal ticks. An unused entry is released when its age is strictly less than the current nominal tick count; equality subtracts to zero and retains the payload. Release clears text, audio, occupancy, label and remembered sample. The full cache reset releases text first, stops a remembered sample only when its status is four, then releases audio and clears occupancy, sample and label.
Dialogue record updates visit records in index order. Each active record first sets its speaker’s dialogue-inhibition attribute to one, then subtracts 21 times the whole nominal tick count from its signed lifetime counter. With speech enabled, a remembered sample that is still playing has its volume refreshed and its line presented; a finished sample clears the line. Either sample case ends the update. A line without a remembered sample remains active and traversal continues. With speech disabled, a positive remaining lifetime presents the line and ends the update; an expired line is cleared and traversal continues.
Before record traversal, dialogue updates edge-detect their skip-input signal.
The signal is the combined KEYS.INI key_a binding pair, the walk/run modifier.
A released signal clears the latch. A new pressed signal sets the latch and,
only when player lockout equals one, visits every record, stops any remembered
sample that is playing, clears the record, and ends the update. Holding the
signal does not repeat that cleanup. The latch starts clear. Fresh dialogue
records have zero-valued sample fields; clearing replaces these with the absent
handle, so fresh inactive records and cleared records have different stored
sample values.
Combat taunts retain an RTX cache reference per actor. Replacing that reference releases the old entry when it remembers a sample; an absent remembered sample leaves the cached payload intact. In both cases the actor drops its reference. The new label uses the shared RTX cache. After lookup, its remembered sample is stopped and cleared before the new taunt is presented, even when the label was already cached. This lookup differs from ordinary dialogue reuse, which retains a remembered sample.
For a nonzero taunt label, presentation releases the retained cache reference
first, then clears its retained dialogue record if that record is active and
dialog_print_text is enabled. It loads the requested label and stops its
remembered sample. A successful lookup starts speech only when
dialog_use_speech equals one and creates a dialogue record only when
dialog_print_text is nonzero. A nonzero accompanying animation is requested
with modifier zero after lookup. A zero label skips replacement and animation.
The result is zero when text is disabled; otherwise it is the retained dialogue
record’s signed active word, or zero for the absent record. Combat construction
initializes the retained record index and result to zero and the cache reference
to absent.
The reset releases cached RTX text and audio payloads and clears their occupied state. It also clears active dialogue records and their text buffers, resets their sample handles. These operations precede stopping all sample channels and setting the shared reset flag. It then clears the ownership byte of every node in every loaded walk-node map, regardless of the owning actor.
Leaving a world, whether to enter another or to load a save, runs the world teardown, which clears every dialogue record and its text buffer and resets its sample handle, stops every entry of the active-sound list, and then stops all sample channels. No dialogue line survives into the next world.
Restoring saved world state from either SAVEGAME.SAV or a per-world .TSG
snapshot clears the RTX cache, clears dialogue records and their text buffers,
then stops all sample channels before restoring actor records. After restoring
the world records, it sets the shared sample-reset flag. Per-world snapshot
restoration follows player initialization.
The next sound-maintenance invocation clears this flag before testing the
periodic-reset threshold. The flag is not retained until a sound task consumes it.
In-game main-save loading completes before that loop iteration’s clock sampling
and sound maintenance, followed later by ordinary object updates.
Saved object state retains native task invocation counters and private state
words. Existing-task dispatch restores those words and derives elapsed input
from the counter and current frame scale. It does not initialize a restored
slot as a new call. With nonzero elapsed and a clear sample-reset flag,
AmbientSound follows its usual remembered-sample status path.
RTX performs its initial label lookup only with zero elapsed and no retained
dialogue record. RTXp requires zero elapsed and a clear start marker for that
lookup. A resumed started call queries its shared dialogue record without
recreating the line.
The runtime menu-state word has an initial value of zero.
MenuBegin sets it to one; MenuEnd clears it.
The SOUP menuProc task activates this state when it is inactive and clears
it when a selection is accepted. These calls occur inside the native task
handler.
Engine initialization clears maintenance inhibition. World-loop entry resets
only the accumulator. During player-controller execution, an active menu-state
word or a nonzero
WaitOnDialog result sets the maintenance inhibition flag. When both are zero,
the flag clears only if player lockout is also zero. A nonzero player lockout
therefore retains the prior inhibition value. This state is sampled by the
player controller, rather than every ordinary object update. The menu-state word is separate
from the dialogue-state word controlled by DialogBegin and DialogEnd.
TaskStop removes the native
task slot without invoking its handler. Stopping an ambient task therefore
does not itself issue a sample-stop command. Sound ownership commands are separate from task-slot removal.
KillMe sets the object’s killed flag and stops every channel the object
owns; Resurrect clears the flag.
KillMe releases held walk-node reservations, clears the current model
reference, sets the object’s killed flag to 1 and runtime death-state selector
to 8, and stops every channel the object owns.
Resurrect always releases held walk-node reservations. When the killed flag
is 1 or the runtime death-state selector is 8, it clears the killed flag. If
the actor has combat state, it reinitializes that state and consumes one waiting
SOUP message when present.
Owner: the owner recorded by Sound, FlatSound, AmbientSound and
AmbientRTX, and matched by EndMySound and EndMySounds, is the calling
object, except for two kinds of script that run on a temporary object:
- Scripts run by the shell system (for example an item’s
use_script, see ITEM.INI) record the shell entry as owner. - Scripts of code objects (scripts without a world object) record the code object as owner.
So an ambient sound follows the listener while its multitask runs, restarts non-looping effects when they end, and stops for good once the listener leaves its range; the script must call it again to resume.
Engine-internal starts
- Combat hits: effect from the attacker’s
hit_soundattribute,off = 0,rate_offset = random(0..127) × 16 − 1024Hz (−1024 to +1008). Not started whenv = 0. The start index is 4 for the effect a combat message response plays (the hit message0xd001and the direct-damage message0xd005, see SOUP). The same effect started by the weapon-contact service, and the sword clank cues started by the contact and defence services, use the shared start index (6). - Speech: RTX clips start at pan 64 with rate offset
0 and volume
clamp(header byte 0x0C, 0, 127)(100 in the shippedENGLISH.RTXaudio headers), start index 0. While the dialogue runs, the volume is refreshed every frame from the speaker’s distance (curve withoff = 0); the pan is not updated. The raw RTX sample start clears its sample user-data slots and does not assign an object owner. Dialogue speaker identity is retained separately by the dialogue record. - Rope sounds: each MPRP rope
record names three effects. A rope plays one of them when its swing
velocity changes sign (or stays at zero), chosen by its current swing speed
against
q = trunc(m / 4), wheremis the rope’s maximum swing speed: the third effect from3q, the second from2q, the first fromq; a zero effect falls through to the next lower one. Ropes whoseqis 2 or less play nothing. Volume from the curve withoff = 0at the rope’s position, rate offset 0, start index 6; not started whenv = 0. Of the 29 shipped rope records, 23 name effects 62, 62, 63, one 62, 62, 62, and five none. - Torch: while the player’s torch burns, effect 25 plays from the player
with
off = −40on one shared channel, owned by the player. It is restarted whenever it has finished, its volume and pan are refreshed every frame, and it stops when the torch goes out.
Mixer gain and pan law
The statically linked Miles mixer derives channel scaling from the sample’s volume, pan and PCM sample width whenever volume or pan changes.
Volume and pan are clamped to 0–127. The effective volume is
v = clamp(trunc(volume × driver_master / 127), 0, 127). The driver master
starts at 127 and the game never changes it, so v equals the sample volume.
Pan uses a 128-entry table T: T[p] = 2p for p 0–62 and T[p] = 128
for 63–127. The left side uses T[127 − pan]; the right side uses T[pan].
At pan 64 both entries are 128. Towards either extreme the far side fades
while the near side retains its centre level.
For 8-bit PCM, the mixer builds lookup tables. Set a = v + 1 when v is
nonzero, otherwise a = 0. For a sample amplitude s expressed in signed
16-bit units and the corresponding pan-table value t, the table contribution
is floor(floor(s × a / 128) × t / 128). At the centre, nonzero volume
therefore scales by (v + 1) / 128.
For 16-bit PCM, the mixer computes a scalar coefficient for each side:
c = floor(t × v / 127). A signed sample contributes floor(s × c / 128)
to that channel. The same coefficient scaling applies when the mixer advances
the source at a different playback rate. There is no nonzero-volume increment
in this path. At the centre, volumes 0–126 scale by v / 128; volume 127
has unity gain. A mono sample supplies both output channels.
SURFACE.INI remap
At startup the engine builds a table [surface 0..11][sound 0..255] → effect,
each row initialised to identity, from the SURFACE.INI
sound sections in this order: unknown (0), water (1), deepwater (2),
scapewater (3), scapedeepwater (4), lava (5), sand (6), wood (7),
tile (8), scape (9), rock (10), gloop (11).
Only animation PlaySound uses it: when the animated object’s surface value is
non-zero, effect = table[surface][effect]. Sound, AmbientSound, combat
hits and speech are not remapped. The configured unknown row is bypassed.
Missing sound sections retain identity; repeated entries overwrite earlier
ones. Targets are unsigned 16-bit effect IDs and still pass through the sound
bank’s playback validation.
The lookup helper accepts nonzero surface codes up to and including 12, though
only rows 0 through 11 exist. Code 12 reads beyond the allocated table and has
no defined remap. Codes greater than 12 and an absent remap table return the
input effect unchanged. There is no source-ID bounds check in this helper;
animation callers must supply an ID in 0..255 for a valid row lookup.
An object’s surface value comes from the face it stands on:
- A model face: the surface type that
[surfaces]assigns to the face’s texture, or 0 when the texture has no entry. - Terrain: each query first resolves texture set 302 and the tile’s low-six-bit
image index through
[surfaces]. The terrain blend mask can replace that value with sand or rock at dry shore pixels. A nonzero water result then selectsscapewater(3), orscapedeepwater(4) for full water coverage. A dry result keeps the surface computed by this query, not a previous object’s terrain-water result. - Other ground-query source kinds assign 0 on the ordinary assignment path.
This uses the ground-support query, not an arbitrary side collision. Model
support supplies the packed texture of the selected face; solid-color faces
supply zero. Terrain support queries the actor’s integer map X/Z. An
out-of-grid terrain tile lookup returns -1, which supplies image 63 after
the low-six-bit mask, before the usual assignment and coverage rules.
The support wrapper updates actor surface state only when collision attribute
mask 0x04 is set. Excluded and query-only paths retain the previous actor
surface. A support miss reaches the updater with a cleared source kind, so it
assigns zero only if the ordinary assignment path is reached. See
surface-query state retention.
Shipped script calls
The 27 shipped RGM files contain 137 Sound, 477 FlatSound, 91
AmbientSound, 97 AmbientRTX, 33 EndMySounds and 12 StopAllSounds
calls, and none to EndSound or EndMySound. Values outside the usual
ranges occur: FlatSound volume 128 (clamped to 127) and rate offset −10000
(clamped to 512 Hz), AmbientSound rate offset 2000, AmbientSound effect 127
(beyond the 118 shipped effects, so it never sounds), and AmbientRTX volume
offsets of 127 and 128 (volume 127 everywhere within the radius R). Every
AmbientRTX call passes 0 as its third argument.
Related
- SFX — effect headers (sample rate, base volume, loop flag)
- RTX — speech clips
- SURFACE.INI — sound remap sections
- SYSTEM.INI —
volume,sound_distance - Item Attachment System — animation opcode 4
- Book menu — Sound slider
- Music — CD audio, separate from effects
Background Music
Redguard’s music is Red Book CD audio, played from the game CD through the Miles Sound System redbook interface. It is not stored in any game file.
The CD
Track 1 is the data track; tracks 2–7 are audio. The GOG release ships the
CD as a cue sheet game.ins and a raw image game.gog (2352-byte sectors)
in the directory above Redguard\, and its DOSBox configuration mounts it as
drive D:.
The audio designation in the cue sheet does not guarantee that every byte in
the range is audio. The image packaged by the GOG 2.0.0.5 installer includes
scrambled Mode 1 data-sector bytes at the beginning of track 2 before its audio
silence. The scrambled region preceding track 2 overlaps the end of the plain
data region and is displaced relative to the sector addresses in its headers.
That displacement carries data beyond the cue sheet’s audio start. The
data-to-audio transition is not aligned to a 2352-byte sector boundary.
Interpreting the overlapping data as 44.1 kHz signed 16-bit stereo PCM produces
a brief loud noise; the bytes are CD-ROM sector content, not recorded music.
All 29 shipped WORLD.INI entries set world_redbook, and every value lies
in 2–7.
Track selection
Loading a world has two CD steps, at different points:
- At the start of the load, before any world data is read, if
world_redbook[N]is not-1and differs from the track the CD is currently playing, the engine stops the CD. The loading screen is therefore silent. - After the world is loaded, if
world_redbook[N]is greater than zero and differs from the track the CD is currently playing, the engine plays trackworld_redbook[N]and appliesredbook_volume.
Consequently:
- A world without
world_redbook(or with-1) keeps the current track playing. - A value of zero, or any negative value other than
-1, stops the CD and starts nothing. - Moving between worlds that share a track does not restart it.
A track number below 1 or above the CD’s track count is ignored.
Per-world snapshot restoration follows these world-load CD steps. A revisit uses the live CD track for its track-selection decision. Loading a compatible main save for the currently loaded world skips world setup and its CD track selection. Loading a compatible main save for another world runs world setup before restoring the saved state.
Book menu and movies
Opening the book menu plays track 2 from its start and applies redbook_volume.
The startup splash and title play no CD audio.
Every movie, whether the introduction, a browsed Movies page entry, or a movie queued before the next world, runs the same sequence: sound effects stop, the CD pauses if it is playing, the movie plays, sound effects stop again, and the CD resumes if it is paused. The CD continues from where it paused. Movie audio comes from the Smacker stream, never from the CD.
New Game plays the introduction movie and then loads world 0. The book menu’s
track 2 pauses for the movie and resumes when it ends; world 0’s
world_redbook is 7, which differs from 2, so the world load then stops the CD
and track 7 starts only after the world is loaded. No CD audio plays during the
load.
The credits scroll (the ending after WonGame, and F10 in a world) stops the
sound effects, plays track 2 at redbook_volume, and stops the CD when the
scroll ends or Escape is pressed. The last requested track stays 2, so after
in-world credits the repetition check replays track 2, not the
world’s track, until a world change to a different track.
Repetition
Periodically from the main loop, the engine compares the CD’s current track with the last track it requested. When they differ — the requested track has ended and the CD has stopped — it plays the requested track again. World music therefore repeats indefinitely, with a short pause between repetitions.
Volume
Each track start applies redbook_volume from SYSTEM.INI. The engine clamps
the value to 0–255 and halves it to the 0–127 range of the redbook volume call,
so the shipped 200 plays at 100 of 127. The book menu’s Music slider sets the
value. On exit the engine sets the CD volume to 127.
redbook (SYSTEM.INI [system], shipped on) gates the redbook system;
when it is disabled the CD is never opened and no music plays. CD initialization
also requires sound to be enabled.
With sound disabled, the engine skips effects, voice and CD initialization and
forces text dialogue with speech off.
Related
- WORLD.INI —
world_redbook[N] - SYSTEM.INI —
redbook,redbook_volume
Subtitles
Spoken dialogue and combat taunts draw their text in
FONTS\REDSEL.FNT on a virtual 640×480 screen, which the Glide renderer
scales to the output size.
Glyphs
- Advance per character: glyph width + 1. A space additionally adds the width
of
!, so a REDSEL space advances 11 pixels. Disabled glyphs advance 0 and are not drawn. - A glyph is drawn at (pen x +
offset_left, line top +offset_top). Palette index 0 is transparent. - Each glyph is drawn twice: first a shadow copy offset by (1, 1), then the main copy.
- Copies of REDSEL differ only in a value pair
A,B: NPC lines useSYSTEM.INI [3dfx] font_sel(shipped255,255), Cyrus’ linesfont_norm(125,255). The main copy is drawn with alphaB, the shadow copy with alphaB × 200 / 256(integer);Adoes not enter either alpha. Glide draws each copy with that value as the constant colour’s alpha. Values below 254 blend over the scene (source alpha, one minus source alpha); 254 and above draw without blending. - For NPC lines using
font_sel, main glyphs are opaque in the font’s own palette colours. BPAL entries are 6-bit; index 19539,3,14is crimson and index 8160,59,11is yellow. The shadow is the glyph shape in black, blended over the scene at alpha 199/255. - A line is centred on x = 320: it starts at
320 + trunc(−width / 2). - Subtitles ignore
[system] disable_text.
Shared text colour state
The Glide text colour setup does not select a palette. For a font with its
indexed-colour flag set, it reads the current text-colour index from the
already selected palette and writes those RGB components to the constant
colour state. The constant alpha is the font’s first style value, A.
For a font without that flag, all four constant colour components are set
to A instead. These are the text setup values; individual glyph and shadow
passes apply the rules above.
The text colour reset sets all four constant colour components to 255. It does not restore an earlier constant colour or change the selected palette.
Dialogue
The text is the RTX entry’s own string: the label of a voiced entry, the text
of a string-only entry. Script functions RTX, rtxAnim and RTXpAnim
(speaker: the scripted actor), RTXp (speaker: Cyrus) and combat taunts
create subtitles.
Slots
[dialog] dialog_max_dialog_lines is the number of subtitle slots, at least
16 (shipped 20), not a line limit. Slot 0 belongs to Cyrus (RTXp) and is
overwritten unconditionally; other speakers take the lowest free slot, and no
subtitle is created when none is free. Only one subtitle is drawn per frame:
the lowest active slot.
Wrapping
Width limit W = max([dialog] dialog_max_text_width, 150) (shipped 500),
measured with REDSEL advances. The text is split into tokens, each an optional
leading space plus a word. A token joins the line while running + token < W (strict); otherwise a line break replaces its leading space and the running
width restarts at the token’s width including the dropped space.
Position
With breaks line breaks, the block height is 26 × (max(breaks, 1) + 1)
and the first line’s top is 470 − height; each next line is 26 lower. One
line therefore sits at y 418, two lines at 418 and 444.
Timing
dialog_use_speech = 1: a voiced line shows while its clip plays and is freed when the clip stops. A string-only entry is neither drawn nor freed.dialog_use_speech = 0: the voice is stopped. The line starts withremaining = clip seconds × 256(voiced) orcharacters × 15(string-only) and loses 21 per game tick; it is freed onceremaining ≤ 0.dialog_print_text = 0: slots keep the same timing (script waits are unchanged) but nothing is drawn.- Script
RTXlines from a speaker farther thandialog_max_distance(1600) are not started.
Movies
Movie subtitles are documented in movies.
In-game HUD
The Glide build draws the in-game overlay (candle, active item, prompt text, compass, power-up icons and logbook icon) over the 3D scene on a virtual 640×480 screen, scaled to the output size.
Shared rules
- A HUD bitmap is drawn with its top-left corner at the configured anchor
plus the frame’s draw offsets (the signed fields at frame-record offsets
0x06and0x08, see GXA). Every frame of the HUD atlases has offsets(0, 0), so each INI coordinate is the bitmap’s top-left corner. The power-up slots and the logbook icon use the anchor alone. - Palette index 0 is transparent.
- HUD text uses
FONTS\REDSEL.FNTwith the[3dfx] font_selpair (shipped255,255), the same glyph, shadow and colour rules as an NPC subtitle. Text is drawn as stored, ignores[system] disable_text, and wraps only when a glyph would cross x = 640. - Timers below count game ticks, the unit of
rtx_pickup_override_timeand the subtitle timers; the tick advance per frame is the FPS-derived frame scale, so ticks run atnormal_frame_rateper 18 BIOS timer ticks (about 12.14 per second with the shipped value 12) while the frame rate stays at or abovemin_frame_rate. Animations marked per rendered frame advance once per drawn frame, independent of ticks. The 3dfx renderer opens Glide at 60 Hz with two colour buffers and swaps with a swap interval of 1, so each frame waits for one vertical retrace: these animations advance at most 60 times per second and otherwise at the frame rate the machine reaches.max_frame_ratelimits only the tick computation, not drawing. - Lockout retains the low 16 bits of the value passed to
LockoutPlayer; the engine also resets it to 0, for example after player death. Each element below tests it differently.LockoutPlayersets the selected player’sattr_no_node_combatto one only when the retained lockout value is exactly one. Other values clear that attribute on the selected player and every registered actor that currently has it set. EveryLockoutPlayercall clears the saved highlighted-choice text used by the SOUP topic menu. Values other than one also clear the HUD prompt-override timer. The function returns zero as its SOUP scalar result. attr_player_typeuses the SOUP386.DEF valuesply_cyrus0,ply_gremlin1,ply_crossbow2.
| Element | Bitmap | Frames | Size | Anchor keys | Shipped anchor |
|---|---|---|---|---|---|
| Candle | SYSTEM\SNUFF.GXA | 81 | 50×98 | candle_xco, candle_yco | 12, 8 |
| Active item | ITEM.INI bitmap_file (SYSTEM\PICKUPS.GXA) | 66 | 119×83 | pickup_xco, pickup_yco | 12, 386 |
| Compass | SYSTEM\COMPASS2.GXA | 32 | 73×65 | compass_xco, compass_yco | 546, 396 |
| Power-up slots | [system] game_bitmap (system\powerup.gxa) | 2 | 39×61, 54×61 | game_xco1/2, game_yco1/2 | 576, 6 and 576, 96 |
| Logbook icon | SYSTEM\SCROLL.GXA | 32 | 73×58 | logbook_xco, logbook_yco | 540, 20 |
All anchor keys are in SYSTEM.INI [system].
Candle
The candle shows the player’s health. Maximum health is a fixed 100 (no INI
key). Health is attr_health (attribute index 3 in SOUP386.DEF), always
clamped to 0..100.
Admission and lifetime
The common candle routine requires a loaded candle bitmap, a separate enabled flag, and rendering to be enabled. It checks these conditions before servicing F8, advancing the candle animation, or processing its show timer. A failed condition skips all of that work, independently of candle mode and lockout. The console’s render on/off control supplies the rendering condition.
Candle initialization releases any previous candle bitmap, loads the requested
bitmap, sets the enabled flag, and resets the shown level and animation step.
The HUD initializes it with SYSTEM\SNUFF.GXA. Candle teardown releases the
bitmap and clears both its reference and the enabled flag.
Level
With integer arithmetic and truncating division:
d = ((100 - min(health, 100)) * 256) / 100
L = (d * 11) >> 8
Level L | Health |
|---|---|
| 0 | 91–100 |
| 1 | 82–90 |
| 2 | 73–81 |
| 3 | 64–72 |
| 4 | 55–63 |
| 5 | 46–54 |
| 6 | 37–45 |
| 7 | 27–36 |
| 8 | 18–26 |
| 9 | 9–17 |
| 10 | 1–8 |
| 11 (snuffed) | 0 |
Frame layout
Levels 0–10 own six frames each: 6L to 6L + 4 are the flicker cycle and
6L + 5 is the transition frame between level L and L + 1 (frames 0–65).
Frames 66–80 are the snuff-out animation.
Animation
The candle keeps a shown level S (unset when a play session opens) and a
step counter C (initially 0). Every rendered frame of a running world,
whether or not the candle is visible:
- If
Sis unset,S = L. - If
L < 11:C = C + 1, wrapping to 0 above 4. Then- if
S < L: show6S + 5, thenS = S + 1,C = 4; - if
S > L:S = S - 1, show6S + 5,C = 4; - otherwise show
6S + C.
- if
- If
L = 11: ifS = 11,C = C + 1; otherwiseS = 11,C = 0(no stepping through levels).Cis clamped to 14 and frame66 + Cis shown, holding frame 80.
The candle therefore steps one level per rendered frame through transition frames and flickers in a five-frame loop. Frames on consecutive rendered frames:
| Situation | Frames |
|---|---|
| Spawn at health 100 | 1, 2, 3, 4, 0, 1, 2, … |
| Level 0, health drops to 50 | 5, 11, 17, 23, 29, 30, 31, 32, 33, 34, 30, … |
| Level 5, health restored to 100 | 29, 23, 17, 11, 5, 0, 1, 2, … |
| Level 5, health drops to 0 | 66, 67, …, 80, 80, … |
| Snuffed, health set to 100 | 65, 59, 53, …, 11, 5, 0, 1, … |
Quick sword
KEYS.INI quick_sword_key reads the held key on each world update. Lockout 1
or selected-player attr_player_type 1 disables this handler. Drawing or
sheathing a sword prevents another sword operation until that transition ends.
When the player is holding a weapon, the key requests sheathing. Otherwise it uses the first owned item in the order 1, 13, 75. Items 1 and 13 become the selected item before use; item 75 is used without changing the selection. An empty search leaves the selection unchanged.
The key remains held after handling. Holding it through a completed draw can therefore request sheathing without another press. Item use retains its normal eligibility and torch precedence: a burning torch is doused instead of drawing the requested sword, after the selection has changed to item 1 or 13.
Visibility
[screen] candle_mode selects the visibility mode (shipped 2):
| Mode | Candle |
|---|---|
| 0 | Shown only while the show timer runs. |
| 1 | Always shown. |
| 2 | Always shown, with the health number below it. |
In every mode the candle is drawn only while lockout is exactly 0. The show
timer T (ticks) is set:
- to 25 when the player’s health changes (damage, healing, health script
functions) while
Tis below 50 (some health script functions only whileTis at most 25); - to 25 when the player draws or sheathes the sword;
- to 50 on every frame while the sword is drawn and lockout is 0;
- to 50 while the inventory screen is open (which draws the candle itself), and to 0 when it closes.
After the candle is drawn, a timer with 0 < T < 50 loses the frame’s ticks,
stopping at 0. A value of 50 never decays; it is replaced by the events above.
T does not decay while lockout hides the candle, and it is stored in save
games. In mode 0 the candle is thus visible while the sword is drawn, for 25
ticks after a health change or a draw/sheathe, and in the inventory.
Health number
In mode 2 the player’s attr_health is drawn as a decimal integer. It is
centred, by the subtitle centring rule, on
x = candle_xco + width / 2 − 5 (integer half) with its top at
y = candle_yco + height; with shipped values, centred on x = 32 at y = 106.
After drawing the candle bitmap, the number pass saves the current text settings, selects the standard GUI font with text-colour indices 1 and 0, and enables text before formatting and painting the number. It reads the selected player’s current health attribute independently of the cached health used to advance the candle animation. It then restores the previous font, colour indices, shadow settings, text cursor, case flags, text viewport and text-disable flag. The selected renderer palette is outside this saved text state; pushing and restoring these text settings does not select a palette.
Mode key
F8 (scan code 66, not configurable in KEYS.INI) cycles candle_mode
0 → 1 → 2 → 0 and immediately writes the new value to SYSTEM.INI
[SCREEN] CANDLE_MODE. No notice is shown.
Active item
The icon is frame bitmap[x] of ITEM.INI bitmap_file for the selected item
x, drawn at (pickup_xco, pickup_yco). The HUD does not use
bitmap_selected_file. All 87 shipped items define bitmap[x].
pickbox.gxa and pickblob.gxa are not used: no box or blob is drawn behind
the icon.
Count
When the selected item’s count is greater than 1, the count is drawn as a
decimal integer, left-aligned with its top-left at
(icon_x + width − 32, icon_y + 8): (99, 394) with shipped values. At a new
game the gold count is 53 (player_total[2] = 52 plus one from
start_item_list), so the gold icon shows 53.
Inventory ownership
The item system initializes each count from player_total[x], then adds one
of each ID in start_item_list through the acquisition rules. Counts and the
selected item belong to the game session and survive ordinary world changes.
HaveItem returns the current player count. AddItem returns zero. A positive
addition is capped by both total[x] - count and player_max[x] - count.
It succeeds only when all recursively required items are owned. A successful
addition performs the configured drop/add lists before incrementing the count.
A nonpositive addition or an exhausted cap leaves the count unchanged.
SelectItem selects the requested defined item without checking ownership or
visibility; the next item-overlay update repairs an exhausted selection.
Selection
- The initial selection is ITEM.INI
start_item_select(shipped 1, the sabre), or the first item when absent. - KEYS.INI
[input] prev_key/next_keystep once per press through items in ID order, wrapping, and skipping items withhide[x] = 1or a count of 0. - Cycling is disabled while the sword is drawn or
attr_holding_weaponis set; the icon stays visible. Drawing or sheathing does not change the selection. Disabling rendering suppresses item artwork and text, but does not disable otherwise-eligible item cycling. - When the selected item’s count drops below 1, the selection moves to the next valid item; with none, nothing is selected and nothing is drawn.
Visibility
Evaluated each frame in this order:
- Lockout 1: neither icon nor prompt is drawn, and the timers below hold.
- Activation hold-off
A > 0:Aloses the frame’s ticks, stopping at 0, and nothing is drawn. - A pending activation prompt replaces the icon (see below).
attr_player_type1: nothing is drawn.- Otherwise the icon and count are drawn.
Lockout values other than 1 do not hide the icon.
Inventory screen
KEYS.INI inventory_key opens a modal screen over the last world frame. The
world update latches the key and opens the screen on its next update. The
screen pauses the game clock and captures the current frame before waiting
for the key to be released. It then installs the inventory text settings.
The saved frame contains tightly packed 16-bit pixels copied from the display
rows using the source stride; capturing it does not select a renderer palette.
The clock resumes on exit. Each screen update polls input once and then waits
for the next 18.2 Hz BIOS tick; the navigation and exit checks read that poll.
A mouse binding therefore reads the button release
between two successive updates.
Each admitted update draws the candle with its show timer pinned to 50 and advances its existing animation state once. Lockout can hide the candle without stopping that state update. The clock wait admits a drawing pass when the sampled tick changes; a jump of several ticks still produces one pass, without catching up the skipped ticks.
Inventory bitmap resources
Inventory bitmap initialization releases the previous mask, border and arrow
images, then loads SYSTEM\INVMASK.GXA, SYSTEM\INVBORDR.GXA and
SYSTEM\INVARROW.GXA in that order. All three loads use the same palette
output buffer. Each successful load overwrites that buffer, leaving the arrow
palette after the final load.
The bitmap loader expands the first decoded frame’s palette by shifting each of its 768 bytes left by two bits. With a palette output buffer, it copies those bytes to the buffer. With no output buffer, it instead calls the palette installation service for 256 entries. Inventory bitmap initialization uses the copy branch for all three resources.
Bitmap image preparation keeps six texture slots arranged as three columns and two rows. The column and row origins are spaced 256 pixels apart. Slots outside the image dimensions contain no texture. Each populated slot receives a separate buffer of indexed pixels, copied row by row using the full image width as the source stride and the region width as the destination stride. A 640 by 480 image therefore produces widths 256, 256 and 128 in each row, with heights 256 and 224.
Each region requests an indexed texture and supplies its tightly packed pixel buffer to the texture population service. The resulting texture identifiers are stored in the image’s six-slot array. The preparation routine assigns consecutive image identity values to populated regions in slot order.
Item list
The list holds every item with hide[x] = 0 and a nonzero count, in ID
order. It is empty while the selected player’s attr_player_type (attribute
105) is 1. The cursor starts on the selected item. When the selected item is
not in the list, the cursor keeps its value from the previous opening (zero at
the first opening). An empty list sets the cursor to −1.
Navigation
The list is a grid of four columns. Right adds 1 to the cursor, left subtracts 1, down adds 4 and up subtracts 4. The result is clamped to the list without wrapping. Left, right, up and down are the KEYS.INI turn and move bindings. A move that changes the cursor plays sound effect 13, and further moves are ignored for the next three ticks while any direction is held. Releasing every direction ends the delay.
Three rows are visible. The first visible row moves up when the cursor is above it and down until the cursor is within the three visible rows. The first visible row is retained between openings.
Exit
The screen begins closing when inventory_key, Activate (key_d), the
walk/run modifier (key_a), Escape or Enter is held. Closing first waits for
the inventory key to release, then for Escape to release. It clears the candle
show timer to zero, then samples Activate and Enter once. If either is held and the cursor is valid, the cursor
item becomes the HUD active item; otherwise selection is
unchanged. A confirmation released during those first waits does not select,
and a confirmation first pressed during them can select.
After that selection test, closing waits for the walk/run modifier, Activate and Enter to release, in that order. Each wait completes once; a key pressed again after its wait has completed does not restart that earlier wait.
Exit selects resource group 31 and releases group 28, then processes deferred texture residency changes. A texture also owned by another group retains that ownership. A texture whose last group is released is freed even when repeated lookups had raised its reference count.
The saved 16-bit frame is copied back using the destination display stride, then its buffer is freed. Exit calls display finalization, clears the cached candle health, frees the temporary item list, restores the saved text state and resumes the clock, in that order. Restoring the saved frame copies its pixel values directly; it does not select a renderer palette.
The optional rectangle drawn during display finalization uses black RGB vertex colour. Its colour setup and rectangle drawing do not select an indexed renderer palette.
The rectangle helper swaps both corner pairs when the second Y coordinate is smaller than the first. It does not independently reorder reversed X coordinates. It rejects rectangles wholly above or left of the viewport, or whose first corner lies beyond the right or bottom viewport limit. It clamps negative first coordinates to zero and second coordinates to the viewport limits, then adds one to the second X and Y coordinates before emitting two triangles. A point on a viewport limit therefore reaches one coordinate unit beyond that limit.
The rectangle’s graphics setup and post-draw reset send the following Glide API arguments in table order. The reset installs fixed values rather than restoring captured caller state. Setup sets chroma-key mode to zero; the reset leaves that setting unchanged.
| API | Rectangle setup arguments | Post-draw reset arguments |
|---|---|---|
grChromakeyMode | 0 | No call |
guColorCombineFunction | 2 | 6 |
grDepthMask | 0 | 1 |
grDepthBufferFunction | 7 | 1 |
guAlphaSource | 1 | 0 |
grAlphaBlendFunction | 1, 5, 4, 0 | 4, 0, 4, 0 |
The queued-text enable flag belongs to the queue’s buffer lifecycle. Initialization first tears down the previous queue, allocates and zeroes 66 buffers of 256 bytes each, then enables processing. Teardown frees and clears all buffer pointers when the enable flag is exactly one, then clears the flag.
For each buffer, initialization stores the allocation result and passes it directly to the 256-byte zero-fill operation. It does not test for a null result or retry the allocation before that operation.
The buffer-fill path repeats the low byte of the fill value across the requested byte range and returns the original destination. A zero byte count skips all destination access. For a positive count, the underlying fill routine reads the first destination byte before writing and has no null-pointer guard.
Startup initializes queued-text suppression from [system] disable_debug_text
in SYSTEM.INI, separately from the ordinary disable_text value. When the
startup q option query succeeds, it sets both values to one and skips those
configuration reads. These are initial values; suppression is mutable engine
state, separate from the queue’s allocation flag. Queue initialization and
display finalization do not restore suppression from saved text state.
Display finalization also processes queued text. When that processing is enabled,
it temporarily enables text output. Unless drawing is suppressed, it visits up
to 66 queued buffers in order and stops at the first empty buffer. The console
output route submits each buffer with %s; the ordinary text route uses %s\n.
It then clears all 66 buffers, 256 bytes each, even when drawing is suppressed
or stops at an empty buffer, and restores the incoming text-disable value.
The queue processor does not save and restore the remaining text state or select
a renderer palette itself; text-service calls are separate operations.
Ordinary queued text starts at the current text cursor and leaves that cursor updated after each message. Line feeds return X to the current line origin and advance Y by the font line height. This route uses the current font, temporarily selecting its linked small font when a configured text viewport is narrower than 640 pixels. It restores the incoming font after each message. The text sink does not install a separate text style or select a renderer palette; shared glyph services handle text colour.
The text viewport-mode setter retains its full 32-bit input value. The setter leaves the display width, display height and selected renderer palette unchanged. Ordinary text treats zero as the fixed 640-pixel viewport mode and any nonzero value as the configured-width mode.
The ordinary route treats message contents as string data: percent characters remain literal. It copies bytes up to the first NUL, appends one line feed, and NUL-terminates the formatted text. An existing trailing line feed is retained, so an additional one follows it.
TAB in ordinary text changes only the horizontal cursor. For a positive text
viewport width W of at least eight pixels, let s = trunc(W / 8). Without
signed arithmetic overflow, the new X is X + rem(X + s, s), where rem is the
signed remainder from division truncated toward zero. This calculation uses
absolute X, without subtracting the line origin, and does not wrap immediately.
At a width of 640 pixels, X values 1, 79 and 80 become 2, 158 and 80 respectively.
Repeated tabs apply the calculation to the updated X. TAB bypasses character
lookup and glyph drawing. NUL ends the formatted text; apart from LF and TAB,
nonzero bytes, including CR, pass unchanged to character lookup.
The ordinary text sink reuses shared formatting storage supplied by the executable data image. It does not allocate or free formatting storage per message. The formatting call receives no destination-size argument.
The buffer-clear wrapper can run optional memory validation before filling. Outside a reentrant validation call, a nonzero byte count first checks the destination address, then the final byte address if the first check returns without an error. These numeric checks enter a diagnostic path for addresses below 16; they do not read the destination bytes. A zero count skips these address checks. If validation returns normally, its return value does not suppress the subsequent fill.
View (key_c) opens the cursor item’s examine view with its
inventory_object_file model. When that view returns a nonzero result, the
inventory screen closes without changing the selection.
Each loop latches View before waiting for the next tick. It updates the cursor and draws the inventory before servicing the pending View request. With a valid cursor, that request is handled before ordinary exit keys are tested, including when View and an exit key are sampled together. After the view returns, the exit test uses the controls remaining after the view’s release wait.
From the screen update that opens View until the examine view returns, the inventory drawing pass omits the cursor item’s bitmap, quantity and description. Other visible cells keep their ordinary drawing behavior. The inventory background and health candle are still drawn.
Examine view
The view shows the item’s inventory_object_file model (loaded from INVENTRY.ROB) over the
frozen screen. The inventory screen opens it with mode 1 and the cursor cell’s screen position. The world loop
also contains a mode 0 entry gated by a request flag, item and position words; the
ShowItem and ShowItemNoRtx natives write those words, so scripts reach it. It pauses the game
clock for its duration, plays sound effect 61 when it opens and 60 when it begins to close, and
waits for the next BIOS tick each update. It returns 0 when the item is not in the list.
Three Euler angles (0..0x7FF, wrapping) turn the model: yaw about Y, pitch about X and roll
about Z, applied to an identity matrix in that order. Offsets are single-precision values. The
model’s extents a, b and c are the values at model offsets 1, 5 and 9 and M is the largest.
The model starts at offset (0, 0, c + 180) and unrotated.
While the view is open, each tick with any direction, A, B or C held applies one step:
| Held | Left / right | Up / down |
|---|---|---|
| none | yaw +0x50 / −0x50 | pitch −0x50 / +0x50 |
B (key_b) | roll −0x50 / +0x50 | depth +80·s / −80·s |
A (key_a) | X offset −40·s / +40·s | Y offset −40·s / +40·s |
The scale s is (depth − c) / (8·M + 180) + 0.1. Depth is clamped to [c, c + 8·M + 180],
and then each offset is clamped to ±2·M·s (the Y offset’s range is scaled by the viewport
height over its width).
The view opens by interpolating a factor from 1 to 0 in steps of 0.15 per tick: the model’s
offset is the target offset plus the fly-in offset times that factor. The screen position that
mode 1 supplies is the cell the item sits in, so the model flies from its cell to the centre: for the cursor cell at visible row r and column c the entry offset is (388c - 582, 378r - 432, 1490 - (z extent + 180)) and the close flies the model to (388c - 582, 378r - 432, 1490). The view
ignores close requests until Activate, View, Enter and Escape are all released after opening.
Pressing View, Activate, Enter or Escape then closes it: the factor rises by 0.15 per tick back
to 1 with the angles interpolating to zero by the shortest way round, and the view returns once it
exceeds 1. The return value is 1 when Activate or Enter was held at the close request and 0
otherwise. The caller waits for View, Activate, Enter and Escape to release before continuing.
The view draws the model with the Glide viewport’s camera at the origin looking along +Z, so
camera-space (x, y, z) projects to (320 + 320*x/(0.788*z), 240 + 320*y/(0.788*z)) at 640 by 480.
Before each draw it registers three local lights in camera space, each with radius 6400:
(0, 42768, 65535) with RGB (180, 180, 180), (0, -256, -200) with RGB (255, 255, 255) and
(0, 256, -200) with RGB (255, 255, 255). The ambient and directional model lighting are the
loaded world’s. Textures and solid colors resolve through the palette selected at that time (the
world’s), not through an interface palette.
Each update draws the backdrop, then the model, then the description text. In mode 0 the
backdrop is a copy of the screen taken when the view opens, which is restored when it returns; no
frame, candle or inventory artwork is drawn. In mode 1 the backdrop is the inventory screen
drawing pass described above: the inventory’s copy of the world frame, the candle, the shaded
quad, INVBORDR.GXA, the arrows and the other visible cells.
The current description line’s RTX text is drawn in REDSEL, the inventory screen’s font. It is
split into lines by the inventory name wrap with a 30-byte step instead of 13. Line n
is centred on x = 320 with its top at 10 + trunc(0.9 × n × line_height). The text is drawn only
when the line has a record, the text does not start with byte 0x9E, the no-text flag is clear and
[dialog] dialog_print_text is nonzero. It is not a dialogue subtitle: it takes no
subtitle slot and does not use the subtitle position. Stopping the line’s speech when the view
begins to close does not remove the text: the line continues as a line without speech, which
with sound on stays until the view returns.
A cursor item without bitmap[x] draws no artwork.
Layout
Coordinates are in the 640 × 480 screen. The screen draws, in order:
- A quad from (90, 49) to (550, 433) in colour (10, 10, 10) with alpha 180, blended as source alpha over one minus source alpha, without depth test.
SYSTEM\INVBORDR.GXAframe 0 at (90, 49).SYSTEM\INVARROW.GXAframe 0 at (313, 54) when the first visible row is not the first row.- For each visible item, column
cand visible rowr, withx = 100 + 105candy = 87 + 102r:- the cursor item draws frame
bitmap[x]ofbitmap_selected_fileat(x − 2, y − 2); other items draw framebitmap[x]ofbitmap_fileat(x, y); - a count greater than 1 draws the count in decimal, centred on
x + 84, with its top aty + 10.
- the cursor item draws frame
SYSTEM\INVARROW.GXAframe 1 at (313, 419) when rows follow the visible rows.
Text uses the REDSEL font with a (2, 2) shadow. The cursor item’s name[x]
RTX text is split into lines like the
logbook wrap with a 13-byte step,
replacing a space with a line break. Line n is centred on
x + w / 2, where w is the selected frame’s width (integer half), with its
top at y + 92 − b + trunc(0.9 × n × line_height). b is the
bottom of the font’s a glyph (top offset plus height). An item without name
text, or whose text starts with byte 0x9E, draws INDEX <id> centred on
x + 52 at y + 92 − b.
The candle is drawn with its timer at 50 while the screen is open and the timer
is 0 after it closes. SYSTEM\INVMASK.GXA is loaded with the screen artwork
but not drawn.
Prompt text
A single text line is drawn left-aligned from x = pickup_xco with its top at
y = pickup_text_yco (shipped 12, 436).
Activation prompt
The script function Activate(label) (also used by TorchActivate) offers
RTX entry label as the prompt when lockout is not 1, no dialogue topic menu
is open, the sword is not drawn, no other object holds the activation, the
hold-off A is 0, and no prompt is already pending. An existing
activation owner may offer its own prompt again when the other gates permit it.
When the player’s attr_player_type is 1, the receiving object’s
attr_special_activate must be nonzero. A receiver attached to a rope or in an
IsOnEdge state cannot offer a prompt. IsOnEdge is true for actor states
10 through 19 inclusive. These two tests apply to the receiving object.
The selected player must not be holding a weapon, drawing a sword, or sheathing
a sword. The shared player-lineup result must be zero. Standalone player
initialization clears this result. Each PlayerLineUp invocation stores its
selected alignment helper’s result in the shared value before returning it. Activate requires the
player’s torch-burning counter to be zero; TorchActivate requires it to be
nonzero. The prompt text is the RTX entry’s text; a missing RTX entry does not
activate the object. RTX lookup folds ASCII uppercase letters in the four-byte
key to lowercase. An existing entry with empty text still supplies a prompt and
can activate the object.
The weapon predicates use the item system and the actor’s current animation.
Outside shell-script execution, with the item system initialized and a current
animation present, drawing is actor state 20 or animation group 91; sheathing
is actor state 21 or animation group 129. Combat capability additionally
requires an assigned weapon. A nonzero attr_holding_weapon makes the actor
combat-capable and holding a weapon directly. Otherwise holding a weapon
requires combat capability, the weapon-drawn flag, and no sheathing state.
- If the use key is held when
Activateis called, the activation fires: the function returns true, the receiving object becomes the activation owner, andAis set to 22 ticks, blanking the icon and prompt area. The resolved prompt remains pending even when activation fires.Deactivatealways clearsA; it clears the activation owner only when called on that owning object. - Otherwise the prompt is drawn in place of the icon.
- While a prompt is pending, holding the previous/next item key sets an
override timer to
rtx_pickup_override_time(shipped 24 ticks). While it runs, the prompt is suppressed, the icon is drawn and items can be cycled. It decays by ticks and is cleared when no prompt is pending. - While the dialogue topic menu is open, and in some player states, a pending prompt draws nothing and still hides the icon.
A pending prompt is consumed when its text is drawn, not at the start or end of
every frame. Lockout 1, activation hold-off, prompt override, and early exits
that suppress prompt drawing retain it. The drawing checks use the selected
player’s rope attachment, IsOnEdge, and IsInDeepWater state, as well as the
dialogue topic menu. IsInDeepWater is true for water states 2 and 4.
Disabling rendering with the render off console command also suppresses prompt
drawing without consuming the pending text. render on removes that suppression.
Lockout 1 freezes the activation hold-off. Otherwise a nonzero hold-off is reduced by supplied ticks, saturating at zero, and that update still draws neither prompt nor item. With no hold-off, a nonzero prompt override similarly decays and selects the item path for that update, even when it reaches zero. An absent pending prompt clears the override before these decisions. Drawing a prompt while a previous/next item key is held sets the configured override after consuming the prompt; item cycling is deferred to a later item-path update.
Item name
The selected item’s name[x] text is drawn here only when the item has no
bitmap[x], which no shipped item does. Cycling items does not show the item
name.
Compass
The compass is drawn at (compass_xco, compass_yco) when lockout is not 1,
attr_player_type is not 1, the player owns item 0, and the compass display
flag is on.
The flag starts off, toggles each time item 0 is used, and is stored in save games. Receiving item 0 does not set it. This differs from the ITEM.INI comment that giving the player item 0 “will automatically turn on the compass display”.
Item use retains a consumed-use latch. The item overlay clears it when the
Activate/Attack input is released. An item-use call leaves the latch untouched
when lockout is 1 or the selected player is on an edge, or when the latch is
already set. Otherwise it sets the latch before testing item-system readiness
and the remaining actor gates. Item use requires the selected player, ordinary
rather than shell execution, water state other than 2 or 4, no held weapon,
no torch lighting or dousing transition, and no burning torch. The transition
checks require an initialized item system and an animation state; they recognize
actor states 33/34 or animation groups 149/150. A rope attachment does not by
itself block an explicit UseItem call, and the call does not remove the
attachment. An owned item 0 then toggles the compass flag. An unowned item cannot toggle it. Holding the use input does not
repeat the accepted toggle.
The Activate/Attack key (key_d) uses the selected item for an unarmed selected player: one press
starts one use of the item (a selected sabre is drawn, a selected drinkable item runs its use script
once) and holding the key does not repeat it. Without the key press nothing is used. Item use itself
keeps the refusals above, so a player holding a weapon uses nothing and the same key attacks instead.
A successful Activate or TorchActivate also marks the held action input as consumed.
The selected player skips the quick-key and action-key handlers while that mark and the
held action input are both set. Releasing the action input clears the mark before those
handlers resume. Releasing the activation owner, including at script End, does not
clear this input mark.
The compass flag lives outside the world and the session initializer. Only the item 0 toggle and a startup command-line switch write it, so it survives world swaps and a New Game.
The frame is
frame = ((-(heading + world_compass[N])) mod 2048) >> 6
where heading is the player heading
and world_compass[N] the current world’s WORLD.INI
value, in the same 2048-unit circle. Shipped worlds 3 (2000), 7 (1024),
13 (1536), 15 (1536), 20 (512), 22 (1024) and 23 (1024) set it; it is 0
elsewhere. Turning right increases the frame index.
| Heading | world_compass | Frame |
|---|---|---|
| 0 | 0 | 0 |
| 1985–2047 | 0 | 0 |
| 1984 | 0 | 1 |
| 1024 | 0 | 16 |
| 65 | 0 | 30 |
| 1–64 | 0 | 31 |
| 0 | 2000 | 0 |
| 1000 | 2000 | 17 |
Power-up slots
Two slots show frames of [system] game_bitmap (system\powerup.gxa: frame 0
gauntlet, frame 1 shield) at (game_xco1, game_yco1) and
(game_xco2, game_yco2), slot 1 then slot 2, while lockout is exactly 0.
ShowBitmap(id, frame)requiresid0 or 1 and a valid frame; other values are a fatal error. Ifframeis already shown in either slot, nothing changes. Otherwise it uses slot 1 if empty, else slot 2 if empty, else the slot after the one last chosen (choosing either slot, including an empty one, makes the other slot next). The slot stores(id, frame).UnShowBitmap(id)clears slot 1 if its id matches, otherwise slot 2 if its id matches.- Slot contents are stored in save games.
- The two slot frame indices live outside the world, with
-1meaning empty, together with the alternation state (which slot a new frame takes when both are full; it starts at slot 1). Loading a world releases the slot bitmaps and resets the alternation to slot 1. World entry then rebuilds each slot’s bitmap from its retained frame index, so a power-up survives a world swap. The session initializer (first start and New Game) sets both indices to-1, so New Game empties both slots.
Script logbook entries
AddLog(tag) accepts a four-byte RTX label packed into an integer in
little-endian byte order and returns 0. It checks a 400-slot tag table before
looking up the RTX entry. A matching tag suppresses the call. Otherwise the
first zero slot receives the tag, even when lookup or file opening later
fails. Zero is the empty-slot sentinel. When all slots are occupied, an
unmatched tag is not retained and the call continues.
A found RTX entry appends its text to LOGBOOK.TXT. A missing entry writes
nothing. Once the log file is opened, the engine writes the text, closes the
file and starts the logbook icon without checking the write result. A suppressed
tag or failed file open leaves the icon unchanged.
Logbook icon
When a new logbook entry is written and the icon is not already playing,
SCROLL.GXA plays frames 0–31 once at (logbook_xco, logbook_yco), one
frame per rendered frame, then disappears. It is not hidden by lockout.
Opening the logbook screen stops it.
Manual logbook entries
The installed C action opens a modal Log Entry dialog from the logbook
reader. The default keyboard binding is Alt, configured by key_c[0] in
KEYS.INI; this is the action binding, not the letter C.
Each dialog starts with empty text and accepts at most 64 bytes. Further input bytes are ignored at the limit; deleting a byte permits another byte to be entered. Inserted control bytes count toward this limit. It converts lowercase ASCII letters to uppercase before processing them, with Caps Lock either on or off and with or without Shift. Unshifted Backspace removes the last byte when the text is nonempty. Unshifted Return accepts nonempty text; an empty submission leaves the dialog open. Escape cancels the dialog, including while Shift is held. Tab produces no character.
Shift+Backspace appends a carriage-return byte (0x0D), and Shift+Return appends
a vertical-tab byte (0x0B), subject to the same byte limit. These combinations
do not delete or submit. Caps Lock does not change these control mappings.
Neither inserted control byte draws a visible symbol or starts a new line in the entry dialog. In the reader, vertical tab remains invisible within its record. Carriage return separates physical records when the accepted text is reloaded, so text after it appears on a new entry line.
Acceptance appends 9,, the entered text and CRLF to LOGBOOK.TXT in binary
append mode. It uses the shared logbook writer and its notification behavior,
bypassing the script tag-deduplication table. The writer’s current style is then
reset to 7. The reader reloads the file and selects its maximum scroll offset,
showing the newest text. Cancellation does not invoke the writer.
This operation adds a new record. It does not select, replace or delete an existing entry.
The dialog restores its captured reader background on each redraw and draws
SYSTEM/MINIMENU.GXA at (175, 140) in the canonical 640-by-480 viewport.
Its backing rectangle uses RGB (10, 10, 10) and alpha 125, including the
right and bottom image boundaries. The Log Entry title is centered at
(320, 170) with REDSEL and the normal font style from SYSTEM.INI.
The edited text starts at (200, 200) with REDSEL and the selected font style.
Both use the shared font shadow and colour rules.
In the dialog, an entered carriage return has no visible glyph and advances
the text pen by one pixel without starting a new line.
An underscore cursor is appended to the display copy while its phase is on. The phase starts off. Each newly sampled BIOS tick toggles it only if that tick is divisible by six; missed ticks do not cause extra toggles. The display copy, including any cursor, replaces spaces with newlines by scanning from byte offset 13, excluding the final byte. After a replacement, scanning resumes fourteen bytes later. The stored edit buffer is unchanged. The glyph painter also wraps before a glyph whose width would extend past x = 450, returning to the text’s starting x one font line lower. This pixel wrap applies to unbroken runs as well as the prepared display lines.
Logbook reading layout and navigation
The logbook reads physical lines from its stored text. CR and LF are line separators; after each record, consecutive separators are skipped. A leading character followed by a comma selects the entry style and is excluded from the entry text.
Text wrapping searches for a space starting at byte offset 65. It replaces the first matching space with a line separator, then resumes searching 66 bytes after that replacement. The final byte is never replaced. Long words remain intact. Every entry occupies 12 pixels per wrapped line plus an 8-pixel gap. The first line starts at y = 70 minus the scroll offset.
The reading area is 340 pixels high. The maximum scroll offset is the sum of entry heights minus 340, clamped to zero. Opening the screen initializes the offset to this maximum, showing the newest text.
Home selects offset zero; End selects the maximum. Ordinary up/down scrolling moves 10 pixels per screen update. Page Up and Page Down request the corresponding direction and select a shared 50-pixel step for that update. Home is processed before End, then upward scrolling before downward scrolling. Each direction clamps independently to the available range. Exit is tested after these changes.
Logbook reading screen
The installed log key opens the reading screen and clears the opening press.
The screen uses SYSTEM/LOGPAPER.GXA, tiled vertically with its first copy at
-(scroll_offset % 480) and the next at that position plus 480. Text starts at
x 30, y 70 minus the scroll offset. Negative line origins are skipped; remaining
glyph pixels clip to the viewport. Text retains its case.
The reader suspends world updates. It saves the normal simulation timing state before entry and restores it with a fresh BIOS-clock baseline on return. Its rendering loop does not advance the simulation clock. Existing audio playback is not stopped by the reader.
Home selects the top, End the bottom. Installed up/down bindings scroll ten pixels per BIOS-timer update. Either Page key or the installed A binding selects the shared fifty-pixel step. Page Up also supplies up; Page Down supplies down. The installed log key, Escape or installed D binding closes the reader. Closing waits for those controls to be released before gameplay resumes.
The reader selects the paper file’s own palette (the LOGPAPER.GXA palette, installed
as the file stores it) for both the paper and the scrolls, so the scroll textures take
their colours from that palette and not from REDGUARD.COL. Apart from the colour-key
entries it overrides, that palette is REDGUARD.COL with each channel truncated to six
bits (4 * floor(channel / 4)).
MENU.ROB supplies two instances of SCROLL.3D. Their positions are
(-324,-212,362) and (-324,200,362) with initial rotations (0,512,0).
The rotations apply the second axis, then the first, then the third. Each admitted
up operation subtracts 20 from the third angle, or 100 for the large step; down
adds the corresponding amount. Angles wrap modulo 2048. Home and End do not
rotate the models. The screen uses the menu viewport projection and a zero camera.
Logbook scroll lighting
The reader lights its two SCROLL.3D instances with a light state of its own,
so the loaded world’s ambient, sun and lights have no effect on them. On entry the
reader sets the ambient RGB to (64, 64, 64) and the directional light to white,
(255, 255, 255), along the vector whose components are 16000 times the sine, cosine
and sine of the table angles 256, 256 and 512, that is (11313, 11313, 16000),
normalised. Every drawn frame also registers two white local lights of strength
(255, 255, 255) and radius 6400 at (-390, 230, 600) and (390, 230, 600), in the zero-camera
space the scrolls are placed in. The lights apply to the scrolls only; the paper
is a flat picture.
The scroll model’s face brightness byte is zero and every corner takes its vertex
normal through the model’s indirection table (an authored radial normal in the XY
plane). Corner lighting follows the model lighting
and local-light rules, with the
instance rotation applied to positions and normals. The textured faces then use
Glide’s texture-times-iterated-colour combination, so a channel T lit to level
L becomes floor(T * (L + 1) / 256). The lit level reaches 255, which leaves the
texture unchanged, over most of the lower scroll and darkens the upper scroll to
about half its brightness where its surface turns away from both light positions.
The single byte before a comma is converted to an integer font-table index;
missing prefixes use index 7. The reader follows the font-table entry’s small-font link directly. Index 7 therefore uses FONTNORS.FNT with main alpha
200; index 9 uses FONTSELS.FNT with main alpha 255. Text shadows are disabled here.
Glyph colors come from the font palette’s 6-bit components. Index 9 text shifts them left by two bits. Index 7 text shifts them left by one bit, so its glyphs are half as bright before the alpha blend with the paper.
Reader style table
The style index selects an entry of the engine’s font table. Each entry is a font
with a value pair A,B; B is the main alpha. Entries 0, 3, 4 and 6 are copies of
FONTS\REDSEL.FNT, entry 7 is a copy of FONTS\FONTNORM.FNT and entry 8 a second
copy of it. Entry 9 is FONTS\FONTSEL.FNT. Entries 1 and 2 use the font named by
SYSTEM.INI [system] gui_low_font.
| Index | Font | A | B |
|---|---|---|---|
| 0 | REDSEL | [3dfx] font_sel A | font_sel B |
| 1, 2 | gui_low_font; glyph indices select page-palette colours | 255 | 255 |
| 3 | REDSEL | font_norm A | font_norm B |
| 4 | REDSEL | font_used A | font_used B |
| 6 | REDSEL | 255 | 180 |
| 7 | FONTNORM; small font FONTNORS with font_norm A,B | font_norm A | 140 |
| 8 | FONTNORM; no separate small font | font_used A | 50 |
| 9 | FONTSEL; small font FONTSELS with font_sel A,B | font_sel A | font_sel B |
Entries without a separate small font draw with the entry itself in the reader, so
styles 0, 3, 4, 6 and 8 use the large fonts and styles 1 and 2 the gui_low_font. Style 7 and 9 text
uses the small-font link, whose alpha the reader sets to 200 and 255. Glyph colours are the font
palette colours multiplied by A / 255, except for styles 1 and 2. Index 5 has no initialised entry;
indices above 9 are not described.
Reader glyphs use the shared glyph alpha rule: main alpha values below 254 blend with the paper, while 254 and 255 draw without blending. The palette colour and the style’s colour factor are applied before this decision.
The reader draws text glyph by glyph. A glyph whose right edge would pass x = 640 moves the pen to the entry’s starting x and down by the font’s line height first, so long lines of the large styles wrap inside words. A line feed does the same. Drawing stops when the pen reaches y = 480. Wrapping by 65 bytes above applies before drawing and is independent of this pixel wrap.
Toggle notices
Tab (scan code 15) toggles walk mode and F7 (scan code 65) toggles
auto-defend; neither key is configurable in KEYS.INI. Each writes SYSTEM.INI
[CYRUS] WALK_MODE or [CYRUS] AUTO_DEFEND and shows an RTX string for 24
ticks, centred on x = 320: walk mode (#WON / #WOF) with its top at
y = 10, auto-defend (#DON / #DOF) at y = 30.
Draw order
After the 3D scene, later entries over earlier ones:
- Compass
- Candle, then its health number
- Logbook icon
- Walk-mode and auto-defend notices
- Power-up slot 1, slot 2
- Activation prompt, or item icon then count (or item name)
- Two additional bitmaps
- Subtitles, the dialogue topic menu and later layers
Torches
LightTorch begins lighting and UnlightTorch begins dousing; both do nothing
without the item system, an animation state with the transition’s group, or outside
the ordinary execution context. Lighting also requires that the actor is not in
water (surface 1 through 4), is not in motion state 27 through 32, 15 or 10 through
19, is not already lighting or dousing, has no burning torch, holds no weapon and is
not drawing or sheathing one, and has no falling or queued motion; dousing refuses
only in motion state 27 through 32, while lighting or dousing, and while drawing or
sheathing. Lighting requests group 149 and sets motion state 33; dousing requests
group 150 and sets motion state 34.
While the held-item byte is set, each update finishes the transition. A held item
in deep water (surface 2 or 4) or with a weapon held is dropped. Motion state 33
requests group 149 and, once that group is current and its frame-progress byte
reaches attribute byte 103, sets the burning counter from the saved counter (or,
when that is zero, from the start value), clears the motion state and leaves the
held item set. Motion state 34 requests group 150 and, at attribute byte 104,
saves the burning counter, clears it and the motion state and the held item, and
drops one unit of the held item when the counter had reached zero. The start value
is torch_time from ITEM.INI times one and a half, rounded to even, at least
torch_time one and at most 255.
Every eighth world-loop iteration, an actor whose counter is nonzero and that is not dousing is updated: in deep water the counter is cleared; in shallow water the torch is doused; otherwise the counter loses the nominal ticks (not below zero) and dousing begins when it reaches zero. When a global torch flag is set, the selected player’s counter is set to 100 instead. Using any item while a torch burns puts the torch out; using the torch item lights it and holds it.
Book menu
The menu renders textured models from fxart/MENU.ROB and paints configured text directly into their TEXBSI surfaces.
Texture filtering combines the encoded eight-bit interface-palette channels,
without an sRGB-to-linear conversion before interpolation. The
Glide texture filter applies to the painted surfaces as
well as the unpainted book and table textures.
Data and scene
MENU.INI supplies ordered labels, texel positions, alignment, selectable/default/grayed flags, texture references, and action IDs. Labels do not identify actions. import::menu_ini::MenuPage::parse exposes a requested page also exposes widget output positions/alignment and inclusive slider bounds.
The model scene uses these placements in original engine coordinates, with positive Y down. Each model turns half a revolution around Y. The default view faces positive Z at the origin.
| Model | X | Y | Z |
|---|---|---|---|
| MENUA001 | 384 | 28 | 396 |
| MB_PG01 | 384 | 28 | 390 |
| MB_TABLE | 150 | -72 | 550 |
| MB_PG02 | 384 | 28 | 392 |
| MB_PG03 | 384 | 28 | 394 |
All four book/page models have nine uncompressed frames. The settled main menu uses frame zero. Entry context 1 moves the cover from its last authored frame to zero, with a one-update turn delay; the page models remain at zero. Its camera begins at (408, 0, 0), moves to (172, 204, -152) with a pitch change of -116 angle units, holds that position, then moves to the origin with a +116 pitch change. All three requests use fractional step 0.15. A revolution is 2048 angle units. Other main-page entry contexts, including death context 3, keep frame zero and move the camera from (-4, -100, 240) to the origin with fractional step 0.15. Contexts 5 through 9 instead enter Save, Load, Display, Sound and Controls respectively through their page handlers.
The Glide viewport has horizontal half-field tangent 0.788. At 640 by 480, screen coordinates for camera-space (x, y, z) are (320 + 320*x/(0.788*z), 240 + 320*y/(0.788*z)). Texture UVs follow the model format’s cumulative 1/16-texel coordinates. REDGUARD.COL is the initial palette; game/save context can replace the image on the main page with a gameplay snapshot. That snapshot is runtime content, not static book artwork.
Book surfaces are single-sided. With positive screen Y down, visible triangles have negative signed area; the opposite winding is culled. Each main, Movies, Options, and Controls page surface has sixteen projected triangles. Turning the page reverses their winding and exposes its other surface, rather than a mirrored copy of the previous page.
The software Voodoo renderer stores book fragments in an RGB565 framebuffer with the configured four-by-four ordered dither, then expands the stored channel bits for presentation. Texture filtering precedes this framebuffer conversion.
Text
At width 640 the menu uses FONTS/FONTNORM.FNT for normal and grayed text, and FONTS/FONTSEL.FNT for the current selection. At other widths it can select the smaller FONTNORS.FNT and FONTSELS.FNT glyphs. hi_res_menu affects that font-switch path. The HINORMAL/HISELECT files are not these book-menu fonts.
Each enabled glyph advances by its width plus one texel. Space also adds the width of the next glyph. Center/right alignment subtracts half/all of that accumulated width before painting; glyph left/top offsets still apply. Zero pixels are transparent. In the normal font, the normal text blitter adds five to each nonzero palette index. For each nonzero glyph pixel, grayed text adds four to the existing destination page index, preserving the paper shading. Selected text copies the selected-font indices. Nonselectable text uses the low four bits plus 144. These styles come from the menu font brightness/blitter settings, not the FNT palette alone.
Selection and actions
Entries form a circular ordered list. Up selects the previous eligible entry, Down selects the next. Eligibility requires selectable, not grayed, and a nonempty label. The first configured default establishes selection. Disabling the selected entry moves it to the next entry. Startup and in-game contexts apply additional Save/Continue restrictions; a menu with no resumable game cannot activate Continue. Enter or the configured primary action activates the selection. The engine’s held-direction repeat delay comes from SYSTEM.INI [dialog] menu_traverse_delay, in BIOS timer ticks.
Action -2 begins a closing-book/camera sequence and exits the game. It does not ask for confirmation. Action -1 resumes; positive IDs dispatch the configured submenu or game function. Those functions must be implemented separately from rendering their labels.
Action 10 starts New Game. Before loading world zero at marker zero, the engine
calls LockoutPlayer(0), clearing the retained lockout and applying its
actor and HUD cleanup. The returned New Game transition resets game state, plays movie zero
(the Introduction), then loads world zero at marker zero. Movie playback
finishes or follows its configured skip points before map loading begins.
The New Game transition and the first entry after engine startup run the same
session initializer, so a second New Game rebuilds the state a first launch
builds. In order, it re-reads the script flag definitions from SOUP386.DEF,
opens the item system from ITEMS (the player’s starting item list and
selection), deletes the per-world saved map files (MAPS\*.TSG), reads the
menu and movie settings from MENU.INI, deletes LOGBOOK.TXT, and then resets
a fixed set of engine globals. Ten of those scalars are also cleared by every
player placement (the routine that sets the player’s position and heading when a
world is entered), so New Game and world entry share that part of the reset. A plain
world replacement does not run the initializer.
The player’s health is retained outside the world. Every health change stores the selected player’s health attribute (attribute 3) in a retained value, world entry copies that value into attribute 3 of both player records, and the session initializer sets it to 100. A plain world replacement therefore keeps the damage the player has taken, and New Game restores full health.
The New Game transition also tears the running session down before the
initializer: it stops and releases every entry of the active-sound list
(stopping the entry’s channel when it is playing), stops all 16 sound channels,
applies LockoutPlayer(0), and replaces the actor message queues with 256 empty
queues of eight 12-byte entries.
Opening the book in any entry context, including the in-game pause book, the function keys and the death book, begins with the same sound teardown: it stops and releases every entry of the active-sound list and stops all 16 sound channels. Sound effects and speech therefore do not continue under the open book; the engine stops them and does not pause them for resumption.
WonGame ends the world loop with a won result (see
session requests).
The engine then runs the ending instead of showing a menu page: sound effects
stop, the CD pauses, movie 4 plays under the movie rules (WIN.SMK
in the installed MENU.INI, with its configured skip point), and the credits
scroll plays (see Credits scroll). The credits stop the CD when they end or when Escape is pressed.
When CREDITS.TXT cannot be loaded the credits are skipped and the paused
world track resumes. F10 in a world plays the same credits scroll without the
movie.
The credits play CD track 2 at the redbook_volume setting. After them the engine opens the
book menu in its ending mode, which starts no CD audio (only the ordinary in-game book plays
track 2), so the page that follows the credits is silent.
Credits scroll
The credits come from CREDITS.TXT in the game directory. The engine reads the whole
file in text mode (carriage returns are dropped and a Ctrl-Z ends the data) and splits it at line feeds into at most 2000 records. A line is
[-]x,align,style,text:
- A leading
-marks the line as continuing the previous one: it is drawn at the same height and adds no line advance. xis the decimal horizontal position (shipped: 320).alignis the first letter after the second comma:Lorlleft,Corccentered onx,Rorrright-aligned tox; other letters leave the record left-aligned.styleis a decimal font-table index with the reader style table, except that style 5 uses the table’s entry 9 (FONTSEL). Shipped styles are 0, 3 and 5.textis the rest of the line, drawn with the same text routine as the reader.- A line with no fields keeps the previous record’s style and draws nothing; it still occupies a line.
The scroll uses the 640 × 480 menu resolution, switching to it first when the world resolution differs. It first sets the glyph shadow to (2, 2). Text starts below the bottom edge and moves up one pixel per displayed frame. Each frame first waits a 2 ms calibrated busy delay (the startup calibration counts DOS time calls over one clock second), then redraws and swaps buffers; there is no other timer. Graphics initialization sets the buffer swap interval to one, so each swap waits for the next vertical retrace of the display: the scroll moves one pixel per display refresh. The Glide window is opened with the 60 Hz refresh setting, so a display that honours it scrolls 60 pixels per second. A line advances by the line height of the engine’s menu font record. Every frame redraws the visible records, then covers the first line height at the top and the last line height at the bottom of the screen with black. The scroll ends when the first visible record index passes the record count. Escape ends it earlier and waits for its release. During this wait, the last displayed frame stays fixed and credits music cleanup has not yet run. The CD plays track 2 for the duration (see music).
The menu inherits the current engine lighting and environment state. The default book menu has no pointer hover or click selection. Pointer coordinates do not determine the selected entry. Mouse buttons and motion have input codes in KEYS.INI, but the shipped menu bindings use keyboard and joystick logical actions. Controls key capture accepts the left and right mouse button codes on release.
The main page disables both Save and Continue while the selected player’s death override is active. This restriction uses the retained death override, not a direct health comparison. The installed main-page entries are addressed by ordered index: Save is entry zero and Continue is entry five. Disabling an entry sets its grayed flag and moves selection away from it when necessary.
The startup entry context (1) also disables Save. Entry context 2 disables
Continue. These context checks run independently of the selected-player death
restriction.
Escape is processed after the key is released. On the main page, the selected player’s death override also prevents Escape from resuming gameplay. This restriction applies to leaving the main page; Escape can still return from a submenu to its parent while the death override is active.
Gameplay book shortcuts
With SYSTEM.INI [debug] final_version enabled, gameplay samples fixed book
shortcuts in this order:
| Key | Destination page |
|---|---|
| F2 | Save, page 1 |
| F3 | Load, page 2 |
| F4 | Display, page 5 |
| F5 | Sound, page 6 |
| F6 | Controls, page 7 |
Escape takes priority and requests the main page. Otherwise, the shortcut sampler chooses the first held key in the table and clears that key’s state. It returns no request when none is held. The retained request is consumed near the start of the following gameplay update. Escape enters the ordinary in-game book context; the five shortcuts enter their destination pages directly.
Save and load pages
Existing saves are ordered by the filesystem modification time of SAVEGAME.SAV, newest first. Equal timestamps retain ascending save-directory number order.
Main action 1 opens Save on page 1. Its camera moves directly to (336, 0, 0) with fractional step 0.15, without turning a page. Action 100 returns directly to the origin. Main action 2 opens Load on page 2 using the three-part camera sweep to the origin and a forward MB_PG01 turn delayed by four updates. Action 200 reverses that turn and repeats the sweep to the origin.
Save displays <NEW SAVE GAME> as its first selectable row on the first list page, followed by eight existing saves. Subsequent list pages contain nine existing saves. Load displays nine existing saves per list page. Rows are addressed by their ordered entry index and list-page offset, not by action alone: existing Save rows share action 102, and Load rows share action 202. Missing saves produce empty, disabled rows. The <NEW SAVE GAME> row and the existing-save rows on both pages draw with the small font pair (FONTNORS.FNT normal, FONTSELS.FNT selected) even at width 640; the page titles and every other entry keep the normal pair. Slot descriptions are copied into menu labels with a limit of forty characters.
Action 101 asks for Game Description before creating a save. Action 102 opens the same input dialog with the existing description before overwriting that save. The input accepts at most 64 characters, requires nonempty text to confirm, and permits Escape to cancel. New manual saves use the first free directory number from 2 through 299. The Save list excludes the slot numbered zero; Load may include it. Delete on an existing save opens a DELETE GAME? confirmation that includes its description.
Newly typed ASCII lowercase letters become uppercase; prefilled text retains its case. Without Shift, Backspace deletes the preceding character and Enter confirms nonempty text. With Shift, Backspace and Enter append character codes 13 and 11 respectively, without deleting or confirming.
The description display wraps a copy of the edited text. It scans for spaces starting at zero-based character 13, excluding the final character. Each encountered space becomes a newline, and scanning resumes fourteen characters after that replacement. This preparation uses character positions without measuring font width; the edited value retains its spaces.
The delete confirmation presents Yes first and No second, with Yes initially selected. Enter confirms the current selection before simultaneous directional selection is applied.
Mini-menu confirmations load SYSTEM/MINIMENU.GXA and capture the current
background. Each redraw restores that capture, draws the backing panel, then
the image and text. At the canonical 640-by-480 viewport, the image starts at
(175, 140). The backing panel uses RGB (10, 10, 10) and alpha 120, blended
with source alpha and one minus source alpha. Its right and bottom coordinates
include the image width and height respectively; rectangle rasterization
includes those boundary pixels.
Confirmation text uses FONTS/REDSEL.FNT. The selected copy uses [3dfx] font_sel; the normal copy uses font_norm. The prompt uses the selected copy.
Both use a (2, 2) glyph-shadow offset and the shared font colour/alpha rules.
The last-game prompt is centered at (320, 160), with subsequent prompt lines
one font line height apart. The first option is centered four line heights
below y = 160, and the second five line heights below it, independently of the
number of prompt lines. The options use the selected or normal copy according
to the current choice. Text centering truncates the half-width toward zero.
The last-game prompt starts with Load Last Game? and a newline, followed by
a formatted copy of the retained save description. Formatting starts at
zero-based character offset 13. A space becomes a newline, then scanning resumes
14 characters after that replacement. The final character is never replaced.
Long runs without spaces remain intact; this is character-offset formatting,
not font-width wrapping.
The last-game confirmation presents Yes first and No second, initially
selecting Yes. The configured KEY_UP action selects Yes; KEY_DOWN
selects No. If both directions are active, Up takes priority. Enter or the
configured KEY_D action confirms the current selection before a simultaneous
direction can change it. The dialog waits until both confirmation inputs are
released before returning. Escape has no separate cancellation path.
If the retained book entry context is three, the confirmation’s first display
waits 72 BIOS timer ticks, then drains held KEY_D and Enter input before
accepting a choice. Later redraws in that dialog do not repeat this delay.
Other retained contexts skip this entry delay.
Centered dialog text starts at x + trunc((available_width - text_width) / 2), where trunc rounds toward zero. A zero available width centers the measured text around x.
The reserved quick-save slot is zero, with description Quick Save Game.
The reserved automatic-save slot is one, with description Redguard AutoSave.
When Quit closes the menu outside the startup context, the menu prepares this
automatic save. With the selected-player death override active, preparation
removes the reserved slot’s SAVEGAME.SAV instead of writing it. Otherwise it
calls the save writer only when the retained gameplay-thumbnail buffer exists.
This path runs before the menu returns to its caller. Startup Quit skips it.
Quick Save reads scan code 67, F9, directly rather than an action binding. It
clears that key state when latching a request near the end of the world loop.
With [debug] final_version enabled, the following loop consumes the pending
request before ordinary world processing, calls Quick Save, then clears the
request. Menu requests and the Quick Save request are latched independently, so
both can be pending together. The next loop handles the menu request first. If
the menu returns without ending the world loop, Quick Save is consumed afterward.
Before preparing a slot, the save writer requires at least 10 MiB of available
disk space. Below that threshold it displays the failure message and returns
without preparing the slot or writing SAVEGAME.SAV. Equality passes the check.
Retained last-save selection
The retained last-save selection starts absent. Admitting the startup autosave
selects slot one and copies its Redguard AutoSave description. Admission requires
final-version mode, ignore_autosave equal to zero, an existing file and an accepted
save header. Rejecting admission leaves the previous last-save selection and its
description unchanged.
After the disk-space check passes, the save wrapper prepares the destination path, removes the previous save file, then retains the requested slot and a copy of its description before calling the save writer. The selection does not depend on the writer’s return value. The insufficient-space path leaves the previous selection unchanged.
Selecting an existing Load row retains its slot and a copy of its description, then publishes the pending load selection before the menu’s exit presentation. The selected list index is the row’s ordered index minus one plus the current list-page offset. A missing list record leaves both retained and pending selections unchanged. The retained selection supplies the death completion prompt; consuming a pending load clears that pending selection independently.
The main page and the second surface of each save/load page receive runtime gameplay snapshots. The image begins at texel row 33, spans the 256-texel surface width and is 192 rows high. Save’s new-save row shows the current gameplay snapshot; existing rows show their stored snapshot. Load previews the selected save, and selecting it also replaces the main-page snapshot before leaving the menu.
Options and settings pages
Main action 4 opens page 4. Actions 405, 406, and 407 open Display, Sound, and Controls on pages 5, 6, and 7. Actions 500, 600, and 700 return to Options; 400 returns to Main. Escape returns from a settings page to Options and from Options or Movies to Main. Each page retains its selection while the menu stays open.
The Glide executable omits actions 501, 502, and 503 when constructing Display entries. These are Screen Size, Resolution, and Gamma. Subtitles, action 504, establishes the Display selection. Pages 4, 5, and 6 each define only texture slot zero.
Options turns MB_PG01 and MB_PG02 to their last frames. MB_PG01 moves four units farther along Z when passing its middle frame. The settled camera is at the origin. Display uses camera (336, -128, 0) and Sound uses (336, 176, 0), retaining those page frames. Controls additionally turns MB_PG03 to its last frame, moving it four units nearer along Z; the camera returns to the origin. Returning reverses those changes. Camera pitch changes during the transition cancel at the settled endpoint.
Left and Right subtract or add one within the installed slider bounds without wrapping. Enter or the primary activation key toggles actions 504, 505, and 603. These preferences map to SYSTEM.INI [dialog] dialog_print_text, [screen] smk_interlace, and [dialog] dialog_use_speech. Sound and Music actions 601 and 602 initialize from [system] volume and redbook_volume, divided by 12.8 and truncated toward zero. Updates multiply the menu value by 12.8 and truncate to the engine volume. The shipped configuration gives Sound a range from zero to twenty and these engine volumes:
| Slider | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Volume | 0 | 12 | 25 | 38 | 51 | 64 | 76 | 89 | 102 | 115 | 128 | 140 | 153 | 166 | 179 | 192 | 204 | 217 | 230 | 243 | 256 |
The shipped volume=255 opens at 19, not 20, and redbook_volume=200 at 15. Initializing these displayed slider values does not update the engine volumes. The reverse conversion applies when a setting is updated. Because both directions truncate, most values reopen one notch lower (slider 1 writes 12, which reads back as 0). The Sound volume becomes the master volume of positional sound; the Music value is described in music.
Checkboxes use MM_CHECK.GXA frames indexed by the toggle value. Sound sliders use MM_SLIDE.GXA, selecting the rounded product of the value and (frame_count - 1) / (slider_max - slider_min), clamped at the endpoints. Widgets paint at output coordinates; their labels retain normal text without a numeric suffix.
Controls actions 710 through 717 map to up, down, left, right, A, B, C, D. Actions 718 through 724 map to inventory, sword, health, map, log, next, previous. Output names come from KEYS.INI [defined] key[code]. While capturing, the book alternates <prompt> and >prompt< in the selection font. Its prompt buffer initializes to WAITING and uses KEYS.INI [misc] waiting_message when configured; the shipped configuration displays HIT KEY. Key names and the configured prompt are uppercased and limited to fifteen characters before display. Escape cancels capture. Tab, Enter, F1 through F10, F11 and F12 are rejected during capture; Minus and Equal are also rejected by binding validation. Rebinding a used code clears the first conflicting binding. Zero displays the installed name for an empty binding. A binding name is drawn glyph by glyph from the value column at x=120 of the 256-pixel page surface and stops at the first glyph whose pen position is 247 or more, so NUM MINUS, LEFT MOUSE and RIGHT MOUSE display as NUM MINU, LEFT MOUS and RIGHT MOU. A glyph that starts at pen 246 is drawn whole, and narrower names such as fifteen i characters fit entirely. Mouse/joystick input shares the original code table but requires separate device handling.
Movies action 3 turns MB_PG01 to its last frame with the same four-unit depth change, and uses camera (336, 0, 0). Page 3 action 300 returns to Main; actions 301 through 305 select movie definitions zero through four. MM_MOVIE.GXA supplies the corresponding preview images. Runtime unlock flags gate movie entries. MENU.INI [general] force_movies=1 initializes every movie as unlocked. Playing a cinematic sets its unlock flag. The startup game path plays movie zero, so Introduction is visible at the first settled menu. MM_MOVIE.GXA preview data begins at row 33 of the second Movies page texture. The preview copies every indexed pixel into the page, including zero; its black pixels are opaque, unlike transparent widget pixels.
Configuration lookup matches section/key names without case and returns the first matching occurrence. Settings outside the requested section do not override that section.
Frame and camera transitions
Page turns use the authored frame arrays, not an independent hinge rotation. A turn request supplies an initial frame, a target frame, and an update delay. The delay counts down first; at zero it selects the initial frame. Later menu updates move one integer frame toward the target. There is no geometric interpolation between those authored frames in this path. Forward turns and the Quit cover turn target the model’s frame count, one past its last authored frame. That final forward step adds an update but does not change the displayed pose. A later reverse turn starts from that index, so its frame stepping takes one update more than the number of authored frames minus one. Placing a page directly on a submenu, without a turn, sets the last authored frame instead.
MB_PG01 changes Z by +4 when a forward turn reaches frame four, and by -4 when a reverse turn reaches frame four. MB_PG03 applies the opposite depth changes. These shifts happen at the middle-frame crossing, not at the final frame. Returning to Main reverses the appropriate page turns. Cover motion and delayed page-turn starts dispatch the effects listed below.
The startup camera move to the origin uses a fractional step of 0.15.
Camera requests form an ordered queue. Each request specifies a target position, angle changes, and a fractional step. Position interpolates linearly from the previous settled position; angles interpolate from their previous values by the supplied changes. The first rendered sample uses the request’s fractional step, rather than zero. Each update samples the current fraction, then adds that same step. If the incremented value is greater than one, that update replaces the sample with the settled target and advances to the next request. Camera coordinates and angle samples truncate toward zero to engine integers before rendering.
The submenu requests use these camera paths. Each row’s targets execute in order. Angle values are changes in the first camera angle, in 2048 units per revolution; the other two angles do not change.
| Navigation | Ordered camera targets | Angle changes | Fractional steps |
|---|---|---|---|
| Main to Movies | (192, 284, -96), (192, 284, -96), (336, 0, 0) | -152, 0, +152 | 0.15, 0.12, 0.15 |
| Main to Options, Movies or Options to Main, Options to Controls, Controls to Options | (192, 284, -96), (192, 284, -96), (0, 0, 0) | -152, 0, +152 | 0.15, 0.12, 0.15 |
| Options to Display | (336, -128, 0) | 0 | 0.15 |
| Options to Sound | (336, 176, 0) | 0 | 0.15 |
| Display or Sound to Options | (0, 0, 0) | 0 | 0.15 |
| Quit | (232, 284, -96), (232, 284, -96), (408, 0, 0) | -152, 0, +152 | 0.15, 0.12, 0.15 |
The repeated target holds the position while the request fraction advances. Main to Movies and its return delay the first page turn by four menu updates. Main to Options delays MB_PG01 by three and MB_PG02 by five; its return delays them by five and three respectively. Options to Controls and back delay MB_PG03 by four. Quit delays the cover turn by two. Outside the startup context, Quit then fades the closed book to black before the menu returns. Each fade update with a nonzero whole nominal-tick increment redraws the book, clamping each page frame to its last authored frame, and raises a black overlay by 12 times that increment, out of 256, until the overlay is opaque. Startup-context Quit returns without this fade. Normal menu direction, activation, and Escape handling wait until the camera queue and page turns finish, including the update that settles their final targets.
For these paths, only the first camera angle changes. Subtract the camera position from a world point to obtain (x, y, z), then apply the view rotation before viewport projection:
angle = (first_angle & 2047) * 6.2831852 / 2048
view_x = x
view_y = y * cos(angle) - z * sin(angle)
view_z = y * sin(angle) + z * cos(angle)
The sine and cosine samples use the shared rotation table, with cosine starting one quarter revolution after sine. The coordinates keep positive Y down. A negative first angle rotates positive-depth points downward on the screen.
Sound cues
Book motion dispatches these zero-based MAIN.SFX effect indices:
| Model | Event | Effect |
|---|---|---|
| MENUA001 | Forward turn reaches frame 7 | 65 |
| MENUA001 | Reverse turn reaches frame 0 | 64 |
| MB_PG01 | Nonzero turn delay counts down to zero and selects the initial frame | 66 |
| MB_PG02 | Nonzero turn delay counts down to zero and selects the initial frame | 67 |
| MB_PG03 | Nonzero turn delay counts down to zero and selects the initial frame | 68 |
Page cues occur in either direction, once at delayed start, before later frame stepping. A turn without a nonzero delay does not dispatch that page cue. MB_TABLE has no turn cue. The short main-page camera entry, including death context 3, leaves the cover at frame zero and does not dispatch a cover cue. Context 1 requests the delayed cover turn. Display and Sound camera moves do not turn a page.
These are FlatSound requests: centered pan 64, zero volume offset, zero rate offset, and no owner or positional attenuation. Playback uses the effect header’s base volume and sample rate with the saved master volume.
Successful directional selection or widget adjustment plays effect 13. Accepted control rebinding also plays effect 13; rejected capture plays effect 18.
Cinematic playback
Movies use the installed MENU.INI definitions and Smacker files, not recorded playback.
import::menu_movies::parse_menu_movies reads ordered names, skip points, and subtitle records from [page3]. A subtitle has seven comma-separated fields: an always-display flag, red, green, blue, start frame, stop frame, and text. Text can contain commas. The shipped configuration defines eleven filenames and five browsable cinematics. Names are uppercased and the original filename buffer holds fifteen characters. Original skip tables hold up to twenty frame numbers. Zero terminates a skip table; a single zero defines no skip points. Values after the terminator or twentieth entry are ignored.
Playback numbers frames from one. A new skip input advances to the next configured forward point; after the last point it finishes the movie. Passed skip points are consumed during normal playback. Held input must be released before another skip. Returning from playback restores the menu and retains its selection. Playback marks the cinematic unlocked.
Subtitles begin at the start frame and end before the stop frame. Their always-display flag overrides the subtitle preference. The original wraps text at 35 characters and centers the lines at X 320, ending at Y 470, using the font’s line height (26 for REDSEL). Glide playback draws the source’s half-height frame over the 640 by 480 viewport. Interlace overlays black odd scanlines. Subtitles use their own copy of FONTS\REDSEL.FNT with the value pair 255,180, not a SYSTEM.INI font: the main copy is drawn with alpha 180 and the shadow copy with alpha 140, offset (2, 2) rather than the (1, 1) of dialogue subtitles. Glyphs use the selected eight-bit texture-upload palette. At startup this is the unmodified REDGUARD.COL palette, including its low two channel bits; the font’s six-bit BPAL does not replace that upload palette. The resulting text is gold. Advances, the space rule and centering follow subtitles. The configured per-record colors are not used.
SYSTEM.INI [system] animation_drive prefixes movie filenames. The GOG configuration uses D:, mapped to the supplied game.gog CD image. Its data track is MODE1/2352 and the eleven configured movies reside in the ISO 9660 root directory, not an ANIMS subdirectory. import::cdrom::CdImage reads root files without sector headers; it also accepts ordinary 2048-byte ISO images. No install data is modified.
Playback clock and drawing
Movie playback advances its own decoded-frame ordinal; gameplay clock progress is not required while a movie is playing. Each iteration updates the palette, decodes the current frame, advances the decoder to the next frame, draws and presents the decoded image, then waits for the movie clock before continuing. At presentation, the decoder’s advanced ordinal equals the one-based frame just drawn.
The wait uses the selected movie audio stream’s playback position when a sound track is active. Without an active audio track, it uses a timer deadline and the frame period from the Smacker header. Movie presentation and gameplay updates therefore have separate clock owners.
The large-image Glide path splits source pixels into tiles of at most 256 by 256. Tile origins advance by 256 source pixels. Each destination rectangle extends one pixel beyond its calculated right and bottom edges; movie destination Y coordinates double the source height. Adjacent tiles overlap at their edges. Unused rows and columns of the power-of-two texture backing are filled with palette index zero. Sampling can wrap into this padding; it does not clamp to the populated source rectangle. A single whole-image resize does not reproduce those sampling coordinates.
Glyph drawing temporarily selects its upload palette and restores the movie palette afterwards. A palette sampled only after the complete presentation therefore need not be the palette used for subtitles.
Glide movie raster
At the 640 by 480 viewport, a 640 by 240 movie uses three 256 by 256 indexed texture backings, populated with source widths 256, 256 and 128. The source tile origins are 0, 256 and 512. Index zero fills unused backing texels. The destination rectangles have nominal widths one greater than the source width and nominal height 481. Rectangle clipping adjusts the source endpoint proportionally before triangle setup. The final tile is clipped to the viewport right edge, and every tile is clipped to the viewport bottom.
Triangle setup normalizes the destination edge deltas by the triangle area
before multiplying by source-coordinate deltas. Each operation is stored at
single precision. For an axis-aligned clipped rectangle of destination extent
W,H and source endpoint S,T, let inverse_area = 1/(W*H). Its texture
gradients are S * (H * inverse_area) and T * (W * inverse_area).
Reassociating these products into a single source/destination division can
change texture filter thresholds. The starting source coordinate is anchored
to the submitted vertex, including when it is the rectangle’s right edge.
The Voodoo texture raster samples at destination pixel centres and subtracts half a source texel before bilinear filtering. Its first-generation filter retains four fractional bits and wraps within the complete power-of-two backing. Both horizontal interpolation results are floored before the vertical interpolation is floored. The framebuffer stores RGB565 with the 4 by 4 ordered dither matrix, then presentation expands the stored bits. These operations precede the final RGB capture format.
Subtitle glyphs use point sampling and index zero transparency. For constant
alpha a, the source term is floor(source*(a+1)/256) and the destination
term is floor(destination*(256-a)/256). The blend unit reads the stored
RGB565 destination with zero low bits, applies framebuffer dither subtraction,
and adds the independently floored terms before the next dithered RGB565
store. Glyph shadow/main submission order and the selected upload palette
remain significant when glyphs overlap.
Unlock flags and queued playback
The movie table holds fifteen records, each with an unlock flag (bit 0). The session initializer, which runs at launch and again at New Game, clears every flag and sets it to MENU.INI [general] force_movies (bit 0). Playing a movie by index sets that record’s flag before playback and sets the stored queued-movie value to -1 afterwards. Save games store the fifteen flag bytes and loading restores them, so unlocks follow the loaded save rather than the process.
On the Movies page, an entry whose action is 302 through 315 (movie 1 through 14) is hidden and unselectable while its flag is clear. Entry 301 (movie 0, the introduction) is always shown.
The stored queued-movie value is played by the main loop after a world loop ends and before the next world is saved to the temporary game and loaded: a value other than -1 plays that movie like any other, with the flag and queue effects above. Launch and New Game also play movie 0 at this point of the loop before the first world loads, and the ending plays movie 4 through the book (see menu). QueueMovie stores or plays an index as described there.
Startup presentation
Glide startup presents the active Glide implementation’s 3dfx splash and
system/STARTUP2.GXA. The splash is supplied by Glide, rather than by a
Redguard movie file.
In final-version mode, a valid SAVEGAME.SAV opens the book-style main menu.
MENU.INI [general] ignore_autosave bypasses autosave handling and enters the
starting world without Introduction. With that flag off, an absent or invalid
autosave follows the fresh-game path: startup plays the Introduction cinematic
before loading the introductory game world. Non-final-version startup also
enters the starting world without Introduction. New Game from the book repeats this
movie-before-world sequence. A book opened from a running world uses the in-game
entry context described in Book menu.
The title image is a single 640×480 frame. The original also draws small version and loading labels along its bottom edge.
Title labels
The title redraw selects the [system] gui_low_font entry from SYSTEM.INI.
The GOG configuration points to fonts\\arialvb.fnt, a 9-pixel line-height
font. The version label starts at (10,467) and requests VGA colour
(32,32,32). Its format is
Version %s.%s, filled from the executable’s
version stamp; the GOG release displays Version 1.0.1.0
and the demo’s RGFX.EXE displays Version 1.0.5.0. Loading
text is right-aligned to (630,467) and requests VGA colour (50,0,0).
The map-registration
stage supplies Loading Map %d. Sound preparation supplies Loading Sound.
The colour requests are six-bit palette searches, not direct eight-bit RGB.
The search excludes index zero and returns the first exact match. Approximate
matching excludes white. Per-channel difference is request - candidate when
nonnegative and its bitwise complement otherwise. Candidates with all three
differences at most eight compete by absolute weighted-brightness difference,
using integer weights 19661, 38666 and 7209 for red, green and blue. A weighted
difference at most 24 selects that result. Otherwise the search minimizes the
sum of the three channel differences. Approximate ties select the last index.
The low font replaces nonzero glyph pixels with palette index 161. The glyph texture retains those indices and uses the active palette when drawn. Its RGB multiplies the selected text tint component by component, divided by 255. Thus both the selected tint and entry 161 of the title palette determine the visible label colour; the font file’s own entry one does not supply it.
Glyph bitmaps use their FNT left and top offsets. Each active character advances by its bitmap width plus one pixel. Space additionally advances by the width of the next glyph, the exclamation mark. Text-width measurement uses the same advances. These rules follow the title caller, text-width routine, and glyph-drawing path, independently of the font-file fields.
Version stamp
The version and build numbers are not compiled into the engine. At startup
the engine opens its own executable and reads a 28-byte XVER trailer from
the end of the file. All fields are little-endian.
| Offset (from EOF) | Size | Type | Name | Description |
|---|---|---|---|---|
| -28 | 4 | [u8; 4] | magic | XVER |
| -24 | 4 | u32 | version_major | First number of the game version |
| -20 | 4 | u32 | version_minor | Second number of the game version |
| -16 | 4 | u32 | build_major | First number of the build |
| -12 | 4 | u32 | build_minor | Second number of the build |
| -8 | 4 | u32 | field_18 | Zero in the shipped executables |
| -4 | 4 | u32 | field_1c | Zero in the shipped executables |
Each pair is formatted as %d.%d. The game version and the build fill the
two %s fields of the title label, and the build is the Build value shown
in the F1 version information. Without the trailer, both pairs stay 1.0.
| Executable | Version | Build | Title label |
|---|---|---|---|
CD and GOG RGFX.EXE | 1.0 | 1.0 | Version 1.0.1.0 |
Demo RGFX.EXE | 1.0 | 5.0 | Version 1.0.5.0 |
The executable also carries change notes for its build, shown with the version information:
- Build 1.0: “Fixed camera reset bug, (see leaving the catacombs).”
- Build 5.0: “Fixed rope death-reattach bug.”, “Fixed texture binding crash bug on certain object flats.” and “Fixed run of edge fall ‘stutter’ bug.”
See Demo release for the other differences of the build 5.0 executable.
Splash variants
The full DOS glide2x_emu.ovl contains the older “3Dfx Interactive” logo on a
white shield with blue and yellow borders. DOSBox-X Voodoo emulation runs this
hardware-facing overlay and displays that animation.
The small DOS forwarding glide2x.ovl routes Glide calls to a host Windows
wrapper. A wrapper using 3DfxSpl2.dll displays the later metallic lowercase
3dfx logo with an orange swoosh. The DLL’s 1999 branding postdates Redguard’s
1998 release. The GOG install includes both paths, so the active Glide
implementation determines the splash. OpenGLide’s NoSplash and nGlide’s
splash setting can also suppress driver startup presentation.
The GOG release’s Windows launch runs its bundled DOSBox with Glide
passthrough ([glide] glide=true) to the bundled OpenGLide wrapper, whose
glide.ini sets NoSplash=1. That DOSBox build has its own [glide] splash
option, which defaults to on and which the GOG configurations do not set.
When the game first opens a Glide window successfully, DOSBox loads
3DfxSpl2.dll from the DOSBox directory and plays its animation once. GOG
Windows players therefore see the metallic splash. Emulated Voodoo hardware,
such as DOSBox-X with glide=emu, runs the full overlay and shows the shield
animation instead.
The DLL resource contains authored geometry, textures and commands for the later animation. The older animation remains available in the DOS overlay tables. These assets are separate from the Smacker New Game introduction.
Windows splash rendering
The DLL command stream supplies the camera, horizontal FOV, model transforms,
material selection, and positional light. Multiply each model transform by
the camera transform. Negate the resulting position and normal Z components,
then normalize the normal. Projection uses the same scale on both axes,
W / (2*tan(horizontal_fov/2)), and centers the viewport. The Glide origin
is lower-left.
Geometry without flag bit 3 uses its authored UV pairs multiplied by 256.
Geometry with that flag intersects the positive normal ray from the transformed
vertex with a sphere of radius 400 centered at the view origin. The far
intersection supplies S/T as (intersection.xy/400 + 1) * 204.8.
Texture coordinates are perspective-correct. Textures use bilinear filtering.
For a normalized light direction L and normal N, diffuse is max(dot(N,L),0).
The reflection direction is normalize(2*N*dot(N,L)-L). Specular is the eighth
power of its nonnegative dot product with the normalized view direction.
The view direction is the camera matrix’s third column with negated Z.
One lighting contribution uses direction (1,1,-1)/sqrt(3); the other uses
the authored positional light minus the transformed vertex. Beyond a squared
light distance of 4,000,000, the positional contribution is multiplied by
(4,000,000/distance_squared)^4. Vertex RGB, in byte units, is:
RGB = clamp(255*positional_RGB*(positional_diffuse + positional_specular)
+ 255*(directional_diffuse + directional_specular) + 16, 0, 255)
During full playback, texture RGB multiplies interpolated vertex RGB. Constant material RGB is used by the single-frame drawing path, which bypasses textures. That diagnostic path therefore differs from full animation playback. Triangles cull by winding. Polygon edges are drawn through Glide’s antialiased line path after pending edge groups flush.
The timed-frame command overlays black before presenting. Its alpha is 1 for
frame counters below 24, then 1-(frame-24)/8 for counters 24 through 31.
Frames from counter 32 onward have no overlay. After presenting, both color and
depth are cleared for the next frame. The final untimed redraws precede a fade
of 2*frame_rate swaps, each waiting two refresh intervals. Fade alpha starts
at zero, then uses the truncated byte value 102*step/(2*frame_rate).
The black overlay accumulates separately on the two alternating buffers.
DOS overlay splash
The Glide splash tables contain the geometry, textures, and authored transforms. No executable code needs to run to extract or display the animation.
The full animation renders authored frames 1 through 74. Frame zero is not part of that playback. Each frame presents with a buffer swap that waits two vertical retraces. Redguard opens Glide at 60 Hz, so the animation advances at 30 frames per second. Each mesh’s position uses its column-major affine matrix; its normal uses the linear part without translation or normalization.
With a viewport of width W and height H, a transformed point (x,y,z) maps to:
screen_x = (x/z) * 0.75W + W/2
screen_y = (y/z) * H + H/2
The splash uses a lower-left origin. Texture coordinates are perspective correct, with 256 S units across the longest texture dimension. A 2:1 texture uses 128 T units. The renderer enables bilinear filtering and selects the nearest mip level.
Vertex lighting uses L = (-0.57735, -0.57735, -0.57735) and
brightness = (dot(L, transformed_normal) + 1) / 2.
| Material | Appearance |
|---|---|
| 0 | Logo color texture multiplied by vertex brightness; additive highlight pass |
| 1 | Highlight texture sampled using the transformed normal; vertex brightness does not modulate the sampled color |
| 2 | Vertex brightness multiplied by RGB (10,75,120)/255 |
| 3 | Vertex brightness, equal RGB channels |
| 4 | Vertex brightness multiplied by RGB (248,204,0)/255 |
Highlight S/T coordinates are (128 + 128*normal.x, 128 + 128*normal.y).
Material 1’s intensity-factor table contains 0.125 in each channel, but the
texture-only color combiner bypasses that factor during full animation.
The outer shield and shield render first without depth writes. A projected shadow multiplies their existing colors, with fog disabled for that pass. The logo then renders with depth testing and writes enabled; its material-0 highlight adds to the existing logo color. Triangles use winding culling and face-selected edge antialiasing.
The fade is black fog on geometry. With 75 frames, fade-in spans the first 22
frames: before frame 22, visibility is the truncated 8-bit value of
255*(frame+1)/22, divided by 255. After frame 60, visibility is the truncated
8-bit value of 255*(75-frame)/15, divided by 255. Other frames have full
visibility. The background remains black.
Projected shadow
The shadow quad’s object-space corners are (-280,0,-160), (-280,0,150),
(280,0,150), and (280,0,-160), with S/T pairs (10.5,127.5), (10.5,0.5),
(255,0.5), and (255,127.5). Transform these corners with the logo matrix.
The light is (5,300,shield_translation_z - 1500); project the light-to-corner
lines onto z = shield_translation_z - 26. Screen projection is the same as
for geometry. Two triangles cover the quad, submitted in both windings.
S and T are stored multiplied by the projected point’s reciprocal Z. The per-TMU reciprocal-W field additionally contains the light-to-projected-point distance divided by Z. The splash sets no STW hint, so texturing ignores that field: S and T interpolate perspective-correctly with the projected point’s reciprocal Z, and the shadow texture spans the projected quad.
External references
World loading screen
World entry uses the destination’s WORLD.INI world_flash_filename GXA.
An archive with several frames supplies a randomly selected background; a
single-frame archive uses its only frame. The full-screen picture uses its
own palette. New-game entry uses start_world and start_marker from the
[world] section.
Completing the New Game Introduction, including leaving through its configured forward skip points, enters the starting marker with heading 1024 in the 2048-unit turn. The direct world-entry path that bypasses Introduction uses heading zero. The marker supplies position, not orientation.
At the 640 by 480 logical viewport, the progress bar’s track covers columns 32
through 608 and rows 448 through 454 (577 by 7 pixels, inclusive edges). The
track darkens the picture behind it to about a third of its brightness rather
than painting opaque black; the fill requests VGA RGB (63,0,0) but is drawn with the nearest palette colour (the system palette block’s entry 147, (54,5,4), on screens whose picture palette lacks an exact match; captures show it dithered around (218,21,14) in 8-bit) and reaches the
column 32 + width for a nearest-integer fill width of 576 times the completed
fraction (a full bar ends at column 608). The displayed integer percentage also
rounds to nearest. Percentage updates advance monotonically; a zero-percent
update does not draw the bar or label.
The percentage label uses FONTS/REDSEL.FNT at (32,451), with foreground
alpha 180 and shadow alpha 140 (fitted to captures: the text is translucent over the
picture, not opaque). The ordinary label is %d%%; debug output
can include world indices before it. These are world-entry loading visuals,
separate from the startup title’s small bottom-edge labels. World loading
does not display a right-aligned status message or an asset filename.
New-game entry fades the loading screen in from black, fades it out to black after loading, and then fades the first world view in from black. Each fade step changes a black overlay by 12 out of 256.
Exit Quotes
On a normal quit, the Glide executable (RGFX.EXE) prints a random line from
The Italian Job (1969) to the DOS console after its version banner.
Output
RGFX.EXE shuts down its subsystems and then writes to the console:
RedGuard version <version> [build <build>]
Copyright (c)1996-98 Bethesda Softworks, All Rights Reserved.
"<quote>"
An ordinary quit exits with status zero and prints the quote. A positive exit status prints its error report in place of the quote. Exit status −99 suppresses both the banner and the quote.
The quote is enclosed in double quotes and word-wrapped at spaces to fit the 80-column console.
Selection
The engine holds 100 quotes in a null-terminated pointer table. Selection
combines the C library rand() with the engine’s frame counter:
offset = frame_counter & 63
repeat
index = rand() + offset
until index < 100
Because offset is fixed for one exit, the result is uniform over entries
offset through 99. Entries 64–99 can appear on every exit; entry 0 appears
only when offset is zero.
Quote Table
The text below matches the strings in the executable, including their spelling and punctuation. Speaker names and stage directions are in square brackets.
| # | Quote |
|---|---|
| 0 | On days like these, when skies are blue…. |
| 1 | And then I hear sweet music float around my head… |
| 2 | As I recall the things we said left unsaid… |
| 3 | It’s on days like this, that I remember… |
| 4 | Say ’ello to the big world for me Charlie…. [Charlie] Bye, Bye… |
| 5 | Did you hear that ? He said goodbye Mr Bridger. Well he’s going Mr Bridger, Well as long as he’s going, that’s all I care. |
| 6 | [Charlie] Well, you’re the last person I expected to see Laurna |
| 7 | [Shop assistant] You know Charles, you could put all these in a museum |
| 8 | [Charlie] …And do us a favor Adrian, shorten the sleeves love, I’ll not a gorilla |
| 9 | [Charlie] …Eeerrr, I wonder if you’d hold this for me ? [Garage attendant] Oh yes, of course sir. |
| 10 | [Garage attendant] Yes,… I gather you’ve been in India for about two years sir. [Charlie] Yes, that’s right… shooting tigers |
| 11 | [Garage attendant] Yes,… [counting money] …you must have shot an awful lot of tigers sir. [Charlie] Yes. I used a machine gun |
| 12 | [Mr Bridger] You’ve been put up to this haven’t you ? You’ve been bribed to upset my natural rhythm and rrrrruin my health |
| 13 | [Mr Bridger] Croaker, this is MY toilet |
| 14 | [Mr Bridger] Last night Mr Governor, my toilet was broken into. [Governor] Toilet ? [Mr Bridger] Toilet |
| 15 | [Launa] And what would you like. [Charlie] Well, everything ! |
| 16 | [Henchman] Right then Fred. [Fred] Wait a minute… [Girls screaming]. [Fred] Charlie’s been caught on the job ! |
| 17 | [Charlie] oh, don’t come the ol’ moody with me Launa, you know ’ow the game’s played |
| 18 | *Bang* *Bang* *Bang* [Launa] It’s the law charlie. [Charlie] What’d you tell them. [Launa] Charlie, would I tell them anything ? |
| 19 | [Camp Freddie] But Croaker. [Mr Bridger] Croaker can handle it. He’s got everything going for him. |
| 20 | [Miss Peach] Oh yes he was doing it. In the lounge |
| 21 | [Miss Peach] Oh… Oh, something quite obscene with Annette |
| 22 | [Miss Peach] Annette. She was terrified of course. [Charlie] Naturally |
| 23 | [Professor Peach] Of course, I wouldn’t want to get Matron into trouble |
| 24 | [Professor Peach] She’s big… BIG ! |
| 25 | [Charlie] We are about to do a job in….err…Italy |
| 26 | [Charlie] First, Bill Bailey, he’ll be my number two |
| 27 | [Dominic] Hello, Chaps. [Everyone grumbles] [Charlie] All right, all right ! |
| 28 | [Charlie] These chinless wonders will get you out of Turin faster than anyone on four wheels |
| 29 | [Charlie] Better known as big William for very obvious reasons |
| 30 | [Charlie] I know he’s got some… err…very funny habits, but make ’im feel at ’ome |
| 31 | [Charlie] Now, it’s a very difficult job… and the only way we’ll only get through it, is if we all work together as a team…and that means you do everything I say |
| 32 | [Mr Bridger] Does he really need all this equipment ? [Camp Freddie] He say’s he does. |
| 33 | [Charlie] No, you’re meant to use your brakes Chris. [Chris] terrib….terribly sorry Charles |
| 34 | [Mr Bridger] ..Err…what’d y’mean, they’re written off ? [Camp Freddie] A series of accidents Mr Bridger |
| 35 | [Charlie] You’re only supposed to blow the bloody doors off ? |
| 36 | [Camp Freddie] It’s the old under and over routine…. |
| 37 | [Charlie] Piatza Arthur, Piatza. [Arthur] Sorry Charlie. |
| 38 | [Charlie] That’s it….wallop |
| 39 | [Camp Freddie] I’ve only one thing to say about that Mr Bridger… good luck. |
| 40 | [Mr Bridger] Keats, I think we’d better arrange a funeral. |
| 41 | [Mr Bridger] They’re the lot that smashed up my cars aren’t they. [Charlie] Practice makes perfect Mr Bridger. [Mr Bridger] You pick ’em, don’t you. |
| 42 | [Mr Bridger] …I also want you to remember, that if you don’t come back with the goods, Nellie here will turn in her grave, and likely as not jump right out of it and kick yer teeth in. |
| 43 | [Charlie] Dave. Take the valley road to Turin. Ok. Go. |
| 44 | [Charlie] Big William, take the bus along the main road. |
| 45 | [Professor Peach] Pity people aren’t as lovely as flowers isn’t it ? |
| 46 | [Caterpillar starts up]….[Sound of E-Type Jag being crunched] |
| 47 | [Charlie] You just lost him his insurance bonus ! [Alto Bene] It cost Beckerman his life. |
| 48 | [Alto Bene] ..Just how d’you think you’re going to do it… oh excuse me. [Sound of E-Type Jag crunching] |
| 49 | [Alto Bene] Your car Mr Croaker ? [Charlie] Yes. [Alto Bene laughing] Pretty car. |
| 50 | [Charlie] Very funny. [Alto Bene] Tut tut tut. |
| 51 | [Alto Bene] Well gentlemen. It’s a long walk back to England, and it’s that way |
| 52 | [Alto Bene] I wouldn’t be too sure about the English Cousin. [Francisco Cosca] They wouldn’t dare ! [Alto Bene] They’re not so stupid as they look. |
| 53 | [Arthur nearly snips Professor Peaches nose off] |
| 54 | [Sound of Billiards falling over] [Alto Bene] Gentlemen, we must assume they are here. |
| 55 | [Charlie] Now, as you walk out to the plane just look straight ahead, look neither to the right, nor to the left. |
| 56 | [Launa] Charlie, listen the plan was that we stay here. [Charlie] Well plans change. |
| 57 | [Charlie] launa, you see that lot out there ? If we slip up, they’ll tear us apart. |
| 58 | [Launa] Charlie. [Charlie] What. [Launa] you care ? [Charlie] Get on the plane. |
| 59 | [Charlie] I’ll see you in Geneva. ’Ave a cup of tea ready. |
| 60 | [Charlie] Before we leave, bill. [Bill] Yes Charlie. [Charlie] I want every one of those tins burned. |
| 61 | [Bill] We’re all wearing our gloves Charlie. [Charlie] yeah, but you can’t be too careful. |
| 62 | [Charlie] Right. Here is the crunch. [Points to map] |
| 63 | [Chris] Actually, we can all read maps Charles. [Charlie] The map’ll be in your head Chris, I’m burning this one. |
| 64 | [Chris] Shall we synchronize our watches ? [Charlie] Nuts to your watches. |
| 65 | [Charlie] Oh…one more thing, just remember this. In this country they drive on the wrong side of the road. |
| 66 | [Charlie] Bill. [Bill] Yes Charlie. [Charlie] Bill ? [Bill] Yes Charlie. [Charlie] Burn this. [Bill] Yes Charlie. [Charlie] Oh Bill [Bill] yes Charlie. [Charlie] Get rid of this lot. [Bill] Yes Charlie. |
| 67 | [Charlie] You are not gonna get sick, you are not gonna get a migraine, and everyone is gonna sit in the back of the motor. [Arthur] Charlie, me in the back of the motor with my asma. |
| 68 | [Charlie] One more word out of you Arthur. All right ! |
| 69 | [Arthur] ..it was all I had to do… [Charlie] Arthur ! [Arthur] .. all the time….what ? |
| 70 | [Old man] Gratzi. [Charlie] Multibene, dad. |
| 71 | [Fat Englishman] Can you direct me to Calda Garibaldi Street ? [Italian speaks in really fast Italian] [fat Englishman] Bloody foreigners. |
| 72 | [Alto Bene] What’s happening ? [Chauffeur] It’s another traffic jam, it gets worse every time. |
| 73 | [Charlie] What are they gabbling about Franco. [Franco] Complaining about the traffic jam. [Arthur] Yeah, well they’ll ’ave something else to complain about in a minute. |
| 74 | [Charlie bangs the side of the truck] Ok Bill ! |
| 75 | [Everyone creeps back] [Charlie] Whad’y think you lot are doing, don’t just panic. Get on with it. Get in the car. Anybody comes through… hit ’em ! |
| 76 | [Charlie] OI ! Get in the Doormobile. |
| 77 | [Bill] Come on Arthur, get your finger out will you. |
| 78 | [Bill] Well look ’appy then you stupid bastards, we won didn’t we ? |
| 79 | [Bill mumbles] They went that-away. |
| 80 | This is the self-preservation society… |
| 81 | [Arthur] I could eat a horse.. ha..ah-ah |
| 82 | [Charlie shouts from the car window] GOOD LUCK ! |
| 83 | [Chris] Try putting your foot down Tony, they’re really getting rather close. |
| 84 | [Charlie] Now as you go round look for that bloody exit, we can’t go round here all night. |
| 85 | [Chris] I think you might make an effort to keep up with the others Tony. [Tony] All right ! |
| 86 | [Arthur holds out the wish-bone] Ah… make a wish. |
| 87 | [Charlie] Look out they’re behind us, you’d better put your foot down, put your foot down we’ll lose ’em easy. |
| 88 | [Keats] Have you heard the news sir, they’ve done it Mr Bridger, all the men are cheering for you. They’ve done it ! |
| 89 | [Mr Bridger] Done what Keats ? [Keats] The job sir ! |
| 90 | Put on your dicky-dirt and your Peckham rye… |
| 91 | [Charlie] Get yer..get yer wheels in line… slam on yer brakes or we’ll be in the cabin. |
| 92 | [Dominic] Ok, hold on. [Wheel crunches] [Wheel crunches again] [Wheel crunches yet again] [Chris] Be very careful ! |
| 93 | [Big William] Charlie boy. [Charlie] NOW ! [Car rolls over the cliff] |
| 94 | [Charlie] Ok, out…OUT. Leave the beer. [Arthur] No ! |
| 95 | [Charlie] Hold still…hold still…hold still…hold still. Nobody move. |
| 96 | [Charlie] We’re balancing right on the edge. Very slowly move this way. Very slowly ! Don’t make any sudden movements. |
| 97 | [Charlie] The gold is pulling it over the edge. We’ll ’ave to get it back. |
| 98 | [Charlie creeps forward] [So does everyone else] [Charlie] Get back ! Get back ! Now hold still. Don’t move. Don’t move at all. Don’t no-one get out the door neither. |
| 99 | [Charlie] Hang on a minute lads. I’ve got a great idea….errr…errr… |
External References
File formats
Binary layouts for game assets, scripts, saved state, and bundled support resources. Integers are little-endian unless a reference states otherwise. For runtime rules, see engine behavior. Asset locations and renderer versions are listed in Asset Directories.
The project README describes CLI conversion commands and the library API.
Models and worlds
| Format | Extension | Contents |
|---|---|---|
| Model | .3d | Static 3D models. |
| Animated Model | .3dc | Animated 3D models (multi-frame). |
| ROB Archive | .rob | Model archive used by worlds and dungeons. |
| Map Data | .rgm | Scene sections for objects, scripts, locations, and collision data. |
| World Geometry | .wld | World geometry and height maps stored in four sections on a 128×128 grid. |
| Visibility Octree | .pvo | Pre-computed visibility octree for level geometry culling. |
Images, textures, and fonts
| Format | Extension | Contents |
|---|---|---|
| TEXBSI | .### | Texture container (TEXBSI.###); indexed-color images with optional palettes and animation. |
| Software textures | .### | Indexed software-renderer texture banks. |
| Software palette tables | .### | Shade and haze palette-index lookup rows. |
| Palette | .col | 256-color palette files; 776 bytes (8-byte header + 256×RGB). |
| Font | .fnt | Bitmap glyphs with a 56-byte header and optional palette. |
| GXA Bitmap | .gxa | Indexed-color bitmap archive used for UI, loading flashes, and sky panoramas. |
| PCX Image | .pcx | Eight-bit indexed PCX image with a trailing palette. |
| IFF Bitmap | .lbm | Packed PBM bitmap with ByteRun1 scanlines. |
| Deluxe Paint Animation | .anm | LPF pages containing indexed bitmap deltas. |
| Standalone BSI | .bsi | Standalone image chunks for named textures such as skies and suns. |
Audio and cinematics
| Format | Extension | Contents |
|---|---|---|
| Sound Effects | .sfx | All game sound effects in a single container (MAIN.SFX); 118 raw PCM clips. |
| Dialogue Audio | .rtx | Dialogue text + voice clip container (ENGLISH.RTX) with chunk index footer (RNAV); 4866 entries (3933 voice clips, 933 text-only). |
| Smacker video | .smk | Compressed indexed video, palettes, audio streams, and frame metadata. |
Scripts and saved state
| Format | Extension | Contents |
|---|---|---|
| SOUP386.DEF | .def | Definition-file format for SOUP callable functions, references/equates, attributes, and global flags. |
| Save Games | .sav, .tsg | Save metadata, state chunks, and per-map snapshots. |
| Cheat States | .cht | Cheat persistence file (REDGUARD.CHT); 256-byte raw dump of 64 u32 LE cheat state slots. |
| Console Preferences | .cpf | Saved developer-console settings. |
Startup graphics
- DOS Glide splash tables: shield geometry and animation tables in the game overlay.
- Windows Glide splash resource: the animated logo resource in
3DfxSpl2.dll.
Startup presentation describes how the splash assets are displayed.
Bundled support files
These resources describe DOS hardware interfaces, setup utilities, controller presets, and development tools. Bundled support resources explains their roles alongside the game assets.
| Resource | Description | Format reference |
|---|---|---|
.dig, .mdi | Executable Miles DOS digital audio and MIDI hardware drivers. | Miles drivers |
.pif | Windows launch settings for DOS audio utilities. | PIF |
.opl, .ad | Two-operator FM MIDI instrument banks. | OPL and AD |
.rst | Sectioned text editor settings and remembered file positions. | RST |
.swg, .swx | Microsoft SideWinder controller preferences. | SideWinder |
Extensionless more | Historical model filenames and version labels. | MORE |
3D Model File Format
Binary, little-endian static (single-frame) model format.
.3D files are static (single-frame) models. For the animated multi-frame variant, see 3DC.md.
Versions
Four versions exist: v2.6, v2.7, v4.0, v5.0. See GOG vs Original CD Differences for which directories contain which versions.
- v2.6 / v2.7 — Different header semantics and face data encoding from v4.0/v5.0.
- v5.0 — Adds Section4 (SubObject BVH).
Header (64 bytes)
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | version | ASCII version string, e.g. v4.0 |
| 0x04 | 4 | u32 | num_vertices | Vertex count |
| 0x08 | 4 | u32 | num_faces | Face count |
| 0x0C | 4 | u32 | radius | Bounding radius in 1/256 engine units |
| 0x10 | 4 | u32 | num_frames | Animation frame count. 1 for v4.0/v5.0 .3D files; 0 in the v2.7 static files. |
| 0x14 | 4 | u32 | offset_frame_data | Offset to frame data array |
| 0x18 | 4 | u32 | total_face_vertices | Sum of all face vertex counts. Also the entry count for the vertex-normal indirection table. |
| 0x1C | 4 | u32 | offset_section4 | Offset to Section4 (SubObject BVH). 0 when absent. Engine header copier zeroes this field at load time. |
| 0x20 | 4 | u32 | section4_count | Number of Section4 entries. |
| 0x24 | 4 | u32 | unused_24 | Unused; the engine bulk-copies the 64-byte header but never reads this field. Always 0 in v4.0/v5.0. Non-zero in 5 v2.6 files in /3dart (values 4490, 6434). Parsers should ignore it. |
| 0x28 | 4 | u32 | offset_normal_indices | Offset to vertex-normal indirection table. Each entry is a u32 file offset into the vertex-normal data section. Count = total_face_vertices. |
| 0x2C | 4 | u32 | offset_vertex_normals | Offset to normal data (f32 × 3 per normal record). |
| 0x30 | 4 | u32 | offset_vertex_coords | Offset to vertex coordinate data. |
| 0x34 | 4 | u32 | offset_face_normals | Offset to face normal data. |
| 0x38 | 4 | u32 | total_face_vertices_dup | Duplicate of field at 0x18 in v4.0/v5.0 (engine header copier copies this from file). Different value in v2.6/v2.7 but the Glide engine rejects those versions. |
| 0x3C | 4 | u32 | offset_face_data | Offset to face data. Always 64 (0x40) in v4.0/v5.0. Variable (112–3736) in v2.6/v2.7. |
v2.6/v2.7 Header Differences
The header is the same 64 bytes, but many fields have different semantics:
| Field | v4.0/v5.0 meaning | v2.6/v2.7 meaning |
|---|---|---|
| 0x18 | total_face_vertices (count) | Offset to vertex-normal data. Parsers should remap this into the 0x2C slot (vertex-normal offset) and zero this field. The engine also zeroes it during frame construction. Not a vertex count despite sharing the offset with v4.0’s total_face_vertices. |
| 0x1C | offset_section4 (0 in .3DC) | Often non-zero but zeroed by the engine header copier regardless of file value. Not Section4 (SubObject BVH does not exist in v2.6/v2.7). Discarded at load time. |
| 0x24 | Always 0 | Non-zero in 5 files (RICHA001=4490, GARDA001-004=6434) |
| 0x2C | offset_vertex_normals | Unrelated to vertex normals in v2.x files. Acts as a variant marker: 0 for v2.7 .3D, 1 for .3DC (both v2.6 and v2.7). Not an offset — treat as a flag. |
| 0x38 | Duplicate of 0x18 | Does NOT equal 0x18 in v2.7. Copied by the engine header copier. However, the Glide engine rejects v2.6/v2.7 files at load time (version byte < '4'), so this field is only used in practice for v4.0+ where it duplicates 0x18. |
| 0x3C | Always 64 | Variable (112–3736) — face data does NOT start after header |
v2.6/v2.7 Frame Data Differences
The 16-byte frame data record has different field semantics:
| Field | v4.0/v5.0 | v2.6/v2.7 |
|---|---|---|
| offset 0x00 (vertex_offset) | File offset to vertex data | Header + frame header size (always 88) — not a useful offset |
| offset 0x04 (normal_offset) | File offset to face normal data | Face data byte count. The total size in bytes of all face records. |
| offset 0x08 (reserved) | Always 0 | Gap byte count between face data end and vertex data (0 when no gap). |
| offset 0x0C (frame_type) | 0, 2, 4, or 8 | Values like 8209 (0x2011), 16402 (0x4012) — different encoding |
v2.6/v2.7 Engine Header Handling
At load time, the software renderer copies the 64-byte header and then zeroes the offset fields 0x18, 0x1C, 0x30, 0x34, 0x38, and 0x3C. It reads the file sequentially using frame_data[0].normal_offset as the face data byte count and frame_data[0].reserved as the gap between face data and vertex data, rebuilding the section layout in memory. The raw offset fields in the file header are not used for data access.
v2.6/v2.7 Face Data Differences
| Aspect | v4.0/v5.0 | v2.6/v2.7 |
|---|---|---|
| Face texture header | 5 bytes (u8 + u32) | 3 bytes (u8 + u16) |
| tex_hi values | 61–91, 255 | 0, 6, 15, 18, 20, 30, 36, 44, 50, 60, 63 |
| Solid-color faces | tex_hi = 0xFF | texture_id < 2 indicates solid color |
| Texture ID decoding | BCD-like: (raw >> 8) - 4000000 | Simple: texture_id = raw >> 7 |
| Vertex index | Direct index | Byte offset (divide by 12) |
Parsers must remap header fields before use. See External References for the remap logic.
Glide version gate: The Glide loader rejects model versions below v4.0.
This gate belongs to RGFX.EXE; it does not describe the software engine’s
v2.6/v2.7 asset path. External parsers support both model generations.
Section Layout
Sections appear in this order (offsets from header):
- Face Data — at 0x40,
num_facesvariable-size records - Vertex Coordinates —
num_vertices× 12 bytes - Face Normals —
num_faces× 12 bytes - Frame Data —
num_frames× 16-byte records - Section4 — SubObject BVH entries (when present)
- Vertex-Normal Indirection Table —
total_face_vertices×u32 - Vertex Normals —
num_vertices× 12 bytes
Frame Data
For v4.0/v5.0, located at offset_frame_data as an array of num_frames × 16-byte records. Software animations instead use the v2.6/v2.7 animation directory.
For v4.0/v5.0 .3D files this is a single entry pointing to the base geometry.
The v2.7 static files store a zero frame count; that value does not make the
static geometry empty.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | vertex_offset | File offset to vertex position data |
| 0x04 | 4 | u32 | normal_offset | File offset to face normal data |
| 0x08 | 4 | u32 | reserved | Always 0 in v4.0/v5.0. |
| 0x0C | 4 | u32 | frame_type | 0 for static .3D models in v4.0. See 3DC.md for animated frame types. |
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 (0x40 in v4.0/v5.0; variable in v2.6/v2.7). Array of num_faces variable-size records.
v4.0 / v5.0 Face Record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | vertex_count | Number of vertices in this face (3–10) |
| 0x01 | 1 | u8 | tex_hi | Per-face flags/high byte field (often 0xFF for solid-color faces) |
| 0x02 | 4 | u32 | texture_data_raw | Packed texture encoding value |
| 0x06 | 4 | u32 | unused_04 | Unused — always 0x00000000. Parsers should skip. |
| 0x0A | 8×N | FaceVertex[N] | vertices | N = vertex_count |
Texture Decoding (v4.0 / v5.0)
Solid color (if texture_data_raw >> 20 == 0x0FFF):
color_index = (texture_data_raw >> 8) & 0xFFtex_hiis always 0xFF for solid-color faces.
Textured (otherwise):
-
Texture file ID (BCD-like encoding):
tmp = (texture_data_raw >> 8) - 4000000 ones = (tmp / 250) % 40 tens = ((tmp - ones*250) / 1000) % 100 hundreds = (tmp - ones*250 - tens*1000) / 4000 texture_id = ones + tens + hundredsThis yields the TEXBSI.### file number.
-
Image sub-ID within that TEXBSI file:
ones = (texture_data_raw & 0xFF) % 10 tens = ((texture_data_raw & 0xFF) / 40) * 10 image_id = ones + tens
v2.6 / v2.7 Face Record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | vertex_count | Number of vertices |
| 0x01 | 1 | u8 | u1 | Separate byte field (not part of texture encoding) |
| 0x02 | 2 | u16 | texture_data | Packed texture encoding |
| 0x04 | 4 | u32 | unused_04 | Unused — always 0x00000000. Parsers should skip. |
| 0x08 | 8×N | FaceVertex[N] | vertices | N = vertex_count |
Texture decode from texture_data (the u16 at offset 0x02):
- Solid color if
(texture_data >> 7) < 2:color_index = texture_data & 0xFF - Textured:
texture_id = texture_data >> 7,image_id = texture_data & 0x7F
FaceVertex
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | vertex_index | Index into vertex coordinate array. For v2.6/v2.7: byte offset, divide by 12. For v4.0+: direct index. |
| 0x04 | 2 | i16 | u_delta | U texture coordinate delta |
| 0x06 | 2 | i16 | v_delta | V texture coordinate delta |
UV coordinates are cumulative deltas in 4-bit fixed-point format: each vertex’s absolute U/V = previous vertex’s U/V + this delta. First vertex in the face starts from 0. The accumulated coordinate does not wrap to 16 bits. The renderer converts each signed delta to pixel-space float before accumulation, then applies the texture descriptor scale. A signed 32-bit fixed-point sum represents every possible face exactly.
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.
Vertex Coordinates
Located at offset_vertex_coords. Array of num_vertices × 12 bytes.
v2.6/v2.7 animated .3DC files: Read positions from the software animation directory, not from a guessed offset after the faces. Compressed frames add shifted deltas to the directory’s reference frame. See software animation data. Static v2.6/v2.7 .3D files use offset_vertex_coords directly.
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). The indirection table determines the required normal records; its largest referenced index can exceed num_vertices - 1. Each record occupies 12 bytes. Without an indirection table, normals use direct vertex indexing and there are num_vertices records.
Each entry is 3 × f32 (IEEE 754 single-precision). Per-vertex normals (unit vectors). Some entries may be NaN (bit pattern 0xFFC00000 for all three components), indicating no normal was computed for that vertex.
Vertex-Normal Indirection Table
Located at offset_normal_indices (header offset 0x28). Array of total_face_vertices × u32.
Each entry is a file offset pointing into the vertex-normal data section. Maps each face-vertex to its vertex normal, allowing faces that share a vertex to use different normals (for hard edges).
Shading Model Selection
Per-vertex shading is determined by the indirection table and vertex normal validity:
| Condition | Shading | Normal used |
|---|---|---|
| Indirection table present, referenced vertex normal is valid | Smooth (Gouraud) | Vertex normal from indirection lookup |
Indirection table absent, vertex_normals[vertex_index] is valid | Smooth (Gouraud) | Vertex normal by direct index |
Vertex normal is NaN (0xFFC00000) or unavailable | Flat | Face normal |
For glTF export, vertex normals whose squared length is zero, subnormal or non-finite also fall back to the authored face normal. This prevents invalid normalization in renderers.
.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 followed by an ordered face-reference list. The fixed header is 18 bytes in v4.0 and 30 bytes in v5.0. The three extent floats exist only in v5.0; reading them in v4.0 consumes face references instead.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 12 | i32[3] | center | Model-local center in 1/256 engine units. |
| 0x0C | 4 | u32 | radius | Bounding radius in 1/256 engine units. |
| 0x10 | 2 | u16 | face_count | Number of references following this header. |
| 0x12 | 12 | f32[3] | extents | Present only in v5.0. |
Runtime centers and radii are floating-point values obtained by dividing the
stored integers by 256. The v5.0 extent factors remain unchanged. Runtime face
indices are the stored indices divided by four, in the same order. The five
nodes of ISLAND.ROB model HBBLD04 contain 26 ordered face references.
Each six-byte reference contains a u32 absolute file offset to the face record
and a u16 face index multiplied by four. Both identify the same face in the
model’s original face table. The next node begins immediately after the last
reference; there is no padding between nodes.
The installed ROB archives contain 3,820 embedded models with these lists: 260 v4.0 and 3,560 v5.0 models, with 22,931 nodes and 222,322 references. Every reference’s file offset agrees with its divided-by-four face index. Lists can repeat a face across nodes: 105 models have repeated references. They need not cover every rendered face: nine palm-tree models omit faces. Preserve the lists and their order rather than reconstructing them from all rendered faces.
External References
- UESP: Mod:Model Files — community-maintained model-format notes and version behavior.
- uesp/redguard-3dfiletest
Redguard3dFile.cpp— DaveHumphrey’s C++ parser, including v2.6/v2.7 header remap logic (ConvertHeader27). - RGUnity/redguard-unity
RG3DFile.cs— 3D model importer (face/texture decoding).
3DC Animated Model File Format
Animated variant of the 3D model format. The fixed header is shared with 3D models; animation directories depend on the renderer generation.
Differences from .3D
| Aspect | .3D | .3DC |
|---|---|---|
| Frames | 0 in software static files, 1 in Glide static files | Multiple animation frames, or a single static frame |
| frame_type (frame 0) | 0 | 2, 4, or 8 |
| Animated frame data | None | Frames 1+ with compressed or full-precision geometry |
| Section4 (SubObject BVH) | Present in v5.0 | Absent in Glide animations; software uses a different auxiliary layout |
| Vertex-normal indirection table | Present | Always absent (offset=0) |
| Version | v4.0 or v5.0 | v4.0 (fxart/), v2.6 or v2.7 (3dart/) |
All shared structures (header, face data, vertex coordinates, face normals, vertex normals, texture encoding) are documented in 3D.md.
Glide 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 encoded vertex data and face normals. Compressed vertices are deltas from frame zero, not absolute positions.
frame_type Values
| Value | Meaning | Frame 1+ vertex encoding | Frame 1+ normal encoding |
|---|---|---|---|
| 2 | Compressed animation | i16 × 3 (6 bytes/vertex) | 10-10-10-2 packed (4 bytes/face) |
| 4 | Full-precision animation | i32 × 3 (12 bytes/vertex) | 10-10-10-2 packed (4 bytes/face) |
| 8 | Static (single-frame .3DC) | N/A | N/A |
frame_type is only meaningful in frame 0’s record. Frame 1+ records always have frame_type = 0.
Compressed vertex reconstruction
The low four bits of frame zero’s type select the vertex encoding. For type 2,
each later signed i16 component is subtracted from the corresponding decoded
frame-zero component. When type flag 0x10 is clear, divide the signed delta by
256 before subtraction. When it is set, subtract the signed delta in whole model
units. Store the resulting absolute coordinate at single precision. Each frame
uses frame zero as its base; deltas are not accumulated between frames.
Type 1 uses signed byte deltas subtracted in whole model units. Type 4 stores absolute signed i32 components scaled by 1/256. Frame zero itself uses decoded base geometry regardless of the later-frame encoding.
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 are 0 and 3. Parsers should mask or discard these bits.
Section Layout (v4.0)
Same as 3D.md — Section Layout, but with animated frame data inserted and .3D-only sections absent:
- Face Data — at 0x40
- Vertex Coordinates — base frame positions
- Face Normals — base frame normals
- Frame Data —
num_frames× 16-byte records - Animated Frame Vertex/Normal Data — frames 1+ geometry
- Vertex Normals — per-vertex normals (f32 × 3)
Software animation directory
In v2.6/v2.7, offset_frame_data points to a 24-byte animation header. It is not the first frame record. Values are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | table_offset | Absolute file offset of the frame directory |
| 0x04 | 4 | u32 | face_data_size | Byte length of shared face records |
| 0x08 | 4 | u32 | unknown_08 | Auxiliary field |
| 0x0C | 2 | u16 | flags | Bit 0 selects compressed frame data |
| 0x0E | 2 | u16 | reference_frame | Frame with full i32 positions when compressed |
| 0x10 | 4 | u32 | unknown_10 | Auxiliary field |
| 0x14 | 2 | u16 | unknown_14 | Auxiliary field |
| 0x16 | 1 | u8 | unknown_16 | Auxiliary field |
| 0x17 | 1 | u8 | delta_shift | Shift applied to compressed position deltas |
The directory contains num_frames records. Compressed records occupy 12 bytes; uncompressed records occupy 16 bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | vertex_offset | Absolute file offset of frame positions or deltas |
| 0x04 | 4 | u32 | normal_offset | Absolute file offset of face normals |
| 0x08 | 4 | u32 | auxiliary_offset | Absolute file offset of per-face auxiliary data |
| 0x0C | 4 | u32 | unknown_0c | Present only in uncompressed records |
Uncompressed positions and face normals contain three signed i32 components, each scaled by 1/256.
In compressed animations, the reference frame contains full signed i32 positions. Other frames contain three signed i16 deltas per vertex. Reconstruct each raw component with wrapping i32 arithmetic:
position = reference_position + (signed_delta << (delta_shift & 31))
Divide the reconstructed raw component by 256. Every frame uses the same reference frame. Deltas are added, not subtracted or accumulated from the preceding frame. Frame zero may itself be compressed.
Every compressed frame’s face normals use signed 10-bit components in this order:
Bits 20-29: x
Bits 10-19: y
Bits 0- 9: z
Bits 30-31: discarded
Sign-extend each component and divide it by 256. This component order differs from Glide’s packed normals. Use decoded frame-zero positions and normals as the model’s base geometry.
External References
- UESP: Mod:Model Files — baseline notes for shared
.3D/.3DCstructures and version differences. - RGUnity/redguard-unity
RG3DFile.cs— 3D/3DC importer with animated-frame decoding.
ROB File Format
Binary container format. Holds multiple 3D model segments — either embedded inline or as references to external .3DC files.
ROB stores model geometry buckets, not per-instance scene placement transforms. World/object placement comes from scene files (RGM), which reference these models.
Overall Structure
[Header — 20 bytes]
[Segment 0 — 80-byte header + data]
[Segment 1 — 80-byte header + data]
...
[Segment N-1]
[Footer — 4 bytes: "END "]
Some fields use big-endian encoding (a remnant of the original Sega Saturn development), while most use little-endian. Endianness is noted per field.
Header (20 bytes)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | magic | "OARC"; exact expansion unknown. |
| 0x04 | 4 | u32 | BE | unused_04 | Always 4. Unused at runtime; the engine reads the file but never dereferences or tests this field. |
| 0x08 | 4 | u32 | LE | num_segments | Number of segments. |
| 0x0C | 4 | [u8; 4] | — | magic2 | "OARD"; exact expansion unknown. |
| 0x10 | 4 | u32 | BE | payload_size | File size minus 24 (= file size - 20-byte header - 4-byte footer). |
Segment Header (80 bytes)
Each segment has a fixed 80-byte header followed by data_size bytes of payload.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | u32 | LE | total_size | Data size + 80 (total segment size including header). |
| 0x04 | 8 | [u8; 8] | — | name | Segment name, ASCII null-padded. Model name or external .3DC filename stem. |
| 0x0C | 2 | u16 | LE | segment_type | 0 = embedded 3D data, 512 = external .3DC reference. See below. |
| 0x0E | 2 | u16 | LE | segment_flags | Render mode flags. Only the high byte (at file offset 0x0F) is used at runtime — it becomes a render mode selector. A value of 0 defaults to 0xFF (normal rendering). See Segment Flags below. |
| 0x10 | 1 | u8 | — | segment_attribs | Per-segment attribute flags. Only this single byte is read at runtime. Bit 1 (0x02) triggers texture pre-loading. Value 0x40 marks special objects (inventory items, shop objects). See Segment Attributes below. |
| 0x11 | 1 | u8 | — | collision_flags | Copied into static-object runtime collision flags. See Collision flags. |
| 0x12 | 1 | u8 | — | reserved_12 | Zero in all 4,761 installed segments. |
| 0x13 | 1 | u8 | — | face_count_low | Build tool artifact: face_count mod 256. |
| 0x14 | 4 | u32 | BE | unused_14 | Unused — never read at runtime. Most commonly 1 (3,690 segments), but other values exist. |
| 0x18 | 4 | u32 | — | reserved_18 | Always 0. |
| 0x1C | 4 | u32 | LE | bbox_extent_x | Bounding box total X extent. |
| 0x20 | 4 | u32 | LE | bbox_extent_y | Bounding box total Y extent. |
| 0x24 | 4 | u32 | LE | bbox_extent_z | Bounding box total Z extent. |
| 0x28 | 4 | u32 | — | reserved_28 | Always 0. |
| 0x2C | 4 | u32 | — | reserved_2C | Always 0. |
| 0x30 | 4 | u32 | — | reserved_30 | Always 0. |
| 0x34 | 4 | u32 | LE | bbox_positive_x | Positive X extent from center. |
| 0x38 | 4 | u32 | LE | bbox_positive_y | Positive Y extent from center. |
| 0x3C | 4 | u32 | LE | bbox_positive_z | Positive Z extent from center. |
| 0x40 | 4 | u32 | LE | bbox_negative_x | Negative X extent from center. |
| 0x44 | 4 | u32 | LE | bbox_negative_y | Negative Y extent from center. |
| 0x48 | 4 | u32 | LE | bbox_negative_z | Negative Z extent from center. |
| 0x4C | 4 | u32 | LE | data_size | Byte count of the data payload that follows. 0 for external references. |
Bounding Box Invariant
bbox_extent == bbox_positive + bbox_negative for all three axes.
For symmetric models: bbox_positive == bbox_negative (center at origin). For asymmetric models, the difference encodes the center offset.
Segment Types
| Type | Description |
|---|---|
| 0 | Embedded 3D model data. Payload is a complete 3D file (v5.0 format). |
| 256 | Embedded 3D model data (menu-specific). Only in MENU.ROB. Structurally identical to type 0 — payload is a complete 3D file. Uses version v4.0. |
| 512 | External reference. name is the .3DC filename stem (e.g. "CYRSA001" → CYRSA001.3DC). data_size is 0. |
Segment Flags (0x0E)
The engine reads only the high byte (file offset 0x0F) of this u16 field. The low byte is always 0x00 and is ignored. The high byte is stored as a render mode selector in the model’s internal data — a value of 0 defaults to 0xFF (standard rendering).
| Value | High byte | Names | Render mode |
|---|---|---|---|
| 0x0000 | 0x00 → 0xFF | (all normal) | Standard (default) |
| 0x8C00 | 0x8C | DR_WA01, DR_WA02, LH_MIRR | Transparency / mirror |
| 0xC800 | 0xC8 | WATERWAT | Water |
| 0x8000 | 0x80 | VR_OHT | Stored as render-mode metadata |
| 0x5A00 | 0x5A | BEAMA001 | Beam / light effect |
Segment Attributes (0x10)
A single byte of per-segment attribute flags.
| Value | Segments | Meaning |
|---|---|---|
| 0x00 | (all normal) | No special attributes |
| 0x02 | PALMTR01–04 (CAVERNS, EXTPALAC, ISLAND) | Texture pre-load trigger (bit 1). Engine calls a texture pre-caching function for all face textures in this segment. |
| 0x40 | SS_OBJ01–06, IGRING, IWATER1 (shop items, inventory) | Special object flag (bit 6). Stored as model metadata. |
In the Glide renderer, bit 0 (0x01) selects point minification and
magnification for queued model draws. When clear, both use bilinear filtering.
Archive loading copies the segment attribute byte into model metadata, and
ordinary model submission copies it into the instance draw flags. The queued
draw pass restores bilinear filtering when it finishes.
Bit 1 (0x02) also enables chroma-key rejection when binding a model face’s
texture. With this bit clear, that binding disables chroma keying. The key
comes from the texture’s stored key color. This is separate from the archive
loader’s texture pre-loading use of the same bit.
Runtime archive selection
Model lookup searches the active primary archive first, then the active world
archive. Within each archive it follows stored segment order and returns the
first matching name. It does not search every installed ROB archive.
Loading a world archive replaces the previous world archive registry. The
world loader takes the configured map filename and replaces its extension with
ROB in the configured art directory.
The ISLAND world uses FXART\ISLAND.ROB. Its ordinary in-world registry has no
primary archive. The names BARREL, BASKET02, LANTERN4, LANTHOL1, MKCARP1
and MKCRATE1 resolve to this world archive even though other installed archives
contain different payloads under the same names.
Collision flags (0x11)
Static object loading copies this byte from the named ROB segment into the
instance’s runtime collision flags. In collision candidate selection, bit
0x02 excludes the static instance. Otherwise, bit 0x01 routes it to the
separate actor-style candidate list; a clear bit selects the model-face list.
Across 4,761 segments in 31 installed ROB archives, the byte values are:
| Value | Count |
|---|---|
0x00 | 4,726 |
0x01 | 28 |
0x02 | 5 |
0x04 | 1 |
0x08 | 1 |
In ISLAND, PLANT000, PLANT001, and PLANT002 use 0x02;
BARREL, BASKET01, and BASKET02 use 0x01.
See player collision.
Segment Data
For segment_type == 0 (embedded): the data payload is a complete 3D model file starting with its own 64-byte header. Parse with the standard 3D parser.
For segment_type == 256 (menu-specific embedded): same as type 0 — payload is a complete 3D model file. Parse identically. See MENU.ROB Segments below.
For segment_type == 512 (external reference): no data payload. Load the referenced .3DC file from the asset directory using name as the filename stem. External references are exclusively .3DC (animated models) — static .3D geometry is always embedded inline as type 0 segments, never referenced externally.
MENU.ROB Segments
MENU.ROB is the only ROB file containing type 256 segments. It holds 3D models used for the game’s menu screens.
| # | Name | Type | Size | Version | Notes |
|---|---|---|---|---|---|
| 0 | MENUA001 | 256 | 79,328 | v4.0 | Menu character model (also exists as standalone MENUA001.3DC in /fxart) |
| 1 | MB_TABLE | 0 | 886 | v5.0 | Small prop |
| 2 | MB_PG01 | 256 | 42,208 | v4.0 | Menu page model |
| 3 | MB_PG02 | 256 | 42,208 | v4.0 | Menu page model (same size as PG01) |
| 4 | MB_PG03 | 256 | 42,208 | v4.0 | Menu page model (same size as PG01) |
| 5 | SCROLL | 0 | 8,136 | v5.0 | Scroll decoration prop |
The three MB_PG segments have the four-byte version v4.0; the following byte belongs to the vertex count, not the version.
Footer
4-byte ASCII marker: "END " (with trailing space). Always present.
External References
- UESP: Mod:ROB File Format — high-level ROB structure and historical notes.
- RGUnity/redguard-unity
RGROBFile.cs— ROB segment parsing.
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 data section starts with an 8-byte header. Some sections then include a 4-byte little-endian record_count word at the beginning of section data.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | section_name | ASCII section tag (for example RAHD, MPOB, MPSO) |
| 0x04 | 4 | u32 | BE | data_length | Payload size in bytes. Big-endian in section-framed formats (RGM, PVO, ROB, TEXBSI). |
For count-prefixed sections (MPOB, MPSO, MPRP, and several others), section payload begins with:
Little-endian.
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | record_count | Number of fixed-size records in this section |
Data sections are followed by the four-byte terminal marker END . The terminal
marker has no length word or payload and is not an eight-byte section header.
MPOB (Object Instances)
MPOB starts with a little-endian object count, followed by 66-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 1 | u8 | object_type | Object kind discriminator |
| 0x05 | 1 | u8 | is_active | Activation flag |
| 0x06 | 9 | [u8; 9] | script_name | Script/object name |
| 0x0F | 9 | [u8; 9] | model_name | Model reference |
| 0x18 | 1 | u8 | is_static | For a type-1 placed-model fallback, 1 selects a static model; other values give the animation frame-table length. See placed-object frame sources. |
| 0x19 | 2 | i16 | reserved | Not read at runtime. |
| 0x1B | 3 | i24 | pos_x | Position X (fixed scale) |
| 0x1E | 1 | u8 | pad_x | Alignment byte |
| 0x1F | 3 | i24 | pos_y | Position Y (fixed scale) |
| 0x22 | 1 | u8 | pad_y | Alignment byte |
| 0x23 | 3 | u24 | pos_z | Position Z (fixed scale) |
| 0x26 | 4 | u32 | angle_x | Bethesda 2048-unit Euler angle |
| 0x2A | 4 | u32 | angle_y | Bethesda 2048-unit Euler angle |
| 0x2E | 4 | u32 | angle_z | Bethesda 2048-unit Euler angle |
| 0x32 | 2 | i16 | texture_data | Packed texture id/image id |
| 0x34 | 2 | i16 | intensity | Light/intensity-like field |
| 0x36 | 2 | i16 | radius | Radius-like field |
| 0x38 | 2 | i16 | model_id | Model index/id-like field |
| 0x3A | 2 | i16 | world_id | World index/id-like field |
| 0x3C | 2 | i16 | red | Color channel |
| 0x3E | 2 | i16 | green | Color channel |
| 0x40 | 2 | i16 | blue | Color channel |
Position decode used by current exporter:
- scale constant:
1 / 5120 x = -(pos_x * 256) * scaley = -(pos_y * 256) * scalez = -(0x00FF_FFFF - (pos_z * 256)) * scale
Bethesda 2048-unit Euler angles:
2048 discrete units represent a full 360° rotation — a power-of-two binary angle encoding. The raw u32 is reduced modulo 2048 (equivalently masked with & 0x7FF), giving a value in the range [0, 2047]. Each unit equals 180/1024 ≈ 0.176°.
degrees = (value % 2048) * (180.0 / 1024.0)
| Units | Degrees |
|---|---|
| 0 | 0° |
| 512 | 90° |
| 1024 | 180° |
| 1536 | 270° |
| 2048 | 360° (wraps to 0) |
The original type-1 placement fallback and its message-group assignment are described in actor initialization.
MPOB exporter model lookup behavior:
- Primary source is
model_name(9 bytes, null-trimmed). - If
model_nameis empty, exporter falls back toRAANusingRAHDscript metadata (see RAAN). - If RAAN also yields no result,
script_nameis used as a last resort. - Example:
script_name = FAVISresolves via RAAN toFVPRA001.
MPSO uses a 12-byte model_name field (no fallback chain — model name is always present).
MPSO (Static Objects)
MPSO starts with a little-endian object count, followed by 66-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 12 | [u8; 12] | model_name | Model reference |
| 0x10 | 3 | i24 | pos_x | Position X |
| 0x13 | 1 | u8 | pad_x | Alignment byte |
| 0x14 | 3 | i24 | pos_y | Position Y |
| 0x17 | 1 | u8 | pad_y | Alignment byte |
| 0x18 | 3 | u24 | pos_z | Position Z |
| 0x1B | 1 | u8 | pad_z | Alignment byte |
| 0x1C | 36 | i32[9] | rotation_matrix | 3x3 Q4.28 rotation matrix |
| 0x40 | 2 | u8[2] | unused | Always 0. |
Matrix entries are row-major: row 0 is [m0, m1, m2], row 1 is
[m3, m4, m5], and row 2 is [m6, m7, m8]. Loading converts each signed
Q4.28 entry to a float in place without transposition or normalization.
The matrix maps local column vectors into world coordinates, before adding
the placement position. Camera transformation multiplies this matrix on the left.
MPRP (Rope Chains)
MPRP starts with a little-endian record count, followed by 80-byte records.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Rope/object id |
| 0x04 | 1 | u8 | reserved | Not read at runtime. |
| 0x05 | 3 | i24 | pos_x | Base position X |
| 0x08 | 1 | u8 | pad_x | Alignment byte |
| 0x09 | 3 | i24 | pos_y | Base position Y |
| 0x0C | 1 | u8 | pad_y | Alignment byte |
| 0x0D | 3 | i24 | pos_z | Base position Z |
| 0x10 | 4 | i32 | angle_y | Rope heading field |
| 0x14 | 4 | i32 | type | Type/discriminator |
| 0x18 | 4 | i32 | swing | Swing parameter |
| 0x1C | 4 | i32 | speed | Speed parameter |
| 0x20 | 2 | i16 | length | Number of rope links |
| 0x22 | 9 | [u8; 9] | static_model | Optional terminal model |
| 0x2B | 9 | [u8; 9] | rope_model | Link model name (for example ROPELINK) |
| 0x34 | 4 | i32 | sound_1 | Rope effect for low swing speed; 0 = none. |
| 0x38 | 4 | i32 | sound_2 | Rope effect for medium swing speed; 0 = none. |
| 0x3C | 4 | i32 | sound_3 | Rope effect for high swing speed; 0 = none. |
| 0x40 | 16 | i32[4] | unknown | Read at runtime; zero in all 29 shipped records. |
Rope instancing behavior:
- Decode base translation with the same MPOB scale/sign rules.
- Spawn
lengthcopies ofrope_model. Every link and the end model are yawed byangle_y(0..2047, 2048 per turn) with the same convention as an MPOB object’sangle_y; pitch and roll are zero for a rope at rest. - Link 0 is placed at the base position; each following link is
0.8lower in Y (the height of the link model, shippedROPELINKandCHAINLNKare both 0.8 tall and centered). - If
static_modelis present, place one additional instance0.8below the last link. - Rope swing sounds use
sound_1–sound_3; see positional sound.
Current parser behavior: MPRP is parsed into typed 80-byte records only when section payload is an exact fit for record_count; otherwise raw fallback is kept.
RALC (Location Data)
RALC contains scripted coordinate offsets for objects (e.g. the Boatman’s waypoints). Records are 12-byte entries.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | s32 | offset_x | X coordinate offset (applied to MPOB translated position) |
| 0x04 | 4 | s32 | offset_y | Y coordinate offset |
| 0x08 | 4 | s32 | offset_z | Z coordinate offset |
Actor setup first stores the original runtime home position as location 0, then appends RAHD.ralc_count entries starting at RAHD.ralc_offset. Each appended location is the home position plus its signed RALC offsets, with wrapping 32-bit addition. Home coordinates are the original MPOB position multiplied by 256, before family-local conversion. Authored offsets already use runtime coordinate units. Locations belong to each actor instance. See location movement.
RAVC (VCollide)
RAVC uses 9-byte entries and appears only in a subset of maps (CATACOMB and DRINT — collision data for the dragon and golem).
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | i8 | offset_x | Local collision offset X |
| 0x01 | 1 | i8 | offset_y | Local collision offset Y |
| 0x02 | 1 | i8 | offset_z | Local collision offset Z |
| 0x03 | 2 | u16 | vertex | Model vertex index used as reference point for the collision sphere |
| 0x05 | 4 | u32 | radius | Collision sphere radius |
Current parser behavior: records are parsed as fixed 9-byte entries only when section payload is an exact fit; otherwise raw fallback is kept. RAVC is flat-out missing (not just empty) in RGM files without collision objects.
WDNM (Walk Node Map)
WDNM defines walk node maps for AI pathfinding. Count-prefixed: record count is the number of walk-map blocks.
All fields little-endian.
WalkMap Record
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | map_length | Payload byte length after this length word |
| +0x04 | 4 | u32 | node_count | Number of walk-nodes in this map |
| +0x08 | 4 | u32 | node_count_dup | Duplicate of node_count |
| +0x0C | 3 | s24 | map_pos_x | Map position X |
| +0x0F | 1 | u8 | pad_x | Alignment byte |
| +0x10 | 3 | s24 | map_pos_y | Map position Y |
| +0x13 | 1 | u8 | pad_y | Alignment byte |
| +0x14 | 3 | s24 | map_pos_z | Map position Z |
| +0x17 | 1 | u8 | pad_z | Alignment byte |
| +0x18 | 4 | u32 | radius | Map bounding radius |
| +0x1C | … | — | walk_nodes | node_count × WalkNode records |
The map length excludes its own four-byte length word. Its payload contains the remaining 24-byte map header and all length-prefixed nodes. Embedded maps are loaded in file order and prepended to the runtime map list, so traversal visits them in reverse file order.
WalkNode Record
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | node_length | Payload byte length after this length word |
| +0x04 | 2 | u16 | node_pos_x | Local position X |
| +0x06 | 2 | s16 | node_pos_y | Local position Y |
| +0x08 | 2 | u16 | node_pos_z | Local position Z |
| +0x0A | 1 | u8 | reserved | File value discarded; runtime storage holds node ownership. |
| +0x0B | 1 | u8 | route_count | Number of routes from this node |
| +0x0C | … | — | routes | route_count × NodeRoute records |
The node length excludes its own four-byte length word and equals
8 + 4 * route_count. Loading copies the packed node coordinates directly;
the map header’s position is not added to them.
The loader clears the reserved byte before using the node. Runtime reservation sets that byte to the low byte of the owning actor identifier. An unowned node or a node already owned by the requesting actor can be acquired; another actor’s reservation prevents acquisition. Releasing a reservation clears the byte only when the releasing actor still owns it. The periodic sound-maintenance reset clears all node ownership bytes regardless of owner.
Runtime node selection
WDNM maps load in file order and each new map is prepended to the runtime map list. Runtime traversal therefore visits maps in reverse file order. Nodes within each map retain their file index order. Map identifiers are assigned in load order independently of runtime list position.
The nearest-node query traverses the loaded map list and each map’s nodes in index order. It compares the sum of absolute X, Y and Z differences, using unsigned X/Z and signed Y from the node record. The query uses unscaled node coordinate units.
Only distances strictly below 4096 qualify. Equal distances retain the first candidate. When a new candidate has distance strictly below 12, the query returns immediately, even if a later node would be closer. Otherwise it returns the best candidate after traversing all maps. With no qualifying node, it returns no map and node index -1. Node reservations do not filter this selection query.
NodeRoute Record (4 bytes)
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 2 | u16 | target_node_id | Destination walk-node index |
| +0x02 | 2 | u16 | cost | Route traversal cost |
RAHD (Actor Header)
RAHD is a count-prefixed section with 165-byte records. Each record provides per-actor metadata: script name, bytecode location in RASC, string/variable table pointers, animation references, collision data, and attribute hooks.
Section payload starts with a 4-byte LE record count, followed by a 4-byte LE def_checksum, then count × 165 bytes of records. def_checksum is the byte sum of the SOUP386.DEF the map was built against. Every map that WORLD.INI references stores 0x0037801B, the byte sum of the shipped file; HIDEOUT.RGM, which no world references, stores 0x00372274. With [debug] def_checksum on (shipped), loading a world whose value differs from the installed SOUP386.DEF is a fatal error. The engine reads the count and checksum separately, then bulk-reads count × 165 bytes as the record array. Offsets below are within each 165-byte record (starting at payload offset 8 + i × 165). The Rust parser in this repo starts records 4 bytes earlier (at 4 + i × 165) and adds 4 to all field offsets.
At load time, the engine converts most offset fields into absolute pointers by adding the corresponding section’s data pointer (rebasing). Offset 0x00 is overwritten with a linked-list next-pointer at runtime.
All typed fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | — | field_00 | Overwritten at runtime (linked-list next-pointer) |
| 0x04 | 9 | [u8; 9] | script_name | Script/actor name, null-padded |
| 0x0D | 2 | u16 | instances | Number of instances for this actor |
| 0x0F | 2 | — | padding | Always 0. |
| 0x11 | 4 | i32 | instance_counter | Runtime instance counter (incremented during setup; advances variable_offset by num_variables × 4 per instance) |
| 0x15 | 4 | u8[4] | anim_speed | Read as individual bytes. Byte 0x15: frame limit — animation advances only while frame_counter < byte_0x15. Byte 0x16: frame increment added per tick. |
| 0x19 | 4 | u32 | ranm_offset | Byte offset into RANM section data (rebased to pointer at load) |
| 0x1D | 4 | u32 | raat_offset | Byte offset into RAAT section data (rebased to pointer at load) |
| 0x21 | 4 | i32 | raan_count | Number of RAAN entries for this actor |
| 0x25 | 4 | u32 | raan_data_size | Total byte size of this actor’s RAAN entries. Zero when raan_count = 0. |
| 0x29 | 4 | i32 | raan_offset | Byte offset into RAAN section data (rebased to pointer at load) |
| 0x2D | 4 | — | anim_control_prefix | Low byte stored at animation control struct offset +0x12 during RAGR loading. The complete signed field is compared with −1 when selecting the static-model shortcut; see model loading. |
| 0x31 | 4 | u32 | ragr_offset | Byte offset into RAGR section data (rebased to pointer at load) |
| 0x35 | 2 | u16 | rafs_count | Number of optional RAFS sphere descriptors. Zero in shipped files. |
| 0x37 | 6 | — | padding | Always 0. |
| 0x3D | 4 | u32 | rafs_index | Index into RAFS data (vestigial — RAFS is 1 byte in all shipped files) |
| 0x41 | 4 | u32 | num_strings | Number of strings used by this actor’s script |
| 0x45 | 4 | — | padding | Always 0. |
| 0x49 | 4 | u32 | string_offsets_index | Byte offset into RASB section data |
| 0x4D | 4 | u32 | script_length | Byte length of this actor’s bytecode block in RASC |
| 0x51 | 4 | u32 | script_data_offset | Byte offset into RASC section data (rebased to pointer at load) |
| 0x55 | 4 | u32 | script_pc | Execution start address; rebased at load to script_data_offset + script_pc (absolute pointer) |
| 0x59 | 4 | — | anim_buffer_swap | Read as byte at 0x59. Boolean: non-zero triggers animation frame buffer swap (copies between offsets +0xCB and +0x108 in actor struct). Zero uses primary buffer only. |
| 0x5D | 4 | u32 | rahk_offset | Byte offset into RAHK section data (rebased to pointer at load) |
| 0x61 | 4 | i32 | ralc_count | Number of authored location offsets appended after the actor’s initial home location. |
| 0x65 | 4 | — | field_65 | Auxiliary actor-header data. |
| 0x69 | 4 | u32 | ralc_offset | Byte offset into RALC section data (rebased: ralc_data + (offset ÷ 12) × 12) |
| 0x6D | 4 | u8[4] | actor_flags | Read as individual bytes. Byte 0x6D: animation state ID loaded into a global during dialogue setup, compared against hook data at +0x247 for state matching. Byte 0x6E: item/equipment flag (toggled at runtime). Byte 0x6F: passed to animation/sound function. |
| 0x71 | 4 | u32 | raex_offset | Byte offset into RAEX section data (rebased to pointer at load) |
| 0x75 | 4 | u32 | num_variables | Number of local variables for this actor |
| 0x79 | 4 | u8[4] | visibility_flags | Read as individual bytes. Byte 0x79: visibility test bypass (non-zero = always visible, skip LOD culling). Byte 0x7B: LOD culling mode (0 = fixed distance threshold, non-zero = dynamic distance threshold). |
| 0x7D | 4 | u32 | variable_offset | Byte offset into RAVA section data (÷ 4 = variable array index) |
| 0x81 | 4 | u32 | variable_offset_dup | Runtime copy of variable_offset; advanced by num_variables × 4 per instance |
| 0x85 | 4 | u32 | anim_frame_data | Animation frame count or group index. Upper 16 bits used as count (× 11 bytes per frame for allocation). |
| 0x89 | 4 | i32 | bounds_primary | Primary MPSZ record index; −1 derives bounds from the model. |
| 0x8D | 4 | i32 | bounds_secondary | Secondary MPSZ record index; −1 = absent. |
| 0x91 | 4 | i32 | bounds_tertiary | Alternate MPSZ record index; −1 = absent. |
| 0x95 | 2 | i16 | combat_flag | Combat/state flag. |
| 0x97 | 2 | i16 | raex_stat | Stored at actor state +0x97 after SOUP function lookup. -1 = disabled. |
| 0x99 | 2 | i16 | reserved_99 | Always -1. Not read at runtime. |
| 0x9B | 2 | i16 | texture_id | Actor texture set. Equal to the RAEX texture_id of the same actor in every shipped file; zero when the actor has none. |
| 0x9D | 4 | i32 | ravc_offset | Byte offset into RAVC section data (rebased to pointer at load; -1 = none) |
| 0xA1 | 4 | i32 | ravc_count | Number of RAVC collision entries for this actor |
Total record size: 165 bytes (0xA5).
RAAN (Animation File References)
RAAN contains animation/model file path entries. Records are variable-length null-terminated strings with a 6-byte prefix.
Entry structure at a given byte offset (from RAHD raan_offset):
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | reserved | Not read by any engine function. Skipped during entry iteration. |
| 0x04 | 1 | u8 | frame_count | Used as loop count for animation handle table entries. Capped at 255. |
| 0x05 | 1 | u8 | model_type | Type flag, converted to lowercase at load. Values: 0x63 (ASCII ‘c’) and 0x73 (ASCII ‘s’). |
| 0x06 | var | [u8] | file_path | Null-terminated file path string (e.g. 3dart\cyrsa001.3d) |
The engine iterates RAAN entries by skipping the 6-byte prefix, then scanning forward to the null terminator of file_path. The 4-byte dword at offset 0x00 is NOT used for seeking — the next entry is found purely by string scan.
The model name is extracted by stripping directory separators, file extension, and uppercasing the stem.
Model Fallback Resolution
When an MPOB record has an empty model_name, the exporter resolves a model via RAHD/RAAN:
- Look up
script_namein the RAHD index to get(raan_offset, raan_count). - Parse the RAAN entry at
raan_offsetto extract the file path. - Strip the path to a bare filename stem (e.g.
fxart\FVPRA001.3DC→FVPRA001). - Use the stem as the model name for asset lookup.
- If no RAHD/RAAN match, fall back to using
script_nameas the model name.
RAFS (Actor FSphere Data)
RAFS was intended to store pre-computed fsphere (combat/collision sphere) data for actors. The engine only loads this section when its size exceeds 10 bytes. In all 27 shipped RGM files, RAFS is exactly 1 byte — the section is vestigial and never loaded at runtime. FSpheres are instead computed from MPSZ bounding volume records indexed via RAHD.
RAFS descriptors are independent of the 9-byte vertex-bound RAVC records. The actor header supplies a descriptor count and an index measured in 11-byte records. Each descriptor contains a flags byte, a 16-bit animation-group selector, three signed 16-bit center offsets and a signed 16-bit radius. During actor initialization, flag 4 resolves the selector to the actor’s animation binding pair; without that flag both selector bytes are cleared.
The contact sphere builder admits flag 2 unconditionally, or flag 4 when the resolved binding pair matches the actor’s current pair. It rotates the center by the actor’s current matrix and adds its position. Unless actor attribute 56 is nonzero, ordinary body spheres follow these descriptors. This list is used for projectile endpoint contact and player contact with ropes. RAVC vertex volumes do not substitute for it.
RAST (String Data)
RAST contains all script string literals as null-terminated strings concatenated into a single blob. No count prefix; it is a flat byte array. Individual strings are located by offsets stored in RASB.
During loading, RASB offsets are rebased by adding the RAST data pointer, converting relative offsets into direct pointers.
RASB (String Offset Table)
RASB contains u32 LE offsets into RAST, one per string per actor. Each actor’s portion starts at string_offsets_index (from RAHD) and contains num_strings entries.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 4 | u32 | string_offset | Byte offset into RAST where the null-terminated string begins |
Total section length: sum of all actors’ num_strings × 4.
RAVA (Local Variables)
RAVA contains initial values for local script variables as a flat array of i32 LE integers. The first 4 bytes are always zero (sentinel).
Each actor’s portion starts at byte offset variable_offset (from RAHD; divide by 4 for the array index) and contains num_variables entries. When an actor has multiple instances, variables are replicated instances times. Variables are addressed by index in script bytecode (opcode 0x0A).
RASC (Script Bytecode)
RASC contains compiled SOUP386 scripting bytecode as a contiguous byte blob. Per-actor instruction blocks are located via RAHD offsets (script_data_offset, script_length, script_pc) and executed by the SOUP virtual machine.
For VM architecture, opcode encoding, operator tables, and execution semantics, see SOUP Scripting. For the definition-file format, see SOUP386.DEF.
Section Layout
| Region | Size | Description |
|---|---|---|
| Preamble | script_data_offset bytes | Zero-padded address space used by rebased hook/offset references |
| Script blocks | remainder | Concatenated per-actor bytecode blocks in RAHD order |
Total payload = first actor script_data_offset + sum of all actors’ script_length values. For each actor, execution starts at script_pc relative to that actor’s block.
Other RA* sections provide separate actor data; their layouts and offset bases
are section-specific.
RAHK (Hook Data)
RAHK contains native call records. An actor’s nonzero RAHD.rahk_offset is a
byte offset into this section’s payload; the loader adds the RAHK payload base.
Zero means no hook. The ordinary actor script wrapper dispatches the referenced
record through the native function table, separately from its RASC entry and
main continuations.
A stored record begins with a little-endian u16 native function index and a
u8 argument count, followed by five-byte encoded arguments. Each argument has
a one-byte parameter-mode tag and four operand bytes. Function indices use the
installed SOUP definition table. Parameter evaluation uses the receiving actor
for local-variable references and the executing actor’s string context.
The native hook is not a table of RASC offsets and has no group-indexed array. Its execution gates are described in SOUP ordinary update gates.
RAEX (Extra Data)
RAEX contains per-actor extra data with 30-byte fixed-size records (15 × i16 LE fields). The engine requires this section during loading. Record count is section_data_length ÷ 30. Per-actor RAEX offsets are stored in RAHD (raex_offset).
Field names Grip0 through RangeMax are from the in-game debug console.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | i16 | grip0 | First source-model vertex for the drawn-weapon attachment. |
| 0x02 | 2 | i16 | grip1 | Second source-model vertex for the drawn-weapon attachment. |
| 0x04 | 2 | i16 | scabbard0 | First source-model vertex for the stowed-weapon attachment. |
| 0x06 | 2 | i16 | scabbard1 | Second source-model vertex for the stowed-weapon attachment. |
| 0x08 | 2 | i16 | anim_frame_ref | Matches RAHD anim_frame_data at 0x85. Set on mobile actors only. |
| 0x0A | 2 | u16 | texture_id | Actor texture set (TEXBSI number) for skin variants; zero selects the models’ own textures. See actor texture sets. |
| 0x0C | 2 | i16 | v_vertex | Model vertex used by combat contact queries. |
| 0x0E | 2 | i16 | v_size | Combat contact radius in model units; copied to runtime Q8 units. |
| 0x10 | 2 | i16 | taunt_id | First taunt animation id; additional taunts count up from this value. |
| 0x12 | 2 | i16 | collision_sphere_scale | Eight-fractional-bit radius scale for actor collision spheres; nonpositive values select 256. |
| 0x14 | 2 | i16 | field_14 | Source value for RAHD raex_stat at 0x97. Set on combat actors. |
| 0x16 | 2 | i16 | field_16 | Set on some combat actors. |
| 0x18 | 2 | i16 | range_min | Combat engagement minimum range. Multiply by 256 for world units. Only set on dragon, golem, serpent. |
| 0x1A | 2 | i16 | range_ideal | Combat ideal range. Same scaling. |
| 0x1C | 2 | i16 | range_max | Combat maximum range. Same scaling. |
Total record size: 30 bytes (0x1E). Record count is section_data_length / 30.
Actor texture sets
A nonzero RAEX texture_id applies to every multi-frame model the actor draws,
including each RAAN frame-source model. The engine keeps one copy of a frame-source
model per texture set and shares it between actors with the same set. Each face
texture word of the copy is bank << 7 | image; words whose bank is greater than 1
take the actor’s texture set as their bank and keep their image index. Words with
bank 0 or 1 are unchanged. For v4.0 frame-source models, loading a solid-colour
face converts its colour descriptor to retained RGB and explicitly sets its
runtime texture word to zero. The virtual copy therefore keeps that face solid;
it does not replace it with image zero of the actor’s texture set. Geometry,
frame data and normals are those of the source model.
RAAT (Attribute Data)
RAAT contains per-actor attribute tables. Each actor has a 256-byte attribute block, ordered sequentially (actor 0 at offset 0, actor 1 at offset 256, etc.).
Attribute names are defined in the auto…endauto section of SOUP386.DEF (see SOUP386.DEF). Each byte is a named attribute value; zero means unset. Attributes are read/written by the script functions GetAttribute and SetAttribute.
Total section length: record_count × 256.
RAGR (Animation Groups)
RAGR contains animation group definitions that link actors to their animation data in RAAN. RAGR provides the RGM-embedded equivalent of the AIAN section found in standalone .AI files; the engine selects one or the other source based on a runtime mode flag.
Per-actor RAGR data is located via RAHD.ragr_offset (offset 0x31). Entries are size-prefixed: the first u16 is the entry payload size (excluding itself); a value of 0 terminates the list. Advance to the next entry: current_position + 2 + entry_size.
The prefix byte used by the AIAN (standalone .AI) path is NOT present in RGM RAGR — instead, that value comes from RAHD offset 0x2D.
Animation Group Entry
All fields little-endian.
| Relative Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| +0x00 | 2 | u16 | entry_size | Payload size in bytes after this field. 0 = end of groups. Should equal 8 + frame_count × 3. |
| +0x02 | 2 | u16 | group_index | Animation group slot (0–177; validated ≤ 0xB1 at load) |
| +0x04 | 2 | u16 | anim_id | Animation identifier |
| +0x06 | 2 | u16 | anim_type | Animation type (only low byte used). Values: 0 = interruptible (idle/panic), 1 = must complete (combat), 2 = no panic revert (ledge-hang loops). |
| +0x08 | 2 | u16 | frame_count | Number of animation frames in this group |
| +0x0A | var | [u8; frame_count × 3] | commands | Packed 3-byte animation commands, one per frame |
Animation command (3 bytes, packed LE)
The low four bits select the opcode. A file ShowFrame command, opcode zero,
uses all remaining twenty bits as an unsigned frame reference. It does not
encode an attachment vertex.
RAAN entries form one concatenated frame table in file order. An entry with frame count N contributes its model’s frames zero through N−1. The frame reference selects one entry in that table. For example, model A with three frames followed by model B with two frames gives references 0–2 for A and 3–4 for B. Reference 4 selects model B, frame 1.
The loader rewrites each ShowFrame into two ten-bit fields: runtime model
handle and frame index. These patched runtime commands have a different
meaning from file commands. Both resolved fields fit in a byte. Raw file
commands must be resolved through RAAN before they can be played.
Opcodes 4 (PlaySound) and 10 (ChangeAnimGroup) use two ten-bit fields in the file. Their parameters are sound settings and animation jump targets, respectively. See animation playback and the opcode table.
RANM (Namespace)
RANM contains object namespace strings used for cross-script object references. Each actor’s portion is located by the RAHD ranm_offset field at 0x19. 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 are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | link | Serialized linkage field, replaced when the light enters a spatial list. |
| 0x04 | 4 | u32 | world_id | World ID used to select the light at load time. Reused as linkage after selection. |
| 0x08 | 4 | i32 | pos_x | X position in engine coordinates. |
| 0x0C | 4 | i32 | pos_y | Y position in engine coordinates. |
| 0x10 | 4 | i32 | pos_z | Z position in engine coordinates. |
| 0x14 | 2 | i16 | radius | Local-light radius in engine coordinates. |
| 0x16 | 2 | i16 | field_16 | Auxiliary field. |
| 0x18 | 6 | [i16; 3] | rgb | Signed red, green and blue strengths. Negative channels subtract light. |
| 0x1E | 1 | u8 | field_1e | Auxiliary byte. |
| 0x1F | 1 | u8 | active | Must equal 1 for load-time selection. |
| 0x20 | 6 | [u8; 6] | field_20 | Auxiliary bytes. |
| 0x26 | 4 | [u8; 4] | field_26 | Auxiliary bytes. |
With map lights enabled, the loader selects records whose active byte equals
1 and whose world_id matches the selected WORLD.INI entry. It inserts those
records into spatial lists and replaces the first two words with list links.
The position, radius and RGB fields retain their file values. The first three
bytes are not a color, and the RGB words are not a direction vector.
Local-light selection for each model is described in model lighting.
MPMK (Markers)
MPMK starts with a little-endian record count, followed by a contiguous array of
12-byte position records, then a separate array of one auxiliary byte per marker.
The payload length is 4 + count * 12 + count. The arrays are not interleaved.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 3 | i24 | pos_x | Position X |
| 0x03 | 1 | u8 | pad_x | Alignment byte |
| 0x04 | 3 | i24 | pos_y | Position Y |
| 0x07 | 1 | u8 | pad_y | Alignment byte |
| 0x08 | 3 | i24 | pos_z | Position Z |
| 0x0B | 1 | u8 | pad_z | Alignment byte |
Position fields use the same i24+pad encoding as MPOB/MPSO. No explicit record ID field.
The auxiliary table begins at 4 + count * 12; byte i belongs to position
record i. A lookup in a loaded world requires the auxiliary byte to equal 1
and the index to be below the marker count; a zero byte identifies an unavailable
marker. All 27 shipped maps hold at most 49 markers and use only the values 0
and 1.
See node-marker lookup. The metadata exporter retains it
as reserved for compatibility. map_position retains the three signed map
coordinates before export-axis conversion and scaling; position retains the
existing converted coordinates. Runtime placement can use map_position without
recovering integers from floating-point export coordinates.
MPSZ (Bounding Volumes)
MPSZ is an array of 49-byte bounding volume records used to build per-actor fspheres (combat/collision bounding spheres) at runtime. Not count-prefixed — record count is section_size / 49. RAHD fields at 0x89, 0x8D and 0x91 store per-actor indices into this table. A primary index of −1 derives bounds from the model; −1 in either alternate index means absent.
Record Layout (49 bytes)
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | flags | Initially zero; runtime sphere construction selects the dominant axis here |
| 0x01 | 4 | i32 | total_x | Total extent X (pos_x + neg_x) |
| 0x05 | 4 | i32 | total_y | Total extent Y (pos_y + neg_y) |
| 0x09 | 4 | i32 | total_z | Total extent Z (pos_z + neg_z) |
| 0x0D | 4 | i32 | center_x | Center offset X |
| 0x11 | 4 | i32 | center_y | Center offset Y |
| 0x15 | 4 | i32 | center_z | Center offset Z |
| 0x19 | 4 | i32 | pos_x | Positive half-extent X |
| 0x1D | 4 | i32 | pos_y | Positive half-extent Y |
| 0x21 | 4 | i32 | pos_z | Positive half-extent Z |
| 0x25 | 4 | i32 | neg_x | Absolute negative half-extent X |
| 0x29 | 4 | i32 | neg_y | Absolute negative half-extent Y |
| 0x2D | 4 | i32 | neg_z | Absolute negative half-extent Z |
Invariant: total = neg + pos for each axis. Center is always zero in shipped files. Bounds can be asymmetric (neg != pos).
At runtime, the engine copies these 49 bytes and calls the fsphere builder to create a 3D bounding volume for the actor. RAHD provides three index fields per actor, allowing reference to different bounding records. Only a subset of actors have direct RAHD indices.
Flags are zero in shipped files. The flag byte precedes the integers; placing it last shifts every decoded field.
ISLAND CYRUS references primary record 120 and secondary record 121, with no tertiary record. Its primary totals are (48, 128, 49), positive extents (24, 62, 11), and negative extents (24, 66, 38).
MPSF (Flat Objects)
MPSF starts with a little-endian record count, followed by 24-byte records. Each record places a textured quad in the scene.
All fields little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | id | Object id |
| 0x04 | 4 | i32 | reserved | Not read at runtime. |
| 0x08 | 3 | i24 | pos_x | Position X |
| 0x0B | 1 | u8 | pad_x | Alignment byte |
| 0x0C | 3 | i24 | pos_y | Position Y |
| 0x0F | 1 | u8 | pad_y | Alignment byte |
| 0x10 | 3 | u24 | pos_z | Position Z |
| 0x13 | 1 | u8 | pad_z | Alignment byte |
| 0x14 | 2 | u16 | texture_data | Packed: texture_id = data >> 7, image_id = data & 0x7F |
| 0x16 | 2 | i16 | reserved | Not read at runtime. |
Position decode uses the same MPOB scale/sign rules. MPSF records are submitted
as camera-facing textured quads centered on their placed position. Their full
width and height in engine units are the source BSI image dimensions multiplied
by the BSI texture scale divided by 256. A zero texture scale selects 256.
The ordinary flat path uses geometry scale 1 and copies the camera orientation;
it does not use a fixed-size model quad or an authored object rotation.
Texture-coordinate extents are computed separately from geometry size. For
unreduced textures, they cover the complete source image, excluding upload
padding. The first two corners use the image top edge, and the next two use
the bottom edge. The
FLAT attributes below modify the submission flags and alpha.
FLAT
FLAT stores per-texture drawing attributes, separate from the placed objects
in MPSF. Its payload begins with 512 little-endian u16 record counts,
one for each texture archive ID. The records follow in ascending archive order.
Each record is three bytes: image ID, drawing flags, and alpha byte. Archives
with a zero count contribute no records. There is no separate total-count word.
Texture lookup splits the packed texture reference into an archive ID and seven-bit image ID, then searches that archive’s records for the first matching image. A missing archive or image has no attribute override.
For an MPSF draw, the base flags are 1 and the default alpha input is 255.
A matching attribute record is ORed into the flags and replaces the alpha input
with alpha_byte + 1, computed without byte wrapping. Thus a stored zero
supplies 1, and a stored 255 supplies 256. The geometry scale and texture
coordinate scale inputs remain 1. The base flag selects a centered quad;
attribute flags do not replace that anchor selection.
FLAT appears in all 27 shipped RGM files as the last section before END .
Its payload is 1111 bytes in 26 files and 1108 bytes in HIDEOUT.RGM.
External References
- UESP: Mod:RGM File Format
- UESP: Mod:Redguard File Formats
- RGUnity/redguard-unity
RGRGMFile.cs— RGM section parser - RGUnity/redguard-unity
RGRGMScriptStore.cs— RASC bytecode interpreter with dispatch loop and flags table (369 entries) - RGUnity/redguard-unity
soupdeffcn_nimpl.cs— SOUP function ID-to-name table (367 functions) - Dillonn241/redguard-mod-manager
ScriptReader.java— RASC bytecode disassembler (bytecode to readable script text) - Dillonn241/redguard-mod-manager
ScriptParser.java— RASC bytecode assembler (script text to bytecode) - Dillonn241/redguard-mod-manager
MapFile.java— RGM section reader/writer with chunk tag list - Dillonn241/redguard-mod-manager
MapHeader.java— RAHD record parser with field offsets - Dillonn241/redguard-mod-manager
MapDatabase.java— SOUP386.DEF parser (function, flag, reference, attribute definitions)
WLD World Geometry File Format
Terrain/world-grid container with a fixed 4-section layout; each section stores four 128x128 byte maps.
4 WLD files exist in /maps: EXTPALAC.WLD, HIDEOUT.WLD, ISLAND.WLD, NECRISLE.WLD.
Overall Structure
All WLD files are exactly 263,432 bytes.
[Header — 1184 bytes]
[Section 0 — 65558 bytes]
[Section 1 — 65558 bytes]
[Section 2 — 65558 bytes]
[Section 3 — 65558 bytes]
[Footer — 16 bytes]
Each section (65,558 bytes) is:
[Section Header — 22 bytes]
[Map 1 — 128×128 bytes (heightmap)]
[Map 2 — 128×128 bytes (unused, zero-filled)]
[Map 3 — 128×128 bytes (texture/material)]
[Map 4 — 128×128 bytes (unused, zero-filled)]
Header (1184 bytes)
The file header is 296 dwords (u32[296]). Most are zero; 12 are non-zero.
Logical field groups within the 296-dword header:
unknown1[6](u32[6]) at0x00..0x17sec_hdr_size(u32) at0x18file_size(u32) at0x1Cunknown2[28](u32[28]) at0x20..0x8Fsec_ofs[4](u32[4]) at0x90..0x9Funknown3[256](u32[256]) at0xA0..0x49F
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | unknown1_0 | Always 16. |
| 0x04 | 4 | u32 | section_cols | Always 2. |
| 0x08 | 4 | u32 | section_rows | Always 2. |
| 0x0C | 4 | u32 | reserved_0c | Always 0. |
| 0x10 | 4 | u32 | unknown1_4 | Always 160 (0xA0). |
| 0x14 | 4 | u32 | unknown1_5 | Always 1. |
| 0x18 | 4 | u32 | section_header_size | Always 22. |
| 0x1C | 4 | u32 | file_size_field | Always 263416 (file_size - 16). |
| 0x90 | 4 | u32 | section0_offset | Always 1184 (0x4A0). |
| 0x94 | 4 | u32 | section1_offset | Always 66742 (0x104B6). |
| 0x98 | 4 | u32 | section2_offset | Always 132300 (0x204CC). |
| 0x9C | 4 | u32 | section3_offset | Always 197858 (0x304E2). |
| 0xA0 | 4 | u32 | unknown3_0 | Always 2135957017 (0x7F501E19); first element of unknown3[256]. |
0xA0..0x49F is a contiguous u32[256] block (unknown3), not a single field. Only unknown3[0] is non-zero; the remaining 255 dwords are zero.
The header is byte-identical across all 4 shipped WLD files. All remaining header dwords not listed above are zero.
The loader reads the first 0x90 bytes, validates 0x14 == 1 and 0x18 == 22, and uses only section_cols, section_rows, and section_header_size at runtime. Fields beyond 0x1C — including sec_ofs[4] and unknown3[256] — are not read by the terrain loader.
Section Header (22 bytes)
Each section starts with 11 little-endian words (u16[11]):
| Offset (in section) | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 6 | u16[3] | unknown1 | Section-local unknown values. |
+0x06 | 2 | u16 | texbsi_file | Section-declared texture archive id (TEXBSI.%03d). In the original engine’s terrain path, texture-bank loading is hard-wired to texbsi.302 (see notes below). |
+0x08 | 2 | u16 | map_size | Always 256 (2 x 128). |
+0x0A | 12 | u16[6] | unknown2 | Always 0. |
Section headers are identical across all 4 shipped WLD files. unknown1[0] varies by section index: section0=2152, section1=568, section2=1308, section3=10. texbsi_file is always 302. map_size is always 256.
The loader reads each 22-byte section header but does not decode or reference any fields — it proceeds directly to map-plane reads. The engine hard-wires texbsi.302 for terrain textures regardless of the per-section texbsi_file value.
unknown1[0] is not a TEXBSI id. Values such as 568 and 1308 have no corresponding files.
Section headers (identical across all files):
| Section | Header bytes (hex) |
|---|---|
| 0 | 68 08 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 1 | 38 02 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 2 | 1C 05 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
| 3 | 0A 00 00 00 00 00 2E 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 |
Map Planes
After each 22-byte section header, four 128x128 byte maps follow.
Map 1: heightmap plane; low 7 bits are height (0–127), high bit is a build-time flag stripped at load time. See Map 1 High Bit.Map 2: unused — always zero-filled; skipped by the engine at load time.Map 3: texture/material plane; packed bits:- low 6 bits (
0..63) = texture index - high 2 bits (
0..3) = quarter-turn rotation
- low 6 bits (
Map 4: unused — always zero-filled; skipped by the engine at load time.
The engine only uses Map 1 and Map 3. Maps 2 and 4 are skipped during loading — their bytes are read to advance the file stream, but the data is never stored or used.
texbsi_file = 302 matches on-disk texture archive fxart/TEXBSI.302.
In TEXBSI.302, image names follow D02xxx, which aligns with TEXBSI naming rules:
-
filename low two digits (
02) match image-name file number (D02xxx) -
filename hundreds digit (
3) matches type-char group (D) -
Map 3packs the texture index and rotation bits described by UESP. -
Original-engine terrain initialization hard-loads
texbsi.302and precomputes a 64-entry terrain texture lookup table from that archive for Map 3’s 0..63 index space. -
The terrain initialization path references
texbsi.302; the generic TEXBSI loader path referencestexbsi.%s. -
The original engine’s terrain renderer does not use per-section
texbsi_fileswitching. Terrain textures come fromTEXBSI.302.
The shipped fxart/TEXBSI.302 has these properties:
Map 3index range0..63is fully covered by image ids inTEXBSI.302(name suffixes00..63).- IDs
00,05,06,07,30,31,32, and52areIFHDanimated records rather thanBSIFimage records. Index mapping therefore does not use simple record order.
Bank summary (fxart/TEXBSI.*, D-prefix terrain-style banks):
- Only
TEXBSI.302has full0..63coverage (56BSIFstatic records + 8IFHDanimated records). OtherD*banks are partial/specialized. - The original engine’s terrain path uses fixed bank
302, not 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.
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. Terrain
initialization rebuilds all 128 lookup entries as offset - ABS(source), storing
each result as a 32-bit float. World setup supplies world_scapeshift as the
offset after retaining its low signed 16-bit word. Missing or empty entries
use zero. A zero offset therefore produces the unshifted negative heights; it does
not skip rebuilding the table. Nonzero offsets shift the same table rather than
selecting a different source table.
Height Lookup Table
The 128-entry source table (values in engine units before negation):
0: 0 40 40 40 80 80 80 120 120 120
10: 160 160 160 200 200 200 240 240 240 280
20: 280 320 320 320 360 360 400 400 400 440
30: 440 480 480 480 520 520 560 560 600 600
40: 600 640 640 680 680 720 720 760 760 800
50: 800 840 840 880 880 920 920 960 1000 1000
60: 1040 1040 1080 1120 1120 1160 1160 1200 1240 1240
70: 1280 1320 1320 1360 1400 1440 1440 1480 1520 1560
80: 1600 1600 1640 1680 1720 1760 1800 1840 1880 1920
90: 1960 2000 2040 2080 2120 2200 2240 2280 2320 2400
100: 2440 2520 2560 2640 2680 2760 2840 2920 3000 3080
110: 3160 3240 3360 3440 3560 3680 3800 3960 4080 4280
120: 4440 4680 4920 5200 5560 6040 6680 7760
The table uses a non-linear encoding with three regions:
- Indices 0–52 (values 0–2120): near-linear with ~40-unit steps and repeated values, giving maximum height precision for flat and gently sloped terrain where the player spends most time.
- Indices 53–69 (values 2120–3240): transition zone with gradually increasing step sizes.
- Indices 70–127 (values 3240–7760): accelerating steps (80 → 120 → 240 → 1080), compressing tall cliffs and peaks into fewer index values.
This is a hand-tuned gamma-like curve that allocates more precision to common terrain heights (flat ground, gentle slopes) while still supporting the full elevation range with 7 bits of storage.
Texture Selection (Map 3)
Each Map 3 byte encodes two fields:
| Bits | Mask | Field | Range |
|---|---|---|---|
| 5:0 | & 0x3F | texture index | 0–63 |
| 7:6 | >> 6 & 3 | quarter-turn rotation | 0–3 |
In the original engine, the texture index selects one of 64 preloaded terrain texture entries sourced from TEXBSI.302 (not a per-section runtime bank switch). The rotation selects one of four UV orientation states (0°, 90°, 180°, 270° counter-clockwise).
Terrain Texture Blending (SURFACE.INI)
The engine loads a SURFACE.INI configuration file (from the game directory) that defines per-texture-index blend behavior and surface-type sound remapping. This drives a pixel-level alpha-blending system for terrain tile transitions — it does not affect geometry, UVs, or material assignment.
Water Tiles
Wave displacement selects a corner with texture index {0, 30, 31} when its three neighbours toward negative engine X and positive engine Z are in {0, 5, 30, 31}. Indices use the lower six bits. Only that corner moves; texture 5 is a permitted neighbour. See Water Waves for raw-row orientation, timing, and the displacement formula.
Terrain Normals
The engine computes smooth vertex normals for terrain in three passes:
- Face normals — Each grid cell is split into two triangles (TL→BR→TR and BR→TL→BL). A cross-product normal is computed per triangle.
- Vertex averaging — At each grid vertex, the face normals from all adjacent triangles are summed and normalized. Each interior vertex touches 6 triangles from 4 cells: both triangles of the cell to the upper-left and lower-right, plus one triangle each from the cells above and to the left.
- Rendering — The averaged vertex normals are used for Gouraud-interpolated shading across each triangle.
This produces smooth terrain shading. The per-triangle face normals (pass 1) are retained as intermediate values but are not used directly for rendering.
Footer (16 bytes)
The final 16 bytes are constant in all files:
54 55 4C 4F 28 C0 43 00 FF FF FF FF 35 37 04 00
Interpreted as four dwords (u32, little-endian):
0x4F4C5554("TULO"bytes in file order)0x0043C0280xFFFFFFFF0x00043735
The WLD loader does not read or parse this footer; it stops after reading the 4 section data blocks. The footer is ignored at runtime.
Relationships to Other Formats
- RGM stores scene/object placement for the same levels.
- PVO stores octree-style spatial/visibility data for some of the same levels. WLD terrain is not included in PVO visibility culling. The engine’s PVO visibility check operates exclusively on placed objects — it searches MLST entries against MPSO records and actor pointers, and is called only from the placed-object render loop. Terrain is rendered through a separate unconditional path (the engine’s terrain surface subsystem). Default visibility is 1 (visible) when PVO data is absent.
- TEXBSI supplies textures referenced by
Map 3indices (Map 3 index range 0–63 is fully covered byTEXBSI.302). - Other XnGine games use the same container layout. See Engine lineage.
External References
- UESP: Mod:World Files
- UESP: Mod:Redguard File Formats
- uesp/redguard-3dfiletest
3DFileTest/Common/ - UESP: Daggerfall Mod:WOODS.WLD format
- Daggerfall Unity
WoodsFile.cs - RGUnity/redguard-unity
RGWLDFile.cs
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 truncates each camera world-space coordinate toward zero, then subtracts the root center. A coordinate outside the root half-extent produces an empty selected-object list. Otherwise, it walks child links using strict positive coordinate comparisons to choose the octant. After descending, it halves the cell extent and subtracts the child center displacement on each axis. A missing octant link ends the walk at the current node.
The selected node’s PLST entries are decoded in reverse entry order. Each entry’s MLST run retains its forward order, including duplicate object IDs. A node without a PLST reference produces an empty list. These IDs identify placed objects, rather than individual model faces.
The data is generated offline by placing a virtual camera at every point on a uniform 256-unit grid across the level, running a visibility query at each point, and baking the results into the octree. See Generation Process for details.
Overall Structure
The file uses the same section-framing as RGM: 4-byte ASCII tag + 4-byte big-endian data length.
[OCTH — header]
[OCTR — octree node records]
[PLST — leaf polygon-list records]
[MLST — master polygon index table]
[END — footer]
Each section is framed as:
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | tag | ASCII section tag |
| 0x04 | 4 | u32 | BE | data_length | Payload size in bytes (0 for END ) |
The optional PTCH section is absent from shipped files. The engine writes and loads PTCH sections through dedicated save/load paths (the pvopatchsave console command triggers a write).
PTCH patch records
PTCHsection length is serialized aspatch_count * 6bytes.- The loader allocates and reads 6-byte records from
PTCH. - Patch application expands record data into MLST object-id lists used by PVO visibility checks.
Each patch record has this layout:
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | u32 | LE | octr_node_index | OCTR-node index ((node_ptr - octr_base) / 5) identifying which octree node receives the patch object id. |
| 0x04 | 2 | u16 | LE | object_index | Object index appended to the runtime-visible MLST id list for that node. |
Engine behavior:
- Add patch: writes
octr_node_indexatrecord+0andobject_indexatrecord+4. - Delete patch: matches/removes records by the same pair (
octr_node_index,object_index). - Apply patch: scans records matching current
octr_node_indexand appendsobject_indexvalues into the runtime visibility-id buffer.
OCTH Section — Header (52 bytes payload)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | magic | OCTH |
| 0x04 | 4 | u32 | BE | header_data_size | Always 52 (0x34). |
| 0x08 | 4 | u32 | LE | depth | Always 10. Maximum octree depth. |
| 0x0C | 4 | u32 | LE | total_nodes | Total octree node count. Equals leaf_nodes + interior_nodes. |
| 0x10 | 4 | u32 | LE | leaf_nodes | Leaf node count. Equals total_nodes - interior_nodes. |
| 0x14 | 4 | u32 | LE | mlst_polygon_count | Total entries in the MLST polygon index table. Invariant: mlst_polygon_count * 2 == MLST data_length. |
| 0x18 | 4 | u32 | LE | reserved | Always 0. |
| 0x1C | 4 | u32 | LE | cell_size | Root cell half-extent. Power of 2: 16384 (4 files) or 8192 (PALACE). |
| 0x20 | 4 | i32 | LE | center_x | Octree root center X coordinate. |
| 0x24 | 4 | i32 | LE | center_y | Octree root center Y coordinate. |
| 0x28 | 4 | i32 | LE | center_z | Octree root center Z coordinate. |
| 0x2C | 16 | — | — | reserved | Always zero (4 × u32). |
Node count relationship
total_nodes = leaf_nodes + interior_nodes where interior_nodes equals the number of 0xFFFFFFFF leaf_ref values in the OCTR section.
Master polygon count
The MLST payload contains mlst_polygon_count two-byte entries, so its byte
length is mlst_polygon_count * 2.
Center coordinates and extents
The octree root spans [center - cell_size, center + cell_size] on each axis.
OCTR Section — Octree Node Records
Serialized octree nodes written sequentially. Each node is a variable-length record addressed by byte offset within the section.
Node record format
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 1 | u8 | child_mask | Bit field. Bit i set = child i is present (octants 0..7). |
| 1 | 4 | u32 | leaf_ref | Byte offset into the PLST section. 0xFFFFFFFF = no leaf data (interior-only node). |
| 5 | 4 × n | u32[n] | child_refs | One entry per set bit in child_mask, low bit first. Each is a byte offset into the OCTR section pointing to a child node. 0xFFFFFFFE = uninitialized-child sentinel (see below). |
Record size = 5 + popcount(child_mask) * 4
Multiple child links may reference the same OCTR node record. Node references therefore do not require unique parent ownership.
Possible sizes: 5, 9, 13, 17, 21, 25, 29, 33, 37 bytes.
Octant assignment
The 3-bit octant index encodes spatial position relative to the node center:
bit 0 (value 1) = z > center_z
bit 1 (value 2) = y > center_y
bit 2 (value 4) = x > center_x
Octant 0 = (x-, y-, z-) Octant 4 = (x+, y-, z-)
Octant 1 = (x-, y-, z+) Octant 5 = (x+, y-, z+)
Octant 2 = (x-, y+, z-) Octant 6 = (x+, y+, z-)
Octant 3 = (x-, y+, z+) Octant 7 = (x+, y+, z+)
Common child_mask patterns
| Pattern | Binary | Meaning |
|---|---|---|
0x00 | 00000000 | Leaf node, no children |
0x33 | 00110011 | Children in octants 0,1,4,5 (one face) |
0xCC | 11001100 | Children in octants 2,3,6,7 (opposite face) |
0xAA | 10101010 | Children in octants 1,3,5,7 (axis-aligned half) |
0x55 | 01010101 | Children in octants 0,2,4,6 (other half) |
0xFF | 11111111 | All 8 children present |
Child and leaf sentinel values
Two sentinel values appear in octree records:
0xFFFFFFFFin theleaf_reffield marks interior-only nodes — nodes with children but no directly associated polygon list. The count of these values equalsinterior_nodesfrom the header. In runtime traversal code,0xFFFFFFFFalso serves as the null terminator that ends octree walks.0xFFFFFFFEinchild_refsmarks an uninitialized/placeholder child node. During runtime octree traversal, this value indicates an unpopulated slot; when traversal visits it, the slot is overwritten with the current position. This is distinct from a null child (0xFFFFFFFF) and from a valid child offset.
PLST Section — Leaf Polygon-List Records
Serialized leaf data written sequentially. This is the largest section in every file. Each leaf describes which polygon groups are visible from an octree cell.
Leaf record format
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 1 | u8 | entry_count | Number of entries in this leaf. |
| 1 | 6 × n | — | entries | Array of entry_count entries (see below). |
Record size = 1 + 6 * entry_count
Each entry:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 2 | u16 | count | Number of polygon indices in this sub-list. |
| 2 | 4 | u32 | mlst_start | Starting index into the MLST array. |
Each entry references a contiguous slice: mlst[mlst_start .. mlst_start + count].
Constraint: mlst_start + count <= mlst_polygon_count.
Entries within a leaf represent distinct polygon groups. The full visible set for a leaf is the union of all its entry slices. Multiple leaves may share the same MLST ranges.
leaf_ref values in OCTR are byte offsets into this section, pointing to the start of a leaf record.
MLST Section — Master Polygon Index Table
A flat array of u16 visibility group indices.
- Length:
mlst_polygon_count * 2bytes.
This table is the master list of visibility groups referenced by the octree. PLST entries reference contiguous ranges within this table.
Index semantics
Each u16 value is a placed-object visibility ID, not an individual face index. The indices form a dense, zero-based sequential range with no gaps. The visibility check at runtime uses two lookup paths based on the index value:
- Indices 0 .. MPSO_count-1: direct MPSO record index. The runtime multiplies the index by 66 (0x42 = MPSO record size) and adds the MPSO array base to get the placed-object record.
- Indices MPSO_count .. N-1: secondary table index. The runtime subtracts MPSO_count and uses the result as an index into a separate pointer table. This table holds additional static objects loaded at runtime (e.g. via
LoadStaticscript commands, MPRP rope chains, or other non-MPSO visibility targets).
| File | Total IDs | MPSO range | Secondary range | MPSO objects | Secondary count |
|---|---|---|---|---|---|
| CATACOMB | 591 | 0–500 | 501–590 | 501 | 90 |
| CAVERNS | 233 | 0–203 | 204–232 | 204 | 29 |
| DRINT | 284 | 0–257 | 258–283 | 258 | 26 |
| ISLAND | 1,704 | 0–1,690 | 1,691–1,703 | 1,691 | 13 |
| PALACE | 289 | 0–262 | 263–288 | 263 | 26 |
Visibility lookups multiply the MPSO index by the 66-byte (0x42) record size.
END Section — Footer
8 bytes: END (4 ASCII bytes) followed by 0x00000000 (4 zero bytes). Data length is 0.
Generation Process
PVO files are generated by iterating a uniform 3D grid over the world bounding box:
- Compute world bounding box from level geometry.
- Iterate a uniform 3D grid at 256-unit spacing.
- At each grid point, run a visibility query to determine which polygons are visible.
- Insert the visible polygon set as a leaf into the octree.
- Prune single-child branches, then write the file.
Debug console commands
| Command | Description |
|---|---|
pvoi / pvotreeinfo | Display PVO tree statistics |
pvoa / pvoaddpatch | Add object to PVO visibility patch |
pvod / pvodeletepatch | Remove object from PVO patch |
pvoonoff | Toggle PVO visibility system on/off |
pvos / pvopatchsave | Save PVO patches to PTCH section |
pvol / pvotreeload | Load PVO tree from file |
Secondary Visibility Table
MLST indices >= MPSO_count reference a secondary pointer table built at runtime. This table is populated by iterating the placed object list and collecting objects whose visibility flag (offset +0x7a in the runtime object struct) is non-zero.
The visibility flag is set by SOUP script functions during object initialization. Objects that receive this flag — such as dynamically loaded static models — become trackable by the PVO system alongside the primary MPSO-based objects.
| Step | Description |
|---|---|
| 1 | Count placed objects with visibility flag set → secondary_count |
| 2 | Allocate secondary_count × 4 bytes for pointer array |
| 3 | Iterate placed objects; for each with flag +0x7a != 0, append pointer to array |
| 4 | At runtime, MLST index - MPSO_count indexes into this array |
External References
- UESP: Mod:Redguard File Formats
- UESP: User:Daveh/Redguard File Formats
- RGUnity/redguard-unity
RGFileImport/RGGFXImport/
PVO Parser — Pseudocode
Reference parser for the PVO (Pre-computed Visibility Octree) file format. See PVO Format for format specification.
Data Types
u8 — unsigned 8-bit
u16_le — unsigned 16-bit, little-endian
u32_le — unsigned 32-bit, little-endian
i32_le — signed 32-bit, little-endian
u32_be — unsigned 32-bit, big-endian
tag — 4 ASCII bytes (e.g. "OCTH")
Structures
struct PvoFile {
header: OcthHeader,
nodes: Vec<OctrNode>, // OCTR section, indexed by byte offset
leaves: Vec<PlstLeaf>, // PLST section, indexed by byte offset
mlst: Vec<u16_le>, // MLST section, flat polygon index table
}
struct OcthHeader {
depth: u32, // always 10 — max octree depth
total_nodes: u32, // len(nodes)
leaf_nodes: u32, // nodes with leaf_ref != 0xFFFFFFFF
mlst_polygon_count: u32, // len(mlst)
reserved: u32, // always 0
cell_size: u32, // root half-extent (16384 or 8192)
center_x: i32, // octree root center
center_y: i32,
center_z: i32,
_pad: [u32; 4], // always zero
}
struct OctrNode {
byte_offset: usize, // position within OCTR data (for child_ref lookups)
child_mask: u8, // bit i set → child i present (octants 0..7)
leaf_ref: u32, // byte offset into PLST, or 0xFFFFFFFF (no leaf)
child_refs: Vec<(u8, u32)>, // (octant_index, byte_offset into OCTR) per set bit
}
struct PlstLeaf {
byte_offset: usize, // position within PLST data (for leaf_ref lookups)
entries: Vec<PlstEntry>,
}
struct PlstEntry {
count: u16, // number of polygon indices in this sub-list
mlst_start: u32, // starting index into the MLST array
}
Section Framing
Every section uses identical framing. Parse sections sequentially until END .
fn read_section_header(reader) -> (tag: [u8; 4], data_length: u32):
tag = reader.read_bytes(4)
data_length = reader.read_u32_be()
return (tag, data_length)
Top-Level Parser
fn parse_pvo(reader) -> PvoFile:
// --- OCTH ---
(tag, data_length) = read_section_header(reader)
assert tag == "OCTH"
assert data_length == 52
header = parse_octh(reader)
// --- OCTR ---
(tag, data_length) = read_section_header(reader)
assert tag == "OCTR"
nodes = parse_octr(reader, data_length)
// --- PLST ---
(tag, data_length) = read_section_header(reader)
assert tag == "PLST"
leaves = parse_plst(reader, data_length)
// --- MLST ---
(tag, data_length) = read_section_header(reader)
assert tag == "MLST"
assert data_length == header.mlst_polygon_count * 2
mlst = parse_mlst(reader, data_length)
// --- END ---
(tag, data_length) = read_section_header(reader)
assert tag == "END "
assert data_length == 0
// --- Validation ---
assert len(nodes) == header.total_nodes
leaf_count = count(n for n in nodes if n.leaf_ref != 0xFFFFFFFF)
assert leaf_count == header.leaf_nodes
return PvoFile { header, nodes, leaves, mlst }
OCTH Parser
fn parse_octh(reader) -> OcthHeader:
header = OcthHeader {
depth: reader.read_u32_le(), // 0x08
total_nodes: reader.read_u32_le(), // 0x0C
leaf_nodes: reader.read_u32_le(), // 0x10
mlst_polygon_count: reader.read_u32_le(), // 0x14
reserved: reader.read_u32_le(), // 0x18 — always 0
cell_size: reader.read_u32_le(), // 0x1C
center_x: reader.read_i32_le(), // 0x20
center_y: reader.read_i32_le(), // 0x24
center_z: reader.read_i32_le(), // 0x28
_pad: reader.read_bytes(16), // 0x2C — always zero
}
return header
OCTR Parser
Octree nodes are serialized as a flat sequence of variable-length records. Each record describes one octree node. The records are addressed by byte offset within the OCTR data section.
fn parse_octr(reader, data_length: u32) -> Vec<OctrNode>:
nodes = []
bytes_read = 0
while bytes_read < data_length:
node_offset = bytes_read
child_mask = reader.read_u8()
leaf_ref = reader.read_u32_le()
bytes_read += 5
// Read one child_ref per set bit in child_mask (low bit first)
child_refs = []
for octant in 0..8:
if child_mask & (1 << octant) != 0:
ref = reader.read_u32_le()
child_refs.push((octant, ref))
bytes_read += 4
nodes.push(OctrNode {
byte_offset: node_offset,
child_mask,
leaf_ref,
child_refs,
})
assert bytes_read == data_length
return nodes
Node record binary layout
Byte 0 Bytes 1..4 Bytes 5..5+4n
┌──────────┬─────────────────┬────────────────────────────────┐
│child_mask│ leaf_ref │ child_ref[0] .. child_ref[n-1] │
│ (u8) │ (u32_le) │ (u32_le each) │
└──────────┴─────────────────┴────────────────────────────────┘
n = popcount(child_mask)
record_size = 5 + 4n
Octant numbering
The 3-bit octant index encodes spatial position relative to the node center:
bit 0 (1) = z > center_z
bit 1 (2) = y > center_y
bit 2 (4) = x > center_x
Octant 0 = (x-, y-, z-) Octant 4 = (x+, y-, z-)
Octant 1 = (x-, y-, z+) Octant 5 = (x+, y-, z+)
Octant 2 = (x-, y+, z-) Octant 6 = (x+, y+, z-)
Octant 3 = (x-, y+, z+) Octant 7 = (x+, y+, z+)
Interpreting references
leaf_ref: byte offset into the PLST section data.0xFFFFFFFF= no leaf (interior-only node).child_refs: byte offset into the OCTR section data. Use to look up child nodes by theirbyte_offsetfield.
Each child_ref names a node byte_offset. Each leaf_ref falls within the
PLST section 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
External References
Software texture banks
TEXTURE.### files store indexed textures for the software renderer.
Overall Structure
[Header — entry count and bank name]
[Directory — 20-byte records]
[Image records — metadata and pixels]
The engine selects banks by a three-digit decimal suffix. Values are little-endian. Colors come from the world’s COL palette.
Header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | u16 | entry_count | Number of directory entries |
| 0x02 | 24 | [u8; 24] | name | NUL-terminated bank name, padded to 24 bytes |
Directory
Each entry occupies 20 bytes, beginning at byte 26.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 1 | u8 | unknown_00 | Auxiliary field |
| 0x01 | 1 | u8 | color | Palette index when the image offset is zero |
| 0x02 | 4 | u32 | image_offset | Absolute file offset of the image record; zero for solid colors |
| 0x06 | 14 | [u8; 14] | auxiliary | Auxiliary fields |
Banks 0 and 1 contain solid-color entries for the two halves of the palette. Image IDs are directory indices.
Image record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | i16 | x_offset | Image X offset |
| 0x02 | 2 | i16 | y_offset | Image Y offset |
| 0x04 | 2 | u16 | width | Image width |
| 0x06 | 2 | u16 | height | Image height |
| 0x08 | 2 | u16 | flags | Bit 8 enables transparent palette index zero; bit 11 selects a 256-byte row stride for single-frame data |
| 0x0A | 4 | u32 | record_size | Record size field |
| 0x0E | 4 | u32 | data_offset | Pixel data offset relative to the image record |
| 0x12 | 2 | u16 | unknown_12 | Auxiliary field |
| 0x14 | 2 | u16 | frame_count | Number of frames; zero represents an empty record |
| 0x16 | 2 | u16 | frame_delay | Animation frame duration in milliseconds |
| 0x18 | 2 | i16 | scale_x | Stored X scale field |
| 0x1A | 2 | i16 | scale_y | Stored Y scale field |
Data offsets may address shared pixel storage beyond the record-size field. Resolve them against the complete bank.
Pixel data
Single-frame images contain raw palette indices. Rows have a stride of 256 bytes when flag bit 11 is set, otherwise the image width. Only width pixels from each row belong to the image.
Animated images begin with frame_count little-endian u32 offsets, each relative to the start of the pixel data. Each frame begins with its u16 width and height, followed by rows of run pairs:
[Transparent count — u8]
[Literal count — u8]
[Literal palette indices — literal count bytes]
Repeat pairs until exactly the frame width has been emitted, then start the next row. Transparent runs emit index zero. A pair must advance the row and must not overrun its width.
Software models select these banks through their v2.6/v2.7 texture fields. The layout differs from TEXBSI.
External References
- Daggerfall Unity TextureFile.cs: related XnGine texture archive structures.
Software palette lookup tables
SHADE.### and HAZE.### files contain palette-index lookup tables for the software renderer.
Overall Structure
[Lookup rows — 64 consecutive rows of 256 palette indices]
Each file contains exactly 16,384 bytes, with no header or embedded palette. The entry for row r and input palette index i is at byte r * 256 + i. Its value is an output index into the world’s COL palette. Preserve all rows and indices when importing a table.
These tables accompany the software TEXTURE banks.
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.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 9 | [u8; 9] | image_name | Image name, null-padded. Format: {type_char}{file_num:02d}{image_idx:03d}. All-zero = end of file. |
| 9 | 4 | u32 | subrecord_bytes | Total size of all subrecords that follow (excludes this 13-byte envelope). |
| 13 | … | — | subrecords | Tagged subrecords until END . |
Image Name Encoding
The 9-byte name encodes the file number and sub-image index:
"E01005\0\0\0" → type 'E', file 01, image index 005
"A02003\0\0\0" → type 'A', file 02, image index 003
The image index (last 3 digits) is how 3D model faces reference sub-images via image_id.
Filename/name coupling across shipped TEXBSI.### files:
### % 100matches the 2-digit file number in image names.### / 100maps to type-char family:0->A,1->B,2->C,3->D,4->E,5->F.
Examples:
TEXBSI.302containsD02xxximages.TEXBSI.114containsB14xxximages.
Type characters range from A through F.
The type character has no semantic meaning — it is a deterministic artifact of the base-40 name encoding used internally. The game converts numeric texture IDs to 6-character strings using the alphabet 0123456789abcdefghijklmnopqrstuvwxyz~_#%, then shifts the first character by subtracting 0x31. The type letter is simply which base-40 digit range the texture ID falls into (A=digit 27, B=28, …, F=32). The game never tests or filters by the type character — it round-trips through the numeric ID.
Subrecord Structure
Every subrecord has an 8-byte header:
Subrecord size words are big-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 4 | [u8; 4] | tag | Tag: BSIF, IFHD, BHDR, CMAP, DATA, or END |
| 4 | 4 | u32 | payload_size | Payload size in bytes (not including this 8-byte header) |
| 8 | … | — | payload | Tag-specific data |
Subrecords always appear in this order:
BSIF or IFHD (mutually exclusive)
BHDR (required)
CMAP (optional, only with IFHD)
DATA (required)
END (terminates the record — no size field)
Subrecord Payloads
BSIF — Static Image Marker
Payload size: 0 bytes (empty). Marks a static (non-animated) image.
IFHD — Animated Image Marker
Payload size: 44 bytes (always 01 followed by 43 00 bytes). Marks an animated image.
When IFHD is present, the DATA subrecord uses the animated offset-table format.
BHDR — Image Header (26 bytes)
All fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 2 | i16 | x_offset | X position hint (placement on virtual canvas) |
| 2 | 2 | i16 | y_offset | Y position hint |
| 4 | 2 | i16 | width | Image width in pixels |
| 6 | 2 | i16 | height | Image height in pixels |
| 8 | 1 | u8 | has_cmap | Export-tool flag, not read at runtime. Set to 1 when the image has an embedded CMAP palette (always co-occurs with IFHD animated images). Per UESP: “images that have 1 are all animated effects such as fire and water.” |
| 9 | 1 | u8 | export_flags | Export-tool metadata. Values: 0, 1, or 9. Packed into the same u16 as has_cmap during export. |
| 10 | 4 | — | reserved | Always 0 |
| 14 | 2 | i16 | frame_count | 1 = static, 2–16 = animated |
| 16 | 2 | i16 | anim_delay | Read at runtime. Animation frame duration in milliseconds. Converted to DOS PIT timer ticks via trunc(anim_delay × 18.2 / 1000); clamped to minimum 1. Typical value 71 → 1 tick (~55 ms); value 500 → 9 ticks (~495 ms). Range: 0–500. |
| 18 | 4 | — | reserved | Always 0 |
| 22 | 2 | u16 | tex_scale | 8.8 fixed-point scale, tex_scale / 256.0; zero defaults to 0x0100 (1.0). Model UV normalization uses 16 × texture_dimension independently of this field. |
| 24 | 2 | i16 | data_encoding | Pixel data encoding mode. Selects which compression method the DATA subrecord uses. Known values: 0 = raw uncompressed, 4 = animated offset table. Values 1–3 are engine-supported but unused. |
CMAP — Embedded Palette (768 bytes)
256 × RGB triplets (3 bytes each, values 0–255). Same layout as COL files.
Optional — always co-occurs with IFHD (animated images). When absent, the image uses an external .COL palette file.
DATA — Pixel Data
Static images (BSIF present, frame_count == 1):
Payload is width × height bytes of 8-bit indexed color, row-major, top-to-bottom. Each byte is a palette index (0–255). Index 0 = transparent.
Animated images (IFHD present, frame_count > 1):
Payload starts with an offset table of height × frame_count LE u32 entries. Each entry is a byte offset from the start of the DATA payload to the first byte of that row. Rows can be shared across frames (identical rows point to the same data).
offset_table[frame * height + row] → byte offset to row data (width bytes)
END — Record Terminator
4-byte tag "END " (with trailing space). No size field. Followed by 4 zero bytes.
Pixel Decoding
for each pixel byte:
if byte == 0: → transparent (alpha = 0)
else: → palette[byte] as RGB (alpha = 255)
Palette values are raw 8-bit RGB (0–255). No gamma correction needed.
Palette Selection
When decoding pixel data, palette lookup order is:
- External
.COLpalette — the scene-level palette file passed to the converter (e.g.ISLAND.COL). Which COL file to use is determined per-scene by the game engine. - Embedded
CMAP— the 256-color palette stored inside the image record (only present in animatedIFHDimages). - Grayscale fallback — if neither is available, each index maps to a gray value.
How 3D Models Reference Textures
Each face in a 3D model encodes a texture_id and image_id:
texture_id→ selects the TEXBSI.### file numberimage_id→ selects the sub-image within that file (0-indexed, matching the 3-digit suffix in the image name)
See 3D.md — Texture Decoding for the BCD encoding.
External References
- UESP: Mod:TEXBSI File Format — community byte-layout notes for TEXBSI records and subrecords.
- uesp/redguard-3dfiletest
RedguardTexBsiFile.cpp - RGUnity/redguard-unity
RGTEXBSIFile.cs - RGUnity/redguard-unity
RGBSIFile.cs
BSI texture file format
Standalone image chunks used by named textures such as sky and sun images.
Overall structure
Unlike a TEXBSI archive, a standalone BSI has no 9-byte image name or record-size envelope. It begins directly with tagged chunks:
[IFHD or BSIF — image marker]
[BHDR — image header]
[CMAP — optional embedded palette]
[DATA — indexed pixels or animation rows]
[END — terminator]
Chunk headers and BHDR fields follow TEXBSI.
IFHD alone does not imply multiple frames. The four installed
sky textures have IFHD, a frame count of 1, and raw 256 by 256 indexed pixels.
The Glide sky uses the matching external COL palette, including index 0 as
an opaque color. General sprite decoding treats index 0 as transparent.
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
Populated COL files are exactly 776 bytes. fxart/CLDTEST.COL is an empty
placeholder, not a valid palette.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | file_size | Always 776. |
| 0x04 | 4 | u32 | magic | Always 0x0000B123. |
| 0x08 | 768 | [u8; 768] | palette | 256 × RGB triplets (3 bytes each, values 0–255). |
Palette entry N is at offset 8 + N × 3, yielding bytes (R, G, B).
Usage
COL files are per-scene, not per-model. Different levels use different palettes (e.g. ISLAND.COL, CATACOMB.COL). The same color_index in a model’s face data produces different colors depending on which palette is active.
Entry zero can be nonblack. The world palette upload forces its displayed RGB to black.
Glide world palette conversion
The world loader converts each channel of a populated COL file to six bits by
shifting it right two bits. The world palette upload shifts these values left
two bits to form the RGB texture table and forces entry zero to black. Thus an
unmodified world palette channel is 4 * floor(channel / 4), with a maximum
of 252. Texture filtering operates on these uploaded colors, before lighting
and framebuffer conversion.
This conversion belongs to the world palette loading and upload path. The renderer also has direct RGB palette uploads, which preserve their input bytes; it is not a conversion inherent to the COL file format or to every texture. Decoders and generic exports retain the original eight-bit file values.
Related Formats
- TEXBSI — the
CMAPsubrecord in animated TEXBSI images uses the identical 256 × RGB triplet layout (768 bytes). CMAP palettes are embedded per-image; COL palettes are per-scene. - FNT — font files embed their own
BPAL/FPALpalettes (same 768-byte layout), independent of scene COL palettes.
External References
- UESP: Mod:Palette Files — dedicated Redguard COL page with the 8-byte header + 768-byte palette layout.
- UESP: User:Daveh/Redguard File Formats — COL size and palette usage notes.
FNT Bitmap Font File Format
Chunked bitmap-font format with per-file palette and per-glyph indexed image data.
.FNT stores UI/dialog font glyphs as palette-indexed bitmaps. Each file embeds its own palette (not scene COL palettes), then stores glyph records in ASCII order. For palette structure background, see COL.md.
Top-Level Layout
The file is a sequence of named chunks, followed by an end marker:
FNHD(always present)BPALorFPAL(always present)FBMP(always present)RDAT(optional)ENDmarker
Common chunk orders:
FNHD -> BPAL -> FBMP -> ENDFNHD -> BPAL -> FBMP -> RDAT -> ENDFNHD -> FPAL -> FBMP -> END(ARIALVS.FNTonly)
Chunk Header
Each chunk begins with an 8-byte header:
Chunk-length fields are big-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | tag | Chunk name (FNHD, BPAL, FPAL, FBMP, RDAT) |
| 0x04 | 4 | u32 | length | Chunk payload size in bytes |
END is a 4-byte marker tag with no payload. ARIALVS.FNT has 4 additional trailing zero bytes after END .
FNHD Chunk (56 bytes)
FNHD payload is always 56 bytes.
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 32 | [u8; 32] | description | Font/tool description string; may contain NUL padding or multiple NUL-terminated fragments |
| 0x20 | 2 | u16 | unknown_24 | Not read at runtime — overwritten during glyph loading. Known values: 0, 1, 3. Export-tool metadata. |
| 0x22 | 2 | u16 | has_rdat | 1 if RDAT chunk present; 0 otherwise. Not checked at runtime — the engine never searches for RDAT. |
| 0x24 | 2 | u16 | reserved_28 | Always 0. Not read at runtime. |
| 0x26 | 2 | u16 | reserved_2a | Always 0. Not read at runtime. |
| 0x28 | 2 | u16 | reserved_2c | Always 0. Not read at runtime. |
| 0x2A | 2 | u16 | max_width | Export-tool hint (range 11–23). Not read at runtime — overwritten with the width of glyph ‘W’ during loading. |
| 0x2C | 2 | u16 | line_height | Used by the engine for text layout and baseline positioning. Values: 9, 10, 12, 14, 16, 22, 25, 26. |
| 0x2E | 2 | u16 | character_start | First encoded codepoint; always 32 (0x20, space). Not read at runtime — engine assumes fixed start. |
| 0x30 | 2 | u16 | character_count | Used by the engine as the glyph loop bound (clamped to 256). Number of glyph records in FBMP. Values: 95, 97, 98, 104, 112, 208. |
| 0x32 | 2 | u16 | reserved_36 | Always 0. Overwritten to 0xFF during glyph loading. |
| 0x34 | 2 | u16 | reserved_38 | Always 0. Overwritten to 0xFF during glyph loading. |
| 0x36 | 2 | u16 | has_palette | Used by the engine to control palette loading. Non-zero = allocate 768-byte palette and search for FPAL/BPAL chunk. Always 1. |
BPAL / FPAL Chunk (Palette)
Palette payload is always 768 bytes (256 RGB triplets).
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 768 | [u8; 768] | rgb_triplets | 256 entries x 3 bytes (R, G, B), palette indices referenced by FBMP pixel bytes |
Notes:
BPALis the normal tag.FPALappears inARIALVS.FNTwith the same 768-byte payload shape.- These palettes are local to each font file, independent of scene palettes in COL.md.
- Components are 6-bit (0–63). Expanding a file palette component to eight bits
uses
c << 2. The palette selected for a Glide glyph upload can instead come from shared renderer state; cinematic glyphs do not necessarily use BPAL colours. See cinematic playback.
FBMP Chunk (Glyph Records)
FBMP payload contains character_count glyph records in sequential codepoint order starting at character_start.
Each glyph record:
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | u16 | enabled | 0 = disabled/unrendered glyph; non-zero = active glyph |
| 0x02 | 2 | i16 | offset_left | Horizontal draw offset in pixels |
| 0x04 | 2 | i16 | offset_top | Vertical draw offset in pixels |
| 0x06 | 2 | u16 | width | Glyph bitmap width in pixels |
| 0x08 | 2 | u16 | height | Glyph bitmap height in pixels |
| 0x0A | width*height | [u8] | pixels | Row-major palette indices |
Value ranges:
offset_left: 0..5offset_top: 0..19width: 1..22height: 1..25
FBMP payload length equals the sum of all glyph record sizes (10-byte header + width * height pixels each).
RDAT Chunk (Optional, 173 bytes)
RDAT is optional and always 173 bytes when present.
Layout (partially decoded):
Numeric fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 136 | [u8; 136] | source_name | NUL-padded source/tool string |
| 0x88 | 4 | u32 | unknown_90 | Non-zero metadata field |
| 0x8C | 4 | u32 | unknown_94 | Non-zero metadata field |
| 0x90 | 4 | u32 | unknown_98 | Always 0 |
| 0x94 | 4 | u32 | unknown_9c | Usually 0; value 2 in ARIALBG.FNT |
| 0x98 | 4 | u32 | unknown_a0 | Unknown field |
| 0x9C | 4 | u32 | unknown_a4 | Unknown field |
| 0xA0 | 4 | u32 | unknown_a8 | Values 1 through 3 |
| 0xA4 | 4 | u32 | unknown_ac | Values 1 through 2 |
| 0xA8 | 4 | u32 | unknown_b0 | Always 0 |
| 0xAC | 1 | u8 | unknown_b4 | Always 0 |
RDAT is metadata. The font loader uses FNHD, FPAL/BPAL, and FBMP; it does not parse RDAT payload data.
External References
- UESP: Mod:Font Files — primary external reference for
FNHD/BPAL/FBMP/RDATchunk semantics. - UESP: Redguard:Glide Differences — renderer-specific font usage differences (non-structure behavior).
- UESP: Mod:RedguardFNTImporter — FNT import/export tooling notes.
GXA bitmap archives
GXA files store indexed bitmap frames for UI and sky textures.
system/pointers.bmp also uses the GXA container and contains 30 cursor
frames. Its first chunk is BMHD, despite the filename’s .bmp extension.
The cursor sprite sheet system/pointers.bmp also uses this chunked format.
Its .bmp extension does not describe a Windows bitmap.
In ordinary play the shipped game draws no cursor: with the pointer moved across the game window during gameplay at several positions, the framebuffer shows no sprite at the pointer. The archive is used only by the interactive editor modes (object select, scene, fxselect and pvoaddpatch), which the shipped game does not enter.
Overall Structure
[Section header — 4-byte tag + BE u32 payload length]
[Section payload — tag-specific]
...
[END section — tag `END ` with no payload length field]
Sections are chunked and use this order in the shipped files:
[BMHD — bitmap header (title + frame count)]
[BPAL — 256-color RGB palette]
[BBMP — bitmap frame records]
[END — terminator]
Section Header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | tag | Chunk name (BMHD, BPAL, BBMP, END ). |
| 0x04 | 4 | u32 | data_length_be | Big-endian payload size. Not present for END . |
BMHD Section
The shipped .gxa files use the title GXlib image conversion.
The cursor archive pointers.bmp has an empty title.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 22 | [u8; 22] | title | Null-terminated title/label. |
| 0x16 | 10 | [u8; 10] | unknown_16 | Unknown header bytes. |
| 0x20 | 2 | i16 | num_images | Number of bitmap frames in BBMP. |
BPAL Section
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 768 | [u8; 768] | colors | 256 palette entries, each RGB triplet (r,g,b). Values are 6-bit VGA (0–63); shift left by 2 for 8-bit RGB. |
BBMP Section
BBMP stores num_images frame records back-to-back. The GOG release contains 65 GXA files with 450 total frames: 320 raw, 128 RLE-compressed, 2 LZHUF-compressed.
Frame Record Header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 2 | i16 | unknown_00 | Unknown frame field. |
| 0x02 | 2 | i16 | width | Frame width in pixels. |
| 0x04 | 2 | i16 | height | Frame height in pixels. |
| 0x06 | 2 | i16 | offset_x | Draw offset added to the anchor X when the frame is drawn as a HUD bitmap. |
| 0x08 | 2 | i16 | offset_y | Draw offset added to the anchor Y when the frame is drawn as a HUD bitmap. |
| 0x0A | 2 | i16 | compression | Compression type (0 = raw, 1 = RLE, 2 = LZHUF). |
| 0x0C | 6 | [i16; 3] | unknown_0c | Three unknown frame fields. |
offset_x and offset_y are zero in shipped files. The candle, active-item and compass HUD elements add them to their anchor; the power-up slots and the logbook icon ignore them. See In-game HUD.
Pixel Decode Rules
- Palette index
0is transparent (RGBA = 0,0,0,0). - Non-zero indices map to
BPALRGB (after 6-bit → 8-bit expansion) and use alpha255. - Rows are stored top-to-bottom (no Y-flip needed).
Compression Type 0 - Raw
Pixel data is width * height uncompressed bytes starting at offset 0x12.
Compression Type 1 - RLE
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x12 | 4 | u32 | compressed_size | Size of RLE-compressed pixel data. |
| 0x16 | compressed_size | [u8; N] | compressed_data | RLE-encoded pixel stream. |
RLE control byte behavior:
0x80..=0xFF: repeat the next byte(ctrl & 0x7F) + 1times.0x00..=0x7F: copy the nextctrl + 1literal bytes.
Compression Type 2 - LZHUF
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x12 | 4 | u32 | compressed_size | Size of LZHUF-compressed pixel data. |
| 0x16 | 4 | u32 | uncompressed_size | Expected decompressed size (width * height). |
| 0x1A | compressed_size | [u8; N] | compressed_data | LZHUF bitstream (Okumura LZSS + adaptive Huffman, 4 KB window). |
LZHUF uses a 4096-byte sliding window initialized to 0x20, with adaptive Huffman coding for both character and position codes. This matches the classic Okumura lzhuf.c algorithm, equivalent to LHA method lh1.
Section Length Quirk
Some files store BBMP data_length_be as file_size - 4 instead of the actual BBMP payload size. Parsers should clamp the BBMP section length to available data.
External References
PCX images
PCX files store palette-indexed raster images with scanline compression.
SKY_61.PCX is an 8-bit, single-plane image with a trailing 256-color palette.
Overall Structure
[PCX header — bounds, color depth, encoding, and scanline stride]
[Image data — raw or run-length encoded scanlines]
[Palette marker — 0C]
[Palette — 256 RGB triples]
Multibyte header integers are little-endian.
Header
The header occupies 128 bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 1 | u8 | manufacturer | 0x0A |
0x01 | 1 | u8 | version | Format revision; 5 in SKY_61.PCX |
0x02 | 1 | u8 | encoding | 0 for raw bytes, 1 for RLE |
0x03 | 1 | u8 | bits_per_pixel | Bits per pixel per plane; 8 |
0x04 | 2 | u16 | x_min | Left bound |
0x06 | 2 | u16 | y_min | Top bound |
0x08 | 2 | u16 | x_max | Inclusive right bound |
0x0A | 2 | u16 | y_max | Inclusive bottom bound |
0x0C | 2 | u16 | horizontal_dpi | Horizontal resolution |
0x0E | 2 | u16 | vertical_dpi | Vertical resolution |
0x10 | 48 | [u8; 48] | header_palette | Legacy 16-color palette |
0x40 | 1 | u8 | reserved | Reserved byte |
0x41 | 1 | u8 | planes | Number of color planes; 1 |
0x42 | 2 | u16 | bytes_per_line | Decoded stride for each plane |
0x44 | 2 | u16 | palette_info | Palette interpretation field |
0x46 | 2 | u16 | horizontal_screen_size | Screen width field |
0x48 | 2 | u16 | vertical_screen_size | Screen height field |
0x4A | 54 | [u8; 54] | reserved_tail | Reserved bytes |
Width is x_max - x_min + 1; height is y_max - y_min + 1.
Each decoded scanline contains bytes_per_line bytes. Bytes beyond the visible
width are padding and do not produce pixels.
Run-length encoding
A byte below 0xC0 is a literal pixel index. A byte with both high bits set
encodes a repeat count in its low six bits; the following byte is the repeated
index. Counts range from 1 through 63. A literal index of 0xC0 or above uses
a one-byte repeat. Runs end within the decoded scanline boundary.
Palette
The final 769 bytes contain marker 0x0C and 256 three-byte RGB entries.
Components use their full eight-bit range. Pixel indices select these colors;
index zero is opaque. The legacy header palette does not supply the colors
for this layout. See also COL palettes.
External References
LBM packed bitmaps
LBM files store IFF bitmap forms containing a header, palette, and pixel data.
gui.lbm uses the PBM form type. Its pixels are packed bytes rather than
the separate bitplanes used by ILBM.
Overall Structure
[FORM header — container size and PBM form type]
[BMHD chunk — dimensions and pixel encoding]
[CMAP chunk — RGB palette]
[Additional chunks — application settings, color ranges, and thumbnail]
[BODY chunk — scanline pixel data]
All multibyte integers are big-endian.
IFF container
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 4 | [u8; 4] | tag | FORM |
0x04 | 4 | u32 | size | Bytes following this size field |
0x08 | 4 | [u8; 4] | form_type | PBM , including the trailing space |
Each child chunk begins with a four-byte tag and a big-endian u32 payload
size. Odd-sized payloads have one padding byte. The size excludes this padding.
Unknown chunks remain independent of pixel decoding.
BMHD bitmap header
The header payload is 20 bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 2 | u16 | width | Visible pixel width |
+0x02 | 2 | u16 | height | Visible pixel height |
+0x04 | 2 | i16 | x | Authored X position |
+0x06 | 2 | i16 | y | Authored Y position |
+0x08 | 1 | u8 | depth | Bits per pixel; 8 |
+0x09 | 1 | u8 | masking | 0 for opaque pixels, 2 for a transparent palette index |
+0x0A | 1 | u8 | compression | 0 for raw rows, 1 for ByteRun1 |
+0x0B | 1 | u8 | reserved | Reserved byte |
+0x0C | 2 | u16 | transparent_color | Transparent index when masking is 2 |
+0x0E | 1 | u8 | x_aspect | Horizontal pixel aspect |
+0x0F | 1 | u8 | y_aspect | Vertical pixel aspect |
+0x10 | 2 | i16 | page_width | Authored page width |
+0x12 | 2 | i16 | page_height | Authored page height |
Palette and pixel data
CMAP contains three-byte RGB entries. Each component uses its full eight-bit
range. BODY stores rows from top to bottom. Each row is padded to an even
byte count; padding is excluded from the visible image.
ByteRun1 interprets each control byte as signed:
| Control | Operation |
|---|---|
| 0 through 127 | Copy the next control + 1 bytes |
| -127 through -1 | Repeat the next byte 1 - control times |
| -128 | No operation |
Each row decodes independently. With masking zero, palette index zero is opaque.
Additional chunks
gui.lbm also contains DPPS, CRNG, and TINY chunks. They do not change the
stored BODY pixels. A CRNG payload is eight bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 2 | u16 | reserved | Reserved word |
+0x02 | 2 | u16 | rate | Color-cycle rate |
+0x04 | 2 | u16 | flags | Color-cycle flags |
+0x06 | 1 | u8 | low | First palette index in the range |
+0x07 | 1 | u8 | high | Last palette index in the range |
gui.lbm has sixteen CRNG chunks. Four are populated with zero rate and zero
flags and the ranges 0x10-0x1f, 0x20-0x2f, 0x60-0x6f and
0x90-0x9f; the other twelve are all zero. No installed LBM or
ANM defines an active palette cycle.
The static image uses the stored palette. See also PCX images and COL palettes.
GUI artwork
system/gui.lbm is a 640 by 480 sheet containing parchment button designs in
multiple sizes and states, along with parchment background artwork.
system/gui.anm packages variants of these GUI
graphics as separate 320 by 200 LPF images. The two files have different
embedded palettes; compare decoded RGB artwork rather than palette indices.
External References
ANM animations
ANM files store Deluxe Paint animations as indexed bitmap deltas in LPF pages.
This is the LPF animation format, distinct from IFF ANIM containers.
See LBM for IFF bitmaps.
Overall Structure
[LPF header — page counts, dimensions, encoding, and frame rate]
[Color-cycle records — authored palette ranges]
[Palette — 256 BGRX entries]
[Page table — 256 page descriptors]
[Large pages — record sizes and RunSkipDump bitmap records]
All multibyte integers are little-endian.
Header and palette
The header occupies 128 bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 4 | [u8; 4] | signature | LPF |
0x04 | 2 | u16 | max_pages | Page-table capacity, 256 |
0x06 | 2 | u16 | page_count | Number of active pages |
0x08 | 4 | u32 | record_count | Total records, including the loop delta |
0x0C | 2 | u16 | max_records_per_page | Authored record capacity |
0x0E | 2 | u16 | page_table_offset | Absolute table position |
0x10 | 4 | [u8; 4] | content_type | ANIM |
0x14 | 2 | u16 | width | Pixel width |
0x16 | 2 | u16 | height | Pixel height |
0x18 | 1 | u8 | variant | Bitmap variant, zero |
0x19 | 1 | u8 | version | Format version |
0x1A | 1 | u8 | has_last_delta | Final record is a loop delta |
0x1B | 1 | u8 | last_delta_valid | Loop delta validity flag |
0x1C | 1 | u8 | pixel_type | Indexed eight-bit pixels, zero |
0x1D | 1 | u8 | compression | RunSkipDump, 1 |
0x1E | 1 | u8 | other_records_per_frame | Additional record types, zero |
0x1F | 1 | u8 | bitmap_type | Single bitmap, 1 |
0x20 | 32 | [u8; 32] | record_types | Record-type fields |
0x40 | 4 | u32 | frame_count | Authored frame-count field |
0x44 | 2 | u16 | frames_per_second | Playback rate |
0x46 | 58 | [u8; 58] | reserved | Reserved header bytes |
Sixteen eight-byte color-cycle records follow at 0x80. The palette begins at
0x100, with 256 entries in blue, green, red, unused order. Pixel index zero
is opaque.
Every installed ANM except system/gui.anm has all sixteen records zero. In
gui.anm four records are populated, each with a zero rate and zero flags and
the palette ranges 0x10-0x1f, 0x20-0x2f, 0x60-0x6f and 0x90-0x9f
(low and high bytes at record offsets 6 and 7). Because rate and flags are zero,
no installed ANM defines an active palette cycle. The first six bytes of every
gui.anm cycle record are zero, including the four populated ranges. These are the same four ranges
as the CRNG chunks of gui.lbm.
Page table and large pages
Each page descriptor is six bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 2 | u16 | base_record | First record number in the page |
+0x02 | 2 | u16 | record_count | Low 14 bits contain the record count; high bits mark continuation records |
+0x04 | 2 | u16 | data_size | Total record payload bytes |
Page i begins at page_table_offset + 256 * 6 + i * 65536. Its header repeats
the descriptor, followed by a reserved word, one u16 size per record, and the
record payloads. The last physical page can end before its 64 KiB boundary.
Playback follows record numbers rather than physical page order.
Bitmap records
A bitmap record begins with byte 0x42 (B), a zero padding flag, and a
two-byte unknown_02 field. RunSkipDump operations follow. Each record starts
its output cursor at pixel zero; skipped pixels retain the preceding frame.
| Control | Operation |
|---|---|
0x01 through 0x7F | Copy that many following pixel indices |
0x00 | Read an eight-bit count and index; repeat the index |
0x81 through 0xFF | Skip the low-seven-bit count |
0x80 | Read a little-endian extended control word |
Extended words select these operations:
| Word | Operation |
|---|---|
0x0000 | End the record |
0x0001 through 0x3FFF | Skip that many pixels |
0x4001 through 0x7FFF | Skip the low-14-bit count plus 0x4000 pixels |
0x8001 through 0xBFFF | Copy the low-14-bit count of following indices |
0xC001 through 0xFFFF | Repeat the following index for the low-14-bit count |
0x4000, 0xC000 | No operation |
0x8000 is invalid. An eight-bit repeat count of zero is also a no-op, but
still consumes its index byte.
Playback and looping
The header supplies a frame rate and can identify a final loop delta. That record converts the last display frame back to the first and is excluded from the displayed image sequence. The authored frame-count field includes it.
GUI image collection
system/gui.anm uses the animation container to bundle separate GUI graphics.
Its 25 display images contain small, medium, and large parchment buttons in
different states, followed by a parchment background. Each image occupies a
320 by 200 canvas. These are selectable artwork variants, rather than a motion
sequence. The header declares 10 frames per second; this is container metadata,
not GUI interaction timing.
system/gui.lbm contains the same button designs arranged
on a 640 by 480 bitmap sheet. The files have different embedded palettes, so
matching artwork can use different palette indices.
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 — effect-bank header]
[FXDT — sequential effect records and PCM data]
[END — four-byte terminator]
Effects are stored sequentially with no offset table. The game references effects by their 0-based index in the file.
FXHD (Header Section)
44 bytes total. Section size word is big-endian; remaining fields are little-endian.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | tag | Always FXHD |
| 0x04 | 4 | u32 | BE | section_size | Payload size excluding this field and the tag |
| 0x08 | 32 | [u8; 32] | — | description | ASCII string, set by internal tool “SoupFX” |
| 0x28 | 4 | u32 | LE | effect_count | Number of sound effects (118 in MAIN.SFX) |
FXDT (Data Section)
Begins with a 4-byte ASCII tag FXDT, followed by a big-endian u32 section size (excluding itself and the tag), followed immediately by sequential effect records.
Effect Record
27-byte header followed by raw PCM audio data.
All fields little-endian unless noted.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | type_id | Audio type: 0 = 8-bit mono, 1 = 16-bit mono, 2 = 8-bit stereo (unused), 3 = 16-bit stereo |
| 0x04 | 4 | u32 | bit_depth | 0 = 8-bit, 1 = 16-bit |
| 0x08 | 4 | u32 | sample_rate | Always 11025 or 22050 Hz |
| 0x0C | 1 | u8 | base_volume | Base volume (Miles 0..127 scale) of every start: the caller’s volume is added and the sum clamped to 0..127. Positional callers pass the distance volume relative to this base; FlatSound(e, 0, 0) plays at the header base volume. See positional sound. |
| 0x0D | 1 | u8 | loop_flag | 0 = play once. Non-zero enables looping; 0xFF loops forever, any other value is passed as the Miles loop count (see Loop Behavior). |
| 0x0E | 4 | u32 | loop_offset | Byte offset into PCM data for loop restart point (always 0) |
| 0x12 | 4 | u32 | loop_end | Sample count before looping (always 0xFFFFFFFF) |
| 0x16 | 4 | u32 | data_length | Byte count of raw PCM data following this header |
| 0x1A | 1 | u8 | reserved_1a | Padding between header and PCM data. Always 0. |
| 0x1B | var | [u8] | pcm_data | Raw PCM audio: u8 samples for 8-bit, i16 LE samples for 16-bit |
Loop Behavior
The engine interprets loop_flag as follows:
loop_flag = 0: play once (non-looping effects)loop_flag = 0xFF: Miles loop count 0, i.e. loop until stopped (ambient loops like fire, water, wind)- any other non-zero value: passed unchanged as the Miles loop count. The only shipped case is effect 117 (the snake charmer tune), flag
0xE1, which plays 225 times rather than indefinitely.
When looping is enabled the engine also passes loop_offset and loop_end as the Miles loop block.
All shipped effects use loop_offset = 0 and loop_end = 0xFFFFFFFF.
Runtime Effect Structure
The engine allocates a 34-byte (0x22) runtime structure per effect, reading 26 bytes (0x00–0x19) from the file. The remaining 8 bytes are computed at runtime:
| Struct Offset | Size | Source | Contents |
|---|---|---|---|
| 0x00–0x19 | 26 | File | Header fields (type_id through data_length) |
| 0x1A–0x1D | 4 | Runtime | Pointer to allocated PCM data buffer |
| 0x1E–0x21 | 4 | Runtime | Computed duration value: (data_length << 8) / (sample_rate × bytes_per_sample × channels) |
Related Formats
- RTX — dialogue audio container. Uses the same 27-byte audio header structure (offsets 0x00–0x1A) as SFX effect records. SFX stores sound effects; RTX stores voice clips.
External References
RTX Dialogue Audio Format
Container format used for dialogue strings and audio assets (for example ENGLISH.RTX).
Overview
RTX stores two payload kinds under a common chunk/index system:
- String-only entries (ASCII text payload)
- Audio entries (string metadata + fixed audio header + audio bytes)
The file uses:
- per-chunk on-disk headers (
tag+ big-endian payload size) - a footer that points to a central index table
The values below describe ENGLISH.RTX.
File Layout
[chunk records ...]
[index table]
[footer]
Footer (12 bytes, file end)
Footer integers are little-endian.
| Offset (from EOF) | Size | Type | Name | Description |
|---|---|---|---|---|
| -12 | 4 | [u8; 4] | footer_tag | Always RNAV |
| -8 | 4 | u32 | index_offset | Absolute file offset of index table |
| -4 | 4 | u32 | index_count | Number of index entries |
For ENGLISH.RTX:
footer_tag = RNAVindex_offset = 184629473index_count = 4866
Chunk Record (on disk)
Every payload in the data region has an 8-byte chunk header before it:
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | tag | 4-character chunk label |
| 0x04 | 4 | u32 | BE | payload_size | Size of payload bytes that follow |
| 0x08 | var | [u8] | — | payload | Entry payload |
Index Table
The index is an array of 12-byte entries. Each entry points to one chunk payload.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 4 | [u8; 4] | — | tag | Same tag as the chunk header |
| 0x04 | 4 | u32 | LE | payload_offset | Absolute file offset of payload (not header) |
| 0x08 | 4 | u32 | LE | payload_size | Payload byte size |
Each index entry has these relationships to its chunk:
payload_offset - 8points to a chunk header with matchingtag- header
payload_size(big-endian) matches indexedpayload_size - all indexed ranges are in bounds (
offset + size <= file_size) - entries are ordered by descending payload offset
Payload Types
Payload type is identified by byte payload[1]:
0= string-only entry1= audio entry
String-Only Payload (payload[1] = 0)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 1 | u8 | — | kind | Always 0 |
| 0x01 | 1 | u8 | — | subtype | Always 0 for text entries |
| 0x02 | 2 | u16 | LE | string_len | Byte length of ASCII text |
| 0x04 | 2 | u16 | LE | reserved | Always 0 |
| 0x06 | var | [u8] | — | text | ASCII text bytes, no terminator |
Payload size rule:
payload_size = 6 + string_len
Audio Payload (payload[1] = 1)
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
| 0x00 | 1 | u8 | — | kind | Always 0 |
| 0x01 | 1 | u8 | — | subtype | Always 1 for voice entries |
| 0x02 | 2 | u16 | LE | string_len | Byte length of ASCII label |
| 0x04 | 2 | u16 | LE | reserved | Always 0 |
| 0x06 | var | [u8] | — | label | ASCII label bytes, no terminator |
| 0x06+N | 27 | struct | — | audio_header | Audio metadata (below), N = string_len |
| 0x21+N | var | [u8] | — | audio_data | Raw PCM audio bytes |
Audio payload size rule:
payload_size = 6 + string_len + 27 + audio_length
Audio Header (27 bytes)
All fields little-endian unless noted.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | type_id | 0 = 8-bit mono, 1 = 16-bit mono |
| 0x04 | 4 | u32 | bit_depth | 0 = 8-bit, 1 = 16-bit |
| 0x08 | 4 | u32 | sample_rate | 11025 or 22050 |
| 0x0C | 1 | u8 | level_0c | Always 100 |
| 0x0D | 1 | i8 | loop_flag | Always 0 |
| 0x0E | 4 | u32 | loop_offset | Always 0 |
| 0x12 | 4 | u32 | loop_end | Always 0xFFFFFFFF |
| 0x16 | 4 | u32 | audio_length | Byte length of audio_data |
| 0x1A | 1 | u8 | reserved_1a | Always 0 |
Notes
- Tags are 4-byte IDs and are not globally reused in
ENGLISH.RTX. - A few tags include punctuation (for example
#bon,?vql). - The index points to payload starts; on-disk chunk headers are always 8 bytes earlier.
Related Formats
- SFX — sound effects container. Uses the same 27-byte audio header structure (offsets 0x00–0x1A) as RTX audio entries. RTX stores voice clips; SFX stores sound effects.
External References
- RGUnity/redguard-unity
RGRTXFile.cs| RTX container reader - Dillonn241/redguard-mod-manager
RtxDatabase.java| RTX read/write - Dillonn241/redguard-mod-manager
RtxEntry.java| RTX entry structure and audio format definitions - UESP: Redguard Console (script command context)
SMK
Smacker files carry compressed indexed video, palette changes, and up to seven audio streams.
Overall structure
[Fixed header, 104 bytes]
[Frame size and stream flag tables]
[Huffman trees]
[Compressed frame and audio packets]
The fixed header uses little-endian fields.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 4 | [u8; 4] | signature | SMK2 or SMK4. |
| 4 | 4 | u32 | width | Source pixel width. |
| 8 | 4 | u32 | height | Source pixel height. |
| 12 | 4 | u32 | frames | Authored frame count, excluding an optional ring frame. |
| 16 | 4 | i32 | timing | Positive values are milliseconds; negative values are hundredths of a millisecond. |
| 20 | 4 | u32 | flags | Bit 0 adds a ring frame; bits 1 or 2 request doubled display height. |
INTRO.SMK has 640 by 240 source pixels, 6895 authored frames, timing -6666,
and doubled display height. Its playback rate is approximately 15.0015 frames
per second, with stereo 22050 Hz Smacker audio.
External references
SOUP386.DEF
Runtime text definition file that declares the SOUP scripting API surface (functions, references, attributes, and flags).
Overview
SOUP386.DEF is loaded by the runtime and used to build script-call metadata at startup. Compiled script bytecode in RASC/.AI refers to function and flag indices that are resolved using this definition file.
The file is plain text and organized as named sections.
Section Layout
| Section | Delimiter | Content |
|---|---|---|
| Functions | [functions] … [refs] | One callable entry per line: <type> <name> parms <count> where type is task, multitask, or function |
| References | [refs] … [equates] | One reference name per line |
| Equates | [equates] … [flags] | Named values and auto … endauto numbering blocks, including attributes and animation groups |
| Flags | [flags] … EOF | One flag per line: <type> <name> <value>[;<comment>] with types BOOL, NUMBER, FLIPFLOP |
Function index 0 is treated as NullFunction in runtime behavior. Flag initial values accept numeric literals and the boolean words true and false.
Equate values
Outside an automatic block, an equate is written as name value. The value may
be a numeric literal or the name of an earlier equate. An omitted value is zero.
Duplicate equate names are errors.
Each auto block starts its counter at zero. A name without a value receives the
current counter. name value and name = value assign an explicit numeric value
or an earlier equate’s value. After every entry, including an alias, the counter
becomes that entry’s value plus one. endauto ends the block; reaching another
section or the end of the file without it is an error.
The equate table is temporary. The flag-state initializer and console flag-name loader both release it before returning. The console numeric reader can search an equate table while one exists, but normal console use does not retain the definition file’s equates.
Relationship to RGM Script Data
RASCand standalone.AIcontain compiled script bytecode. No.AIfiles ship with the game — see SOUP Scripting for details.- Bytecode function calls encode function IDs (u16 indices).
- Those indices are resolved using the runtime function table built from
[functions]inSOUP386.DEF. - RAAT attribute bytes are interpreted using names declared in the
auto…endautoattribute block.
See RGM.md for RASC/RAAT container details and SOUP.md for script-source boundaries.
Notes
- Each RGM stores the byte sum of the SOUP386.DEF it was built against in its RAHD header. With
[debug] def_checksumon, loading a world whose stored sum differs from this file’s byte sum is a fatal error. - Some declared functions do not appear in shipped map scripts; declaration in DEF does not imply invocation.
External References
- RGUnity/redguard-unity
soupdeffcn_nimpl.cs - Dillonn241/redguard-mod-manager
MapDatabase.java - UESP
Mod:RGM File Format
SOUP property descriptors
Script property operands (Object.name, for example Player.hide or Me.pos.vx) are resolved through an engine descriptor table. Each descriptor names one property and gives the byte offset of its field and the field’s width. The [refs] names of SOUP386.DEF select descriptors by name, case-insensitively.
The table has 226 descriptors in the order below. Offsets are hexadecimal byte offsets from the start of the actor record. The tag column is the descriptor’s type letter: b, w and d are one-, two- and four-byte values. An uppercase tag (or x, o, A) marks a field that holds a pointer to, or the body of, a sub-record. The dotted names that follow such an entry (anim.*, size.*) address fields inside that sub-record, and their offsets are relative to the sub-record rather than the actor.
Several descriptors name state that other documents describe by function: fall_flag is the falling-state byte, extra_flag the living-state byte, torch_timer the burning-torch byte, id the low-word runtime identifier, attribute the pointer to the 256-byte attribute table, origin the retained fall origin Y, under_surface the support surface code, and pos.vx, pos.vy and pos.vz the local Q8 position. grip[0], grip[1], scabbard[0] and scabbard[1] are the four signed words the loader copies from RAEX before attachment queries. A script write replaces the stored word with the low sixteen bits of the value, and later attachment queries read the stored word.
| Name | Offset | Tag | Width |
|---|---|---|---|
address | 0x0 | x | 4 |
next | 0x0 | O | 4 |
list | 0x4 | O | 4 |
parent | 0x8 | O | 4 |
linked | 0xc | O | 4 |
grid | 0x10 | d | 4 |
id | 0x14 | w | 2 |
name | 0x16 | A | 4 |
ftype | 0x1a | b | 1 |
sflags | 0x1b | b | 1 |
runtime_local_list | 0x1c | D | 4 |
runtime_locals | 0xc1 | b | 1 |
extra | 0x21 | D | 4 |
object_data | 0x25 | o | 156 |
static_data | 0xc1 | b | 1 |
loaded_matrix | 0xc2 | b | 1 |
data | 0xc3 | B | 4 |
header | 0xc7 | B | 4 |
pos.vx | 0xcb | d | 4 |
pos.vy | 0xcf | d | 4 |
pos.vz | 0xd3 | d | 4 |
slave_pos.vx | 0xd7 | d | 4 |
slave_pos.vy | 0xdb | d | 4 |
slave_pos.vz | 0xdf | d | 4 |
last_slave_pos.vx | 0xe3 | d | 4 |
last_slave_pos.vy | 0xe7 | d | 4 |
last_slave_pos.vz | 0xeb | d | 4 |
start_pos.vx | 0xef | d | 4 |
start_pos.vy | 0xf3 | d | 4 |
start_pos.vz | 0xf7 | d | 4 |
last_pos.vx | 0xfb | d | 4 |
last_pos.vy | 0xff | d | 4 |
last_pos.vz | 0x103 | d | 4 |
last_valid_flag | 0x107 | b | 1 |
last_valid_pos.vx | 0x108 | d | 4 |
last_valid_pos.vy | 0x10c | d | 4 |
last_valid_pos.vz | 0x110 | d | 4 |
angle.vx | 0x120 | d | 4 |
angle.vy | 0x124 | d | 4 |
angle.vz | 0x128 | d | 4 |
slave_angle.vx | 0x12c | d | 4 |
slave_angle.vy | 0x130 | d | 4 |
slave_angle.vz | 0x134 | d | 4 |
last_slave_angle.vx | 0x138 | d | 4 |
last_slave_angle.vy | 0x13c | d | 4 |
last_slave_angle.vz | 0x140 | d | 4 |
start_angle.vx | 0x144 | d | 4 |
start_angle.vy | 0x148 | d | 4 |
start_angle.vz | 0x14c | d | 4 |
last_angle.vx | 0x150 | d | 4 |
last_angle.vy | 0x154 | d | 4 |
last_angle.vz | 0x158 | d | 4 |
move.vx | 0x15c | d | 4 |
move.vy | 0x160 | d | 4 |
move.vz | 0x164 | d | 4 |
ov.vx | 0x168 | d | 4 |
ov.vy | 0x16c | d | 4 |
ov.vz | 0x170 | d | 4 |
size | 0x174 | Z | 4 |
location | 0x1a5 | D | 4 |
yangle_change | 0x1a9 | w | 2 |
rope | 0x1ab | R | 4 |
rope_id | 0x1af | b | 1 |
rope_wait | 0x1b0 | b | 1 |
slide_flag | 0x1b1 | b | 1 |
fall_flag | 0x1b2 | b | 1 |
jump_flag | 0x1b3 | b | 1 |
extra_flag | 0x1b4 | b | 1 |
extra_time | 0x1b5 | b | 1 |
action_flag | 0x1b6 | d | 4 |
action | 0x1ba | d | 4 |
action_time | 0x1be | b | 1 |
arms_time | 0x1bf | b | 1 |
jump_time | 0x1c0 | b | 1 |
jump_height | 0x1c1 | b | 1 |
origin | 0x1c2 | d | 4 |
origin_time | 0x1c6 | b | 1 |
origin_accum | 0x1c7 | d | 4 |
last_direction | 0x1cb | b | 1 |
move_counter | 0x1cc | b | 1 |
last_collide | 0x1cd | b | 1 |
last_collide_count | 0x1d1 | b | 1 |
under_object | 0x1d4 | O | 4 |
under_surface | 0x1dc | b | 1 |
surface_time | 0x1dd | b | 1 |
ai_type | 0x1de | b | 1 |
ai_mode | 0x1df | b | 1 |
ai_delay | 0x1e0 | b | 1 |
ai_var1 | 0x1e1 | w | 2 |
ai_var2 | 0x1e3 | w | 2 |
ai_length | 0x1e5 | d | 4 |
node_map | 0x1e9 | N | 4 |
node | 0x1ed | d | 4 |
dnode | 0x1f1 | d | 4 |
fdnode | 0x1f9 | d | 4 |
weapon_drawn | 0x1fd | b | 1 |
torch_timer | 0x1fe | b | 1 |
hand_item | 0x200 | b | 1 |
hand_pos.vx | 0x201 | d | 4 |
hand_pos.vy | 0x205 | d | 4 |
hand_pos.vz | 0x209 | d | 4 |
hand_angle.vx | 0x20d | d | 4 |
hand_angle.vy | 0x211 | d | 4 |
hand_angle.vz | 0x215 | d | 4 |
grip[0] | 0x219 | w | 2 |
grip[1] | 0x21b | w | 2 |
scabbard[0] | 0x21d | w | 2 |
scabbard[1] | 0x21f | w | 2 |
sphere_object | 0x221 | S | 4 |
fsphere_heap | 0x229 | D | 4 |
fspheres | 0x22d | w | 2 |
active | 0x237 | b | 1 |
hide | 0x238 | b | 1 |
dead | 0x239 | b | 1 |
invisible | 0x23a | b | 1 |
combat | 0x23b | C | 4 |
light | 0x23f | L | 4 |
flat | 0x243 | F | 4 |
anim | 0x247 | M | 4 |
anim_list | 0x24b | W | 4 |
frame | 0x24f | w | 2 |
handle | 0x251 | w | 2 |
last_frame | 0x253 | w | 2 |
last_handle | 0x255 | w | 2 |
panic_group | 0x257 | w | 2 |
message_index | 0x259 | b | 1 |
broadcast_index | 0x25a | b | 1 |
anchor | 0x25b | b | 1 |
hook_data | 0x25c | B | 4 |
script_data | 0x260 | B | 4 |
script_int | 0x264 | B | 4 |
script_pc | 0x268 | B | 4 |
script_return | 0x26c | B | 4 |
script_rv | 0x270 | b | 1 |
script_ret_rv | 0x271 | b | 1 |
attribute | 0x272 | B | 4 |
local_index | 0x276 | d | 4 |
string_index | 0x27a | d | 4 |
disable_script | 0x27e | b | 1 |
task[0] | 0x27f | B | 4 |
task[1] | 0x283 | B | 4 |
task[2] | 0x287 | B | 4 |
task[3] | 0x28b | B | 4 |
task[4] | 0x28f | B | 4 |
task[5] | 0x293 | B | 4 |
task_variable[0] | 0x297 | d | 4 |
task_variable[1] | 0x29b | d | 4 |
task_variable[2] | 0x29f | d | 4 |
task_variable[3] | 0x2a3 | d | 4 |
task_variable[4] | 0x2a7 | d | 4 |
task_variable[5] | 0x2ab | d | 4 |
task_status[0] | 0x2af | b | 1 |
task_status[1] | 0x2b0 | b | 1 |
task_status[2] | 0x2b1 | b | 1 |
task_status[3] | 0x2b2 | b | 1 |
task_status[4] | 0x2b3 | b | 1 |
task_status[5] | 0x2b4 | b | 1 |
task_pause[0] | 0x2b5 | b | 1 |
task_pause[1] | 0x2b6 | b | 1 |
task_pause[2] | 0x2b7 | b | 1 |
task_pause[3] | 0x2b8 | b | 1 |
task_pause[4] | 0x2b9 | b | 1 |
task_pause[5] | 0x2ba | b | 1 |
task_timer[0] | 0x2bb | w | 2 |
task_timer[1] | 0x2bd | w | 2 |
task_timer[2] | 0x2bf | w | 2 |
task_timer[3] | 0x2c1 | w | 2 |
task_timer[4] | 0x2c3 | w | 2 |
task_timer[5] | 0x2c5 | w | 2 |
task_variable2[0] | 0x2c7 | d | 4 |
task_variable2[1] | 0x2cb | d | 4 |
task_variable2[2] | 0x2cf | d | 4 |
task_variable2[3] | 0x2d3 | d | 4 |
task_variable2[4] | 0x2d7 | d | 4 |
task_variable2[5] | 0x2db | d | 4 |
task_variable3[0] | 0x2df | d | 4 |
task_variable3[1] | 0x2e3 | d | 4 |
task_variable3[2] | 0x2e7 | d | 4 |
task_variable3[3] | 0x2eb | d | 4 |
task_variable3[4] | 0x2ef | d | 4 |
task_variable3[5] | 0x2f3 | d | 4 |
task_variable4[0] | 0x2f7 | d | 4 |
task_variable4[1] | 0x2fb | d | 4 |
task_variable4[2] | 0x2ff | d | 4 |
task_variable4[3] | 0x303 | d | 4 |
task_variable4[4] | 0x307 | d | 4 |
task_variable4[5] | 0x30b | d | 4 |
anim.address | 0x0 | M | 4 |
anim.current | 0x0 | M | 2 |
anim.next | 0x2 | M | 2 |
anim.next_speed | 0x4 | M | 1 |
anim.frame | 0x5 | M | 2 |
anim.speed | 0x7 | M | 1 |
anim.speed_count | 0x8 | M | 1 |
anim.stop_flag | 0xa | M | 1 |
anim.halt_flag | 0xb | M | 1 |
anim.hold_flag | 0xc | M | 1 |
anim.rep_count | 0xd | M | 1 |
anim.rewind_flag | 0xe | M | 1 |
anim.loop_count | 0xf | M | 1 |
anim.sound_handle | 0x10 | M | 1 |
anim.real_frame | 0x11 | M | 1 |
anim.max_group | 0x12 | M | 1 |
anim.pos.vx | 0x13 | M | 2 |
anim.pos.vy | 0x15 | M | 2 |
anim.pos.vz | 0x17 | M | 2 |
anim.angle.vx | 0x19 | M | 2 |
anim.angle.vy | 0x1b | M | 2 |
anim.angle.vz | 0x1d | M | 2 |
anim.last_angle.vx | 0x1f | M | 2 |
anim.last_angle.vy | 0x21 | M | 2 |
anim.last_angle.vz | 0x23 | M | 2 |
size.address | 0x0 | Z | 4 |
size.orientation | 0x0 | Z | 1 |
size.size.vx | 0x1 | Z | 4 |
size.size.vy | 0x5 | Z | 4 |
size.size.vz | 0x9 | Z | 4 |
size.pos.vx | 0xd | Z | 4 |
size.pos.vy | 0x11 | Z | 4 |
size.pos.vz | 0x15 | Z | 4 |
size.psize.vx | 0x19 | Z | 4 |
size.psize.vy | 0x1d | Z | 4 |
size.psize.vz | 0x21 | Z | 4 |
size.msize.vx | 0x25 | Z | 4 |
size.msize.vy | 0x29 | Z | 4 |
size.msize.vz | 0x2d | Z | 4 |
Extra-time counter
extra_time is a receiver-owned unsigned byte. Script reads return values from
0 through 255; writes retain the low eight bits and change only that field.
Actor allocation clears it with the rest of the actor record.
An admitted ordinary actor update leaves zero and 255 unchanged. Other values lose the nominal tick count, saturating at zero. This stage follows the early action and ledge work and precedes the ordinary motion-vector clear and the subsequent animation advance. Actors excluded from ordinary processing do not reach this countdown stage.
Successful torch-lighting and torch-dousing entry request their animation first,
then assign their living state and clear extra_time. A refused entry leaves
it unchanged.
Stored playback default
panic_group is the receiver actor’s stored playback default, separate from
the installed PanicGroup attribute. Reads sign-extend its 16-bit word; writes
retain the low 16 bits. A property write changes only the stored default. It
does not request a group, replace the current or pending group, advance commands
or dispatch animation sounds. Later request fallback and default comparisons
consume the changed word. Ordinary default selection can overwrite it.
Actor allocation clears this word, including reuse of pooled records. Actor animation-control initialization also sets it to zero. The word belongs to the actor record even when no playback allocation exists. Object save records retain it in the packed actor image, and object restoration copies it back independently of the optional playback trailer.
Packed movement counter
move_counter is the actor’s unsigned packed movement-counter byte. Raw property
writes retain the low eight bits and do not themselves request or advance
animation or translate the actor. The forward startup phase occupies bits 4 and
5. A forward amount query below phase three performs its support admission before
requesting animation. After animation admission it advances that phase, preserving
the other bits. Phase three keeps the full amount and remains saturated.
The byte clears with actor allocation, including reuse, and remains part of
the saved actor body.
Vertical-motion timer
origin_time is the receiver actor’s retained queued/falling vertical-motion
timer byte. Property reads are unsigned and writes retain the low eight bits.
The vertical-motion consumers interpret that same byte as signed: queued motion
uses it as the factor index and advances it with byte wrapping, while falling
uses factor zero when the signed timer is nonpositive. A positive falling timer
selects its factor and decreases by the sum of the two fine-tick increments,
with byte wrapping. Model-controlled vertical motion skips these arithmetic
updates. A property write does not initialize a fall or move the actor.
Actor allocation clears the byte, including reused pooled records. Jump entry
initializes it to zero; ordinary fall entry initializes it to 30 when it records
the fall origin. The timer remains part of the saved actor body, independently
of playback allocation. It is distinct from the rope regrab countdown and
extra_time.
Accumulated displacement
origin_accum is the receiver actor’s signed 32-bit accumulated displacement,
shared by sliding and the pre-script queued/falling jump displacement. Property
access retains the entire word and does not itself move the actor. Allocation
clears it, including pooled reuse, and the saved actor body retains it. Jump
initialization clears it; the first slide also clears it before adding the slide
distance. Later slide responses add their distance with 32-bit wrapping.
The pre-script jump displacement compares this accumulator with its motion’s cap as a signed value. Below the cap, the candidate sum of accumulator and step wraps at 32 bits before the signed cap comparison. A clamped step is the cap minus the accumulator with 32-bit wrapping. When model-controlled motion does not suppress displacement, the selected step is added to the accumulator with 32-bit wrapping; only a positive step translates the actor. At or above the cap, the step is zero. Slide termination compares this same signed accumulator with the slide-hurt distance threshold.
Pending-action timer
action_time is the receiver actor’s unsigned byte timer for pending-action
promotion, separate from the state-17 action-transition counter. Property reads
are unsigned and writes retain the low eight bits. Allocation clears it, including
pooled reuse, and the saved actor body retains it. The pending-action branch of
SetAction clears it when it sets the pending directional bits.
The late jump-window stage adds the low byte of the nominal-tick increment with
byte wrapping. A result greater than 36 merges the pending action word into the
accepted word, clears the pending word and resets action_time to zero. A result
of 36 does not promote. This timer update precedes the queued-motion and current
animation gates that clear the directional low bits and decide jump entry. A
raw property write does not perform promotion or start a jump.
Arms timer
arms_time is the receiver actor’s unsigned byte action-transition counter.
Property reads are unsigned and writes retain the low eight bits. Allocation
clears it, including pooled reuse, and the saved actor body retains it. The
admitted state-17 transition assigns six after requesting its transition group.
The ordinary actor update consumes this byte using the signed-byte action-counter
rule in SOUP.md: zero is unchanged; the early-clear test precedes wrapping tick
subtraction and negative-result clamping. A raw property write does not request
a transition group or change living state. This field is separate from the
pending-action promotion timer action_time.
Pending directional action word
action_flag is the receiver actor’s pending directional action word. Property
access preserves all 32 bits; the script integer carries the same bit pattern.
Allocation clears the word, including pooled reuse, and the saved actor body
retains it. A raw write does not reset action_time or execute an action.
The pending-action branch of SetAction sets its low two bits. The late
jump-window promotion merges the entire pending word into the accepted action
word, then clears the pending word. The later motion and animation gates clear
only the low two directional bits, retaining the other bits. These words are
separate from the queued-motion and falling-motion bytes.
Accepted directional action word
action is the receiver actor’s accepted directional action word, separate from
pending action_flag. Property access preserves all 32 bits through the script
integer’s bit pattern. Allocation clears it, including pooled reuse, and the
saved actor body retains it. A raw write does not execute an action or reset
the promotion timer.
The late jump-window stage consumes accepted low bits independently of whether the pending word was promoted on that update. A queued motion or a current group other than running or torch-running clears the low two bits of both words. Otherwise accepted bit zero selects running motion eight before bit one can select motion nine; consumption clears the low two bits in both words before jump admission. All higher bits remain unchanged. Promotion merges the entire pending word into the accepted word before these gates and consumption.
Runtime script-control property
disable_script is the receiver actor’s runtime script-control byte, shared by
the enable and disable control natives. Property reads are unsigned and writes
retain the low eight bits. The masks retain the independent script, hook, task,
object and combat meanings documented in SOUP.md. The descriptor is distinct
from sflags and from the actor’s attribute table.
Actor allocation clears this byte, including pooled reuse, and the saved actor body retains it. A raw property write stores the complete byte; control natives modify only their own mask and preserve the other bits. Ordinary stage admission reads this same byte.
Collision-metadata property
sflags is the receiver actor’s retained collision-metadata byte. Reads are
unsigned and writes retain the low eight bits. It is independent of the runtime
script-control byte disable_script and the actor’s attribute table.
Allocation clears the byte, including reuse of pooled actors, and the saved actor body retains it. On the static model-load path, a matching named ROB segment supplies its collision byte. The MPOB static-model fallback performs the same copy. Without a matching segment, that copy is skipped rather than assigning a new value. Live static face candidates retain this actor byte as support metadata. See actor model initialization for the model-load lifecycle.
AI lifecycle-selector property
ai_type is the receiver actor’s lifecycle-selector byte, shared by
MyAiType and SetAiType. Reads are unsigned and writes retain the low eight
bits. A raw property write replaces only this byte; it does not change the
adjacent AI mode, health, the killed flag, or script-control masks.
Actor allocation clears the byte, including pooled reuse, and the saved actor
body retains it. Combat entry uses selector six, while the ordinary IsDead
predicate tests selector eight. The selected player’s independent death override
can also make IsDead succeed. The selector is distinct from the dead property.
See receiver AI type and
dead actor default selection.
AI mode property
ai_mode is the receiver actor’s unsigned AI-mode byte, shared by MyAiMode
and SetAiMode. Writes retain the low eight bits and replace only this byte.
The adjacent ai_type lifecycle selector, the independent dead property,
health, and script-control masks are unchanged. A raw mode write does not
invoke a lifecycle transition.
Actor allocation clears the byte, including pooled reuse, and the saved actor
body retains it. endCombat clears this mode while setting the lifecycle
selector to one. See AI mode query and setter
and AI lifecycle-selector property.
AI delay property
ai_delay is the receiver actor’s unsigned AI-delay byte. Reads return values
from zero through 255 and writes retain the low eight bits. It is separate
from the adjacent AI mode and lifecycle selector and from extra_time. Actor
allocation clears it, including pooled reuse, and the saved actor body retains it.
The AI delay update retains zero and 255. For any other value, it compares the
unsigned byte with the shared nominal tick count. A smaller value becomes zero;
otherwise the low byte of the tick count is subtracted. The update then
continues into lifecycle-selector dispatch even when the delay was zero or 255.
GoToLocation invokes this update after selecting lifecycle state four and
storing the requested location word.
One AI admission branch requires delay zero as well as a mode other than eleven before invoking its candidate service. In the successful path-selection branch, a delay of 255 becomes 64; other delay values remain unchanged. The temporary AI-state save and restore pair retains this byte alongside the mode, length and AI-variable state. Raw property writes invoke none of those services.
Position and angle history properties
last_pos.vx, last_pos.vy and last_pos.vz are the actor’s previous local position (the triple
SetPosition writes alongside the current position), start_pos.* its saved start position (the
triple SetStartPosition restores from and DistanceFromStart measures against) and last_angle.*
the angle history that SetRotation stores with the local angles. Each is a four-byte field read and
written whole, without a matrix refresh or any movement service; a write to start_pos therefore
changes what DistanceFromStart reports, and a write to last_pos or last_angle changes only the
retained history.
Stable-position and creation-angle properties
last_valid_flag is a byte and last_valid_pos.vx, last_valid_pos.vy and last_valid_pos.vz are
three four-byte fields: the actor’s stable ordinary position
record, a validity byte followed by the Q8 local position it holds. The stable recovery runs only
while the byte is nonzero and restores the position from the record. Raw writes change only the
fields, so a script can arm a recovery by setting the position and then the flag, or disarm it by
clearing the flag.
start_angle.vx, start_angle.vy and start_angle.vz are the actor’s creation angles, four-byte
fields. The orientation setter that actor creation, the player placement and the player swap use
masks the angles to 0 through 2047 and stores them in the current angles, the angle history and
this triple before it rebuilds the matrix; ResetRotation restores a
component from it. SetRotation does not change it. A property write stores the whole value with
no matrix refresh.
Motion triple properties
move.vx, move.vy and move.vz are the actor’s retained motion components, three four-byte fields
read and written whole. The fall start and obstacle interruption clear them. The rope release service
stores the jump-off speed in move.vz (the installed jump-off speed, plus the node’s measured speed
for a non-player receiver, negative for a backward jump) and queued motion 24 reads its sign and
magnitude to displace the actor, so a script write to move.vz while that motion runs changes the
jump. A property write moves no actor and runs no service.
Slave pose properties
slave_pos.vx, slave_pos.vy and slave_pos.vz are the actor’s attached position and
slave_angle.vx, slave_angle.vy and slave_angle.vz its attached angles, the pose the family
update composes for an actor attached to a master; last_slave_pos.* and last_slave_angle.* hold
the values the previous family update published. Each is a four-byte field read and written whole,
without a matrix refresh.
Every family update copies the current attached position and angles into the last_slave_* fields,
then stores the new composed position and the composed angles (masked to 0 through 2047) in the
slave_* fields. An actor with a nonzero attr_slave takes its effective position and angles for
queries from slave_pos and slave_angle and every other actor from its local position and angles,
so a property write changes what distance and placement queries see for an attached actor until the
next family update replaces it, and changes nothing for an unattached one.
Task slot word properties
task_variable[n], task_variable2[n], task_variable3[n] and task_variable4[n] (n from 0 through 5)
are the four 32-bit words that task slot n of the receiver retains for its native handler. The task
resume pass copies them into the shared native command scratch before the handler runs and copies the
scratch back afterwards, so a property write to a slot is what the next resumption of that slot sees,
and a property read shows the state saved by its last resumption. The words are:
task_variable: the first native continuation word (the loaded image resource ofAnimateGXA).task_variable2: the pause countdown in nominal ticks that point-walk handlers decrement before doing any other work.task_variable3: the third word, the second native continuation word of handlers such asFxPhase; point-walk handlers also hold the 32-bit command value here, which the wrapper copies to and from the actor’s AI length.task_variable4: the packed command word (low 16 bits), phase byte (bits 16 to 23) and counter byte (bits 24 to 31).
Each is read and written whole. A property write moves no handler and clears no slot; a slot that is empty or paused keeps whatever the resume pass last stored for it.
Task slot control properties
task_status[n] and task_pause[n] are bytes and task_timer[n] a 16-bit word of the receiver’s task
slot n (n from 0 through 5), kept in parallel arrays beside the slot’s call word. A property read
zero-extends a byte and sign-extends the timer; a write keeps the low byte or word.
task_statusis nonzero while the slot blocks its thread: allocation stores one for a blocking task and zero for a multitask. The task pass adds the status of every unpaused occupied slot, so a status of any nonzero value blocks.task_pausepauses the slot while nonzero.TaskPausestores one,TaskResumezero, and the task pass skips a paused slot without invoking its handler or advancing its timer.task_timeris the invocation counter. Allocation stores one; each pass invocation derives its elapsed input from the counter times the frame scale shifted right by eight, then increments it with 16-bit wrap.
A slot whose call word is zero is empty and is skipped whatever its three bytes hold. Completion of a
handler clears the call word and the status and leaves the pause byte and the timer as they are.
TaskStop clears the call word, status and pause of every matching slot and leaves the timer.
Allocation overwrites all three. A property write changes only the field: it removes no call, runs no
handler and cancels no native state.
Animation record properties
The anim.* properties address the receiver’s animation record. A property read extends a byte with
zeros and a word with its sign; a write keeps the low byte or word, runs nothing and validates
nothing until playback uses the value. An actor without an animation record reads zero. The
fields the playback routines keep there are:
| Property | Contents |
|---|---|
anim.current | current group, signed word, -1 for none |
anim.next | pending group, signed word, -1 for none |
anim.next_speed | pending-speed byte: every request stores its speed modifier, then adds the countdown byte of the group it selected (a request that finds no group leaves just the modifier). Selecting the group reads it into anim.speed and leaves it in place, so it stays readable with no request pending; writing anim.next changes only the pending group, which a later selection then runs with the stored byte |
anim.frame | signed command cursor of the current group |
anim.speed | countdown reload byte, taken from anim.next_speed when the group is selected |
anim.speed_count | frame countdown byte; the cursor advances when a tick finds it zero, and it reloads from anim.speed before dropping once |
anim.stop_flag | exit marker |
anim.halt_flag | BreakPoint translation suppression: one after a BreakPoint, cleared again by every cursor step |
anim.hold_flag | wait byte: zero runs, 255 holds indefinitely, other values lose the tick input per update before playback resumes |
anim.loop_count | finite loop countdown byte |
anim.rewind_flag | reverse playback when the byte is one |
anim.sound_handle | remembered sound slot, 255 for none |
anim.real_frame | wrapping frame-progress byte |
Selecting a group clears the cursor, progress byte, stop, halt, hold, loop and rewind bytes and
the repetition byte, sets the sound slot to 255 and the countdown to the reload byte. A property
write therefore changes only the written field: a script that writes anim.speed_count to zero
makes the next tick with nonzero input advance the cursor and count one frame, and writing
anim.hold_flag to 255 stops all playback until it is cleared. anim.current is the group whose command table the playback interprets: each advance reads the
table and the type byte of the group the word names, so a write switches both while the cursor, countdowns,
loop, repeat and markers stay, like a ChangeAnimGroup that keeps the cursor. The word -1 leaves no current
group, and the next advance selects any pending request. A group without a table is dereferenced at the next advance. anim.max_group is the low byte of the RAHD anim_control_prefix field, stored when the animation record is built. anim.rep_count is the repeat byte of the
ConditionalDelay and LoopControl commands, and
anim.pos.* and anim.angle.* are the 16-bit translation and rotation words those and the
translation commands retain until the next frame step; they are read sign-extended.
anim.last_angle.* are the three rotation words the model refresh copied from anim.angle.* the last time it ran for the
actor: the refresh applies the difference of the two to the model’s orientation and then stores the current words as the
history, whenever the record has a current or a pending group. A script write replaces a history word like any other
field.
Rope wait property
rope_wait is the byte at record offset 0x1b0. A rope attachment clears it; a rope release sets it to 24. The per-update
rope grab check counts it down by the update’s nominal ticks, and when the count reaches zero it also clears the byte that
names the last rope (rope_id); while it is nonzero the grab skips the rope that byte names. The late collision stage reads it
as a recovery window (values within 3 of 24) and the fall clears it. The weapon routines leave it as it is.
Heading change property
yangle_change is a signed word the model refresh stores for an actor that is not attached to a rope: the Y angle minus the
Y angle the ordinary update saved when it began (the last_angle history), truncated to sixteen bits. The family composition
of an attached actor stores its composed history angle instead. Nothing in the executable reads the word; scripts may read
and write it like any other field.
Frame properties
frame is the frame index the last selected ShowFrame command names inside its model (the second of the two fields the
loader patches into the command), a signed word that is -1 until a group selection reaches its first frame. last_frame is
the same word as it stood when the actor’s last ordinary update began. The word that precedes them, handle, is the
runtime model handle from the command’s first patched field.
Killed, weapon-drawn and invisible properties
dead, weapon_drawn and invisible are bytes of the receiver’s actor record. dead is the killed marker:
KillMe sets it, Resurrect clears it, and the
ordinary update loops test it before running a receiver, so a script that writes it nonzero is no
longer updated. It is distinct from the ai_type lifecycle selector. weapon_drawn is the byte the
equipped-weapon draw and cleanup routines set and clear and that the weapon predicates test. A property read
returns the unsigned byte and a write keeps the low eight bits, without running any of those routines.
invisible is a separate byte that only the draw stage reads. The world render loop still updates the
frame and refreshes the model of an actor whose invisible byte is nonzero, and skips the calls that
draw its body, so scripts, animation and collision continue while the body is not drawn. The hide
byte, by contrast, removes the actor from the whole block.
Runtime identifier property
id is the receiver’s low-word runtime identifier, the signed 16-bit value MyId returns. A property
read sign-extends it to the script integer width. The identifier is the key name lookup, route-node
ownership and the saved-actor identity check use, so a script that stores to it changes which actor
those services find.
Held-item and surface-time properties
hand_item is the byte HandItem assigns (see held-item assignment),
zero when no item was ever assigned. A script write replaces the whole byte, so a stored value
reads back as its low eight bits, zero-extended; the weapon draw and item-use checks test the byte for
zero and for the item numbers they name.
surface_time, jump_time, jump_height and anchor are plain bytes: no engine routine reads or
writes the actor-record bytes they name (the jump_time and jump_height attributes are separate
fields). Each is zero until a script stores to it and then keeps the low eight bits of the stored value.
Message selection properties
message_index and broadcast_index are the receiver actor’s unsigned selection bytes for its
message channel and for the broadcast channel: the cursors the message natives (ReceiveMessage,
DeleteMessage, IsMessageWaiting, GetMessageSender and its identifier form) and the broadcast
natives (ReceiveBroadcast, DeleteBroadcast, IsBroadcastWaiting, GetBroadcastSender and
its identifier form, ReplyBroadcastSender) read and advance. Property reads are unsigned and
writes retain the low eight bits, with no clamp to the entry count: a stored value outside the
queue is used unchecked by the natives. Actor allocation clears both bytes, including pooled reuse.
A raw write moves no cursor service and changes no entry.
AI record properties
ai_var1 and ai_var2 are two-byte fields and ai_length a four-byte field in the actor record’s
AI block next to ai_delay. A property read sign-extends a two-byte field and returns a four-byte
field whole; a write keeps the low sixteen bits of a two-byte field and all of a four-byte one.
Actor allocation zeroes the record and then sets ai_var1 to minus one (the word 0xffff),
leaving ai_var2 and ai_length zero, including for pooled reuse. GoToLocation stores the low
sixteen bits of its location argument in ai_var1, and its sentinel test reads that word back as a
signed value.
node, dnode and fdnode are the actor’s held current, next and final-destination walk-node
indices, and node_map the walk map they index; the previous-next record sits between dnode and
fdnode. Allocation sets the three indices to minus one and node_map to null. Assigning the
current node first copies a held next node into the previous-next record; the next-node assignment
and the walk code move the held values between the fields. A raw property write stores the value
without claiming or releasing a node reservation.
The HeaderHook diagnostic prints node, dnode, ai_length as signed integers and both
variables as signed sixteen-bit values. The temporary AI-state save and restore pair retains
ai_length and ai_var1 with the mode and delay. Raw property writes invoke no AI service.
Save-game containers
SAVEGAME.SAV and per-map .TSG files store saved engine state in tagged chunks.
Overall Structure
[SVGM — save metadata]
[Image and state chunks — length-prefixed payloads]
[END — four-byte terminator]
Chunk lengths are big-endian. Numeric fields within the SVGM payload are
little-endian. Chunks have no alignment padding.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
0x00 | 4 | [u8; 4] | — | name | Four-byte chunk identifier. |
0x04 | 4 | u32 | BE | data_length | Payload size, excluding the eight-byte header. |
0x08 | data_length | [u8] | — | data | Chunk payload. |
The terminal identifier is END , including its trailing space. It has no
length field or payload. An empty chunk still has an eight-byte header with
a zero length.
SVGM metadata
Version 1.31 uses a 272-byte metadata payload. Offsets below are relative to
the payload, after the chunk header.
| Offset | Size | Type | Endian | Name | Description |
|---|---|---|---|---|---|
0x00 | 4 | [u8; 4] | — | version | Save-format version text, 1.31. |
0x04 | 4 | u32 | LE | unknown_04 | Engine compatibility value. |
0x08 | 4 | u32 | LE | unknown_08 | Engine compatibility value. |
0x0C | 128 | [u8; 128] | — | description | Slot display text, ending at the first NUL or the field boundary. |
0x8C | 4 | u32 | LE | world_id | Current world identifier, corresponding to WORLD.INI. |
0x90 | 128 | [u8; 128] | — | bitmap_filename | Stored bitmap filename, ending at the first NUL or the field boundary. |
The engine checks the version and compatibility values before restoring a save.
An incompatible format version prevents state restoration. A mismatch in the two
compatibility values displays a warning but does not prevent state restoration
when the format version matches.
The stored bitmap filename is separate from the THMB thumbnail and SCR3
screen-image chunks.
State chunks
Save containers include global state and per-world state. Object and script records depend on the corresponding RGM scene and runtime definitions. They are not replacement scene assets.
The world loader constructs object, rope and code owners before applying their
saved records. The SMOB, SMRP and SMCO readers consume the number of records
established by that initialized world, rather than deriving a count from the
chunk length. SMSH includes a count, which must match the initialized shell
count.
Each SMCO record starts with a four-byte little-endian code-object identifier
followed by a 204-byte packed runtime body. The reader resolves that identifier
against the initialized code owners. Nonzero saved script references are offsets
rebased against the loaded world’s code and native-hook bases. Live list links
and the code owner’s runtime-only pointer are retained instead of accepting the
saved pointer values. Matching a world number alone therefore does not supply
the owners and script bases needed to interpret these records.
SMOB, SMRP, SMCO, and SMSH hold object, rope, code, and shell state,
respectively. SVCM holds camera records, and SMFM holds family references.
Other state chunks include SVGF, SMGR, SVRG, SVGV, SVG2, SVIT,
SVCB, SVMV, and SVMD.
Temporary .TSG snapshots use the same container and metadata layout.
Their image chunks can be empty. The engine can restore these snapshots during
a world reload after initializing the player.
Global flags
The SVGF payload stores the flag values and state bytes in installed global
flag order. Its payload length is 4 + 5 × count. Numeric fields are
little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0 | 4 | u32 | count | Number of global flags. |
4 | 4 × count | [i32; count] | values | Current global flag values. |
4 + 4 × count | count | [u8; count] | states | Native flag kind and latch state. |
General state and clock
Version 1.31 uses a 1,542-byte SVGV payload. It concatenates the retained
global fields without alignment padding. The clock fields below are four-byte
little-endian words. Offsets are relative to the payload.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x094 | 4 | u32 | measurement_baseline | Last FPS measurement baseline in BIOS ticks. |
0x098 | 4 | i32 | measurement_remainder | BIOS ticks retained toward the next measurement window. |
0x09C | 4 | u32 | measured_fps | Measured frame rate. |
0x0A0 | 4 | u32 | next_scale | Next movement scale, with eight fractional bits. |
0x0A4 | 4 | u32 | nominal_increment | Last computed whole nominal-tick increment. |
0x0A8 | 4 | u32 | fine_increment | Last computed finer-clock increment; eight units per nominal tick. |
0x0AC | 4 | u32 | warmup | Remaining startup invocations. |
0x0B4 | 4 | u32 | bios_sample | Last sampled BIOS tick. |
0x0B8 | 4 | u32 | measurement_frames | Frames counted toward the next FPS measurement. |
0x0C0 | 4 | u32 | smoothed_fps | Retained smoothed frame rate. |
0x0C4 | 4 | u32 | frame_scale | Currently exposed movement scale, with eight fractional bits. |
0x0C8 | 4 | u32 | exposed_nominal_ticks | Currently exposed whole nominal-tick increment. |
0x0CC | 4 | u32 | nominal_total | Whole nominal-tick accumulator. |
0x0D0 | 4 | u32 | nominal_scaled_total | Nominal-tick accumulator with eight fractional bits. |
0x0D4 | 4 | u32 | nominal_scaled_increment | Last change in the scaled nominal accumulator. |
0x0D8 | 4 | u32 | exposed_fine_ticks | Currently exposed finer-clock increment; eight units per nominal tick. |
0x0DC | 4 | u32 | fine_total | Whole finer-clock accumulator. |
0x0E0 | 4 | u32 | fine_scaled_total | Fine-tick accumulator with five fractional bits. |
0x0E4 | 4 | u32 | fine_scaled_increment | Last change in the scaled fine accumulator. |
0x0E8 | 4 | u32 | min_frame_rate | Retained minimum FPS setting. |
0x0EC | 4 | u32 | max_frame_rate | Retained maximum FPS setting. |
0x0F0 | 4 | u32 | use_smooth_fps | Retained smoothed-FPS selection switch. |
0x0F4 | 4 | u32 | use_smooth_divisor | Retained movement-scale smoothing switch. |
0x0F8 | 4 | u32 | normal_rate_scaled | Retained normal frame rate multiplied by 256. |
The two scaled totals advance by next_scale; their low eight and five bits
retain the fractional remainders. Exposed timing can differ from the last
computed timing. Save restoration copies these fields directly, then captures
the partial clock checkpoint used during subsequent resource preparation.
The saved clock therefore includes fields omitted by a menu checkpoint,
including smoothed FPS, the measurement remainder and startup countdown.
It also retains the FPS settings rather than storing only their derived scale.
See FPS-derived frame scale
for measurement and checkpoint behavior. Other SVGV fields are retained state
and must be preserved when editing a save.
Rope jump latch
The selected player’s rope jump latch is a four-byte little-endian word at
SVGV payload offset 0x06C. The global-state writer stores the latch directly,
and the global-state reader copies the saved word back before restoring the
per-world object and rope records. It is separate from the SMRP rope records.
See rope control while attached
for its input behavior.
SMOB restores an actor’s rope attachment by resolving the saved rope identity
and constructing a fresh attachment. The actor receives that fresh pointer;
the saved attachment pointer is not reused. The saved attachment node and motion
word are then restored into the fresh attachment.
When restoring SMRP, the rope’s list references are resolved by rope identity.
The current link-array and end-model pointers are retained. If the selected
player has an attachment, the rope’s current attachment-list head is retained
as well. If the selected player has no attachment, the current head is freed
and the restored head is null. The rope jump latch is restored through SVGV
independently of these attachment pointers.
Actor script-state ownership
SMOB retains the actor’s existing script-state pointer instead of restoring the
saved pointer value. When that actor has script state, its saved script-state
record is copied into the existing allocation. The main save reader restores
object and rope records before inventory, then reads SVCB into the selected
player’s existing script state. These records depend on initialized actor script
owners; they do not allocate a replacement selected-player script owner.
The script-state copy preserves the live taunt-pointer array, all nine attack references and all four defence references. Saved pointer values do not replace these dependencies of the initialized owner.
Engine globals
The main save (SAVEGAME.SAV, not the per-world .TSG files) also writes an SVG2 chunk after
SVGV. Its payload is a fixed 1,666-byte concatenation of retained engine globals without padding or
per-field headers, in this order:
| Size | Contents |
|---|---|
| 4 | Retained player health, copied into both player records at load. |
| 4 | Health-candle feedback state. |
| 2 | Retained state word, written only by the save loader. |
| 12 | Retained state block. |
| 1600 | The journal tag table: 400 four-byte tags, zero marking an empty entry. |
| 4 | Selected-player switch. |
| 4 × 5 | Power-up slots: the two slots’ frame values, their two retained frame indices (-1 empty), then the slot alternation (see power-up slots). |
| 4 × 4 | Four retained world-state words written when a world loads. |
| 4 | Retained HUD state word. |
Inventory
SVIT contains 22-byte records in initialized item-list order,
followed by a signed 32-bit selected-item ID. All numeric fields are little-endian.
The payload contains neither an item count nor per-record IDs. Its length is
22 × saved_record_count + 4. The save reader and writer traverse one fewer
record than item initialization creates. The final initialized item remains at
its initialized values when loading; it has no record in SVIT.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 1 | u8 | hide | Current item visibility field. |
0x01 | 1 | u8 | pushed | Nonzero while a pushed value is retained. |
0x02 | 4 | i32 | total | Current item total limit. |
0x06 | 4 | i32 | player_max | Current player ownership limit. |
0x0A | 4 | i32 | player_total | Current owned count. |
0x0E | 4 | u32 | unknown_0e | Retained item-state word. |
0x12 | 4 | u32 | saved_player_total | Count saved by the first push and restored by a pop. |
The pushed byte and saved count are restored directly, without normalizing the byte or replacing the saved count with the current count. PushItem and PopItem use these fields: the first push saves the current count, later pushes retain that saved count, and a pop clears the pushed byte and restores the saved count.
The selected-item value -1 means no selection. Other values are looked up as
full item IDs after the records have been restored. An absent ID leaves no
selection. Restoration directly replaces the saved fields in initialized item
records. It does not invoke acquisition, requirements, drop lists or pickup effects.
See ITEM.INI for the initialized limits and acquisition rules. These save records can replace limits as well as owned counts.
Save-slot directories
[SAVEGAME.NNN — slot directory]
[SAVEGAME.SAV — current saved state]
[Map-name.TSG files — other per-world snapshots]
[LOGBOOK.TXT — journal content, when present]
The main save identifies the current world. The accompanying .TSG files
retain state for other worlds. LOGBOOK.TXT contains game journal content;
it is separate from the historical TXT tool reports described in
Bundled support resources.
External References
CHT — Cheat Persistence
Raw dump of the engine’s cheat state array, written by the savecheats cheat command to REDGUARD.CHT in the game directory.
Overall Structure
[Cheat state array — 64 × u32 LE values, 256 bytes total]
Format
The file is exactly 256 bytes: 64 little-endian u32 slots. Each slot holds the state value for one cheat. The first 13 slots correspond to the built-in cheat codes; slots 13–63 are unused (always zero in practice).
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 4 | u32 | oracle | Cheat #0 state |
| 0x04 | 4 | u32 | nodemarker | Cheat #1 state |
| 0x08 | 4 | u32 | moonraker | Cheat #2 state |
| 0x0C | 4 | u32 | task | Cheat #3 state |
| 0x10 | 4 | u32 | animation | Cheat #4 state |
| 0x14 | 4 | u32 | magiccarpet | Cheat #5 state |
| 0x18 | 4 | u32 | savecheats | Cheat #6 state |
| 0x1C | 4 | u32 | drevil | Cheat #7 state |
| 0x20 | 4 | u32 | drno | Cheat #8 state |
| 0x24 | 4 | u32 | goldfinger | Cheat #9 state |
| 0x28 | 4 | u32 | neversaydie | Cheat #10 state |
| 0x2C | 4 | u32 | oddjob | Cheat #11 state |
| 0x30 | 4 | u32 | yeahbaby | Cheat #12 state |
| 0x34 | 204 | — | unused | Slots 13–63, always zero |
State values: 0 = off, 1 = on. The console cheat = N syntax can set arbitrary integer values, but normal toggle usage produces only 0 or 1.
Behavior
- Write: The
savecheatscheat (index 6) clears its own slot, writes the 256-byte state array toREDGUARD.CHT, then sets its slot to one after closing the file. The storedsavecheatsvalue is therefore zero. - Read: On startup, the engine checks for
REDGUARD.CHTand restores all cheat states from it. - Reset: Delete the file to clear all persisted cheats.
See Cheat System for cheat descriptions and activation methods.
External References
CPF — Console preferences
REDGUARD.CPF stores the console settings written by console save.
Overall Structure
[Signature — three ASCII bytes]
[Console options — eight 32-bit fields]
[Background palette index — one byte]
[Screen limit — one 32-bit field]
The file is 40 bytes. All multi-byte fields are little-endian.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0x00 | 3 | [u8; 3] | signature | ASCII 2.0, without a terminator |
| 0x03 | 4 | u32 | font | Font selector; console font cycles zero and one |
| 0x07 | 4 | u32 | shadow | Written as zero for disabled or one for enabled |
| 0x0B | 4 | u32 | edit_color | GUI color index for editable text |
| 0x0F | 4 | u32 | report_color | GUI color index for report text |
| 0x13 | 4 | u32 | screen_color | GUI color index for screen reports |
| 0x17 | 4 | u32 | paper_color | GUI color index for the solid background |
| 0x1B | 4 | i32 | hex_lines | Number of lines in a hexadecimal dump |
| 0x1F | 4 | u32 | paper_disabled | Zero selects paper artwork; one selects a solid background |
| 0x23 | 1 | u8 | background | Palette index for the solid background |
| 0x24 | 4 | i32 | screen_limit | Screen limit with eight fractional bits |
Loading and commands
The loader requires the signature and reads the fields in file order. It raises
hex_lines to one when the stored value is below one. A zero shadow value disables
shadows; a nonzero value retains the existing shadow state.
The color commands cycle their GUI color indices modulo 16. Changing the paper
color also updates the stored background palette index, and is allowed only when
paper artwork is disabled. console limit accepts values from zero through 300
and stores the value multiplied by 256.
Initial settings select font zero, disabled shadows, color indices zero, 13, nine and zero, 16 hexadecimal lines, enabled paper artwork, background palette index zero and screen limit zero.
See Developer console for command syntax and presentation.
External References
Glide splash tables
The GOG Glide overlay contains authored geometry, animation transforms, and textures for the animated 3dfx splash.
rgpre::import::glide_splash::parse_glide_splash extracts data from the player’s
DOSBOX/glide2x_emu.ovl. It does not execute the overlay. This library API
supports the GOG LE table layout; other overlay versions and the
Windows 3DfxSpl2.dll animation resource use different layouts. The latter
is supported by parse_windows_glide_splash.
Overall Structure
[DOS header and LE executable header]
[LE object table and page map]
[Initialized writable object]
[Mesh pointers and vertex counts]
[Face pointers and triangle counts]
[Authored frame count and transforms]
[Vertex and face arrays]
[Texture descriptors, color tables, and mip chains]
Writable LE objects are reconstructed from their page maps. Table pointers are relative to their data object.
Mesh tables
There are three meshes, in shield, logo, outer-shield order:
| Mesh | Vertices | Triangles |
|---|---|---|
| Shield | 68 | 100 |
| Logo | 1694 | 1606 |
| Outer shield | 34 | 32 |
The table contains three vertex pointers, three vertex counts, three face pointers, three face counts, then the frame count. All scalar values are little-endian 32-bit words. There are 75 authored frames; each contains three column-major 4×4 float matrices in mesh order.
A vertex occupies 32 bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 12 | [f32; 3] | position | Object-space position |
| 12 | 12 | [f32; 3] | normal | Object-space normal |
| 24 | 8 | [f32; 2] | texture coordinates | Glide S/T coordinates |
A face occupies 20 bytes:
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
| 0 | 12 | [u32; 3] | vertices | Indices into this mesh’s vertex array |
| 12 | 4 | u32 | material | Material ID, 0 through 4 |
| 16 | 4 | u32 | edges | Three bits selecting antialiased triangle edges |
Textures
The three texture blocks begin with width, height, small LOD, large LOD, aspect
ratio, and Glide texture format, as six u32 values. A 1028-byte color-table
union follows, then the mip-chain byte length and pixel data. The largest mip
is first. The current API returns that mip as RGBA, without embedding assets
in the library.
| Texture | Dimensions | Glide format | Interpretation |
|---|---|---|---|
| Logo color | 64×64 | 1 | YIQ8 with an NCC color table |
| Highlight | 32×32 | 3 | Intensity8 |
| Shadow | 64×32 | 3 | Intensity8 |
Intensity8 produces (intensity, intensity, intensity, 255). It is opaque
grayscale, not an alpha mask.
YIQ8 uses the first 64 bytes of the color-table union: 16 unsigned luma bytes,
then four signed three-channel I vectors and four signed three-channel Q
vectors, each component stored in a u16 slot as a signed nine-bit value.
Sign extension uses bit 8: values 256 through 511 represent -256 through -1.
For pixel p, RGB is the clamped sum of
Y[p >> 4] + I[(p >> 2) & 3] + Q[p & 3]; alpha is 255.
See startup rendering for transforms, materials, and screen sequence.
External References
Windows Glide splash resource
3DfxSpl2.dll contains the animated metallic 3dfx logo with an orange swoosh in its PE ANIM resource.
This is a Glide driver asset, separate from the game’s Smacker cinematics and
from the older shield animation in the DOS overlay.
The DLL’s version metadata identifies the splash as a 1999 3dfx asset. The
GOG copies with file versions 1.00.00.0004 and 1.00.00.0005 have identical
ANIM resource payloads.
Overall Structure
[PE32 resource directory]
[ANIM / 101 / language]
[Version line]
[Animation frame count and frame rate]
[Texture records and mip chains]
[Material groups]
[Stored geometry slots and records]
[Animation command stream]
[PLUG / 107 / language]
[Static plug image in TGA format]
The resource directory supplies the payload’s RVA and length. Resource RVAs
map through the PE section table; they are not file offsets. No DLL code needs
to execute to extract the resources. ANIM starts with the newline-terminated
ASCII line 3Dfx Splash Version 1.2, followed by little-endian u32 frame
count and frame rate. The supplied animation has 90 timed frames at 30 fps.
Texture records
A u32 count precedes the textures. Each record has five u32 fields:
small LOD, large LOD, aspect ratio, Glide format, and an unused serialized
pointer. A u32 byte length and that many mip-chain bytes follow. The loader
uses the large LOD as both ends of the uploaded range.
The two supplied textures use square RGB565 data, with largest levels of 128×128 and 256×256. Large LOD values 1 and 0 select those dimensions; small LOD is 8, aspect ratio is 3, and format is 10. Mips descend to 1×1. RGB565 stores red in bits 11–15, green in bits 5–10, and blue in bits 0–4.
Materials
A u32 group count precedes groups. Each group begins with a u32 material
count, then records of six floats and a u32 texture index. The first three
floats are RGB factors; the next three are additional material factors. A
texture index of 0xffffffff means no texture. The supplied animation uses
two groups, each containing one material, referring to textures 0 and 1.
Stored geometry
A u32 slot count precedes indexed records. Each starts with a u32 slot ID,
then flags, triangle count, triangle indices, vertex count, vertices, texture
coordinate count, and texture coordinates. Slot ID 0xffffffff terminates
the records. The supplied animation fills all 74 slots.
Triangle indices are three u32 values per triangle. Vertices contain three
position floats and three normal floats. Texture coordinates are pairs of
floats. Only pairs corresponding to existing vertices are assigned; remaining
pairs are consumed and discarded. Geometry flag bit 3 selects generated
sphere-map coordinates instead of these authored coordinates.
Commands
Commands have a one-byte opcode followed by an unaligned payload. All scalar payloads are little-endian; matrices contain 16 column-major floats.
| Opcode | Payload | Meaning |
|---|---|---|
| 2 | None | Present a timed frame and advance the frame counter |
| 3 | Two u32, matrix | Draw a stored geometry slot; the second ID groups edge processing |
| 4 | Float, matrix | Horizontal field of view in radians and camera transform |
| 5 | None | Fade out and end playback |
| 6 | u32 | Select a material group |
| 7 | Seven floats | Set directional RGB and position inputs; the fourth float is skipped |
| 8 | Byte | Update the auxiliary toggle |
| 9 | u32 | Set alpha from the high byte; 0 and 0xff000000 select opaque |
| 11 | None | Start the playback clock and reset frame state |
| 12 | None | Flush pending edges without advancing the clock |
| 13 | None | Swap buffers without advancing the clock |
| 14 | Seven floats | Set positional RGB and light position; the fourth float is skipped |
The supplied resource uses these opcodes. The last timed frame is followed by two final redraws, a buffer swap, and the fade command. See startup presentation for the rendering rules.
External References
Bundled support resources
Bundled support files contain hardware configuration, controller presets, audio test data, and development reports.
Runtime roles
| Resource | Original role | Reference |
|---|---|---|
.DIG | Executable DOS drivers for digital sound hardware. DIG.INI selects the driver used for sample playback. | Miles drivers |
.MDI | Executable DOS MIDI drivers. The shipped engine disables MIDI initialization. | Miles drivers |
.OPL, .AD | FM MIDI instrument banks. These do not supply the CD background music. | Instrument banks |
DIG.INI, MDI.INI | Driver selection and sound-card I/O parameters. | Miles configuration |
MSSDRVR.LST | Driver selection criteria and installation messages for audio setup. | Driver catalog |
.PIF | Windows launch settings for the bundled DOS audio utilities. | PIF settings |
STATE.RST | Text editor settings and remembered file positions. | Editor state |
.SWG, .SWX | Microsoft SideWinder controller preferences. | Controller profiles |
fxart/more | Model filenames and version labels from Hugh’s 3D information tool. | Model report |
fxart.txt, fxart3dc.txt | Historical directory listings with model filenames, timestamps, and sizes. | Directory reports |
fxart/LOGFILE.TXT | Collision-sphere generation tool output. | Collision tool report |
sound/R212.WAV | PCM sample selected by the DIGPLAY.PIF audio test. | Audio test sample |
A reimplementation using modern audio APIs plays samples from SFX, RTX, and movie streams, and uses the CD audio tracks for background music. DOS driver images supply the original hardware interface. They do not supply the game’s sound effects, dialogue, or music recordings.
Controller presets describe one hardware setup. Game action bindings are separate settings in KEYS.INI. Historical reports record tool output at the time of generation. Current model headers and archive data define the installed assets.
Miles driver catalog
MSSDRVR.LST is a line-oriented text file. Sections use a driver filename
in brackets, such as [iwav.dig]. Lines beginning with ; are comments.
Dot-prefixed directives define selection conditions and setup behavior.
| Directive | Meaning |
|---|---|
.E NAME | Environment variable NAME must be present. |
.NE NAME | Environment variable NAME must be absent. |
.C TEXT | CONFIG.SYS must contain TEXT. |
.NC TEXT | CONFIG.SYS must not contain TEXT. |
.A TEXT | AUTOEXEC.BAT must contain TEXT. |
.NA TEXT | AUTOEXEC.BAT must not contain TEXT. |
.ND | The entry cannot be selected as the default. |
.T1 | If the BLASTER T parameter is present, its value must be 1 or 3. |
.T2 | The BLASTER T parameter must be present and equal to 2, 4, or 5. |
.T3 | The BLASTER T parameter must be present and at least 6. |
.IOB | Derive I/O defaults from the BLASTER environment variable. |
.IOD | Use the first default I/O configuration from the driver. |
.ION | Disable manual I/O configuration. |
.IOA | Configure only the I/O address. Appears at the end of the I/O options. |
.IOL | Suppress the high-DMA prompt. Appears at the end of the I/O options. |
.NAME | The following line supplies a displayed device name. |
.SPECIAL_TEXT | Begin the installation message block. |
.END_SPECIAL_TEXT | End the installation message block. |
Message blocks retain their line breaks. Device labels and setup criteria are
separate from the executable driver’s VDI header. For example, the catalog
supplies the WaveJammer device label for JAMMER.DIG, whose stored header has
no device-name block.
Directory reports
fxart.txt and fxart3dc.txt contain DOS directory output. The heading records
the drive label and the original E:\PROJECTS\REDGUARD\demo\fxart path.
Each entry records a date, a time, a decimal file size with optional comma
grouping, and a filename. The footer records totals and free disk space.
The listings describe the files at the time of the report. Their timestamps, sizes, and totals are historical metadata, not current asset requirements.
Collision tool report
fxart/LOGFILE.TXT begins with Collision Sphere Generation, version 1.5,
and identifies Alphacom Systems Limited as the tool author. Its output contains
Processing (N) file : NAME messages and runs of progress dots. Messages can
follow one another on the same line.
The report records tool activity. Collision data resides in the corresponding model files.
Audio test sample
R212.WAV is a RIFF/WAVE PCM file with one channel, eight-bit samples, and a
sample rate of 11,025 Hz. DIGPLAY.PIF selects R212.WAV as the argument to
DIGPLAY.EXE. The recording is an audio utility test input.
Cursor asset
system/pointers.bmp contains a GXA bitmap archive with 30 frames.
Its first chunk is BMHD; the filename’s .bmp extension does not describe
the container. This is a cursor image asset, separate from the controller
profiles and setup files.
The [system] pointers setting in SYSTEM.INI supplies the cursor archive
path. Cursor initialization consumes consecutive frame pairs. The first frame
of each pair supplies the displayed image. The second supplies storage read
during cursor-offset initialization. In the Glide path, bitmap preparation
replaces decoded pixel storage with texture identifiers before cursor-offset
initialization reads that storage.
In the software path, bitmap preparation retains the decoded pixel buffer. Cursor loading supplies a palette destination, which bypasses pixel remapping. The second frame of each pair is scanned in row order, from left to right. Every pixel with palette index 16 stores its column and row as the cursor offset, so the last matching pixel supplies the final offset. If no pixel matches, this scan does not write the offset fields. These pixel semantics apply to the software path; Glide preparation replaces the pixel buffer with texture identifiers as described above.
Glide preparation allocates a table of six 32-bit texture identifiers per frame. Cursor-offset initialization still scans a byte count equal to the frame width times its height. For the shipped cursor pairs, that scan exceeds the table’s 24-byte payload and reads beyond it. The scan therefore does not define an offset derived from the decoded marker image in the Glide path.
Initialization creates (frame_count - 1) / 2 cursor descriptors, with integer
division rounded toward zero, in a table of 64 slots. The 30-frame cursor
archive therefore supplies 14 cursors; its last two frames are not admitted.
The cursor draw routine uses the selected pair’s first frame and places its top-left corner at the pointer position minus the stored cursor offset. Each coordinate subtraction wraps to a signed 16-bit value before the image is drawn. Drawing requires the cursor archive to be loaded, the mouse driver to be initialized, cursor drawing to be enabled, and the selected descriptor to be present.
Initialization requests cursor zero. Selecting a cursor requires an initialized
mouse driver and enables cursor drawing. The interactive modes entered by
object select, scene, fxselect, and pvoaddpatch initialize the cursor
resources and call the cursor draw routine.
External References
Miles DOS Drivers (.DIG, .MDI)
.DIG and .MDI files are Miles Sound System hardware drivers with a shared VDI header and 16-bit x86 code.
.DIG drivers provide digital sample playback. .MDI drivers provide MIDI output or synthesis, including FM synthesis.
These are executable driver images, not audio containers such as SFX or RTX.
The image is loaded into DOS conventional memory and called through interrupt 66h.
It has no DOS MZ executable header or relocation table.
Overall Structure
[VDI header — driver identity, runtime configuration, interrupt entry, scratch space]
[Optional device name — 80-byte name block in newer headers]
[Driver body — callback tables, hardware data, and 16-bit machine code]
All multibyte values are little-endian. Header offsets are relative to the beginning of the file. The driver body has no common fixed layout. Its dispatcher offset comes from the header, rather than a fixed code position.
VDI Header
The common header is 0xBA bytes. Revisions 0x112 and later add an 80-byte device name, extending the header to 0x10A bytes.
AUDIODRV.DIG stores revision zero but includes this name block.
JAMMER.DIG and SNDSCAPE.DIG use revision 0x111; SNDSCAPE.MDI uses 0x100. These three images omit the name block.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 8 | [u8; 8] | id | AIL3DIG\x1A for digital audio or AIL3MDI\x1A for MIDI. The loader compares the first seven bytes without case sensitivity. |
0x08 | 4 | u32 | driver_version | Driver interface revision. Independent of the version of the host Miles library. |
0x0C | 4 | u32 | common_io_configurations | Packed DOS pointer to the driver’s default I/O configuration records. |
0x10 | 2 | u16 | num_io_configurations | Number of default I/O configuration records. |
0x12 | 4 | u32 | environment_string | Packed DOS pointer to the name of an environment variable used for hardware configuration. |
0x16 | 24 | — | io | Current hardware I/O parameters, described below. |
0x2E | 2 | i16 | service_rate | Background service frequency in Hz. A non-positive value disables periodic service. |
0x30 | 2 | u16 | busy | Non-zero while the driver is servicing a request. |
0x32 | 2 | u16 | driver_number | Runtime slot assigned by the loader. |
0x34 | 2 | u16 | this_isr | File-relative offset of the driver’s interrupt 66h dispatcher. |
0x36 | 4 | u32 | prev_isr | Packed DOS pointer to the previous interrupt 66h handler. |
0x3A | 128 | [u8; 128] | scratch | Shared workspace, including the hardware environment string passed to the driver. |
0xBA | 80, if present | [u8; 80] | dev_name | NUL-terminated ASCII device name, followed by padding. |
The default-configuration pointer, configuration count, environment pointer, I/O parameters, and service rate are zero in the shipped images. Driver initialization populates configuration fields in memory. A file reader cannot recover the driver’s default hardware settings by reading these zeros.
Packed DOS pointers contain a 16-bit offset in the low word and a 16-bit segment in the high word.
Their linear address is segment × 16 + offset. They are runtime addresses, not file offsets.
this_isr is the exception: it is a near offset within the loaded driver image.
The loader saves the previous interrupt vector in prev_isr and installs the dispatcher into the interrupt chain.
I/O Parameters
The io record occupies 24 bytes. Offsets in this table are relative to that record.
A value of -1 means that a hardware parameter is unspecified.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 2 | i16 | io_address | Sound-card I/O base address. |
0x02 | 2 | i16 | irq | Interrupt request number. |
0x04 | 2 | i16 | dma_8_bit | DMA channel for 8-bit transfers. |
0x06 | 2 | i16 | dma_16_bit | DMA channel for 16-bit transfers. |
0x08 | 16 | [i32; 4] | reserved | Driver-specific parameters. |
Driver Calls
The function ID is passed in register AX. The driver number in BX selects the installed image.
The dispatcher invokes a driver callback and returns through the interrupt interface.
Callback tables and their positions are private to each image.
| ID | Name | Purpose |
|---|---|---|
0x300 | DRV_INIT | Initialize the driver interface and publish configuration information. |
0x301 | DRV_GET_INFO | Return pointers to driver description and state tables. |
0x302 | DRV_SERVE | Perform background service. |
0x303 | DRV_PARSE_ENV | Parse hardware parameters from the shared scratch workspace. |
0x304 | DRV_VERIFY_IO | Detect or validate hardware configuration. |
0x305 | DRV_INIT_DEV | Initialize the hardware device. |
0x306 | DRV_SHUTDOWN_DEV | Shut down the hardware device. |
Digital playback and MIDI drivers add type-specific operations in the 0x400 and 0x500 ranges, respectively.
Hardware Drivers
| File | Interface | Hardware |
|---|---|---|
ESFM.MDI | MIDI synthesis | ESS ES1688, ES1788, and ES1888 enhanced FM audio. |
IWAV.DIG | Digital audio | InterWave audio processors. |
IWAV.MDI | MIDI synthesis | InterWave General MIDI. |
JAMMER.DIG | Digital audio | New Media Corporation WaveJammer PCMCIA sound cards. |
MDIMAXI.MDI | MIDI | Maxi Sound 64 or compatible hardware. |
Hardware selection and installation messages are separate from the binary header.
MSSDRVR.LST supplies selection rules and device labels, including a label for JAMMER.DIG, whose header has no name block.
DIG.INI and MDI.INI select the digital and MIDI driver filenames and their I/O parameters. See the configuration overview.
Background music uses CD audio, separate from these MIDI synthesis drivers.
Use by the Game
The game initializes digital audio from DIG.INI. The shipped configuration
selects SB16.DIG for SoundBlaster sample playback, including sound effects,
dialogue, and movie audio. The other DIG images provide alternative hardware
drivers.
The Miles library also contains a MIDI initialization path that reads
MDI.INI. The shipped engine disables that path at startup. Its MDI drivers
and OPL and AD instrument banks remain bundled MIDI resources,
separate from the CD audio used for background music.
The PIF files configure the bundled DIGPLAY.EXE and SETSOUND.EXE
utilities. They are Windows launch settings, not sound drivers or audio data.
A reimplementation with modern audio APIs uses the decoded sample data and engine playback rules. The DOS driver images describe the original hardware interface and are independent of those recordings. The whitespace-separated configuration syntax is described in Miles audio configuration. The accompanying setup messages and hardware-selection rules are described in Bundled support resources.
External References
- Syndicate Wars
mssal.h, VDI header and driver call definitions - Syndicate Wars
ail.c, Miles driver installation interface
PIF program settings
PIF files store Windows launch settings for DOS programs.
DIGPLAY.PIF and SETSOUND.PIF describe the corresponding sound utilities.
They contain a base header, Windows 386 settings, and a Windows VMM extension.
DIGPLAY.PIF launches DigPlay.EXE with argument R212.WAV.
SETSOUND.PIF launches SETSOUND.EXE without arguments. These settings belong
to the audio utilities’ Windows environment. They do not control sample
playback in a modern game engine. See Bundled support resources.
Overall Structure
[Base header — DOS program, directory, arguments, and display settings]
[PIFEX root — linked extension header referring to the base header]
[Extension records — headers and file-relative payloads]
The base header is 369 bytes. A base-only file has no extension chain. All multibyte integers are little-endian.
Base header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 2 | [u8; 2] | unknown_00 | Stored header bytes |
0x02 | 30 | [u8; 30] | title | Space-padded window title |
0x20 | 2 | u16 | memory_max_kb | Desired conventional memory, in KiB |
0x22 | 2 | u16 | memory_min_kb | Required conventional memory, in KiB |
0x24 | 63 | [u8; 63] | program | Executable filename |
0x63 | 1 | u8 | flags_63 | Launch flags; bit 0x10 closes the window on exit |
0x64 | 1 | u8 | start_drive | Startup drive byte |
0x65 | 64 | [u8; 64] | start_directory | Working directory |
0xA5 | 64 | [u8; 64] | parameters | Program arguments |
0xE5 | 1 | u8 | video_mode | Video mode byte |
0xE6 | 1 | u8 | unknown_e6 | Stored byte |
0xE7 | 1 | u8 | irq_low | Low interrupt vector byte |
0xE8 | 1 | u8 | irq_high | High interrupt vector byte |
0xE9 | 1 | u8 | rows | Screen rows |
0xEA | 1 | u8 | columns | Screen columns |
0xEB | 1 | u8 | window_y | Window Y position |
0xEC | 1 | u8 | window_x | Window X position |
0xED | 2 | u16 | unknown_ed | Stored word |
0xEF | 64 | [u8; 64] | unknown_ef | Stored bytes |
0x12F | 64 | [u8; 64] | unknown_12f | Stored bytes |
0x16F | 1 | u8 | flags_16f | Additional flags |
0x170 | 1 | u8 | flags_170 | Additional flags |
Text fields end at the first NUL or their fixed field boundary. Title padding is removed when displayed. Bytes after a NUL are outside the displayed string. Text fields contain legacy character bytes. A Unicode representation depends on the OEM or Windows code page used to display them.
Extension chain
Each extension header is 22 bytes. Its offsets are absolute file positions.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 16 | [u8; 16] | signature | NUL-terminated extension name |
+0x10 | 2 | u16 | next_offset | Next header position; 0xFFFF ends the chain |
+0x12 | 2 | u16 | data_offset | Payload position |
+0x14 | 2 | u16 | data_size | Payload size in bytes |
The root header starts at 0x171. Its signature is MICROSOFT PIFEX, its
payload offset is zero, and its payload size is 369. This payload is the base
header. Each later extension has its own signature, payload position, and size.
Windows 386 settings
The WINDOWS 386 3.0 extension contains a 104-byte settings record.
Its memory limits and arguments supersede the corresponding base fields for
enhanced-mode execution.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 2 | u16 | memory_max_kb | Desired conventional memory |
+0x02 | 2 | u16 | memory_min_kb | Required conventional memory |
+0x04 | 2 | u16 | foreground_priority | Foreground scheduling priority |
+0x06 | 2 | u16 | background_priority | Background scheduling priority |
+0x08 | 2 | u16 | ems_max_kb | Expanded memory limit |
+0x0A | 2 | u16 | ems_min_kb | Required expanded memory |
+0x0C | 2 | u16 | xms_max_kb | Extended memory limit |
+0x0E | 2 | u16 | xms_min_kb | Required extended memory |
+0x10 | 2 | u16 | option_flags | Execution options |
+0x12 | 2 | u16 | memory_flags | Memory options |
+0x14 | 2 | u16 | video_flags | Display options |
+0x16 | 18 | [u16; 9] | hotkey | Hotkey fields |
+0x28 | 64 | [u8; 64] | parameters | Enhanced-mode arguments |
Memory sizes are in KiB. 0xFFFF is retained as the stored limit value.
The WINDOWS VMM 4.0 payload occupies 428 bytes in both shipped utility files.
External References
OPL and AD instrument banks
OPL and AD files store Miles FM synthesis timbres indexed by MIDI patch and bank.
The sound directory contains sample.opl, Fatman.opl, sample.ad, and
Fatman.ad. Each AD bank contains the same bytes as its matching OPL bank.
These banks provide two-operator instrument settings for FM MIDI playback. See also
Miles DOS drivers.
The shipped game disables Miles MIDI initialization. These banks belong to the bundled FM MIDI system. Background music uses CD audio and does not use the banks.
Overall Structure
[Instrument index — patch and bank identifiers with file-relative offsets]
[Index terminator — FF FF]
[Timbre records — two-operator register settings]
All multibyte integers are little-endian. There is no file signature or instrument-count field.
Instrument index
Each entry is six bytes.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 1 | u8 | patch | MIDI patch identifier |
+0x01 | 1 | u8 | bank | Bank identifier |
+0x02 | 4 | u32 | offset | Absolute file offset of the timbre record |
The index ends when both identifiers are 0xFF. This terminator is two bytes
and has no offset field. Timbre offsets must point beyond the complete index.
Entries may share a timbre offset; record ordering need not match index ordering.
Timbre record
The two-operator record is 14 bytes, including its size word.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
+0x00 | 2 | u16 | size | Record size, 14 |
+0x02 | 1 | i8 | transposition | Melodic pitch adjustment or percussion note |
+0x03 | 1 | u8 | modulator.reg_20 | Operator characteristics and multiplier |
+0x04 | 1 | u8 | modulator.reg_40 | Key scale level and total level |
+0x05 | 1 | u8 | modulator.reg_60 | Attack and decay rates |
+0x06 | 1 | u8 | modulator.reg_80 | Sustain level and release rate |
+0x07 | 1 | u8 | modulator.reg_e0 | Waveform selection |
+0x08 | 1 | u8 | reg_c0 | Channel feedback and connection settings |
+0x09 | 1 | u8 | carrier.reg_20 | Operator characteristics and multiplier |
+0x0A | 1 | u8 | carrier.reg_40 | Key scale level and total level |
+0x0B | 1 | u8 | carrier.reg_60 | Attack and decay rates |
+0x0C | 1 | u8 | carrier.reg_80 | Sustain level and release rate |
+0x0D | 1 | u8 | carrier.reg_e0 | Waveform selection |
For melodic channels, transposition adjusts the incoming note. On the
percussion channel it supplies the note directly.
Register values are stored bytes, before playback adjusts pitch, volume, or channel assignment. The two-operator layout described here uses 14-byte timbre records. The banks contain synthesis parameters rather than recorded samples. Modern playback of the game’s sound effects and dialogue uses the audio in SFX and RTX.
External References
RST editor state
STATE.RST stores sectioned text editor settings and file-position records.
Its file entries refer to editor documents such as d:\redguard\sound\dig.ini.
These are remembered editing positions, separate from game save state and
runtime audio configuration. See Miles audio configuration.
Overall Structure
[Section header — bracketed section name]
[Assignments — ordered key=value lines]
[Additional sections — further headers and assignments]
The file is text with CRLF line endings. There is no binary header or byte-order requirement.
Sections and assignments
STATE.RST contains these sections:
| Section | Keys |
|---|---|
edit- | screen, toggles, srch, src, rpl, file |
brief | file |
shared- | pmark |
Section names retain their trailing hyphens. Assignments split at the first
equals sign. Empty values are valid. Values can contain Windows paths,
space-separated fields, and further equals signs, such as c=0 in a file
record. Repeated file keys represent separate editor records, so section and
entry order are part of the stored state.
[edit-]
screen=80 50
toggles=1 1 0 1 0 0
srch=
file=d:\redguard\sound\dig.ini 1 1 1 7
Blank lines separate records, and lines beginning with a semicolon are comments. Semicolons within values remain part of the value. Each assignment belongs to the preceding section header.
The file contains legacy text bytes. Their displayed characters depend on the editor’s code page.
External References
SideWinder controller profiles
SWG and SWX files store Microsoft SideWinder controller preferences and control mappings.
REDGUARD.SWG is the profile for the Microsoft SideWinder Gamepad.
redguard.swx is the profile for the Microsoft SideWinder Freestyle Pro.
Both use the same version 3 binary layout.
Overall Structure
[Header — size, version, signature, device identifier, and settings]
[Control records — fixed-size labels and mapping data]
Integers use little-endian byte order. The header occupies 142 bytes, and each control record occupies 684 bytes. Each shipped profile contains 27 control records and occupies 18,610 bytes.
Header
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 4 | u32 | file_size | Total file size, including the header. |
0x04 | 4 | u32 | version | Profile version, 3. |
0x08 | 4 | u32 | field_08 | Zero in the shipped profiles. |
0x0C | 10 | [u8; 10] | signature | ASCII SideWinder, without a terminating NUL. |
0x16 | 16 | [u8; 16] | device_id | Device identifier bytes. |
0x26 | 104 | [u8; 104] | settings | Opaque controller settings. |
The Gamepad identifier is 84 CE 48 C9 26 90 D0 11 9F FE 00 A0 C9 11 F5 AF.
The Freestyle Pro identifier is 86 CE 48 C9 26 90 D0 11 9F FE 00 A0 C9 11 F5 AF.
Control Records
Offsets in this table are relative to the beginning of a control record.
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0x00 | 64 | [u8; 64] | name | Label ending at the first NUL, or at the end of the field. |
0x40 | 4 | u32 | control_mask | Controller input the record assigns. See Record order. |
0x44 | 4 | u32 | field_44 | 0 or 1. |
0x48 | 4 | u32 | field_48 | 1 in every record that sends keys, 0 otherwise. |
0x4C | 4 | u32 | field_4c | Zero in the shipped profiles. |
0x50 | 1 | u8 | key_count | Number of keys the record sends at once. |
0x51 | 2 × key_count | [[u8; 2]] | keys | Keys sent: a PC keyboard scancode followed by a prefix byte, 0xE0 for extended keys such as the arrow keys and 0x00 otherwise. |
| — | — | — | — | Remaining bytes up to 0x2AC hold further mapping settings; 0x57 and 0x59 (u16) and 0x5D (u16, 0xFFFF) are nonzero in records that send keys. |
Record order
Both profiles use the same 27-record order:
| Records | Assigned input | control_mask |
|---|---|---|
| 0–7 | Buttons A, B, C, X, Y, Z, Left Trigger, Right Trigger | One bit each, 0x0001 through 0x0080 |
| 8 | Start | 0x0100 |
| 9–17 | The same nine controls while the modifier button is held (Button M on the Gamepad, Shift on the Freestyle Pro) | Control bit plus 0x0200 |
| 18–25 | Up, Up + Right, Right, Down + Right, Down, Down + Left, Left, Up + Left | One bit each, 0x0001 through 0x0080 |
| 26 | Unlabelled | 0x0100 in the Gamepad profile, 0x0001 in the Freestyle Pro profile |
Shipped mappings
REDGUARD.SWG (Gamepad), scancodes in decimal:
| Control | Label | Key |
|---|---|---|
| A | Use/Attack | 29 (Ctrl) |
| B | Jump | 56 (Alt) |
| C | Inventory | 23 (I) |
| X | View/Defend | 42 (Left Shift) |
| Y | Walk | 57 (Space) |
| Z | Quick Sword | 31 (S) |
| Left Trigger | Last Item | 51 (comma) |
| Right Trigger | Next Item | 52 (period) |
| Start | Menu | 1 (Escape) |
| M + Left Trigger | Log | 38 (L) |
| M + Right Trigger | (no label) | 50 (M) |
| D-pad directions | direction labels | Extended arrow keys; diagonals send two arrow keys |
The labels are text only; the keys determine the effect, through the game’s
keyboard bindings. redguard.swx (Freestyle Pro) sends keys only for Button Y
(I), Button Z (S), Left Trigger (comma), Right Trigger (period), and Start
(Escape).
An empty label does not remove its record. Bytes following the first NUL remain part of the stored field. Text bytes map directly to Unicode code points U+0000 through U+00FF without selecting a Windows code page.
These profiles are controller configuration files. They do not contain game assets, save state, or swap memory. KEYS.INI describes the separate game keyboard bindings.
Use by the game
Neither RGFX.EXE nor the REDGUARD.EXE launcher opens these files. They are
read by Microsoft’s SideWinder software on the host system, which sends PC
keyboard scancodes for controller input; see Shipped
mappings. The game receives those keystrokes through the
keyboard bindings in KEYS.INI, not through its joystick
input.
The GOG release runs RGFX.EXE in DOSBox, where the SideWinder software is not
involved, so these profiles have no effect. Controller input reaches the game
only through DOSBox’s emulated gameport or keyboard mapper; see
KEYS.INI: GOG release.
See Bundled support resources for the other support files shipped with the game.
External References
MORE model information report
The extensionless more file is a text report of model filenames and exporter versions from Hugh’s Marvelous 3D Information Program.
Overall Structure
[Program heading — tool name and version]
[Copyright line — publisher notice]
[Blank line — heading separator]
[Model entries — filename and version separated by a tab]
[Blank line — footer separator]
[Footer — declared number of files]
The report uses ASCII text and CRLF line endings. Its heading identifies
version 2.2 of the information program.
Model Entries
Each entry contains a .3D filename, one tab, and a version label. The
label consists of v, a decimal major version, a period, and a decimal
minor version. Entries retain their original order.
XBUCKETE.3D v2.7
TV_BTTL3.3D v2.7
XWANTED.3D v4.0
The report ends with a decimal count followed by files found..
The shipped report lists 65 models and ends with 65 files found..
These are report-time version labels. They can differ from the versions in
the installed 3D model headers. For example, the report lists
XBUCKETE.3D as v2.7, while the installed file has a v5.0 header.
The report is not a model archive and contains no geometry or textures.
The related fxart.txt and fxart3dc.txt files are directory listings rather
than version reports. They record historical model sizes and timestamps. See
Bundled support resources.
External References
Configuration
INI settings for worlds, actors, rendering, input, and presentation.
Settings and runtime behavior
| Area | Configuration | Engine reference |
|---|---|---|
| World rendering | WORLD.INI, FOG.INI | Lighting, fog, sky, water |
| Actors and input | COMBAT.INI, ITEM.INI, KEYS.INI | Movement, animation, attachments |
| Menus and movies | MENU.INI | Book menu, cinematics |
| Sound | SURFACE.INI, Miles settings | Positional sound, music |
SYSTEM.INI contains the shared engine settings, including camera, dialogue, rendering, and debug options.
Game Root Directory
| File | Size | Docs | Description |
|---|---|---|---|
SYSTEM.INI | 5.2 KB | system-ini.md | Primary engine configuration: rendering, gameplay, camera, dialog, debug, and 3D subsystem parameters. |
COMBAT.INI | 54 KB | combat-ini.md | Combat system: attack/defense moves, combos, and dialogue taunts for all combatants. |
ITEM.INI | 30 KB | item-ini.md | Item database: all collectible objects, weapons, potions, keys, and quest items. |
WORLD.INI | 19 KB | world-ini.md | World/level database: map files, palettes, lighting, sky, weather, and PVO node maps. |
MENU.INI | 42 KB | menu-ini.md | Menu system layout: page structure, text placement, textures, and movie definitions. |
KEYS.INI | 3 KB | keys-ini.md | Input bindings: keyboard scancodes, mouse buttons, and joystick axes to game actions. |
REGISTRY.INI | 302 B | registry-ini.md | File-system abstraction: archive lookup and 32-bit file access (non-functional in shipped game). |
SURFACE.INI | 4.3 KB | surface-ini.md | Texture surface-class assignments and animation sound remapping. |
Asset Directory Files
| File | Path | Docs | Description |
|---|---|---|---|
FOG.INI | fxart/ | fog-ini.md | Fog density ramp table for the terrain renderer. |
DIG.INI | sound/ | miles-ini.md | Miles digital audio driver configuration. |
MDI.INI | sound/ | miles-ini.md | Miles 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).
Settings use key=value records grouped by section. Semicolons introduce
comments. Switches use 0/1, no/yes, off/on, or false/true. Most numeric fields
contain decimal integers, including negative position offsets and thresholds.
Camera glide factors and cam_prox_up contain decimal floating-point values.
The [3dfx] scale and font-color settings contain comma-separated integer pairs.
The integer configuration reader maps true, yes, and on to 1, and
false, no, and off to 0. Missing or empty values produce 0. Other values
use signed decimal conversion; numeric values are not normalized to booleans.
In particular, [debug] disable_family clears actor family links only when its
value is exactly 1. See actor initialization.
The descriptions below describe the Glide executable, RGFX.EXE. Several keys
in the shipped file are never read by it, and several are read but have no
effect. Those keys are marked Not read and No effect. A few keys the
engine reads are absent from the shipped file; they are listed at the end of
their section with the behavior of the missing value.
Map, world, and model distances are in engine world units. Angles use 2048 units per full turn. “Q8” values are world units multiplied by 256. “Ticks” are nominal game ticks; see frame scale.
File Structure
Standard Windows-style INI file with 10 sections:
[screen]— display and movie settings[system]— core engine paths, audio, physics, object pools, and HUD layout[debug]— developer diagnostics and logging flags[game]— falls, slides, ropes, and interaction distances[cyrus]— player turning and input parameters[3dmanager]— 3D object cache and memory budget[xngine]— clipping planes, ambient light, and sky switch[camera]— camera rig offsets, distances, and glide factors for each mode[dialog]— dialogue menu layout, subtitles, and speech settings[3dfx]— Glide resolution, texture reduction, and text colors
[screen]
| Key | Default Value | Description |
|---|---|---|
candle_mode | 2 | Candle HUD visibility: 0 = only while the show timer runs, 1 = always, 2 = always with the health number. Any other nonzero value behaves as 1. F8 cycles 0→1→2 and writes the value back. See candle. |
colour_bits | 8 | Not read. |
resolution | 1 | Software-renderer mode index (0/3/4 = 320×200, 1 = 640×480, 2 = 320×400; out-of-range values become 0). It does not change the display in the Glide executable, whose display mode comes from [3dfx] resolution. |
Palette_red | 0 | No effect. Palette brightness offset of the software renderer’s Gamma option; the Glide executable never creates that option. |
Palette_green | 0 | Not read. Written only by the software renderer’s Gamma option. |
Palette_blue | 0 | Not read. Written only by the software renderer’s Gamma option. |
smk_interlace | 0 | Movie interlace effect: nonzero draws a black line over every odd output scanline of each movie frame. 0 = off. The menu’s interlace option toggles it. See movies. |
Keys read but absent from the shipped file: use_uvblib, linear_copy_mode,
and linear_interlace. All three have no effect.
[system]
world_ini and item_ini point to WORLD.INI and ITEM.INI respectively, which the engine loads for world and item definitions.
Files and fonts
| Key | Default Value | Description |
|---|---|---|
game_bitmap | system\powerup.gxa | GXA for the two HUD power-up slots (frame 0 gauntlet, frame 1 shield). The path is stored in save games and restored when one is loaded. |
pointers | system\pointers.bmp | Cursor GXA bitmap archive, stored with a BMP extension. A pixel of color index 16 marks each cursor’s hotspot. Loaded only by a developer mode enabled from the command line. |
system_font | fonts\redguard.fnt | Not read. |
icon_font | fonts\arialvs.fnt | Not read. |
gui_font | fonts\arialbg.fnt | Not read. |
gui_low_font | fonts\arialvb.fnt | Font loaded at startup as font-table entries 1 and 2 (see reader style table). A missing file stops startup. |
animation_drive | D:\ | CD drive path. Movie filenames are prefixed with it (a \ is added when missing), and its first letter selects the drive opened for CD music. Without it, movies load from the bare filename and no CD music plays. |
back_texture | 0 | Not read. |
rtx_filename | ENGLISH.RTX | RTX dialogue file (voice and text). Without it, the first dialogue line stops the game with an error. |
world_ini | WORLD.INI | INI file holding the [world] section (WORLD.INI). Absent means SYSTEM.INI. |
item_ini | ITEM.INI | INI file holding the [ITEMS] section (ITEM.INI). Absent means SYSTEM.INI. |
Sound and music
| Key | Default Value | Description |
|---|---|---|
volume | 255 | Master sound-effect volume: every sample volume is scaled by volume / 256. Missing means silence. The menu Sound slider sets it. |
sound | 1 | Enables sound. 0 skips sound startup entirely: no effects, voices, or CD music, and subtitles are forced on with speech off. The nosound command-line option also disables it. |
fast_sound | 1 | Miles Sound System mixer timing. Nonzero keeps the Miles defaults: a 100 ms mixing buffer, mixer serviced at 200 Hz, interrupts enabled while mixing. 0 halves the buffer to 50 ms, services the mixer at 250 Hz, and disables interrupts while it mixes. Output is 22050 Hz 16-bit stereo either way. |
fidelity | 0 | Load-time quality reduction for sound effects and voice clips. See Sound fidelity. |
redbook | on | Enables CD music. Requires sound on and animation_drive naming the CD drive. |
redbook_volume | 200 | CD music volume, clamped to 0–255 and halved to the Miles 0–127 range; the shipped 200 plays at 100 of 127. Applied when each CD track starts. The menu Music slider sets it. See music volume. |
sound_distance | 64 | Sound radius: sound_distance × 36 world units (shipped 2304); see positional sound. Values of 3 or less make the divisor zero. |
Sound fidelity
fidelity converts every sample as it loads, both the MAIN.SFX effect bank
and RTX voice clips. Conversion keeps every other sample
frame to halve the rate and keeps the high byte to turn 16-bit samples into
8-bit ones. No filtering is applied.
| Value | 16-bit stereo | 16-bit mono | 8-bit stereo | 8-bit mono |
|---|---|---|---|---|
0 | unchanged | unchanged | unchanged | unchanged |
1 | half rate | half rate | unchanged | unchanged |
2 | 8-bit stereo | 8-bit mono | unchanged | unchanged |
3 | 8-bit stereo, half rate | 8-bit mono, half rate | half rate | half rate |
4 | 8-bit mono (left channel), half rate | 8-bit mono, half rate | mono (left channel), half rate | half rate |
Other values leave samples unchanged. Halving the rate turns a 22050 Hz sample into 11025 Hz and an 11025 Hz sample into 5512 Hz.
Frame rate and movement physics
| Key | Default Value | Description |
|---|---|---|
normal_frame_rate | 12 | Nominal game tick rate. The per-frame scale is normal_frame_rate × 256 / fps, so game time advances normal_frame_rate ticks per second regardless of frame rate. 0 means 12. See frame scale. |
use_smooth_fps | yes | When on, the frame scale uses a smoothed FPS value instead of the raw measured FPS. Each update moves the smoothed value one step toward the measured value, or half the difference for gaps of 5 or more. |
use_smooth_divisor | yes | When on, the frame scale steps toward its new target over several frames instead of jumping to it. |
min_frame_rate | 6 | Lowest FPS used in the frame-scale division. Below this rate game time slows down rather than taking larger steps; the shipped value caps each frame at two nominal ticks. |
max_frame_rate | 300 | Highest FPS used in the frame-scale division. It does not cap rendering: above this rate each frame still advances by the step for max_frame_rate, so game time runs faster than real time. 0 means 36. |
jump_time | 6 | Default for actor attribute attr_jump_time when the actor’s value is zero or negative (low byte used). Values below 1 mean 6. |
jump_height | 80 | Default for actor attribute attr_jump_height when the actor’s value is zero. The engine stores bits 8–15 of this value, so the shipped 80 (and the fallback 80 for values below 1) leaves the attribute at zero. |
post_collide_height | 50 | Vertical probe height in map units for actor collision: forward-obstacle probes, step and support checks, body-shape sphere layout, and the falling-speed limit. See player movement. |
hpost_collide_height | 50 | No effect. |
pre_validate | no | When on, actors outside living states 27–32 skip the forward obstacle probes during movement sub-steps while the measured frame rate is at least 6. |
sphere_object_scale | 256 | Collision-size scale in 1/256 units, applied at load to static-model X/Z bounds and actor collision-sphere radii (radius minimum 1). 256 leaves sizes unchanged; values below 1 mean 256. |
standing_height | -2 | Vertical offset in map units added when an actor is placed on ground support or a ledge. The shipped -2 sinks actors two units into their support. |
slide_range | -60000 | Wall-slide threshold for the dot product of the normalized movement and the contact normal. Contacts above it slide along the wall; at or below it the actor stops. See body-contact response. |
floating_point_physics | yes | No effect. |
Map loading and engine subsystems
| Key | Default Value | Description |
|---|---|---|
statics_load | yes | When off, map loading skips MPSO static objects. |
flats_load | yes | When off, map loading skips MPSF flat objects. |
objects_load | yes | When off, map loading skips all MPOB records: actors, markers, and object local lights. |
lights_load | yes | When off, map loading skips MPSL light records. |
ropes_load | yes | When off, map loading builds no MPRP ropes. |
task_system | yes | Initial value of the task cheat: on runs actor entry, task, and main script stages. See cheats and SOUP update gates. |
animation_system | yes | Initial value of the animation cheat: on runs actor animation playback and animation-gated combat timing. See cheats. |
start_fade | on | Whether the first world entered at startup without the book menu (first run with no autosave) fades the loading screen out and the world in from black. 0 cuts directly. Later world loads always fade. |
max_active_objects | 32 | Reusable actor-pool capacity, clamped to at least 10. |
max_effects | 32 | Capacity of the environmental effect pool. Negative values become 0; 0 disables the pool. |
max_particles | 512 | Capacity of the particle array used by ScriptParticles. 0 or negative disables particles. |
max_remap_objects | 64 | Capacity of the list of moved actors waiting to be re-filed in the map grid (console show remap), clamped to at least 32. Exceeding it raises runtime error 196. |
disable_effects | no | When on, the environmental effect pool is not created. |
Text and startup checks
| Key | Default Value | Description |
|---|---|---|
disable_text | yes | Initial state of the engine’s formatted-text suppression flag. While set, diagnostic text printers draw nothing. Menus, loading progress, and other screens clear the flag around their own text, and HUD prompts and subtitles ignore it. |
disable_debug_text | yes | Initial state of queued debug-text suppression. The shipped yes hides queued debug text. |
normal_ink | 1 | No effect. |
lock_windows | no | Not read. |
disable_drive_check | yes | When off, startup enumerates drive letters for the console show machine report. It does not gate startup. The nodrive command-line option forces it on. |
disable_cpu_check | yes | When off, startup detects the CPU type for the show machine report. It does not gate startup. The nocpu option forces it on. |
disable_svga_check | yes | When off, startup identifies the SVGA chipset for the show machine report; otherwise the report shows “SVGA TEST DISABLED”. The nosvga option forces it on. |
report_machine | no | When on, startup runs the console command show machine. The console opens for it only when disable_debug_text is off. |
HUD layout
| Key | Default Value | Description |
|---|---|---|
compass_xco | 546 | Compass top-left X. |
compass_yco | 396 | Compass top-left Y. |
candle_xco | 12 | Candle top-left X. |
candle_yco | 8 | Candle top-left Y. |
logbook_xco | 540 | Logbook icon top-left X. |
logbook_yco | 20 | Logbook icon top-left Y. |
pickup_xco | 12 | Active item icon top-left X, and start X of the prompt text. |
pickup_yco | 386 | Active item icon top-left Y. |
pickup_text_yco | 436 | Top Y of the HUD prompt text line. |
game_xco1 | 576 | Power-up slot 1 X. |
game_yco1 | 6 | Power-up slot 1 Y. |
game_xco2 | 576 | Power-up slot 2 X. |
game_yco2 | 96 | Power-up slot 2 Y. |
Key read but absent from the shipped file: archive_file_first. 1 opens a
loose file in preference to an archived copy; 0 (the missing value) searches
.ZAP archives first and falls back to the loose file. It has no effect on an
install without .ZAP archives.
[debug]
Developer diagnostics, logging, and display flags. Most are disabled in the shipped build.
| Key | Default Value | Description |
|---|---|---|
final_version | 1 | Selects release behavior. 0 selects developer mode, which also enables the queued developer text used by other [debug] displays. See Developer mode. |
enable_logs | 0 | Log output level. 0 writes no logs, 1 appends engine messages to *.LOG files in the game directory, 2–3 echo them to the console only, and 4–5 do both; 3 and 5 also open the console at startup. A fatal error switches logging to 1. |
map_log | no | No effect. |
console_error | yes | 1 prints the fatal-error report in the in-game console before the game shuts down. Any other value leaves only the report printed after exit. |
software_interrupt | yes | Enables the engine’s debug-break checks during logging and input waits: a developer key combination triggers a debugger breakpoint, and a pending break request ends or interrupts the session. Required by software_break. |
software_break | yes | 1 lets a break request from the keyboard handler exit the game during startup, before the first world starts. The engine clears it when gameplay begins. |
attempt_recover | no | Nonzero makes fatal errors non-fatal: the engine shows the error report in the console and keeps running instead of exiting. Also forces console_error on. |
object_log | no | 1 restarts OBJECT.LOG each time a world loop begins. Requires nonzero enable_logs. |
video_log | no | Not read. |
node_marker | no | Initial value of the nodemarker cheat slot, which has no effect. See cheats. |
task_debug | no | Nonzero turns invalid script operations (unknown opcodes, out-of-range function numbers, failed lookups) into fatal errors; 1 also range-checks property access by index. |
memory_manager | 0 | Heap-tracking mode. 0 is off, 1 counts allocated bytes, and 2 or higher also records each block so that freeing an unknown pointer is a fatal error. Nonzero modes add memory statistics to error reports. |
monitor_object | (empty) | Name of an actor to monitor. The matching actor is selected for the console’s per-frame animation-state readout and for fatal-error reports. |
display_masters | no | No effect. |
display_slaves | no | No effect. |
display_edges | no | Nonzero prints the player’s ledge-detection candidates and results as developer text. Visible only in developer mode. |
ignore_errors | yes | No effect. The ie command-line option sets the same unused value. |
disable_family | no | 1 clears every actor’s master/slave attributes at creation. Any nonzero value also skips building the world’s family tree at world load. |
disable_master_slaves | no | Nonzero excludes actors that are both master and slave from object updates. |
disable_slaves | no | Nonzero excludes slave actors that are not also masters from object updates. |
family_log | no | 1 writes each world’s actor family tree to FAMILY.LOG at world load. Requires nonzero enable_logs and disable_family=0. |
script_log | no | No effect. The console log command writes SCRIPT.LOG. |
show_manager | no | No effect. |
display_node_map | no | Nonzero draws a marker sprite above every navigation node of the loaded node maps. |
display_nodes | no | Nonzero draws marker sprites at each navigating actor’s movement target and current navigation node. |
display_markers | no | Nonzero draws a marker sprite at every enabled world marker (RGM MPMK). |
network_marker_file | g:\PROJECTS\REDGUARD\DEMO\NETWORK.MRK | No effect. The shipped value is a leftover developer path. |
object_system_log | no | No effect. |
convert_static_angles | no | Nonzero forces [3dmanager] dummy_manager on. It performs no angle conversion. |
def_checksum | yes | Nonzero makes world loading require the RGM’s stored SOUP386.DEF checksum (the byte sum of the file) to match the installed SOUP386.DEF; a mismatch is a fatal error. |
render_log | no | Nonzero logs the draw list of one rendered frame, then switches itself off. Works regardless of enable_logs. |
idebug | 0 | Nonzero sends log output to a remote network monitor session instead of log files. |
idebug_refresh | no | With idebug, nonzero makes each log message wait for the remote monitor to acknowledge it. |
memory_monitor | no | Nonzero shows an on-screen free-memory readout and a scrolling graph of heap usage changes. It also forces on-screen text on. |
Key read but absent from the shipped file: camera_log. Nonzero restarts
CAMERA.LOG when the camera system starts, if enable_logs is nonzero. No
camera messages are written to it.
[game]
| Key | Default Value | Description |
|---|---|---|
fall_bounce_height | 100 | Minimum fall distance in map units before a falling actor that overlaps an actor with nonzero attr_bounce_power tries to land on it. Shorter falls use the ordinary overlap response. |
fall_death_height | 568 | Fall distance in map units above which landing is fatal (health change -256). Landings on surfaces 1 through 4 count as zero distance. |
fall_hurt_height | 300 | Fall distance in map units above which landing hurts: animation group 73 and a health change of -fall_hurt_zap. |
fall_hurt_zap | 10 | Health removed by a landing that exceeds fall_hurt_height but not fall_death_height. |
slide_hurt_height | 1024 | Accumulated slide distance in map units above which ending a slide hurts the actor (knock-back, group 73, -slide_hurt_zap health). |
slide_hurt_zap | 10 | Health removed when a slide longer than slide_hurt_height ends. |
rope_jump_add | 18 | Base speed of a jump off a rope, in map units. A swinging rope adds its node speed on top. |
rope_attach_angle | 512 | Largest heading difference at which the player can grab a directional rope; masked to 0–1023, and 0 means 256. |
slide_speed | 36 | Slope-slide speed: each update moves slide_speed / 2 map units at nominal frame scale. Values below 8 become 8. |
rtx_pickup_override_time | 24 | Ticks for which holding the previous/next item key suppresses an activation prompt and shows the item icon. |
swim_depth | 40 | Model Y offset in map units for animation groups 141 through 143 on the non-inhibited surface-2/3/4 model-position path. Zero is replaced with 40. |
player_dead_time | 3 | No effect. |
player_fall_dead_time | 12 | Falling-death timeout input. Clamped to at least 3, then multiplied by 12 nominal ticks; the world loop exits only after the elapsed value exceeds that limit. |
old_combat | off | Not read. |
lineup_distance | 128 | Distance in map units at which PlayerLineUp switches from walking the player toward the target to aligning in place. |
dialog_radius | 512 | Player-proximity radius in map units for walk-route navigation: while the exclusion switch is on, route nodes closer than this to the player are not used. |
combat_sphere_scale | 200 | Radius scale in 1/256 units for the secondary (fighting) collision shape: each sphere radius becomes the larger of two opposite bounds extents times this value divided by 256. 0 means 230. |
[cyrus]
| Key | Default Value | Description |
|---|---|---|
sheath_sword_delay | 6 | No effect. Read at startup (0 becomes 6) but not used. |
auto_defend | 1 | No effect on combat. F7 toggles the value, shows the RTX line AUTO-DEFEND MODE ON or AUTO-DEFEND MODE OFF (#don/#dof), and writes it back to this file. |
walk_mode | 0 | No effect on movement. Tab toggles the value, shows the RTX line WALK MODE ON or WALK MODE OFF (#won/#wof), and writes it back to this file. |
turn_speed | 12 | Turn acceleration: added to the held-direction turn rate each input update while the rate is below turn_max_speed. |
turn_max_speed | 48 | Turn rate limit in heading units per update at nominal frame scale. Acceleration stops once the rate reaches it. |
mouse_turn | 0 | Nonzero enables mouse turning: horizontal mouse motion plays the turn animation groups (14/15, or 16/17 with a burning torch). Only the low byte is used. |
joy_tolerance | 60 | Joystick dead zone: an axis registers as left/right/up/down only when its reading is more than this many units from the calibrated center. |
camera_distance | 200 | Not read. The camera uses [camera] camera_distance. |
tap_time | 12 | Multi-tap window: a press counts as a repeated tap only when it follows a release within this many updates at nominal frame scale (minimum 6). 0 means 3. |
poly_push_units | 16 | Minimum separation distance in map units used when pushing an actor out of face geometry. Values below 4 become 4. |
auto_grab | off | 1/on makes the forward-obstacle response try the ledge-grab path automatically; any other value disables it. |
smooth_post_min | 2 | No effect. |
smooth_post_max | 10 | No effect. |
smooth_post_divisor | 2 | No effect. |
[3dmanager]
| Key | Default Value | Description |
|---|---|---|
buffer_kbytes | 800 | 3D manager workspace size in kilobytes as shown in the developer console. No allocation uses it. Values below 100 become 100. |
heap_kbytes | 22000 | 3D object manager heap size in kilobytes. Values below 500 become 500. |
max_objects | 255 | Number of 3D object slots in the object manager and its cache table. Values below 10 become 10. |
compress | 1 | Not read. |
save_compressed | 0 | Not read. |
cache_objects | yes | When on, loaded 3D objects stay in a slot cache until they have been unused for cache_lifetime main-loop iterations. Off disables the cache. |
cache_lifetime | 64 | Number of main-loop iterations an unused cached 3D object is kept before it is released. |
dummy_manager | 0 | 1 replaces the 3D object manager with a stub that loads no model data. The dm command-line option forces it on. |
in_view_enabled | yes | No effect. |
shutdown_mode | yes | Not read. |
[xngine]
| Key | Default Value | Description |
|---|---|---|
texture_kbytes | 12000 | No effect. Shown only in the developer console header as the texture heap size. |
gfx_kbytes | 256 | No effect. |
front_plane | 7 | Near clipping distance of the scene. See fog and clipping. |
back_plane | 3800 | Far clipping distance of the scene; a nonzero world_back_plane[N] replaces it for that world. See fog and clipping. |
perspective_low_x … perspective_ultra_y | 190–800 | Not read. All eight perspective_* keys. |
detail | 8 | No effect. |
ambient_light | 32 | No effect. Initial environmental-effect brightness, replaced by world_ambientfx[N] on every world load. |
screen_scale | 256 | No effect. Shown only by the console show xngine report. |
haze_depth | 768 | No effect. Scene fog comes from FOG.INI. |
sky_disable | 0 | 1 disables the sky in every world; any other value leaves it enabled. |
sky_move | 1 | No effect. |
sky_xrotate | 3 | No effect. |
sky_yrotate | 40 | No effect. |
game_detail | 2 | No effect. Shown only by the console show xngine report. |
exclusion | 0 | Not read. |
[camera]
Camera rig configuration for the normal, combat, hanging, rope, and debug cameras. Each camera orbits a pivot point near the player at its own distance and angles. See the camera sections of SOUP for the runtime behavior.
Pivot, target, and normal camera
| Key | Default Value | Description |
|---|---|---|
target_offset_x / _y / _z | -6000 / -14000 / 0 | Camera target point relative to the player, in Q8 units in the player’s rotated frame. It is the look-at point and the start of the camera’s obstacle test. Applies to the normal and combat cameras; the normal camera uses only Y. |
offset_pos_x / _y / _z | 0 / -14000 / 0 | Camera pivot offset from the player in Q8 units, rotated with the player. The camera sits distance behind this point; the shipped Y puts it about 55 units above the player origin. |
offset_angle_x / _y / _z | 0 / 0 / 0 | Normal camera orbit angles (pitch, yaw, roll) relative to the player. 0 places the camera straight behind the player. |
camera_distance | 250 | Normal camera distance behind the pivot, in world units. |
camera_min_distance | 120 | When an obstacle pulls the normal or combat camera closer than this, the camera rises so that its height above the player scales by the ratio of minimum to actual distance. |
camera_left_pos / camera_right_pos | 5000 / 5000 | No effect. |
Mode cameras
All angle triples are pitch, yaw, and roll.
| Key | Default Value | Description |
|---|---|---|
camera_combat_angle_offset_x / _y / _z | 134 / 245 / 0 | Combat camera orbit angles relative to the player, used while the player holds a weapon. |
camera_combat_distance | 260 | Combat camera distance in world units. |
camera_hang_angle_offset_x / _y / _z | 256 / 0 / 0 | Hanging (ledge) camera orbit angles relative to the player. |
camera_hang_distance | 250 | Hanging camera distance in world units. |
static_rope_threshold | 5 | Ropes whose maximum swing amplitude (MPRP swing) is below this value are static. On a static rope the camera uses the rope-above settings while the player holds a link in the upper two thirds and the rope-below settings otherwise. Other ropes use the rope settings. |
camera_rope_max_vel | 18000 | Movement limit of the swinging-rope camera, in the units of max_vel. 0 means max_vel. |
camera_rope_angle_offset_x / _y / _z | 96 / 128 / 0 | Swinging-rope camera orbit angles. The pitch is absolute (the player’s pitch is ignored); yaw and roll are relative to the player. |
camera_rope_distance | 300 | Swinging-rope camera distance in world units. |
camera_rope_above_angle_offset_x / _y / _z | 96 / 128 / 0 | Static-rope camera orbit angles for the upper part of the rope, relative to the aim angles. |
camera_rope_above_distance | 300 | Upper static-rope camera distance in world units. |
camera_rope_above_aim_x / _y / _z | -512 / 0 / 0 | Fixed angles that the upper static-rope camera uses in place of the player’s orientation. -512 pitches the view a quarter turn from level. |
camera_rope_below_angle_offset_x / _y / _z | 96 / 128 / 0 | Static-rope camera orbit angles for the lower part of the rope, relative to the aim angles. |
camera_rope_below_distance | 300 | Lower static-rope camera distance in world units. |
camera_rope_below_aim_x / _y / _z | 512 / 0 / 0 | Fixed angles that the lower static-rope camera uses in place of the player’s orientation. |
camera_debug_angle_offset_x / _y / _z | 128 / 256 / 0 | Debug camera orbit angles. All three values are stored into the pitch, so only camera_debug_angle_offset_z takes effect (as pitch); yaw and roll come from offset_angle_y and offset_angle_z. |
camera_debug_distance | 300 | Debug camera distance in world units. |
Collision, limits, and smoothing
| Key | Default Value | Description |
|---|---|---|
obstacle_size | 20 | Only models whose bounding radius is strictly larger than this can block the camera’s line of sight. See camera collision. |
camera_size | 10 | Camera body radius in world units. On contact the camera is pushed this far along the contact normal. See camera collision response. |
camera_scape_size | 5 | Minimum camera height above WLD terrain, in steps of 4 world units (shipped 20 units). It has no effect on maps without terrain. |
max_x_angle / min_x_angle | 1692 / 400 | Pitch limits for the look key: the look pitch offset never enters the band between min_x_angle and max_x_angle, so the shipped values allow -356 to +400 units around level. The automatic cameras ignore them. |
max_vel | 8000 | Maximum camera movement per frame step in Q8 units (shipped about 31 units), scaled by frame time. See movement limiting. |
max_y_vel | 5000 | Maximum vertical camera movement per frame step in Q8 units (shipped about 20 units). The upward limit is scaled by frame time; the downward limit is not. |
max_acc | 5000 | Camera acceleration: each frame step may exceed the previous one by this many Q8 units, up to max_vel. |
player_control_x_inc / player_control_y_inc | 70 / -70 | Pitch (x) and yaw (y) change per frame while the look key is held and a direction key is pressed. |
glide_x / glide_y / glide_z | 0.9 / 0.2 / 0.9 | Fraction of the remaining distance that the rendered camera moves toward its target each frame on each axis. 1.0 means no lag. See render camera step. |
glide_angle_x / glide_angle_y / glide_angle_z | 0.7 / 0.1 / 0.7 | The same fraction for the rendered camera angles. glide_angle_x applies to yaw, glide_angle_y to pitch, and glide_angle_z to roll. |
cam_prox_up | 70.0 | No effect. |
Keys read but absent from the shipped file:
camera_combat_max_vel,camera_hang_max_vel,camera_rope_above_max_vel,camera_rope_below_max_vel,camera_debug_max_vel: movement limit for that camera, in the units ofmax_vel. Missing or0meansmax_vel.fudge_factor,max_angular_velocity: no effect.
[dialog]
| Key | Default Value | Description |
|---|---|---|
menu_start_x | 35 | Left edge of the dialogue topic menu, in text-screen pixels. |
menu_start_y | 25 | Top of the first dialogue topic-menu entry, in text-screen pixels; each later entry is one font line lower. |
dialog_max_menu_items | 20 | Capacity of the dialogue topic menu, at least 300. The shipped 20 therefore gives 300 entries. |
dialog_max_dialog_lines | 20 | Number of subtitle slots (minimum 16), not a line limit. |
dialog_max_text_width | 500 | Subtitle wrap width in 640-wide screen pixels (minimum 150). |
dialog_print_text | 1 | 0 hides dialogue subtitles without changing their timing. The book’s Subtitles checkbox edits this value. |
dialog_use_speech | 1 | 1 plays dialogue speech and shows each line for the length of its clip; 0 silences speech and times lines from their text length. |
dialog_max_distance | 1600 | Scripted RTX lines from a speaker farther than this distance (in PlayerDistance units) are not started. |
menu_traverse_delay | 4 | Delay in BIOS timer ticks (18.2 per second) before a held direction key moves the selection again, in the dialogue topic menu and the book menu. 0 means one tick. |
[3dfx]
Glide-specific display, texture, and text settings.
| Key | Default Value | Description |
|---|---|---|
resolution | 12 | Glide display mode: 3 = 512×384, 7 = 640×480, 8 = 800×600, 9 = 960×720, 12 = 1024×768, 13 = 1280×1024, 14 = 1600×1200 (the engine’s own size table uses 1600×1280 for this mode). Other values are not supported. If the mode cannot be opened, the game falls back to 640×480. |
text_scale | 1, 1 | Texture reduction factors for 256-texel and 128-texel textures (256 factor, 128 factor). 1 keeps full resolution; a missing key means 4, 2. Sky textures are never reduced. |
anim_text_scale | 1, 1 | No effect. |
font_sel | 255,255 | Color scale and alpha (A,B, 0–255) for the REDSEL text style used by NPC subtitles, HUD text, and the selected topic-menu entry. |
font_norm | 125,255 | Color scale and alpha for the normal REDSEL style, used for Cyrus’s subtitles and unselected topic-menu entries. A missing key copies font_sel. |
font_used | 125,128 | Color scale and alpha for topic-menu entries already chosen. A missing key copies font_sel. |
For these styles, the main glyph pass uses alpha B and the shadow uses
B × 200 / 256, truncated to an integer. Each pass blends only below alpha
254; alpha 254 and 255 replace the destination without blending. See
subtitle glyph rendering for the shadow offsets
and palette rules.
Developer mode
[debug] final_version=0 selects developer behavior. The fv command-line
option forces release behavior and takes precedence over nfv, which forces
developer behavior. Either option overrides the INI value.
In developer mode, startup does not present the book menu and enters the starting world directly. In a world, Escape waits for its release and then exits the game instead of opening the book. Developer mode also enables the camera position/angle diagnostic keys and other developer display paths.
Demo executable keys
The demo’s RGFX.EXE (build 5.0, see
Demo release) reads these keys in
addition to the ones above. The CD and GOG executable does not read them.
| Section | Key | Description |
|---|---|---|
[debug] | demo_version | Nonzero restricts play to world 1. Every world load becomes world 1 at marker 1, a load while world 1 is already loaded does nothing, and world-change requests are discarded. A missing key means 0. |
[RTX_PATCH] | bad[N], good[N] | RTX entry substitutions for N from 0 to 14. Each value is a four-character RTX entry name. A dialogue request for the bad[N] entry plays the good[N] entry instead. A pair takes effect only when both keys are present. |
[3DFX] | warnings | Nonzero prints Glide texture warnings: Warning: Could not find match for texture '%s'! and Warning: Attempt to delete invalid texture! (Tex:%d). A missing key means 0. The CD and GOG executable always keeps these warnings off. |
External References
COMBAT.INI
Combat system configuration defining attack moves, defense moves, and combat dialogue (taunts) for all combatants.
Shipped sample path: /Redguard/COMBAT.INI (e.g. .../GOG Galaxy/Redguard/Redguard/COMBAT.INI).
The file is 54 KB and 3,728 lines, the largest INI in the game. Every move, defense, and voice line for every combatant is defined here, making the combat system fully data-driven.
File Structure
The file contains 474 sections in four types:
| Section type | Count | Index range | Purpose |
|---|---|---|---|
[misc] | 1 | — | Global combat parameters |
[attackNN] | 89 | 00–88 | Attack move definitions |
[defendNN] | 5 | 00–04 | Defense move definitions |
[tauntNN] | 379 | 00–599 (with gaps) | Combat dialogue and voice lines |
The file opens with a block of commented-out constants defining animation group IDs and attack type enums, followed by the sections in the order listed above.
Section suffixes are numeric IDs. A missing section leaves a gap in the ID
space; later sections keep their suffixes. Numeric fields contain decimal
integers, including signed arc_center offsets. An omitted field is distinct
from an explicit zero in the file. Taunt labels retain their RTX spelling,
including leading & characters.
Animation Groups
The header comments define the animation group ID table used by animation fields throughout the file:
| ID | Name |
|---|---|
| 1 | anim_defend_low |
| 2 | anim_defend_right |
| 3 | anim_defend_left |
| 4 | anim_defend_high |
| 5 | anim_attack_1 |
| 6 | anim_attack_2 |
| 7 | anim_attack_3 |
| 8 | anim_attack_thrust |
| 9 | anim_attack_lunge |
| 10 | anim_attack_1_end |
| 11 | anim_attack_2_end |
| 12 | anim_fight_disarm |
| 13 | anim_fight_low |
| 14 | anim_fight_jump_start |
| 15 | anim_fight_jump |
| 16 | anim_fight_fall |
| 17 | anim_fight_land |
| 18 | anim_fight_fall_attack |
| 19 | anim_fight_land_attack |
| 20 | anim_fight_hurt_1 |
| 21 | anim_fight_hurt_2 |
| 22 | anim_death_fight_stab |
| 23 | anim_death_fight_hard |
| 24 | anim_sheath_sword |
| 25 | anim_explore_hurt_1 |
| 26 | anim_explore_hurt_2 |
| 27 | anim_death_explore |
[misc] Section
Global parameters for the combat system.
| Key | Value (shipped) | Description |
|---|---|---|
sword_clank_1 | 57 | Sound effect ID for sword clash, variant 1 |
sword_clank_2 | 58 | Sound effect ID for sword clash, variant 2 |
non_engaged_dist | 100 | Distance threshold for non-engaged state |
in_face_dist | 100 | Distance threshold for in-face proximity |
non_engaged_spacing | 500 | Spacing between non-engaged combatants, in engine angles |
in_combat_threshold | 200 | Strict horizontal distance threshold for isFighting, shifted left eight bits at load |
player_can_die | 1 | Whether the player can be killed (1 = yes) |
cyrus_defend_interval | 2 | Minimum frames between Cyrus defends |
node_timer | 120 | Timer value for combat node transitions |
max_taunts | 600 | Maximum taunt index (exclusive upper bound for taunt pool) |
[attackNN] Sections
Each [attackNN] section defines one attack move. There are 89 sections (attack00 through attack88).
Attack Types
The type field selects the attack category:
| Value | Name |
|---|---|
| 0 | melee |
| 1 | missile |
| 2 | combo |
| 3 | stab |
| 4 | finishing |
Elevation Values
The elevation field controls the vertical targeting zone:
| Value | Meaning |
|---|---|
| 0 | regular (default when omitted) |
| 1 | above |
| 2 | below |
Field Schema
Standard attack (type 0, 1, 3, 4):
| Field | Type | Description |
|---|---|---|
type | integer | Attack category (see enum above) |
arc | integer | Horizontal hit arc width, in engine angle units |
arc_center | integer | Horizontal offset of arc center from forward direction; omitted when centered (0) |
elevation | integer | Vertical targeting zone (see enum above); omitted when regular (0) |
min_range | integer | Minimum distance to target for hit to register |
max_range | integer | Maximum distance to target for hit to register |
first_collide_frame | integer | Animation frame on which collision detection begins |
collide_duration | integer | Number of frames collision detection remains active |
damage | integer | Hit point damage dealt on successful hit |
force | integer | Knockback force applied to target |
defense | integer | Defense animation group ID triggered on the target |
first_defend_frame | integer | Animation frame on which the attacker can be defended against; omitted on some attacks |
defend_duration | integer | Number of frames the attacker is vulnerable to defense; omitted on some attacks |
animation | integer | Animation group ID (see animation group table above) |
col_vertex | integer | Model vertex index used as the collision sphere origin; present only on large creature attacks |
col_sphere_size | integer | Radius of the collision sphere; present only on large creature attacks |
Combo attack (type 2):
Combo sections reference other attack sections by index rather than defining collision geometry directly.
| Field | Type | Description |
|---|---|---|
type | integer | Always 2 for combos |
num_attacks | integer | Number of sub-attacks in the combo (2 or 3) |
attack00 | integer | Index of the first sub-attack section |
attack01 | integer | Index of the second sub-attack section |
attack02 | integer | Index of the third sub-attack section; present only when num_attacks = 3 |
Attack Allocation by Combatant
The file comments identify which attack indices belong to each combatant:
| Combatant | Attack indices |
|---|---|
| Cyrus (early) | 00–14 |
| Skeleton | 15–17 |
| Golem | 18–22 |
| Zombie | 23–25 |
| Serpent | 26 |
| Troll | 27–29 |
| Guard (low attack) | 30 |
| Goblin | 31–34 |
| Dragon | 35–37 |
| Richton | 38–47 |
| Dram | 48–52 |
| Ogre | 53 |
| Pirate (initial, weaker) | 54–64 |
| Cyrus (later) | 65–74 |
| Vermai | 75–77 |
| Tavern thugs (no damage) | 78–88 |
Large creature attacks (Golem, Serpent, Dragon) use col_vertex and col_sphere_size to attach the collision sphere to a specific model vertex rather than the combatant’s origin point.
[defendNN] Sections
Each [defendNN] section defines one defense move. There are 5 sections (defend00 through defend04), shared by Cyrus and guards.
Field Schema
| Field | Type | Description |
|---|---|---|
arc | integer | Horizontal arc width covered by the defense; omitted on defend00 |
min_range | integer | Minimum distance at which the defense is effective |
max_range | integer | Maximum distance at which the defense is effective |
first_collide_frame | integer | Animation frame on which the defense window opens |
collide_duration | integer | Number of frames the defense window remains active |
animation | integer | Animation group ID (see animation group table above) |
The five shipped defenses correspond to: default (defend00), low (defend01), right (defend02), left (defend03), and high (defend04).
Runtime Attack Loading
The engine scans attack00, attack01, and subsequent numbered sections in
order. A section is admitted when either damage or type has a nonempty value.
The first section with neither value ends the scan; later sections are not loaded.
This runtime rule differs from parsing the INI as a collection of sparse sections.
A non-combo attack loads one move. A combo reads its referenced moves in the
order of attack00, attack01, and attack02, using num_attacks as the count.
Each attack reserves three zero-initialized motion slots. Loading fills the
authored motions and leaves unused slots zeroed.
Each move’s numeric loader reads an omitted or empty value as zero. This applies
to motion type, ranges, collision and defence timing, arc, damage, force,
defence selector, collision vertex, collision sphere size, animation and
elevation. Raw INI parsing still distinguishes omission from an explicit zero.
The runtime motion record stores the converted value for every loaded field.
Each loaded move adds 97 with signed 32-bit wrapping to its configured
animation value to obtain the actor animation group. An omitted animation
therefore selects group 97. A single attack copies its move’s minimum and maximum ranges.
A nonempty combo starts its minimum at 65,535 and maximum at -1, then lowers the
minimum and raises the maximum for each referenced move.
Each attack also retains a damage total, initialized to zero. A single attack adds its move’s damage; a combo adds each referenced move’s damage in authored order. The additions wrap as signed 32-bit values. A combo with no moves retains zero damage. Defence pacing reads this accumulated total, rather than only the first motion’s damage.
Runtime Defense Loading
Defense loading scans defend00, defend01, and subsequent numbered sections
in order. A section is admitted when its numeric max_range is nonzero. The
first zero value ends the table; later sections are not loaded. Each admitted
record loads its minimum and maximum ranges, first collision frame, collision
duration, arc and animation. The configured animation value is increased by 97
to obtain the actor animation group.
Actor Move Selection
Combat initialization binds nine attack slots from actor attribute bytes 70 through 78. Each nonzero byte selects the attack record with that index; zero leaves the corresponding slot empty. An empty attack slot does not prevent later attack slots from being populated.
Four defense slots use an attribute cursor starting at byte 79. A nonzero byte
selects the defense record with that index and advances the cursor. A zero byte
leaves the slot empty without advancing the cursor, so all remaining defense
slots also stay empty. Defense selection therefore uses only the nonzero prefix
of attribute bytes 79 through 82. A zero selector does not select defend00.
Binding a nonzero defense slot marks its shared defense record available and stores the actor animation group’s signed frame count in that record. An absent animation group supplies zero. The four-slot availability query succeeds when one bound record has its availability marker equal to one.
[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.
An actor’s taunt list begins at its RAEX taunt_id and
contains attr_num_taunts consecutive entries. A zero count creates no list.
The range is selected per actor; it is independent of which taunt sections are
present elsewhere in the file. Without RAEX, the first index is zero. Each actor
allocates exactly attr_num_taunts pointers and fills them from that range in
order. The per-actor pointer list has no additional sentinel entry.
The loader creates an empty record for each absent section and an additional
empty sentinel at max_taunts. Empty records have a zero label, type, and
animation. Present records copy the first four rtx_label bytes directly,
including a leading &. A nonzero animation value is increased by 152 before
storage; zero remains zero and requests no accompanying animation.
Taunt Types
The type field identifies when the taunt fires:
| Value | Trigger |
|---|---|
| 0 | begin combat |
| 1 | attack |
| 2 | hurt by opponent |
| 3 | hit opponent |
| 4 | defend |
| 5 | misc, during move |
| 6 | death |
| 7 | after kill opponent |
| 8 | want to switch out |
| 9 | want to switch in |
| 10 | opponent is unarmed (male) |
| 11 | opponent is unarmed (female) |
Field Schema
| Field | Type | Description |
|---|---|---|
type | integer | Trigger condition (see enum above) |
rtx_label | string | Four-byte label key into ENGLISH.RTX, preserved literally including &NNN spellings |
animation | integer | Combat animation offset; nonzero values become group 152 + animation, while 0 disables accompanying animation |
Combat taunt selection is skipped when the actor satisfies IsDead, including
the selected player’s death override. Begin-combat writes lifecycle selector 6
before this check; the selected-player override remains independent.
Begin-combat, hurt, death, and opponent-unarmed selection scans the actor’s
list in order. Only entries with the requested type and a nonzero RTX label
participate. Each matching entry becomes the current selection and consumes one
random draw. The scan stops when floor(4 * draw / 32767) == 1; otherwise the
last matching entry wins. Death selection retries with the hurt type only when
no death entry matched. The other four types have no such fallback.
Taunt Allocation by Combatant
The file comments define the index ranges reserved for each combatant:
| Index range | Combatant |
|---|---|
| 0–49 | Cyrus |
| 50–149 | Guards |
| 150–154 | Skeletons |
| 155–159 | Zombies |
| 160–179 | Tavern Thug 1 |
| 180–199 | Tavern Thug 2 |
| 200–219 | Tavern Thug 3 (Dagoo) |
| 220–228 | Brennan |
| 229–234 | Golem |
| 235–239 | Serpent / Vermai |
| 240–249 | Troll |
| 250–259 | Goblin |
| 260–279 | Richton |
| 280–299 | Dram |
| 300–319 | Pirate 1 |
| 320–339 | Pirate 2 |
| 340–349 | Ngasta |
| 350–359 | Urik |
| 360–369 | Zombie |
| 370–389 | Vander |
| 390–409 | Island Thug 1 |
| 410–429 | Island Thug 2 |
| 430–449 | Island Thug 3 |
| 450–469 | Island Thug 4 |
| 470–489 | Island Thug 5 |
| 490–509 | Island Thug 6 |
| 510–529 | Pirate Hideout 1 |
| 530–549 | Pirate Hideout 2 |
| 550–569 | Pirate Hideout 3 |
| 570–589 | Pirate Hideout 4 |
| 580–587 | Dragon |
| 591–599 | Jail interior (reuses guard lines) |
Not all reserved slots are populated. Many combatants use only a subset of their allocated range, and some ranges overlap in the comments (Dragon at 580–587 falls inside Pirate Hideout 4 at 570–589).
External References
ITEM.INI
Item database defining all collectible objects, weapons, potions, keys, and quest items with their properties, models, and scripted behaviors.
Shipped sample path: /Redguard/ITEM.INI (e.g. .../GOG Galaxy/Redguard/Redguard/ITEM.INI).
The file is data-driven: every game object from the compass to the soul sword is defined here with associated 3D models, inventory bitmaps, and AI scripts. Item 0 is always the compass, a special case. The file’s comment says giving the player this item turns on the compass display; the display actually toggles when item 0 is used (see compass). Any item index declared as 0 in a weapon or hand-object field is treated as “no object” for this reason.
File Structure
The file contains a single [items] section with two parts:
- A header block with global inventory settings.
- Per-item field blocks indexed by item ID (e.g.
name[0],type[1]).
[items] Header Fields
| Field | Value | Description |
|---|---|---|
bitmap_file | SYSTEM\PICKUPS.GXA | GXA atlas indexed by bitmap[x]; the HUD active item icon uses this atlas. pickbox.gxa and pickblob.gxa are not used by the Glide build. |
bitmap_selected_file | SYSTEM\PICKUPSS.GXA | GXA atlas of selected icons, indexed by the same bitmap[x]. Not used by the HUD. |
start_item_list | 1, 2, 4, 18 | Item IDs the player starts the game with. |
start_item_select | 1 | Item ID selected by default at game start. |
additional_length | 8 | Extra inventory display length (slots). |
weapon_sphere_size | 10 | Collision sphere radius for weapon hit detection. |
default_weapon_item | 1 | Item ID used as the default weapon (the sabre). |
torch_time | 60 | Torch burn duration in seconds (tolerance of +/- 3 seconds). |
Per-Item Field Schema
All fields are optional. Missing fields are treated as unused. The index x in each field name is the item ID used in all script commands.
A field with no text after = is equivalent to an omitted field. Explicit zero
remains a stored value. Item lists contain comma-separated decimal item IDs.
Paths, script names, and RTX labels retain their spelling and capitalization.
General fields
| Field | Description |
|---|---|
type[x] | Item type: 0 = general item, 1 = sword/weapon (can be drawn and sheathed, has both drawn and sheathed 3D files), 2 = hand object (may override a weapon, e.g. torch), 3 = torch (used by the shipped torch; the file’s comment block does not list it). |
flags[x] | Bit-field of misc flags. 1 = remove on drop regardless of player total; 2 = drop item after use; 4 = remove item after use. Add values together to combine. |
hide[x] | When 1, the item is hidden from screen and inventory but still exists. Scripts may still call UseItem() on it; the player cannot select or use it directly. |
total[x] | Total number of this item in the game world. |
player_max[x] | Maximum number of this item the player can carry. |
player_total[x] | Number of this item the player starts the game with. |
name[x] | Short name RTX dialogue label (e.g. ?xsw). |
description[x] | Longer item description RTX dialogue label (e.g. cisw). |
name[x] and description[x] values are keys into the RTX dialogue/text system, not literal strings. Labels beginning with ? are short display names; labels beginning with ci or ti are longer descriptions.
AI script fields
| Field | Description |
|---|---|
use_script[x] | AI script file to execute when the item is used through the shell system. |
script_instances[x] | Maximum concurrent instances of the use script. 1 prevents a new instance from starting if one is already running. Zero or a negative value means unlimited instances. |
Using an item
UseItem(id) does nothing while the player is locked out (LockoutPlayer value
- or in an edge state (10 through 19), after another use attempt in the same
world-loop iteration, in shell context, with the inventory closed, in deep water,
while holding a weapon, or while lighting or dousing a torch. An unknown or unowned
item is an error, and so is a receiver other than the selected player. Ownership
requires the current count to be greater than zero as a signed 32-bit value.
Counts with the high bit set are rejected before the item-specific action, use
script, or drop-list effects. Item zero
toggles the compass. Weapon items (type 1) set the weapon attribute and draw the
sword, and torch items (code 3) light or put out the torch. A general item starts
its
use_scriptas a shell whenscript_instancesis zero or negative or fewer than that many shells of the script are running. It then drops one unit of eachdrop_use_item_listentry. When that changed the item’s own count and flag bit2is set, one unit of the item is dropped; flag bit4then empties the item.
Island map request
Using general item 83 sets a pending island-map request before the item’s use
script and drop-list processing. The display handler treats this request like
the map input action. It clears the request before checking admission, so a
rejected request is not retained for a later update. Display requires ownership
of item 83, a player lockout value other than 1, and player attribute 105 other
than 1. The map screen loads SYSTEM\MAPMAP.GXA and SYSTEM\CROSS.GXA.
Island map screen
An admitted request freezes the simulation clock, stores the player marker position (below) and, when audio is enabled
and the current music mode is not 1, switches the music mode to 1 for the duration, restoring it on exit. It then
loads SYSTEM\MAPMAP.GXA and SYSTEM\CROSS.GXA, installs the palette read with the second file as palette 5 and
activates it. The screen is 640 by 480: the map image is drawn at X 0 with its top at (480 - height) / 2.
The marker position is chosen from the current world when the screen opens. With no selected player or no current world, and in world 0, it is the fixed Q8 position (X 0xC68000, Z 0x793000), which is map units (50816, 31024). In worlds 7 and 14 it is the fixed Q8 position (X -0x196400, Z 0xB1BC00), map units (-6500, 45500). In worlds 1, 6, 27, 28 and 30 it is the selected player’s current position. In every other world the previous position is kept.
Each axis is the map position (the Q8 value divided by 256, truncating toward zero), multiplied by
image_width / 65535 and by a per-world scale, then truncated toward zero to an integer and offset. For X the position is used directly; for Z the value is 65536 - z. image_width is the
16-bit width field of the MAPMAP header, used for both axes. In world 6 the scales are 0.82 (X) and 0.53 (Z) and the
offsets are -86 (X) and 26 (Z); in every other world the scales are 0.88 and 1.0 and the offsets 101 and -27. The
scale factors are single-precision values.
The cross animation plays once: for each frame of CROSS.GXA the map image is drawn again, the frame is drawn at the marker position, the screen is presented and the loop waits 100 ms. The screen then waits for the map action to be released. It then repeats a blocking key wait until the map action and the adjacent action word are both released: the wait polls every 50 ms and returns the first keyboard key, or a code for one of six game-controller button words (0x80, 0x81, 0x84, 0x85, 0x86, 0x87) when no key is down. Any key therefore dismisses the screen once the map action is no longer held. Both images are then freed and the clock resumes.
Objects and bitmaps
| Field | Description |
|---|---|
bitmap[x] | Frame index into the bitmap_file and bitmap_selected_file atlases; the HUD shows it for the selected item. |
inventory_object_file[x] | 3D object file used in the inventory screen. |
game_object_file[x] | 3D object file used in the game world. |
Drop and add lists
| Field | Description |
|---|---|
add_item_list[x] | List of item IDs to add to the player when this item is picked up. |
drop_add_item_list[x] | List of item IDs passed to DropItem when this item is acquired; each entry requests one unit. |
remove_drop_item_list[x] | Owned counts recursively cleared by DropItem while the dropped item still has a nonzero count. |
required_item_list[x] | List of item IDs the player must already have before picking up this item. |
drop_use_item_list[x] | List of item IDs to drop into the world when this item is used. |
Weapon fields
| Field | Description |
|---|---|
hand_object_file[x] | 3D object for the item when held in hand. For weapon-type items this must be a weapon object. Ignored for general-type items. |
hilt_object_file[x] | 3D object for the item when sheathed. |
Object file paths reference 3DART\ assets, which are the software-renderer models from the original CD release. The GOG release uses fxart/ equivalents at runtime; the 3DART\ paths in this file reflect the original CD asset layout.
Initialization and acquisition
The item database path comes from SYSTEM.INI’s item_ini setting. Initialization
first closes the previous item system. For each new item record, it requests a
zero-fill of the entire record before assigning its identifier and linking it
into the ordered item list. Authored item fields are loaded after this step.
Each loaded item’s initial owned count is player_total[x]. The ordered start_item_list then requests acquisition of one
unit per entry, including repeated entries. Starting items therefore add to the
configured counts rather than replacing them.
Acquisition clamps the requested quantity to both total[x] - player_total[x]
and player_max[x] - player_total[x]. A nonpositive result does nothing. For a
positive result, every entry in required_item_list[x] must have a nonzero owned
count and must recursively satisfy its own requirements.
After requirements pass, acquisition processes drop_add_item_list[x] in order,
requesting removal of one unit from each listed item. It then processes
add_item_list[x] in order, recursively requesting acquisition of one unit from
each entry. These lists run once per acquisition request, independent of the
quantity admitted. Active-item fallback selection runs next; the acquired item’s
owned count increases last. The quantity is not recalculated after list effects.
The authored player_total[x] assignment and the final acquisition increment
write the current owned count only. Neither direct count write replaces the
separate pushed-value flag or saved count used by
PushItem and PopItem.
An absent start_item_select selects the first loaded item record. A supplied
value selects its matching record. This initialization selection does not filter
by owned count or visibility.
Empty-selection fallback
Acquisition’s fallback does nothing while an item record is selected. With no
selection, it first selects item ID 1 if that record exists, regardless of its
owned count, type, or visibility. This is a literal item ID, independent of
default_weapon_item.
If item 1 is absent, the fallback scans records in ascending ID order. It selects the first owned type-1 item, or the first owned item of any type if no owned weapon exists. Hidden items remain eligible. If no candidate exists, selection stays empty. Whenever the fallback selects a record, it also writes the low byte of that ID to the selected player’s weapon attribute. The check runs before the acquired item’s count increases, so a newly acquired first unit is not yet owned for that scan.
Removal during acquisition
DropItem(id, quantity) does nothing when the item system is closed or the
requested quantity is nonpositive. Otherwise it subtracts at most the item’s
current owned count. Flag bit 0x01 then clears that count completely.
The removal-list helper runs after this subtraction and flag handling. It
traverses remove_drop_item_list[id] only if the item’s remaining count is
nonzero. For each listed item, it recursively applies the same helper and then
clears that listed item’s owned count. If the dropped item’s count has reached
zero, its removal list is not traversed.
Script count and selection queries
HaveItem(id) returns the item’s owned count, not a Boolean. It returns zero
when the item system is closed. With an open item system, an unknown ID reports
an item lookup error.
SelectItem(id) selects the matching record without checking its owned count,
visibility, or type. It reports an error for an unknown ID. It does not assign the
selected player’s weapon attribute.
ActiveItem() takes no script arguments. It returns the selected record’s ID
when the item system is open, a record is selected, and the invocation has an
actor receiver. Otherwise it returns zero, which is also the compass’s item ID.
Selected-player weapon validation
Before ordinary actor traversal, the engine validates the selected player’s weapon attribute against the open item system. A closed item system or absent invocation receiver leaves weapon state unchanged. Once admitted, validation uses the selected player, even if the supplied receiver is another actor.
A valid weapon requires an animation state, execution outside the shell context, a nonzero weapon attribute, and a matching type-1 item with a positive owned count. If any of these conditions fails, the engine clears the selected player’s weapon attribute and performs the normal weapon cleanup. Item visibility and inventory selection do not affect this check. The check does not acquire or select the configured default weapon.
Item Catalog
87 items are defined (IDs 0 through 86). Item 0 is always the compass.
| ID | Comment | Type |
|---|---|---|
| 0 | compass | 0 (general) |
| 1 | sabre | 1 (weapon) |
| 2 | gold | 0 (general) |
| 3 | Potion of ironskin | 0 (general) |
| 4 | health potion | 0 (general) |
| 5 | ring of invisibility | 0 (general) |
| 6 | Voa’s ring | 0 (general) |
| 7 | guard sword | 1 (weapon) |
| 8 | rusty key | 0 (general) |
| 9 | gold Key | 0 (general) |
| 10 | silver key | 0 (general) |
| 11 | amulet | 0 (general) |
| 12 | soul gem | 0 (general) |
| 13 | soul sword | 1 (weapon) |
| 14 | crow bar | 0 (general) |
| 15 | rune 1 | 0 (general) |
| 16 | rune 2 | 0 (general) |
| 17 | rune 3 | 0 (general) |
| 18 | letter | 0 (general) |
| 19 | orc’s blood | 0 (general) |
| 20 | orc’s blood w/stuff | 0 (general) |
| 21 | spider’s milk | 0 (general) |
| 22 | spider’s milk w/stuff | 0 (general) |
| 23 | ectoplasm | 0 (general) |
| 24 | ectoplasm w/stuff | 0 (general) |
| 25 | hist sap | 0 (general) |
| 26 | hist sap w/ stuff | 0 (general) |
| 27 | book of dw lore | 0 (general) |
| 28 | dwarven gear | 0 (general) |
| 29 | vial | 0 (general) |
| 30 | vial w/elixir | 0 (general) |
| 31 | iron weight | 0 (general) |
| 32 | bucket | 0 (general) |
| 33 | bucket w/water | 0 (general) |
| 34 | fist rune | 0 (general) |
| 35 | elven book (copy) DO NOT USE | 0 (general) |
| 36 | elven book | 0 (general) |
| 37 | redguard book | 0 (general) |
| 38 | flora of hammerfell book | 0 (general) |
| 39 | reference map | 0 (general) |
| 40 | pouch | 0 (general) |
| 41 | trithick’s map piece | 0 (general) |
| 42 | silver ship | 0 (general) |
| 43 | shovel | 0 (general) |
| 44 | aloe | 0 (general) |
| 45 | torch | 3 (torch) |
| 46 | monocle | 0 (general) |
| 47 | red flag | 0 (general) |
| 48 | silver locket that talks of lakene | 0 (general) |
| 49 | redguard insignia | 0 (general) |
| 50 | joto’s piece | 0 (general) |
| 51 | flask of lillandril | 0 (general) |
| 52 | talisman of hunding | 0 (general) |
| 53 | izara’s journal open | 0 (general) |
| 54 | canah feather | 0 (general) |
| 55 | kithral’s journal | 0 (general) |
| 56 | starsign’s book | 0 (general) |
| 57 | izara’s journal closed | 0 (general) |
| 58 | starstone | 0 (general) |
| 59 | krisandra’s key | 0 (general) |
| 60 | key to iszara’s lodge | 0 (general) |
| 61 | necro book - view only | 0 (general) |
| 62 | bar mug ..for thugs hands, etc | 0 (general) |
| 63 | mariah’s watering can | 0 (general) |
| 64 | glass bottle | 0 (general) |
| 65 | glass bottle with water | 0 (general) |
| 66 | glass bottle w/aloe and water | 0 (general) |
| 67 | Strength Potion | 0 (general) |
| 68 | bandage | 0 (general) |
| 69 | bloody bandage | 0 (general) |
| 70 | skeleton sword | 1 (weapon) |
| 71 | keep out poster…mage’s guild | 0 (general) |
| 72 | keep out poster…dwarven ruins | 0 (general) |
| 73 | tobias mar mug | 0 (general) |
| 74 | Bone Key | 0 (general) |
| 75 | flaming sabre | 1 (weapon) |
| 76 | goblin sword | 1 (weapon) |
| 77 | ogre’s axe | 1 (weapon) |
| 78 | dram’s sword | 1 (weapon) |
| 79 | palace key | 0 (general) |
| 80 | dram’s bow | 0 (general) |
| 81 | dram’s arrow | 0 (general) |
| 82 | silver locket that just says silver locker | 0 (general) |
| 83 | island map | 0 (general) |
| 84 | wanted poster | 0 (general) |
| 85 | palace diagram | 0 (general) |
| 86 | last | 0 (general) |
Notes on specific entries:
- Items 19/20, 21/22, 23/24, 25/26 are paired: the base ingredient and the same ingredient already combined with another substance. Both variants share the same
name[x]label. - Items 29/30 are the empty vial and the vial filled with elixir.
- Items 32/33 are the empty bucket and the bucket filled with water.
- Items 53/57 are the open and closed versions of Izara’s journal, sharing the same
name[x]label. - Item 35 is marked “DO NOT USE” in the file comment; item 36 is the live elven book.
- Item 45 (torch) uses type
3, which the file’s comment block does not define. - Items 62 and 63 (bar mug, watering can) reuse the journal inventory model (
Ijourn.3D) as a placeholder, with distinctgame_object_filepaths for the actual in-world mesh. - Items 80 and 81 (dram’s bow and arrow) reuse the bone key inventory model (
ibone.3d) as a placeholder. - Item 86 is labeled “last” with no meaningful content, serving as a sentinel or end-of-list marker.
External References
- UESP: Redguard:Console — documents the
item addcommand, which uses item IDs matching the index numbers in this file.
WORLD.INI
World/level database defining map files, palettes, lighting, sky, weather, and PVO node maps for all game locations.
Shipped path: /Redguard/WORLD.INI. Referenced by SYSTEM.INI via world_ini=WORLD.INI. The file contains a single [world] section with four global keys followed by per-world-index entries for every game location.
File Structure
Single section: [world]
Global Keys
| Key | Example value | Description |
|---|---|---|
start_world | 0 | World index loaded on new game start. |
start_marker | 0 | Spawn marker index within the start world. |
test_map_delay | 1 | Delay (seconds) between worlds during test-map cycling. |
test_map_order | 0,1,2,1,...,-2 | Comma-separated world index sequence for the test-map loop. -2 terminates the list. The shipped file includes a commented-out compact variant and an active interleaved variant that returns to world 1 between each location. |
import::world_ini::NewGameEntry::parse reads the required start_world and
start_marker indices from this section. Names match without case; the first
occurrence of each key wins. Missing, negative, or malformed indices return a
contextual error.
Per-World Keys
Each world is identified by an integer index N. Keys follow the pattern key_name[N]=value.
Core
| Key | Type | Description |
|---|---|---|
world_map[N] | path | RGM scene file for this world. Present on every entry. |
world_world[N] | path | WLD terrain file. Only present for outdoor worlds with a heightmap terrain mesh. |
world_redbook[N] | integer | CD audio track number for background music; see music. |
world_palette[N] | path | COL palette file. |
world_shade[N] | integer | Shade table index. |
world_haze[N] | integer | Software-renderer haze table index; Glide selects FOG.INI by world ID. |
world_back_plane[N] | integer | Nonzero scene far-distance override in engine units; zero or omitted keeps SYSTEM.INI back_plane. |
world_background[N] | integer | Legacy background fill mode. 0 = black, 2 = sky color, other values are palette indices. The shipped Glide build does not read this key; see sky background fill. |
world_compass[N] | integer | Compass heading offset (fixed-point). Omitted for most worlds; present where the player can see a compass. |
world_flash_filename[N] | path | GXA file used for screen-flash transitions when entering or leaving this world. |
Lighting
| Key | Type | Description |
|---|---|---|
world_sun[N] | x, y, z, intensity | Sun direction vector and intensity. The three components are large fixed-point integers defining the light direction; intensity is a scalar. |
world_ambient[N] | integer | Global ambient light level. |
world_ambientfx[N] | integer | Scene ambient brightness used by explicit-frame environmental effects. Stored in sixteen bits; stored zero becomes 128, then world entry sign-extends the value. |
world_ambientrgb[N] | r, g, b | Ambient light color as three 0..255 components. |
world_sunangle[N] | integer | Sun angle (fixed-point). Controls the horizontal rotation of the sun direction. |
world_sunskew[N] | integer | Sun skew (fixed-point). Controls the vertical tilt of the sun direction. |
world_sunrgb[N] | r, g, b, scale | Sun color as three 0..255 components plus a scale factor. |
world_fogrgb[N] | r, g, b | Distance fog and Glide background-clear color as three 0..255 components. Omitted values default to black. |
Sky
| Key | Type | Description |
|---|---|---|
world_sky[N] | path | GXA skybox texture. Only present for outdoor worlds. |
world_skyfx[N] | filename | BSI sky texture (scrolling sky layer). |
world_skyscale[N] | integer | Scale factor for the sky layer. |
world_skylevel[N] | integer | Vertical offset of the sky layer (negative = below horizon). |
world_skyspeed[N] | integer | Scroll speed of the sky layer. |
world_sky_xrotate[N] | integer | Sky rotation rate around the X axis. |
world_sky_yrotate[N] | integer | Sky rotation rate around the Y axis. |
Sun Disc
| Key | Type | Description |
|---|---|---|
world_sunimg[N] | filename | BSI texture for the rendered sun disc. |
world_sunimgrgb[N] | r, g, b | Tint color for the sun disc texture. |
world_sunscale[N] | integer | Size scale of the sun disc. |
Terrain height offset
| Key | Type | Description |
|---|---|---|
world_scapeshift[N] | integer | Offset added to the negative source heights when rebuilding the terrain height table. The parsed value is retained as a signed 16-bit word, then converted to a float for terrain setup. |
A missing or empty value supplies zero. The integer-setting reader also returns zero when the world section is absent. This zero still rebuilds the height table.
Water
| Key | Type | Description |
|---|---|---|
world_wave[N] | a, b, c | Wave animation parameters for water surfaces. Three integers: a = amplitude (vertical displacement scale), b = speed (phase advance per BIOS timer tick), c = spatial frequency (ripple density, multiplies the squared-distance term). See Water Waves for the displacement formula. |
PVO Visibility
| Key | Type | Description |
|---|---|---|
world_node_mapN[W] | path | PVO .noo node map file, where N is a 1-based sequence index and W is the world index. Each world can have multiple node maps. See PVO for the octree format. |
Rain / Weather
Only world 6 (necrisle) uses these keys. No other world has weather effects.
| Key | Type | Description |
|---|---|---|
world_rain_delay[N] | integer | Frames between rain drop spawns. |
world_rain_drops[N] | integer | Maximum simultaneous rain drops. |
world_rain_start[N] | integer | Vertical start height for rain drops (negative = above ground). |
world_rain_end[N] | integer | Vertical end height where drops are removed. |
world_rain_sphereN[W] | x, y, z, r | Sphere defining a rain zone: center coordinates plus radius. Multiple spheres (indexed 1..4 in world 6) define the areas where rain falls. |
World Catalog
IDs 9, 10, and 16 have no entries in the file and are skipped.
Worlds 0, 1, 6, 27, 28, and 30 have world_world entries and use a WLD terrain mesh. All other worlds are indoor or dungeon locations with no terrain.
| ID | RGM File | WLD File | Palette | Location |
|---|---|---|---|---|
| 0 | MAPS\start.rgm | MAPS\hideout.WLD | 3DART\sunset.COL | Starting hideout (exterior, sunset) |
| 1 | MAPS\ISLAND.rgm | MAPS\ISLAND.WLD | 3DART\island.COL | Stros M’Kai island (daytime) |
| 2 | MAPS\catacomb.rgm | — | 3DART\catacomb.COL | Catacombs |
| 3 | MAPS\PALACE.rgm | — | 3DART\palace00.COL | Palace interior |
| 4 | MAPS\caverns.rgm | — | 3DART\REDcave.COL | Caverns |
| 5 | MAPS\observe.rgm | — | 3DART\observat.COL | Observatory |
| 6 | MAPS\necrisle.rgm | MAPS\necrisle.WLD | 3DART\necro.COL | Necromancer’s Isle (rain, rotating sky) |
| 7 | MAPS\necrtowr.rgm | — | 3DART\necro.COL | Necromancer’s Tower interior |
| 8 | MAPS\drint.rgm | — | 3DART\observat.COL | Dwemer ruin interior |
| 11 | MAPS\jailint.rgm | — | 3DART\necro.COL | Jail interior |
| 12 | MAPS\temple.rgm | — | 3DART\island.COL | Temple |
| 13 | MAPS\mguild.rgm | — | 3DART\redcave.COL | Mages Guild |
| 14 | MAPS\vile.rgm | — | 3DART\island.COL | Vile Lair |
| 15 | MAPS\tavern.rgm | — | 3DART\island.COL | Tavern |
| 17 | MAPS\hideint.rgm | MAPS\hideout.WLD | 3DART\hideout.COL | Hideout interior (shares hideout WLD) |
| 18 | maps\silver1.rgm | — | 3DART\island.COL | Silversmith (area 1) |
| 19 | maps\silver2.rgm | — | 3DART\island.COL | Silversmith (area 2) |
| 20 | maps\belltowr.rgm | — | 3DART\island.COL | Bell Tower |
| 21 | maps\harbtowr.rgm | — | 3DART\island.COL | Harbor Tower |
| 22 | maps\gerricks.rgm | — | 3DART\island.COL | Gerrick’s |
| 23 | maps\cartogr.rgm | — | 3DART\island.COL | Cartographer’s |
| 24 | maps\smden.rgm | — | 3DART\island.COL | Smuggler’s Den |
| 25 | maps\rollos.rgm | — | 3DART\island.COL | Rollo’s |
| 26 | maps\jffers.rgm | — | 3DART\island.COL | Jeffers’ |
| 27 | MAPS\island.rgm | MAPS\ISLand.WLD | 3DART\nightsky.COL | Island (night variant) |
| 28 | MAPS\ISLAND.rgm | MAPS\ISLAND.WLD | 3DART\sunset.COL | Island (sunset variant) |
| 29 | maps\brennans.rgm | — | 3DART\island.COL | Brennan’s Farm |
| 30 | MAPS\extpalac.rgm | MAPS\ISLAND.WLD | 3DART\sunset.COL | Palace exterior (sunset, uses island WLD) |
| 99 | maps\brennans.rgm | — | 3DART\island.COL | Brennan’s Farm (alternate entry) |
Notes
Typos in the shipped file. Line 286 reads orld_ambientfx[8]=128 (missing leading w) and line 498 reads orld_sunangle[23]=256. These malformed names do not match the expected keys, so the engine uses the 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.
External References
- UESP: Redguard:Console — the
show worldconsole command displays the current world index at runtime
NewGameEntry also retains the selected world’s optional world_flash_filename.
MENU.INI
Menu system layout configuration defining page structure, text placement, textures, selectable items, and embedded movie (Smacker) playback definitions.
Shipped path: MENU.INI (game root directory, 42 KB, 961 lines).
Sections
[general]
Global menu system flags:
| Key | Default | Description |
|---|---|---|
preload_sprites | 1 | Preload menu sprite textures at startup. |
hi_res_menu | 1 | Use high-resolution menu rendering. |
ignore_autosave | 0 | Skip auto-save slot handling and the startup Introduction; enter the starting world directly. |
force_movies | 0 | Mark all configured cinematics unlocked for Movies browsing. |
[pageN] (pages 0–7)
Each page section defines the visual layout and interactive elements for one menu screen. 8 pages are defined in the shipped file.
Page assignments
| Page | Menu Screen |
|---|---|
| 0 | Main Menu (Save, Load, New, Options, Movies, Continue, Quit) |
| 1 | Save Game |
| 2 | Load Game |
| 3 | Movies (cinematic list with Smacker playback) |
| 4 | Options (Display, Sound, Controls) |
| 5 | Display |
| 6 | Sound |
| 7 | Key Binding Options |
Texture fields
Each page uses up to 2 background textures sourced from TEXBSI archives:
| Key | Description |
|---|---|
texture_set[0], texture_set[1] | Texture set number (TEXBSI archive id). |
texture_index[0], texture_index[1] | Index within the texture set. |
All shipped pages use texture set 289.
Per-element fields
Elements are indexed sequentially within each page (e.g. text_x[0], text_x[1], etc.):
| Key | Description |
|---|---|
text_x[N] | X position of the text element. |
text_y[N] | Y position of the text element. |
text[N] | Display text string. |
justify[N] | Text alignment: 0 = left, 1 = center, 2 = right. |
selectable[N] | 1 if the user can select/interact with this element. |
default[N] | 1 if this element is selected when the page first opens. |
texture[N] | Which background texture to place text on (0 or 1). |
action[N] | Action id dispatched when selected (program-defined). |
grayed[N] | 1 to display in the grayed-out font. |
output_x[N] | X position for special output (key name text, slider art). |
output_y[N] | Y position for special output. |
output_justify[N] | Output alignment, using the text alignment codes. |
slider_min[N] | Minimum value for slider elements. |
slider_max[N] | Maximum value for slider elements. |
Movie fields (page 3)
Page 3 defines cinematic movie entries with timed subtitle overlays:
| Key | Description |
|---|---|
movie_name[N] | Smacker (.SMK) filename in the anims directory. |
movie_keys[N] | Comma-separated forward skip frames, up to twenty entries. Zero terminates the list. |
movieM_text[N] | Subtitle overlay: always,red,green,blue,start_frame,stop_frame,text. |
The shipped configuration defines 11 movie filenames. Five entries are browsable in the movie page; additional definitions supply introduction cutaways. Read the installed file rather than assuming a fixed cinematic list.
Action id ranges
Action ids follow a convention per page:
| Range | Meaning |
|---|---|
-1 | Continue / resume game. |
-2 | Quit game. |
| 1–10 | Main menu navigation (Save=1, Load=2, Movies=3, Options=4, New=10). |
| 100–102 | Save game page actions. |
| 200–202 | Load game page actions. |
| 300–305 | Movie page actions. |
| 400–407 | Options navigation. |
| 500–505 | Display controls and return. |
| 600–603 | Sound controls and return. |
| 700–724 | Key binding page actions. |
External References
KEYS.INI
Input binding configuration mapping keyboard scancodes, mouse buttons, and joystick axes to game actions.
Shipped path: KEYS.INI (game root directory).
Sections
[input]
Action-to-scancode bindings. Two binding slots exist per directional/action key (index [0] for keyboard, index [1] for joystick/gamepad).
| Key | Default | Description |
|---|---|---|
next_key | 52 (period) | Next item in inventory. |
prev_key | 51 (comma) | Previous item in inventory. |
inventory_key | 23 (I) | Open the inventory screen. |
log_key | 38 (L) | Open logbook. |
quick_sword_key | 16 (Q) | Quick-draw sword. |
quick_health_key | 35 (H) | Quick-use health potion. |
map_key | 50 (M) | Open map. |
key_up[0] | 17 (W) | Move forward (keyboard). |
key_down[0] | 31 (S) | Move backward (keyboard). |
key_left[0] | 30 (A) | Turn left (keyboard). |
key_right[0] | 32 (D) | Turn right (keyboard). |
key_a[0] | 42 (L Shift) | Walk/run modifier (keyboard). |
key_b[0] | 57 (Space) | Jump (keyboard). |
key_c[0] | 56 (Alt) | View / Look (keyboard). |
key_d[0] | 18 (E) | Activate / Attack (keyboard). |
key_up[1] | 138 | Move forward (joystick). |
key_down[1] | 139 | Move backward (joystick). |
key_left[1] | 136 | Turn left (joystick). |
key_right[1] | 137 | Turn right (joystick). |
key_a[1] | 135 | Walk/run modifier (joystick button 4). |
key_b[1] | 133 | Jump (joystick button 2). |
key_c[1] | 134 | View / Look (joystick button 3). |
key_d[1] | 132 | Activate / Attack (joystick button 1). |
user_key_up … user_key_d | 0 | Which binding of the action the Controls page shows and edits: 0 = keyboard binding [0], 1 = mouse/joystick binding [1]. Only the low bit is used. Both bindings stay active in play. |
Assigning a keyboard key on the Controls page stores it in [0] and sets the
action’s user_key_* to 0; assigning a mouse button or joystick input stores it
in [1] and sets it to 1. The page saves all three keys for the action. A [0]
or [1] value of 0 selects the action’s built-in binding for that slot.
The shipped file footer contains NEXT_KEY = 37 and PREV_KEY = 38 inside [defined]. These do not override bindings in [input]. Numeric configuration lookup returns the first matching key in the requested section.
[misc]
| Key | Default | Description |
|---|---|---|
waiting_message | HIT KEY | Prompt inside the Controls page capture brackets. |
[defined]
Scancode-to-name lookup table used by the key binding UI. Maps scancode indices 0–139 to human-readable names.
Scancodes 0–127 are keyboard keys (standard PC AT scancodes):
| Range | Keys |
|---|---|
| 0 | empty (no key) |
| 1 | escape |
| 2–11 | 1 through 0 |
| 12–13 | minus, plus |
| 14 | backspace |
| 15 | tab |
| 16–25 | q through p |
| 26–27 | l bracket, r bracket |
| 28 | enter |
| 29 | ctrl |
| 30–38 | a through l |
| 39–41 | :, ", tildy |
| 42 | l shift |
| 43 | \ |
| 44–50 | z through m |
| 51–53 | comma, period, slash |
| 54 | r shift |
| 55 | prt scr |
| 56 | alt |
| 57 | space |
| 58 | caps lock |
| 59–68 | f1 through f10 |
| 69–70 | num lock, scrl lock |
| 71–73 | home, kbd up, page up |
| 74 | num minus |
| 75 | kbd left |
| 77 | kbd right |
| 78 | num plus |
| 79–81 | end, kbd down, page down |
| 82–83 | insert, delete |
| 87–88 | f11, f12 |
| 76, 84–86, 89–127 | unkn NN (unused scancodes) |
Scancodes 128–139 are mouse/joystick inputs:
| Scancode | Name |
|---|---|
| 128 | left mouse |
| 129 | right mouse |
| 130 | mouse x (axis) |
| 131 | mouse y (axis) |
| 132–135 | button 1 through button 4 (joystick) |
| 136–137 | joy left, joy right |
| 138–139 | joy up, joy down |
Input state
Every input update fills one state table indexed by input code, then each
action reads the state at its primary code OR its secondary code (the [0] and
[1] bindings). Codes below 128 are keyboard scancodes written by the keyboard
interrupt. Codes 128 through 139 are written by the pointer and joystick polls,
which run at the start of each update:
| Code | Value |
|---|---|
| 128, 129 | Left and right mouse button release: nonzero for one update, when the button goes from pressed to released, and zero otherwise. A held button never reads as pressed. |
| 130, 131 | Horizontal and vertical mouse motion since the previous poll: the mouse driver’s signed motion count for that axis, halved and rounded toward zero. Nonzero on an update in which the mouse moved at least two counts along that axis. |
| 132–135 | Joystick buttons 1 through 4: nonzero while the button is pressed. |
| 136 | Joystick X reading is more than joy_tolerance below the calibrated center. |
| 137 | Joystick X reading is more than joy_tolerance above the calibrated center. |
| 138 | Joystick Y reading is more than joy_tolerance below the calibrated center. |
| 139 | Joystick Y reading is more than joy_tolerance above the calibrated center. |
Joystick polling
The joystick is enabled only when the DOS joystick BIOS service reports a nonzero axis value for stick A or stick B at startup. When enabled, one axis reading taken at startup becomes the calibrated center for X and Y; it is never recalibrated. When disabled, codes 132 through 139 always read zero.
An axis reading is a loop count, not a calibrated unit. The poll starts the
gameport one-shot timers with a write to port 0x201, then re-reads that port
up to 6144 times. Each axis reads as the number of iterations completed when its
timer bit (bit 0 for X, bit 1 for Y) dropped. A poll that times out reports 0 for
both axes, including an axis that had already finished. The count therefore
scales with the stick’s resistance and with the CPU’s port-read time. Only the
first stick’s axes are used. Each poll also records the change of each axis
reading since the previous poll.
Per update, with tolerance = joy_tolerance, x and y the readings and
cx and cy the calibrated centers, each comparison strict:
| Code | Reads 1 when |
|---|---|
| 136 | x < cx - tolerance |
| 137 | x > cx + tolerance |
| 138 | y < cy - tolerance |
| 139 | y > cy + tolerance |
Buttons 1 through 4 (codes 132 through 135) are the inverted high nibble of the gameport port, bit 4 upward: buttons 1 and 2 of stick A, then buttons 1 and 2 of stick B.
The game therefore uses at most two axes and four buttons of a controller. SideWinder profiles do not change this; they act through keyboard input.
For an ordinary keyboard key that passes the interrupt handler’s special-key handling, a make event writes 1 to its 16-bit state slot and a break event writes 0. A make writes the slot even when the key was already held.
Consumers can clear a key’s slot, as the candle mode key does when it changes mode. The slot can therefore be zero while the physical key remains held. A later accepted make event sets it again without requiring an intervening release; a release clears an unconsumed make.
Controls capture reads the same table: it accepts a keyboard scancode, a mouse button release (128 or 129) or a joystick button (132 through 135).
GOG release
The GOG release runs the game in DOSBox, so the gameport is DOSBox’s emulation,
selected by joysticktype in the [joystick] section of the DOSBox
configuration. The game launch of the GOG 2.0.0.5 installer loads
dosbox_redguard.conf and dosbox_redguard_single.conf; the first sets
joysticktype=none, which disables the gameport emulation, so the game has no
joystick input. The joysticktype=auto in DOSBOX/dosbox.conf is not part of
that launch, and the Graphic Mode Setup tool changes only display settings.
With none, the bundled DOSBox build also creates no controller bindings in its
keyboard mapper, so a controller cannot be mapped to keys there either. With
4axis, DOSBox emulates one joystick with four buttons, all of which the game
reads. With 2axis it emulates two two-button joysticks, so one host controller
provides only buttons 1 and 2. Once emulation is enabled, further controller
buttons reach the game only as keys assigned in DOSBox’s keyboard mapper
(mapperfile=mapper.txt, which the installer does not include).
Action semantics
The shipped manual, User’s Guide page 1, identifies Shift as Walk/Run and
Ctrl as Activate/Attack. These correspond to key_a and key_d, respectively,
in the GOG keyboard layout. The example bindings above describe one layout;
read the installed file rather than assuming those codes are universal.
External References
REGISTRY.INI
File-system abstraction configuration controlling archive (.ZAP) lookup, 32-bit file access, and registry path resolution.
Shipped path: REGISTRY.INI (game root directory, 302 bytes).
The registry subsystem configured by this file is non-functional in the shipped game. The console command show registry reports the registry system is not open, and altering this file has no observable effect at runtime.
Sections
[registry]
Archive-to-path mapping entries. Each line maps a .ZAP archive file to a file glob pattern:
3DART\OBJECTS.ZAP 3DART\*.3D
SYSTEM\GXA.ZAP SYSTEM\*.GXA
INI.ZAP *.INI
The shipped file also contains a commented-out entry: SOUP386\SOUP386.ZAP SOUP386\*.ai.
These entries would allow the engine to look up files inside .ZAP archives instead of (or in addition to) loose files on disk. In practice, the GOG release does not ship any .ZAP archive files — all assets are loose.
[file32]
File access configuration flags:
| Key | Default | Description |
|---|---|---|
use_registry | false | Enable the Windows registry path lookup subsystem. |
use_32bit | true | Use 32-bit file I/O (DOS/4GW extended file access). |
archive_file_first | false | Not read from this file. The Glide engine reads archive_file_first from SYSTEM.INI [system]. |
local_path | true | Use local (relative) file paths. |
ignore_registry_errors | false | Suppress errors from registry path lookup failures. |
registry_log | false | Enable logging of registry system operations. |
advanced_exist | true | Use advanced file-existence checking. |
External References
- UESP: Redguard:Console (documents
show registrycommand)
SURFACE.INI
Text configuration mapping texture keys to surface classes and animation sound IDs to effect IDs.
The file lives in the game root directory as surface.ini.
The engine reads this file at startup and requires a surfaces section. It
validates surface type values, texture set and index bounds, and parts of each
sound section.
File Structure
Top-level sections:
[surfaces]section: texture-set/index to surface-type assignments- Sound remap sections:
[unknown],[water],[deepwater],[scapewater],[scapedeepwater],[lava],[sand],[wood],[tile],[scape],[rock],[gloop]
Surface Assignment Records ([surfaces])
set, index, typeset, all, type
Where:
set: texture set id, range 0..511index: texture index inside the settype: case-insensitive name from the surface-class table below
The shipped comments describe all as a wildcard, but the executable’s loader
converts the index with its decimal integer reader. all therefore becomes
index zero; it does not expand to every texture. Numeric indices must be in
0..127. The reader accepts an optional sign and an initial sequence of decimal
digits; a token without initial digits produces zero.
Assignments retain file order. Each lookup key is (set << 7) | index, and the
first matching assignment wins. A zero key terminates lookup before matching,
so an assignment to set zero, index zero hides itself and every later assignment.
An unmatched key returns surface zero (unknown).
For v4/v5 model faces, solid-color materials resolve to packed surface key zero, and therefore to surface zero. Both palette-indexed and directly encoded solid colors follow this rule; their color value does not select a texture assignment.
Surface classes
The same class codes select sound rows and describe the actor’s ground state.
| Code | Name |
|---|---|
| 0 | unknown |
| 1 | water |
| 2 | deepwater |
| 3 | scapewater |
| 4 | scapedeepwater |
| 5 | lava |
| 6 | sand |
| 7 | wood |
| 8 | tile |
| 9 | scape |
| 10 | rock |
| 11 | gloop |
Sound remap records
Each named sound section contains <source> = <target> records. The equals
sign is optional. Names are case-insensitive; semicolons start comments.
Each class has an independent 256-entry table initialized to identity. Missing or empty sections keep that identity table. Repeated sections are processed in file order, and later sound assignments overwrite earlier ones. Water and terrain-water classes have separate tables; neither inherits the other’s entries.
Both numeric tokens must begin with a decimal digit. Conversion uses the
initial decimal digits, ignoring a following suffix. Source IDs must be in
0..255; invalid sources cause a configuration error. Target IDs are stored
as unsigned 16-bit values, retaining the low 16 bits of the converted integer.
They are not restricted to the animation source range or to the loaded sound
bank’s size. Tokens after the target on the same line are ignored.
Only animation PlaySound commands use these rows. Surface zero bypasses the
lookup even if [unknown] contains remaps. Script sounds, combat hit sounds
and speech do not use the remap. See sound playback
for lookup bounds and the ground-state source.
Terrain surface selection
Terrain starts with the assignment for texture set 302 and the low-six-bit
image index from WLD Map 3. Fixed engine blend-mask rules can replace this
base class with sand, rock, scapewater or scapedeepwater. These rules are
not configurable records in SURFACE.INI. The file has no CFG mask paths or
palette-remap tables. See terrain material lookup and coverage.
Rust API
rgpre::import::surface_ini::SurfaceIni::parse reads the assignments and
sound rows. Its assignments.surface(packed_texture) preserves first-match
texture lookup and zero-key termination. sound_table(surface) exposes the
configured row; animation_sound(surface, source) also applies the surface-zero
bypass. Both sound accessors return None for class codes outside 0..11.
Animation sources are u8; resolved bank indices are u16.
The parser rejects numeric overflow beyond a nonnegative signed 32-bit integer
for sound tokens. Errors carry the file, section, line and offending key.
SurfaceAssignments::parse remains available for assignment-only consumers
and does not validate sound sections.
Relationships to Other Formats
- WLD terrain Map 3 supplies image indices for terrain surface queries.
- TEXBSI supplies the texture images identified by assignment keys.
External References
FOG.INI
Glide scene fog density table for models and terrain.
Selection and format
The Glide renderer first reads fxart/FOG<world ID>.INI, then
falls back to fxart/FOG.INI. If neither exists, all 64 entries remain zero.
This selection is independent of the software renderer’s world_haze field.
The installed GOG data contains only the fallback file.
Comma-separated index/value pairs fill a zero-initialized 64-entry table. Each value fills its index through the index before the next pair. This file expansion does not interpolate values. Semicolon comments are ignored.
- Index is a table position from 0 through 63.
- Value is a density byte from 0 through 255.
- The final index
63has no value and remains zero.
Shipped values
The installed fallback contains these 18 index records.
| Index | Value |
|---|---|
| 0 | 0 |
| 33 | 1 |
| 34 | 2 |
| 35 | 4 |
| 36 | 6 |
| 37 | 8 |
| 38 | 10 |
| 39 | 12 |
| 40 | 14 |
| 41 | 16 |
| 42 | 18 |
| 43 | 22 |
| 44 | 38 |
| 45 | 76 |
| 46 | 136 |
| 47 | 196 |
| 48 | 255 |
| 63 | End marker, remains zero |
Entries 0 through 32 are zero; entries 48 through 62 are 255. The shipped comment says “46 is the final value for 3800 render distance”. The actual density at distance 3800 uses entries 47 and 48, as described in Glide fog and clipping. Changing the clipping distance does not rescale this table.
External References
Miles audio configuration
DIG.INI and MDI.INI store a selected Miles driver and its sound-card I/O parameters.
Record syntax
Each setting consists of a key, whitespace, and its value. These files do not
use section headers or equals signs. A DEVICE value can contain spaces.
Lines beginning with ; are comments, and blank lines separate records.
The shipped files use CRLF line endings.
DEVICE Creative Labs SoundBlaster 16, AWE32 or AWE64
DRIVER SB16.DIG
IO_ADDR 220h
IRQ 7
DMA_8_BIT 1
DMA_16_BIT 5
Settings
| Key | Value syntax | Meaning |
|---|---|---|
DEVICE | Text | Displayed device description. |
DRIVER | Filename | Selected .DIG or .MDI driver image. |
IO_ADDR | Hexadecimal digits followed by h | Sound-card I/O base address. 220h is 0x220. |
IRQ | Decimal integer | Interrupt request number. |
DMA_8_BIT | Decimal integer | DMA channel for eight-bit transfers. |
DMA_16_BIT | Decimal integer | DMA channel for sixteen-bit transfers. |
The shipped digital configuration selects SB16.DIG. The MIDI configuration
selects SBLASTER.MDI and labels it Creative Labs Sound Blaster or 100% compatible. Both store I/O address 220h, IRQ 7, eight-bit DMA channel 1,
and sixteen-bit DMA channel 5.
Runtime role
The original engine initializes digital sample playback from DIG.INI.
Its MIDI initialization path reads MDI.INI, but the shipped engine disables
that path. Background music uses CD audio.
These values configure DOS sound hardware. A modern audio backend handles its own device selection. The driver format and setup catalog describe the accompanying hardware resources.