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
Jump to navigation
Jump to search
R2
R5 Save
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.
Contents
- 1 R2 Variables
- 2 R2 Interrupt Handler Check
- 3 R2 Startup Notice
- 4 R2 Keymap
- 5 R2 RLE
- 6 R2 SYS005.EGA
- 7 R2 Sound Mute
- 8 R2 Instant Win
- 9 R2 sound_is_playing
- 10 R2 High Scores
- 11 R3 Startup Notice
- 12 R3 Esc
- 13 R3 Keyboard Setup Hack
- 14 R3 Keyboard Buffer
- 15 R3 Key Mapping
- 16 R3 Key Press/Release
- 17 R3 Unpause
- 18 R3 Initialize Jump Counter
- 19 R3 Jump Into Ceiling
- 20 R4 Startup Notice
- 21 R4 Joystick
- 22 R5 Copyright Notice
- 23 R5 Keymap
- 24 R5 Joystick Speed Loop
- 25 R5 Joystick Thresholds
- 26 R5 Joystick Interrupt Handler
- 27 R5 Joystick Input
- 28 R5 Save Video Mode
- 29 R5 Interrupt Handler Check
- 30 R5 Startup Notice
- 31 R5 Save es
- 32 R5 Title
- 33 R5 Keyboard Buffer
- 34 R5 RLE
- 35 R5 Swap Video Buffers
- 36 R5 Recap Music
- 37 R5 Respawn
- 38 R5 Termination Notice
- 39 R5 Unused
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