Script State and Progression
Primary-state structure
The 200-byte live block at DS:727A is the scene interpreter’s shared state.
New-game initialization writes 100 zero words with rep stosw, and the save
system copies exactly those 100 words to and from the checkpoint block at
DS:7BF2. Values are signed 16-bit integers where arithmetic or comparison
requires a sign.
BIN operands do not store variable ordinals. They store even byte offsets
from DS:727A; the interpreter shifts an offset right once before indexing
the word array. Thus operand 0x002A identifies variable 21:
address = DS:727A + encoded_offset
index = encoded_offset / 2
Across all 64 recovered BIN code regions, the core variable instructions reference 39 of the 100 slots. Every one of their encoded operands is even and below 200. Many high-numbered slots are scene-local temporaries rather than persistent player attributes.
Identified variables
| Index | Byte offset | DS address | Current meaning | Evidence |
|---|---|---|---|---|
| 0 | 0x00 | 727A | Difficulty: 0 Easy, 1 Normal, 2 Difficult | Difficulty input writes these values; map loading indexes END; faith damage branches on the same values. |
| 11 | 0x16 | 7290 | Current map X | Used in every 3*x cell calculation and exploration-bit update. |
| 12 | 0x18 | 7292 | Current map Y | Used in every 48*y cell calculation and as the exploration-row index. |
| 16 | 0x20 | 729A | Current map level letter | load_map_resource compares and caches its level argument here. |
| 17 | 0x22 | 729C | Current cell parameter A | process_current_map_cell copies cell byte +1 here. |
| 18 | 0x24 | 729E | Current cell parameter B | process_current_map_cell copies cell byte +2 here. |
| 21 | 0x2A | 72A4 | Faith in hundredths of a percent | Initialized and clamped to 10,000; the F3 display divides by 100. |
The first gameplay entry in FIRST.BIN demonstrates the interface directly:
it stores X=0 in offset 0x16, Y=6 in offset 0x18, and faith=10,000 in
offset 0x2A before processing the current map cell.
Map processing also produces several context-local words for the hall scene:
| Index | DS address | Map-processing role |
|---|---|---|
| 13 | 7294 | Connected-cell low kind, or zero-mask room class 0–4. |
| 14 | 7296 | Room entrance code 0–2, or a neighboring-cell kind during hall processing. |
| 23 | 72A8 | Parameter B of a Trap room immediately right of the hall. |
| 24 | 72AA | Parameter B of a Trap room immediately left of the hall. |
| 25 | 72AC | Parameter B of a Trap room immediately above the hall. |
These are not durable player attributes. process_current_map_cell rebuilds
them from the live map, and other scenes can reuse the same general-purpose
slots. The world-map chapter describes the room quotient/remainder and the
adjacent Trap interaction that establish these meanings.
Variable bytecode
The core instruction family is now recovered. In this table, var operands
are encoded byte offsets, value is a signed immediate where relevant, and
target is an absolute BIN file offset.
| Opcode | Operands | Effect |
|---|---|---|
0x1E | source, destination | Copy a variable. |
0x1F | value, destination | Store an immediate value. |
0x20 / 0x21 | var, target | Jump if zero / nonzero. |
0x22 / 0x24 | left, right, target | Jump if two variables are equal / unequal. |
0x23 / 0x25 | var, value, target | Jump if a variable equals / does not equal an immediate. |
0x26 / 0x28 | left, right, target | Signed jump if left is greater than / less than right. |
0x27 / 0x29 | var, value, target | Signed jump if a variable is greater than / less than an immediate. |
0x2A / 0x2B | source-or-value, destination | Add a variable / immediate to the destination. |
0x2C / 0x2D | source-or-value, destination | Subtract a variable / immediate from the destination. |
0x2E / 0x2F | source-or-value, destination | Signed multiply the destination by a variable / immediate. |
0x30 / 0x31 | source-or-value, destination | Signed divide the destination by a variable / immediate. |
0x32 / 0x33 | var | Increment / decrement. |
0x8F / 0x90 | source-or-value, destination | Bitwise-AND the destination with a variable / immediate. |
The disassembler annotates known operands with both forms, for example
var[21:faith]@0x002a. This also prevents immediate values and jump targets
from being mistaken for variable numbers.
Boolean state flags
Variables 3 through 10, at DS:7280..728F, are also treated as a 128-bit
flag bank. Identifier n selects word n >> 4 and mask
1 << (n & 15). Dedicated helpers test, set, and clear one identifier using
mask and inverted-mask tables in the executable.
| Opcode | Operands | Effect |
|---|---|---|
0x73 | flag, target | Jump if the flag is clear. |
0x74 | flag, target | Jump if the flag is set. |
0x75 | flag | Clear the flag. |
0x76 | flag | Set the flag. |
The scene corpus uses 78 distinct identifiers through 0x55. They mix
temporary navigation/action state with durable progression. When the current
map cell is processed, the executable clears the first three flag words
(0x00..0x2F) and rebuilds movement and action availability from the cell
and its neighbors. Flags at 0x30 and above survive that operation.
Five durable identifiers map exactly to the F4 through F8 status icons:
| Flag | Capability |
|---|---|
0x30 | Sword |
0x31 | Shield |
0x32 | No Trap |
0x33 | Candle |
0x34 | Flight |
Flag 0x36 is the status-artwork gate. The renderer at 0x641B releases
the Computer Bible, Map, Faith, and power slots and returns when it is clear;
several shipped conversation scenes clear it and restore it around their
authored sequences. The main loop separately gates F1 through F8 on action
selection word 004C. Its pointer-status path requires flag 0x36 as well,
so keyboard shortcuts can remain active when the row is hidden but clicks
cannot. The branch at 0x8845 also maps ASCII b to the same Computer Bible
routine as F1 whenever word 004C is nonzero. The renderer clamps Faith for
frame selection and retains the empty frame at exactly zero.
The same renderer owns the dormant transient started by unused opcode 8B.
While flag 0x36 and timer word 005C are nonzero, its pre- and post-VM
refreshes each consume the runtime random source and use the low two bits as
flip flags for zero-based STUFF.ART frame 27. The exact timer and call-order
rules are recorded under Runtime random source.
Two adjacent flags control automatic combat:
| Flag | Meaning |
|---|---|
0x37 | Automatic Combat option is enabled. |
0x38 | An ordinary combat scene is active; lock the option against changes. |
The Game Options routine displays the on/off state from 0x37. When 0x38
is set it assigns the Automatic Combat row a disabled target instead of the
normal toggle target. COMBAT1 through COMBAT5 and COMBAT7 set and clear
0x38 around their shared encounter lifetime. The exceptional guard program
COMBAT6 does neither.
Game-options dispatcher
The main input loop handles Escape at 0x88FE after the action-selection
branches have rejoined. It calls the options routine at 0x2F36 without
testing word 004C, so the menu remains available while the status-control
row and ordinary scene actions are disabled. Ordinary dialogue and choice
routines translate their own Escape input into status event 12, which
poll_input_event at 0x7C3F returns to this dispatcher. Their modal state
remains active and is redrawn after Game Options closes. Other modal
interfaces may consume Escape locally.
The dialogue routine at 0x2A89 and text selector at 0x2758 similarly
propagate BIOS F-key values BBh..C5h through status word 0052.
poll_input_event adds BBh back. The main loop sends F1..F8 to the status
switch only when word 004C is nonzero, but its F9 quick-load and F10
quick-save tests at 0x892D and 0x890A are outside that gate. Status
screens entered this way are synchronous: returning from one redraws an
interrupted dialogue or choice unless a load changed the session.
Faith and power descriptions take a subtly different route. Status dispatcher
0x83B6 calls wrapper 0x2AFD, which invokes the same dialogue reader but
ignores its return value. A plain F1 through F10 still makes the reader return
negative and closes the description, yet that key never returns to the main
dispatcher and cannot open another status screen or trigger quick save/load.
The next scene poll redraws any interrupted dialogue or choice.
The Computer Bible’s printable b alias is different. Its comparison at
0x884C..0x885F is an exact test against 62h and is reached only after a
blocking text routine has returned the key to the main dispatcher. Ordinary
dialogue does not return b, and
the common choice selector consumes it as a first-letter row accelerator when
one matches. Only a top-level lowercase b therefore opens the Bible;
Shift+B and Caps-Lock B do not, while Shift plus Caps produces lowercase
ASCII and does.
Because those routines are blocking, the key or pointer event used to leave one is consumed before the main dispatcher resumes. It cannot also activate the restored dialogue, set a scene confirmation latch, or select an underlying world target during the same poll.
The status switch at 0x83B6 has 13 entries. Entries 0 through 7 are the
Computer Bible, Map, Faith, and five powers; entries 8 through 11 are no-ops;
entry 12 calls the same options routine. The click dispatcher at 0x87E0
special-cases entry 12 before testing action selection or flag 0x36. This is
the always-present upper-right disk indicator, so its authored bounds remain
an options control even when the rest of the status row is hidden.
The options routine at 0x2F36 temporarily replaces the active text-menu
table and builds its rows in this exact order:
- Continue
- Load Game
- Save Game
- New Game
- the translation string indexed by word
007C - Music On or Music Off from word
0048 - Sound Effects On or Sound Effects Off from word
004A - Automatic Combat On or Off from flag
37 - Quit
The installation no-combat word at 0058 suppresses row 8. Flag 38
replaces that row’s target with -2, the menu’s disabled value. A translation
lock leaves the current translation row visible but prevents its target from
cycling word 007C.
Before calling select_from_text_menu, the routine sets the text origin to
logical (180,10) and the width to 130. choose_save_slot at 0x2B6F uses
ten rows at (140,10) with width 170: the appropriate Cancel string followed
by all nine labels. The confirmation helper at 0x2EEC builds two rows from
Start New Game or Quit Game plus Cancel.
For each load row, choose_save_slot compares the mutable filename suffix at
DS:0045 with that row’s digit and confirms that the state file exists. A
match is copied to a temporary buffer and has literal << appended. Selecting
a normal save or load leaves its digit as the active suffix; the F9/F10 path
temporarily uses Q and restores suffix 0, so quick operations do not mark a
numbered row.
The save-name routine at 0x2DF7 copies the selected label to a 27-byte local
buffer and clears it first when it equals (EMPTY). Its editor at 0x2C8B
limits input to 26 bytes and accepts ASCII ranges 20..3B, 3F..5A, and
61..7A, except for & and *. Backspace (08h) and Left (CBh) delete,
Enter accepts, and Escape cancels. The local timer at 0x2CA5..0x2CD6
toggles a literal > at the current end every 02BCh (700) reference-timer
units. Editing or deleting writes a new terminator and hides the cursor until
the next toggle. After acceptance, initialize_empty_save_slot at 0x815A
replaces an empty or (EMPTY) label with Game 1 through Game 9.
Before entering that editor, 0x2DF7 rebuilds a ten-row text table: disabled
row zero points to Enter New Name, and rows one through nine retain every
save label. It calls select_from_text_menu with the chosen slot plus one so
the complete panel is drawn without entering the selector loop, releases the
SELECT render slots, and passes the chosen row’s render slot and text
coordinates to 0x2C8B. The editor therefore redraws only that row in style
2 while all other labels remain visible in style 1.
The common selector’s Escape branch at 0x278E reaches 0x28A3 and returns
-1. The options caller distinguishes that value by nesting level. At
0x3267 it exits the main menu; the save/load calls at 0x3257 and 0x3276
loop back to rebuild the main menu; and the confirmation wrapper at 0x2EEC
also reports a negative selection as unconfirmed. The label editor differs:
0x2ECC skips copying the edited local buffer on Escape, but the caller at
0x3261 still continues to save_selected_slot. Thus Escape there rejects
the label edit without cancelling the selected save operation.
The function-key branch at 0x2758..0x2772 also makes the selector return
-1, after recording the BIOS value relative to BBh in word 0052.
Dialogue and choice callers expose that negative return directly to
poll_input_event, but Game Options consumes it inside its own call stack.
The main selector consequently closes at case -1/0x3267; a negative
save/load selection returns nonzero to 0x325D or 0x327C and rebuilds the
main menu; and a negative confirmation becomes false at 0x2F2B. No
top-level F1..F10 action runs from that same key. The save-name editor at
0x2C8B uses a separate input loop and rejects function-key values.
F9’s state replacement is also a loop boundary, not a deferred scene request
inside the controller. The branch at 0x8934 reads .SVQ, stores its return
mode at 007A, and reaches the return test at 0x8966. A successful read
therefore exits main_menu_and_game_loop. game_main stops boundary music,
dispatches mode 2 at 0x8C4A..0x8C75, copies the retained checkpoint buffers
to live state, and only then re-enters the scene loop. The old scene cannot
execute another update with the restored variables.
F10 takes the neighboring but non-transitioning path. At 0x8911 it calls
start_palette_blackout(1), which stores 0081 in the palette-effect word.
The immediate compositor call at 0x8920 runs update_palette_effect; that
clears the high bit, writes all 256 VGA entries black, decrements the effect
to zero, and renders before write_save_state. The next compositor update
reapplies the mapped scene palette. The save data and indexed framebuffer are
not themselves blackened.
The same selector supplies first-letter accelerators. Its loop at
0x27AA..0x28E7 lowercases the key, scans menu rows from zero, compares the
first displayed byte, and activates the first match. It does not skip the
disabled target value -2; that value reaches the normal rebuild
continuation. Options, slot selectors, and confirmation panels all inherit
this ordering.
Home (C7) and End (CF) assign the first and last menu indices directly at
0x27BD and 0x27EF. The neighboring Up/Down paths inspect and skip disabled
row marker FE; the edge assignments do not. Enter on such a directly
selected disabled row returns target -2, which rebuilds the appropriate
options level.
Selection dispatch proves the immediate settings behavior: translation is
advanced modulo four and reloads the current text bank, music and effects XOR
their enable bytes with one, and Automatic Combat toggles flag 37. The
translation path at 0x31D0 calls load_text_bank directly. That loader
rewrites descriptor offsets +0, +2, +5, +6, and +8, but deliberately
leaves acquisition state byte +4 unchanged. Collected records therefore
remain collected when their citation and verse strings change translation.
Opcode 6B differs because its handler clears all 66 +4 bytes after the
load.
The effects case at 0x31F5 consists only of
xor byte [0x4A],1 followed by the common menu rebuild. It does not call
release_sound_effect_buffer, digpak_stop_current_sound, or any other audio
routine. An active effect therefore continues after the option changes to Off,
although later opcode 58 starts are suppressed. Opcode 59 independently
tests the same effects word at 0x5125; while it is zero the script takes the
100-unit silent fallback rather than polling the still-running driver.
Continue, a completed save, and a completed load close the interface. New Game and Quit only proceed after their confirmation menu.
The seven victim scenes each set a distinct rescue flag at successful progression points:
| Flag | Scene / victim identifier |
|---|---|
0x3A | JELO |
0x3B | FEAR |
0x3C | CULT |
0x3D | LAW |
0x3E | RICH |
0x3F | DENY |
0x40 | NAGE |
GANTRY.BIN tests those seven flags and mirrors the set members into
0x42..0x48 before the Unibot sequence. The bytecode proves the one-to-one
transition. CP1.BIN counts those later flags as the rescued crew physically
present aboard the Unibot and requires all seven before departure.
One further durable flag is specific to the Unibot road network:
| Flag | Meaning |
|---|---|
0x54 | The one-time Annoy Cyber verse-loss event has occurred. |
The late-game programs also give exact meanings to variables 53 through 65:
| Index | Byte offset | Current meaning |
|---|---|---|
| 53 | 0x6A | Unibot turn/rotation offset. |
| 54 | 0x6C | Current Unibot node. |
| 55 | 0x6E | Heading: north 0, east 1, south 2, west 3. |
| 56–62 | 0x70..0x7C | Pylons 1–7 rescued/destroyed. |
| 63 | 0x7E | Next node selected by forward movement. |
| 64 | 0x80 | Active pylon number; 100 means none. |
| 65 | 0x82 | Tower confrontation state: 0, 1, 2, or failure 9. |
ROBOT.BIN initializes variables 53 through 55. CP2.BIN uses variables
56 through 62 as both pylon-completion state and the seven-part Tower gate.
FACE.BIN and CP3.BIN alternate on variable 65 to implement the final
study prompt and its success/failure branches. See the Unibot and endgame
chapter for the complete graph and state machine.
Two lower flags carry the result of a conversation’s study-Bible prompt:
| Flag | Conversation meaning |
|---|---|
0x14 | The player selected the expected text descriptor. |
0x15 | The player left the browser without that match. |
The browser clears both flags before accepting input. Victim scenes branch on them after requesting the study screen, so they are transient result flags rather than durable progression markers. See the conversation-flow chapter for the complete prompt and suspension sequence.
Faith
Faith is variable 21 and uses a 0–10,000 scale, so one displayed percentage
point is 100 internal units. The status renderer clamps values above 10,000
and below zero before selecting meter artwork. The F3 detail screen divides
by 100 and writes the resulting two integer digits into the mutable
Your faith is at 00%. template. A separate
Your faith is at 100%. string handles 10,000.
The status-control dispatcher at 0x83B6 passes every Faith or power notice
to the ordinary dialogue routine with logical presentation values
(24,28,150). The five power messages are executable strings rather than
generated labels:
Helps you hit harder during battle.Helps protect you during battle.Warns of rooms that are traps.Causes dark halls to be lit.Lets you fly in some places.
Opcode 0x81 passes an immediate loss to reduce_faith at 0x3979:
- Easy divides the loss by two.
- Normal applies it unchanged.
- Difficult multiplies it by four.
- No-combat mode suppresses the subtraction.
This directly supports the manual’s statement that Easy mode loses faith less readily and connects the installation no-combat option to the same damage path.
Faith exhaustion is checked centrally after input processing rather than by
individual scene scripts. handle_faith_depletion at 0x7B12 clamps a
negative value to zero and calls enter_game_over_scene at 0x1B86. That
routine selects the initialized resource strings OVER and seg, sets the
pending-scene state, and starts the accompanying palette effect. The
POWER.BIN resource is a separate in-combat study interface and must not be
confused with this game-over transition.
One encounter also raises faith rather than reducing it: the Zapper victory
subroutine in COMBAT7.BIN alternates direct assignments of 1 and 10,000,
ending at the maximum. This implements the special full-faith reward stated
in the manual.
Text-record progression state
Each loaded text descriptor has a persistent byte at record offset +4.
The save chapter describes its compact checkpoint copy and serialized live
records. The bytecode interpreter addresses these bytes by the descriptor’s
one-byte selector:
| Opcode | Operands | Effect |
|---|---|---|
0x36 | selector | Set the matching record’s state byte and select it. |
0x37 | selector | Clear the matching record’s state byte. |
0x38 | selector, target | Jump if the matching state byte is set. |
0x39 | selector, target | Jump if the matching state byte is clear. |
0x88 | none | Clear all 66 loaded state bytes. |
This is the persistent bridge between dialogue/study records and scene control flow. The exact user-facing meaning varies by record: the same mechanism can represent an obtained verse, completed interaction, or another text-related condition.
Save inspection
Show named, nonzero, or checkpoint-different variables and decode the flag bank with:
tools/inspect_save.py CB/DDGAMES.SV9 --variables
The supplied saves have no active boolean flags. Both copies keep variable
16 at -1. Their live copies vary at general-purpose variable 28, and SV9
also has variable 27 set to 5; static evidence does not justify assigning
gameplay meanings to those temporary slots.
Relevant executable functions
| Load offset | Current name |
|---|---|
0x1191 | initialize_script_state |
0x1B86 | enter_game_over_scene |
0x3979 | reduce_faith |
0x43F5 | test_state_flag |
0x4413 | set_state_flag |
0x4433 | clear_state_flag |
0x5B24 | get_text_record_state |
0x5B76 | set_text_record_state |
0x5BBF | clear_text_record_state |
0x7B12 | handle_faith_depletion |
Offsets use the unpacked load-module convention documented elsewhere in this book.