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

Logic bytecode interpreter notes

This page records clean-room observations about the interpreter for logic payload bytecode. It is based on local disassembly of build/cleanroom/AGI.decrypted.exe, the runtime tables in SQ2/AGIDATA.OVL, and decoded SQ2 logic payloads from an explicitly selected local game directory.

For a compact generated coverage index that lists every action opcode, every known condition opcode, operand shapes, and current evidence level, see Logic Opcode Evidence Matrix.

Runtime tables

After startup, DS points at AGIDATA.OVL. Both bytecode dispatchers use tables in that overlay rather than in the executable image.

The action dispatcher at image offset 0x02c4 calls through:

handler = u16le(DS:0x061d + opcode * 4)

Each action-table entry is four bytes:

u16 handler_image_offset
u8  fixed_operand_count
u8  variable_operand_bitmask

The condition dispatcher at image offset 0x07e3 calls through:

handler = u16le(DS:0x08fd + opcode * 4)

Each condition-table entry has the same four-byte shape. The condition scanner uses the third byte as the fixed operand count when it needs to skip an unexecuted condition.

The fourth byte is an operand metadata bitmask. From decoded handlers, bit 7 corresponds to operand 0, bit 6 to operand 1, and so on. A set bit means that the corresponding operand is a variable slot/reference rather than an immediate literal for table-aware decoding. The dispatcher does not apply this bitmask generically; each handler still consumes operands directly and decides whether a variable slot is read from or written to.

Examples:

EntryMetadataMeaning confirmed by handler
action 0x03 (assignn)0x80Operand 0 is a variable slot, operand 1 is an immediate literal.
action 0x04 (assignv)0xc0Operands 0 and 1 are variable slots.
action 0x26 (set_object_pos_var)0x60Operand 0 is an object index; operands 1 and 2 are variable slots.
action 0x52 (move_object_to_var)0x70Operand 0 is an object index; operands 1, 2, and 3 are variable slots; operand 4 is immediate.
action 0x7b (setup_transient_object_var)0xfeSeven operands are variable slots.
condition 0x0a (obj_table_room_eq_var)0x40Operand 0 is a table/object index; operand 1 is a variable slot.

AGIDATA.OVL contains valid-looking condition entries for opcodes 0x00 through 0x12. Bytes after that are strings and zero-filled data, even though the dispatcher only rejects condition opcodes >= 0x26. In the current local SQ2 scan, condition opcodes above 0x12 were not needed for successfully parsing ordinary condition lists.

Main interpreter loop

The main logic interpreter is at image offset 0x293c. It starts from the current instruction pointer stored in a logic cache record:

si = logic_record[0x06]

loop:
    opcode = *si++

    if opcode == 0x00:
        return si

    if opcode == 0xfe:
        delta = s16le(*si); si += 2
        si += delta
        continue

    if opcode == 0xff:
        evaluate_condition_then_maybe_execute_block()
        continue

    execute_action(opcode)
    if action returns si == 0:
        return 0
    continue

The 0xfe branch is visible at image offsets 0x2953..0x295a: it reads a little-endian word with lodsw and adds it to SI.

Action execution is delegated to 0x02c4. That dispatcher rejects opcodes 0x00 and 0xfc..0xff, reports an error for opcodes above 0xaf, then calls the action table at DS:0x061d.

Putting the observed dispatch ranges together:

Main-stream byte/rangeObserved role
0x00Logic-path terminator handled before the action dispatcher.
0x01..0xafNormal action opcodes. The action table contains one entry for each byte in this range.
0xb0..0xfbInvalid as action opcodes in this build. They reach the action dispatcher and take its “opcode above 0xaf” error path.
0xfcInvalid outside condition parsing. It is rejected by the action dispatcher as a structural/control byte.
0xfdInvalid outside condition parsing. It is rejected by the action dispatcher as a structural/control byte.
0xfeRelative jump handled by the main interpreter loop.
0xffConditional block marker handled by the main interpreter loop.

Cross-version note: the local Gold Rush / AGI v3 interpreter accepts action opcodes through 0xb5. Those slots are documented below as GR-specific extensions; the invalid-range statement above remains the SQ2 / AGI 2.936 behavior.

Shared opcode version note: the local Gold Rush / AGI 3.002.149 interpreter keeps the same parser contracts as SQ2 for shared action opcodes 0x00..0xaf, but source comparison found the following behavior-level deltas. These are version observations, not replacements for the SQ2 rows in the main catalog.

Opcode groupGold Rush / AGI v3 source-backed difference
0x6f, 0x73, 0x76, 0x77, 0x78, 0x89, 0x8aThe normal input-line and blocking prompt path is relocated, but the SQ2 display-mode-2/input-width branches are gone in GR. GR input-line setup computes the display offset as arg0 << 3 unconditionally.
0xa3, 0xa4, 0xa9GR maps the SQ2 input-width set/clear actions to the generic no-op handler. Its close-window action restores and clears active saved-window state, but does not clear a width flag.
0x79, 0xad, 0xb1, 0xb5GR expands the script key-map table to 0x31 slots; QEMU validates slot 48 by filling the first 48 slots with dummy mappings, mapping typed x in the final slot, and comparing the resulting nonblank picture capture with a direct draw while a no-key control remains blank. GR also changes the key-release gate from SQ2’s incremented [0x1530] byte to set/clear byte [0x0405], and adds a menu interaction gate word [0x0403]. QEMU menu_gate_suite validates that 0xb1(0) blocks a later menu request while 0xb1(1) lets the modal menu path run.
0x12GR calls a small helper before room switch: immediate target bytes 0x7e..0x80 become 0x49; other bytes pass through unchanged. Local GR scripts contain 0x12 operands 0x7e, 0x7f, and 0x80, so this is live compatibility behavior for this interpreter/game pair.
0x7c, 0x7d, 0x80, 0x84GR adds a temporary carried-item selector word, XORs the object/inventory chunk before and after save-file writes using repeating Avis Durgan bytes at data address DS:0x072c, records prompt-marker visibility before restart confirmation and redraws on accepted restart or on canceled restart only when the marker had been visible, and preserves object 0 motion mode 4 in 0x84. The save transform is modeled in tools/agi_save.py as gr_v3_object_inventory_save_xor() and QEMU-validated by blank-prefix save extraction build/gr-v3-behavior/save_xor_extract_qemu_001.json plus signed 0x8f("GR") extraction build/gr-v3-behavior/save_xor_extract_signed_qemu_001.json; the restart redraw branch is modeled in tools/agi_restart.py.

The motion-mode 4 observation is intentionally narrower than an ordinary script-level opcode claim. Static comparison shows GR’s mode dispatcher accepts object byte +0x22 == 4 and routes it to the same target-direction helper used for mode 3. A copied-interpreter QEMU probe then patches only the generated fixture’s action-0x51 setup byte from mode 3 to mode 4; that instrumented case renders identically to the unmodified mode-3 fixture while a stationary control remains different. No ordinary bytecode setter for mode 4 has been observed yet.

Top-Level Cycle Timing

The top-level engine cycle observed at code.engine.main_cycle (0x0150) wraps logic execution with object and input/update passes. The current source model is:

  1. Run several input, sound, and display-maintenance helpers. One of these is code.engine.wait_for_cycle_counter (0x7f78), which reads byte DS:0x0013 (v10), waits until word [0x1784] is at least that value, then clears [0x1784]. Lower v10 values therefore reduce the top-level cycle wait; generated timed-carousel fixtures use v10 = 1 for fast but still capturable pacing.
  2. Mirror direction state between object 0 byte +0x21 and global byte [0x000f], depending on global word [0x0139]: when [0x0139] == 0, copy object 0 +0x21 to [0x000f]; otherwise copy [0x000f] back to object 0 +0x21.
  3. Call code.motion.pre_mode_and_boundary_update (0x0644). This pass scans active/update-eligible objects whose byte +0x01 == 1, dispatches motion mode byte +0x22 through code.motion.dispatch_mode_step (0x067a), and applies rectangle-boundary checks through code.motion.rectangle_boundary_check (0x06d9) when enabled.
  4. Invoke logic 0 through code.logic.call_logic (0x12ae). Nested logic calls use the same helper but preserve and restore the previous current-logic record.
  5. If byte [0x1757] is zero, call code.object.frame_timer_update (0x0563). That pass performs automatic direction-based group selection, frame-timer callbacks, movement update code.motion.update_objects (0x150a), and update-list draw/dirty-rectangle refresh. If [0x1757] is nonzero, the top-level loop skips 0x0563 for that cycle.

This ordering matters for generated probes and for compatibility. After logic 0 returns, code.engine.main_cycle also restores object 0 byte +0x21 from global byte [0x000f] before continuing the frame/update path. A logic script that writes object 0 +0x21 after the top-of-cycle mirror is therefore too late to seed [0x000f] for the following cycle unless another helper also updates the global direction byte. For example, automatic group selection observes object byte +0x01 before code.motion.update_objects later decrements that countdown byte. A one-shot script write of +0x01 = 2 therefore delays direction-based group selection by one later cycle rather than suppressing it permanently.

SQ2 logic 0 is the global per-cycle script. A source pass over the actual SQ2 logic resource shows that logic 0 handles global menu/input/status work first, then calls the current room script with action 0x17 (call_logic_var) using byte variable 0 at logic bytecode offset 0x053e. Room logics sampled from logic resources 1 through 10 begin with an if flag 5 entry block that loads views, pictures, sounds, and object state for that room, then fall through to normal per-cycle behavior. This means room-switch compatibility fixtures need to model both pieces: the engine’s room-switch helper and logic 0’s later call_logic_var(v0) room dispatch.

Conditional blocks

Opcode 0xff introduces a condition list. The condition parser uses these marker bytes:

ByteObserved role
0xfdInvert the next condition result.
0xfcOR-group marker.
0xffEnd of condition list.

The condition parser keeps two state bytes in BX. BL is the pending inversion flag for 0xfd; after each condition result is XORed with BL, the parser clears BL. BH tracks whether parsing is inside an OR group.

Observed 0xfc behavior:

  • If BH == 0, 0xfc starts an OR group by setting BH = 1.
  • If BH != 0, a second 0xfc means the OR group ended without a true term, so the whole condition list fails.
  • A false condition inside an OR group continues scanning for another OR term.
  • A true condition inside an OR group clears BH, skips the remaining OR terms until the next 0xfc, then resumes normal condition parsing.
  • A false condition outside an OR group fails the whole condition list.
  • A true condition outside an OR group simply advances to the next condition.

When all required conditions pass, execution continues with the following bytecode. When the condition list fails, the interpreter scans forward without executing actions until it finds a block-ending 0xff, then reads a little-endian relative offset and adds it to SI.

When a condition list has already failed, or when a true OR-term needs to skip the rest of its OR group, the condition-list scanner must know condition instruction lengths. For most condition opcodes it uses the fixed operand count in the condition dispatch table. There is a special case for condition opcode 0x0e (input_word_sequence): the scanner reads one count byte and skips count * 2 additional bytes. This special case appears in the condition skip paths at image offsets 0x29af..0x29b8 and 0x29d7..0x29e0.

Condition table

The current DS:0x08fd condition table entries are:

OpcodeLabelHandlerFixed operandsMetadata
0x00always_false0x09d800x00
0x01var_eq_imm0x082320x80
0x02var_eq_var0x083420xc0
0x03var_lt_imm0x084b20x80
0x04var_lt_var0x085c20xc0
0x05var_gt_imm0x087320x80
0x06var_gt_var0x088420xc0
0x07flag_set0x089b10x00
0x08flag_set_var0x08a010x80
0x09obj_table_room_ff0x08ad10x00
0x0aobj_table_room_eq_var0x093b20x40
0x0bobject_left_baseline_in_rect0x08c650x00
0x0cstatus_byte_12180x093110x00
0x0draw_key_event_available0x09be00x00
0x0einput_word_sequence0x095c00x00
0x0fstring_slots_equal_normalized0x09db20x00
0x10object_width_baseline_in_rect0x08e850x00
0x11object_center_baseline_in_rect0x08cc50x00
0x12object_right_baseline_in_rect0x08db50x00

The bytes after condition-table entry 0x12 decode as string/data bytes and then zero fill if forced through the same 4-byte entry parser. Although the condition dispatcher only rejects opcodes >= 0x26, no valid local condition list uses opcodes 0x13..0x25; for this build they are treated as invalid/reserved rather than real predicates.

Condition-list byte ranges:

Condition byte/rangeObserved role
0x00..0x12Valid predicates in this SQ2 build, listed above.
0x13..0x25Reserved/invalid for the portable model of this build. The dispatcher bound would allow them, but the underlying bytes are not a valid dispatch-table region.
0x26..0xfbRejected by the condition dispatcher if encountered as predicate opcodes.
0xfcOR-group marker interpreted by the condition-list scanner.
0xfdInvert-next-condition marker interpreted by the condition-list scanner.
0xfeRejected by the condition dispatcher if encountered as a predicate opcode.
0xffCondition-list terminator interpreted by the condition-list scanner.

The first seven handlers directly expose byte variable comparisons. The byte variable array begins at DS:0x0009.

OpcodeLabelObserved predicate
0x00always_falseAlways false. Handler 0x09d8 returns zero.
0x01var_eq_immbyte[0x0009 + arg0] == arg1
0x02var_eq_varbyte[0x0009 + arg0] == byte[0x0009 + arg1]
0x03var_lt_immbyte[0x0009 + arg0] < arg1
0x04var_lt_varbyte[0x0009 + arg0] < byte[0x0009 + arg1]
0x05var_gt_immbyte[0x0009 + arg0] > arg1
0x06var_gt_varbyte[0x0009 + arg0] > byte[0x0009 + arg1]
0x07flag_setTests flag bit arg0.
0x08flag_set_varTests flag bit byte[0x0009 + arg0].
0x09obj_table_room_ffLooks up a 3-byte table entry at [0x0971] + arg0 * 3 and tests whether byte +2 is 0xff.
0x0aobj_table_room_eq_varCompares byte +2 of that 3-byte table entry with byte[0x0009 + arg1].
0x0cstatus_byte_1218Returns byte DS:0x1218 + arg0.
0x0draw_key_event_availableChecks or obtains a raw key-like event byte through helper 0x459e, caching a non-zero byte at DS:0x001c.
0x0einput_word_sequenceVariable-length parsed-input word sequence test.
0x0fstring_slots_equal_normalizedCompares two fixed string slots after a small normalization pass.

The handler reads two byte operands and passes them to helper 0x0eac. Helper 0x0eac builds two temporary normalized strings through helper 0x0ef8, then compares the resulting zero-terminated byte strings exactly. The string-slot address is computed as 0x020d + slot * 0x28.

The 0x0ef8 normalization step:

  • walks the source slot until a zero byte;
  • skips bytes present in the zero-terminated table at DS:0x094b;
  • in local SQ2 data, DS:0x094b contains space, tab, ., ,, ;, :, ', !, and -;
  • converts ASCII uppercase A..Z to lowercase a..z through helper 0x4fea;
  • writes a zero terminator to the temporary buffer.

Unlike action 0x75 (parse_string_slot), this predicate does not parse dictionary words and does not use the parsed-word tables. It is a direct case-insensitive comparison after dropping the listed punctuation/spacing bytes.

Condition opcodes 0x0b (object_left_baseline_in_rect), 0x10 (object_width_baseline_in_rect), 0x11 (object_center_baseline_in_rect), and 0x12 (object_right_baseline_in_rect) all load an entry from a 43-byte structure array rooted at [0x096b] using the first operand as an index. They compare that object’s baseline position or horizontal extent against four subsequent byte operands:

arg0: object index
arg1: left bound
arg2: top/Y bound
arg3: right bound
arg4: bottom/Y bound

The shared helper at 0x091a loads:

object = [0x096b] + arg0 * 0x2b
dh = object[+0x03]
ch = object[+0x03]
dl = object[+0x05]

The common comparison at 0x08f0 returns true when:

dh >= arg1
dl >= arg2
ch <= arg3
dl <= arg4

The four handlers differ only in how they choose dh and ch before the comparison:

OpcodeLabelHorizontal test before shared rectangle comparison
0x0bobject_left_baseline_in_rectTests object left X and baseline Y inside the rectangle: dh = ch = x.
0x10object_width_baseline_in_rectTests the full object horizontal span at baseline Y: dh = x, ch = x + width - 1.
0x11object_center_baseline_in_rectTests object horizontal center and baseline Y: dh = ch = x + floor(width / 2).
0x12object_right_baseline_in_rectTests object right X and baseline Y: dh = ch = x + width - 1.

Flag helpers use a bitfield rooted at DS:0x0109. Helper 0x7511 computes:

byte_address = DS:0x0109 + flag_number / 8
mask = 0x80 >> (flag_number & 7)

Helper 0x74ee sets the bit, 0x74f4 clears it, 0x74fc toggles it, and 0x7502 tests it. Initialization helper 0x752a clears 0x20 bytes starting at DS:0x0109.

Condition opcode 0x0e (input_word_sequence) is the most common condition in the local SQ2 scripts. Its bytecode operands are variable length:

u8 count
u16le word_id[count]

Handler 0x095c compares those word IDs with a parsed input-word buffer rooted at DS:0x0c7b; word [0x0ca3] is used as the parsed-word count. The handler returns false without consuming the word list when the parsed-word count is zero, when flag 4 is already set, or when flag 2 is clear. Otherwise it walks the operand word IDs and the parsed input words together. Operand word 0x270f terminates the test successfully and skips any remaining operand words; operand word 0x0001 behaves as a wildcard for one parsed word. On a full match the handler sets flag 4 and returns true; on a mismatch it skips the remaining operand words and returns false. Because parser failures store a nonzero count/error-position and set flag 2, a terminator-only pattern can match an unknown-token parse state; QEMU batch parser_unknown_terminator_001 confirms this source-modeled edge.

Parsed input producer

Action 0x75 (parse_string_slot) is the observed producer for the parsed-word state consumed by condition 0x0e (input_word_sequence). It reads a string-slot index, accepts only slots 0..11, and passes fixed string slot 0x020d + slot * 0x28 to parser helper 0x18ac. Before parsing it clears flags 2 and 4.

Helper 0x18ac clears two 20-byte word tables:

0x0c7b: parsed dictionary word IDs, up to 10 words
0x0c8f: pointers to the normalized words in buffer 0x0ca7, up to 10 words

It normalizes the source string through helper 0x199d into buffer 0x0ca7. The normalization step:

  • treats bytes from DS:0x0c67 as separators. In SQ2 this string is ,.?!();:[]{}.
  • treats bytes from DS:0x0c75 as ignored punctuation. In SQ2 these bytes are 0x27, 0x60, 0x2d, and 0x22.
  • collapses runs of separators to one space.
  • drops ignored punctuation instead of making a new word.
  • trims a trailing space and writes a zero terminator.

Helper 0x1a6b then looks up each normalized word in WORDS.TOK, whose loaded base pointer is stored at [0x0ca5]. The file starts with 26 big-endian word offsets, one for each lowercase initial letter. A zero offset means no words for that initial. Dictionary entries are prefix-compressed:

u8 prefix_len_from_previous_decoded_word
encoded suffix bytes, last byte has bit 7 set
u16be word_id

For each suffix byte, (byte & 0x7f) ^ 0x7f gives the decoded lowercase character. The high bit only marks the final suffix byte. Local inspection of SQ2/WORDS.TOK finds 1,099 entries; for example look has word ID 0x0002, get has word ID 0x0005, and anyword has word ID 0x0001.

Parser result handling in 0x18ac:

  • Recognized words with nonzero IDs are appended to 0x0c7b, and their normalized-word pointers are appended to 0x0c8f.
  • Word ID zero is ignored; single-letter a and i followed by a space or terminator are handled this way, as are entries whose dictionary ID is zero. Ignored zero-ID words do not consume one of the ten parsed output slots.
  • An unrecognized token stores its pointer in the next 0x0c8f output slot, sets byte variable [0x0012] and word [0x0ca3] to parsed_nonzero_word_count + 1, and stops parsing. Thus an unknown word after ignored zero-ID words such as the still reports output slot 1, not the raw token ordinal.
  • If at least one token position is produced, flag 2 is set. Condition 0x0e (input_word_sequence) later sets flag 4 after a successful command-pattern match.

Raw event queue

Condition 0x0d (raw_key_event_available) is separate from parsed-word matching. Handler 0x09be first checks byte variable [0x001c]; if it is already nonzero, the condition returns true. Otherwise it calls helper 0x459e until that helper returns something other than 0xffff:

event = dequeue_event()                         # 0x44f9
if no event:
    return false
normalize_enter_escape(event)                   # 0x4634
if event.type == 1:
    return event.value
return 0xffff

When 0x459e returns a nonzero key-like value, condition 0x0d (raw_key_event_available) stores the low byte in [0x001c] and returns true. A zero return means no available event and the condition returns false. A 0xffff return means a non-key event was discarded and polling should continue.

QEMU fixture raw_key_condition_001 validates this predicate dynamically. The generated logic draws only when condition 0x0d succeeds, does not install any script key mapping with 0x79, and then sends a plain x key through the QEMU monitor. The original interpreter capture matched the validation draw, proving that the raw type-1 event path can satisfy 0x0d directly.

The queue itself is a 20-entry circular buffer of 4-byte records:

storage: 0x11ba .. 0x1209
[0x120a]: write pointer
[0x120c]: read pointer

record +0x00: event type word
record +0x02: event value word

Helper 0x44a9(type, value) enqueues a record and fails if advancing the write pointer would collide with the read pointer. Helper 0x44f9() dequeues one record, returning zero when the queue is empty.

The keyboard IRQ hook at image 0x6036 also feeds this queue. For selected scan codes in the range 0x47..0x51, it checks an enable table at 0x1519 + (scan - 0x47) and a pressed latch at 0x1524 + (scan - 0x47). On a key release, if the latch was set, it clears the latch and tests byte [0x1530]. When that byte is nonzero, the hook enqueues event (type=2, value=0) through code.input.enqueue_event (0x44a9). Action 0xad increments [0x1530], so it acts as a source-backed gate/count for this release-event path.

Keyboard helper 0x5a89 uses BIOS int 16h: it returns zero if no key is waiting, otherwise reads one key. If the ASCII byte is nonzero it clears AH and returns just that byte; if ASCII is zero, the BIOS scan-code word is returned intact.

Helper 0x467f drains available BIOS key events through 0x5a89 and enqueues them:

  • If helper 0x46b6 finds the returned key word in table DS:0x16b3, it enqueues type 2 with the mapped value.
  • Otherwise it enqueues type 1 with the raw key word.

The local DS:0x16b3 table maps BIOS arrow/keypad scan words to direction-like values:

Key wordMapped value
0x48001
0x49002
0x4d003
0x51004
0x50005
0x4f006
0x4b007
0x47008

The adjacent hardware/display remap table at DS:0x16d7 maps single-byte numeric keypad values to the same type-2 movement values when display adapter word [0x112e] == 2: 0x38, 0x39, 0x36, 0x33, 0x32, 0x31, 0x34, and 0x37 map to 1..8. The trailing 0x35 -> 0 entry is present in the table but does not enqueue a movement value.

Helper 0x4634 normalizes some multi-byte key words after dequeue: values 0x0101 and 0x0301 become 0x000d, while 0x0201 and 0x0401 become 0x001b.

Helper 0x4566(event_record) performs a script-configured remap for type-1 events. It scans four-byte slots rooted at 0x0145; when slot word +0 equals the event value, it changes the event type to 3 and replaces the value with slot word +2. Action 0x79 (map_key_event) appends entries to this table. An attempted QEMU probe mapping 0x5000 through 0x79 did not set the target status byte when driven with monitor sendkey down; this is recorded as a QEMU/input-instrumentation gap, not as evidence against the source table.

When display adapter word [0x112e] == 2, helper 0x46e8 can also remap type-1 event values through table DS:0x16d7, changing the event type to 2. The local table maps ASCII digits to direction-like values:

Key wordMapped value
0x00381
0x00392
0x00363
0x00334
0x00325
0x00316
0x00347
0x00378

Action table

The action table at DS:0x061d has entries for opcodes 0x00..0xaf. The main dispatcher only executes opcodes 0x01..0xaf; 0x00 terminates the current logic loop and 0xfc..0xff are structural bytes.

All action-table entries through 0xaf now have local labels. Some labels remain deliberately implementation-shaped where the handler’s state mutation is clear but the user-facing command name is not.

Coverage audit:

  • tools/disassemble_logic.py labels every action byte 0x00..0xaf.
  • 0x00 is a structural byte handled by the main interpreter loop, not by the normal action dispatcher.
  • 0xfc..0xff are structural bytes and are rejected by the action dispatcher.
  • The current condition catalog labels all valid-looking condition-table entries 0x00..0x12; bytes 0x13..0x25 are treated as reserved/invalid in this SQ2 build even though the dispatcher only rejects >= 0x26.

Examples of action entries:

OpcodeLabelHandlerFixed operandsMetadata
0x00end0x505100x00
0x01inc_var0x735510x80
0x02dec_var0x736810x80
0x03assignn0x737b20x80
0x0cset_flag0x748410x00
0x14load_logic0x113d10x00
0x16call_logic0x125a10x00
0x1eload_view0x39b110x00
0x21reset_object_state0x04d910x00
0x23activate_object0x09ea10x00
0x25set_object_pos0x7c1a30x00
0x29set_object_resource0x3a7720x00
0x3fset_global_012d0x7e7c10x00
0x51move_object_to0x6ce450x00
0x62load_sound0x510a10x00
0x64stop_sound_or_clear_sound_state0x522500x00
0x7asetup_transient_object0x2c7a70x00
0x82random_range_to_var0x500930x20
0x93set_object_pos_dirty0x7d7730x00
0xa7divn0x744c20x80

This table entry points at the same no-op helper used by action 0x7f, but the main interpreter loop handles opcode byte 0x00 before entering the action dispatcher. Runtime opcode 0x00 returns the instruction pointer immediately after the terminator. It does not write the logic record’s resume-pointer field. Later bytes in the same logic payload can still be reached by a jump from earlier code.

The action dispatcher itself does not use the operand-count byte; handlers consume operands directly from SI and return the next SI in AX. The table metadata is used by scanner/debug paths that need to skip or display bytecode.

Decoded action families

Variable action handlers directly operate on the byte array rooted at DS:0x0009.

OpcodeLabelHandlerObserved action
0x01inc_var0x7355Increment var[arg0] unless it is already 0xff.
0x02dec_var0x7368Decrement var[arg0] unless it is already 0x00.
0x03assignn0x737bvar[arg0] = arg1
0x04assignv0x7388var[arg0] = var[arg1]
0x05addn0x739bvar[arg0] += arg1
0x06addv0x73a8var[arg0] += var[arg1]
0x07subn0x73bbvar[arg0] -= arg1
0x08subv0x73c8var[arg0] -= var[arg1]
0x09indirect_assignv0x73dbvar[var[arg0]] = var[arg1]
0x0aassign_indirectv0x7405var[arg0] = var[var[arg1]]
0x0bindirect_assignn0x73f4var[var[arg0]] = arg1
0xa5muln0x741evar[arg0] *= arg1; low byte of the product is stored.
0xa6mulv0x7431var[arg0] *= var[arg1]; low byte of the product is stored.
0xa7divn0x744cvar[arg0] /= arg1; 8-bit quotient is stored.
0xa8divv0x7465var[arg0] /= var[arg1]; 8-bit quotient is stored.

Flag action handlers use the same bitfield helpers as condition opcodes 0x07 and 0x08.

OpcodeLabelHandlerObserved action
0x0cset_flag0x7484Set flag bit arg0.
0x0dclear_flag0x748bClear flag bit arg0.
0x0etoggle_flag0x7492Toggle flag bit arg0.
0x0fset_flag_var0x7499Set flag bit var[arg0].
0x10clear_flag_var0x74a8Clear flag bit var[arg0].
0x11toggle_flag_var0x74b7Toggle flag bit var[arg0].

Several object/view actions use entries in the 43-byte structure array rooted at [0x096b]. Field names remain provisional; these notes record observed storage and calls rather than assigning final game-level names.

OpcodeLabelHandlerObserved action
0x23activate_object0x09eaReads an object index and calls 0x0a06. The helper validates the object, requires word [object+0x10] != 0, copies [+0x10] to [+0x12], [+0x03] to [+0x16], and [+0x05] to [+0x18], sets bits in word [+0x25], and calls several list/graphics helpers.
0x24deactivate_object0x0a8fCalls helper 0x0aab for object arg0. If the object has bit 0x0001 set in [+0x25], the helper clears that bit and calls list/graphics helpers to remove or deactivate the object.
0x25set_object_pos0x7c1aSet object position-like fields from immediate bytes: [+0x16] = [+0x03] = arg1, [+0x18] = [+0x05] = arg2.
0x26set_object_pos_var0x7c57Set the same fields from variables var[arg1] and var[arg2].
0x27get_object_pos0x7ca4Store low bytes of object fields [+0x03] and [+0x05] into var[arg1] and var[arg2].
0x28add_object_pos_from_vars0x7ce7Reads object index arg0, signed deltas from var[arg1] and var[arg2], adds those deltas to object fields [+0x03] and [+0x05], clamps each field to zero when a negative delta would underflow, sets bit 0x0400 in [+0x25], then calls 0x593a.
0x29set_object_resource0x3a77Resolve object arg0, pass immediate arg1 to helper 0x3ae7.
0x2aset_object_resource_var0x3aabResolve object arg0, pass var[arg1] to helper 0x3ae7.
0x2bset_object_subresource0x3b47Resolve object arg0, pass immediate arg1 to helper 0x3bb7.
0x2cset_object_subresource_var0x3b7bResolve object arg0, pass var[arg1] to helper 0x3bb7.
0x2fset_object_derived_resource_20x3c55Resolve object arg0, pass immediate arg1 to helper 0x3ccb, then clear bit 0x1000 in object word field [+0x25].
0x30set_object_derived_resource_2_var0x3c8cSame as 0x2f, but the helper argument is read from var[arg1]. Helper 0x3ccb selects a derived subresource/loop-like entry, updates object byte [+0x0e], pointer [+0x10], width-like word [+0x1a], and height-like word [+0x1c], then clamps object fields [+0x03] and [+0x05] against visible bounds and sets bit 0x0400 when it adjusts them.
0x31get_object_resource_loop_count0x3d9fStores byte [*([object+0x0c])] - 1 into var[arg1]. This appears to report a count from the object’s loaded resource table.
0x32get_object_field_0e0x3dedStores object byte [+0x0e] into var[arg1].
0x33get_object_field_0a0x3e25Stores object byte [+0x0a] into var[arg1].
0x34get_object_field_070x3e5dStores object byte [+0x07] into var[arg1].
0x35get_object_field_0b0x3e95Stores object byte [+0x0b] into var[arg1]. This opcode was present in the action table but not encountered in the current SQ2 scan.

Helper 0x3ae7 finds a cached resource record via 0x3979, stores the resource payload pointer at object field [+0x08], stores the selected resource number at byte [+0x07], copies byte [payload+0x02] to object byte [+0x0b], then calls 0x3bb7. Helper 0x3bb7 validates object field [+0x08], checks its second argument against byte [+0x0b], then calls 0x3c1b and 0x3ccb to update derived object fields.

Resource and interpreter-control actions observed so far:

OpcodeLabelHandlerObserved action
0x12switch_room_like0x175cReads immediate arg0 and calls helper 0x1792. The helper stops active sound state, restores heap/update-list state through 0x1485, calls cleanup helpers 0x4482, 0x707c, and 0x706d, resets selected object fields, truncates/clears resource caches, sets [0x0139] = 1, stores 0x24 in word [0x012d], copies byte variable 0 from DS:0x0009 to DS:0x000a, writes arg0 to byte variable 0, clears bytes DS:0x000d and DS:0x000e, records object 0’s current view/resource byte in DS:0x0019, calls 0x117d to load logic arg0, optionally loads another logic from [0x1d12], may reposition object 0 from boundary byte [0x000b], sets flag 5, and calls redraw/reinitialization helpers. This is a broad room/state switch action; the final name is still provisional.
0x13switch_room_like_var0x1773Same as 0x12, but the target number is read from var[arg0].
0x14load_logic0x113dReads immediate arg0, calls 0x117d. Helper 0x117d loads logic resource arg0 through 0x119a, then records pair (0, arg0) through helper 0x70b1.
0x15load_logic_var0x1159Same as 0x14, but logic number is var[arg0].
0x16call_logic0x125aReads immediate arg0, calls 0x12ae, and returns zero to the action dispatcher if 0x12ae returns zero. Helper 0x12ae preserves the previous current logic pointer at [0x0981], finds or loads the target logic, calls main interpreter 0x293c, and frees the target record afterward only if it had to be loaded transiently for this call.
0x17call_logic_var0x1280Same as 0x16, but logic number is var[arg0].
0x18load_picture_var0x4a16Reads a picture-like resource number from var[arg0] and calls loader helper 0x4a3b. When the resource is not already cached, the helper records event pair (2, resource), uses directory accessor 0x43d9 and the generic volume reader 0x2e32, then stores the loaded payload pointer in a linked cache entry rooted at 0x120e.
0x19prepare_picture_var0x4aaaReads a picture-like resource number from var[arg0], requires an existing cached entry through 0x4acf, records event pair (4, resource), stores the resource payload pointer at global 0x1377, and calls helpers 0x6a54, 0x6445, and 0x6a8e.
0x1ashow_picture_like0x4b82Clears flag 15 through wrapper 0x74d0, calls helper 0x1f2b with argument 0, calls 0x5546, and sets global word [0x1216] = 1. This appears to be a picture/display finalization action.
0x1bdiscard_picture_var0x4baaReads a picture-like resource number from var[arg0] and calls helper 0x4bce, which records event pair (6, resource), clears the link preceding the matching cache record so the match and every later picture record become unreachable, rewinds the shared heap through 0x143c, and calls update/free-state helpers 0x6a8e and 0x14a0.
0x1coverlay_picture_var0x4b17Reads a picture-like resource number from var[arg0], requires an existing cached entry through helper 0x4b3b, records event pair (8, resource), stores that resource’s payload pointer at global [0x1377], calls helpers 0x6a54, 0x6440, 0x6a8e, and 0x6aab, and clears word [0x1216]. Unlike 0x19 (prepare_picture_var), this path enters the picture decoder at 0x6440 rather than 0x6445, so it skips the extra buffer-fill setup performed by 0x6445.
0x1dshow_priority_screen0x731bSets word [0x1755] = 1, calls full-screen refresh helper 0x5546, waits for an event through 0x4618, calls 0x5546 again, then clears [0x1755]. Helper 0x5546 swaps the high and low nibbles of every byte in the logical graphics buffer while [0x1755] & 1 is set before copying the full screen to the display. The only observed local input phrase reaching this action is show pri, where WORDS.TOK word id 0x0028 maps to “show” and word id 0x003f maps to “pri”.
0x1eload_view0x39b1Loads or refreshes a view-like resource through helper 0x39f7 using immediate arg0. When no cached view entry exists, the helper records event pair (1, resource) before allocating and reading the resource.
0x1fload_view_var0x39d0Same as 0x1e, but resource number is var[arg0].
0x20discard_view0x3ecdReads immediate view-like resource number arg0 and calls helper 0x3f0d. That helper requires a matching cached view record through 0x3979, records pair (7, resource) through 0x70b1, clears the preceding link through *([0x1000]) so the match and every later view record become unreachable, flushes update lists with 0x6a54, rewinds to the selected record with 0x143c, rebuilds update lists with 0x6a8e, and calls 0x14a0.
0x62load_sound0x510aLoads a sound-like resource through helper 0x5126 using immediate arg0. When no cached sound entry exists, the helper records event pair (3, resource), uses sound directory accessor 0x440d, and builds four internal pointers from the loaded payload.
0x63start_sound_with_flag0x51d3Stops any active sound-like state through 0x5234, reads immediate sound number arg0 and immediate flag number arg1, stores arg1 in word [0x126a], clears flag arg1, locates or loads sound arg0 through helper 0x50d8, and starts it through helper 0x7f96. If the sound cannot be loaded, it reports error code 9 with the sound number.
0x64stop_sound_or_clear_sound_state0x5225Calls helper 0x5234, which clears a pending sound-like state at [0x1258], sets flag [0x126a], and calls 0x080af when that state was active.

QEMU fixture resource_lifecycle_003 validates several resource lifecycle paths with synthetic resources:

  • 0x15 (load_logic_var) can load a variable-selected logic resource before 0x16 calls it.
  • 0x1c (overlay_picture_var) requires the target picture to have already been loaded with 0x18. The overlay changes logical picture state, but the composed picture was not visible in the QEMU capture until 0x1a (show_picture_like) ran afterward.
  • 0x1b (discard_picture_var) can discard a loaded picture and allow a later reload/overlay path.
  • 0x20 (discard_view) and 0x99 (discard_view_var) can discard a loaded view; reloading that view with 0x1e then permits normal drawing.
  • 0x1f (load_view_var) is validated by starting without the usual preloaded view 11, setting a variable to 11, executing 0x1f, and then successfully drawing that view.

QEMU fixture menu_sound_001 dispatch-smokes 0x62 (load_sound) followed by 0x64 (stop_sound_or_clear_sound_state) with sound resource 1. QEMU fixture sound_completion_001 adds a behavior assertion across 0x62, 0x63, and 0x64: it first loads sound 1 with 0x62, starts it with completion flag operand 77 through 0x63, then clears sound state with 0x64 and draws only after flag 77 is set. This validates the load/start/stop completion-flag contract for those opcodes, but it does not yet model audio output or asynchronous playback timing.

QEMU fixture priority_diag_sound_001 validates the same stop-flag behavior in the narrower priority/diagnostic batch: after 0x63 has associated completion flag 77 with sound 1, 0x64 sets that flag while clearing sound state. The same batch validates 0x1d as a blocking priority/control display that returns after Enter and 0x85 as an object-diagnostics display action that returns after Enter.

Source mapping clarifies the sound state transition. Action 0x63 first calls the same stop helper used by 0x64, then reads the sound resource number and completion flag operand. It stores the flag number in [0x126a], clears that flag, locates an already loaded sound resource through helper 0x50d8, reports an error if it cannot be found, and calls the hardware/driver start helper 0x7f96 with the located record. Action 0x64 is a thin wrapper around code.sound.stop_or_clear_state; that helper only sets the configured completion flag when active-state word [0x1258] is nonzero, then clears the active-state word and calls the hardware/driver stop helper 0x80af.

The sound loader and driver-start helpers also reveal the sound resource container. code.sound.load_resource reads the payload through the sound directory, stores the raw payload pointer at cache-record +0x04, then reads four little-endian words from the payload header and stores four derived payload-relative channel pointers at record offsets +0x06, +0x08, +0x0a, and +0x0c. code.sound.driver_start copies those four pointers into the runtime channel pointer table, initializes four countdown words to 1, initializes active/tone state words, and marks sound state active at [0x1258]. The driver tick reads duration u16, treats 0xffff as a channel terminator, otherwise reads a 16-bit tone/control word followed by one control byte whose low nibble is the observed attenuation/control value.

The timer interrupt hook calls code.sound.driver_tick while [0x1258] is nonzero. code.sound.driver_tick first tests flag 9 through the shared flag helper; if flag 9 is clear, it immediately runs the stop/core completion path. When playback is allowed, the tick loop advances channel 0 only for hardware selector [0x112e] values 0 and 8, and advances channels 0 through 3 for other observed selector values. Each channel’s countdown starts at 1, so its first event or terminator is consumed on the first tick. Event durations are stored as 16-bit countdown values. Duration zero would wrap before the next record read; no present SQ2 sound resource uses duration zero. Natural completion occurs when all active channels have consumed 0xffff terminators, which reaches the same stop/core path that clears active state and sets the configured completion flag. Tone-to-frequency conversion, attenuation envelopes, and hardware port effects remain provisional.

Resource Event Log

The engine keeps a compact replay log for resource and transient-display work that must be reconstructed after a restore or display-mode rebuild. The log is not a general trace. It records the operations needed to rebuild persistent room state from a reset cache, and some helper paths intentionally disable recording while doing temporary work.

Each entry is a two-byte pair:

u8 kind
u8 value

data.event.pair_capacity ([0x0141]) is the maximum number of pairs. code.event.reset_pair_buffer allocates capacity * 2 bytes if the base pointer is zero, stores the base at [0x1707], resets the write pointer [0x1709] to the base, and clears data.event.pair_count ([0x0143]).

code.event.record_pair appends only when both gates allow it:

  • flag 7 must be clear;
  • data.event.recording_enabled ([0x170d]) must be nonzero.

When appending, it checks the write pointer against base + capacity * 2, reports error code 0x0b on overflow, writes the low bytes of (kind, value), advances the write pointer by two, and increments the pair count. It also maintains data.event.pair_high_water ([0x170f]) from the current write distance; the heap/status diagnostic displays this value.

Known event kinds:

KindRecorded byRestore replay effect
0load_logic / load_logic_var through code.logic.load_cachedLoad the logic resource through code.logic.load_resource, then restore saved logic resume metadata with 0x13a5.
1load_view / load_view_var when a view cache entry is createdLoad or refresh the view resource through code.view.load_resource with the force/refresh argument set.
2load_picture_var when a picture cache entry is createdLoad the picture resource through code.picture.load_resource.
3load_sound when a sound cache entry is createdLoad the sound resource through code.sound.load_resource.
4prepare_picture_varPrepare/decode the already-loaded picture through code.picture.prepare.
5setup_transient_object / setup_transient_object_varRead the next three pairs into staged bytes 0x0eae..0x0eb3, then call code.object.setup_transient_display_object.
6discard_picture_varDiscard the picture and truncate later picture cache records through code.picture.discard.
7discard_view / discard_view_varDiscard the view and truncate later view cache records through 0x3f0d.
8overlay_picture_varOverlay/decode the already-loaded picture through code.picture.overlay_prepare.

Kind 5 is a four-pair packet rather than a single resource event. Helper 0x2d52 first records (5, 0), then records ([0x0eae], [0x0eaf]), ([0x0eb0], [0x0eb1]), and ([0x0eb2], [0x0eb3]). Restore replay consumes those three following pairs and writes them back to the same staged globals before calling 0x2d52.

Restore-time replay at code.restore.replay_resource_events stops sound, resets room caches, disables event recording, prepares the replay cursor, then walks pairs with code.event.next_replay_pair. Disabling recording is what prevents the replayed loads/discards from appending duplicates to the log. The event-kind dispatch table lives at image 0x6915; a linear disassembly can misread the first post-table instruction as data. When disassembled at the loop-exit target, image 0x6927 is call 0x706d, so replay re-enables event recording immediately after code.event.next_replay_pair returns zero and before it rebinds object views. After that it restores object flags saved around the cache reset, refreshes the display, and redraws text/input state. This source finding explains the earlier QEMU observation that recording was enabled again by the following script action.

Because kinds 6 and 7 enter the ordinary discard helpers, replay reproduces the same ordered family truncation. A later kind 1 or 2 pair can retain a resource again after that truncation; restore therefore rebuilds retention order from the active pair sequence rather than from an unordered final set.

The temporary view-resource display actions 0x81 and 0xa2 also call code.event.disable_recording before their internal load/display/discard sequence, then call code.event.enable_recording before returning. This keeps view preview/text display from becoming part of the room restore model.

Room/state switch helper 0x1792, reached by actions 0x12 (switch_room_like) and 0x13 (switch_room_like_var), first stops active sound state, resets heap/update-list state through code.heap.reset_dynamic_state, clears parser/input state, initializes the resource/event pair buffer, and enables resource/event pair recording. It then resets all object records from [0x096b] to [0x096d]. For each 43-byte record it clears active/update bits 0x0001 and 0x0040, sets bit 0x0010, clears pointer fields +0x08, +0x10, and +0x14, and stores 1 in bytes +0x00, +0x01, +0x1e, +0x1f, and +0x20. The last three are the step-size byte and the frame-timer reload/current bytes; they are seeded to one, not zero.

After the object reset, helper 0x10d0 resets the room resource caches. It preserves the first logic cache record by writing zero to that record’s next link, then clears the view cache root [0x0ffa], sound cache root [0x125a], and picture cache root [0x120e]. The room-switch helper then sets the object-boundary flag word [0x0139] = 1, clears word [0x013d], restores the horizon-like global [0x012d] to 0x24, copies current room byte variable 0 to previous-room byte variable 1, writes the destination room to byte variable 0, clears object-boundary bytes [0x000d] and [0x000e], copies object 0’s view/resource byte +0x07 to byte variable 16, refreshes object/resource state, loads the destination logic through code.logic.load_cached, reloads optional trace logic [0x1d12] when configured, handles the entry-boundary selector, sets flag 5, refreshes display/resource state, redraws the status line, redraws the input line, and returns zero.

The zero return is important. code.engine.main_cycle calls logic 0 through code.logic.call_logic(0); if that call returns zero, it clears temporary room-boundary variables and immediately calls logic 0 again instead of continuing the post-logic frame/update path. This explains why one-shot synthetic room-switch probes that draw after 0x12/0x13 are the wrong shape: the switch action intentionally aborts the current bytecode stream so the next logic-0 pass can perform entry initialization and room dispatch.

Entry-boundary selector byte variable 2 (DS:0x000b) is interpreted after the destination logic is loaded: 1 places object 0 Y at 0xa7, 2 places X at 0, 3 places Y at 0x25, and 4 places X at 0xa0 - object_width; the byte is then cleared.

In script terms, byte variable 0 (DS:0x0009) is the current room, byte variable 1 (DS:0x000a) is the previous room, and byte variable 2 (DS:0x000b) is the entry-boundary selector consumed by the switch helper. The helper loads the destination logic resource but does not by itself execute that room’s bytecode. In SQ2, logic 0 later dispatches the current room via call_logic_var(v0).

Several early QEMU probes for 0x12/0x13 did not produce stable reusable evidence. A direct var0 assertion after the switch failed, a target-logic draw probe failed, a logic-0 re-entry fixture with 0x92 (restore_logic_entry_ip) before the switch did not reach its validation draw, and a first synthetic call_logic_var(v0) dispatch fixture still produced the same blank-screen mismatch. Those failures were fixture-shape problems rather than disproofs of the source model.

A later QEMU fixture room_switch_reentry_001 matched the original engine for both 0x12 and 0x13. In that fixture, logic 0 sets a private init flag before executing the switch action. Because the switch returns zero, the current logic invocation aborts and code.engine.main_cycle immediately calls logic 0 again. On the second pass, logic 0 skips the switch and executes call_logic_var(v0). The destination logic then checks flag 5, loads and shows its own picture, loads its view, and draws a validation sprite. This validates the user-visible re-entry/current-room dispatch shape: a room switch prepares state and loads the destination logic, but room-local drawing/setup belongs to the destination room logic reached on the following logic-0 pass.

QEMU fixture room_previous_001 validates the byte-variable room update contract for both 0x12 and 0x13. The fixture writes a synthetic previous room into byte variable 0 before switching to room 1, and writes an invalid boundary selector into byte variable 2. The destination room logic draws only if byte variable 0 is the destination room, byte variable 1 is the synthetic previous room, and byte variable 2 has been cleared. The matched immediate and variable-selected cases confirm that the switch helper copies old v0 to v1, writes the destination room to v0, and clears v2 on both operand paths.

QEMU fixtures room_boundary_002 and room_boundary_var_001 validate the four entry-boundary selector cases through normal bytecode-visible object state for both immediate and variable-selected room switches. The fixture initializes object 0 with loaded view 11 frame 0 at (44,80) before switching rooms. The destination room logic then reads object 0 with action 0x27 and draws only if the expected position and cleared selector byte are observed. The matched cases confirm selector 1 writes Y 0xa7, selector 2 writes X 0, selector 3 writes Y 0x25, selector 4 writes X 140 (0xa0 - 20 for the selected cel width), and all four clear byte variable 2 on both operand paths. An earlier attempt that bound object 0’s view without first loading view 11 did not reach the switch; the right-edge selector is only meaningful when object width has already been derived.

QEMU fixture room_object_reset_001 validates one visible object-reset effect for both operand paths. Logic 0 loads view 11, sets up object 10, activates it as a persistent object, and then switches rooms. The destination room logic draws a single validation sprite. Both immediate and variable-selected cases match only when the pre-switch persistent object is absent from the destination room render, confirming that active object draw state does not survive the room switch.

The broader internal reset effects are still source-backed from the disassembly: object/resource reset beyond the bytecode-visible fields checked above, resource/event recording, optional trace logic reload, and the exact redraw/input refresh calls.

Additional object-state actions:

OpcodeLabelHandlerObserved action
0x21reset_object_state0x04d9Calls helper 0x04f5 for object arg0. If object word [+0x25] does not have bit 0x0040, the helper sets [+0x25] = 0x0070 and clears bytes [+0x22], [+0x23], and [+0x21].
0x22clear_all_object_bits0x053dIterates all 43-byte object entries from [0x096b] to [0x096d] and clears bits 0x0041 in word field [+0x25].
0x2dset_object_bit_20000x497bSets object bit 0x2000 in [+0x25]. In the per-cycle frame/group update helper, this bit suppresses automatic direction-based group selection.
0x2eclear_object_bit_20000x49a3Clears object bit 0x2000 in [+0x25], allowing automatic direction-based group selection when the object’s view/group count field is in a supported range.
0x3aclear_object_bit_00100x6ac8Calls helper 0x6b44, which clears object bit 0x0010 if it was set. The bit partitions active objects between update-list root 0x16ff and root 0x1703; the helper wraps a membership change with 0x6a54 and 0x6a8e to flush/rebuild the two lists.
0x3bset_object_bit_00100x6af0Calls helper 0x6b62, which sets object bit 0x0010 if it was clear. As with 0x3a, a real membership change flushes and rebuilds the two update-list roots through 0x6a54 and 0x6a8e.
0x3crefresh_object_lists0x6b18Computes the object record address from arg0, then calls helpers 0x6a54, 0x6a8e, and 0x6aab. The computed object address is not passed to those helpers; the observed effect is an all-list flush, rebuild/draw, and dirty-rectangle refresh pass, with no direct object field writes in the handler itself.
0x3dset_object_bit_00080x7e94Sets object bit 0x0008 in [+0x25].
0x3eclear_object_bit_00080x7eb9Clears object bit 0x0008 in [+0x25].
0x3fset_global_012d0x7e7cStores immediate arg0 as a word at DS:0x012d.
0x40set_object_bit_01000x7e0dSets bit 0x0100 in object word field [+0x25]. QEMU validates that this makes a priority-14 object on a full control-class-2 picture remain visible but unable to move from (20,80) to (50,80).
0x41set_object_bit_08000x7e32Sets bit 0x0800 in object word field [+0x25]. QEMU validates that this makes a priority-14 object on a full control-class-3 picture remain visible but unable to move from (20,80) to (50,80).
0x42clear_object_bits_09000x7e57Clears object bits 0x0100 and 0x0800 by ANDing [+0x25] with 0xf6ff. QEMU validates that clearing after 0x40 or 0x41 restores movement to (50,80) on the same synthetic control-class pictures.
0x43set_object_bit_02000x479fSets bit 0x0200 in object word field [+0x25].
0x44clear_object_bit_02000x47c7Clears bit 0x0200 in object word field [+0x25].
0x45object_distance_to_var0x47efReads two object indices and a destination variable index. If either object lacks bit 0x0001, stores 0xff in the destination variable. Otherwise computes abs(y0-y1) + abs((x0 + width0/2) - (x1 + width1/2)), caps the result at 0xfe, and stores it in the destination variable.
0x46clear_object_bit_00200x6c97Clears object bit 0x0020 in [+0x25].
0x47set_object_bit_00200x6cbcSets object bit 0x0020 in [+0x25].
0x48set_object_field_23_mode00x6b82Sets object byte [+0x23] = 0 and sets bit 0x0020 in [+0x25].
0x49set_object_field_23_mode10x6baeSets object byte [+0x23] = 1, sets bits 0x1030 in [+0x25], stores immediate arg1 in byte [+0x27], and clears flag arg1.
0x4aset_object_field_23_mode30x6bebSets object byte [+0x23] = 3 and sets bit 0x0020 in [+0x25].
0x4bset_object_field_23_mode20x6c17Sets object byte [+0x23] = 2, sets bits 0x1030 in [+0x25], stores immediate arg1 in byte [+0x27], and clears flag arg1.
0x4cset_object_field_1f_var0x6c54Stores var[arg1] in object byte [+0x1f] and copies the same byte to object byte [+0x20]. These bytes are the reload and current countdown for code.object.frame_timer_update.
0x4dclear_object_fields_21_220x6ec8Clears object bytes [+0x21] and [+0x22]; if the object is the first object entry, also clears byte DS:0x000f and word [0x0139].
0x4eclear_object_field_22_and_global0x6f05Clears object byte [+0x22]; if the object is the first object entry, clears byte DS:0x000f and sets word [0x0139] = 1. A QEMU movement probe validates the byte [+0x22] effect by starting random motion with 0x54, immediately executing 0x4e, and observing that the object remains at its starting position.
0x4fset_object_field_1e_var0x6f3eStores var[arg1] in object byte [+0x1e].
0x50set_object_field_01_var0x6f7cStores var[arg1] in object byte [+0x01] and clears object byte [+0x00].

QEMU batch object_root_partition_004 validates the visible behavior of 0x3a, 0x3b, and 0x3c: clearing bit 0x0010 moves an active object into the root 0x1703 partition drawn before root 0x16ff; setting the bit moves it back into the later-drawn 0x16ff partition; and 0x3c refreshes the lists without an object-local field write.

Static analysis of code.object.frame_timer_update (0x0563) ties bit 0x2000 to automatic group selection. The helper initializes a local target group to sentinel value 4. If bit 0x2000 is clear and object byte +0x0b is 2 or 3, it indexes data.object.group_for_direction_two_or_three_groups (AGIDATA.OVL:0x08dd) by direction byte +0x21; if +0x0b >= 4, it instead indexes data.object.group_for_direction_four_plus_groups (AGIDATA.OVL:0x08e7). When object byte +0x01 == 1, the target group is not sentinel 4, and the target differs from byte +0x0a, it calls code.object.select_group (0x3bb7). QEMU batches object_bit_2000_002 and object_bit_2000_004 validate this behavior:

Gold Rush / AGI v3 keeps the same table values but changes the high group-count branch. Its code.object.frame_timer_update at image 0x055c sends exactly four groups directly through the four-plus table, while views with more than four groups use that table only when flag 0x14 is set. QEMU report build/gr-v3-behavior/frame_selection_gate_qemu_001.json validates exact-four view 177 selecting group 1 for direction 6 whether flag 0x14 is clear or set, and more-than-four view 39 selecting group 1 only when flag 0x14 is set.

  • In the four-plus-groups table, view 4 with direction 6 changes from group 0 to group 1 after 0x2e; after 0x2d, the same object remains on group 0.
  • In the two/three-groups table, view 5 with direction 6 changes from group 0 to group 1.
  • Direction 5 in the two/three-groups table yields sentinel 4, so no group change occurs.
  • A one-shot +0x01 = 2 delays selection until the countdown reaches 1 in a later cycle. A probe that writes +0x01 = 2 every logic cycle keeps the object on its original group, confirming the exact +0x01 == 1 gate.

QEMU probes validate the observable flag-clearing side effect of 0x49 and 0x4b: each clears its flag operand before following bytecode tests that the flag is no longer set. Later frame-timer movement probes validate visible frame-mode behavior for 0x48, 0x4a, and 0x4b.

Static analysis now ties 0x46..0x4c to the frame-cycling path. Per-cycle helper code.object.frame_timer_update (0x0563) scans active objects with (flags & 0x0051) == 0x0051; when bit 0x0020 is set, byte +0x20 is nonzero, and decrementing +0x20 reaches zero, it calls code.object.advance_frame_by_mode (0x48b3) and reloads +0x20 from +0x1f. The advance helper treats +0x23 as a frame mode: mode 0 loops forward, mode 1 advances forward and completes at the last frame, mode 2 steps backward until it reaches frame 0 and completes, and mode 3 loops backward. Completion in modes 1 and 2 sets flag +0x27, clears bit 0x0020, clears direction byte +0x21, and resets +0x23 to 0. Actions 0x49 and 0x4b also set bit 0x1000; the first advance callback after setup clears that bit and returns without changing frame.

QEMU movement batch frame_timer_001 validates the visible mode-1 path: after 0x4c seeds the interval and 0x49 starts mode 1, view 11/group 0 advances from frame 0 to frame 1. The same batch validates that 0x46 prevents the advance by clearing bit 0x0020, and 0x47 restores the advance when executed after 0x46.

QEMU movement batch frame_timer_modes_002 validates the remaining visible frame-mode actions: 0x48 mode 0 wraps view 11/group 0 from frame 1 to frame 0, 0x4b mode 2 moves from frame 1 to frame 0 and stops, and 0x4a mode 3 wraps backward from frame 0 to frame 1. The looping-mode fixtures stop the timer with 0x46 after a bytecode guard observes the expected frame through 0x32.

Movement probe move_collision_clear_skip_bit_blocks_again validates 0x44 by first setting collision-skip bit 0x0200 with 0x43, then clearing it with 0x44, and observing that the default object-object collision stop returns.

QEMU horizon probes validate 0x3f, 0x3d, and 0x3e together. With 0x3f setting [0x012d] = 100, ordinary placement at baseline 80 clamps to baseline 101. Setting bit 0x0008 with 0x3d keeps the object at baseline 80, and clearing the bit with 0x3e restores the clamp.

Movement-mode actions have compact setup contracts but richer per-cycle semantics, so the entry points are summarized in a table and the state machines are described below it.

OpcodeLabelHandlerSetup contract
0x51move_object_to0x6ce4Starts targeted object movement. Operand 0 is the object index, operand 1 is target X, operand 2 is target Y, operand 3 is an optional step-size override, and operand 4 is the completion flag.
0x52move_object_to_var0x6d61Same contract as 0x51, but target X, target Y, and optional step-size override are read through variables while the object index and completion flag remain immediate operands.
0x53approach_first_object_until_near0x6e02Starts autonomous movement for object operand 0 toward the first object entry. Operand 1 is a proximity threshold or step floor, and operand 2 is a completion flag.
0x54start_random_motion0x6e68Starts autonomous random movement for object operand 0. If the object is the first object entry, sets [0x0139] = 0; then stores [+0x22] = 1 and sets object bit 0x0010.

Actions 0x51 and 0x52 share the same targeted-movement state machine.

The handler sets object byte [+0x22] = 3, stores target X/Y in [+0x27] and [+0x28], copies old step size [+0x1e] to [+0x29], stores the completion flag in [+0x2a], clears that flag, sets object bit 0x0010, and calls helper 0x1672. If operand 3 is nonzero, it temporarily replaces [+0x1e] with that step size; completion helper 0x16b9 restores [+0x1e] from [+0x29], sets the completion flag through 0x74c6, clears [+0x22], and clears ego direction byte [0x000f] when the object is object 0.

Helper 0x1672 computes the initial direction byte [+0x21] from current position [+0x03]/[+0x05] to target [+0x27]/[+0x28] using step size [+0x1e]. The direction lookup table at DS:0x0a85 is:

target above:  8 1 2
target level:  7 0 3
target below:  6 5 4
               left near right

The center value is zero, so an object already within one step of both target coordinates completes immediately.

QEMU probes with generated logic resources confirm two valid ways this target mode can complete. A script can reissue 0x51 each cycle while the completion flag is clear; that matched QEMU for reachable horizontal and vertical targets, and for unreachable right/bottom targets that complete at the movement clamp. A one-shot 0x51 setup can also complete without script reissue when object byte +0x01 is set to 1, because the pre-movement dispatcher path 0x067a -> 0x1672 recomputes the direction and detects arrival.

For action 0x53, the setup handler sets object byte [+0x22] = 2; stores max(operand1, object[+0x1e]) in [+0x27]; stores the completion flag number in [+0x28]; clears that flag; stores sentinel 0xff in [+0x29]; and sets object bit 0x0010. The per-cycle helper at 0x0b36 computes the object’s horizontal center and the first object’s horizontal center, calls direction helper 0x16ed(object_center_x, object_y, first_object_center_x, first_object_y, object[+0x27]), and writes the returned direction to [+0x21] when its local delay permits. If the returned direction is zero, the object is already within the threshold: the helper clears [+0x21] and [+0x22] and sets the completion flag through 0x74c6. If object bit 0x4000 says the object did not move in the last position comparison, the helper temporarily chooses a random nonzero direction and computes a retry delay in [+0x29] before returning to the direct approach direction.

QEMU validates the direct autonomous mode-2 path with object 1 approaching object 0: with step 5, countdown byte +0x01 = 1, and threshold 35, object 1 stops at (50,80) when object 0 is at (80,80). An exploratory threshold 25 probe landed at (60,75), consistent with the source path that enters stuck recovery after a blocked or stationary comparison.

For action 0x54, the per-cycle helper at 0x3f5a decrements [+0x27] as a countdown; when it reaches zero, or when object bit 0x4000 reports no movement, it chooses a random direction 0..8 via helper 0x3fa3, stores it in [+0x21], mirrors it to byte [0x000f] for the first object, and reseeds [+0x27] to a random delay in the range 6..50.

A QEMU property probe validates this path without assuming a deterministic final coordinate: after setting step 5, countdown byte +0x01 = 1, and starting 0x54, the object rendered exactly at a valid final position. In the recorded run the final position was (140,112).

OpcodeLabelHandlerObserved action
0x55stop_motion_mode0x6ea1Stops the autonomous motion mode for object operand 0 by clearing object byte [+0x22]. Unlike 0x4d (clear_object_fields_21_22) it does not clear direction byte [+0x21], and unlike 0x4e (clear_object_field_22_and_global) it does not update the first-object globals.
0x56set_object_field_21_var0x6fbeStores var[arg1] in object byte [+0x21].
0x57get_object_field_210x6ffcStores object byte [+0x21] into var[arg1].
0x58set_object_bit_00020x7b9cSets bit 0x0002 in object word field [+0x25]. QEMU validates that this bypasses the rectangle-boundary crossing stop configured by 0x5a; the object reaches (50,80) instead of stopping at (30,80).
0x59clear_object_bit_00020x7bc1Clears bit 0x0002 in object word field [+0x25]. QEMU validates that clearing after 0x58 restores the rectangle-boundary crossing stop at (30,80).
0x83clear_global_01390x702fSets global word [0x0139] = 0. Source-backed main-cycle analysis shows this selects the next pre-logic mirror direction: object 0 byte +0x21 is copied to global byte [0x000f] when the selector is zero. The same cycle later restores object 0 +0x21 from [0x000f] after logic returns, so script-level probes that set object 0 direction after the pre-logic mirror are clobbered before they can seed the next cycle.
0x84set_global_0139_and_clear_object0_field_220x7041Sets global word [0x0139] = 1 and clears byte [+0x22] on the first object entry. A QEMU movement probe validates the byte [+0x22] effect by starting random motion on object 0 with 0x54, immediately executing 0x84, and observing that the object remains at its starting position.
0x85display_object_diagnostics_var0x72b5Reads an object index from var[arg0], gathers object fields [+0x03], [+0x05], [+0x1a], [+0x1c], [+0x24], and [+0x1e], formats them with string template DS:0x1713 through helper 0x2374, and displays the result through 0x1ce8.

In local SQ2 data, the template at DS:0x1713 is:

Object %d:
x: %d  xsize: %d
y: %d  ysize: %d
pri: %d
stepsize: %d

The single observed local use is in logic 99. The script accepts parsed words object or sp, prompts with message 7 (object #:), stores the number in variable 64, and then executes this action. This makes 0x85 a developer or diagnostic object-inspection command, and it independently confirms that object fields [+0x03], [+0x05], [+0x1a], [+0x1c], [+0x24], and [+0x1e] are displayed as X, Y, width, height, priority/control, and step size.

OpcodeLabelHandlerObserved action
0x93set_object_pos_dirty0x7d77Sets object fields [+0x03] = arg1 and [+0x05] = arg2, sets bit 0x0400 in [+0x25], then calls helper 0x593a.
0x94set_object_pos_dirty_var0x7dbaSame broad shape as 0x93, but fields [+0x03] and [+0x05] are loaded from var[arg1] and var[arg2]. It also sets object bit 0x0400 and calls helper 0x593a.

Actions that update object byte [+0x24]:

OpcodeLabelHandlerObserved action
0x36set_object_field_240x7a80Sets bit 0x0004 in object word field [+0x25] and writes immediate arg1 to byte [+0x24].
0x37set_object_field_24_var0x7b08Same as 0x36, but writes var[arg1] to byte [+0x24].
0x38clear_object_bit_00040x7ab0Clears bit 0x0004 in object word field [+0x25]. A QEMU probe confirms that this stops using the fixed byte [+0x24]; a view placed at baseline 80 then derives priority 7 and draws over a control-6 background.
0x39get_object_field_240x7ad5Stores object byte [+0x24] into var[arg1].

Message-display actions use the current logic resource’s message table through helper 0x21f0.

OpcodeLabelHandlerObserved action
0x65display_message0x1c06Resolve message arg0 with 0x21f0, then pass the string pointer to display helper 0x1ce8.
0x66display_message_var0x1c29Same as 0x65, but message number is var[arg0].
0x67display_formatted_message0x2250Calls setup helper 0x2b28, passes immediate operands arg0 and arg1 to helper 0x2b0d, resolves message arg2, copies or formats it into a 1000-byte stack buffer with helper 0x1f54 and maximum 0x28, sends the resulting string to formatter/display helper 0x2390, then calls cleanup helper 0x2b4f. This appears to be a positioned or configured formatted-message display action.
0x68display_formatted_message_var0x22a7Same as 0x67, but all three operands are read through variables.
0x97display_message_configured0x1c54Reads immediate message number arg0, then reads row, column, and width bytes into globals [0x0d0b], [0x0d0d], and [0x0d09] before resolving and displaying the message. A zero width is replaced with 0x1e before display. The globals are reset to 0xffff afterward.
0x98display_message_configured_var0x1c71Same as 0x97, but message number is var[arg0].

QEMU fixture text_input_002 validates that 0x65, 0x66, and 0x97 display a message/window, accept Enter through the display helper, then return to following bytecode. The same batch validates one typed-input path for 0x76: entering 42 stores byte value 42 in the destination variable. Later fixture text_ui_003 validates the formatted/positioned display variants 0x67, 0x68, and 0x98 with the same return-to-bytecode behavior. These message paths can leave text-plane pixels visible after dismissal; the QEMU fixtures issue a full picture refresh (0x1a) before comparing graphics output when the validation draw would otherwise be contaminated by that display state.

Menu/list-like UI actions use a circular list rooted at word globals [0x1d2c], [0x1d2e], and [0x1d30]. The final user-level names remain provisional; the observed structures look like a top-level list of headings with per-heading circular sublists of selectable items.

OpcodeLabelHandlerObserved action
0x9cadd_menu_heading_like0x911dResolves message arg0, allocates an 18-byte node through 0x13d6, links it into the top-level circular list rooted at [0x1d2c], stores the message pointer at [node+0x04], stores the current horizontal/text position from [0x1d24] at [node+0x08], marks [node+0x0a] = 1, clears its item-list pointer at [node+0x0c], advances [0x1d24] by the message width plus one using helper 0x4d99, clears [0x1d30], and resets [0x1d26] = 1.
0x9dadd_menu_item_like0x91cfResolves message arg0, reads immediate item id arg1, allocates a 14-byte node, links it into the current heading’s circular item list at [current_heading+0x0c], stores the message pointer at [node+0x04], stores a 1-based row/order at [node+0x06], stores a column-like value at [node+0x08], marks [node+0x0a] = 1, stores item id arg1 at [node+0x0c], and increments [current_heading+0x10].
0x9efinalize_menu_like0x92baFinalizes the current menu/list setup: if the current heading has no item list, clears [heading+0x0a]; calls helper 0x1476; sets current heading [0x1d2e] to the list root [0x1d2c]; sets current item [0x1d30] from [heading+0x0c]; and sets [0x1d2a] = 1, which makes later 0x9c/0x9d additions no-op.
0x9fenable_menu_item_like0x92eeCalls helper 0x935f(arg0, 1). The helper walks all active heading/item lists, finds item nodes whose id at [node+0x0c] equals arg0, and writes [node+0x0a] = 1.
0xa0disable_menu_item_like0x9340Calls helper 0x935f(arg0, 0), clearing [node+0x0a] for matching item ids.
0xa1mark_menu_if_flag_0e0x93b1Tests flag 0x0e through helper 0x74e4; if set, stores word [0x1d22] = 1. Its role is likely tied to the same menu/list UI state, but no direct list traversal occurs in this handler.

QEMU fixture menu_sound_001 dispatch-smokes 0x9c..0xa0 by creating a heading, adding one item, finalizing the structure, disabling and re-enabling that item id, and then drawing a known view. A separate case sets flag 0x0e, runs 0xa1, and draws.

QEMU fixture menu_interaction_001 validates the one-item interactive Enter path. The fixture builds a menu heading and enabled item id 7, finalizes the structure, sets flag 0x0e, and runs 0xa1. The following input cycle enters code.menu.interact (0x93d1); pressing Enter on the enabled item calls 0x44a9(3, 7). The generated logic then draws only when condition 0x0c 7 observes the resulting status byte. The comparison refreshes the picture before the validation draw because the menu leaves a top text strip visible.

QEMU fixture menu_edges_002 validates three additional menu edge cases. Escape exits the interactive menu without setting item status byte 7. Enter on a disabled item also leaves status byte 7 clear before Escape exits. Disabling then re-enabling the same item allows Enter to enqueue status byte 7 again. These promote 0x9c..0xa0 from setup-only dispatch evidence to behavior evidence for the tested setup, disable, and re-enable paths. A down-arrow navigation fixture was attempted twice, but the QEMU key sequence did not reach the expected second-item status path, so arrow-key navigation remains source-backed from the code.menu.interact dispatch table.

Source mapping of code.menu.interact shows the navigation model. The menu loop waits through code.input.wait_for_event_or_tick, then normalizes the event record through the Enter/Escape helper and the display-adapter remap helper. Event type 1 is interpreted as raw Enter/Escape: Enter enqueues a type-3 event with the current item’s id only if the item enable word [item+0x0a] is nonzero; Escape leaves without enqueueing a selection. Event type 2 is interpreted as a movement code. Movement values 1..8 dispatch through the word table at 0x9526, then the loop stores the current heading/item in [0x1d2e] and [0x1d30] before waiting again. Item navigation does not skip disabled items; the enable word is tested when Enter is pressed. Left/right heading navigation skips headings whose enable word is zero, while Home/End-like heading jumps select the root or root-previous heading directly.

Movement valueRaw key table sourceTargetSource-backed effect
10x48000x9492Move to previous item in the current heading ([item+0x02]).
20x49000x94a6Move to the first item in the current heading ([heading+0x0c]).
30x4d000x94b2Move to the next enabled heading ([heading+0x00] loop), restoring that heading’s remembered item from [heading+0x0e].
40x51000x94cbMove to the last item in the current heading ([first_item+0x02]).
50x50000x94daMove to the next item in the current heading ([item+0x00]).
60x4f000x94e5Move to the root list’s previous heading ([root+0x02]) and restore its remembered item.
70x4b000x94f6Move to the previous enabled heading ([heading+0x02] loop), restoring that heading’s remembered item.
80x47000x9509Move to the root heading ([0x1d2c]) and restore its remembered item.

Text-window and input-line actions:

OpcodeLabelHandlerObserved action
0x69clear_text_rect0x7714Reads immediates arg0, arg1, and arg2; transforms arg2 through helper 0x78ad; then calls code.text.clear_rows (0x2b78) with top row arg0, bottom row arg1, left column 0, right column 0x27, and the transformed attribute. QEMU validates that rows 5..6 clear logical Y 40..55 to visual color 0 without repainting the picture.
0x9aclear_text_rect_bounds0x7753Reads five immediates. The first four are passed as top row, left column, bottom row, and right column to code.text.clear_bounds (0x2bc4); the fifth is transformed through helper 0x78ad and passed as the text attribute. Helper 0x2bc4 saves the current cursor, calls BIOS int 10h scroll/clear-window service AH=0x06 with those bounds, then restores the cursor. In the EGA target, QEMU validates text columns as four logical pixels wide and text rows as eight logical pixels tall.
0x6aenable_text_attr_mode_17570x76caCalls prompt/input cleanup helper 0x382e, sets byte [0x1757] = 1, derives text attributes through 0x77d5 using globals [0x05cd] and [0x05cf], calls helper 0x9803, then clears or fills a text rectangle through 0x2b78. QEMU validates that in the normal EGA path this leaves the visible logical surface cleared to black, and a following transient-object validation draw is not visible while this alternate text-attribute mode remains active.
0x6bdisable_text_attr_mode_17570x7702Calls prompt/input cleanup helper 0x382e, then helper 0x78cb, which clears byte [0x1757], recomputes text attributes from [0x05cd] and [0x05cf], calls helper 0x9806, redraws the status-line-like area through 0x34bd, and refreshes the input-line-like area through 0x38d7. QEMU validates that after 0x6a, running 0x6b and then refreshing the picture allows the normal transient-object validation draw to appear again.
0x6cset_input_prompt_char0x38b4Resolves current-logic message arg0 through 0x21f0 and stores the first byte of that message in [0x05d7]. Helpers 0x37f7, 0x382e, and 0x38d7 test [0x05d7] while drawing or erasing the prompt/input marker. QEMU validates the empty-message case by first setting a nonempty marker, then setting an empty message; a following input-line redraw stays black with no prompt marker glyph.
0x6dset_text_window_pair0x77afReads immediates arg0 and arg1, then calls helper 0x77d5(arg0, arg1). That helper stores derived values in globals [0x05d1], [0x05cd], and [0x05cf] using helpers 0x7803, 0x78a1, and 0x78ad. In the observed EGA path, setting pair (0, 1) while normal text mode is active stores [0x05cd]=0 and [0x05cf]=0xff; a following 0x6a recomputes [0x05d1] with [0x1757]=1, producing a packed low-byte text attribute 0xf0. QEMU validates the resulting alternate text-surface clear as full-screen visual color 15.
0x6eshake_screen_like0x7a00Reads an immediate count into CL and performs a display-shake-like loop. Display mode [0x1130] == 3, 2, or 4 delegates to overlay/helper paths 0x99b8, 0x9be3, or 0x9916. Otherwise it chooses base byte [0x1779] = 0x70 when hardware selector [0x112e] == 0, or 0x38 otherwise, then repeatedly writes CRT controller registers 0x02 and 0x07 through ports 0x3d4/0x3d5. Each step reads bytes from the small table at 0x177a, adds [0x1365] to the register-2 value and [0x1779] to the register-7 value, waits for timer word [0x0129] to change, advances until the register-7 value returns to the base, and repeats for the count. QEMU dispatch-smokes the action returning to following bytecode; the transient hardware timing effect is source-backed.
0x6fset_input_line_config0x78f0Reads immediates arg0, arg1, and arg2; stores arg0 in [0x05dd], arg0 + 0x15 in [0x05df], arg1 in [0x05d5], and arg2 in [0x05db]. It also computes [0x1379] from arg0: normally arg0 << 3, but in display mode [0x1130] == 2 it stores arg0 * 6 for values 0 or 1, and clamps larger values to 6. Nearby redraw helpers use these globals for the input-line/status text areas, so the user-level name remains provisional.
0x70show_status_line_like0x3547Sets word [0x05d9] = 1 and calls helper 0x34bd, which redraws a status-line-like text area using helpers 0x2b28, 0x7989, 0x2ba6, 0x2b0d, 0x2390, 0x79c3, and 0x2b4f. QEMU validates that after 0x6f(0, 0, 5), showing the status line writes visible color-15 pixels in logical row 40..47, the row configured by operand 2.
0x71hide_status_line_like0x355cSets word [0x05d9] = 0, then calls code.text.clear_row (0x2ba6) with row [0x05db] and attribute 0, clearing the associated text row. QEMU validates that after 0x6f(0, 0, 5), hiding the status line clears logical Y 40..47 to visual color 0 without repainting the picture.
0x72set_string_slot_from_message0x0d37Computes destination 0x020d + arg0 * 0x28, resolves current-logic message arg1, and copies up to 0x28 bytes into that fixed-size string slot through helper 0x4de8.
0x73prompt_string_to_slot0x0c44Reads fixed string slot arg0, message number arg1, row-like byte arg2, column-like byte arg3, and max-length byte arg4. It clears the destination string slot, optionally positions text with 0x2b0d(arg2, arg3) when arg2 < 0x19, displays the resolved current-logic message, accepts edited input through code.input.edit_string (0x0da9), then restores the input-line/status display as needed. The accepted length is min(arg4 + 1, 0x28).
0x74set_string_slot_from_table0x0d70Computes destination 0x020d + arg0 * 0x28, reads a word pointer from DS:0x0c8f + arg1 * 2, and copies up to 0x28 bytes from that pointer into the string slot through helper 0x4de8. In the static SQ2 AGIDATA.OVL, the sampled table entries at 0x0c8f are zero-filled and this opcode was not encountered in the current local logic scan. QEMU fixture input_key_string_behaviour_001 validates the copy semantics by patching only the generated fixture’s AGIDATA.OVL so table entry 0 points to a synthetic look string.
0x75parse_string_slot0x1958Clears flags 2 and 4, reads a string-slot index arg0, and if arg0 < 12 parses fixed string slot 0x020d + arg0 * 0x28 through helper 0x18ac. The parser normalizes the string, looks words up in WORDS.TOK, and fills parsed-word tables used by condition 0x0e (input_word_sequence).
0x76prompt_number_to_var0x71edDisplays current-logic message arg0 as a prompt, accepts/edits up to four characters through helper 0x0da9, parses the resulting buffer as a decimal number through helper 0x4e8d, and stores the low byte in var[arg1]. It has two display paths: one using text helpers 0x2b0d, 0x1f54, 0x2390, 0x37f7, and 0x38d7, and another using helpers 0x9c52 and 0x9d93 when display mode [0x1130] == 2 and [0x0d0f] == 0.
0x77disable_input_line_like0x386fSets word [0x05d3] = 0; unless display mode [0x1130] == 2, it calls helper 0x382e and clears a text area through code.text.clear_row (0x2ba6) with row [0x05d5] and attribute 0. QEMU validates that after 0x6f(0, 5, 22), disabling the input line clears logical Y 40..47 to visual color 0 without repainting the picture.
0x78enable_input_line_like0x3898Sets word [0x05d3] = 1; unless display mode [0x1130] == 2, calls helper 0x38d7 to redraw the input-line-like display. QEMU validates that after 0x6f(0, 5, 22), enabling the input line clears/redraws logical Y 40..47 to visual color 0 when the prompt marker and input buffers are empty.
0x89refresh_input_line0x3753If the input line is enabled through word [0x05d3], refreshes the input-line display. In display mode [0x1130] == 2 with word [0x0d0f] == 0, it displays the string at 0x1e2e (“ENTER COMMAND”) through the alternate display helpers, clears or rewrites the current input character byte [0x001c], and passes that byte to helper 0x3652. In other modes it calls helper 0x37a5, which appends bytes from the buffer/string at 0x0fce into the visible input buffer 0x0fa4 until the input length word [0x0ff8] reaches that string’s length. QEMU validates the normal EGA path by typing look plus Enter, then observing that 0x89 repaints visible input glyph pixels from the entered source buffer. A failed pre-Enter probe showed that unaccepted live edit characters are not sufficient for this replay path.
0x8aerase_input_line0x3726Erases the visible input-line buffer by repeatedly passing byte 0x08 to helper 0x3652 while word [0x0ff8] is nonzero. In display mode [0x1130] == 2, it skips the erase loop when word [0x0d0f] == 0. QEMU validates the normal EGA path by first confirming typed characters are visible on the configured input row, then running 0x8a each cycle and matching logical Y 40..47 back to black.
0xa3set_global_0d0f0x3939Sets word [0x0d0f] = 1. Helper 0x3652 uses this global while computing input-line width: when set, it starts from a fixed 0x24 character cap instead of deriving the cap from fixed string slot 0. QEMU validates this by using a long blank string slot 0 and observing accepted live-input glyphs on the wrapped input row only after 0xa3.
0xa4clear_global_0d0f0x394bClears word [0x0d0f]. QEMU validates the visible counterpart to 0xa3: after 0xa3 followed by 0xa4, the same long blank string slot 0 leaves the wrapped input row without live-input glyph pixels.
0xa9close_text_window_state0x1f2bIf word [0x0d1d] is nonzero, calls helper 0x560c([0x0d23], [0x0d25]), which restores the display rectangle saved by the modal message-window opener at 0x1d96. That opener closes any prior active window, computes packed rectangle words [0x0d23] and [0x0d25], calls helper 0x5590 to save/fill/draw the window region, and only then sets [0x0d1d] = 1. After the conditional restore, 0xa9 clears words [0x0d0f] and [0x0d1d]. QEMU validates the unconditional [0x0d0f] clear by setting 0xa3, running 0xa9 with no active saved window, and observing the same narrowed input-width behavior as 0xa4; the active restore lifecycle is source-backed from the producer/consumer code.

code.input.edit_string (0x0da9) is the shared line editor used by both 0x73 and 0x76. Static disassembly shows that it clamps the requested maximum length to 0x28, copies the destination buffer into a local edit buffer, draws the existing text, waits for a nonzero/non-0xffff event through code.input.wait_event (0x45d7), and dispatches key values through data.input.edit_key_table (0x0e64). Observed table entries map 0x08 to single-character backspace, 0x03 and 0x18 to clear-current-input, 0x0d to accept by zero-terminating the local buffer and copying it back to the destination slot, and 0x1b to cancel without copying. Other input bytes append when space remains and are echoed through the character display helper.

QEMU fixture prompt_string_003 validates the 0x73 path. One case proves the prompt returns after typed text plus Enter; a second initializes string slot 1 from message text look, runs 0x73 into slot 0, then conditionally draws only if the two string slots compare equal. The matched capture proves that typed text was copied into the destination slot. Like the formatted-message probes, the fixture refreshes the picture with 0x1a before the validation draw because the text overlay can otherwise remain visible in the captured display.

QEMU fixture text_ui_003 originally dispatch-smoked 0x77, 0x78, 0x89, and 0x8a as an input-line toggle/refresh/erase group, and 0xa9 as text-window cleanup. Later focused batches promote the input-line opcodes and the unconditional [0x0d0f] clear side of 0xa9 to the behavior-level observations above. Follow-up fixture text_rect_clear_behaviour_003 validates the display-surface effect of 0x69 and 0x9a without relying on a picture refresh: formatted message text is acknowledged, the clear action runs, and the captured screen matches only when the expected display surface includes the black text-cell rectangle written by the BIOS clear helper.

QEMU fixture text_hide_clear_behaviour_001 extends that display-surface coverage to the single-row text clear used by the status and input line handlers. The two cases first display formatted text on row 5, configure the row globals through 0x6f, run either 0x71 or 0x77, and then validate that logical Y 40..47 is black before the final object draw. This promotes 0x71 (hide_status_line_like) and 0x77 (disable_input_line_like) beyond dispatch-smoke coverage for the normal EGA display path.

QEMU fixture text_enable_attr_behaviour_002 promotes two more handlers from that cluster. Case input_line_enable_clears_configured_row sets the prompt marker to an empty message, displays formatted text on row 5, configures the input row with 0x6f(0, 5, 22), then runs 0x78; the capture matches when the configured row is black before the final object draw. Case text_attribute_enable_clears_visible_surface runs 0x6a and then attempts the usual transient-object validation draw; the original capture remains a fully black logical surface, so the case compares only the visible surface and does not compose the validation object. This is evidence for the alternate text-attribute mode surface clear, not for ordinary graphics drawing.

QEMU fixture text_prompt_attr_behaviour_001 promotes two neighboring handlers. Case text_attribute_disable_restores_picture_draw runs 0x6a, then 0x6b, then refreshes the picture and draws the validation object; the match confirms that leaving alternate text-attribute mode restores the normal graphics path. Case input_prompt_empty_message_suppresses_marker first sets a nonempty prompt marker, displays/acknowledges formatted text on row 5, sets the prompt marker from an empty message, and runs 0x78; the capture matches only when the configured input row is black with no prompt-marker glyph.

QEMU fixture text_status_002 originally dispatch-smoked low-risk text/status/input handlers in this cluster: 0x6d for text-attribute configuration; 0x6e for a one-count screen-shake return; 0x70 for status-line show; and 0x79 for key-event mapping table insertion. Later focused batches promote 0x6d and 0x70 to the behavior-level observations above. The source-backed details still matter: 0x6f stores its first operand in [0x05dd], stores operand + 0x15 in [0x05df], and derives display offset global [0x1379] from that first operand. An intermediate QEMU run with first operand 1 shifted the later validation draw relative to the local renderer, so the final smoke fixture uses operand 0 and leaves non-default display-offset semantics for a dedicated behavior probe.

QEMU fixture input_key_string_behaviour_001 adds that dedicated behavior coverage. Case input_line_config_operand1_offsets_display_by_8 runs 0x6f(1, 0, 22), refreshes the picture, and draws a view at script baseline 80; the original interpreter capture matches the local renderer only when the expected baseline is 88, confirming the observed arg0 << 3 offset in normal display mode. Case mapped_key_sets_status_byte installs 0x79('x', 0, 7), sends key x, and draws only when condition 0x0c observes status byte [0x1218 + 7]. This confirms the path through code.input.map_key_event (0x4c3d) and helper 0x4566, where a matching type-1 event becomes type 3 and sets the mapped status byte.

QEMU fixture diagnostics_system_001 validates that 0x87, 0x88, and 0x8d display their diagnostic/pause/version messages, accept Enter, and return to following bytecode. The same batch originally smoke-exercised several global/system handlers. Separate focused or source-backed work now covers the formerly smoke-only actions: 0xab/0xac, 0xa3/0xa4, and the object-0 motion-byte effect of 0x84 have focused QEMU evidence; 0x83, 0x8e, 0xaa, and 0xad are covered from source because their main effects are global/timing/save-selector state not cleanly exposed by ordinary single-room script probes.

Resource/table actions outside the main object table:

OpcodeLabelHandlerObserved action
0x5aset_rect_bounds_01310x7b4eSets word [0x013d] = 1 and stores four immediate operands as words at [0x0131], [0x0133], [0x0135], and [0x0137]. Helper 0x7be6 later tests whether an (x,y) pair lies strictly inside those bounds. QEMU movement probes validate that these bounds stop an object before it crosses the configured rectangle.
0x5bclear_rect_bounds_01310x7b8aClears word [0x013d]. QEMU movement probe rect_bounds_clear_001 validates that clearing after 0x5a lets the same object cross the former rectangle boundary and reach its target.
0x5cset_entry_0971_marker_ff0x7538Resolves a 3-byte entry from the table rooted at [0x0971] using immediate index arg0, validates it against end pointer [0x0973], and stores byte [entry+0x02] = 0xff. Invalid indices report error code 0x17 through helper 0x3fe8.
0x5dset_entry_0971_marker_ff_var0x7554Same as 0x5c, but the entry index is read from var[arg0].
0x5eclear_entry_0971_marker0x7570Resolves a 3-byte entry from the table rooted at [0x0971] using immediate index arg0 and stores byte [entry+0x02] = 0.
0x5fset_entry_0971_marker_from_var0x758cResolves a table entry using immediate index arg0, then stores var[arg1] in byte [entry+0x02].
0x60set_entry_0971_marker_from_var_var0x75b7Resolves a table entry using index var[arg0], then stores var[arg1] in byte [entry+0x02].
0x61get_entry_0971_marker_to_var0x75e2Resolves a table entry using index var[arg0], then stores byte [entry+0x02] into var[arg1].
0x7cshow_inventory_selection0x31d8Builds a temporary 8-byte-per-row list from the 3-byte table rooted at [0x0971], including only entries whose marker byte [entry+0x02] is 0xff. For each included entry it stores the original table index, an item-name pointer computed as [0x0971] + word[entry+0x00], and a two-column row/column position. It displays the header string at 0x0f26 (“You are carrying:”), a fallback string at 0x0f1e (“nothing”) when no rows exist, and either a selection prompt at 0x0f38 or a noninteractive return prompt at 0x0f5d depending on flag 13. In interactive mode Enter writes the selected table index to absolute byte DS:0x0022; Escape writes 0xff there. Because script byte variables begin at DS:0x0009, this is exposed to logic bytecode as variable 0x19. QEMU batch inventory_selection_001 validates Enter storing selected carried-entry index 0, Escape storing 0xff, and the noninteractive acknowledgement path returning to following bytecode.
0x79map_key_event0x4c3dCombines arg0 and arg1 into a little-endian key/event word, scans up to 39 four-byte slots rooted at 0x0145 for an empty first word, and stores the combined word at slot offset +0 and arg2 at slot offset +2. Helper 0x4566 later uses this table to convert matching type-1 event records into type-3 records carrying the mapped value.
0x81display_view_resource_text_like0x5ebfDisplays or previews a view-like resource selected by immediate arg0. Helper 0x5edb disables resource-event recording, ensures the resource is loaded, temporarily sets [0x0f18] = 1 around 0x39f7, builds a temporary object-like record through 0x3ae7, may render/cache it through helpers 0x9097, 0x9db0, 0x9db6, and 0x5762 if enough memory is available, displays a string pointer derived from the loaded resource through 0x1ce8, optionally discards a view it loaded only for this display, then re-enables recording.
0xa2display_view_resource_text_like_var0x5e9bSame as 0x81, but the resource number is read from var[arg0]. The action table metadata byte is 0x01, but the handler itself clearly performs the variable lookup. Its helper also disables resource-event recording around the temporary load/display/discard sequence.
0x99discard_view_var0x3ee9Same helper path as 0x20 (discard_view), but the view-like resource number is read from var[arg0].

QEMU fixture view_resource_display_001 validates that 0x81 and 0xa2 display/preview view resource 11, accept Enter, return to following bytecode, and allow a subsequent picture refresh plus validation draw.

Interpreter/session control actions:

OpcodeLabelHandlerObserved action
0x86confirm_and_restart_like0x027fStops active sound state through helper 0x5234, reads immediate arg0, and if arg0 == 1 calls helper 0x02ae immediately. Otherwise it displays string pointer 0x05e3 through 0x1ce8 and calls 0x02ae only if the display helper returns 1. Helper 0x02ae calls 0x8275 and then 0x00ae(0). This is a confirmation-gated restart/exit-like action; the final user-level name is still provisional.
0x87show_heap_status0x14bdFormats a diagnostic heap/status message into a 100-byte stack buffer with helper 0x2374, then displays it with 0x1ce8. The format string at 0x0a19 is heapsize: %u, now: %u max: %u, rm.0, etc.: %u, and max script: %d. The values are derived from globals [0x0a55], [0x0a57], [0x0a59], [0x0a5b], [0x0a5f], and [0x170f].
0x88pause_game_message0x0257Sets word [0x0615] = 1, calls helper 0x4482, stops sound through 0x5234, displays the fixed message at 0x0c0d (“Game paused. Press Enter to continue.”), then clears [0x0615].
0x8bcalibrate_joystick0x613cInitializes joystick-related globals [0x15c5] and [0x15c7] to 0xffff, calls helper 0x63be, and if joystick axes/state globals [0x15c1] and [0x15c3] are nonzero, displays the message at 0x1549, which starts “Please center your joystick.” It waits for Enter to continue or Escape to cancel. On acceptance it closes any active text window, computes min/max centered bounds around [0x15c1] and [0x15c3] into [0x15c9], [0x15cd], [0x15cb], and [0x15cf], then repeatedly calls helper 0x6425 while calibration records at 0x1531 or 0x153d remain active. It finishes by calling helper 0x4482.
0x80confirm_restart_game0x2472Stops active sound state, clears the prompt/input line, and either proceeds immediately if flag 16 is set or displays the confirmation text at 0x0adb (“Press ENTER to restart the game… Press ESC to continue this game.”). On confirmation it calls input/display cleanup helper 0x3726, preserves flag 9, resets heap/update-list state through 0x1485, calls helpers 0x0fa5 and 0x30d6, sets flag 6, restores flag 9 if it had been set, clears timer/event words [0x0129] and [0x012b], reloads logic [0x1d12] if configured, and calls menu/list refresh helper 0x930e. It then redraws the input prompt through 0x37f7. When restart is accepted it returns zero to the dispatcher.
0x7dsave_game_state0x2753Save-game-state path. It marks modal state [0x0615] = 1, temporarily changes byte [0x0d15] to 0x40, asks helper code.save.select_slot_or_path(0x73) for a selected save slot/path, optionally displays the confirmation text at 0x0db6, creates file 0x1c8c through DOS wrapper 0x5cad, writes a 31-byte description/header from 0x1c6c, then writes five length-prefixed memory blocks through helper 0x28c6: 0x05e1 bytes from 0x0002, [0x096f] bytes from [0x096b], [0x0975] bytes from [0x0971], [0x0141] * 2 bytes from [0x1707], and the 0x1364-sized block from 0x0985. Local SQ2 save files confirm block lengths 1505, 903, 328, 200, and a variable fifth block. Create failure displays the directory-full/write-protected text at 0x0df0 and returns to the following bytecode. Header or block write failure closes and deletes the partial file, displays the disk-full text at 0x0e46, restores text state, restores [0x0d15], clears [0x0615], and returns to the following bytecode.
0x7erestore_game_state0x2512Restore-game-state path. It marks modal state [0x0615] = 1, saves and temporarily changes byte [0x0d15], asks helper code.save.select_slot_or_path(0x72) for a selected restore slot/path, optionally displays the confirmation text at 0x0d34, opens file 0x1c8c through DOS wrapper 0x5cce, seeks past a 31-byte description/header, then reads the same five length-prefixed block families into 0x0002, [0x096b], [0x0971], [0x1707], and 0x0985 through helper 0x26b0. Open failure displays the can’t-open-file text at 0x0d73, restores modal/text state, and returns to the following bytecode. Block read failure closes the file, displays the error text at 0x0d87, and calls 0x02ae. On success it restores hardware/display byte variables from [0x112e] and [0x1130], sets flag 11 according to hardware mode, calls code.restore.replay_resource_events, refreshes display/resource state through 0x4c23 and 0x30d6, clears the caller return pointer so execution restarts through the restored state, calls menu/list refresh helper 0x930e, restores text state, restores [0x0d15], and clears [0x0615].
0x8eset_global_0141_and_refresh0x716aStores immediate arg0 as data.event.pair_capacity, then wraps code.event.reset_pair_buffer with update-list flush/rebuild calls 0x6a54 and 0x6a8e. Source-backed reset semantics: if capacity is positive and no pair buffer exists, code.event.reset_pair_buffer allocates capacity * 2 bytes, stores the pointer in data.event.pair_buffer_base, initializes allocator state, resets data.event.pair_buffer_write to the base pointer, and clears data.event.pair_count.
0x8ctoggle_display_mode_bit0x794cIf word [0x112e] == 0, byte variable 0 is nonzero, and display mode word [0x1130] is neither 2 nor 3, calls helper 0x1364, toggles bit 0 of word [0x1130], and refreshes display state through helpers 0x2b28, 0x5528, 0x2b4f, and 0x681c. A QEMU memory probe with SIERRA -p -c and patched guard words confirmed the branch executes and toggles [0x1130] from 0 to 1. The same probe showed replay excludes a picture drawn while flag 7 was set or after 0xab/0xac rollback. Source inspection now indicates the visible row-interleaved background is a CGA-mode remapping of the replayed recorded picture, not the excluded picture surviving. Because the handler requires [0x112e] == 0, this behavior is outside the full 16-color EGA target path.
0x8fverify_game_signature0x0e7eReads immediate message number arg0, resolves that current-logic message through 0x21f0, copies up to seven bytes into absolute buffer 0x0002 with 0x4de8, then calls helper 0x5b49. Helper 0x5b49 compares bytes at 0x0002 against an embedded SQ2\0 string at code offset 0x5b6c, calling helper 0x02ae on the first mismatch. The one observed local use is in logic 140 immediately before action 0x6f (set_input_line_config), consistent with a game-signature/configuration guard.
0x90append_message_to_log_file0x828fReads immediate message number arg0. If global file handle [0x1823] is 0xffff, helper 0x833f opens or creates the file named at 0x1825 (logfile) and seeks it to the end. The handler then appends a formatted room/input-line record using template 0x1809 (Room %d\nInput line: %s\n) with byte variable 0 and string/input buffer 0x0fce, resolves message arg0 through 0x21f0, formats it into the same stack buffer through 0x1f54, appends it, and closes the file handle with 0x5d52. If opening fails, it returns after consuming the operand.
0x91save_logic_resume_ip0x1335Stores the current bytecode pointer SI into word [current_logic+0x06], where current_logic is the record pointed to by [0x0981].
0x92restore_logic_entry_ip0x134aRestores word [current_logic+0x06] from [current_logic+0x04].
0x95enable_action_trace_window0x8c91If word [0x1d10] is nonzero, returns SI + 1, consuming one byte beyond the opcode. Otherwise it calls helper 0x8cae, which starts an action-trace display only when flag 10 is set: it sets [0x1d10] = 1, computes a text-window rectangle from input-line base row [0x05dd], trace offset [0x1d08], and trace height [0x1d0a], stores the derived bounds in [0x1d14], [0x1d16], [0x1d18], [0x1d1a], [0x1d1c], and [0x1d1e], then draws a boxed area through helper 0x5590. QEMU validates the flag-set enabled path by observing the red-bordered, white-filled trace window and black trace text.
0x96configure_action_trace_window0x8d3dReads three immediates into [0x1d12], [0x1d08], and [0x1d0a], clamping [0x1d0a] upward to at least 2. The first value names an optional logic resource used by the action-trace formatter around 0x8e0b; the second and third values control the trace window’s row offset and height. Restart and room-switch paths also reload logic [0x1d12] when it is nonzero, so this configuration participates in both trace display and session reset. QEMU validates 0x96(0,1,2) feeding the enabled trace-window draw path.

QEMU fixture system_dialog_001 validates several control paths in this cluster. 0x8f returns when the resolved message begins with the expected SQ2 signature. 0x80 and 0x86(0) display confirmation prompts and return to following bytecode when Escape cancels. 0x8b follows the no-joystick path under the current QEMU environment and returns. 0x8c returns without toggling when byte variable 0 is zero. Two later display-mode replay cases patch the hardware/mode guard words and launch with SIERRA -p -c; screenshot comparison validates the observable CGA row-interleaved remapping of the recorded picture, while the paired QEMU memory probe validates the internal replay log rather than relying on the screenshot alone. 0x96 followed by 0x95 dispatch-smokes the trace-window configuration path with flag 10 clear. Later fixture trace_window_enable_002 sets flag 10, configures the trace window with 0x96(0, 1, 2), and runs 0x95; the original-engine capture matches bounded checks for a red border, white fill, and black trace text. The trace window then becomes active, so the dispatcher may pause before the next action and format the current opcode/operands into that window.

QEMU fixture file_log_001 validates that 0x7d and 0x7e open their save/restore selector paths and return after Escape cancellation. It also dispatch-smokes 0x90 by appending a formatted record to the DOS logfile and returning to following bytecode.

The shared save/restore selector helper code.save.select_slot_or_path (0x85e5) is responsible for the modal UI around choosing a slot or path. It captures whether the prompt marker was visible, erases the marker, pushes text attribute state, stops active sound, sets a text attribute pair, and calls the path prompt helper at 0x8705. If runtime buffer [0x0e72] is empty, that helper displays the save or restore path question, edits path buffer 0x1962 through modal helper 0x8794, and validates the path with 0x5bdd; helper 0x86a3 displays the insert-disk style message when the selected drive/path is not available. Slot helper 0x8814 scans up to 12 numbered save files through summary reader 0x8b9f, displays their 31-byte descriptions, highlights the current row with glyph 0x1a, and handles Enter, Escape, and movement events 1 and 5 for accept, cancel, up, and down. In save mode, accepting an empty description slot prompts for a new 31-byte description into 0x1c6c before the caller creates the file. On exit, 0x85e5 formats the selected filename into 0x1c8c, restores text state, and redraws the prompt marker if it had been visible. A zero return means cancel/no selection; a nonzero return lets the caller perform actual file I/O.

Follow-up fixture log_file_contents_001 ran the log append case alone, then converted the post-run qcow2 disk image to raw and extracted LF00000\LOGFILE with mtools. The file bytes were:



Room 0
Input line:
LOG

This matches the source-derived 0x90 format: leading newlines, current room, empty input-line buffer, and the resolved current-logic message.

Miscellaneous actions:

OpcodeLabelHandlerObserved action
0x7asetup_transient_object0x2c7aCopies seven immediate operands into globals 0x0eae..0x0eb3, with operand 6 shifted into the high nibble of 0x0eb3, then calls helper 0x2d52. The staged values select view resource, group, frame, X, Y, and priority/control nibbles. Helper 0x2d52 records event pair (5, 0) followed by the three staged byte pairs, initializes a transient object-like record at 0x0eb4 through 0x3ae7, 0x3bb7, and 0x3ccb, places it with 0x593a, draws/marks it through 0x57cf, rebuilds update lists, and calls 0x5762.
0x7bsetup_transient_object_var0x2ccaSame as 0x7a, but all seven operands are read through variables before the seventh value is shifted into the high nibble of 0x0eb3. The replay event sequence is the same four-pair packet emitted by helper 0x2d52.
0x82random_range_to_var0x5009Reads low bound arg0, high bound arg1, and destination variable index arg2. Calls helper 0x71c0, takes the returned value modulo (high - low + 1), adds low, and stores the low byte in var[arg2]. Helper 0x71c0 seeds from BIOS timer interrupt 1a when needed and advances a 16-bit state at 0x1711.
0x8dshow_interpreter_version0x733cDisplays the static interpreter identification string at 0x0aab through helper 0x1ce8. In the sampled SQ2 overlay this string reads Adventure Game Interpreter followed by Version 2.936.

Remaining table entries:

OpcodeLabelHandlerObserved action
0x7fnoop0x5051Performs no state change and returns the current bytecode pointer unchanged. This opcode was present in the action table but not encountered in the local SQ2 logic scan.
0x9bnoop_20x4c15Consumes two operand bytes and performs no state change. The handler returns SI + 2.
0xaacopy_save_description_to_string_slot0x2726Reads immediate string-slot index arg0, computes destination string slot 0x020d + arg0 * 0x28, and copies up to 0x1f bytes from runtime save-description buffer 0x0e72 into that slot through helper 0x4de8. Save/restore handlers test byte [0x0e72] after slot/path selection, so this action exposes the last selected/entered save-description buffer to logic string storage. A previous QEMU attempt patched static AGIDATA.OVL bytes at 0x0e72, but that did not populate the interpreter’s runtime data segment; direct dynamic validation would need to drive the save/restore selector path that fills the buffer.
0xabsave_event_buffer_count0x718bCopies data.event.pair_count to data.event.saved_pair_count. This marks a rollback point in the resource-event pair log.
0xacrestore_event_buffer_count0x719dRestores data.event.pair_count from data.event.saved_pair_count, then recomputes data.event.pair_buffer_write = data.event.pair_buffer_base + data.event.pair_count * 2.
0xadincrement_global_15300x602fIncrements byte [0x1530] and returns the current bytecode pointer. Source-backed keyboard-IRQ analysis shows [0x1530] is a nonzero gate for selected scan-code release paths: when an enabled tracked key in range 0x47..0x51 has its pressed latch set, release clears the latch and, if [0x1530] != 0, enqueues event (type=2, value=0) through code.input.enqueue_event (0x44a9). Local model tools/agi_input.py pins the latch semantics and the 8-bit increment wraparound.
0xaerebuild_priority_table_from_y0x4d10Reads immediate row/value arg0, clears word [0x124a], and rebuilds the 168-byte priority/control table at 0x127a. Entries below arg0 are set to 4; entries from arg0 upward are assigned a rising value starting at 5, using a scale derived from (0xa8 - arg0) * 0xa8 / 10, and capped at 0x0f. Helper 0x4cbb later maps priority/control values back through this table when [0x124a] == 0.
0xafnoop_1_table_count0x5051Uses the same no-op handler as action 0x7f, returning the bytecode pointer it was given. The action table gives this opcode one fixed operand byte, so table-driven static scans skip one byte, but the handler itself does not read or advance past that operand. This opcode was not encountered in the local SQ2 logic scan.

Gold Rush v3 extra action slots

The local Gold Rush / AGI 3.002.149 action dispatcher accepts entries through 0xb5 using the v3 table at AGIDATA.OVL:0x0440. KQ4D / AGI 3.002.102 has the same v3 action-table shape at AGIDATA.OVL:0x0620. The decoded Gold Rush scripts observed so far do not use these slots, so the following descriptions are source-backed from handler disassembly and cross-references rather than QEMU behavior probes.

OpcodeLocal labelGR handlerObserved source effect
0xb0reserved_noop_v3_00x5286Generic no-op/return handler with no operands.
0xb1set_menu_interaction_gate0x970bReads one immediate byte, zero-extends it, and stores it in word [0x0403]. GR code.menu.interact (0x9724) tests this word first and returns immediately while it is zero; nonzero values allow the existing menu interaction path to run after 0xa1 has set the menu-request flag. QEMU report build/gr-v3-behavior/menu_gate_suite.json validates zero as matching a blocked control and nonzero as producing a distinct modal-menu capture.
0xb2reserved_noop_v3_20x5286Generic no-op/return handler with no operands.
0xb3reserved_noop_v3_4args0x5286The table declares four fixed operands, but the handler itself performs no state change.
0xb4reserved_noop_v3_2varargs0x5286The table declares two variable operands with metadata 0xc0, but the handler itself performs no state change.
0xb5clear_key_release_event_gate0x63b0Stores zero in byte [0x0405]. GR’s shared action 0xad stores one in the same byte, and the GR keyboard IRQ hook (0x63b8) tests [0x0405] before enqueueing a type-2 zero event on selected key-release paths. Local model tools/agi_input.py covers the GR set/clear gate against the shared tracked-key latch state machine.

QEMU fixture display_mode_replay_uses_rolled_back_event_count validates the observable pair-log effect of 0xab and 0xac. The script saves the current pair count, records a later picture operation, restores the saved count, then enters display-mode replay. The memory-backed replay notes show the rolled-back picture is absent from the active pair buffer, and the automated capture matches the expected replayed-picture state.

Local SQ2 scan

The local helper tools/disassemble_logic.py extracts logic resources, applies the dispatch tables, and prints decoded bytecode. It detects dispatch-table bases from the observed operand metadata signature, so v3 inputs whose tables move within AGIDATA.OVL can still be decoded after their resource container is recognized. It also has a --stats mode for linear opcode counts. This is still a static bytecode listing, not a live execution trace.

One LOGDIR entry decodes to VOL.0:0x1ffff but does not have a valid volume record header; the stats mode reports this as logic 141 and skips it.

Using the action table operand counts and the condition table operand counts, the current local scan of present SQ2 logic resources found these structural byte counts:

ByteCountMeaning
0x00144End of the current execution path. Later bytecode in the same logic can still be reached by jumps.
0xfe365Relative jump.
0xff2224Conditional block marker.

Frequently encountered action opcodes at statement boundaries included:

OpcodeLabelCountFixed operands
0x03assignn20192
0x65display_message13011
0x2bset_object_subresource5322
0x29set_object_resource5202
0x23activate_object5161
0x2fset_object_derived_resource_24662
0x0cset_flag4641
0x25set_object_pos4483
0x36set_object_field_243452
0x1eload_view3231

Frequently encountered condition opcodes included:

OpcodeLabelCountFixed operands
0x0einput_word_sequence2472variable
0x01var_eq_imm19492
0x07flag_set10981
0x05var_gt_imm5042
0x0bobject_left_baseline_in_rect4505
0x03var_lt_imm1982
0x09obj_table_room_ff1531

The current scanner is intentionally conservative. It correctly identifies the main structural bytes and many statement boundaries. Remaining interpreter work is concentrated less on unnamed action opcodes and more on refining implementation-level contracts for object/view fields, display-specific buffer variants, and the more hardware-facing input and graphics helpers.