castlevania3.asm
meatfighter_castlevania3-passwords/asm/castlevania3.asm
; Segments of "Castlevania III: Dracula's Curse" related to password encoding and decoding.

; RAM map
;
; Addr Size Description
; ---- ---- -----------------------------------------------------------------------------------------------------------
; 0004      toggleMask (50 or A0)
; 0008    9 _unscrambledPassword
; 0010      _nameHash (00--07)
; 001A      frameCounter (00--FF)
; 002E      savePoint (00--11)
; 002F      escapedClockTower (00 = no, 01 = yes)
; 0032      block (00--0E)
; 0032      subBlock (zero-indexed)
; 0035      lives
; 003A      partner (FF = none, 01 = Sypha, 02 = Grant, 03 = Alucard)
; 003C      playerEnergy (00--40)
; 003D      bossEnergy (00--40)
; 00FF      hud (B0 = visible, B1 = hidden)
; 0084      hearts
; 0400   16 SPRITE_0
; 041C   16 MARK_YS
; 0438   16 MARK_XS
; 0454   16 MARK_ATTRIBS
; 048C   16 SPRITE_1
; 0788      payload (b765 = name hash, b4 = savePoint.0, b3 = frameCounter.0, b21 = partner, b0 = mode)
; 0789      payloadHash
; 078A      nameHash (00--07)
; 078B      badPasswordReason
; 078F      scramblesRowIndex (00--02)
; 0790   16 password (b54 = row, b32 = column, b10 = mark [0 = none, 1 = whip, 2 = rosary, 3 = heart])
; 07A0    9 unscrambledPassword
; 07F6      mode (00 = normal, 01 = hard)
; 07F8    8 name ([ ] = 00, [.] = 4B, [A-Z!?] = 50--6B)

; ROM map
;
; bk:addr Description
; ------- -------------------------------------------------------------------------------------------------------------
; 00:8FB0 convertBlockSubBlockToSavePoint()
; 00:8FD1 BLOCK_SUB_BLOCK_TO_SAVE_POINT (45 bytes)
; 00:8FFE checkForSpecialNames()
; 00:9005 checkForSomeSpecialNames()
; 00:9031 isSpecialName()
; 00:904D SPECIAL_NAME_ADDRESSES (5 words)
; 00:9057 SPECIAL_NAMES (40 bytes)
; 00:90C0 resetHeartsAndEnergy()
; 00:90CD reset4Aand4D()
;
; 03:B2F8 submitPassword()
; 03:B339 handleValidPassword:
; 03:B5AF showPassword()
; 03:B647 _drawPassword()
; 03:B64A encode()
; 03:B656 decode()
; 03:B675 resetPasswordVars()
; 03:B682 clearPassword()
; 03:B68F unscramblePassword()
; 03:B6B2 SCRAMBLES (27 bytes)
; 03:B6CD hashName()
; 03:B6E6 NAME_HASH_SEEDS (8 bytes)
; 03:B6EE encodePayload()
; 03:B756 extractPayloadVarsAndVerifyNameHash()
; 03:B79A findScrambles()
; 03:B72A hashPayload()
; 03:B7D6 throwDefaultBadPassword:
; 03:B7D8 throwBadPassword:
; 03:B7DF LEADERS (3 bytes)
; 03:B7E2 squeezeRowCol()
; 03:B7F0 verifyAllNonblanksInScrambles()
; 03:B82C decodePayloadAndPayloadHash()
; 03:B865 verifyPayloadHash()
; 03:B87F isValidSavePoint()
; 03:B8B6 VALID_SAVE_POINTS (6 bytes)
; 03:B8BC BIT_MASKS (8 bytes)
; 03:B8C4 createUnscrambledPassword:
; 03:B940 drawPassword()
; 03:B937 SELECTORS (9 bytes)
; 03:B97E SPRITE_B (4 bytes)
; 03:B982 SPRITE_A (4 bytes)
; 03:B986 MATRIX_COORDINATES (32 bytes)
;
; 7F:E2E6 switchBanks()
; 7F:E593 _checkForSomeSpecialNames()


; convertBlockSubBlockToSavePoint()
00:8FB0  LDA $0032
00:8FB2  ASL A
00:8FB3  CLC
00:8FB4  ADC $0032
00:8FB6  TAY          ; Y = 3 * block;
00:8FB7  LDA $8FD1,Y  ; if (subBlock > BLOCK_SUB_BLOCK_TO_SAVE_POINT[Y]) {
00:8FBA  CMP $0033    ;     ++Y;
00:8FBC  BCS $8FBF    ; }
00:8FBE  INY
00:8FBF  LDA $8FD2,Y
00:8FC2  CMP #$03
00:8FC4  BEQ $8FC9
00:8FC6  STA $002E    ; savePoint = (BLOCK_SUB_BLOCK_TO_SAVE_POINT[Y + 1] != 3 || escapedClockTower == 0)
                      ;         ? BLOCK_SUB_BLOCK_TO_SAVE_POINT[Y + 1] : 4;
00:8FC8  RTS
00:8FC9  LDY $002F
00:8FCB  BEQ $8FC6
00:8FCD  LDA #$04
00:8FCF  BNE $8FC6    ; return;

; BLOCK_SUB_BLOCK_TO_SAVE_POINT
; Each row corresponds to a block. Each block contains 1 or 2 save points. If a block contains 1 save point, the first
; column contains $10 and the second and third columns both contain its save point. Otherwise, if the sub-block exceeds
; the value in the first column, then the save point in the third column is used; else, the save point in the second
; column is used.
00:8FD1  .byte $10, $00, $00 ; 0: 1-1
00:8FD4  .byte $02, $01, $02 ; 1: 2-1, 2-4
00:8FD7  .byte $10, $03, $04 ; 2: 3-0, 3-1
00:8FDA  .byte $10, $05, $05 ; 3: 4-A
00:8FDD  .byte $10, $06, $06 ; 4: 5-A
00:8FE0  .byte $10, $07, $07 ; 5: 6-A
00:8FE3  .byte $10, $08, $08 ; 6: 4-1
00:8FE6  .byte $04, $09, $0A ; 7: 5-1, 5-6
00:8FE9  .byte $10, $0B, $0B ; 8: 6-1
00:8FEC  .byte $10, $0C, $0C ; 9: 6-1'
00:8FEF  .byte $10, $0D, $0D ; A: 7-1
00:8FF2  .byte $10, $0E, $0E ; B: 7-A
00:8FF5  .byte $10, $0F, $0F ; C: 8-1
00:8FF8  .byte $10, $10, $10 ; D: 9-1
00:8FFB  .byte $10, $11, $11 ; E: A-1

; checkForSpecialNames()
; out: carry (false = no, true = yes)
;      Y (1 = "HELP ME ", 2 = "AKAMA   ", 3 = "OKUDA   ", 4, = "URATA   ", 5 = "FUJIMOTO")
00:8FFE  LDY #$00     ; Y = 0;
00:9000  JSR $9031    ; if (isSpecialName()) {
00:9003  BCS $902E    ;     Y = 1;
                      ;     return;
                      ; }

; checkForSomeSpecialNames()
; out: carry (false = no, true = yes)
;      Y (2 = "AKAMA   ", 3 = "OKUDA   ", 4, = "URATA   ", 5 = "FUJIMOTO")
00:9005  LDY #$02     ; Y = 2;
00:9007  JSR $9031    ; if (isSpecialName()) {
00:900A  BCS $902B    ;     Y = 2;
                      ;     return;
                      ; }

00:900C  LDY #$04     ; Y = 4;
00:900E  JSR $9031    ; if (isSpecialName()) {
00:9011  BCS $9022    ;     Y = 3;
                      ;     return;
                      ; }

00:9013  LDY #$06     ; Y = 6;
00:9015  JSR $9031    ; if (isSpecialName()) {
00:9018  BCS $9025    ;     Y = 4;
                      ;     return;
                      ; }

00:901A  LDY #$08     ; Y = 8;
00:901C  JSR $9031    ; if (isSpecialName()) {
00:901F  BCS $9028    ;     Y = 5;
                      ;     return;
                      ; }

00:9021  RTS

00:9022  LDY #$03
00:9024  RTS

00:9025  LDY #$04
00:9027  RTS

00:9028  LDY #$05
00:902A  RTS

00:902B  LDY #$02
00:902D  RTS

00:902E  LDY #$01
00:9030  RTS

; isSpecialName()
;  in: Y (2 * special name index)
; out: carry (false = no, true = yes)
00:9031  LDA $904D,Y
00:9034  STA $0008
00:9036  LDA $904E,Y
00:9039  STA $0009    ; specialName = *SPECIAL_NAME_ADDRESSES[Y / 2];
00:903B  LDY #$00     ;
00:903D  LDA $07F8,Y  ; for (Y = 0; Y < 8; ++Y) {
00:9040  CMP ($08),Y  ;     if (name[Y] != specialName[Y]) {
00:9042  BNE $904B    ;         carry = false;
00:9044  INY          ;         return;
00:9045  CPY #$08     ;     }
00:9047  BNE $903D    ; }

00:9049  SEC          ; carry = true;
00:904A  RTS          ; return;

00:904B  CLC
00:904C  RTS

; SPECIAL_NAME_ADDRESSES
00:904D  .word $9057, $905F, $9067, $906F, $9077

; SPECIAL_NAMES
00:9057  .byte $57, $54, $5B, $5F, $00, $5C, $54, $00  ; "HELP ME " // Start and continue with 10 lives.
00:905F  .byte $50, $5A, $50, $5C, $50, $00, $00, $00  ; "AKAMA   " // Start in Hard Mode alone.
00:9067  .byte $5E, $5A, $64, $53, $50, $00, $00, $00  ; "OKUDA   " // Start in Normal Mode with Alucard.
00:096F  .byte $64, $61, $50, $63, $50, $00, $00, $00  ; "URATA   " // Start in Normal Mode with Sypha.
00:0977  .byte $55, $64, $59, $58, $5C, $5E, $63, $5E  ; "FUJIMOTO" // Start in Normal Mode with Grant.


00:907F  LDA #$B0
00:9081  STA $00FF    ; hud = 0xB0; // visible
00:9083  JSR $90CD    ; reset4Aand4D();
00:9086  JSR $90C0    ; resetHeartsAndEnergy();
00:9089  LDA #$02
00:908B  STA $003E    ; mem[$003E] = 0x02;
00:908D  JSR $8FFE    ; checkForSpecialNames(); // results in carry and Y
00:9090  BCC $90B7    ; if (carry == 1) { // if special name
00:9092  DEY          ;     if (--Y == 0) {
00:9093  BEQ $90BC    ;         lives = 10;
                      ;         return; // "HELP ME ": start/continue with 10 lives
                      ;     }
00:9095  DEY          ;     if (--Y == 0) {
00:9096  BEQ $90B2    ;         lives = 2;
                      ;         mode = 1;
                      ;         return; // "AKAMA   ": start/continue in Hard Mode alone
                      ;     }
00:9098  LDA $003A    ;     if (partner != 0xFF) { // if (partner != none)
00:909A  CMP #$FF     ;         lives = 2;
00:909C  BNE $90B7    ;         return; // continue in Normal Mode with a partner
                      ;     }
00:909E  DEY          ;     if (--Y == 0) {
00:909F  BEQ $90AC    ;         partner = 0x03;
                      ;         lives = 2;
                      ;         return; // "OKUDA   ": start/continue in Normal Mode with Alucard
                      ;     }
00:90A1  DEY          ;     if (--Y == 0) {
00:90A2  BEQ $90A8    ;         partner = 0x01;
                      ;         lives = 2;
                      ;         return; ; "URATA   ": start/continue in Normal Mode with Sypha.
                      ;     }
00:90A4  LDA #$02
00:90A6  BNE $90AE
00:90A8  LDA #$01
00:90AA  BNE $90AE
00:90AC  LDA #$03
00:90AE  STA $003A    ;     partner = 0x02;
00:90B0  BNE $90B7    ; }
00:90B2  LDA #$01
00:90B4  STA $07F6
00:90B7  LDA #$02
00:90B9  STA $0035    ; lives = 2;
00:90BB  RTS          ; return;
00:90BC  LDA #$10
00:90BE  BNE $90B9

; resetHeartsAndEnergy()
00:90C0  LDA #$05
00:90C2  STA $0084    ; hearts = 5;
00:90C4  LDA #$40
00:90C6  STA $003C    ; playerEnergy = 0x40; // full energy
00:90C8  LDA #$40
00:90CA  STA $003D    ; bossEnergy = 0x40; // full energy
00:90CC  RTS          ; return;

; reset4Aand4D()
00:90CD  LDA #$40
00:90CF  STA $004A    ; mem[0x004A] = 0x40;
00:90D1  LDA #$43
00:90D3  STA $004D    ; mem[0x004D] = 0x43;
00:90D5  RTS          ; return;

; submitPassword()
03:B2F8  INC $0019
03:B2FA  JSR $B48D
03:B2FD  JMP $B471
03:B300  JSR $B3DB
03:B303  JSR $B50C
03:B306  JSR $B3B9
03:B309  LDA $0026
03:B30B  AND #$30
03:B30D  BNE $B313
03:B30F  LDA $002D
03:B311  BEQ $B338
03:B313  LDA $0026
03:B315  AND #$20
03:B317  BNE $B349
03:B319  JSR $B656    ; decode();
03:B31C  LDX #$05
03:B31E  JSR $B627
03:B321  LDA $078B    ; if (badPasswordReason == 0) {
03:B324  BEQ $B339    ;     goto handleValidPassword;
03:B326  LDA #$40     ; }
03:B328  JSR $E25F
03:B32B  LDA #$09
03:B32D  STA $0019
03:B32F  LDA #$23
03:B331  JSR $ECE9
03:B334  LDA #$78
03:B336  STA $0030
03:B338  RTS          ; return;

; handleValidPassword:
03:B339  LDA #$78
03:B33B  STA $0030
03:B33D  LDA #$07
03:B33F  STA $0160
03:B342  LDA #$0A
03:B344  STA $0019
03:B346  JMP $B066
03:B349  JSR $B066
03:B34C  LDA #$0B
03:B34E  STA $0019
03:B350  RTS          ; return;

; showPassword()
03:B5AF  STA $0025
03:B5B1  STA $5105
03:B5B4  JSR $EBFD
03:B5B7  LDA #$98
03:B5B9  LDX #$1A
03:B5BB  JSR $EBD5
03:B5BE  JSR $E2D6
03:B5C1  LDA #$62
03:B5C3  JSR $E25F
03:B5C6  INC $0019
03:B5C8  JSR $B1C7
03:B5CB  JSR $B625
03:B5CE  JSR $B675    ; resetPasswordVars();
03:B5D1  JSR $B64A    ; encode();
03:B5D4  JSR $B28B
03:B5D7  JSR $B647    ; _drawPassword();
03:B5DA  JSR $B066
03:B5DD  LDA #$03
03:B5DF  STA $001C
03:B5E1  JMP $B3FB
03:B5E4  LDA $00B4
03:B5E6  CMP #$FF
03:B5E8  BEQ $B604
03:B5EA  LDA $001D
03:B5EC  STA $0015
03:B5EE  JSR $B598
03:B5F1  JSR $FBA4
03:B5F4  LDA $00B4
03:B5F6  CMP #$FF
03:B5F8  BNE $B60F
03:B5FA  LDA #$00
03:B5FC  LDX $0015
03:B5FE  STX $001D
03:B600  STA $0300,X
03:B603  RTS          ; return;

; _drawPassword()
03:B647  JMP $B940    ; drawPassword();

; encode()
03:B64A  JSR $B6CD    ; hashName(); // result in A
03:B64D  STA $078A    ; nameHash = A;
03:B650  JSR $B6EE    ; encodePayload();
03:B653  JMP $B8C4    ; goto createUnscrambledPassword;

; decode()
03:B656  JSR $B6CD    ; hashName(); // result in A
03:B659  STA $0010    ; _nameHash = A;
03:B65B  JSR $B79A    ; findScrambles();
03:B65E  JSR $B7F0    ; verifyAllNonblanksInScrambles();
03:B661  JSR $B68F    ; unscramblePassword();
03:B664  JSR $B82C    ; decodePayloadAndPayloadHash();
03:B667  JSR $E593    ; _checkForSomeSpecialNames(); // results in carry and Y
03:B66A  BCS $B66F    ; if (carry == 1) {
03:B66C  JSR $B87F    ;     isValidSavePoint(); // name is special
                      ; }
03:B66F  JSR $B756    ; extractPayloadVarsAndVerifyNameHash();
03:B672  JMP $B865    ; verifyPayloadHash();

; resetPasswordVars()
03:B675  LDA #$00
03:B677  LDX #$00     ; for (X = 0; X < 0x10; ++X) {
03:B679  STA $0780,X  ;     mem[0x0780 + X] = 0;
03:B67C  INX
03:B67D  CPX #$10
03:B67F  BCC $B679    ; }
03:B681  RTS          ; return;

; clearPassword()
03:B682  LDY #$00
03:B684  LDA #$00     ; for (Y = 0; Y < 0x10; ++Y) {
03:B686  STA $0790,Y  ;     password[Y] = 0; // no mark
03:B689  INY
03:B68A  CPY #$10
03:B68C  BCC $B686    ; }
03:B68E  RTS          ; return;

; unscramblePassword()
; Copies 9 marks from password to unscrambledPassword based on the SCRAMBLES[9 * scramblesRowIndex] sequence.
03:B68F  LDX #$00
03:B691  LDA $078F
03:B694  ASL A
03:B695  ASL A
03:B696  ASL A
03:B697  ADC $078F
03:B69A  STA $0000
03:B69C  LDY $0000    ; for (X = 0; X < 9; ++X, ++v0000) {
03:B69E  LDA $B6B2,Y  ;     A = SCRAMBLES[9 * scramblesRowIndex + X];
03:B6A1  JSR $B7E2    ;     squeezeRowCol(); // result in Y
03:B6A4  LDA $0790,Y
03:B6A7  STA $07A0,X  ;     unscrambledPassword[X] = password[Y];
03:B6AA  INC $0000
03:B6AC  INX
03:B6AD  CPX #$09
03:B6AF  BCC $B69C    ; }
03:B6B1  RTS          ; return;

; SCRAMBLES
; This table contains 3 sequences of matrix elements used to encode the game state. The row and column are stored in
; the high and low nibbles, respectively.
03:B6B2  .byte $00, $33, $20, $13, $22, $01, $11, $03, $32  ; 0
03:B6BB  .byte $12, $10, $02, $32, $23, $13, $30, $21, $01  ; 1
03:B6C4  .byte $31, $13, $01, $22, $10, $30, $33, $03, $21  ; 2

; hashName()
; out: A = name hash (0--7)
03:B6CD  LDA #$00
03:B6CF  STA $0000    ; sum = 0;
03:B6D1  TAX
03:B6D2  LDA $07F8,X  ; for (X = 0; X < 8; ++X) {
03:B6D5  CLC
03:B6D6  ADC $B6E6,X
03:B6D9  CLC
03:B6DA  ADC $0000
03:B6DC  STA $0000    ;     sum += name[X] + NAME_HASH_SEEDS[X];
03:B6DE  INX
03:B6DF  CPX #$08
03:B6E1  BNE $B6D2    ; }
03:B6E3  AND #$07     ; A = sum % 8;
03:B6E5  RTS          ; return;

; NAME_HASH_SEEDS
; Due to the modulo operation, this table is pointless; the values can be tallied ahead of time. However, the
; intention may have been to apply this table only to the nonblank characters. But that check is not there.
03:B6E6  .byte $07, $03, $01, $06, $02, $04, $05, $00

; encodePayload()
03:B6EE  LDA $078A
03:B6F1  STA $0000    ; payload = nameHash;
03:B6F3  LDA $002E    ; if (savePoint >= 0x11) {
03:B6F5  CMP #$11     ;     savePoint = 0x11;
03:B6F7  BCC $B6FB    ; }
03:B6F9  LDA #$11
03:B6FB  STA $002E
03:B6FD  LSR A
03:B6FE  ROL $0000    ; payload = (payload << 1) | (savePoint & 1);
03:B700  LDA $001A
03:B702  LSR A
03:B703  ROL $0000    ; payload = (payload << 1) | (frameCounter & 1);
03:B705  ROL $0000
03:B707  ROL $0000
03:B709  LDA $003A
03:B70B  BPL $B70F
03:B70D  LDA #$00
03:B70F  ORA $0000    ; payload = (payload << 2) | (partner == 0xFF ? 0 : partner);
03:B711  ASL A
03:B712  ORA $07F6
03:B715  STA $0788    ; payload = (payload << 1) | mode;
03:B718  LDA $001A
03:B71A  LSR A
03:B71B  LDA #$50
03:B71D  BCC $B721
03:B71F  LDA #$A0
03:B721  STA $0004    ; toggleMask = (frameCounter & 1) == 0 ? 0xA0 : 0x50;
03:B723  JSR $B72A    ; hashPayload(payload); // result in A
03:B726  STA $0789    ; payloadHash = A;
03:B729  RTS          ; return;

; hashPayload()
03:B72A  LDA $0788
03:B72D  AND #$F0
03:B72F  STA $0002    ; highNibble = payload & 0xF0;
03:B731  LDA $0788
03:B734  ASL A
03:B735  ASL A
03:B736  ASL A
03:B737  ASL A
03:B738  STA $0003    ; lowNibble = payload << 4;
03:B73A  CLC
03:B73B  ADC $0002
03:B73D  STA $0001    ; nibbleSum = highNibble + lowNibble;
03:B73F  LDA $0004
03:B741  EOR $0002
03:B743  STA $0000    ; toggledHighNibble = highNibble ^ toggleMask;
03:B745  LDA $0004
03:B747  EOR $0003
03:B749  CLC
03:B74A  ADC $0000
03:B74C  LSR A
03:B74D  LSR A
03:B74E  LSR A
03:B74F  LSR A
03:B750  ORA $0001
03:B752  CLC
03:B753  ADC $002E    ; A = savePoint + (nibbleSum | ((toggledHighNibble + (lowNibble ^ toggleMask)) >> 4));
03:B755  RTS          ; return;

; extractPayloadVarsAndVerifyNameHash()
03:B756  LDA $0788
03:B759  AND #$01
03:B75B  STA $07F6    ; mode = payload & 1;
03:B75E  LDA $0788
03:B761  LSR A
03:B762  AND #$03     ; A = (payload >> 1) & 3;
03:B764  BNE $B768    ; if (A == 0) {
03:B766  LDA #$FF     ;     A = 0xFF; // no partner
                      ; }
03:B768  STA $003A    ; partner = A;
03:B76A  LDA $0788
03:B76D  AND #$10
03:B76F  BEQ $B777
03:B771  LDA $002E
03:B773  ORA #$01     ; if ((payload & 0x10) != 0) {
03:B775  STA $002E    ;     savePoint |= 1;
03:B777  LDA $0788    ; }
03:B77A  LSR A
03:B77B  LSR A
03:B77C  LSR A
03:B77D  LSR A
03:B77E  LSR A
03:B77F  STA $078A    ; nameHash = payload >> 5;
03:B782  CMP $0010    ; if (nameHash != _nameHash) {
03:B784  BEQ $B78B    ;     A = 0x10;
03:B786  LDA #$10     ;     goto throwBadPassword;
03:B788  JMP $B7D8    ; }
03:B78B  LDA $002E    ; if (savePoint == 2 || savePoint == 4) {
03:B78D  CMP #$02     ;     escapedClockTower = 1;
03:B78F  BEQ $B795    ; }
03:B791  CMP #$04
03:B793  BNE $B799
03:B795  LDA #$01
03:B797  STA $002F
03:B799  RTS          ; return;

; findScrambles()
03:B79A  LDA #$02
03:B79C  STA $0000
03:B79E  LDA #$00
03:B7A0  STA $0001    ; markCount = 0;
03:B7A2  LDY $0000    ; for (i = 2; i >= 0; --i) {
03:B7A4  LDA $B7DF,Y  ;     Y = LEADERS[i];
03:B7A7  JSR $B7E2    ;     squeezeRowCol(); // result in Y
03:B7AA  LDA $0790,Y  ;     if ((password[Y] & 3) == 0) {
03:B7AD  AND #$03     ;         continue; // if blank, continue
03:B7AF  BEQ $B7CB    ;     }
03:B7B1  LDA $0000
03:B7B3  STA $078F    ;     scramblesRowIndex = i;
03:B7B6  INC $0001    ;     ++markCount;
03:B7B8  LDX #$00     ;     for (X = 0; X < 9; ++X) {
03:B7BA  LDA $0790,Y  ;
03:B7BD  CMP $B937,X  ;         if (password[Y] == SELECTORS[X]) {
03:B7C0  BEQ $B7C7    ;             break;
03:B7C2  INX          ;         }
03:B7C3  CPX #$09
03:B7C5  BNE $B7BD    ;     }
03:B7C7  TXA
03:B7C8  ASL A
03:B7C9  STA $002E    ;     savePoint = X << 1;
03:B7CB  DEC $0000
03:B7CD  BPL $B7A2    ; }
03:B7CF  LDA $0001    ; if (markCount != 1) {
03:B7D1  CMP #$01     ;     goto throwDefaultBadPassword;
03:B7D3  BNE $B7D6    ; }
03:B7D5  RTS          ; return;

throwDefaultBadPassword:
03:B7D6  LDA #$01     ; A = 1;

throwBadPassword:
;  in: A = bad password reason
03:B7D8  ORA $078B
03:B7DB  STA $078B    ; badPasswordReason |= A;
03:B7DE  RTS          ; return;

; LEADERS
; Exactly one of the elements at (0, 0), (1, 2), and (3, 1) is marked nonblank. The index of the element of this table
; corresponding to that nonblank mark determines which of the 3 scramble sequences is used (scramblesRowIndex).
03:B7DF  .byte $00, $12, $31

; squeezeRowCol()
;  in: A = ..rr..cc
; out: Y = ....rrcc
03:B7E2  PHA
03:B7E3  AND #$30
03:B7E5  LSR A
03:B7E6  LSR A
03:B7E7  STA $0007
03:B7E9  PLA
03:B7EA  AND #$03
03:B7EC  ORA $0007
03:B7EE  TAY          ; Y = ((A & 0x30) >> 2) | (A & 0x03);
03:B7EF  RTS          ; return;

; verifyAllNonblanksInScrambles()
03:B7F0  LDA $078F
03:B7F3  ASL A
03:B7F4  ASL A
03:B7F5  ASL A
03:B7F6  ADC $078F
03:B7F9  STA $0000    ; scramblesRowOffset = 9 * scramblesRowIndex;
03:B7FB  LDA #$0F
03:B7FD  STA $0001
03:B7FF  LDY $0001    ; outer: for (rowCol = 0x0F; rowCol >= 0; --rowCol) { // ....rrcc
03:B801  LDA $0790,Y
03:B804  AND #$03
03:B806  BEQ $B827    ;     if ((password[rowCol] & 0x03) == 0) { // if blank, continue
                      ;         continue;
                      ;     }
03:B808  LDA $0000
03:B80A  STA $0002    ;     offset = scramblesRowOffset;
03:B80C  LDA #$09
03:B80E  STA $0003    ;     for (i = 9; i > 0; --i, ++offset) {
03:B810  LDY $0002
03:B812  LDA $B6B2,Y  ;         A = SCRAMBLES[offset];
03:B815  JSR $B7E2    ;         squeezeRowCol(); // result in Y
03:B818  CPY $0001    ;         if (Y == rowCol) {
03:B81A  BEQ $B827    ;             continue outer;
                      ;         }
03:B81C  INC $0002
03:B81E  DEC $0003
03:B820  BNE $B810    ;     }
03:B822  LDA #$02     ;     A = 2;
03:B824  JMP $B7D8    ;     goto throwBadPassword; // nonblank element not in scramble row
03:B827  DEC $0001
03:B829  BPL $B7FF    ; }
03:B82B  RTS          ; return;

; decodePayloadAndPayloadHash()
03:B82C  LDX #$00
03:B82E  LDA $07A1,X  ; for (X = 0; X < 8; ++X) {
03:B831  STA $08,X    ;     _unscrambledPassword[X] = unscrambledPassword[X + 1];
03:B833  INX
03:B834  CPX #$08
03:B836  BCC $B82E    ; }
03:B838  LDA #$00
03:B83A  STA $0000
03:B83C  STA $0001
03:B83E  LDY #$00     ; payloadHash = payload = 0;
03:B840  LDX #$00
03:B842  LSR $08,X    ; for (X = Y = 0; Y < 8; ++Y, ++X) {
03:B844  ROR $0001    ;     payloadHash = ((_unscrambledPassword[X] & 1)) << 7) | (payloadHash >> 1);
03:B846  LSR $08,X    ;     _unscrambledPassword[X] >>= 1;
03:B848  ROR $0000    ;     payload = ((_unscrambledPassword[X] & 1) << 7) | (payload >> 1);
03:B84A  INX          ;     _unscrambledPassword[X] >>= 1;
03:B84B  INY
03:B84C  CPY #$08
03:B84E  BCC $B842    ; }
03:B850  LDA $0001
03:B852  STA $0789
03:B855  LDA $0000
03:B857  STA $0788
03:B85A  AND #$10
03:B85C  LSR A
03:B85D  LSR A
03:B85E  LSR A
03:B85F  LSR A
03:B860  ORA $002E
03:B862  STA $002E    ; savePoint |= (payload & 0x10) >> 4;
03:B864  RTS          ; return;

; verifyPayloadHash()
03:B865  LDY #$50
03:B867  LDA $0788
03:B86A  AND #$08
03:B86C  BEQ $B870
03:B86E  LDY #$A0
03:B870  STY $0004    ; toggleMask = ((payload & 0x08) != 0) ? 0xA0 : 0x50;
03:B872  JSR $B72A    ; hashPayload(); // result in A
03:B875  CMP $0789    ; if (A == payloadHash) {
03:B878  BEQ $B864    ;     return;
                      ; }
03:B87A  LDA #$04     ; A = 4;
03:B87C  JMP $B7D8    ; goto throwBadPassword;

; isValidSavePoint()
03:B87F  LDA $0788    ; if ((payload & 1) != 0) {
03:B882  AND #$01     ;     return; // Hard Mode
03:B884  BNE $B8B5    ; }
03:B886  LDA $002E    ; if (savePoint >= 0x12) {
03:B888  CMP #$12     ;     A = 8;
03:B88A  BCS $B8B0    ;     goto throwBadPassword; // Invalid savePoint
                      ; }
03:B88C  CMP #$10     ; if (savePoint >= 0x10) {
03:B88E  BCS $B8B5    ;     return; // final 2 blocks
                      ; }
03:B890  LDA $0788
03:B893  AND #$06
03:B895  STA $0000    ; _partner = payload & 0x06;
03:B897  BEQ $B8B5    ; if (_partner == 0) {
                      ;     return; // no partner
                      ; }
03:B899  LDA $002E
03:B89B  AND #$08
03:B89D  LSR A
03:B89E  LSR A
03:B89F  LSR A
03:B8A0  ORA $0000
03:B8A2  TAY
03:B8A3  LDA $002E
03:B8A5  AND #$07
03:B8A7  TAX
03:B8A8  LDA $B8B4,Y
03:B8AB  AND $B8BC,X
03:B8AE  BNE $B8B5    ; if ((VALID_SAVE_POINTS[(_partner | ((savePoint & 0x08) >> 3)) - 2]
                      ;         & BIT_MASKS[savePoint & 0x07]) != 0) {
                      ;     return;
                      ; }
03:B8B0  LDA #$08
03:B8B2  JMP $B7D8
03:B8B5  RTS

; VALID_SAVE_POINTS
; In normal mode, a partner may only be used along pathway of save points which starts when the partner was first
; encountered in the game. Each bit of the elements of this table correspond to a save point. A partner may only be
; used in a save point where the associated bit is 1. Each pair of elements maps to a different partner. Within the
; pair, the bits correspond to the following save points:
; 0: (1-1, 2-1, 2-4, 3-0, 3-1, 4-A, 5-A, 6-A)
; 1: (4-1, 5-1, 5-6, 6-1, 6-1', 7-1, 7-A, 8-1)
; All partners may be used in save points 9-1 and A-1.
03:B8B6  .byte $07  ;   Sypha (4-A, 5-A, 6-A)
03:B8B7  .byte $03  ;   Sypha (7-A, 8-1)
03:B8B8  .byte $2F  ;   Grant (2-4, 3-1, 4-A, 5-A, 6-A)
03:B8B9  .byte $FF  ;   Grant (4-1, 5-1, 5-6, 6-1, 6-1', 7-1, 7-A, 8-1)
03:B8BA  .byte $00  ; Alucard ()
03:B8BB  .byte $3D  ; Alucard (5-6, 6-1, 6-1', 7-1, 8-1)

; BIT_MASKS
; This is used to extract bits from the elements of VALID_SAVE_POINTS.
03:B8BC  .byte $80, $40, $20, $10, $08, $04, $02, $01

createUnscrambledPassword:
03:B8C4  LDA $0788
03:B8C7  STA $0000    ; _payload = payload;
03:B8C9  LDA $0789
03:B8CC  STA $0001    ; _payloadHash = payloadHash;
03:B8CE  LDX #$08
03:B8D0  LDA #$00
03:B8D2  STA $08,X    ; for (X = 8; X >= 0; --X) {
03:B8D4  DEX          ;     _unscrambledPassword[X] = 0;
03:B8D5  BPL $B8D2    ; }
03:B8D7  LDX #$00
03:B8D9  LSR $0000    ; for (X = 0; X < 8; ++X) {
03:B8DB  ROL $08,X    ;     _unscrambledPassword[X] = (_unscrambledPassword[X] << 1) | (_payload & 1);
03:B8DD  LSR $0001    ;     _payload >>= 1;
03:B8DF  ROL $08,X    ;     _unscrambledPassword[X] = (_unscrambledPassword[X] << 1) | (_payloadHash & 1);
03:B8E1  INX          ;     _payloadHash >>= 1;
03:B8E2  CPX #$08
03:B8E4  BCC $B8D9    ; }
03:B8E6  LDA $002E
03:B8E8  LSR A
03:B8E9  TAY          ; Y = savePoint >> 1;
03:B8EA  LDX #$02
03:B8EC  LDA $B937,Y  ; for (X = 2; X >= 0; --X) {
03:B8EF  AND #$0C
03:B8F1  LSR A
03:B8F2  LSR A
03:B8F3  STA $0000
03:B8F5  LDA $B937,Y
03:B8F8  AND #$30
03:B8FA  ORA $0000    ;
03:B8FC  CMP $B7DF,X  ;     if (LEADERS[X] == ((SELECTORS[Y] & 0x30) | ((SELECTORS[Y] & 0x0C) >> 2))) {
03:B8FF  BEQ $B904    ;         break;
03:B901  DEX          ;     }
03:B902  BPL $B8EC    ; }
03:B904  STX $078F    ; scramblesRowIndex = X;
03:B907  TXA
03:B908  ASL A
03:B909  ASL A
03:B90A  ASL A
03:B90B  ADC $078F
03:B90E  TAY          ; Y = 9 * scramblesRowIndex;
03:B90F  LDX #$00
03:B911  LDA $B6B3,Y  ; for (X = 0; X < 9; ++X, ++Y) { // one too many iterations?
03:B914  AND #$30
03:B916  STA $0001
03:B918  LDA $B6B3,Y
03:B91B  AND #$03
03:B91D  ASL A
03:B91E  ASL A
03:B91F  ORA $0001
03:B921  ORA $08,X
03:B923  STA $07A1,X  ;     unscrambledPassword[X + 1] = ((SCRAMBLES[Y + 1] & 0x03)) << 2) | (SCRAMBLES[Y + 1] & 0x30)
03:B926  INY          ;             | _unscrambledPassword[X]; // ..rrccmm
03:B927  INX
03:B928  CPX #$09     ;
03:B92A  BCC $B911    ; }
03:B92C  LDA $002E
03:B92E  LSR A
03:B92F  TAY
03:B930  LDA $B937,Y
03:B933  STA $07A0    ; unscrambledPassword[0] = SELECTORS[savePoint >> 1];
03:B936  RTS          ; return;

; SELECTORS
; Exactly one of the elements at (0, 0), (1, 2), and (3, 1) is marked nonblank. This table contains the 9 possible ways
; to achieve that. The element to mark is determined by bits 1--4 of savePoint, which is used as the index into this
; table. Since savePoint cannot exceed $11, the index covers 0--8.
;              W00, H12, R00, W31, W12, H00, H31, R12, R31
03:B937  .byte $01, $1B, $02, $35, $19, $03, $37, $1A, $36

; drawPassword()
03:B940  LDA #$00
03:B942  STA $0000
03:B944  LDX #$05
03:B946  LDY $0000    ; for(X = 5, markIndex = 0; markIndex < 9; ++markIndex, ++X) {
03:B948  LDA $07A0,Y
03:B94B  AND #$03
03:B94D  TAY          ;     Y = unscrambledPassword[markIndex] & 0x03; // ......mm
03:B94E  LDA $B982,Y
03:B951  STA $0400,X  ;     SPRITE_0[X] = SPRITE_A[Y];
03:B954  LDA $B97E,Y
03:B957  STA $048C,X  ;     SPRITE_1[X] = SPRITE_B[Y];
03:B95A  LDY $0000
03:B95C  LDA $07A0,Y
03:B95F  AND #$3C
03:B961  LSR A
03:B962  TAY          ;     Y = (0x3C & unscrambledPassword[markIndex]) >> 1; // ...rrcc.
03:B963  LDA $B986,Y
03:B966  STA $041C,X  ;     MARK_YS[X] = MATRIX_COORDINATES[Y];
03:B969  LDA $B987,Y
03:B96C  STA $0438,X  ;     MARK_XS[X] = MATRIX_COORDINATES[Y + 1];
03:B96F  LDA #$00
03:B971  STA $0454,X  ;     MARK_ATTRIBS[X] = 0;
03:B974  INX
03:B975  INC $0000
03:B977  LDA $0000
03:B979  CMP #$09
03:B97B  BCC $B946    ; }
03:B97D  RTS          ; return;

; SPRITE_B
03:B97E  .byte $00, $14, $0C, $0C  ; blank, whip, rosary, heart

; SPRITE_A
03:B982  .byte $00, $42, $FC, $F4  ; blank, whip, rosary, heart

; MATRIX_COORDINATES
; These are (y, x)-coordinates corresponding to positions within the password matrix.
03:B986  .byte $7A, $5D, $7A, $75, $7A, $8D, $7A, $A5, $92, $5D, $92, $75, $92, $8D, $92, $A5
03:B996  .byte $AA, $5D, $AA, $75, $AA, $8D, $AA, $A5, $C2, $5D, $C2, $75, $C2, $8D, $C2, $A5

; switchBanks()
7F:E2E6  STA $0021
7F:E2E8  STA $5115
7F:E2EB  RTS          ; return;

; _checkForSomeSpecialNames()
; out: carry (false = no, true = yes)
;      Y (2 = "AKAMA   ", 3 = "OKUDA   ", 4, = "URATA   ", 5 = "FUJIMOTO")
7F:E593  LDA #$80     ; A = 0x80;
7F:E595  JSR $E2E6    ; switchBanks();
7F:E598  JSR $9005    ; checkForSomeSpecialNames();
7F:E59B  LDA #$82     ; A = 0x82;
7F:E59D  JMP $E2E6    ; switchBanks();