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 Bytecode

Overview

The 62 expanded BIN members are the game’s scene programs. Most are pure bytecode streams with no header. They select art and palettes, start music, change scenes, update variables, branch, call subroutines, and coordinate animation. The executable’s interpreter dispatches opcodes 0x01 through 0x91 through a 145-entry near-pointer table at load offset 0x59AB.

The operand layout and an evidence-based handler name have now been recovered for every dispatched opcode. Linear decoding identifies 25,829 commands in 64 code regions and uses 122 of the 145 possible opcodes. Names for commands that do not occur in the shipped scripts describe their direct engine effects rather than claiming an unobserved gameplay role.

An independent second pass reads the 145 dispatch pointers directly from the unpacked executable and finds 134 distinct implementation addresses. It walks every handler’s control-flow graph, records all operand-reader calls, and joins the results to the complete shipped corpus. The checked result is analysis/opcode-audit.tsv; tools/audit_bin_opcodes.py regenerates and verifies it.

Runtime model

initialize_scene at 0x6631 appends .BIN to a scene base name, loads that archive member into far memory, installs its base and initial cursor, resets scene-thread state, and calls execute_bin_commands at 0x451B with file offset zero. Later, update_scene_threads at 0x7997 resumes active command streams by passing their saved file offsets back to the same interpreter.

Four words in the data segment hold the current input state:

DS offsetRuntime exampleInterpretation
0x00F60x004BCurrent file-relative bytecode offset.
0x00F80x4C13Segment containing the current bytecode cursor.
0x00FA0x0000Offset of the loaded resource’s far-memory base.
0x00FC0x4C13Segment containing the resource base.

The interpreter sets its cursor to base + requested_offset, fetches one byte, converts opcode 1 to dispatch index 0, range-checks through 0x91, and calls the corresponding handler. Branch and call targets are absolute offsets within the expanded BIN member, rather than relative displacements.

Three shared operand readers make the encoding unambiguous:

Load offsetCurrent nameOperation
0x3A1Ebin_read_u8Read one byte and advance the far cursor.
0x3A30bin_read_u16Read one little-endian word.
0x3A64bin_read_cstring_offsetReturn a base-relative string offset: consume an inline NUL-terminated string, or consume FF plus an explicit 16-bit offset.

Four other handlers implement their own byte-at-a-time inline string loops: opcodes 0x01, 0x0D, 0x4D, and 0x6D. Those loops do not recognize the 0xFF offset escape. Keeping the two encodings separate prevents an art, palette, or scene name beginning with CP437 byte 0xFF from being misinterpreted as a pointer.

Operand schema

The decoder records each opcode with a compact schema:

MarkerEncoding
BUnsigned byte.
HUnsigned little-endian 16-bit word.
zInline NUL-terminated CP437 string.
pString pointer encoded inline as a NUL-terminated string, or as FF plus an explicit 16-bit resource-relative offset.
9Opaque nine-byte animation record skipped by opcode 0x07.
sAn additional word only when the preceding H, interpreted as signed, is negative.

The conditional BHs form is used by opcodes 0x11, 0x12, and 0x17 through 0x1A. For example, bytes 11 01 F8 FF E6 01 contain byte 1, signed word -8, and therefore the extra word 0x01E6.

The complete machine-readable table is OPCODE_SCHEMAS in tools/inspect_bin.py. It contains one entry for every value from 0x01 through 0x91; tests extract the resources directly from CB/DD1.DAT and exercise every known code region against that table.

Top-level selector input

The main input dispatcher at 0x875D separates confirmation input from exploration actions with global action-selection word DS:004C. When it is zero, Enter (0x0D) or the primary-button event (0x7E) sets the opcode-64 latch at DS:7CC0. When it is nonzero, those events instead query render slot 0x124, require the current selector index at DS:00EE to be nonnegative, and call the selector activator at 0x851E. A press with no current selector does nothing; it does not leak into the confirmation latch.

ASCII Space reaches the cycling routine at 0x6939 only while action selection is enabled. That routine refreshes the current selector with update_action_selector_overlay, then advances through two tables. Kind zero visits the 16-byte scene-thread records at DS:7DC0; kind one visits the 10-byte opcode-3A records at DS:4810. Exhausting either table wraps into the other. An actor needs its byte at +7 set, while an action needs its byte at +8 set. Both require signed X less than 320 and signed Y less than 200. The successful record writes X and Y+4 to the logical pointer words and calls the overlay updater again.

The four extended arrow events have the same action-selection split. 0xC8, 0xD0, 0xCB, and 0xCD search for selector keys u, d, l, and r while DS:004C is nonzero. While it is zero, the branches at 0x889C, 0x88BD, 0x88F7, and 0x88D7 instead subtract or add eight to the corresponding logical pointer word.

Opcode 02 initializes the scene-thread selector X/Y fields to 30000 and its string pointer to -1; its navigation X/Y live elsewhere in the same record. Opcode 10 installs the on-screen selector anchor and string. Consequently an unlabeled navigation record such as GANTRY’s opening remains clickable through the navigation hit-test, but its untouched off-screen selector fields keep it out of proximity hover and Space cycling.

The standalone Bible routine at 0x1C88 is a blocking top-level interface. It saves action-selection state at 0x1D5F, clears DS:004C at 0x1D66, and services its own input loop before returning. The map, options, save, and status-notice dispatchers follow the same nested-interface model rather than returning to the scene controller on every host frame.

Independent audit

The second pass does not use successful linear decoding as proof of operand width. tools/audit_bin_opcodes.py instead combines five independent views:

  1. the exact 145-word dispatch table at load offset 0x59AB;
  2. every reachable path through the interpreter’s 1,817 Rizin instructions;
  3. calls to the byte, word, and pointer-string readers, plus the four inline string loops and opcode 0x07’s direct nine-byte cursor advance;
  4. the handler aliases and per-symbol evidence in analysis/cb.rz and analysis/symbol-map.tsv; and
  5. all 25,829 decoded commands in all 64 known code regions.

The audit expands conditional schema BHs into both legal paths, BH and BHH. It also records the dialogue handler’s intentional no-read retry path: while a modal message is active, the command suspends without consuming its p operand; after the modal state clears, it consumes that same pointer and continues. No other handler is allowed an undeclared operand path.

Run the complete comparison with:

tools/audit_bin_opcodes.py

The report records, for every opcode value, the dispatch address, handler symbol and confidence, declared schema, independently observed read paths, script-variable operand positions, use count, resource set, and first shipped site. Twenty-three opcode values are absent from shipped code: 03, 0E, 13, 18, 1B, 2E, 30, 37, 39, 47, 4A, 4B, 4F, 50, 56, 5E, 65, 66, 6E, 6F, 8B, 8F, and 90.

Identified commands

These handler meanings have direct static support from their callees or clear control-flow behavior:

OpcodeOperandsCurrent nameEvidence
0x01zload_artPasses the name to load_art_resource, which appends .ART.
0x05nonereturn_minus_oneTerminates interpretation with return value -1.
0x02BHHHcreate_scene_threadInitializes a thread slot and appends a type-0x02 display record.
0x03BBHHBadd_native_scale_display_objectUnused. Appends a directly rendered object with implicit scale 0x0100.
0x04 / 0x43BBHHHBadd_scaled_display_objectAppends a directly rendered object with frame, ART slot, X, Y, scale, and flags.
0x06Hbegin_animation_sequenceCreates animation state and appends a type-0x06 display record.
0x079animation_stepAdvances over a fixed nine-byte step retained for later animation updates; it does not require a preceding sequence.
0x08BBstart_animationReads animation, mode, then starts the selected slot.
0x5FBBBstart_linked_animationReads animation, linked animation, mode, then starts the selected slot.
0x09Bstop_animationStops an animation and releases its render slot.
0x0Dzzchange_sceneSelects a new scene and secondary segment name.
0x0FHadjust_thread_delaySubtracts the operand from the current scheduler slot’s delay word at offset +0x0C.
0x13Hremove_dialogue_choiceUnused. Removes the first six-byte choice record with the matching target.
0x14pshow_adversary_dialogueUses the adversary presentation channel; all ten uses are in FACE.BIN.
0x1EHHcopy_variableCopies one signed script-variable word to another.
0x1FHHset_variableStores an immediate in a script variable.
0x20HHjump_if_zeroSelects an absolute target when a variable is zero.
0x21HHjump_if_nonzeroSelects an absolute target when a variable is nonzero.
0x22HHHjump_if_variables_equalCompares two variables and jumps when they are equal.
0x23HHHjump_if_variable_equalsCompares a variable with an immediate and jumps when they are equal.
0x24HHHjump_if_variables_not_equalCompares two variables and jumps when they differ.
0x25HHHjump_if_variable_not_equalCompares a variable with an immediate and jumps when they differ.
0x26HHHjump_if_variable_greater_than_variableSigned-compares two variables and jumps when the first is greater.
0x27HHHjump_if_variable_greater_thanSigned-compares a variable with an immediate and jumps when the variable is greater.
0x28HHHjump_if_variable_less_than_variableSigned-compares two variables and jumps when the first is less.
0x29HHHjump_if_variable_less_thanSigned-compares a variable with an immediate and jumps when the variable is less.
0x2AHHadd_variableAdds one script variable to another.
0x2BHHadd_to_variableAdds an immediate to a script variable.
0x2CHHsubtract_variableSubtracts one script variable from another.
0x2DHHsubtract_from_variableSubtracts an immediate from a script variable.
0x2EHHmultiply_variablesUnused. Signed-multiplies a script variable by another variable.
0x2FHHmultiply_variableSigned-multiplies a script variable by an immediate.
0x30HHdivide_variablesUnused. Signed-divides a script variable by another variable.
0x31HHdivide_variableSigned-divides a script variable by an immediate.
0x32Hincrement_variableIncrements a numbered variable.
0x33Hdecrement_variableDecrements a numbered variable.
0x34HcallSaves a return offset and jumps to an absolute target.
0x35nonereturnResumes the saved bytecode return offset.
0x36Bset_text_record_stateSets persistent descriptor byte +4 selected by record identifier.
0x37Bclear_text_record_stateUnused. Clears persistent descriptor byte +4 selected by record identifier.
0x38BHjump_if_text_record_setSelects a target when a record state is set.
0x39BHjump_if_text_record_clearUnused. Selects a target when a record state is clear.
0x3AHHHpadd_action_targetReads target, x, y, label and appends one selectable screen action.
0x3BBenable_action_targetSets one action record’s active byte.
0x3CBdisable_action_targetClears one action record’s active byte.
0x3DHjumpReplaces the cursor with an absolute file offset.
0x3EBHstart_scene_thread_atFreshly activates a BIN scheduler slot at an absolute target, clears stale scheduling state, and runs it before the caller continues.
0x3FBwait_for_animationContinues for animation state 0, 5, or 6; otherwise suspends and retries the command.
0x41noneenable_action_selectionEnables screen-action input for the current scene.
0x42nonedisable_action_selectionDisables screen-action input for the current scene.
0x44Hpadd_dialogue_choiceAppends an absolute target and far text pointer to the choice table.
0x45noneclear_dialogue_choicesClears the choice count and dialogue state.
0x46nonepresent_dialogue_choicesSuspends the thread until the selected choice supplies a new BIN target.
0x48pshow_character_dialoguePresents the character/boss/victim dialogue channel.
0x49nonerequest_study_bibleRequests the modal study interface and suspends the current thread.
0x4Dzload_paletteCalls load_palette_resource, which appends .PAL.
0x4Epshow_captain_bible_dialoguePresents Captain Bible’s dialogue channel, also reused for some captions.
0x52Bplay_musicBuilds MUS### or IBM### and loads an XMI member.
0x55nonesnapshot_stateCopies the live state into a retained buffer.
0x57BHprepare_sound_effectStops/releases the preceding sample, builds and decodes D###.ABT, and preformats it at the supplied rate without starting playback. Identifier/rate zero only stops and releases.
0x58nonestart_prepared_sound_effectStops any active instance and starts or restarts the retained preformatted sample.
0x59nonewait_for_sound_effectWith usable digital playback, yields and retries while playback is active. Without it, subtracts 100 from the calling thread’s delay and advances past the command.
0x60nonenopContinues directly to the next command.
0x61Bstop_scene_threadClears one BIN scheduler slot’s active byte.
0x65BBclear_display_object_framesUnused. Reads first, count and clears frame byte +7 for records [first, first + count).
0x66BBBBadvance_display_object_framesUnused. Reads first, count, minimum, maximum; increments selected frames and resets a result below minimum or above maximum to minimum.
0x6CHHHHrotate_palette_rangeAdvances a script-variable phase by a signed step, wraps it within an inclusive palette-index range, and rotates that range’s mapping.
0x6Dzload_paletteUses the same palette-loading path as 0x4D.
0x70noneunload_last_artReleases the most recently loaded art slot.
0x72noneyield_scene_threadStores -1 in the current scheduler slot’s delay word and returns the following BIN offset; the next controller update can resume there.
0x73BHjump_if_state_flag_clearSelects a target when a boolean state flag is clear.
0x74BHjump_if_state_flag_setSelects a target when a boolean state flag is set.
0x75Bclear_state_flagClears one identifier in the 128-bit state bank.
0x76Bset_state_flagSets one identifier in the 128-bit state bank.
0x77noneprocess_current_map_cellCalls the current-cell handler, which consults the cell and its neighbors.
0x78Bload_mapCombines a level letter with the current E/N/D difficulty code and loads a .MAP member.
0x7AHHpatch_bin_byte_from_variableWrites the low byte of a script variable to an absolute offset in the current BIN resource.
0x7BHset_current_map_cell_kindPreserves the cell’s high nibble and ORs it with the variable’s low byte; shipped callers supply low-nibble kind values.
0x7CHset_current_map_cell_parameter_aWrites the current cell’s second byte from a script variable.
0x7DBHconfigure_study_promptSelects a companion-text component and record selector for the next study screen.
0x7Enoneblackout_paletteStarts an immediate black palette effect before a scene transition.
0x7FHset_current_map_cell_parameter_bWrites the current cell’s third byte from a script variable.
0x80BHjump_if_animation_activeSelects a target when an animation state byte is nonzero.
0x81Hreduce_faithSubtracts a difficulty-scaled immediate from faith unless no-combat mode is active.
0x82HHset_variable_random_moduloAdvances the shared DOS runtime generator and stores its 15-bit result modulo the immediate in a script variable.
0x85Bhide_display_objectSets the high hidden bit in a display record’s ART-slot byte.
0x86Bshow_display_objectClears the high hidden bit in a display record’s ART-slot byte.
0x87nonenormalize_map_cellsApplies recovered location-kind and parameter transitions across the grid.
0x88noneclear_text_record_statesClears persistent byte +4 in all 66 text descriptors.
0x89nonemark_current_map_cell_exploredSets the current X bit in the current Y exploration row.
0x8Enonesync_current_cell_flags_23_to_27The DOS handler uses a coordinate-derived index into an 80-byte object and copies bits 0 through 4 into state flags 0x23 through 0x27; the clean-room behavior draws a random byte instead.
0x8FHHand_variablesUnused. ANDs a destination with another variable.
0x90HHand_variableUnused. ANDs a destination with an immediate.

Formerly structural commands

The final unnamed-handler pass connected the remaining 51 opcode values to their consumers. Thirteen of these values never occur in the shipped command regions; their rows are marked unused and rely on static handler behavior. Ten already-named values are also absent, producing the audit’s overall total of 23 unused opcodes.

OpcodeOperandsRecovered nameOperation
0x0Anonewait_for_scene_thread_movementYields the current BIN thread until primary movement state is 0 or 2.
0x0BBBadd_navigation_edgeAppends an undirected two-node edge in the insertion order used by the bounded iterative-deepening route finder.
0x0CBBpadd_scene_entryAssociates an entry/segment string with the initial/current node byte followed by the destination node byte used after change_scene.
0x0E, 0x4A, 0x4B, 0x56nonenopUnused. All four jump-table entries point at the interpreter’s continue loop.
0x10BHHpconfigure_scene_thread_actionGives a scene thread selector X/Y coordinates and an action-label pointer; newly created scene-thread selectors start enabled.
0x11BHsadd_navigation_arrival_handlerMaps a destination node to a BIN target and optional explicit thread slot.
0x12BHsadd_navigation_departure_handlerMaps a source node to a BIN target and optional explicit thread slot.
0x15Bselect_study_recordSelects the text descriptor expanded by study placeholders and clears both success continuations.
0x16HHHset_palette_adjustment_range_from_variableFills an inclusive signed palette-brightness adjustment range with one script-variable value and schedules an update.
0x17BHsadd_forward_edge_departure_handlerAdds a callback for starting traversal in an edge’s stored first-to-second order.
0x18BHsadd_reverse_edge_departure_handlerUnused. Adds a callback for starting traversal from the stored second node to the first.
0x19BHsadd_reverse_edge_arrival_handlerAdds a callback for completing traversal from the stored second node to the first.
0x1ABHsadd_forward_edge_arrival_handlerAdds a callback for completing traversal in stored first-to-second order.
0x1BHprime_primary_scene_thread_timerUnused. Stores the negated operand in the primary motion timer and sets a shared transition latch; motion updates consume the latch by selecting state 4 or 6 and clearing it.
0x1CBenable_scene_thread_actionEnables one scene thread as an input selector.
0x1DBdisable_scene_thread_actionDisables one scene thread as an input selector.
0x40Bset_scene_thread_motion_stateWrites the current thread’s motion state. State 0 makes the scene-motion updater release that thread’s normal actor render slot; state 2 immediately runs the updater.
0x47Bset_modal_menu_selectionUnused. Seeds the selection consumed and reset by the modal text-menu path.
0x4CBfill_screenFills the complete 320-by-200 framebuffer with one palette index.
0x4FBBconfigure_study_navigation_successUnused. Selects a study record and the navigation node entered after success.
0x50noneclear_study_record_selectionUnused. Clears both active study-record selector words.
0x51BHBconfigure_study_thread_successSelects a study record plus the BIN target and thread slot started after success.
0x53Bset_scene_thread_originInitializes the primary navigation object’s nodes and X/Y/scale without replacing its retained destination, then starts a minimum same-node controller traversal.
0x54Bmove_scene_thread_toRetains a desired navigation node; starts movement while idle or replaces only the destination while an edge remains active.
0x5AHjump_if_digital_audio_fallbackJumps when the digital-driver word is zero, effects are disabled, driver-state bit 0 is clear, or the fallback word is nonzero. Falls through only for a usable driver with a zero fallback word.
0x5BBset_scene_thread_directionSelects one of four movement orientations and its sprite/render offset.
0x5CBBBconfigure_captain_bible_dialogueWrites Captain Bible text X, text Y, and wrap width.
0x5DBBBconfigure_character_dialogueWrites character text X, text Y, and wrap width.
0x5EHset_deferred_scene_thread_targetUnused. Sets a target that the main loop later starts in scheduler slot 2.
0x62Hstore_mouse_xStores the current mouse X coordinate in the selected script variable.
0x63Hstore_mouse_yStores the current mouse Y coordinate in the selected script variable.
0x64Hjump_if_confirm_pressedConsumes the Enter-or-click latch and jumps to an absolute target when set.
0x67nonerequest_restore_saved_gameLeaves the scene loop through mode 2, which restores retained save buffers.
0x68Hadjust_variable_1280_onceApplies one correction: subtract 1,280 above 640, then add 1,280 if the result is below -639. It does not loop for values more than one interval away.
0x69HHload_bin_wordLoads a little-endian word at an immediate current-BIN offset into a variable.
0x6AHHpatch_bin_word_from_variableWrites a variable word to an immediate current-BIN offset.
0x6BBload_text_bankReplaces the active companion-text bank and clears all 66 descriptors.
0x6EBstart_primary_scene_thread_overlayUnused. Loads and starts a resource-driven transient overlay for scene thread zero.
0x6Fnonewait_for_primary_scene_thread_overlayUnused. Yields while that transient overlay remains active.
0x71HHload_bin_word_indirectUses one variable as the BIN offset, loads a word, and stores it in another variable.
0x79noneclear_navigation_handlersClears only scene-entry, edge-transition, node-arrival, and node-departure callback counts. It neither clears the navigation-edge table nor cancels active movement.
0x83HBHcopy_text_record_component_to_binSelects a text record through a variable and copies one component to an immediate BIN offset.
0x84HHload_bin_byteSign-extends a byte at an immediate current-BIN offset into a variable.
0x8ABHjump_if_animation_finishedJumps when the selected animation is in state 0, 5, or 6.
0x8Bnoneconsume_random_text_recordUnused. When variable zero is 2, chooses rand() % count, scans cyclically past state-zero descriptors, clears the first set state byte, and stores 3,000 at variable offset 5C.
0x8CHjump_if_no_combatJumps when the installation’s SOUND.5 no-combat flag is set.
0x8DHjump_if_file_missingAppends the mutable save suffix to the active player prefix, opens the result as rb, and jumps on failure. Its sole shipped site tests .SV0, with target equal to fallthrough.
0x91HHset_variable_current_cell_byte_moduloThe DOS handler reduces its coordinate-indexed object byte modulo the immediate; the clean-room behavior reduces a random byte instead.

The movement opcodes share the controller recovered at 0x6F46, 0x7469, and 0x779D. Opcode 0x53 uses the same edge initializer with identical start and end nodes; opcode 0x0A therefore continues waiting until that minimum traversal completes. Opcode 0x54 first compares its operand with the retained desired node. During active interpolation a different operand only replaces that byte. The current edge, timer, pose, phase, and callbacks remain intact, and route search runs again from the reached node after its arrival callbacks. The retained byte starts at 0xFF, persists across scene changes, and is not reset by opcode 0x53.

Navigation callbacks are fresh synchronous command-stream invocations: each one runs through its first yield or return before the dispatcher examines the next matching record. A same-slot callback’s delay does not pause the outer command interpreter that synchronously invoked it; if that outer invocation later yields, its cursor replaces the nested cursor while both delay changes remain accumulated. Initial movement searches, runs the source-node and directional-edge departure callbacks in that order, and then initializes the edge. State-five continuation searches, initializes the edge, and runs the directional-edge then source-node departure callbacks. Completion makes the controller inactive for request purposes before running the directional-edge then destination-node arrival callbacks.

Opcode 0x69 was previously recorded as a one-word instruction. Its handler actually reads an immediate BIN offset and a destination-variable offset. In CP2.BIN, the second word’s low byte happened to be opcode 0x40, so all streams still decoded while eleven destination operands appeared as phantom commands. Correcting the schema reduces the corpus total from 25,840 to 25,829 without changing the set of 122 opcodes genuinely used by shipped scripts.

DOS coordinate-indexed object overrun

The operand source used by opcodes 0x8E and 0x91 is not a MAP field and is not a separately loaded 16×16 resource. Both handlers calculate 16 * map_y + map_x and read from DS:008A. The initialized object at that address contains only 80 bytes, covering rows zero through four:

92 DF 1A 72 01 83 9C E5 00 B8 A8 01 2C F7 E2 AA
64 F3 43 CA 7D 7C 6A 51 37 A3 33 7A 70 1F 8B 4F
98 EF 83 6F 65 41 A3 0F C8 93 80 3F 1F 18 90 6E
AC 5D 3E 31 46 2C 3A 40 4E B1 13 C1 E5 DB 07 CA
5C 05 1A 21 EC D6 65 46 A8 65 B1 B3 AF A1 44 62

The handlers perform no bounds check. Rows five through fifteen therefore alias the initialized data and live globals which immediately follow the 80-byte object. Those aliases explain why a literal 256-byte zero replacement removed combat variation and made every Communications room choose the same initial face during reverse engineering. They are DOS implementation details, not clean-room compatibility requirements.

The live aliases identified while explaining the DOS captures are:

Grid coordinate(s)Original bytesLogical value
(6,5), (7,5)DS:E0 wordNumber of scene-loaded ART slots, excluding permanently loaded RUN.ART.
(8,5), (9,5)DS:E2 wordDisplay-record count.
(4,6), (5,6)DS:EE wordCurrent pointer-selector index, or -1.
(6,6), (7,6)DS:F0 wordPreviously rendered pointer-selector index, or -1.
(8,6), (9,6)DS:F2 wordCurrent selector kind: scene thread 0, action target 1.
(10,6), (11,6)DS:F4 wordPreviously rendered selector kind, initially -1.
(12,6), (13,6)DS:F6 wordCurrent BIN cursor immediately after the opcode byte.
(14,6), (15,6)DS:F8 wordCurrent BIN cursor segment.
(0,7), (1,7)DS:FA wordNormalized current-BIN base offset, normally zero.
(2,7), (3,7)DS:FC wordCurrent-BIN base segment.

The two segment words are allocator-derived. They were 42C5 in the focused fresh-load probes and 4C13 in a longer prior session, even though the resource-relative cursor behavior was identical. This is the only part of the lookup whose exact value can vary with the DOS heap history.

The clean-room engine treats this entire lookup as a DOS implementation detail. Every opcode-8E or opcode-91 execution advances the one portable runtime generator and uses the returned value’s low byte, regardless of map coordinate. Opcode 0x82, the text shuffle, unused opcode 0x8B, and the transient-status refresh started by 0x8B share that stream. The algorithm and call order are now explicit so, from the same initial state and inputs, a patched DOS run, Rust, and future implementations can replay the same branches without depending on resource addresses, selector state, or unrelated scene globals.

Instruction-boundary QEMU probes validate both consumers. COMBAT1.BIN reached 0627:5913 at map coordinate (0,4) with BX=0040, ART/display counts 6/35, and cursor 07F3, exactly one byte after its opcode-8E at 07F2. A patched ROOM3 save reached 0627:597F at coordinate (13,6) with BX=006D, ART/display counts 3/16, and cursor 169C, exactly one byte after opcode 91 at 169B.

The latter probe proves the problematic alias directly. DS:008A + 006D is DS:00F7, the high byte of the current BIN instruction pointer stored at DS:00F6; opcode 91 read 16h from the pointer value 169Ch. The deterministic DOS patch removes the whole coordinate calculation at 0x5905 and 0x5971 and routes each handler through the portable generator exactly once.

The pointer-capable p encoding is used by seven opcodes: 0x0C, 0x10, 0x14, 0x3A, 0x44, 0x48, and 0x4E. Only two shipped commands use its explicit-offset form: ROOM3.BIN at 0x181C (0x48) and 0x18CE (0x4E) both point back to string offset 0x0336. All other shipped p operands are inline strings.

Opcode 0x8D copies the same active player prefix and mutable suffix used by the save routines, then requests fopen(..., "rb"). At its only site, TITLE.BIN:0x012C, the initialized suffix is .SV0, so the constructed name is DDGAMES.SV0 with the default prefix. A failed open selects target 0x012F, which is also the next command; either result therefore reaches the same change_scene 'INTRO', 'seg'. The handler passes the returned stream to fclose after either path, including the null result on failure.

Opcode 0x7A deliberately modifies the loaded BIN buffer. Combat exits use it to replace the C in inline CHAL from the current level-letter variable; POWER.BIN replaces the digit in combat1 from the selected combat number. It is therefore a resource-name templating mechanism rather than a persistent-state write.

Opcode 0x6C calls rotate_palette_range at 0xB5A8. Its operands are inclusive minimum, inclusive maximum, signed step, and phase variable. The helper wraps the updated phase, fills the palette-index mapping across the range, and schedules a palette update. Opcode 0x7E calls start_palette_blackout at 0x1B6C; the next palette update writes an all black palette and counts down the effect state.

The suffix strings are present in the executable data segment and were also checked in the QEMU process image: .PAL at DS:0434, .ART at DS:0490, and .BIN at DS:0721. This corrects an early interpretation of bytes such as 4D 54 49 54 4C 45 00 as the string MTITLE: byte 0x4D is actually the palette opcode followed by the string TITLE.

Startup programs

The QEMU DOS trace and archive directory give this resource-load sequence:

LOGO.BIN -> LOGO.PAL -> LOGO.ART -> D003.ABT
TITLE.BIN -> TITLE.PAL -> TITLE.ART -> TITLE2.ART -> MUS001.XMI
INTRO.BIN -> INTRO.ART

The scripts involved decode completely:

ResourceExpanded bytesCommands
LOGO.BIN640114
TITLE.BIN43680
INTRO.BIN18439
MENU.BIN2,00499

INTRO.BIN begins by setting two small state values, loading TITLE.PAL, loading INTRO.ART, and drawing the opening sequence. Opcode 0x52 selects music index 1, which produces MUS001.XMI. At file offset 0x009A, opcode 0x0D carries strings dome and seg to enter the first gameplay scene.

The runtime dump contains all 184 INTRO.BIN bytes at physical address 0x4C130. The base pointer 4C13:0000 appears at physical address 0x14F0A, stored as offset word 0000 at DS:00FA and segment word 4C13 at DS:00FC. The live cursor is 4C13:004B; resource offset 0x004B begins opcode 0x42 and is exactly the boundary after the preceding return_minus_one command. This ties the static decoder’s command boundaries to live interpreter state.

Mixed code and data

Sixty members decode linearly from byte zero through end of file. Two members contain non-code regions:

  • CP2.BIN has commands at 0x0000..0x1D54, followed by a 256-byte structured data trailer at 0x1D55..0x1E54. It contains a 16-node four-heading adjacency table, node types, transition values, and lower-right map coordinates. The Unibot chapter decodes every entry.
  • ROOM3.BIN has command regions 0x0000..0x0336, 0x0C96..0x1754, and 0x1768..0x19DB. A 2,400-byte zero-filled block and a 20-byte zero-filled block separate them.

Opcode zero is invalid. The decoder deliberately reports it instead of guessing that arbitrary padding or embedded tables are executable commands. The later ROOM3.BIN entry points are therefore decoded explicitly rather than reached by a single linear sweep.

Inspection tool

After extracting the archive, inspect a complete stream with:

tools/inspect_bin.py build/dd1/all/005_INTRO.BIN

Use explicit bounds for embedded regions:

tools/inspect_bin.py \
  build/dd1/all/334_ROOM3.BIN --start 0x0c96 --limit 0x1754
tools/inspect_bin.py \
  build/dd1/all/334_ROOM3.BIN --start 0x1768
tools/inspect_unibot.py build/dd1/all/315_CP2.BIN

Output includes the file-offset range, opcode, current semantic name, and typed operands. Variable operands show both their word index and encoded byte offset, with recovered names such as var[21:faith]@0x002a. Other words with their high bit set are displayed in both unsigned hexadecimal and signed decimal forms. Every dispatched opcode now has a name; low-level names for unused commands deliberately describe state changes rather than inferred UI.

Add --objects to append a linear summary of commands which define display records, --choices to inventory dialogue target/text pairs, or --animations --actions to inventory animation sequences and selectable screen targets. Action summaries name the four combat selectors and the hall selectors for movement, Confront Cyber, Unlock, and Get Verse. The scene-display-object chapter documents the ten-byte runtime record, while the conversation-flow chapter documents the six-byte choice table. The combat-runtime chapter documents animation slots, action targets, and the BIN scheduler; the world-map chapter correlates the hall actions with map-cell features. These summaries warn that branches can change which definitions execute.

Executable routines

Load offsetCurrent name
0x034Fload_map_resource
0x0457normalize_map_cells
0x075Fshow_map_screen
0x0C6Cprocess_current_map_cell
0x1B6Cstart_palette_blackout
0x1C88show_study_bible
0x2556select_from_text_menu
0x2933show_dialogue_message
0x3AD2reset_scene_display_records
0x3AFFrender_scene_display_records
0x3B9Bresolve_animation_transform
0x3D08render_animation_slot
0x3DA8update_animation_slots
0x3F59start_animation_slot
0x3FDFstop_animation_slot
0x3A1Ebin_read_u8
0x3A30bin_read_u16
0x3A64bin_read_cstring_offset
0x4001load_palette_resource
0x4091play_music_resource
0x446Frender_study_prompt
0x451Bexecute_bin_commands
0x6631initialize_scene
0x6939cycle_action_selector
0x6A23update_action_selector_overlay
0x7997update_scene_threads
0x7A5Cstart_scene_thread
0x834Ehandle_study_bible_request
0x8558find_action_target_by_key
0xB5A8rotate_palette_range
0xB948release_render_slot
0xBCACrender_scene_display_object

Offsets use the unpacked load-module convention documented elsewhere in this book.