Lode Runner: Apple II reverse engineering

5. Input#

5.1 Joystick input#

Analog joysticks (or paddles) on the Apple //e are just variable resistors. The resistor on a paddle creates an RC circuit with a capacitor which can be discharged by accessing the PTRIG location. Once that is done, the capacitor starts charging through the resistor. The lower the resistor value, the faster the charge.

At the start, each PADDL value has its high bit set to one. When the voltage on the capacitor reaches 2/3 of the supply voltage, the corresponding PADDL switch will have its high bit set to zero. So, we just need to watch the PADDL value until it is non-negative, counting the amount of time it takes for that to happen.

In the READ_PADDLES routine, we trigger the paddles and then alternately read PADDL0 and PADDL1 until one of them indicates the threshold was reached. If the PADDL value hasn't yet triggered, we increment the corresponding PADDLE_VALUE location.

Once a PADDL triggers, we stop incrementing the corresponding PADDLE_VALUE.

Once both PADDL have been triggered, we end the routine.

⟨defines [110]⟩+=
PADDLE0_VALUE       EQU     $65
PADDLE1_VALUE       EQU     $66
PADDL0              EQU     $C064
PADDL1              EQU     $C065
PTRIG               EQU     $C070
Used in ⟨*⟩
Defines PADDL0, PADDL1, PADDLE0_VALUE, PADDLE1_VALUE
⟨read paddles [112]⟩=
    ORG     $8746
READ_PADDLES:
    SUBROUTINE

    LDA     #$00
    STA     PADDLE0_VALUE
    STA     PADDLE1_VALUE       ; Zero out values
    LDA     PTRIG

.loop:
    LDX     #$01                ; Start with paddle 1

.check_paddle:
    LDA     PADDL0,X
    BPL     .threshold_reached
    INC     PADDLE0_VALUE,X
.check_next_paddle
    DEX
    BPL     .check_paddle

    ; Checked both paddles
    LDA     PADDL0
    ORA     PADDL1
    BPL     .end                ; Both paddles triggered, then end.
    LDA     PADDLE0_VALUE
    ORA     PADDLE1_VALUE
    BPL     .loop               ; Unconditional

.threshold_reached:
    NOP
    BPL     .check_next_paddle      ; Unconditional

.end:
    RTS
Defines READ_PADDLES

The INPUT_MODE location tells whether the player is using keyboard or joystick input.

The CHECK_JOYSTICK_OR_DELAY routine, if we are in joystick mode, reads the paddle values and checks to see if any value is below 0x12 or above 0x3A, and if so, declares that a paddle has a large enough input by setting the carry flag and returning.

If neither paddle has a large enough input, we also check the paddle buttons, and if either one is triggered, we set the carry and return.

Otherwise, if no paddle input was detected, or we're in keyboard mode, we clear the carry and return.

⟨defines [114]⟩+=
INPUT_MODE  EQU     $95         ; 0xCA = Joystick mode (J), 0xCB = Keyboard mode (K)
                                ; initially set to 0xCA
JOYSTICK_MODE   EQU     #$CA
KEYBOARD_MODE   EQU     #$CB

BUTN0       EQU     $C061       ; Or open apple
BUTN1       EQU     $C062       ; Or solid apple
Used in ⟨*⟩
Defines BUTN0, BUTN1, INPUT_MODE
⟨check joystick or delay [116]⟩=
    ORG     $876D
CHECK_JOYSTICK_OR_DELAY:
    SUBROUTINE

    LDA     INPUT_MODE
    CMP     #KEYBOARD_MODE
    BEQ     .delay_and_return       ; Keyboard mode, so just delay and return

    JSR     READ_PADDLES

    LDA     PADDLE0_VALUE
    CMP     #$12
    BCC     .have_joystick_input           ; PADDLE0_VALUE < 0x12
    CMP     #$3B
    BCS     .have_joystick_input           ; PADDLE0_VALUE >= 0x3B

    LDA     PADDLE1_VALUE
    CMP     #$12
    BCC     .have_joystick_input
    CMP     #$3B
    BCS     .have_joystick_input

    LDA     BUTN1
    BMI     .have_joystick_input
    LDA     BUTN0
    BMI     .have_joystick_input

    CLC
    RTS

.have_joystick_input:
    SEC
    RTS

.delay_and_return:
    LDX     #$02
.loop:
    DEY
    BNE     .loop
    DEX
    BNE     .loop
    CLC
    RTS
Defines CHECK_JOYSTICK_OR_DELAY

If, after a timeout, a button hasn't been pressed, just assume keyboard mode.

⟨detect lack of joystick [118]⟩=
    ORG     $87A2
DETECT_LACK_OF_JOYSTICK:
    SUBROUTINE

    LDA     PTRIG
    LDX     #$10

.loop:
    LDA     PADDL0
    ORA     PADDL1
    BPL     .return

    DEY
    BNE     .loop
    DEX
    BNE     .loop

    LDA     #KEYBOARD_MODE
    STA     INPUT_MODE

.return:
    RTS
Defines DETECT_LACK_OF_JOYSTICK

5.2 Keyboard routines#

The WAIT_KEY routine accesses the keyboard strobe softswitch KBDSTRB, which clears the keyboard strobe in readiness to get a key. When a key is pressed after the keyboard strobe is cleared, the key (with the high bit set) is accessible through KBD

⟨defines [120]⟩+=
KBD         EQU     $C000
KBDSTRB     EQU     $C010
Used in ⟨*⟩
Defines KBD, KBDSTRB
⟨wait key [122]⟩=
    ORG     $869F
WAIT_KEY:
    SUBROUTINE

    STA     KBDSTRB
    LDA     KBD
    BMI     WAIT_KEY
    RTS
Defines WAIT_KEY
Uses KBD, KBDSTRB

The WAIT_KEY_QUEUED routine does not clear the keyboard strobe first, so if a key had been pressed before entering the routine, the routine will immediately return.

⟨wait key queued [124]⟩=
    ORG     $86A8
WAIT_KEY_QUEUED:
    SUBROUTINE

    LDA     KBD
    BPL     WAIT_KEY_QUEUED
    STA     KBDSTRB
    RTS
Defines WAIT_KEY_QUEUED
Uses KBD, KBDSTRB
⟨defines [126]⟩+=
    ORG     $8745
CURSOR_SPRITE:
    HEX     06
Used in ⟨*⟩
Defines CURSOR_SPRITE
⟨wait for key [128]⟩=
    ORG     $85F3
WAIT_FOR_KEY:
    SUBROUTINE
    ; Enter routine with A set to cursor sprite. If zero, sprite 10 (all white)
    ; will be used.

    STA     CURSOR_SPRITE

.loop:
    LDA     #$68
    STA     SCRATCH_A1
    LDA     CURSOR_SPRITE
    BNE     .draw_sprite
    LDA     #SPRITE_ALLWHITE
.draw_sprite:
    JSR     DRAW_SPRITE_PAGE2

.loop2:
    LDA     KBD
    BMI     .end            ; on keypress, end

    JSR     CHECK_JOYSTICK_OR_DELAY
    DEC     SCRATCH_A1
    BNE     .loop2

    ; Draw a blank
    LDA     #$00
    JSR     DRAW_SPRITE_PAGE2
    LDA     #$68
    STA     SCRATCH_A1

.loop3:
    LDA     KBD
    BMI     .end
    JSR     CHECK_JOYSTICK_OR_DELAY
    DEC     SCRATCH_A1
    BNE     .loop3
    JMP     .loop

.end:
    PHA
    LDA     CURSOR_SPRITE
    JSR     DRAW_SPRITE_PAGE2
    PLA
    RTS
⟨wait for key page1 [130]⟩=
    ORG     $8700
WAIT_FOR_KEY_WITH_CURSOR_PAGE_1:
    SUBROUTINE
    ; Enter routine with A set to cursor sprite. If zero, sprite 10 (all white)
    ; will be used.

    STA     CURSOR_SPRITE

.loop:
    LDA     #$68
    STA     SCRATCH_A1
    LDA     #$00
    LDX     CURSOR_SPRITE
    BNE     .draw_sprite
    LDA     #SPRITE_ALLWHITE
.draw_sprite:
    JSR     DRAW_SPRITE_PAGE1

.loop2:
    LDA     KBD
    BMI     .end            ; on keypress, end

    JSR     CHECK_JOYSTICK_OR_DELAY
    BCS     .end

    DEC     SCRATCH_A1
    BNE     .loop2

    LDA     CURSOR_SPRITE
    JSR     DRAW_SPRITE_PAGE1
    LDA     #$68
    STA     SCRATCH_A1

.loop3:
    LDA     KBD
    BMI     .end

    JSR     CHECK_JOYSTICK_OR_DELAY
    BCS     .end

    DEC     SCRATCH_A1
    BNE     .loop3
    JMP     .loop

.end:
    PHA
    LDA     CURSOR_SPRITE
    JSR     DRAW_SPRITE_PAGE1
    PLA
    RTS
Defines WAIT_FOR_KEY_WITH_CURSOR_PAGE_1

This routine is used by the level editor whenever we need to wait for a key. If the key isn't the escape key, we can immediately exit, and the caller interprets the key. However, on escape, we abort whatever editor command we were in the middle of, and just go back to the main editor command loop, asking for an editor command.

⟨editor wait for key [132]⟩=
    ORG     $823D
EDITOR_WAIT_FOR_KEY:
    SUBROUTINE

    LDA     #$00
    JSR     WAIT_FOR_KEY_WITH_CURSOR_PAGE_1
    STA     KBDSTRB
    CMP     #$9B        ; ESC
    BNE     .return
    JMP     EDITOR_COMMAND_LOOP

.return
    RTS
⟨hit key to continue [134]⟩=
    ORG     $80D8
HIT_KEY_TO_CONTINUE:
    SUBROUTINE

    ; "\r"
    ; "\r"
    ; "HIT A KEY TO CONTINUE "
    JSR     PUT_STRING
    HEX     8D 8D C8 C9 D4 A0 C1 A0 CB C5 D9 A0 D4 CF A0 C3
    HEX     CF CE D4 C9 CE D5 C5 A0 00

    JSR     BEEP
    STA     TXTPAGE2
    LDA     #$00
    JSR     WAIT_FOR_KEY
    STA     KBDSTRB
    STA     TXTPAGE1
RETURN_FROM_SUBROUTINE:
    RTS
Defines HIT_KEY_TO_CONTINUE, RETURN_FROM_SUBROUTINE

The GET_LEVEL_FROM_KEYBOARD is used by the level editor to ask the user for a 3-digit level number. The current level number, given by DISK_LEVEL_LOC, is put on the screen. Note that DISK_LEVEL_LOC is 0-based, while the levels the user enters are 1-based, so there's an increment at the beginning and a decrement at the end.

The routine handles forward and backward arrows. Hitting the escape key aborts the editor action and dumps the user back into the editor command loop. Hitting the return key accepts the user's input, and the level is stored in DISK_LEVEL_LOC and LEVELNUM.

⟨tables [136]⟩+=
    ORG     $824E
SAVED_GAME_COLNUM:
    HEX     85
Used in ⟨*⟩
Defines SAVED_GAME_COLNUM
⟨get level from keyboard [138]⟩=
    ORG     $817B
GET_LEVEL_FROM_KEYBOARD:
    SUBROUTINE

    LDY     DISK_LEVEL_LOC
    INY
    TYA
    JSR     TO_DECIMAL3     ; make 1-based
    LDA     GAME_COLNUM
    STA     SAVED_GAME_COLNUM
    LDY     #$00

    ; Print current level
.loop:
    LDA     HUNDREDS,Y
    STY     KBD_ENTRY_INDEX     ; save Y
    JSR     PUT_DIGIT
    LDY     KBD_ENTRY_INDEX     ; restore Y
    INY
    CPY     #$03
    BCC     .loop

    LDA     SAVED_GAME_COLNUM
    STA     GAME_COLNUM
    LDY     #$00
    STY     KBD_ENTRY_INDEX

.loop2
    LDX     KBD_ENTRY_INDEX
    LDA     HUNDREDS,X
    CLC
    ADC     #$3B            ; sprite = '0' + X
    JSR     WAIT_FOR_KEY_WITH_CURSOR_PAGE_1
    STA     KBDSTRB
    CMP     #$8D            ; return
    BEQ     .return_pressed

    CMP     #$88            ; backspace
    BNE     .check_for_fwd_arrow

    LDX     KBD_ENTRY_INDEX
    BEQ     .beep           ; can't backspace past the beginning

    DEC     KBD_ENTRY_INDEX
    DEC     GAME_COLNUM
    JMP     .loop2

.check_for_fwd_arrow:
    CMP     #$95            ; fwd arrow
    BNE     .check_for_escape

    LDX     KBD_ENTRY_INDEX
    CPX     #$02
    BEQ     .beep           ; can't fwd past the end

    INC     GAME_COLNUM
    INC     KBD_ENTRY_INDEX
    JMP     .loop2

.check_for_escape:
    CMP     #$9B            ; ESC
    BNE     .check_for_digit
    JMP     EDITOR_COMMAND_LOOP

.check_for_digit:
    CMP     #$B0            ; '0'
    BCC     .beep           ; less than '0' not allowed
    CMP     #$BA            ; '9'+1
    BCS     .beep           ; greater than '9' not allowed

    SEC
    SBC     #$B0            ; char - '0'
    LDY     KBD_ENTRY_INDEX
    STA     HUNDREDS,Y
    JSR     PUT_DIGIT
    INC     KBD_ENTRY_INDEX
    LDA     KBD_ENTRY_INDEX
    CMP     #$03
    BCC     .loop2

    ; Don't allow a fourth digit
    DEC     KBD_ENTRY_INDEX
    DEC     GAME_COLNUM
    JMP     .loop2

.beep:
    JSR     BEEP
    JMP     .loop2

.return_pressed:
    LDA     SAVED_GAME_COLNUM
    CLC
    ADC     #$03
    STA     GAME_COLNUM
    LDA     #$00
    LDX     HUNDREDS
    BEQ     .add_tens

    CLC
.loop_hundreds:
    ADC     #100
    BCS     .end
    DEX
    BNE     .loop_hundreds

.add_tens:
    LDX     TENS
    BEQ     .add_units

    CLC
.loop_tens:
    ADC     #10
    BCS     .end
    DEX
    BNE     .loop_tens

.add_units:
    CLC
    ADC     UNITS
    BCS     .end

    STA     LEVELNUM
    TAY
    DEY
    STY     DISK_LEVEL_LOC
    CPY     #$96

.end:
    RTS