Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scene runtime

Scene state

Entering a scene creates a fresh scene-local runtime while preserving global game state. The engine MUST load the requested BIN, reset its display list, action list, dialogue choices, navigation graph and callbacks, animations, and scene threads, then execute the program from offset zero. Art and palette loads performed by the program populate that scene’s rendering state.

Scene changes are deferred requests. Once requested, the current update MUST stop accepting scene actions, finish safe cleanup, and enter the named scene with the named secondary entry string. Resource names are case-insensitive.

Display objects

The scene maintains an ordered display list. A logical display object has:

  • a type identifying direct, thread-driven, or animation-driven rendering;
  • X and Y coordinates in the 320×200 logical space;
  • a 16-bit scale where 0x0100 is native scale;
  • render flags, including horizontal and vertical reflection;
  • an ART slot and frame number; and
  • a hidden state.

Direct, thread, and animation declarations append objects to one mixed display list. Their controllers update the frame and transform in the corresponding stable slot, and all active slots MUST be painted in global list order. Hiding an object preserves the record and its animation state but omits its pixels. Showing it makes the current state visible again.

The on-disk animation-step record is exactly nine bytes:

OffsetTypeMeaning
0u8One-based ART frame; zero suppresses drawing.
1u8ART slot.
2i16leX.
4i16leY.
6u16leScale.
8u8Render flags.

An animation definition consists of the interval word from opcode 06 followed by its contiguous 07 records. Runtime animation state MUST retain the first and current step, interval, optional linked animation, mode/state, and display object. Starting resets the selected sequence as required by its mode; stopping releases its visible object. The mode byte is also the live state observed by the VM.

Opcode 07 itself only advances over a nine-byte record. An implementation MUST accept such records before any opcode 06 and leave them unowned rather than rejecting the scene. CP1.BIN begins with six of these records after a direct display object and before its first animation definition. Later animation definitions continue to own the contiguous records following their respective opcode 06.

Its complete resource-visible transition table is:

An unlinked animation renders the current step’s absolute X, Y, and scale. A linked animation recursively resolves its parent animation and applies the child’s signed delta from the child’s first step:

x     = parent.x     + (current.x     - first.x)
y     = parent.y     + (current.y     - first.y)
scale = parent.scale + (current.scale - first.scale)

The arithmetic has 16-bit wrapping behavior. Frame, ART slot, and render flags still come from the child’s current step. A link whose parent is stopped or otherwise not renderable suppresses the child as well. Implementations MUST reject or safely suppress invalid and cyclic links rather than recurse without a bound.

Start modeInitial stepDirectionEnd-of-sequence behavior
1FirstForwardSet state 0 and release the visible render slot.
2LastBackwardSet state 0 and release the visible render slot.
3FirstForwardWrap to the first step, retaining state 3.
4LastBackwardWrap to the last step, retaining state 4.
5FirstNoneRemain on the first step in terminal state 5.
6LastNoneRemain on the last step in terminal state 6.
7FirstForwardBack up to the last valid step and change to state 8.
8FirstBackwardRetain the first step and change to state 7.
9FirstForwardRemain on the last step and change to terminal state 6.
10LastBackwardRemain on the first step and change to terminal state 5.

Modes 7 and 8 therefore form a ping-pong pair. A forward overrun in mode 7 retains the last step before changing to mode 8; a backward underrun in mode 8 retains the first step before changing to mode 7. Mode 8 is unusual because an explicit start begins at the first step even though its first attempted move is backward. Starting a mode renders its initial step immediately and initializes its countdown from the negated sequence interval. Each animation update adds the logical tick increment; when the countdown becomes positive, it performs the table’s step and subtracts the interval again.

State values 0, 5, and 6 are terminal for the VM’s wait and finished predicates, even though states 5 and 6 retain and render their endpoint. Stopping any mode sets state 0 and releases its visible render slot. The precise subdivision of the remaining private timing storage is not exposed and need not be reproduced as a host data layout.

Scene threads and movement

At least ten independent scene command streams MUST be supported because the shipped content addresses ten scheduler slots. A thread retains its active or suspended status, next command offset, delay, motion state, navigation node, direction, position, selector label and coordinates, and any active movement or overlay.

Each update decreases eligible delays, resumes ready command streams, advances movement and animations, and resolves completed arrival/departure callbacks. The reference supplies the elapsed timer delta to these controllers; it does not add one merely because one host frame was presented. The controller clock runs at 2,880 units per second. Its normal interrupt contribution is 24 units, and the elapsed delta supplied to a single update is capped at 400. Movement steps use an interval of 20 (about 6.94 ms), animation steps commonly use 40 (about 13.89 ms), and ordinary scheduler delays such as 3,000 last about 1.04 seconds. A host may subdivide or batch updates, but it MUST accumulate elapsed time at this rate and preserve command-boundary ordering. While the opcode-8B transient-status timer is active, an outer controller boundary is itself behavioral because it controls shared-random-stream consumption.

A real-time graphical frontend MUST target the reference’s normal controller and presentation cadence of approximately 120 Hz. It must compose and submit one visible frame after each ordinary boundary, whose elapsed delta is normally 24 units. Small host-clock jitter may place neighboring deltas around 24, but a frontend MUST NOT deliberately use a 60 Hz cadence whose routine delta is about 46 units. COMBAT4.BIN, for example, uses 30- and 40-unit animation intervals, so such batching can advance past an authored frame before that frame is ever presented.

After a genuine host stall, the reference can accumulate several interrupts, cap their combined delta at 400, advance through multiple animation records, and compose only the resolved state. A graphical host MAY reproduce that exceptional catch-up behavior. A non-visual host MAY batch simulation and omit intermediate presentation entirely, but that mode does not establish visual conformance. Comparison runs MUST supply the same sequence of controller boundaries and elapsed deltas; they MUST NOT coalesce or subdivide those logical updates merely to change presentation frequency. Movement must be smooth enough for the original art and action coordinates, and waits must not complete before their controller reaches the states named in the bytecode chapter.

The scheduler delay is a signed countdown. A newly runnable stream has delay zero. Opcode 0F subtracts its operand from that value, commonly making it negative, and yields that invocation. The outer update adds the logical elapsed-tick increment to negative delays and does not enter that stream again until its delay is nonnegative. Eligibility is checked on invocation entry, not between every pair of commands. Changes that a synchronous same-slot callback makes to the shared active, suspended, or delay state therefore do not pause the already-running outer invocation; only a yield from the outer invocation returns its cursor to the scheduler. This is how nested callbacks preserve DOS command ordering while palette fades and input-polling loops remain paced.

Starting a thread at a new target is a fresh invocation, not a resume. The engine replaces the cursor, marks the slot active and unsuspended, clears its previous delay and bytecode return stack, and makes it immediately eligible. Opcode 3E executes the named slot synchronously until it yields before continuing the caller. Action selection, navigation callbacks, and successful study callbacks apply the same fresh state; modal dismissal instead resumes the suspended invocation and therefore preserves its return stack and delay. When multiple navigation callbacks match, each fresh invocation MUST run synchronously through its first yield or return before the next record is examined. A later callback assigned to the same slot may consequently replace an invocation left runnable by an earlier callback. The cursor published by a later yield of the resumed outer invocation likewise replaces the nested callback cursor, while their changes to the shared delay accumulate.

Scene-thread movement artwork

Opcode 02 records are navigation-node geometry, not independent visible actors. The primary movement controller uses the source node’s display slot and renders one global RUN.ART sprite while traversing an edge. Opcode 53 sets both current-node identifiers, snaps X, Y, and scale to that node, and starts the ordinary controller with a minimum same-node traversal without changing the retained desired destination. It MUST NOT put the actor directly into idle state. The traversal preserves the previous walking phase, suppresses turn-pose selection because its distance is one step, and dispatches the destination-node arrival callback when it completes. A following opcode 54 therefore replaces the desired destination while this initial traversal remains active. Merely changing the identifiers without the same-node controller lifecycle is incompatible.

For one edge, let dx, dy, and ds be the differences in X, Y, and scale, and let average_scale = (start_scale + end_scale) / 2. The reference derives the number of 20-unit interpolation steps as:

planar = (max(abs(dx), abs(dy)) + min(abs(dx), abs(dy)) / 2)
         * average_scale / 256
depth  = abs(ds)
steps  = max(1, max(planar, depth) + min(planar, depth) / 2)

Coordinates and scale use fixed-point increments across those steps. The horizontal direction is right when dx > 0 and left otherwise. It switches to the away/toward orientation when 2 * abs(ds) exceeds abs(dx) * average_scale / 256; nonnegative ds selects away and negative ds selects toward.

The walking phase advances by 28 modulo 0x0600 per interpolation step. Its high byte selects one of six positions. Direction offsets are 0 for left and right, 24 for away, and 12 for toward. An idle actor adds six to that index. The resulting index selects a one-based RUN.ART frame from this exact table:

01 02 03 04 05 06  13 13 13 13 13 13
07 08 09 0A 0B 0C  14 14 14 14 14 14
0D 0E 0F 10 11 12  15 15 15 15 15 15

Left-facing horizontal frames set horizontal reflection; the other three orientations do not. A moving actor remains at the display-list position of the opcode-02 record that created its controller slot. Later display records can therefore occlude it.

The walking phase MUST remain continuous when the controller reaches an intermediate node and starts another edge. It resets to 0x0200 only when the controller determines that no further edge is required or available and enters idle state.

Changing orientation can insert a stationary turn pose before interpolation. A left/right horizontal reversal uses one-based RUN.ART frame 20. A direct toward/away reversal uses frame 19. Both poses block coordinate and walking phase advancement for 224 controller units; the override frame is displayed while that countdown is nonzero. Horizontal-to-depth and depth-to-horizontal changes do not use either special pose. A minimum one-step traversal also does not select a turn pose.

Navigation edges are undirected. Route selection is iterative-deepening depth-first search for paths of one through 20 edges. Each depth attempt scans edge records in insertion order and, within one record, tests traversal from the stored first endpoint before traversal from the second endpoint. The search has no visited-node exclusion. A route requiring more than 20 edges is unreachable to the controller even if the graph contains a longer path.

A movement request retains one desired destination. It starts as 0xFF, persists across scene replacement, and is reset to the current node only when the controller resolves final idle or an ordinary request has no route. If interpolation is already active, a different request replaces only that destination; it MUST NOT reset the current edge, its position, timing, walking phase, turn pose, or callbacks. A request equal to the retained destination is a no-op. After completing an edge and its arrival callbacks, the controller searches again from the reached node toward the latest desired destination. Thus callbacks can change the graph or desired destination before the next search.

For each traversed edge, the controller updates the current node and processes arrival callbacks before replanning. The forward and reverse callback families distinguish traversal relative to the edge’s stored node order. The byte operand of opcodes 17 through 1A is the zero-based index of the opcode-0B edge record, not a node number. For each edge, opcode 17 is forward departure, 18 is reverse departure, 19 is reverse arrival, and 1A is forward arrival. The engine dispatches the matching directional edge callback and node callback in phase-specific order:

  1. Initial movement searches, runs the source-node departure, runs the directional edge departure, and then initializes the selected edge.
  2. State-five continuation searches, initializes the selected edge, runs its directional edge departure, and then runs the source-node departure.
  3. Edge completion first makes the interpolation state inactive for movement requests, then runs the directional edge arrival and destination-node arrival callbacks. A first arrival callback may therefore start a fresh edge while a later callback retargets that active edge.

If no route exists, the request MUST leave the object at its current node but still run a minimum same-node controller traversal before becoming idle. A request whose destination is already the retained desired node is a no-op. Opcode 53 leaves that retained destination unchanged.

Opcode 79 clears navigation callback registrations only. It MUST preserve the navigation graph and any movement already in progress.

After the initial offset-zero invocation has built the entry, edge, and callback tables and returns, scene entry dispatch matches the requested secondary entry string case-insensitively. The matching opcode-0C record’s first node byte is the initial/current node and its second byte is the destination. That traversal uses the ordinary departure and arrival callback machinery; it is not a direct jump to a hard-coded scene-specific target. For example, the startup seg entry in LOGO.BIN traverses its declared 34 edge and dispatches the arrival handler attached to node 4.

Opcode 05 terminates the offset-zero invocation before this traversal begins. Starting a nonempty entry route MUST NOT let that loader invocation fall through to the bytes after opcode 05; only a configured callback may reactivate a thread at its target. A departure callback activated by entry dispatch runs as a fresh invocation after the loader has returned and MUST NOT be deactivated by that loader’s opcode 05. Otherwise a departure callback can be lost, or a destination arrival handler can run once as fallthrough and again when movement completes.

Actions

An action target is logically:

FieldType
Program targetu16 absolute BIN offset
X, Yu16, u16 logical coordinates
Labelstring reference
Activeboolean

The engine displays or otherwise exposes active actions when global action selection is enabled. Selecting an action resumes execution at its target. Thread selectors use the same player-facing interaction model but are attached to a moving scene object. The engine MUST preserve list order for keyboard cycling and deterministic hit testing.

Each scene-thread motion state also controls that thread’s normal actor render slot. State zero releases the slot and suppresses the ordinary actor sprite. This permits a scene animation to supply a temporary replacement pose without drawing both poses. Nonzero states allow the normal actor controller to submit its current frame. State two additionally requests an immediate motion update when written by opcode 40.

A newly entered scene starts with global action selection enabled. A newly created scene-thread target is also enabled. Opcode 10 optionally assigns a selector label and separate selector coordinates; opcodes 1C and 1D enable or disable that individual target, and opcodes 41 and 42 gate all targets for the current scene.

New opcode-02 records initialize their separate selector anchor to (30000,30000). Opcode 10 replaces that sentinel with the authored selector X/Y and string. A thread which never receives opcode 10 therefore cannot enter proximity selection, hover rendering, keyboard matching, or the Space cycle, but remains in pointer hit-testing through its opcode-02 navigation geometry. The current unlabeled node is not eligible. This permits a clickable passage to be represented solely by navigation geometry.

An empty opcode-10 selector string prevents keyboard matching and hover artwork. Space-cycle eligibility itself follows the enabled state and on-screen selector coordinates rather than testing the string. Ordinary opcode-3A actions still require their authored selector string.

This initialization is observable in FIRST.BIN: it configures node 2 at (282,124) with selector .r but never executes 1C or 41 on the initial path. After the entry animation completes, that target must therefore be available by pointer activation, the Right arrow, and the selector’s keyboard key. Treating either initial enable state as false leaves the first explorable scene inescapable.

GANTRY.BIN demonstrates the unlabeled case. Nodes 2 and 3 are both at (190,70), the seg entry settles on node 2, and node 3 has the arrival callback that enters CP1. Neither node has an opcode-10 label. Excluding the current node and retaining table order makes a click near the opening select node 3 and enter the Unibot.

Palette, audio, and modal waits

Palette mapping and blackout effects are advanced as part of the scene update. Loading a palette resets a 256-entry source-index map to identity and clears a signed adjustment word for every output index. Palette output index i is computed component-wise as:

clamp(base_palette[mapping[i]] + adjustment[i], 0, 63)

Opcode 16 fills an inclusive adjustment range. Opcode 6C advances and wraps its phase inside the inclusive range, then maps successive output indexes to successive source indexes beginning at that phase and wrapping at the range end. Rotation changes this mapping; it does not destructively rotate the already adjusted RGB output.

Audio waits complete on actual playback completion when audio is enabled and on an equivalent logical timer when it is muted or unavailable. This prevents muting sound from changing scene progression.

Dialogue, choice menus, study screens, pause screens, save/restore screens, and help/status screens are modal. While one is active, ordinary scene actions do not advance. Animations and audio MAY continue where that does not alter program outcomes. Dismissing a modal interface resumes the suspended command at the specified continuation.

The standalone Computer Bible, map, Faith/power notices, options, and save-name editor are blocking top-level interfaces: the scene controller is not invoked while they remain open. A study screen requested by scene opcode 49 instead belongs to the requesting command stream; that owner remains suspended until Apply or Off supplies the result.

Capacity and validation

A clean implementation SHOULD use dynamically sized collections. It MUST support every table size used by the shipped resources, including 66 text descriptors and ten scene-thread slots. It MUST reject invalid resource offsets, unterminated strings, impossible frame references, and table growth that would exhaust its configured safety limits with a descriptive error, rather than corrupting adjacent state.