If you appreciate the work done within the wiki, please consider supporting The Cutting Room Floor on Patreon. Thanks for all your support!

The Adventures of Captain Comic (DOS)/Code Changes

From The Cutting Room Floor
Jump to navigation Jump to search

This is a sub-page of The Adventures of Captain Comic (DOS).

These diffs come from annotated disassembly available from the repo linked in the notes page, as of commit b645df3ec3377bf6d050543e3afb6003d0f8ed62.

R2 Variables

+; Dead code: executable actually starts at ..start below, according to the EXE
+; header.
+       jmp main
+       nop

R2 Interrupt Handler Check

+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
+
+       ; Install our custom interrupt handlers. We store a magic value in
+       ; interrupt_handler_install_sentinel and expect int3_handler to
+       ; bit-flip it. Later, at .check_interrupt_handler_install_sentinel, we
+       ; check that the value is bit-flipped as expected. If it is not, it
+       ; means our interrupt handlers somehow did not get installed.
+.INTERRUPT_HANDLER_INSTALL_SENTINEL    equ     0x25
+       call install_interrupt_handlers
+       mov al, .INTERRUPT_HANDLER_INSTALL_SENTINEL
+       ; When given an unknown operation code in al, the int3_handler
+       ; (otherwise devoted to handling sound) bit-flips the operation code
+       ; and returns it in al.
+       int3    ; call int3_handler
+       mov [interrupt_handler_install_sentinel], al    ; al should be bit-flipped here
+.check_interrupt_handler_install_sentinel:
+       ; Check that out custom interrupts were installed successfully, by
+       ; checking the value that should have been modified by int3_handler in
+       ; the `int3` call above.
+       xor byte [interrupt_handler_install_sentinel], 0xff     ; undo the bit-flip that int3_handler should have done
+       lea bx, [STARTUP_NOTICE_TEXT]
+       cmp byte [interrupt_handler_install_sentinel], .INTERRUPT_HANDLER_INSTALL_SENTINEL      ; as expected?
+       je display_startup_notice       ; all good, continue
+       jmp title_sequence
 int3_handler:
        ; Dispatch on the operation code in al.
        or al, al
        je .unmute
        cmp al, SOUND_PLAY
        je .play
        cmp al, SOUND_MUTE
        je .mute
        cmp al, SOUND_STOP
        je .stop
        cmp al, SOUND_QUERY
        je .query
+       ; For any other value, invert al and return. This action is used by the
+       ; main function to check for the correct installation of interrupt
+       ; handlers.
+       xor al, 0xff
        jmp .return
+interrupt_handler_install_sentinel     db      0

R2 Startup Notice

+; Display STARTUP_NOTICE_TEXT and wait for a keypress to configure the
+; keyboard, quit, or begin the game.
+display_startup_notice:
+       call display_xor_decrypt        ; decrypt and display STARTUP_NOTICE_TEXT
+
+       xor ax, ax      ; ah=0x00: get keystroke; returned al is ASCII code
+       int 0x16
+       cmp al, 'k'
+       je setup_keyboard
+       cmp al, 'K'
+       je setup_keyboard
+       jmp check_ega_support
-; Install interrupt handlers. Display STARTUP_NOTICE_TEXT.
-display_startup_notice:
-       call install_interrupt_handlers
-       lea bx, [STARTUP_NOTICE_TEXT]
-       call display_xor_decrypt
-       ; We don't wait for a keypress, so the startup text is never visible.
 STARTUP_NOTICE_TEXT:   xor_encrypt `\
-   The Adventures of Captain Comic\r\n\
- Copyright (c) 1988 by Michael Denio\r\n\
-\x1a\x1a`
-
+                The Adventures of Captain Comic  --  Revision 2\r\n\
+                        Copyright 1988 by Michael Denio\r\n\
+\r\n\
+  This software is being distributed under the Shareware concept, where you as\r\n\
+  the user  are  allowed  to use the program on a "trial" basis.  If you enjoy\r\n\
+  playing  Captain  Comic,  you  are encouraged to register yourself as a user\r\n\
+  with a $10 to $20 contribution. Registered users will be given access to the\r\n\
+  official Captain Comic  question hotline (my home phone number), and will be\r\n\
+  the first in line to receive new Comic adventures.\r\n\
+\r\n\
+        This product is copyrighted material, but may be re-distributed\r\n\
+                 by complying to these two simple restrictions:\r\n\
+\r\n\
+        1. The program and graphics (including world maps) may not be\r\n\
+           distributed in any modified form.\r\n\
+        2. No form of compensation is be collected from the distribution\r\n\
+           of this program, including any disk handling costs or BBS\r\n\
+           file club fees.\r\n\
+\r\n\
+    Questions and contributions can be sent to me at the following address:\r\n\
+                                Michael A. Denio\r\n\
+                             1420 W. Glen Ave #202\r\n\
+                                Peoria, IL 61614\r\n\
+\r\n\
+      Press \'K\' to define the keyboard  ---  Press any other key to begin.\
+\0\0`
+
+; The first byte of SOUND_TITLE_RECAP is simultaneously the terminator for
+; STARTUP_NOTICE_TEXT. NOTE_E3 is 0x1c3f, whose little-endian first byte is
+; 0x3f, which is the terminator sentinel value that display_xor_decrypt looks
+; for.
 SOUND_TITLE_RECAP:
 dw     NOTE_E3, 6

R2 Keymap

+; Keyboard state variables, set by int9_handler.
+key_state_esc          db      0
+key_state_f1           db      0
+key_state_f2           db      0
+key_state_f3           db      0       ; written but never read
+key_state_f4           db      0       ; written but never read
+key_state_open         db      0
+key_state_jump         db      0
+key_state_teleport     db      0
+key_state_left         db      0
+key_state_right                db      0
+key_state_fire         db      0
+
+; Default keyboard scancode mappings; may be overridden by configuration or
+; KEYS.DEF.
+keymap:
+scancode_jump          db      SCANCODE_SPACE
+scancode_fire          db      SCANCODE_INS
+scancode_left          db      SCANCODE_LEFT
+scancode_right         db      SCANCODE_RIGHT
+scancode_open          db      SCANCODE_ALT
+scancode_teleport      db      SCANCODE_CAPSLOCK
+
+; The scancode of the most recent key release event (set in int9_handler).
+recent_scancode                db      0
+; Load key mappings from KEYS.DEF, if present.
+.try_load_keymap_file:
+       lea dx, [FILENAME_KEYMAP]       ; "KEYS.DEF"
+       mov ax, 0x3d00  ; ah=0x3d: open existing file
+       int 0x21
+       jc .check_interrupt_handler_install_sentinel    ; if file doesn't exist, silently skip keymap loading
+
+       mov bx, ax      ; bx = file handle
+       ; keymap is in the cs segment. File reads go into a buffer starting at
+       ; ds:dx, so temporarily assign ds=cs.
+       push ds
+       mov ax, cs
+       mov ds, ax
+       lea dx, [keymap]        ; ds:dx = destination buffer
+       mov cx, 6       ; cx = number of bytes to read
+       mov ax, 0x3f00  ; ah=0x3f: read from file or device
+       int 0x21
+       mov ax, 0x3e00  ; ah=0x3e: close file
+       int 0x21
+       pop ds          ; point ds at the data segment again
+; Do the interactive keyboard setup. Optionally save the configured key mapping
+; to KEYS.DEF. Jump to title_sequence when done.
+setup_keyboard:
+       push ds         ; temporarily work relative the data segment containing input-related strings
+       mov ax, input_config_strings
+       mov ds, ax
+
+       call input_unmapped_scancode    ; eat the 'k'/'K' that was already pressed and released to get us to this screen
+
+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
+
+       lea dx, [STR_DEFINE_KEYS]       ; "Define Keys"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+
+       ; Initialize the key mapping. input_unmapped_scancode blocks until a
+       ; key is pressed that does not conflict with one of these variables
+       ; already assigned. scancode_teleport does not need to be initialized
+       ; because it is assigned last; there are no later key assignments that
+       ; would need to be compared against it.
+       mov byte [cs:scancode_jump], 0
+       mov byte [cs:scancode_left], 0
+       mov byte [cs:scancode_right], 0
+       mov byte [cs:scancode_fire], 0
+       mov byte [cs:scancode_open], 0
+
+       lea dx, [STR_MOVE_LEFT]         ; "Move Left"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_left], al
+
+       lea dx, [STR_MOVE_RIGHT]        ; "Move Right"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_right], al
+
+       lea dx, [STR_JUMP]              ; "Jump"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_jump], al
+
+       lea dx, [STR_FIREBALL]          ; "Fireball"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_fire], al
+
+       lea dx, [STR_OPEN_DOOR]         ; "Open Door"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_open], al
+
+       lea dx, [STR_TELEPORT]          ; "Teleport"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call input_unmapped_scancode
+       mov byte [cs:scancode_teleport], al
+
+       ; Clear the BIOS keyboard buffer.
+       xor ax, ax
+       mov es, ax      ; set es = 0x0000
+       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
+       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+
+       lea dx, [STR_THIS_SETUP_OK]     ; "This setup OK? (y/n)"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       xor ax, ax      ; ah=0x00: get keystroke
+       int 0x16
+       cmp al, 'n'
+       je .start_over
+       cmp al, 'N'
+       je .start_over
+       ; Any key other than 'n'/'N' means the setup is OK.
+
+       lea dx, [STR_SAVE_SETUP_TO_DISK]        ; "Save setup to disk? (y/n)"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       xor ax, ax      ; ah=0x00: get keystroke
+       int 0x16
+       cmp al, 'y'
+       je .save_keymap_file
+       cmp al, 'Y'
+       je .save_keymap_file
+       ; Any key other than 'y'/'Y' means don't save the keymap.
+
+       pop ds          ; revert the temporary data segment
+       jmp check_ega_support
+
+.start_over:
+       pop ds          ; revert the temporary data segment
+       jmp setup_keyboard
+
+.save_keymap_file:
+       pop ds          ; revert the temporary data segment
+       lea dx, [FILENAME_KEYMAP]       ; "KEYS.DEF"
+       mov ax, 0x3c00  ; ah=0x3c: create or truncate file
+       xor cx, cx      ; file attributes
+       int 0x21
+       jc .create_failed       ; failure to create the file is a fatal error
+
+       mov bx, ax      ; bx = file handle
+       mov si, ds      ; save ds
+       mov ax, cs
+       mov ds, ax      ; temporarily set ds = cs
+       mov ax, 0x4000  ; ah=0x40: write to file
+       mov cx, 6       ; cx = number of bytes to write
+       lea dx, [keymap]        ; ds:dx = data to write
+       int 0x21
+       mov ds, si      ; restore ds
+       mov ax, 0x3e00  ; ah=0x3e: close file
+       int 0x21
+
+       jmp check_ega_support
+       nop             ; dead code
+
+.create_failed:
+       jmp terminate_program.no_audiovideo_cleanup
+
+; Wait for a key to be pressed whose scancode has not already been assigned to
+; scancode_jump, scancode_left, scancode_right, scancode_fire, or
+; scancode_open; is not Escape; and is within the range of permitted scancodes.
+; Display a text representation of the scancode.
+; Output:
+;   al = scancode of key pressed
+input_unmapped_scancode:
+       ; Loop until a key is released.
+       mov byte [cs:recent_scancode], 0        ; set in int9_handler
+.loop:
+       cmp byte [cs:recent_scancode], 0
+       je .loop
+
+       mov bl, [cs:recent_scancode]
+       xor bh, bh      ; bx = scancode
+
+       ; Compare to already-mapped scancodes.
+       cmp bl, [cs:scancode_jump]
+       je input_unmapped_scancode
+       cmp bl, [cs:scancode_left]
+       je input_unmapped_scancode
+       cmp bl, [cs:scancode_right]
+       je input_unmapped_scancode
+       cmp bl, [cs:scancode_fire]
+       je input_unmapped_scancode
+       cmp bl, [cs:scancode_open]
+       je input_unmapped_scancode
+       ; No need to compare against scancode_teleport, because it is the last
+       ; mapping to be assigned and so cannot pre-empt any other mappings.
+
+       ; bl now contains a scancode that is not mapped to any game action.
+       ; Check it against other reserved scancodes.
+       dec bx
+       jz input_unmapped_scancode      ; Escape is reserved, try again
+       cmp bx, SCANCODE_INS - 1        ; scancode > Ins? (subtract 1 to compensate for bx decrement)
+       jg input_unmapped_scancode      ; scancodes outside the range 2..82 are not allowed
+
+       ; Display a text representation of the scancode. Multiply by 8 to index
+       ; SCANCODE_LABELS.
+       shl bx, 1
+       shl bx, 1
+       shl bx, 1       ; (scancode - 1) * 8
+       lea dx, [SCANCODE_LABELS]
+       add dx, bx      ; SCANCODE_LABELS[scancode - 1]
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+
+       mov al, [cs:recent_scancode]    ; return the scancode in al
+       ret
-; Keyboard state variables, set by int9_handler.
-key_state_esc          db      0
-; A table of booleans for every key from SCANCODE_ALT to SCANCODE_INS,
-; inclusive.
-keys_state:
-       resb    SCANCODE_INS - SCANCODE_ALT + 1
-key_state_f1           equ     keys_state + SCANCODE_F1 - SCANCODE_ALT
-key_state_f2           equ     keys_state + SCANCODE_F2 - SCANCODE_ALT
-key_state_open         equ     keys_state + SCANCODE_ALT - SCANCODE_ALT
-key_state_jump         equ     keys_state + SCANCODE_SPACE - SCANCODE_ALT
-key_state_teleport     equ     keys_state + SCANCODE_CAPSLOCK - SCANCODE_ALT
-key_state_left         equ     keys_state + SCANCODE_LEFT - SCANCODE_ALT
-key_state_right                equ     keys_state + SCANCODE_RIGHT - SCANCODE_ALT
-key_state_fire         equ     keys_state + SCANCODE_INS - SCANCODE_ALT
-; INT 9 is called for keyboard events. Update the state of the keys_state
-; array. Call the original INT 9 handler too.
+; INT 9 is called for keyboard events. Update the state of the key_state_*
+; variables and updates recent_scancode in the case of a key press. Call the
+; original INT 9 handler too.
 ; Input:
 ;   saved_int9_handler_offset:saved_int9_handler_segment = address of original INT 9 handler
+;   scancode_jump, scancode_fire, scancode_left, scancode_right, scancode_open,
+;     scancode_teleport = scancodes for game actions
 ; Output:
-;   keys_state = entries set to 1 or 0 according to whether the key is currently released or pressed
+;   recent_scancode = scancode of the most recent key release (not modified on key press events)
+;   key_state_jump, key_state_fire, key_state_left, key_state_right,
+;     key_state_open, key_state_teleport, key_state_esc, key_state_f1,
+;     key_state_f2, key_state_f3, key_state_f4 = set to 0 or 1 according to
+;     whether the key is currently released or pressed
 int9_handler:
        push ax
        push bx
        push cx
        push dx
        ; Read the scancode into al and call the original handler before
        ; continuing.
        in al, 0x60     ; read the keyboard scancode
        push ax         ; save it
        pushf           ; push flags for recursive call to original INT 9 handler
        call far [cs:saved_int9_handler_offset] ; call the original INT 9 handler
        pop ax          ; al = keyboard scancode
        mov dx, 1       ; dl distinguishes key pressed/released; initially assume pressed
        test al, 0x80   ; most significant bit cleared means key pressed; set means key released
        jz .continue    ; if 0, it was indeed a press
 .released:
        and al, 0x7f
-       mov dx, 0       ; unset the "key pressed" flag before continuing
+       mov [cs:recent_scancode], al    ; clear high bit and store scancode of released key
+       xor dx, dx      ; unset the "key pressed" flag before continuing
 .continue:
+       ; Check for the scancodes that are mapped to game actions.
+       cmp al, [cs:scancode_jump]
+       je .jump
+       cmp al, [cs:scancode_fire]
+       je .fire
+       cmp al, [cs:scancode_left]
+       je .left
+       cmp al, [cs:scancode_right]
+       je .right
+       cmp al, [cs:scancode_open]
+       je .open
+       cmp al, [cs:scancode_teleport]
+       jne .other
+.teleport:
+       mov [cs:key_state_teleport], dl
+       jmp .return
+.jump:
+       mov [cs:key_state_jump], dl
+       jmp .return
+.fire:
+       mov [cs:key_state_fire], dl
+       jmp .return
+.left:
+       mov [cs:key_state_left], dl
+       jmp .return
+.right:
+       mov [cs:key_state_right], dl
+       jmp .return
+.open:
+       mov [cs:key_state_open], dl
+       jmp .return
+.other:
        ; Check for the non-mappable scancodes.
        cmp al, 1       ; scancode == Escape?
        je .esc
-       sub al, SCANCODE_ALT    ; scancode < LAlt?
+       sub al, SCANCODE_F1     ; scancode < F1?
        jb .return
-       cmp al, SCANCODE_INS - SCANCODE_ALT + 1 ; scancode > Ins?
+       cmp al, SCANCODE_F4 - SCANCODE_F1 + 1   ; scancode > F4?
        jae .return
-       lea bx, [keys_state]
+       lea bx, [key_state_f1]
        xor ah, ah
        add bx, ax
-       mov [cs:bx], dl ; keys_state[scancode - SCANCODE_ALT] = dl
+       mov [cs:bx], dl ; set key_state_f1, key_state_f2, key_state_f3, or key_state_f4
        jmp .return
 .esc:
        mov [cs:key_state_esc], dl
 .return:
        pop dx
        pop cx
        pop bx
        pop ax
        iret
+FILENAME_KEYMAP                db      `KEYS.DEF\0`
+STR_DEFINE_KEYS                db      `\n\n\n\n\n\n\r                                   Define Keys\n$`
+STR_MOVE_LEFT          db      `\n\r                                Move Left  : $`
+STR_MOVE_RIGHT         db      `\n\r                                Move Right : $`
+STR_JUMP               db      `\n\r                                Jump       : $`
+STR_FIREBALL           db      `\n\r                                Fireball   : $`
+STR_OPEN_DOOR          db      `\n\r                                Open Door  : $`
+STR_TELEPORT           db      `\n\r                                Teleport   : $`
+STR_THIS_SETUP_OK      db      `\n\n\r                                This setup OK? (y/n)$`
+STR_SAVE_SETUP_TO_DISK db      `\n\r                             Save setup to disk? (y/n)$`
+
+; Indices are off by one: SCANCODE_LABELS[0] is the label for scancode 1.
+SCANCODE_LABELS:
+       db      "Esc    $"
+       db      "1      $"
+       db      "2      $"
+       db      "3      $"
+       db      "4      $"
+       db      "5      $"
+       db      "6      $"
+       db      "7      $"
+       db      "8      $"
+       db      "9      $"
+       db      "0      $"
+       db      "-      $"
+       db      "=      $"
+       db      "Back Sp$"
+       db      "Tab    $"
+       db      "Q      $"
+       db      "W      $"
+       db      "E      $"
+       db      "R      $"
+       db      "T      $"
+       db      "Y      $"
+       db      "U      $"
+       db      "I      $"
+       db      "O      $"
+       db      "P      $"
+       db      "[      $"
+       db      "]      $"
+       db      "Enter  $"
+       db      "Ctrl   $"
+       db      "A      $"
+       db      "S      $"
+       db      "D      $"
+       db      "F      $"
+       db      "G      $"
+       db      "H      $"
+       db      "J      $"
+       db      "K      $"
+       db      "L      $"
+       db      ";      $"
+       db      "'      $"
+       db      "`      $"
+       db      "L Shift$"
+       db      "\      $"
+       db      "Z      $"
+       db      "X      $"
+       db      "C      $"
+       db      "V      $"
+       db      "B      $"
+       db      "N      $"
+       db      "M      $"
+       db      ",      $"
+       db      ".      $"
+       db      "/      $"
+       db      "R Shift$"
+       db      "*      $"
+       db      "Alt    $"
+       db      "Space  $"
+       db      "Caps   $"
+       db      "F1     $"
+       db      "F2     $"
+       db      "F3     $"
+       db      "F4     $"
+       db      "F5     $"
+       db      "F6     $"
+       db      "F7     $"
+       db      "F8     $"
+       db      "F9     $"
+       db      "F10    $"
+       db      "NumLock$"
+       db      "Scroll $"
+       db      "Home   $"
+       db      "Up     $"
+       db      "PgUp   $"
+       db      "- Key  $"
+       db      "Left   $"
+       db      "5 Key  $"
+       db      "Right  $"
+       db      "+ Key  $"
+       db      "End    $"
+       db      "Down   $"
+       db      "PgDn   $"
+       db      "Ins    $"
+       db      "Del    $"

R2 RLE

 title_sequence:
+       lea dx, [FILENAME_TITLE_GRAPHIC]        ; "sys000.ega"
+
+       mov ax, 0xa000
+       mov es, ax      ; es points to video memory
+
        ; The program uses various 8 KB buffers within the video memory segment
        ; 0xa000. The two most important are a000:0000 and a000:2000, which are
        ; the ones swapped between on every game tick for double buffering. The
        ; variable offscreen_video_buffer_ptr and function swap_video_buffers
        ; handle double buffering, swapping the displayed offset between 0x0000
        ; and 0x2000.
        ;
        ; The title sequence juggles a few video buffers, loading fullscreen
        ; graphics from .EGA files into memory and switching to them as
        ; appropriate. sys000.ega is loaded into a000:8000 and displayed
        ; immediately. sys001.ega is loaded into a000:a000 after 10 ticks have
        ; elapsed, but not immediately displayed. Then sys003.ega is loaded
        ; into *both* buffers a000:0000 and a000:2000, but also not immediately
        ; displayed. sys003.ega contains the gameplay UI and so needs to be in
        ; the double buffers. Then video buffer switches to a000:a000 to
        ; display sys001.ega. sys004.ega is loaded into a000:8000 (replacing
        ; sys000.ega) and displayed after a keypress. Finally, we switch to the
        ; buffer a000:2000, which contains sys003.ega, after another keypress,
        ; in preparation for starting gameplay.
        ;
        ; The complete rendered map for the current stage also lives in video
        ; memory, between a000:4000 and a000:dfff. That happens after the title
        ; sequence is over, so it doesn't conflict with the use of that region
        ; of memory here. See render_map and blit_map_playfield_offscreen.
 
        ; Load the title graphic into video buffer 0x8000.
-       lea dx, [FILENAME_TITLE_GRAPHIC]        ; "sys000.ega"
-       mov ax, 0x3d00  ; ah=0x3d: open existing file
-       int 0x21
-       jnc .open_title_ok      ; failure to open a .EGA file is a fatal error
-       jmp .terminate_program_trampoline
-.open_title_ok:
-       mov bp, ax      ; bp = file handle
-       push ds
-       mov ax, 0xa000
-       mov ds, ax      ; ds points to video memory
-       mov es, ax      ; es points to video memory
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       xor bx, bx      ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       mov ax, 0x3e00  ; ah=0x3e: close file
-       int 0x21
-       pop ds
+       mov di, 0x8000
+       call load_fullscreen_graphic
        ; Load the story graphic into video buffer 0xa000.
        lea dx, [FILENAME_STORY_GRAPHIC]        ; "sys001.ega"
-       mov ax, 0x3d00  ; ah=0x3d: open existing file
-       int 0x21
-       jnc .open_story_ok      ; failure to open a .EGA file is a fatal error
-       jmp .terminate_program_trampoline
-.open_story_ok:
-       mov bp, ax      ; bp = file handle
-       push ds
        mov ax, 0xa000
-       mov ds, ax      ; ds points to video memory
        mov es, ax      ; es points to video memory
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       xor bx, bx      ; plane index
-       call enable_ega_plane_write
-       mov dx, 0xa000  ; ds:dx = destination (video buffer 0xa000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0xa000  ; ds:dx = destination (video buffer 0xa000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0xa000  ; ds:dx = destination (video buffer 0xa000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0xa000  ; ds:dx = destination (video buffer 0xa000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       mov ax, 0x3e00  ; ah=0x3e: close file
-       int 0x21
-       pop ds
+       mov di, 0xa000
+       call load_fullscreen_graphic
 
-       ; Load the UI graphic into video buffers 0x0000 and 0x2000.
+       ; Load the UI graphic into video buffer 0x0000.
        lea dx, [FILENAME_UI_GRAPHIC]   ; "sys003.ega"
-       mov ax, 0x3d00  ; ah=0x3d: open existing file
-       int 0x21
-       jnc .open_ui_ok ; failure to open a .EGA file is a fatal error
-       jmp .terminate_program_trampoline
-.open_ui_ok:
-       mov bp, ax      ; bp = file handle
+       mov ax, 0xa000
+       mov es, ax      ; es points to video memory
+       xor di, di      ; video buffer 0x0000
+       call load_fullscreen_graphic
+
+       ; Copy the UI graphic from video buffer 0x0000 to video buffer 0x2000
+       ; (these are the two buffers that swap every tick during gameplay). We
+       ; need to copy the graphic one plane at a time, into the same nominal
+       ; buffer.
        push ds
        mov ax, 0xa000
        mov ds, ax      ; ds points to video memory
-       mov es, ax      ; es points to video memory
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       xor bx, bx      ; plane index
+       ; Copy plane 0 from video buffer 0x0000 to video buffer 0x2000.
+       mov ah, 1       ; ah = mask for plane 0
+       xor bx, bx      ; bl = index of plane 0
        call enable_ega_plane_write
-       mov dx, 0x0000  ; ds:dx = destination (video buffer 0x0000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Copy from video buffer 0x0000 to video buffer 0x2000.
-       xor si, si      ; ds:si = destination (video buffer 0x0000)
-       mov di, 0x2000  ; es:di = source (video buffer 0x2000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
+       mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
+       xor si, si
+       mov di, 0x2000
+       rep movsw       ; copy from ds:si to es:di
+       ; Copy plane 1 from video buffer 0x0000 to video buffer 0x2000.
+       mov ah, 2       ; ah = mask for plane 1
+       mov bx, 1       ; bl = index of plane 1
        call enable_ega_plane_write
-       mov dx, 0x0000  ; ds:dx = destination (video buffer 0x0000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Copy from video buffer 0x0000 to video buffer 0x2000.
-       xor si, si      ; ds:si = destination (video buffer 0x0000)
-       mov di, 0x2000  ; es:di = source (video buffer 0x2000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
+       mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
+       xor si, si
+       mov di, 0x2000
+       rep movsw       ; copy from ds:si to es:di
+       ; Copy plane 2 from video buffer 0x0000 to video buffer 0x2000.
+       mov ah, 4       ; ah = mask for plane 2
+       mov bx, 2       ; bl = index of plane 2
        call enable_ega_plane_write
-       mov dx, 0x0000  ; ds:dx = destination (video buffer 0x0000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Copy from video buffer 0x0000 to video buffer 0x2000.
-       xor si, si      ; ds:si = destination (video buffer 0x0000)
-       mov di, 0x2000  ; es:di = source (video buffer 0x2000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
+       mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
+       xor si, si
+       mov di, 0x2000
+       rep movsw       ; copy from ds:si to es:di
+       ; Copy plane 3 from video buffer 0x0000 to video buffer 0x2000.
+       mov ah, 8       ; ah = mask for plane 3
+       mov bx, 3       ; bl = index of plane 3
        call enable_ega_plane_write
-       mov dx, 0x0000  ; ds:dx = destination (video buffer 0x0000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Copy from video buffer 0x0000 to video buffer 0x2000.
-       xor si, si      ; ds:si = destination (video buffer 0x0000)
-       mov di, 0x2000  ; es:di = source (video buffer 0x2000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       mov ax, 0x3e00  ; ah=0x3e: close file
-       int 0x21
+       mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
+       xor si, si
+       mov di, 0x2000
+       rep movsw       ; copy from ds:si to es:di
        pop ds
 
        ; Switch to the story graphic and fade in.
        call palette_darken
        mov cx, 0xa000  ; switch to video buffer 0xa000, into which we loaded the story graphic
        call switch_video_buffer
        call palette_fade_in
 
        ; Load the items graphic into video buffer 0x8000 (over the title
        ; screen graphic).
        lea dx, [FILENAME_ITEMS_GRAPHIC]        ; "sys004.ega"
-       mov ax, 0x3d00  ; ah=0x3d: open existing file
-       int 0x21
-       jnc .open_items_ok      ; failure to open a .EGA file is a fatal error
-       jmp .terminate_program_trampoline
-.open_items_ok:
-       mov bp, ax      ; bp = file handle
-       push ds
        mov ax, 0xa000
-       mov ds, ax      ; ds points to video memory
        mov es, ax      ; es points to video memory
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       xor bx, bx      ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov dx, 0x8000  ; ds:dx = destination (video buffer 0x8000)
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       mov ax, 0x3e00  ; ah=0x3e: close file
-       int 0x21
-       pop ds
+       mov di, 0x8000
+       call load_fullscreen_graphic
+; Load a fullscreen graphic from a .EGA file and decode its to a specified
+; destination buffer.
+; Input:
+;   ds:dx = address of filename
+;   es:di = 32000-byte destination buffer
+load_fullscreen_graphic:
+       ; Load the entire file into load_fullscreen_graphic_buffer, without
+       ; decoding.
+       mov ax, 0x3d00  ; ah=0x3d: open existing file
+       int 0x21
+       jnc .open_ok    ; failure to open a .EGA file is a fatal error
+       jmp title_sequence.terminate_program_trampoline
+.open_ok:
+       mov bx, ax      ; bx = file handle
+       push ds
+       mov ax, input_config_strings
+       mov ds, ax      ; load_fullscreen_graphic_buffer is in the input_config_strings segment
+       lea dx, [load_fullscreen_graphic_buffer]        ; ds:dx = destination buffer
+       mov cx, 0x7fff  ; cx = number of bytes to read (overflow possible here; buffer is only 0x3e82 bytes)
+       mov ax, 0x3f00  ; ah=0x3f: read from file or device
+       int 0x21        ; no error check
+       mov ax, 0x3e00  ; ah=0x3e: close file
+       int 0x21
+
+       ; Decode file contents as RLE.
+       ; http://www.shikadi.net/moddingwiki/Captain_Comic_Image_Format#File_format
+       ; Ignore the first word, which is the plane size, always 8000.
+       lea ax, [load_fullscreen_graphic_buffer + 2]
+       mov si, ax
+       mov ah, 1       ; blue plane mask
+       xor bx, bx      ; blue plane index
+       call enable_ega_plane_write
+       call rle_decode
+       mov ah, 2       ; green plane mask
+       mov bx, 1       ; green plane index
+       call enable_ega_plane_write
+       call rle_decode
+       mov ah, 4       ; red plane mask
+       mov bx, 2       ; red plane index
+       call enable_ega_plane_write
+       call rle_decode
+       mov ah, 8       ; intensity plane mask
+       mov bx, 3       ; intensity plane index
+       call enable_ega_plane_write
+       call rle_decode
+       pop ds
+       ret
+
+; Decode RLE data until a certain number of bytes have been decoded.
+; Input:
+;   ds:si = input RLE data
+;   es:di = output buffer
+;   load_fullscreen_graphic_buffer = first word is the number of bytes to decode
+;     (returns when at least that many bytes have been written to es:di)
+; Output:
+;   ds:si = advanced
+;   es:di = unchanged
+rle_decode:
+       ; http://www.shikadi.net/moddingwiki/Captain_Comic_Image_Format#File_format
+       mov bx, di
+.loop:
+       lodsb           ; read from [ds:si] into al
+       test al, 0x80   ; is the high bit set?
+       jnz .repeat
+.copy:
+       ; High bit not set means copy the next n bytes.
+       xor ah, ah
+       mov cx, ax      ; cx = n
+       rep movsb       ; copy n bytes from ds:si to es:di
+       jmp .next
+.repeat:
+       ; High bit set means repeat the next byte n times.
+       xor ah, ah
+       and al, 0x7f    ; unset the high bit
+       mov cx, ax      ; cx = n
+       lodsb           ; read from [ds:si] into al
+       rep stosb       ; repeat al into [es:di] n times
+.next:
+       mov ax, di
+       sub ax, bx      ; how many bytes have been written so far?
+       cmp ax, [load_fullscreen_graphic_buffer]        ; compare with the plane size at the beginning of the buffer
+       jl .loop        ; loop until we have decoded enough
+       mov di, bx
+       ret
        ; Load the win graphic into the offscreen video buffer.
        lea dx, [FILENAME_WIN_GRAPHIC]  ; "sys002.ega"
-       mov ax, 0x3d00  ; ah=0x3d: open existing file
-       int 0x21
-       jnc .open_ok    ; failure to open a .EGA file is a fatal error
-       jmp terminate_program
-.open_ok:
-       mov bp, ax      ; bp = file handle
-       push ds
-       mov ax, ds
-       mov es, ax      ; es = ds
        mov ax, 0xa000
-       mov ds, ax      ; ds points to video memory
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       xor bx, bx      ; plane index
-       call enable_ega_plane_write
-       mov dx, [es:offscreen_video_buffer_ptr] ; ds:dx = destination
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov dx, [es:offscreen_video_buffer_ptr] ; ds:dx = destination
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov dx, [es:offscreen_video_buffer_ptr] ; ds:dx = destination
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov dx, [es:offscreen_video_buffer_ptr] ; ds:dx = destination
-       mov cx, 320*200 / 8     ; cx = number of bytes to read
-       mov ax, 0x3f00  ; ah=0x3f: read from file or device
-       mov bx, bp      ; bx = file handle
-       int 0x21        ; no error check
-       mov ax, 0x3e00  ; ah=0x3e: close file
-       int 0x21
-       pop ds          ; restore the usual value of ds
+       mov es, ax      ; es points to video memory
+       mov di, [offscreen_video_buffer_ptr]
+       call load_fullscreen_graphic
        ; Load the high scores graphic into video buffer 0x0000.
-       mov ax, high_scores_graphic     ; the graphic is in its own segment at the end of the executable
-       mov ds, ax      ; ds = high_scores_graphic segment
+       lea dx, [FILENAME_HIGH_SCORES_GRAPHIC]  ; "sys005.ega"
        mov ax, 0xa000
        mov es, ax      ; es points to video memory
-       lea si, [GRAPHIC_HIGH_SCORES]   ; ds:si = source buffer
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       mov bx, 0       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-
-       ; The next two instructions seem to be erroneous. They appear to be
-       ; trying to enable plane mask 0 (i.e., no plane at all) and plane index
-       ; 3 (bl=3 left over from doing the intensity plane just above).
-       xor ah, ah
-       call enable_ega_plane_write
+       xor di, di      ; video buffer 0x0000
+       call load_fullscreen_graphic
+load_fullscreen_graphic_buffer:
+       resb    16002

R2 SYS005.EGA

        ; Load the high scores graphic into video buffer 0x0000.
-       mov ax, high_scores_graphic     ; the graphic is in its own segment at the end of the executable
-       mov ds, ax      ; ds = high_scores_graphic segment
+       lea dx, [FILENAME_HIGH_SCORES_GRAPHIC]  ; "sys005.ega"
        mov ax, 0xa000
        mov es, ax      ; es points to video memory
-       lea si, [GRAPHIC_HIGH_SCORES]   ; ds:si = source buffer
-       ; Blue plane.
-       mov ah, 1       ; plane mask
-       mov bx, 0       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Green plane.
-       mov ah, 2       ; plane mask
-       mov bx, 1       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Red plane.
-       mov ah, 4       ; plane mask
-       mov bx, 2       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-       ; Intensity plane.
-       mov ah, 8       ; plane mask
-       mov bx, 3       ; plane index
-       call enable_ega_plane_write
-       mov cx, 320*200 / 8 / 2 ; cx = number of words to copy
-       xor di, di      ; es:di = destination (video buffer 0x0000)
-       rep movsw       ; copy cx bytes from ds:si to es:di
-
-       ; The next two instructions seem to be erroneous. They appear to be
-       ; trying to enable plane mask 0 (i.e., no plane at all) and plane index
-       ; 3 (bl=3 left over from doing the intensity plane just above).
-       xor ah, ah
-       call enable_ega_plane_write
+       xor di, di      ; video buffer 0x0000
+       call load_fullscreen_graphic
-FILENAME_HIGH_SCORES_GRAPHIC   db      `sys005.ega\0`  ; unused; this graphic is in GRAPHIC_HIGH_SCORES instead
+FILENAME_HIGH_SCORES_GRAPHIC   db      `sys005.ega\0`
-section high_scores_graphic
-
-GRAPHIC_HIGH_SCORES:
-incbin "graphics/high_scores_320x200.ega"

R2 Sound Mute

 initialize_lives_sequence:
+       mov al, [cs:sound_is_enabled]
+       push ax         ; save sound mute status
        mov ax, SOUND_MUTE      ; so the extra life sound doesn't play
        int3
        mov cx, MAX_NUM_LIVES   ; initially award max lives
 .loop:
        mov ax, 1
        call wait_n_ticks       ; wait 1 tick between each life awarded
        push cx         ; award_extra_life uses cx
        call award_extra_life
        pop cx
        loop .loop
 
        mov ax, SOUND_STOP      ; stop the extra life sound that is playing but muted
        int3
-       mov ax, SOUND_UNMUTE    ; unmute the sound
-       int3
+       pop ax
+       mov [cs:sound_is_enabled], al   ; restore the saved sound mute status

R2 Instant Win

+       cmp byte [comic_num_lives], MAX_NUM_LIVES - 1   ; is the number of lives 1 less than the max?
+       je .num_lives_ok        ; always true
+       ; A weird line of dead code here: if, after counting up to the max and
+       ; subtracting 1, the number of lives is not 1 less than the max, then
+       ; set the game to end after 200 ticks. (Actually 199 ticks, because it
+       ; ends when the counter reaches 1.)
+       ; https://tcrf.net/The_Adventures_of_Captain_Comic_(DOS)#Unused_Instant-Win_Mode
+       mov byte [win_counter], 200
+.num_lives_ok:
        jmp load_new_level

R2 sound_is_playing

 .play:
-       cmp byte [cs:sound_is_playing], -1      ; this special value is not used anywhere
-       je .return
        cmp [cs:sound_priority], cl     ; is the new sound's priority less than what's already playing?
        jg .return
        mov [cs:sound_priority], cl     ; raise sound_priority to that of the new sound

R2 High Scores

-.terminate:
-       mov ah, 0x4c    ; ah=0x4c: terminate with return code
-       int 0x21
+.load_defaults:
+       push es
+
+       mov bx, NUM_HIGH_SCORES
+       mov ax, ds
+       mov es, ax      ; es = ds
+       lea di, [high_scores]
+.load_defaults_loop:
+       mov cx, high_score_size
+       lea si, [DEFAULT_HIGH_SCORE]
+       rep movsb       ; copy high_score_size bytes from ds:si to es:di
+       dec bx
+       jne .load_defaults_loop
+
+       pop es
+       jmp .rank_player_score
+       nop             ; dead code
 
 .try_open_high_scores:
        mov ax, 0x3d00  ; ah=0x3d: open existing file
        lea dx, [FILENAME_HIGH_SCORES]  ; "COMIC.HGH"
        int 0x21
-       jc .terminate   ; failure to open the high scores file is a fatal error
+       jc .load_defaults       ; load default high scores if opening the file failed
+; A zero score with a blank name, used to fill uninitialized entries in the
+; high score table.
+DEFAULT_HIGH_SCORE:
+istruc high_score
+at high_score.name,    db      "             $"
+at high_score.score,   db      0, 0, 0
+iend


R3 Startup Notice

 STARTUP_NOTICE_TEXT:   xor_encrypt `\
-                The Adventures of Captain Comic  --  Revision 2\r\n\
-                        Copyright 1988 by Michael Denio\r\n\
+                The Adventures of Captain Comic  --  Revision 3\r\n\
+                     Copyright 1988, 1989 by Michael Denio\r\n\
 \r\n\
   This software is being distributed under the Shareware concept, where you as\r\n\
   the user  are  allowed  to use the program on a "trial" basis.  If you enjoy\r\n\
   playing  Captain  Comic,  you  are encouraged to register yourself as a user\r\n\
   with a $10 to $20 contribution. Registered users will be given access to the\r\n\
   official Captain Comic  question hotline (my home phone number), and will be\r\n\
   the first in line to receive new Comic adventures.\r\n\
 \r\n\
         This product is copyrighted material, but may be re-distributed\r\n\
                  by complying to these two simple restrictions:\r\n\
 \r\n\
         1. The program and graphics (including world maps) may not be\r\n\
            distributed in any modified form.\r\n\
         2. No form of compensation is be collected from the distribution\r\n\
            of this program, including any disk handling costs or BBS\r\n\
            file club fees.\r\n\
 \r\n\
     Questions and contributions can be sent to me at the following address:\r\n\
                                 Michael A. Denio\r\n\
                              1420 W. Glen Ave #202\r\n\
                                 Peoria, IL 61614\r\n\
 \r\n\
-      Press \'K\' to define the keyboard  ---  Press any other key to begin.\
+      Press \'K\' to define the keyboard  ---  Press any other key to begin\
 \0\0`

R3 Esc

 ; Display STARTUP_NOTICE_TEXT and wait for a keypress to configure the
 ; keyboard, quit, or begin the game.
 display_startup_notice:
        call display_xor_decrypt        ; decrypt and display STARTUP_NOTICE_TEXT
 
        xor ax, ax      ; ah=0x00: get keystroke; returned al is ASCII code
        int 0x16
        cmp al, 'k'
        je setup_keyboard
        cmp al, 'K'
        je setup_keyboard
+       cmp al, 27      ; Escape
+       jne .not_esc
+       jmp terminate_program   ; Escape allows exiting directly from the startup notice screen
+.not_esc:
        jmp check_ega_support

R3 Keyboard Setup Hack

 ; Do the interactive keyboard setup. Optionally save the configured key mapping
 ; to KEYS.DEF. Jump to title_sequence when done.
 setup_keyboard:
        push ds         ; temporarily work relative the data segment containing input-related strings
        mov ax, input_config_strings
        mov ds, ax
 
-       call input_unmapped_scancode    ; eat the 'k'/'K' that was already pressed and released to get us to this screen
-
        mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
        int 0x10

R3 Keyboard Buffer

The keyboard setup code to wait for a keypress was moved into a subroutine (which also clears the BIOS keyboard buffer after every input):

-       ; Clear the BIOS keyboard buffer.
-       xor ax, ax
-       mov es, ax      ; set es = 0x0000
-       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
-       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+; Wait for a keypress. Additionally clear the BIOS keyboard buffer.
+; Output:
+;   al = scancode of key pressed
+wait_for_keypress:
+       ; Set recent_scancode to 0 and loop until int9_handler makes it
+       ; nonzero.
+       mov byte [cs:recent_scancode], 0
+.loop:
+       cmp byte [cs:recent_scancode], 0
+       je .loop
+
+       ; Clear the BIOS keyboard buffer.
+       xor ax, ax
+       mov es, ax      ; set es = 0x0000
        mov al, [cs:recent_scancode]    ; return the scancode in al
+       cli             ; disable interrupts
+       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
+       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+       sti             ; enable interrupts
        ret
        ; Clear the BIOS keyboard buffer.
        xor ax, ax
        mov es, ax      ; es = 0x0000
+       cli             ; disable interrupts
        mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
        mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+       sti             ; enable interrupts

R3 Key Mapping

+SCANCODE_DEL           equ     83
 ; Wait for a key to be pressed whose scancode has not already been assigned to
 ; scancode_jump, scancode_left, scancode_right, scancode_fire, or
 ; scancode_open; is not Escape; and is within the range of permitted scancodes.
 ; Display a text representation of the scancode.
 ; Output:
 ;   al = scancode of key pressed
 input_unmapped_scancode:
-       ; Loop until a key is released.
-       mov byte [cs:recent_scancode], 0        ; set in int9_handler
-.loop:
-       cmp byte [cs:recent_scancode], 0
-       je .loop
-
-       mov bl, [cs:recent_scancode]
+       call wait_for_keypress  ; al = scancode
+       mov bl, al
        xor bh, bh      ; bx = scancode
+       mov si, bx      ; remember the scancode in si
 
        ; Compare to already-mapped scancodes.
        cmp bl, [cs:scancode_jump]
        je input_unmapped_scancode
        cmp bl, [cs:scancode_left]
        je input_unmapped_scancode
        cmp bl, [cs:scancode_right]
        je input_unmapped_scancode
        cmp bl, [cs:scancode_fire]
        je input_unmapped_scancode
        cmp bl, [cs:scancode_open]
        je input_unmapped_scancode
        ; No need to compare against scancode_teleport, because it is the last
        ; mapping to be assigned and so cannot pre-empt any other mappings.
 
        ; bl now contains a scancode that is not mapped to any game action.
        ; Check it against other reserved scancodes.
        dec bx
        jz input_unmapped_scancode      ; Escape is reserved, try again
-       cmp bx, SCANCODE_INS - 1        ; scancode > Ins? (subtract 1 to compensate for bx decrement)
-       jg input_unmapped_scancode      ; scancodes outside the range 2..82 are not allowed
+       cmp bx, SCANCODE_DEL - 1        ; scancode > Del? (subtract 1 to compensate for bx decrement)
+       jg input_unmapped_scancode      ; scancodes outside the range 2..83 are not allowed
 
        ; Display a text representation of the scancode. Multiply by 8 to index
        ; SCANCODE_LABELS.
        shl bx, 1
        shl bx, 1
        shl bx, 1       ; (scancode - 1) * 8
        lea dx, [SCANCODE_LABELS]
        add dx, bx      ; SCANCODE_LABELS[scancode - 1]
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
 
+       mov ax, si      ; return the scancode in al
+       ret
+
+; Wait for a keypress. Additionally clear the BIOS keyboard buffer.
+; Output:
+;   al = scancode of key pressed
+wait_for_keypress:
+       ; Set recent_scancode to 0 and loop until int9_handler makes it
+       ; nonzero.
+       mov byte [cs:recent_scancode], 0
+.loop:
+       cmp byte [cs:recent_scancode], 0
+       je .loop
+
+       ; Clear the BIOS keyboard buffer.
+       xor ax, ax
+       mov es, ax      ; set es = 0x0000
        mov al, [cs:recent_scancode]    ; return the scancode in al
+       cli             ; disable interrupts
+       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
+       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+       sti             ; enable interrupts
        ret

R3 Key Press/Release

-; The scancode of the most recent key release event (set in int9_handler).
+; The scancode of the most recent key press event (set in int9_handler).
 recent_scancode                db      0
 ; INT 9 is called for keyboard events. Update the state of the key_state_*
 ; variables and updates recent_scancode in the case of a key press. Call the
 ; original INT 9 handler too.
 ; Input:
 ;   saved_int9_handler_offset:saved_int9_handler_segment = address of original INT 9 handler
 ;   scancode_jump, scancode_fire, scancode_left, scancode_right, scancode_open,
 ;     scancode_teleport = scancodes for game actions
 ; Output:
-;   recent_scancode = scancode of the most recent key release (not modified on key press events)
+;   recent_scancode = scancode of the most recent keypress (not modified on key release events)
 ;   key_state_jump, key_state_fire, key_state_left, key_state_right,
 ;     key_state_open, key_state_teleport, key_state_esc, key_state_f1,
 ;     key_state_f2, key_state_f3, key_state_f4 = set to 0 or 1 according to
 ;     whether the key is currently released or pressed
 int9_handler:
        push ax
        push bx
        push cx
        push dx
        ; Read the scancode into al and call the original handler before
        ; continuing.
        in al, 0x60     ; read the keyboard scancode
        push ax         ; save it
        pushf           ; push flags for recursive call to original INT 9 handler
        call far [cs:saved_int9_handler_offset] ; call the original INT 9 handler
        pop ax          ; al = keyboard scancode
        mov dx, 1       ; dl distinguishes key pressed/released; initially assume pressed
        test al, 0x80   ; most significant bit cleared means key pressed; set means key released
-       jz .continue    ; if 0, it was indeed a press
+       jz .pressed
 .released:
+       ; If key release, update key_state_* but do not update recent_scancode.
        and al, 0x7f
-       mov [cs:recent_scancode], al    ; clear high bit and store scancode of released key
        xor dx, dx      ; unset the "key pressed" flag before continuing
+       jmp .continue
+.pressed:
+       ; If key press, update recent_scancode and go on to update key_state_*.
+       mov [cs:recent_scancode], al
 .continue:

R3 Unpause

        call pause
+       ; Wait for the escape key to be released after unpausing, to avoid
+       ; flickering the pause screen when the key is held.
+.wait_for_esc_release:
+       cmp byte [cs:key_state_esc], 1
+       je .wait_for_esc_release
+
+       mov si, [comic_y]       ; restore si, which was trashed by pause

R3 Initialize Jump Counter

 .respawn:
        call lose_a_life
        mov byte [comic_run_cycle], 0
        mov byte [comic_is_falling_or_jumping], 0       ; bug: Comic is considered to be "on the ground" (can jump and teleport) immediately after respawning, even if in the air
        mov byte [comic_x_momentum], 0
        mov byte [comic_y_vel], 0
-       mov byte [comic_jump_counter], 0        ; bug: jumping immediately after respawning underflows comic_jump_counter (hover glitch)
+       mov byte [comic_jump_counter], 4        ; bug: jumping immediately after respawning acts as if comic_jump_power were 4, even if Comic has the Boots
        mov byte [comic_animation], COMIC_STANDING
        mov byte [comic_hp_pending_increase], MAX_HP    ; let the HP fill up from zero after respawning
        mov byte [fireball_meter_counter], 2
        ; comic_is_teleporting is set to 0 in load_new_stage.comic_located.
        jmp load_new_stage.comic_located        ; respawn in the same stage at (comic_x_checkpoint, comic_y_checkpoint)
-comic_jump_counter     db      0       ; a jump stops moving upwards when this counter decrements to 1
+comic_jump_counter     db      4       ; a jump stops moving upwards when this counter decrements to 1

R3 Jump Into Ceiling

 ; Handle Comic movement when comic_is_falling_or_jumping == 1. Apply upward
 ; acceleration due to jumping, apply downward acceleration due to gravity,
 ; check for solid ground, and handle midair left/right movement.
 ; Input:
 ;   si = coordinates of Comic
 ;   key_state_left, key_state_right, key_state_jump = state of inputs
 ;   current_level_number = if LEVEL_NUMBER_SPACE, use lower gravity
 ;   comic_jump_counter = how many ticks Comic can continue moving upward
 ;   comic_x_momentum = Comic's current x momentum
 ;   comic_y_vel = Comic's current y velocity, in units of 1/8 game units per tick
+;   ceiling_stick_flag = whether Comic is jumping upward against a ceiling
 ;   comic_facing = COMIC_FACING_RIGHT or COMIC_FACING_LEFT
 ; Output:
 ;   si = updated coordinates of Comic
 ;   comic_is_falling_or_jumping = updated
 ;   comic_jump_counter = decremented by 1 unless already at 1
 ;   comic_x_momentum = updated
 ;   comic_y_vel = updated
+;   ceiling_stick_flag = updated
 ;   comic_facing = updated
 handle_fall_or_jump:
        ; Are we still in the state where a jump can continue accelerating
        ; upward? When comic_jump_counter is 1, the upward part of the jump is
        ; over.
        dec byte [comic_jump_counter]   ; decrement the counter
        jz .jump_counter_expired        ; when it hits bottom, this jump can no longer accelerate upward
 
        ; We're still able to accelerate upward in this jump. Is the jump key
        ; still being pressed?
        cmp byte [cs:key_state_jump], 1
-       jne .integrate_vel
+       jne .not_accelerating_upward
 
        ; We're still accelerating upward in this jump. Subtract a fixed value
        ; from the vertical velocity.
        sub byte [comic_y_vel], 7
        jmp .integrate_vel
 
 .jump_counter_expired:
        ; The upward part of the jump is over (comic_jump_counter was 1 when
        ; the function was called, and just became 0). Clamp it to a minimum
        ; value of 1.
        inc byte [comic_jump_counter]
 
+.not_accelerating_upward:
+       ; We're no longer accelerating upward, so reset the ceiling-stick flag.
+       mov byte [ceiling_stick_flag], 0
+
 .integrate_vel:
        mov al, [comic_y_vel]
        mov cl, al
        sar al, 1
        sar al, 1
        sar al, 1       ; comic_y_vel / 8
        mov bx, si      ; bl = comic_y, bh = comic_x
        add bl, al      ; comic_y += comic_y_vel / 8
 
        jge .l1         ; did comic_y just become negative (above the top of the screen)?
        mov bl, 0       ; clip to the top of the screen
 
 .l1:
+       add bl, byte [ceiling_stick_flag]       ; push 1 unit downward if we're against a ceiling
+       mov byte [ceiling_stick_flag], 0
+
        mov si, bx      ; return value in si, with modified comic_y
 
        ; Is the top of Comic's head within 3 units of the bottom of the screen
        ; (i.e., his feet are below the bottom of the screen)?
        cmp bl, PLAYFIELD_HEIGHT - 3
        jb .apply_gravity
        jmp comic_dies  ; if so, it's a death by falling
 
 .apply_gravity:
        cmp byte [current_level_number], LEVEL_NUMBER_SPACE     ; is this the space level?
        jne .l2
        sub cl, COMIC_GRAVITY - COMIC_GRAVITY_SPACE     ; if so, low gravity
 .l2:
        add cl, COMIC_GRAVITY   ; otherwise, gravity is normal
 
        ; Clip downward velocity.
        cmp cl, TERMINAL_VELOCITY + 1
        jl .l3
        mov cl, TERMINAL_VELOCITY
 .l3:
        mov [comic_y_vel], cl
 
        ; Adjust comic_x_momentum based on left/right inputs.
 .check_left_input:
        mov cl, [comic_x_momentum]
        ; Is the left key being pressed?
        cmp byte [cs:key_state_left], 1
        jne .check_right_input
        mov byte [comic_facing], COMIC_FACING_LEFT      ; immediately face left when in the air
        dec cl          ; decrement horizontal momentum
        cmp cl, -5
        jge .check_right_input
        mov cl, -5      ; clamp momentum to not go below -5
 
 .check_right_input:
        cmp byte [cs:key_state_right], 1
        jne .check_move_left
        mov byte [comic_facing], COMIC_FACING_RIGHT     ; immediately face right when in the air
        inc cl          ; increment horizontal momentum
        cmp cl, +5
        jle .check_move_left
        mov cl, +5      ; clamp momentum to not go above +5
 
 .check_move_left:
        mov [comic_x_momentum], cl      ; store the possibly modified horizontal momentum
        or cl, cl
        jge .check_move_right
        inc byte [comic_x_momentum]     ; drag momentum towards 0
        call move_left
 .check_move_right:
        mov al, [comic_x_momentum]
        or al, al
        jle .check_solidity_upward
        dec byte [comic_x_momentum]     ; drag momentum towards 0
        call move_right
 
        ; While moving upward, we check the solidity of the tile Comic's head
        ; is in. While moving downward, we instead check the solidity of the
        ; tile under his feet.
 .check_solidity_upward:
        mov ax, si      ; al = comic_y, ah = comic_x
        call address_of_tile_at_coordinates     ; find the tile Comic's head is in
        mov dl, [tileset_last_passable]
        cmp [bx], dl    ; is it a solid tile?
        jg .head_in_solid_tile
 
        test ah, 1      ; is Comic halfway between two tiles (comic_x is odd)?
        je .check_solidity_downward
        cmp [bx + 1], dl        ; if so, also check the solidity of the tile 1 to right
        jle .check_solidity_downward
 
 .head_in_solid_tile:
-       mov byte [comic_y_vel], 8       ; bounce downward off the ceiling
+       ; Comic's head is in a solid tile. Are we even moving upward though?
+       cmp byte [comic_y_vel], 0
+       jg .check_solidity_downward
+
+       ; Comic's head has hit a solid tile while jumping upward. Set the
+       ; ceiling-stick flag and reset his vertical velocity to 0. This doesn't
+       ; immediately terminate the jump; Comic can stick to the ceiling as
+       ; long as comic_jump_counter > 1.
+       mov byte [ceiling_stick_flag], 1
+       mov byte [comic_y_vel], 0
+       jmp .still_falling_or_jumping
 
 .check_solidity_downward:
        ; Are we even moving downward?
        cmp byte [comic_y_vel], 0
        jle .still_falling_or_jumping
 
        mov cx, si      ; cl = comic_y, ch = comic_x
        mov ax, cx      ; al = comic_y, ah = comic_x
        add al, 5       ; comic_y + 5, the y-coordinate 1 unit below Comic's feet
        call address_of_tile_at_coordinates
        mov dl, [tileset_last_passable]
        cmp [bx], dl    ; is it a solid tile?
        jg .hit_the_ground
 
        test ah, 1      ; is Comic halfway between two tiles (comic_x is odd)?
        je .still_falling_or_jumping
        cmp [bx + 1], dl        ; if so, also check the solidity of the tile 1 to right
        jg .hit_the_ground
 
 .still_falling_or_jumping:
        mov byte [comic_animation], COMIC_JUMPING
        jmp game_loop.check_pause_input ; jump back into game_loop, skipping its open/teleport/left/right/collision code
 
 .hit_the_ground:
        ; Above, under .check_solidity_downward, we checked the tile at
        ; (comic_x, comic_y + 5) and found it to be solid. But if comic_y is 15
        ; or greater (i.e., comic_y + 5 is 20 or greater), that tile lookup
        ; actually looked at garbage data outside the map. Override the earlier
        ; decision and act as if the tile had not been solid, because there can
        ; be nothing solid below the bottom of the map.
        cmp cl, PLAYFIELD_HEIGHT - 5
        jb .l4
        jmp .still_falling_or_jumping
 .l4:
        ; Now we actually are about to hit the ground. Clamp Comic's feet to an
        ; even tile boundary and reset his vertical velocity to 0.
        inc cl
        and cl, 0xfe    ; clamp to an even tile boundary: comic_y = floor((comic_y + 1)/2)*2
        mov si, cx      ; return value in si
        mov byte [comic_is_falling_or_jumping], 0       ; no longer falling or jumping
        mov byte [comic_y_vel], 0       ; stop moving vertically
        jmp game_loop.check_pause_input ; jump back into game_loop, skipping its open/teleport/left/right/collision code
+ceiling_stick_flag     db      0       ; is Comic jumping upwards with his head against a ceiling?


R4 Startup Notice

 STARTUP_NOTICE_TEXT:   xor_encrypt `\
-                The Adventures of Captain Comic  --  Revision 3\r\n\
+               The Adventures of Captain Comic  --  Revision 4\r\n\
                      Copyright 1988, 1989 by Michael Denio\r\n\
 \r\n\
   This software is being distributed under the Shareware concept, where you as\r\n\
   the user  are  allowed  to use the program on a "trial" basis.  If you enjoy\r\n\
   playing  Captain  Comic,  you  are encouraged to register yourself as a user\r\n\
-  with a $10 to $20 contribution. Registered users will be given access to the\r\n\
-  official Captain Comic  question hotline (my home phone number), and will be\r\n\
-  the first in line to receive new Comic adventures.\r\n\
+  with a $10 to $20 contribution.   Registered users will be the first in line\r\n\
+  to receive new Comic adventures.\r\n\
 \r\n\
         This product is copyrighted material, but may be re-distributed\r\n\
                  by complying to these two simple restrictions:\r\n\
 \r\n\
         1. The program and graphics (including world maps) may not be\r\n\
            distributed in any modified form.\r\n\
         2. No form of compensation is be collected from the distribution\r\n\
            of this program, including any disk handling costs or BBS\r\n\
            file club fees.\r\n\
 \r\n\
     Questions and contributions can be sent to me at the following address:\r\n\
                                 Michael A. Denio\r\n\
-                             1420 W. Glen Ave #202\r\n\
-                                Peoria, IL 61614\r\n\
+                           15700 Lexington Blvd #1010\r\n\
+                              Sugar Land, TX 77478\r\n\
 \r\n\
-      Press \'K\' to define the keyboard  ---  Press any other key to begin\
+                          Press \'J\' for Joystick Play.\r\n\
+      Press \'K\' to define the keyboard  ---  Press any other key to begin.\r\
 \0\0`

R4 Joystick

+; Joystick calibration settings, set by calibrate_joystick and used in
+; int8_handler.
+joystick_x_zero                dw      0
+joystick_y_zero                dw      0
+joystick_x_low         dw      0
+joystick_x_high                dw      0
+joystick_y_low         dw      0
+joystick_y_high                dw      0
+joystick_is_calibrated dw      0
+setup_keyboard_trampoline:
+       jmp setup_keyboard
+
 ; Display STARTUP_NOTICE_TEXT and wait for a keypress to configure the
-; keyboard, quit, or begin the game.
+; keyboard, configure the joystick, quit, or begin the game.
 display_startup_notice:
        call display_xor_decrypt        ; decrypt and display STARTUP_NOTICE_TEXT
 
        xor ax, ax      ; ah=0x00: get keystroke; returned al is ASCII code
        int 0x16
        cmp al, 'k'
-       je setup_keyboard
+       je setup_keyboard_trampoline
        cmp al, 'K'
-       je setup_keyboard
+       je setup_keyboard_trampoline
        cmp al, 27      ; Escape
        jne .not_esc
        jmp terminate_program   ; Escape allows exiting directly from the startup notice screen
 .not_esc:
+       cmp al, 'j'
+       je calibrate_joystick
+       cmp al, 'J'
+       je calibrate_joystick
+
+       jmp calibrate_joystick.check_ega_support_trampoline
+
+calibrate_joystick_cancel_trampoline:
+       jmp calibrate_joystick.cancel
+
+; Do the interactive joystick calibration setup. Jump to title_sequence when
+; done, or back to display_startup_notice if calibration is cancelled.
+;
+; In the left and right directions, the thresholds are halfway between the zero
+; value and the respective extreme value.
+;   joystick_x_low  = 1/2 * joystick_x_zero + 1/2 * extreme_left
+;   joystick_x_high = 1/2 * joystick_x_zero + 1/2 * extreme_right
+; In the down and up directions, the threshold is three quarters of the way
+; between the zero value and the respective extreme value.
+;   joystick_y_low  = 1/4 * joystick_y_zero + 3/4 * extreme_up
+;   joystick_y_high = 1/4 * joystick_y_zero + 3/4 * extreme_down
+;
+; Output:
+;   joystick_x_zero = joystick horizontal neutral value
+;   joystick_y_zero = joystick vertical neutral value
+;   joystick_x_low = joystick left threshold
+;   joystick_x_high = joystick right threshold
+;   joystick_y_low = joystick up threshold
+;   joystick_y_high = joystick down threshold
+;   joystick_is_calibrated = 0 if cancelled, 1 if calibrated
+calibrate_joystick:
+       push ds         ; temporarily work relative the data segment containing input-related strings
+       mov ax, input_config_strings
+       mov ds, ax
+
+       ; wait_for_joystick_button_or_keypress interprets recent_scancode != 0
+       ; to mean that a key was pressed, so initialize it to a no-keypress
+       ; state.
+       mov byte [cs:recent_scancode], 0
+
+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
+
+       lea dx, [STR_JOYSTICK_CENTER]   ; "Center Joystick and Press Button"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call wait_for_joystick_button_or_keypress
+       or ax, ax       ; was it a joystick button?
+       je calibrate_joystick_cancel_trampoline ; a keyboard press cancels joystick calibration
+.center:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15
+       mov [cs:joystick_x_zero], ax
+       mov [cs:joystick_y_zero], bx
+
+       lea dx, [STR_JOYSTICK_LEFT]     ; "Press Joystick Left and Press Button"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call wait_for_joystick_button_or_keypress
+       or ax, ax       ; was it a joystick button?
+       jz calibrate_joystick_cancel_trampoline ; a keyboard press cancels joystick calibration
+.x_low:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15        ; x-axis is in ax.
+       mov bx, [cs:joystick_x_zero]
+       sub bx, ax      ; joystick_x_zero - x
+       shr bx, 1       ; (joystick_x_zero - x) / 2
+       add ax, bx      ; x + (joystick_x_zero - x) / 2 = (x + joystick_x_zero) / 2
+       mov [cs:joystick_x_low], ax     ; threshold is halfway between zero and extreme left
+
+       lea dx, [STR_JOYSTICK_RIGHT]    ; "Press Joystick Right and Press Button"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call wait_for_joystick_button_or_keypress
+       or ax, ax       ; was it a joystick button?
+       jz .cancel      ; a keyboard press cancels joystick calibration
+.x_high:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15        ; x-axis is in ax.
+       mov bx, [cs:joystick_x_zero]
+       sub ax, bx      ; x - joystick_x_zero
+       shr ax, 1       ; (x - joystick_x_zero) / 2
+       add bx, ax      ; joystick_x_zero + (x - joystick_x_zero) / 2 = (joystick_x_zero + x) / 2
+       mov [cs:joystick_x_high], bx    ; threshold is halfway between zero and extreme right
+
+       lea dx, [STR_JOYSTICK_UP]       ; "Press Joystick Up and Press Buttton"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call wait_for_joystick_button_or_keypress
+       or ax, ax       ; was it a joystick button?
+       jz .cancel      ; a keyboard press cancels joystick calibration
+.y_low:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15        ; y-axis is in bx.
+       mov ax, [cs:joystick_y_zero]
+       sub ax, bx      ; joystick_y_zero - y
+       shr ax, 1       ; (joystick_y_zero - y) / 2
+       shr ax, 1       ; (joystick_y_zero - y) / 4
+       add bx, ax      ; y + (joystick_y_zero - y) / 4 = 1/4 * joystick_y_zero + 3/4 * y
+       mov [cs:joystick_y_low], bx     ; threshold is 3/4 of the way between zero and extreme up
+
+       lea dx, [STR_JOYSTICK_DOWN]     ; "Press Joystick Down and Press Button"
+       mov ah, 0x09    ; ah=0x09: write string to standard output
+       int 0x21
+       call wait_for_joystick_button_or_keypress
+       or ax, ax       ; was it a joystick button?
+       jz .cancel      ; a keyboard press cancels joystick calibration
+.y_high:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15        ; y-axis is in bx.
+       mov ax, [cs:joystick_y_zero]
+       sub bx, ax      ; y - joystick_y_zero
+       shr bx, 1       ; (y - joystick_y_zero) / 2
+       add ax, bx      ; joystick_y_zero + (y - joystick_y_zero) / 2
+       shr bx, 1       ; (y - joystick_y_zero) / 4
+       add ax, bx      ; joystick_y_zero + (y - joystick_y_zero) / 2 + (y - joystick_y_zero) / 4 = 1/4 * joystick_y_zero + 3/4 * y
+       mov [cs:joystick_y_high], ax    ; threshold is 3/4 of the way between zero and extreme down
+
+       pop ds          ; restore the original data segment
+       mov word [cs:joystick_is_calibrated], 1
+
+.check_ega_support_trampoline:
        jmp check_ega_support
 
+.cancel:
+       pop ds
+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
+
+       lea bx, [STARTUP_NOTICE_TEXT]
+
+       ; Clear the BIOS keyboard buffer: http://www.fysnet.net/kbuffio.htm.
+       xor ax, ax
+       mov es, ax      ; temporarily set es = 0x0000
+       cli             ; disable interrupts
+       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
+       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+       sti             ; enable interrupts
+
+       jmp display_startup_notice
+; Wait for a joystick switch press and release, or for a keypress, whichever
+; happens first.
+; Output:
+;   ax = 0 if a keypress, 1 if a joystick switch press and release
+wait_for_joystick_button_or_keypress:
+.press:
+       ; Wait for a switch press.
+       mov ax, 2       ; wait 2 ticks
+       call wait_n_ticks       ; sets ax = 0
+       cmp byte [cs:recent_scancode], 0        ; was a key pressed?
+       jne .return_key
+
+       mov ah, 0x84    ; ah=0x84: joystick
+       xor dx, dx      ; dx=0: read joystick switches
+       int 0x15
+       test al, 0x10   ; fire switch pressed?
+       je .press
+       test al, 0x20   ; jump switch pressed?
+       je .press
+
+.release:
+       ; Wait for a switch release.
+       xor ax, ax      ; ax = 0
+       cmp byte [cs:recent_scancode], 0        ; was a key pressed?
+       jne .return_key
+
+       mov ah, 0x84    ; ah=0x84: joystick
+       xor dx, dx      ; dx=0: read joystick switches
+       int 0x15
+       test al, 0x10   ; fire switch released?
+       je .return_joystick
+       test al, 0x20   ; jump switch released?
+       jne .release
+
+.return_joystick:
+       mov ax, 1
+.return_key:
+       ret
 ; INT 8 is called for every cycle of the programmable interval timer (IRQ 0).
-; Poll the F1 and F2 keys (on odd interrupts only) and advance the current
-; sound playback. Tail call into the original INT 8 handler.
+; Poll the joystick and F1 and F2 keys (on odd interrupts only) and advance the
+; current sound playback. Tail call into the original INT 8 handler.
 ; Input:
 ;   saved_int8_handler_offset:saved_int8_handler_segment = address of original INT 8 handler
 ;   irq0_parity = even/odd counter of calls to this interrupt handler
+;   joystick_is_calibrated = boolean controlling whether to read the joystick
+;   joystick_x_low = joystick left threshold
+;   joystick_x_high = joystick right threshold
+;   joystick_y_low = joystick up threshold
+;   joystick_y_high = joystick down threshold
 ;   key_state_f1 = if 1, unmute the sound
 ;   key_state_f2 = if 1, mute the sound
 ;   sound_is_playing = if 1, deal with the currently playing sound
 ;   sound_data_segment:sound_data_offset = address of the current sound
 ;   sound_note_counter = how many more interrupts to continue playing the current note in the current sound
 ;   sound_is_enabled = if 1, actually send sound data to the PC speaker
 ; Output:
 ;   irq0_parity = opposite of its input value
 ;   game_tick_flag = set to 1 if irq0_parity was odd
+;   key_state_jump = set to 1 if the joystick jump switch is pressed
+;   key_state_fire = set to 1 if the joystick fire switch is pressed
+;   key_state_right = set to 1 if the joystick is pressed right
+;   key_state_left = set to 1 if the joystick is pressed left
+;   key_state_open = set to 1 if the joystick is pressed up
+;   key_state_teleport = set to 1 if the joystick is pressed down
 ;   sound_data_offset = advanced by 4 bytes if a note transition occurred
 ;   sound_is_playing = set to 0 if the current sound finished playing
 ;   sound_priority = set to 0 if the current sound finished playing
 int8_handler:
        push ax
+       push bx
+       push cx
+       push dx
+       push es
 
        ; irq0_parity keeps track of whether we are in an even or an odd call
        ; to this interrupt handler. irq0_parity advances 0→1 or 1→0 on each
        ; interrupt.
        mov al, [cs:irq0_parity]
        inc al
        cmp al, 2       ; did irq0_parity overflow to 2?
-       jl .store_irq0_parity   ; irq0_parity was 0, now is 1
-       ; Otherwise irq0_parity was 1, now is 2 (and will become 1→0 at .wrap_irq0_parity below).
+       jge .irq0_parity_odd    ; irq0_parity was 1, now is 2 (and will become 1→0 at .wrap_irq0_parity below)
+       jmp .store_irq0_parity  ; irq0_parity was 0, now is 1
 
 .irq0_parity_odd:
-       ; On odd interrupts, poll the F1/F2 keys.
+       ; On odd interrupts, poll the joystick and F1/F2 keys.
        mov byte [cs:game_tick_flag], 1 ; flag the beginning of a game tick
+       cmp word [cs:joystick_is_calibrated], 1 ; is the joystick calibrated?
+       je .joystick    ; if so, read it
+       jmp .try_F1     ; if not, go straight to checking F1/F2
+       nop             ; dead code
+
+.joystick:
+       mov byte [cs:key_state_jump], 0
+       mov byte [cs:key_state_fire], 0
+       mov byte [cs:key_state_right], 0
+       mov byte [cs:key_state_left], 0
+       mov byte [cs:key_state_open], 0
+       mov byte [cs:key_state_teleport], 0
+
+.joystick_switches:
+       mov ah, 0x84    ; ah=0x84: joystick
+       xor dx, dx      ; dx=0: read joystick switches
+       int 0x15
+.joystick_try_jump_switch:
+       test al, 0x20   ; al=0x20: test switch state
+       jne .joystick_try_fire_switch
+       mov byte [cs:key_state_jump], 1
+.joystick_try_fire_switch:
+       test al, 0x10   ; al=0x10: test switch state
+       jne .joystick_axes
+       mov byte [cs:key_state_fire], 1
+
+.joystick_axes:
+       mov ah, 0x84    ; ah=0x84: joystick
+       mov dx, 1       ; dx=1: read joystick axes
+       int 0x15
+       ; ax is the x-axis position
+       ; bx is the y-axis position
+.joystick_try_left:
+       cmp ax, [cs:joystick_x_low]
+       jg .joystick_try_right
+       mov byte [cs:key_state_left], 1
+       jmp .joystick_try_up
+.joystick_try_right:
+       cmp ax, [cs:joystick_x_high]
+       jl .joystick_try_up
+       mov byte [cs:key_state_right], 1
+
+.joystick_try_up:
+       cmp bx, [cs:joystick_y_low]
+       jg .joystick_try_down
+       mov byte [cs:key_state_open], 1
+       jmp .try_F1
+.joystick_try_down:
+       cmp bx, [cs:joystick_y_high]
+       jl .try_F1
+       mov byte [cs:key_state_teleport], 1
 
 .try_F1:
        cmp byte [cs:key_state_f1], 1
        jne .try_F2
        mov ax, SOUND_UNMUTE
        int3            ; call sound control interrupt
 .try_F2:
        cmp byte [cs:key_state_f2], 1
        jne .wrap_irq0_parity
        mov ax, SOUND_MUTE
        int3            ; call sound control interrupt
 
 .wrap_irq0_parity:
        xor al, al      ; wrap irq0_parity to 0
 
 .store_irq0_parity:
        ; Store the new value of irq0_parity, which has just transitioned
        ; either 0→1 or 1→0.
        mov [cs:irq0_parity], al
 
 .sound:
-       push es
-       push bx
-       push dx
        ; Deal with sound on both even and odd interrupts.
        mov ax, [cs:sound_data_segment]
        mov es, ax
        cmp byte [cs:sound_is_playing], 1
        jne .disable_sound_output
 
        ; A sound is currently playing.
        dec word [cs:sound_note_counter]        ; is the current note over yet?
        jg .return      ; if not, we're done for now
        ; Time to change to the next note. The data for the next note is stored
        ; as 2 consecutive words. The first is a frequency divider value, and
        ; the second is a duration.
        mov bx, [cs:sound_data_offset]
        add word [cs:sound_data_offset], 2      ; get the next frequency divider
        mov ax, [es:bx]
        or ax, ax       ; a frequency divider of 0 is a sentinel indicating end of sound
        jz .sound_finished
 
        ; Program the frequency divider into PIT channel 2 (connected to the PC
        ; speaker).
        mov bx, ax
        in al, 0x61
        and al, 0xfc    ; disable sound output while we change settings
        out 0x61, al
        ; Write the frequency divider value.
        ; https://wiki.osdev.org/Programmable_Interval_Timer#I.2FO_Ports
        ; 0xb6 = 0b10110110
        ;          10 = channel 2 (connected to the PC speaker)
        ;            11 = access mode lobyte/hibyte
        ;              011 = mode 3 (square wave generator)
        ;                 0 = 16-bit binary
        mov al, 0xb6
        out 0x43, al    ; PIT mode/command register
        mov al, bl      ; low byte of frequency divider
        out 0x42, al    ; PIT channel 2 data port
        mov al, bh      ; high byte of frequency divider
        out 0x42, al    ; PIT channel 2 data port
 
        mov bx, [cs:sound_data_offset]
        add word [cs:sound_data_offset], 2      ; get the duration of the next note
        mov ax, [es:bx]
        mov [cs:sound_note_counter], ax         ; and store in sound_note_counter
 
        cmp byte [cs:sound_is_enabled], 1       ; if the sound is muted, just return
        jne .return
 
        in al, 0x61
        or al, 0x03     ; enable sound output
        out 0x61, al
        jmp .return
 
 .sound_finished:
        mov byte [cs:sound_is_playing], 0
        mov byte [cs:sound_priority], 0
 
 .disable_sound_output:
        in al, 0x61
        and al, 0xfc    ; disable sound output
        out 0x61, al
 
 .return:
+       pop es
        pop dx
+       pop cx
        pop bx
-       pop es
        pop ax
        jmp far [cs:saved_int8_handler_offset]  ; tail call into the original interrupt handler
+STR_JOYSTICK_CENTER    db      `\n\n\n\n\n\n\r                               Calibrate Joystick\r\n                             Press any key to abort\n\n\r                       Center Joystick and Press Button$`
+STR_JOYSTICK_LEFT      db      `\n\r                       Press Joystick Left and Press Button$`
+STR_JOYSTICK_RIGHT     db      `\n\r                       Press Joystick Right and Press Button$`
+STR_JOYSTICK_UP                db      `\n\r                       Press Joystick Up and Press Buttton$`
+STR_JOYSTICK_DOWN      db      `\n\r                       Press Joystick Down and Press Button$`


R5 Copyright Notice

-; Dead code: executable actually starts at ..start below, according to the EXE
-; header.
-       jmp main
-       nop
+db `\n\r\n\rCaptain Comic I - Planet of Death, Version SH1.0\n\r`
+db `Copyright 1990 by Michael A. Denio\n\r\x1a`

R5 Keymap

-STR_DEFINE_KEYS                db      `\n\n\n\n\n\n\r                                   Define Keys\n$`
-STR_MOVE_LEFT          db      `\n\r                                Move Left  : $`
-STR_MOVE_RIGHT         db      `\n\r                                Move Right : $`
-STR_JUMP               db      `\n\r                                Jump       : $`
-STR_FIREBALL           db      `\n\r                                Fireball   : $`
-STR_OPEN_DOOR          db      `\n\r                                Open Door  : $`
-STR_TELEPORT           db      `\n\r                                Teleport   : $`
-STR_THIS_SETUP_OK      db      `\n\n\r                                This setup OK? (y/n)$`
-STR_SAVE_SETUP_TO_DISK db      `\n\r                             Save setup to disk? (y/n)$`
+STR_DEFINE_KEYS                db      `\n\n\n\n\n\n\r                                  Define Keys\n$`
+STR_MOVE_LEFT          db      `\n\r                               Move Left  : $`
+STR_MOVE_RIGHT         db      `\n\r                               Move Right : $`
+STR_JUMP               db      `\n\r                               Jump       : $`
+STR_FIREBALL           db      `\n\r                               Fireball   : $`
+STR_OPEN_DOOR          db      `\n\r                               Open Door  : $`
+STR_TELEPORT           db      `\n\r                               Teleport   : $`
+STR_THIS_SETUP_OK      db      `\n\n\r                               This setup OK? (y/n)$`
+STR_SAVE_SETUP_TO_DISK db      `\n\r                            Save setup to disk? (y/n)$`

R5 Joystick Speed Loop

+; How many `in` instructions to wait for joystick inputs to converge. Set by
+; main.cpu_speed_loop and used by read_joystick_axis.
+max_joystick_reads     dw      0
+       ; Measure the CPU speed in order to calibrate the joystick. First, wait
+       ; until the beginning of a tick interval.
+       mov ax, 1
+       call wait_n_ticks       ; sets game_tick_flag = 0
+
+       ; Count how many `in` instructions we can run in a loop during one game
+       ; tick.
+       mov cx, -1      ; the counter starts at -1 and counts downward
+.cpu_speed_loop:
+       in al, dx       ; dummy `in` instruction
+       cmp byte [cs:game_tick_flag], 1 ; has a game tick elapsed yet?
+       je .finished    ; just turned over a new game tick
+       loop .cpu_speed_loop
+       ; If no tick happened before cx made a full cycle, assign a default.
+       mov word [cs:max_joystick_reads], 1280
+       jmp .save_video_mode
+
+.finished:
+       neg cx          ; flip count from negative to positive
+       mov ax, cx      ; ax = count
+       xor dx, dx      ; dividend is 32-bit dx:ax
+       mov cx, 28      ; divide by 28
+       div cx          ; ax = floor(dx:ax / cx)
+       mov [cs:max_joystick_reads], ax ; max_joystick_reads = count/28
+       ; Inflate the (divided) count by 25% of the interval between it and
+       ; 1280, unless already greater than 1280. We are computing this
+       ; weighted average:
+       ;   max_joystick_reads = max(count/28, 0.75 * (count/28) + 0.25 * 1280)
+       mov cx, 1280
+       sub cx, ax      ; 1280 - count/28
+       jb .save_video_mode     ; count/28 >= 1280, don't average
+       shr cx, 1
+       shr cx, 1       ; 0.25 * (1280 - count/28)
+       add [cs:max_joystick_reads], cx ; max_joystick_reads = count/28 + 0.25 * (1280 - count/28)
+                                       ;                    = count/28 - 0.25 * count/28 + 0.25 * 1280
+                                       ;                    = 0.75 * count/28 + 0.25 * 1280

R5 Joystick Thresholds

 ; Do the interactive joystick calibration setup. Jump to title_sequence when
 ; done, or back to display_startup_notice if calibration is cancelled.
 ;
-; In the left and right directions, the thresholds are halfway between the zero
-; value and the respective extreme value.
+; It looks like the code intends to set the left/right/up/down thresholds to be
+; halfway between the zero value and the respective extreme value. But it gets
+; the calculation wrong in the left and up cases. Instead of the correct
 ;   joystick_x_low  = 1/2 * joystick_x_zero + 1/2 * extreme_left
+;   joystick_y_low  = 1/2 * joystick_y_zero + 1/2 * extreme_up
+; it does the incorrect
+;   joystick_x_low  = 1/2 * joystick_x_zero - 1/2 * extreme_left
+;   joystick_y_low  = 1/2 * joystick_y_zero - 1/2 * extreme_up
+; which means that the threshold values may be even smaller than the extreme
+; values. The right and down cases are done correctly:
 ;   joystick_x_high = 1/2 * joystick_x_zero + 1/2 * extreme_right
-; In the down and up directions, the threshold is three quarters of the way
-; between the zero value and the respective extreme value.
-;   joystick_y_low  = 1/4 * joystick_y_zero + 3/4 * extreme_up
-;   joystick_y_high = 1/4 * joystick_y_zero + 3/4 * extreme_down
+;   joystick_y_high = 1/2 * joystick_y_zero + 1/2 * extreme_down
 ;
 ; Output:
 ;   joystick_x_zero = joystick horizontal neutral value
 ;   joystick_y_zero = joystick vertical neutral value
 ;   joystick_x_low = joystick left threshold
 ;   joystick_x_high = joystick right threshold
 ;   joystick_y_low = joystick up threshold
 ;   joystick_y_high = joystick down threshold
 ;   joystick_is_calibrated = 0 if cancelled, 1 if calibrated
 calibrate_joystick:
+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
+
        push ds         ; temporarily work relative the data segment containing input-related strings
        mov ax, input_config_strings
        mov ds, ax
 
        ; wait_for_joystick_button_or_keypress interprets recent_scancode != 0
        ; to mean that a key was pressed, so initialize it to a no-keypress
        ; state.
        mov byte [cs:recent_scancode], 0
 
-       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
-       int 0x10
-
        lea dx, [STR_JOYSTICK_CENTER]   ; "Center Joystick and Press Button"
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
        call wait_for_joystick_button_or_keypress
        or ax, ax       ; was it a joystick button?
-       je calibrate_joystick_cancel_trampoline ; a keyboard press cancels joystick calibration
+       jnz .center
+       jmp .cancel     ; a keyboard press cancels joystick calibration
 .center:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
-       int 0x15
+       int 0x15        ; call int21_handler
        mov [cs:joystick_x_zero], ax
        mov [cs:joystick_y_zero], bx
 
        lea dx, [STR_JOYSTICK_LEFT]     ; "Press Joystick Left and Press Button"
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
        call wait_for_joystick_button_or_keypress
        or ax, ax       ; was it a joystick button?
-       jz calibrate_joystick_cancel_trampoline ; a keyboard press cancels joystick calibration
+       jnz .x_low
+       jmp .cancel     ; a keypress cancels joystick calibration
 .x_low:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
        int 0x15        ; x-axis is in ax.
        mov bx, [cs:joystick_x_zero]
        sub bx, ax      ; joystick_x_zero - x
        shr bx, 1       ; (joystick_x_zero - x) / 2
-       add ax, bx      ; x + (joystick_x_zero - x) / 2 = (x + joystick_x_zero) / 2
-       mov [cs:joystick_x_low], ax     ; threshold is halfway between zero and extreme left
+       ; missing `add bx, ax` here
+       mov [cs:joystick_x_low], bx     ; threshold is half the difference between zero and extreme left (looks like a bug)
 
        lea dx, [STR_JOYSTICK_RIGHT]    ; "Press Joystick Right and Press Button"
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
        call wait_for_joystick_button_or_keypress
        or ax, ax       ; was it a joystick button?
-       jz .cancel      ; a keyboard press cancels joystick calibration
+       jnz .x_high
+       jmp .cancel     ; a keypress cancels joystick calibration
+       nop             ; dead code
 .x_high:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
        int 0x15        ; x-axis is in ax.
        mov bx, [cs:joystick_x_zero]
        sub ax, bx      ; x - joystick_x_zero
        shr ax, 1       ; (x - joystick_x_zero) / 2
        add bx, ax      ; joystick_x_zero + (x - joystick_x_zero) / 2 = (joystick_x_zero + x) / 2
        mov [cs:joystick_x_high], bx    ; threshold is halfway between zero and extreme right
 
        lea dx, [STR_JOYSTICK_UP]       ; "Press Joystick Up and Press Buttton"
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
        call wait_for_joystick_button_or_keypress
        or ax, ax       ; was it a joystick button?
-       jz .cancel      ; a keyboard press cancels joystick calibration
+       jnz .y_low
+       jmp .cancel     ; a keypress cancels joystick calibration
+       nop             ; dead code
 .y_low:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
        int 0x15        ; y-axis is in bx.
        mov ax, [cs:joystick_y_zero]
        sub ax, bx      ; joystick_y_zero - y
        shr ax, 1       ; (joystick_y_zero - y) / 2
-       shr ax, 1       ; (joystick_y_zero - y) / 4
-       add bx, ax      ; y + (joystick_y_zero - y) / 4 = 1/4 * joystick_y_zero + 3/4 * y
-       mov [cs:joystick_y_low], bx     ; threshold is 3/4 of the way between zero and extreme up
+       ; missing `add ax, bx` here
+       mov [cs:joystick_y_low], ax     ; threshold is half the difference between zero and extreme up (looks like a bug)
 
        lea dx, [STR_JOYSTICK_DOWN]     ; "Press Joystick Down and Press Button"
        mov ah, 0x09    ; ah=0x09: write string to standard output
        int 0x21
        call wait_for_joystick_button_or_keypress
        or ax, ax       ; was it a joystick button?
-       jz .cancel      ; a keyboard press cancels joystick calibration
+       jnz .y_high
+       jmp .cancel     ; a keypress cancels joystick calibration
+       nop             ; dead code
 .y_high:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
        int 0x15        ; y-axis is in bx.
        mov ax, [cs:joystick_y_zero]
        sub bx, ax      ; y - joystick_y_zero
        shr bx, 1       ; (y - joystick_y_zero) / 2
-       add ax, bx      ; joystick_y_zero + (y - joystick_y_zero) / 2
-       shr bx, 1       ; (y - joystick_y_zero) / 4
-       add ax, bx      ; joystick_y_zero + (y - joystick_y_zero) / 2 + (y - joystick_y_zero) / 4 = 1/4 * joystick_y_zero + 3/4 * y
-       mov [cs:joystick_y_high], ax    ; threshold is 3/4 of the way between zero and extreme down
+       add ax, bx      ; joystick_y_zero + (y - joystick_y_zero) / 2 = (joystick_y_zero + y) / 2
+       mov [cs:joystick_y_high], ax    ; threshold is halfway between zero and extreme down
 
        pop ds          ; restore the original data segment
        mov word [cs:joystick_is_calibrated], 1
-
-.check_ega_support_trampoline:
-       jmp check_ega_support
+       jmp title_sequence
+       nop             ; dead code
 
 .cancel:

R5 Joystick Interrupt Handler

 ; Wait for a joystick switch press and release, or for a keypress, whichever
 ; happens first.
 ; Output:
 ;   ax = 0 if a keypress, 1 if a joystick switch press and release
 wait_for_joystick_button_or_keypress:
 .press:
        ; Wait for a switch press.
        mov ax, 2       ; wait 2 ticks
        call wait_n_ticks       ; sets ax = 0
        cmp byte [cs:recent_scancode], 0        ; was a key pressed?
        jne .return_key
 
        mov ah, 0x84    ; ah=0x84: joystick
        xor dx, dx      ; dx=0: read joystick switches
-       int 0x15
+       int 0x15        ; call int21_handler
        test al, 0x10   ; fire switch pressed?
        je .press
        test al, 0x20   ; jump switch pressed?
        je .press
 
 .release:
        ; Wait for a switch release.
        xor ax, ax      ; ax = 0
        cmp byte [cs:recent_scancode], 0        ; was a key pressed?
        jne .return_key
 
        mov ah, 0x84    ; ah=0x84: joystick
        xor dx, dx      ; dx=0: read joystick switches
-       int 0x15
+       int 0x15        ; call int21_handler
        test al, 0x10   ; fire switch released?
        je .return_joystick
        test al, 0x20   ; jump switch released?
        jne .release
 
 .return_joystick:
        mov ax, 1
 .return_key:
        ret
 .joystick_switches:
        mov ah, 0x84    ; ah=0x84: joystick
        xor dx, dx      ; dx=0: read joystick switches
-       int 0x15
+       int 0x15        ; call int21_handler
 .joystick_try_jump_switch:
        test al, 0x20   ; al=0x20: test switch state
        jne .joystick_try_fire_switch
        mov byte [cs:key_state_jump], 1
 .joystick_try_fire_switch:
        test al, 0x10   ; al=0x10: test switch state
        jne .joystick_axes
        mov byte [cs:key_state_fire], 1
 
 .joystick_axes:
        mov ah, 0x84    ; ah=0x84: joystick
        mov dx, 1       ; dx=1: read joystick axes
-       int 0x15
+       int 0x15        ; call int21_handler
        ; ax is the x-axis position
        ; bx is the y-axis position
 .joystick_try_left:
        cmp ax, [cs:joystick_x_low]
        jg .joystick_try_right
        mov byte [cs:key_state_left], 1
        jmp .joystick_try_up
 .joystick_try_right:
        cmp ax, [cs:joystick_x_high]
        jl .joystick_try_up
        mov byte [cs:key_state_right], 1
+saved_int21_handler_offset     dw      0
+saved_int21_handler_segment    dw      0
+; INT 21 is for joystick support. INT 21, ah=0x84 is the BIOS call for joystick
+; support. This interrupt hijacks calls that have ah=0x84, and passes all
+; others through to the original INT 21 handler. This may be because INT 21, as
+; https://wiki.osdev.org/Game_port#Programming_the_game_port says, "is poorly
+; supported, and most BIOSes have a buggy implementation."
+; Input:
+;   saved_int21_handler_segment:saved_int21_handler_offset = address of original INT 21 handler
+;   ah = 0x84
+;   dx = 0 to read joystick switches, 1 to read joystick axes
+; Output for ah=0x84, dx=0:
+;   al = bitmap of switch status
+;        0x10 = joystick A fire switch
+;        0x20 = joystick A jump switch
+;        0x40 = joystick B fire switch
+;        0x80 = joystick B jump switch
+; Output for ah=0x84, dx=1:
+;   ax = joystick A x-axis
+;   bx = joystick A y-axis
+;   cx = joystick B x-axis
+;   dx = joystick B y-axis
+int21_handler:
+       cmp ah, 0x84    ; we only handle ah=0x84: joystick support
+       je .ok
+       jmp goto_saved_int21_handler    ; all other values of ah we pass to the original handler
+.ok:
+       cmp dl, 1       ; dx is the subfunction: 0 = read joystick switches; 1 = read joystick axes
+       jg .return      ; if dx != 0 && dx != 1, return
+       mov dx, 0x201   ; read/write joystick status: https://wiki.osdev.org/Game_port#Programming_the_game_port
+       je .axes        ; if dl == 1, read axes, otherwise read switches
+.switches:
+       in al, dx       ; read switches from the joystick port
+       and al, 0xf0
+       jmp .return
+       nop             ; dead code
+.axes:
+       mov bl, 1       ; joystick A x-axis
+       call read_joystick_axis
+       push cx
+       shl bl, 1       ; joystick A y-axis
+       call read_joystick_axis
+       push cx
+       shl bl, 1       ; joystick B x-axis
+       call read_joystick_axis
+       push cx
+       shl bl, 1       ; joystick B y-axis
+       call read_joystick_axis
+       mov dx, cx      ; dx = joystick B y-axis
+       pop cx          ; cx = joystick B x-axis
+       pop bx          ; bx = joystick A y-axis
+       pop ax          ; ax = joystick A x-axis
+.return:
+       iret
+
+; Read a single joystick axis.
+; Input:
+;   bl = axis selection:
+;     bl=1: joystick A x-axis
+;     bl=2: joystick A y-axis
+;     bl=4: joystick B x-axis
+;     bl=8: joystick B y-axis
+;   dx = I/O port to use, should be 0x0201.
+; Output:
+;   cx = 1/16 of the number of PIT oscillations it took for the selected
+;     axis reading to settle in, or 0 if max_joystick_reads was reached
+read_joystick_axis:
+       ; The overall procedure is:
+       ;   1. read the current value of the PIT counter
+       ;   2. `out` to port 0x201, to set all axis bits to 1
+       ;   3. loop until the bit for the selected axis becomes 0
+       ;   4. read the new value of the PIT counter
+       ;   5. subtract the two counter values and divide by 16
+       ;   6. keep looping until the other axes also become 0
+       ; https://www.dsi.unive.it/~franz/c_program/joystick.htm
+       ; https://wiki.osdev.org/Game_port#Programming_the_game_port
+       cli
+.pre:
+       call pit_count  ; get the pre-read PIT counter in ax
+       push ax
+       out dx, al      ; write to the port to start the procedure (the value doesn't matter)
+
+       mov cx, [cs:max_joystick_reads] ; bail out after this many iterations even if the bit has not become 0
+.selected_axis_loop:
+       in al, dx       ; get the joystick bits
+       test al, bl     ; has the bit for the selected axis become 0?
+       loopne .selected_axis_loop
+
+       or cx, cx       ; was max_joystick_reads exhausted?
+       jnz .post
+       pop ax          ; if so, discard the pre-read PIT counter and take the difference to be 0
+       jmp .finish     ; looks like a bug here: this branch skips the `sti` that undoes the earlier `cli`
+
+.post:
+       call pit_count  ; get the post-read PIT counter in ax
+       ; Subtract the pre-read PIT counter from the post-read PIT counter,
+       ; accounting for wraparound.
+       ; (I actually don't know why the wraparound case is checked separately;
+       ; two's complement should make both paths below the same.)
+       pop cx
+       cmp cx, ax
+       jg .no_wraparound
+.wraparound:
+       neg ax
+       add cx, ax      ; cx = cx + (-ax)
+       jmp .l1
+.no_wraparound:
+       sub cx, ax      ; cx = cx - ax
+.l1:
+       ; cx contains the difference of counters. Divide by 16.
+       shr cx, 1
+       shr cx, 1
+       shr cx, 1
+       shr cx, 1
+       and ch, 0x01    ; throw away the 3 high bits after shifting (not sure what this is for)
+       sti
+
+.finish:
+       push cx         ; divided difference of PIT counters
+       mov cx, [cs:max_joystick_reads]
+.all_axes_loop:
+       in al, dx
+       test al, 0xf
+       loopne .all_axes_loop
+       pop cx          ; divided difference of PIT counters
+       ret
+
+; Get the current value of the PIT counter. The counter increments once every
+; (approximately) 1/1.193e6 seconds = 0.84 microseconds. It wraps every 5.5
+; milliseconds.
+; Output:
+;   ax = counter value
+pit_count:
+       ; https://wiki.osdev.org/Programmable_Interval_Timer#Reading_The_Current_Count
+       ; https://wiki.osdev.org/Programmable_Interval_Timer#I.2FO_Ports
+       ; al = 0b00000000
+       ;        00 = channel 0
+       ;          00 = latch count value command
+       ;            000 = mode 0
+       ;               0 = 16-bit binary
+       mov al, 0
+       out 0x43, al    ; send the latch command
+       ; I don't know the purpose of these jumps. To cause a delay?
+       jmp .l1
+.l1:
+       jmp .l2
+.l2:
+       in al, 0x40     ; low byte of count
+       mov ah, al
+       jmp .l3
+.l3:
+       jmp .l4
+.l4:
+       in al, 0x40     ; high byte of count
+       xchg ah, al
+       ret
+; Call the original INT 21 handler.
+; Input:
+;   saved_int21_handler_segment:saved_int21_handler_offset = address of original INT 21 handler
+goto_saved_int21_handler:
+       jmp far [cs:saved_int21_handler_offset]
 ; Install the custom interrupt handlers int3_handler, int8_handler,
-; int9_handler, and int35_handler. Store the addresses of the original
-; handlers.
+; int9_handler, int21_handler, and int35_handler. Store the addresses of the
+; original handlers.
 ; Output:
 ;   saved_int3_handler_segment:saved_int3_handler_offset = address of original INT 3 handler
 ;   saved_int8_handler_segment:saved_int8_handler_offset = address of original INT 8 handler
 ;   saved_int9_handler_segment:saved_int9_handler_offset = address of original INT 9 handler
+;   saved_int21_handler_segment:saved_int21_handler_offset = address of original INT 21 handler
 ;   saved_int35_handler_segment:saved_int35_handler_offset = address of original INT 35 handler
 install_interrupt_handlers:
        ; The interrupt vector table starts at 0000:0000. Each entry is 4
        ; bytes: 2 bytes offset, 2 bytes segment.
        ; https://wiki.osdev.org/Interrupt_Vector_Table
        push ds
        xor ax, ax
        mov ds, ax
        cli
        
        ; INT 9
        lea bx, [saved_int9_handler_offset]
        mov ax, [9*4+2]         ; original segment
        mov [cs:bx+2], ax       ; save it
        mov ax, [9*4+0]         ; original offset
        mov [cs:bx+0], ax       ; save it
        lea ax, [int9_handler]
        mov [9*4+0], ax         ; overwrite offset
        mov [9*4+2], cs         ; overwrite segment

        ; INT 8
        lea bx, [saved_int8_handler_offset]
        mov ax, [8*4+2]         ; original segment
        mov [cs:bx+2], ax       ; save it
        mov ax, [8*4+0]         ; original offset
        mov [cs:bx+0], ax       ; save it
        lea ax, [int8_handler]
        mov [8*4+0], ax         ; overwrite offset
        mov [8*4+2], cs         ; overwrite segment

        ; INT 35
        lea bx, [saved_int35_handler_offset]
        mov ax, [35*4+2]        ; original segment
        mov [cs:bx+2], ax       ; save it
        mov ax, [35*4+0]        ; original offset
        mov [cs:bx+0], ax       ; save it
        lea ax, [int35_handler]
        mov [35*4+0], ax        ; overwrite offset
        mov [35*4+2], cs        ; overwrite segment

        ; INT 3
        lea bx, [saved_int3_handler_offset]
        mov ax, [3*4+2]         ; original segment
        mov [cs:bx+2], ax       ; save it
        mov ax, [3*4+0]         ; original offset
        mov [cs:bx+0], ax       ; save it
        lea ax, [int3_handler]
        mov [3*4+0], ax         ; overwrite offset
        mov [3*4+2], cs         ; overwrite segment

+       ; INT 21
+       lea bx, [saved_int21_handler_offset]
+       mov ax, [21*4+2]        ; original segment
+       mov [cs:bx+2], ax       ; save it
+       mov ax, [21*4+0]        ; original offset
+       mov [cs:bx+0], ax       ; save it
+       lea ax, [int21_handler]
+       mov [21*4+0], ax        ; overwrite offset
+       mov [21*4+2], cs        ; overwrite segment
+
        sti
        pop ds
        ret
-; Restore the original handlers for INT 3, INT 8, INT 9, and INT 31 that were
-; replaced in install_interrupt_handlers.
+; Restore the original handlers for INT 3, INT 8, INT 9, INT 21, and INT 31
+; that were replaced in install_interrupt_handlers.
 ; Input:
 ;   saved_int3_handler_segment:saved_int3_handler_offset = address of original INT 3 handler
 ;   saved_int8_handler_segment:saved_int8_handler_offset = address of original INT 8 handler
 ;   saved_int9_handler_segment:saved_int9_handler_offset = address of original INT 9 handler
+;   saved_int21_handler_segment:saved_int21_handler_offset = address of original INT 21 handler
 ;   saved_int35_handler_segment:saved_int35_handler_offset = address of original INT 35 handler
 restore_interrupt_handlers:
        ; The interrupt vector table starts at 0000:0000. Each entry is 4
        ; bytes: 2 bytes offset, 2 bytes segment.
        ; https://wiki.osdev.org/Interrupt_Vector_Table
        push ds
        xor ax, ax
        mov ds, ax
        cli
 
        ; INT 9
        lea bx, [saved_int9_handler_offset]
        mov ax, [cs:bx+2]       ; saved segment
        mov [9*4+2], ax         ; restore it
        mov ax, [cs:bx+0]       ; saved offset
        mov [9*4+0], ax         ; restore it
 
        ; INT 8
        lea bx, [saved_int8_handler_offset]
        mov ax, [cs:bx+2]       ; saved segment
        mov [8*4+2], ax         ; restore it
        mov ax, [cs:bx+0]       ; saved offset
        mov [8*4+0], ax         ; restore it
 
        ; INT 35
        lea bx, [saved_int35_handler_offset]
        mov ax, [cs:bx+2]       ; saved segment
        mov [35*4+2], ax        ; restore it
        mov ax, [cs:bx+0]       ; saved offset
        mov [35*4+0], ax        ; restore it
 
        ; INT 3
        lea bx, [saved_int3_handler_offset]
        mov ax, [cs:bx+2]       ; saved segment
        mov [3*4+2], ax         ; restore it
        mov ax, [cs:bx+0]       ; saved offset
        mov [3*4+0], ax         ; restore it
 
+       ; INT 21
+       lea bx, [saved_int21_handler_offset]
+       mov ax, [cs:bx+2]       ; saved segment
+       mov [21*4+2], ax        ; restore it
+       mov ax, [cs:bx+0]       ; saved offset
+       mov [21*4+0], ax        ; restore it
+
        sti
        pop ds
        ret

R5 Joystick Input

-; The scancode of the most recent key press event (set in int9_handler).
+; Temporary storage used by int8_handler. The default values are not
+; meaningful.
+key_state_jump_tmp     db      1
+key_state_fire_tmp     db      2
+key_state_right_tmp    db      3
+key_state_left_tmp     db      4
+key_state_open_tmp     db      5
+key_state_teleport_tmp db      6
+
+; The scancode of the most recent key press event (set in int9_handler), or
+; 0xff in the case of a joystick input event (set in int8_handler).
 recent_scancode                db      0
 ; INT 8 is called for every cycle of the programmable interval timer (IRQ 0).
 ; Poll the joystick and F1 and F2 keys (on odd interrupts only) and advance the
 ; current sound playback. Tail call into the original INT 8 handler.
 ; Input:
 ;   saved_int8_handler_offset:saved_int8_handler_segment = address of original INT 8 handler
 ;   irq0_parity = even/odd counter of calls to this interrupt handler
 ;   joystick_is_calibrated = boolean controlling whether to read the joystick
 ;   joystick_x_low = joystick left threshold
 ;   joystick_x_high = joystick right threshold
 ;   joystick_y_low = joystick up threshold
 ;   joystick_y_high = joystick down threshold
 ;   key_state_f1 = if 1, unmute the sound
 ;   key_state_f2 = if 1, mute the sound
 ;   sound_is_playing = if 1, deal with the currently playing sound
 ;   sound_data_segment:sound_data_offset = address of the current sound
 ;   sound_note_counter = how many more interrupts to continue playing the current note in the current sound
 ;   sound_is_enabled = if 1, actually send sound data to the PC speaker
 ; Output:
 ;   irq0_parity = opposite of its input value
 ;   game_tick_flag = set to 1 if irq0_parity was odd
 ;   key_state_jump = set to 1 if the joystick jump switch is pressed
 ;   key_state_fire = set to 1 if the joystick fire switch is pressed
 ;   key_state_right = set to 1 if the joystick is pressed right
 ;   key_state_left = set to 1 if the joystick is pressed left
 ;   key_state_open = set to 1 if the joystick is pressed up
 ;   key_state_teleport = set to 1 if the joystick is pressed down
+;   recent_scancode = set to 0xff if the joystick caused any key_state_* to change
 ;   sound_data_offset = advanced by 4 bytes if a note transition occurred
 ;   sound_is_playing = set to 0 if the current sound finished playing
 ;   sound_priority = set to 0 if the current sound finished playing
 int8_handler:
        push ax
        push bx
        push cx
        push dx
        push es
        
        ; irq0_parity keeps track of whether we are in an even or an odd call
        ; to this interrupt handler. irq0_parity advances 0→1 or 1→0 on each
        ; interrupt.
        mov al, [cs:irq0_parity]
        inc al
        cmp al, 2       ; did irq0_parity overflow to 2?
        jge .irq0_parity_odd    ; irq0_parity was 1, now is 2 (and will become 1→0 at .wrap_irq0_parity below)
        jmp .store_irq0_parity  ; irq0_parity was 0, now is 1
 
 .irq0_parity_odd:
        ; On odd interrupts, poll the joystick and F1/F2 keys.
        mov byte [cs:game_tick_flag], 1 ; flag the beginning of a game tick
        cmp word [cs:joystick_is_calibrated], 1 ; is the joystick calibrated?
        je .joystick    ; if so, read it
        jmp .try_F1     ; if not, go straight to checking F1/F2
-       nop             ; dead code
 
 .joystick:
+       ; Save all key_state_* in key_state_*_tmp, then set all to 0.
+       mov al, [cs:key_state_jump]
+       mov [cs:key_state_jump_tmp], al
        mov byte [cs:key_state_jump], 0
+       mov al, [cs:key_state_fire]
+       mov [cs:key_state_fire_tmp], al
        mov byte [cs:key_state_fire], 0
+       mov al, [cs:key_state_right]
+       mov [cs:key_state_right_tmp], al
        mov byte [cs:key_state_right], 0
+       mov al, [cs:key_state_left]
+       mov [cs:key_state_left_tmp], al
        mov byte [cs:key_state_left], 0
+       mov al, [cs:key_state_open]
+       mov [cs:key_state_open_tmp], al
        mov byte [cs:key_state_open], 0
+       mov al, [cs:key_state_teleport]
+       mov [cs:key_state_teleport_tmp], al
        mov byte [cs:key_state_teleport], 0
 .joystick_try_up:
        cmp bx, [cs:joystick_y_low]
        jg .joystick_try_down
        mov byte [cs:key_state_open], 1
-       jmp .try_F1
+       jmp .merge_joystick_inputs
 .joystick_try_down:
        cmp bx, [cs:joystick_y_high]
-       jl .try_F1
+       jl .merge_joystick_inputs
        mov byte [cs:key_state_teleport], 1

+.merge_joystick_inputs:
+       ; Compare the new joystick inputs that are in key_state_* with the
+       ; saved values in key_state_*_tmp. Set recent_scancode to 0xff if any
+       ; of them differ (meaning that the joystick changed the input state in
+       ; some way).
+       mov al, [cs:key_state_jump]
+       xor [cs:key_state_jump_tmp], al
+       mov al, [cs:key_state_fire]
+       xor [cs:key_state_fire_tmp], al
+       mov al, [cs:key_state_right]
+       xor [cs:key_state_right_tmp], al
+       mov al, [cs:key_state_left]
+       xor [cs:key_state_left_tmp], al
+       mov al, [cs:key_state_open]
+       xor [cs:key_state_open_tmp], al
+       mov al, [cs:key_state_teleport]
+       xor [cs:key_state_teleport_tmp], al
+       mov al, [cs:key_state_jump_tmp]
+       or al, [cs:key_state_fire_tmp]
+       or al, [cs:key_state_right_tmp]
+       or al, [cs:key_state_left_tmp]
+       or al, [cs:key_state_open_tmp]
+       or al, [cs:key_state_teleport_tmp]
+       jz .try_F1
+       mov byte [cs:recent_scancode], 0xff     ; signal that the input state changed somehow

R5 Save Video Mode

-       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
-       int 0x10
+.save_video_mode:
+       mov ax, 0x0f00  ; ah=0x0f: get video mode
+       int 0x10
+       cmp al, 13      ; useless instruction; looks like a copy-paste error from below
+       xor ah, ah      ; store ah=0x00 (set video mode) in saved_video_mode along with the video mode itself
+       mov [saved_video_mode], ax      ; the video mode to be restored by terminate_program
 setup_keyboard:
+       jmp .l1
+       nop             ; dead code
+.l1:
+       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       int 0x10
-; Mute the sound, restore video mode 2, restore the original interrupt
+; Mute the sound, restore saved_video_mode, restore the original interrupt
 ; handlers, and exit.
+; Input:
+;   saved_video_mode = upper 8 bits are 0x00, lower 8 bits are video mode
 terminate_program:
        mov ax, SOUND_MUTE
        int3
-       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
+       mov ax, [saved_video_mode]      ; ah is 0x00 (set video mode), al is video mode
        int 0x10
+; saved_video_mode's lower byte is the video mode and the upper byte is 0x00,
+; so you can call `int 0x10` right after loading the value into ax.
+saved_video_mode       dw      0

R5 Interrupt Handler Check

        cmp byte [interrupt_handler_install_sentinel], .INTERRUPT_HANDLER_INSTALL_SENTINEL      ; as expected?
        je display_startup_notice       ; all good, continue
-       jmp title_sequence
-
-setup_keyboard_trampoline:
-       jmp setup_keyboard
+       jmp terminate_program   ; our interrupt handlers were not installed; terminate

R5 Startup Notice

-       lea bx, [STARTUP_NOTICE_TEXT]
        cmp byte [interrupt_handler_install_sentinel], .INTERRUPT_HANDLER_INSTALL_SENTINEL      ; as expected?
        je display_startup_notice       ; all good, continue
-       jmp title_sequence
-
-setup_keyboard_trampoline:
-       jmp setup_keyboard
+       jmp terminate_program   ; our interrupt handlers were not installed; terminate
 
 ; Display STARTUP_NOTICE_TEXT and wait for a keypress to configure the
-; keyboard, configure the joystick, quit, or begin the game.
+; keyboard, configure the joystick, see the registration information, quit, or
+; begin the game.
 display_startup_notice:
-       call display_xor_decrypt        ; decrypt and display STARTUP_NOTICE_TEXT
+       mov ax, 0x0003  ; ah=0x00: set video mode; al=3: 80×25 text
+       int 0x10
+       mov si, STARTUP_NOTICE_TEXT
+       ; STARTUP_NOTICE_TEXT is stored with XOR obfuscation. This loop
+       ; deobfuscates and displays the text.
+.loop:
+       lodsb           ; get next byte into al
+       xor al, XOR_ENCRYPTION_KEY      ; decrypt
+       mov ah, 0x0e    ; ah=0x0e: teletype output
+       or al, al       ; hit a nul byte?
+       jz .finished    ; if so, break
+       int 0x10        ; otherwise, output al
+       jmp .loop       ; and try the next byte
+.finished:
+       ; Clear the BIOS keyboard buffer: http://www.fysnet.net/kbuffio.htm.
+       xor ax, ax
+       push es
+       mov es, ax      ; temporarily set es = 0x0000
+       mov al, [cs:recent_scancode]    ; useless instruction; looks like a copy-paste error from wait_for_keypress
+       cli             ; disable interrupts
+       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
+       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
+       sti             ; enable interrupts
+       pop es          ; restore es
 
-       xor ax, ax      ; ah=0x00: get keystroke; returned al is ASCII code
-       int 0x16
+       int 0x16        ; ah=0x00: get keystroke; returned al is ASCII code
        cmp al, 'k'
-       je setup_keyboard_trampoline
+       je setup_keyboard
        cmp al, 'K'
-       je setup_keyboard_trampoline
+       je setup_keyboard
+       cmp al, 'j'
+       je calibrate_joystick_trampoline
+       cmp al, 'J'
+       je calibrate_joystick_trampoline
+       cmp al, 'r'
+       je display_registration_notice
+       cmp al, 'R'
+       jne display_startup_notice_any_other_key
+
+; Display REGISTRATION_NOTICE_TEXT and wait for any keystroke to return to
+; display_startup_notice.
+display_registration_notice:
+       mov ax, 0x0003  ; ah=0x00: set video mode; al=3: 80×25 text
+       int 0x10
+       mov si, REGISTRATION_NOTICE_TEXT
+       ; REGISTRATION_NOTICE_TEXT is stored with XOR obfuscation. This loop
+       ; deobfuscates and displays the text.
+.loop:
+       lodsb           ; get next byte into al
+       xor al, XOR_ENCRYPTION_KEY      ; decrypt
+       mov ah, 0x0e    ; ah=0x0e: teletype output
+       or al, al       ; hit a nul byte?
+       jz .finished    ; if so, break
+       int 0x10        ; otherwise, output al
+       jmp .loop       ; and try the next byte
+.finished:
+       xor ax, ax      ; ah=0x00: get keystroke
+       int 0x16        ; wait for any keystroke
+       jmp display_startup_notice
+
+; If the key pressed was Escape, jump to terminate_program. Otherwise, jump to
+; title_sequence.
+; Input:
+;   al = ASCII code of key pressed
+display_startup_notice_any_other_key:
        cmp al, 27      ; Escape
        jne .not_esc
        jmp terminate_program   ; Escape allows exiting directly from the startup notice screen
 .not_esc:
-       cmp al, 'j'
-       je calibrate_joystick
-       cmp al, 'J'
-       je calibrate_joystick
-
-       jmp calibrate_joystick.check_ega_support_trampoline
+       jmp title_sequence      ; let the game begin
 
-calibrate_joystick_cancel_trampoline:
-       jmp calibrate_joystick.cancel
+calibrate_joystick_trampoline:
+       jmp calibrate_joystick
-; Decrypt an obfuscated string and display it on the console. The end of the
-; string is marked by the byte value 0x1a (encrypted value 0x3f).
-; Input:
-;   bx = pointer to encrypted string
-display_xor_decrypt:
-.loop:
-       mov al, [bx]    ; get next byte into al
-       inc bx
-       xor al, XOR_ENCRYPTION_KEY      ; decrypt
-       cmp al, 0x1a    ; terminator sentinel?
-       je .finished
-       push bx
-       mov bx, 0x0007  ; bh=0x00: page number 0, bl=0x0c: color 7
-       mov ah, 0x0e    ; ah=0x0e: teletype output
-       int 0x10        ; output al
-       pop bx
-       jmp .loop       ; and try the next byte
-.finished:
-       ret
 STARTUP_NOTICE_TEXT:   xor_encrypt `\
-               The Adventures of Captain Comic  --  Revision 4\r\n\
-                     Copyright 1988, 1989 by Michael Denio\r\n\
+                The Adventures of Captain Comic  --  Revision 5\r\n\
+                     Copyright 1988 - 91 by Michael A. Denio\r\n\
 \r\n\
   This software is being distributed under the Shareware concept, where you as\r\n\
   the user  are  allowed  to use the program on a "trial" basis.  If you enjoy\r\n\
-  playing  Captain  Comic,  you  are encouraged to register yourself as a user\r\n\
-  with a $10 to $20 contribution.   Registered users will be the first in line\r\n\
-  to receive new Comic adventures.\r\n\
+  playing  Captain  Comic,  you are encouraged to register yourself as a user.\r\n\
+  Registered users will be given access to the official Captain Comic question\r\n\
+  hotline (my home phone number).  Press [R] for registration details.\r\n\
+\r\n\
+              For those agile enough to complete this adventure...\r\n\
+                  CAPTAIN COMIC EPISODE II: FRACTURED REALITY\r\n\
+                  is now available. (Press [R] for details.)\r\n\
+\r\n\
+   This software may be freely re-distributed by complying to the following:\r\n\
+\r\n\
+    1. The  program,  graphics,  and  document  files may not be modified.\r\n\
+    2. No  form  of  compensation  (other  than  handling  costs)  may  be\r\n\
+       collected  from  the  distribution or publication of this software.\r\n\
+\r\n\
+  ---------------------------------- Select ----------------------------------\r\n\
+\r\n\
+    [K]eyboard Definition    [J]oystick Play     [R]egistration Information\r\n\
+\r\n\
+  -------------------------- any other key to begin --------------------------\
+\0\0\0`
+
+REGISTRATION_NOTICE_TEXT:      xor_encrypt `\
+           The Adventures of Captain Comic  --  Registration Details\r\n\
+\r\n\
+  If you enjoy playing Captain Comic,  you are encouraged to register yourself\r\n\
+  as a user with a $10 to $20 contribution.  Registered users are given access\r\n\
+  to  the official Captain Comic question hotline (my home phone number),  and\r\n\
+  are supplied with a hint sheet for solving Episode I: Planet of Death.\r\n\
 \r\n\
-        This product is copyrighted material, but may be re-distributed\r\n\
-                 by complying to these two simple restrictions:\r\n\
+         Catpain Comic Episode II: Fractured Reality is now available!\r\n\
 \r\n\
-        1. The program and graphics (including world maps) may not be\r\n\
-           distributed in any modified form.\r\n\
-        2. No form of compensation is be collected from the distribution\r\n\
-           of this program, including any disk handling costs or BBS\r\n\
-           file club fees.\r\n\
+   * Advanced Puzzle Solving           * Save / Continue Game Feature\r\n\
+   * Hundreds of objects to discover   * Mutiple hidden rooms & bonus objects\r\n\
+   * 4 Way Scrolling Playfield         * Multi-terrain worlds\r\n\
+   * Fully Definable Keyboard          * Big! (3 times the size of Comic I)\r\n\
+\r\n\
+     * Multiple Tools for Comic to Use (Blastola, Pick, Jet Pack and Wand)\r\n\
+              * Walk, swim, jump, fly, ride a mine car and a sled!\r\n\
+\r\n\
+  Captain Comic II comes with printed documentation and is available for $20.\r\n\
+       Register Comic I, get the Comic I hint sheet and Comic II for $25.\r\n\
+      (Outside the U.S. - Please add $5 for shipping on Comic II orders.)\r\n\
 \r\n\
     Questions and contributions can be sent to me at the following address:\r\n\
                                 Michael A. Denio\r\n\
-                           15700 Lexington Blvd #1010\r\n\
-                              Sugar Land, TX 77478\r\n\
-\r\n\
-                          Press \'J\' for Joystick Play.\r\n\
-      Press \'K\' to define the keyboard  ---  Press any other key to begin.\r\
+                              3106 Twin Oaks Drive\r\n\
+                                Joliet, IL 60435\
 \0\0`

R5 Save es

 ; Wait for an input event (keypress or joystick input). Additionally clear the
 ; BIOS keyboard buffer.
 ; Output:
 ;   al = scancode of key pressed, or 0xff for a joystick input
 wait_for_keypress:
        ; Set recent_scancode to 0 and loop until int9_handler makes it
        ; nonzero.
        mov byte [cs:recent_scancode], 0
 .loop:
        cmp byte [cs:recent_scancode], 0
        je .loop
 
        ; Clear the BIOS keyboard buffer.
        xor ax, ax
+       push es
        mov es, ax      ; set es = 0x0000
        mov al, [cs:recent_scancode]    ; return the scancode in al
        cli             ; disable interrupts
        mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
        mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
        sti             ; enable interrupts
+       pop es          ; restore es
        ret

R5 Title

 ; Switch into graphics mode. Show the title graphic and await keypresses to
 ; advance through the story screen and items/enemies screen. Jump to
 ; initialize_lives_sequence when done.
 title_sequence:
-       lea dx, [FILENAME_TITLE_GRAPHIC]        ; "sys000.ega"
+       mov ax, 0x000d  ; ah=0x00: set video mode; al=13: 320×200 16-color EGA
+       int 0x10
        
        mov ax, 0xa000
        mov es, ax      ; es points to video memory

        ; The program uses various 8 KB buffers within the video memory segment
        ; 0xa000. The two most important are a000:0000 and a000:2000, which are
        ; the ones swapped between on every game tick for double buffering. The
        ; variable offscreen_video_buffer_ptr and function swap_video_buffers
        ; handle double buffering, swapping the displayed offset between 0x0000
        ; and 0x2000.
        ;
        ; The title sequence juggles a few video buffers, loading fullscreen
        ; graphics from .EGA files into memory and switching to them as
        ; appropriate. sys000.ega is loaded into a000:8000 and displayed
-       ; immediately. sys001.ega is loaded into a000:a000 after 10 ticks have
-       ; elapsed, but not immediately displayed. Then sys003.ega is loaded
-       ; into *both* buffers a000:0000 and a000:2000, but also not immediately
-       ; displayed. sys003.ega contains the gameplay UI and so needs to be in
-       ; the double buffers. Then video buffer switches to a000:a000 to
-       ; display sys001.ega. sys004.ega is loaded into a000:8000 (replacing
-       ; sys000.ega) and displayed after a keypress. Finally, we switch to the
-       ; buffer a000:2000, which contains sys003.ega, after another keypress,
-       ; in preparation for starting gameplay.
+       ; immediately. sys001.ega is loaded into a000:a000 and displayed after
+       ; 14 ticks have elapsed. Then sys003.ega is loaded into *both* buffers
+       ; a000:0000 and a000:2000, but not immediately displayed. sys003.ega
+       ; contains the gameplay UI and so needs to be in the double buffers.
+       ; sys004.ega is loaded into a000:8000 (replacing sys000.ega) and
+       ; displayed after a keypress. Finally, we switch to the buffer
+       ; a000:2000, which contains sys003.ega, after another keypress, in
+       ; preparation for starting gameplay.
        ;
        ; The complete rendered map for the current stage also lives in video
        ; memory, between a000:4000 and a000:dfff. That happens after the title
        ; sequence is over, so it doesn't conflict with the use of that region
        ; of memory here. See render_map and blit_map_playfield_offscreen.

        ; Load the title graphic into video buffer 0x8000.
+       lea dx, [FILENAME_TITLE_GRAPHIC]        ; "sys000.ega"
        mov di, 0x8000
        call load_fullscreen_graphic

        ; Start playing the title music.
        lea bx, [SOUND_TITLE]
        mov ax, SOUND_PLAY
        mov cx, 4       ; priority 4
        int3
        
        ; Switch to the title graphic and fade in.
        call palette_darken
        mov cx, 0x8000  ; switch to video buffer 0x8000, into which we loaded the title screen graphic
        call switch_video_buffer
        call palette_fade_in

-       mov ax, 10      ; linger on the title screen for 10 ticks
+       mov ax, 14      ; linger on the title screen for 14 ticks
        call wait_n_ticks

        ; Load the story graphic into video buffer 0xa000.
        lea dx, [FILENAME_STORY_GRAPHIC]        ; "sys001.ega"
-       mov ax, 0xa000
-       mov es, ax      ; es points to video memory
        mov di, 0xa000
        call load_fullscreen_graphic
+       ; Switch to the story graphic and fade in.
+       call palette_darken
+       mov cx, 0xa000  ; switch to video buffer 0xa000, into which we loaded the story graphic
+       call switch_video_buffer
+       call palette_fade_in

        ; Load the UI graphic into video buffer 0x0000.
        lea dx, [FILENAME_UI_GRAPHIC]   ; "sys003.ega"
        mov ax, 0xa000
        mov es, ax      ; es points to video memory
        xor di, di      ; video buffer 0x0000
        call load_fullscreen_graphic

        ; Copy the UI graphic from video buffer 0x0000 to video buffer 0x2000
        ; (these are the two buffers that swap every tick during gameplay). We
        ; need to copy the graphic one plane at a time, into the same nominal
        ; buffer.
        push ds
        mov ax, 0xa000
        mov ds, ax      ; ds points to video memory
        ; Copy plane 0 from video buffer 0x0000 to video buffer 0x2000.
        mov ah, 1       ; ah = mask for plane 0
        xor bx, bx      ; bl = index of plane 0
        call enable_ega_plane_read
        mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
        xor si, si
        mov di, 0x2000
        rep movsw       ; copy from ds:si to es:di
        ; Copy plane 1 from video buffer 0x0000 to video buffer 0x2000.
        mov ah, 2       ; ah = mask for plane 1
        mov bx, 1       ; bl = index of plane 1
        call enable_ega_plane_read
        mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
        xor si, si
        mov di, 0x2000
        rep movsw       ; copy from ds:si to es:di
        ; Copy plane 2 from video buffer 0x0000 to video buffer 0x2000.
        mov ah, 4       ; ah = mask for plane 2
        mov bx, 2       ; bl = index of plane 2
        call enable_ega_plane_read
        mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
        xor si, si
        mov di, 0x2000
        rep movsw       ; copy from ds:si to es:di
        ; Copy plane 3 from video buffer 0x0000 to video buffer 0x2000.
        mov ah, 8       ; ah = mask for plane 3
        mov bx, 3       ; bl = index of plane 3
        call enable_ega_plane_read
        mov cx, 4000    ; 4000 words = 8000 bytes = size of one plane
        xor si, si
        mov di, 0x2000
        rep movsw       ; copy from ds:si to es:di
        pop ds
 
-       ; Switch to the story graphic and fade in.
-       call palette_darken
-       mov cx, 0xa000  ; switch to video buffer 0xa000, into which we loaded the story graphic
-       call switch_video_buffer
-       call palette_fade_in
-
        ; Load the items graphic into video buffer 0x8000 (over the title
        ; screen graphic).
        lea dx, [FILENAME_ITEMS_GRAPHIC]        ; "sys004.ega"
-       mov ax, 0xa000
-       mov es, ax      ; es points to video memory
        mov di, 0x8000
        call load_fullscreen_graphic

R5 Keyboard Buffer

        pop ds
-       mov ax, 0x0002  ; ah=0x00: set video mode; al=2: 80×25 text
-       int 0x10
-
-       lea bx, [STARTUP_NOTICE_TEXT]
-
-       ; Clear the BIOS keyboard buffer: http://www.fysnet.net/kbuffio.htm.
-       xor ax, ax
-       mov es, ax      ; temporarily set es = 0x0000
-       cli             ; disable interrupts
-       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
-       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
-       sti             ; enable interrupts
-
        jmp display_startup_notice
-       ; Clear the BIOS keyboard buffer.
-       xor ax, ax
-       mov es, ax      ; es = 0x0000
-       cli             ; disable interrupts
-       mov cl, [es:BIOS_KEYBOARD_BUFFER_HEAD]
-       mov [es:BIOS_KEYBOARD_BUFFER_TAIL], cl  ; assign tail = head
-       sti             ; enable interrupts
-       
        ; Wait for a keystroke at the story screen.
-       int 0x16        ; ah=0x00: get keystroke
+       xor ax, ax      ; ah=0x00: get keystroke
+       int 0x16        ; wait for any keystroke

R5 RLE

+; rle_decode_size is effectively an extra parameter passed to rle_decode. It
+; stores the first word in a .EGA file, which is the number of bytes to read
+; out of the run-length encoding. (Which happens to always be 8000.)
+rle_decode_size                dw      0
+
 ; Load a fullscreen graphic from a .EGA file and decode its to a specified
 ; destination buffer.
 ; Input:
 ;   ds:dx = address of filename
 ;   es:di = 32000-byte destination buffer
 load_fullscreen_graphic:
+       push ds
+       call .load      ; ds:si points to un-decoded file contents
+       call .decode
+       pop ds
+       ret
+; Sub-subroutine to load the file contents.
+; Input:
+;   ds:dx = filename
+; Output:
+;   ds:si = destination buffer
+.load: 
        ; Load the entire file into load_fullscreen_graphic_buffer, without
        ; decoding.
        mov ax, 0x3d00  ; ah=0x3d: open existing file
        int 0x21
        jnc .open_ok    ; failure to open a .EGA file is a fatal error
        jmp title_sequence.terminate_program_trampoline
 .open_ok:
        mov bx, ax      ; bx = file handle
-       push ds
        mov ax, input_config_strings
        mov ds, ax      ; load_fullscreen_graphic_buffer is in the input_config_strings segment
-       lea dx, [load_fullscreen_graphic_buffer]        ; ds:dx = destination buffer
-       mov cx, 0x7fff  ; cx = number of bytes to read (overflow possible here; buffer is only 0x3e82 bytes)
+       mov dx, load_fullscreen_graphic_buffer  ; ds:dx = destination buffer
+       mov cx, 0x7fff  ; cx = number of bytes to read (overflow possible here; buffer is only 0x7d02 bytes)
        mov ax, 0x3f00  ; ah=0x3f: read from file or device
        int 0x21        ; no error check
        mov ax, 0x3e00  ; ah=0x3e: close file
        int 0x21
-
-       ; Decode file contents as RLE.
+       mov si, load_fullscreen_graphic_buffer  ; return ds:si = load_fullscreen_graphic_buffer back to load_fullscreen_graphic
+       ret
+; Sub-subroutine to decode file contents as RLE.
+; Input:
+;   ds:si = address of file contents
+;   es:di = destination of decoding
+.decode:
        ; http://www.shikadi.net/moddingwiki/Captain_Comic_Image_Format#File_format
-       ; Ignore the first word, which is the plane size, always 8000.
-       lea ax, [load_fullscreen_graphic_buffer + 2]
-       mov si, ax
-       mov ah, 1       ; blue plane mask
-       xor bx, bx      ; blue plane index
-       call enable_ega_plane_write
+       ; The first word is the plane size, always 8000.
+       lodsw           ; read plane size from [ds:si] into ax
+       mov [cs:rle_decode_size], ax    ; rle_decode reads from this memory location
+       mov cl, 0       ; blue plane
+       call enable_ega_plane_read_write
        call rle_decode
-       mov ah, 2       ; green plane mask
-       mov bx, 1       ; green plane index
-       call enable_ega_plane_write
+       mov cl, 1       ; green plane
+       call enable_ega_plane_read_write
        call rle_decode
-       mov ah, 4       ; red plane mask
-       mov bx, 2       ; red plane index
-       call enable_ega_plane_write
+       mov cl, 2       ; red plane
+       call enable_ega_plane_read_write
        call rle_decode
-       mov ah, 8       ; intensity plane mask
-       mov bx, 3       ; intensity plane index
-       call enable_ega_plane_write
+       mov cl, 3       ; intensity plane
+       call enable_ega_plane_read_write
        call rle_decode
-       pop ds
        ret

 ; Decode RLE data until a certain number of bytes have been decoded.
 ; Input:
 ;   ds:si = input RLE data
 ;   es:di = output buffer
-;   load_fullscreen_graphic_buffer = first word is the number of bytes to decode
+;   rle_decode_size = number of bytes to decode
 ;     (returns when at least that many bytes have been written to es:di)
 ; Output:
 ;   ds:si = advanced
 ;   es:di = unchanged
 rle_decode:
        ; http://www.shikadi.net/moddingwiki/Captain_Comic_Image_Format#File_format
        mov bx, di
 .loop:
        lodsb           ; read from [ds:si] into al
        test al, 0x80   ; is the high bit set?
        jnz .repeat
 .copy:
        ; High bit not set means copy the next n bytes.
        xor ah, ah
        mov cx, ax      ; cx = n
        rep movsb       ; copy n bytes from ds:si to es:di
        jmp .next
 .repeat:
        ; High bit set means repeat the next byte n times.
        xor ah, ah
        and al, 0x7f    ; unset the high bit
        mov cx, ax      ; cx = n
        lodsb           ; read from [ds:si] into al
        rep stosb       ; repeat al into [es:di] n times
 .next:
        mov ax, di
        sub ax, bx      ; how many bytes have been written so far?
-       cmp ax, [load_fullscreen_graphic_buffer]        ; compare with the plane size at the beginning of the buffer
+       cmp ax, [cs:rle_decode_size]
        jl .loop        ; loop until we have decoded enough
        mov di, bx
        ret
+; Enable an EGA plane for read/write.
+; Input:
+;   cl = plane index (0, 1, 2, 3)
+enable_ega_plane_read_write:
+      ; https://www.jagregory.com/abrash-black-book/#at-the-core
+      ; https://www.jagregory.com/abrash-black-book/#color-plane-manipulation
+      ; https://wiki.osdev.org/VGA_Hardware#Read.2FWrite_logic
+      ; Compute plane mask.
+      mov ah, 1
+      shl ah, cl      ; mask = 1 << index
+
+      ; Enable write.
+      mov al, 2       ; SC Map Mask register: https://sourceforge.net/p/dosbox/code-0/HEAD/tree/dosbox/tags/RELEASE_0_74_3/src/hardware/vga_seq.cpp#l66
+      mov dx, 0x3c4   ; SC Index register
+      out dx, al
+      inc dx          ; SC Data register
+      xchg al, ah
+      out dx, al      ; write plane mask
+      dec dx          ; useless instruction
+      xchg al, ah     ; useless instruction
+
+      mov ah, cl      ; plane index
+
+      ; Enable read.
+      mov al, 4       ; GC Read Map Select register: https://sourceforge.net/p/dosbox/code-0/HEAD/tree/dosbox/tags/RELEASE_0_74_3/src/hardware/vga_gfx.cpp#l90
+      mov dx, 0x3ce   ; GC Index register
+      out dx, al
+      inc dx          ; GC Data register
+      xchg al, ah
+      out dx, al
+      dec dx          ; useless instruction
+      xchg al, ah     ; useless instruction
+
+      ret
-load_fullscreen_graphic_buffer:
-       resb    16002
+load_fullscreen_graphic_buffer:
+       times   32002   db      0

R5 Swap Video Buffers

-; Change the video start offset.
+; Change the high byte of the video start offset. Leaves the low byte of the
+; pointer unchanged.
 ; Input:
-;   cx = new video start offset
+;   ch = new high byte of video start offset
 switch_video_buffer:
+       ; Bit 3 of port 0x3da is set while vertical retrace is in progress.
        ; https://www.jagregory.com/abrash-black-book/#at-the-core
+       ; https://www.jagregory.com/abrash-black-book/#page-flipping
+       mov dx, 0x3da   ; Input Status 1 register
+.wait_for_vsync_start:
+       in al, dx
+       test al, 0x08
+       jnz .wait_for_vsync_start
+
        mov dx, 0x3d4   ; CRTC Index register
        mov al, 0x0c    ; CRTC Start Address High register
+       mov ah, ch
        out dx, al
        inc dx          ; CRTC Data register
-       mov al, ch
+       xchg al, ah
        out dx, al      ; write high byte
+       dec dx          ; useless instruction
+       xchg al, ah     ; useless instruction
 
-       mov dx, 0x3d4   ; CRTC Index register
-       mov al, 0x0d    ; CRTC Start Address Low register
-       out dx, al
-       inc dx          ; CRTC Data register
-       mov al, cl
-       out dx, al      ; write low byte
-
-       ret
+       mov dx, 0x3da   ; Input Status 1 register
+.wait_for_vsync_end:
+       in al, dx
+       test al, 0x08
+       jz .wait_for_vsync_end
+       ret

R5 Recap Music

-       ; Start playing the title music recapitulation.
-       lea bx, [SOUND_TITLE_RECAP]
-       mov ax, SOUND_PLAY
-       mov cx, 1       ; priority 1
-       int3
 .delay_loop:
        push cx
        call blit_map_playfield_offscreen
        call handle_item
        call swap_video_buffers
        mov ax, 1
        call wait_n_ticks
        pop cx
        loop .delay_loop
 
-       ; Then additionally wait until SOUND_TITLE_RECAP stops playing.
+       ; Then additionally wait until sound stops playing. This has no effect
+       ; in Revision 5 because initialize_lives_sequence stops the title music
+       ; and there's no introductory music played before beaming in.
        mov ax, SOUND_QUERY
        int3            ; get whether a sound is playing in al
        or ax, ax
        jz .play_sound  ; break if no sound playing
        mov cx, 1
        jmp .delay_loop ; otherwise wait a tick and check again
-; The first byte of SOUND_TITLE_RECAP is simultaneously the terminator for
-; STARTUP_NOTICE_TEXT. NOTE_E3 is 0x1c3f, whose little-endian first byte is
-; 0x3f, which is the terminator sentinel value that display_xor_decrypt looks
-; for.
-SOUND_TITLE_RECAP:
-dw     NOTE_E3, 6
-dw     NOTE_F3, 6
-dw     NOTE_G3, 9
-dw     NOTE_REST, 1
-dw     NOTE_G3, 9
-dw     NOTE_REST, 1
-dw     NOTE_G3, 9
-dw     NOTE_REST, 1
-dw     NOTE_G3, 9
-dw     NOTE_REST, 1
-dw     NOTE_G3, 14
-dw     NOTE_C4, 7
-dw     NOTE_G3, 18
-dw     NOTE_B3, 20
-dw     NOTE_C4, 20
-dw     SOUND_TERMINATOR, 0

R5 Respawn

 .respawn:
        call lose_a_life
        mov byte [comic_run_cycle], 0
        mov byte [comic_is_falling_or_jumping], 0       ; bug: Comic is considered to be "on the ground" (can jump and teleport) immediately after respawning, 
even if in the air
        mov byte [comic_x_momentum], 0
        mov byte [comic_y_vel], 0
        mov byte [comic_jump_counter], 4        ; bug: jumping immediately after respawning acts as if comic_jump_power were 4, even if Comic has the Boots
        mov byte [comic_animation], COMIC_STANDING
+       mov byte [comic_hp], 0  ; set HP to zero, so there won't be any bonus points for surplus HP when comic_hp_pending_increase takes effect
        mov byte [comic_hp_pending_increase], MAX_HP    ; let the HP fill up from zero after respawning
        mov byte [fireball_meter_counter], 2
        ; comic_is_teleporting is set to 0 in load_new_stage.comic_located.
        jmp load_new_stage.comic_located        ; respawn in the same stage at (comic_x_checkpoint, comic_y_checkpoint)

R5 Termination Notice

-
-       lea bx, [TERMINATE_PROGRAM_TEXT]
-       call display_xor_decrypt
-; This string (an xor-encrypted empty string) is displayed by
-; terminate_program.
-TERMINATE_PROGRAM_TEXT:        xor_encrypt `\x1a`
-       xor_encrypt `\x1a`      ; an unused xor-encrypted empty string

R5 Unused

-; Unused garbage bytes? They vaguely resemble the format of a sound, a 1193.182
-; Hz tone played three times. The final 0x0000, 0x0000 is the same as
-; SOUND_TERMINATOR. But 0x0001 for a rest does not match the convention of
-; 0x0028 for NOTE_REST used elsewhere in the program.
-db     0xe8, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00
-db     0xe8, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00
-db     0xe8, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
+                       db      `sys006.ega\0`  ; unused
+                       db      `sys007.ega\0`  ; unused
                        db      `File Error\n\r$`       ; unused