Sounds like soon we will have a total control TAS of this game!
EDIT: Because "mostly warping me to the title screen" sounds like it executes some random code. Executing BRK (00) often has the same effect as resetting the game in many games.
Debugging the context where the crash happens. Capital-letter comments contain comments relevant to this movie.
;------------------------------------------
GameLogic_DoorScrolling
$C382 A5 28: lda CurrentStage
$C384 C9 12: cmp #$12
$C386 D0 0B: bne + ; $C393 (THIS BRANCH IS TAKEN)
---- $C388 85 29: sta $29
$C38A A9 0C: lda #$0C
$C38C 85 18: sta GameMode
$C38E A9 08: lda #$08
$C390 85 19: sta GameModeAux
$C392 60: rts
+ $C393 20 6A FA: jsr _func_1FA6A ; THIS FUNCTION DOES NOTHING OF IMPORTANCE IN THIS CONTEXT
$C396 A9 00: lda #$00
$C398 20 BA 9B: jsr _func_1BBA ; CALLS $19BBA (BANK 6, WHICH IS CORRECT)
$C39B 20 F0 E8: jsr GameLogic_GameRunning_ObjectAI ; THIS CHANGES THE CURRENT BANK
$C39E AD 84 05: lda ObjectBonusType
$C3A1 29 03: and #$03
$C3A3 85 3C: sta SimonMovingDirection
$C3A5 A6 19: ldx GameModeAux
$C3A7 D0 26: bne ++ ; $C3CF (THIS BRANCH IS TAKEN)
$C3A9 A5 2E: lda NumberofLives
$C3AB C9 80: cmp #$80
$C3AD F0 17: beq + ; $C3C6
--- $C3AF 20 0B 99: jsr _func_190B
$C3B2 A0 04: ldy #$04
$C3B4 20 D6 C1: jsr SwitchBank_SaveNewBank
$C3B7 20 EA F5: jsr _func_1F5EA
$C3BA 20 D4 C1: jsr SwitchBank_Bank6
$C3BD A5 3C: lda SimonMovingDirection
$C3BF 49 03: eor #$03
$C3C1 85 3C: sta SimonMovingDirection
$C3C3 4C B7 98: jmp _loc_198B7
+ $C3C6 20 74 C4: jsr _func_1C474
$C3C9 20 A4 C4: jsr _func_1C4A4
$C3CC 4C FB C3: jmp +++ ; $C3FB -> _loc_1C365
++ $C3CF CA: dex
$C3D0 D0 2C: bne ++++ ; $C3FE (THIS BRANCH IS TAKEN)
$C3D2 A5 1A: lda FrameCounter
$C3D4 29 0F: and #$0F
$C3D6 D0 0D: bne + ; $C3E5
$C3D8 E6 25: inc $25
$C3DA A5 25: lda $25
$C3DC C9 02: cmp #$02
$C3DE D0 05: bne + ; $C3E5
$C3E0 A9 1D: lda #$1D
$C3E2 20 A7 C1: jsr PlaySFX
+ $C3E5 A5 25: lda $25
$C3E7 C9 03: cmp #$03
$C3E9 F0 07: beq + ; $C3F2
$C3EB 18: clc
$C3EC 69 1C: adc #$1C
$C3EE 8D 1F 03: sta $031F
$C3F1 60: rts
+ $C3F2 A9 01: lda #$01
$C3F4 85 1A: sta FrameCounter
$C3F6 C6 25: dec $25
$C3F8 4C FB C3: jmp +++ ; $C3FB -> _loc_1C365
+++ -- $C3FB 4C 65 C3: jmp _loc_1C365
++++ $C3FE CA: dex
$C3FF D0 17: bne ++ ; $C418
$C401 A2 B0: ldx #$B0
$C403 AD 84 05: lda ObjectBonusType
$C406 C9 01: cmp #$01
$C408 F0 02: beq + ; $C40C
$C40A A2 50: ldx #$50
+ $C40C 8A: txa
$C40D CD 8C 03: cmp ObjectScreenXcoordInt
$C410 F0 E9: beq -- ; $C3FB -> _loc_1C365
$C412 20 58 94: jsr _func_1458 ; CALLS $15458 (BANK 5), SHOULD CALL $19458 (BANK 6)
$C415 4C B7 98: jmp _loc_18B7 ; JUMPS $158B7, SHOULD JUMP TO $198B7 -- EXECUTES GARBAGE CODE -- GAME CRASHES
++ $C418 CA: dex
$C419 D0 27: bne ++ ; $C442
$C41B A5 1A: lda FrameCounter
$C41D 29 07: and #$07
$C41F D0 02: bne + ; $C423
$C421 C6 25: dec $25
+ $C423 A5 25: lda $25
$C425 F0 07: beq + ; $C42E
$C427 18: clc
$C428 69 1C: adc #$1C
$C42A 8D 1F 03: sta $031F
$C42D 60: rts
+ $C42E A9 1D: lda #$1D
$C430 20 A7 C1: jsr PlaySFX
$C433 A9 00: lda #$00
$C435 8D 1F 03: sta $031F
$C438 20 A8 C4: jsr _func_1C4A8
$C43B A9 30: lda #$30
$C43D 85 25: sta $25
$C43F 4C FB C3: jmp -- ; $C3FB -> _loc_1C365
++ $C442 CA: dex
$C443 D0 05: bne + ; $C44A
$C445 C6 25: dec $25
- $C447 F0 B2: beq -- ; $C3FB -> _loc_1C365
$C449 60: rts
+ $C44A CA: dex
$C44B D0 08: bne + ; $C455
$C44D A5 2E: lda NumberofLives
$C44F F0 F6: beq - ; $C447 -> $C3FB
$C451 4C AF C3: jmp --- ; $C3AF
$C454 .byte $60
+ $C455 AD 50 04: lda ObjectUnknown450
$C458 D0 02: bne + ; $C45C
$C45A E6 28: inc CurrentStage
+ $C45C 20 96 A2: jsr _func_2296
$C45F 20 94 C9: jsr _func_1C994
$C462 20 BD CF: jsr _func_1CFBD
$C465 20 62 D1: jsr _func_1D162
$C468 20 C3 C8: jsr _func_1C8C3
_loc_1C46B
$C46B A9 05: lda #$05
$C46D 85 18: sta GameMode
$C46F A9 06: lda #$06
$C471 85 19: sta GameModeAux
$C473 60: rts
;------------------------------------------
So GameLogic_GameRunning_ObjectAI at $E8F0 does something wrong when the amphora is active.
Let's see what:
GameLogic_GameRunning_ObjectAI
$E8F0 A5 5B: lda $5B
$E8F2 F0 0B: beq + ; $E8FF
$E8F4 A5 1A: lda FrameCounter
$E8F6 29 01: and #$01
$E8F8 D0 05: bne + ; $E8FF
$E8FA C6 5B: dec $5B
$E8FC 20 06 FB: jsr _func_1FB06 ; THIS FUNCTION MESSES UP THE BANK NUMBER
+ $E8FF A0 00: ldy #$00
$E901 A5 2B: lda $2B
$E903 F0 02: beq + ; $E907
$E905 A0 20: ldy #$20
+ $E907 84 7C: sty $7C
$E909 20 08 EB: jsr _func_1EB08 ; THIS FUNCTION DOES NOTHING OF IMPORTANCE IN THIS CONTEXT
; Delay loop begin: 6 cycles (2.0 cycles per byte); ends at $1E90F
$E90C EA: nop
$E90D EA: nop
$E90E EA: nop
; End of delay loop (3 bytes)
$E90F 20 FF E3: jsr GameLogic_GameRunning_WhipCollisionTester ; NOTHING IMPORTANT
$E912 4C 22 E9: jmp GameLogic_GameRunning_ObjectAI_loop ; NOTHING IMPORTANT
So what happens in $FB06? Let's go deeper!
_func_1FB06
$FB06 A5 5B: lda $5B ; My guess: $5B = Simon's invincibility counter.
$FB08 A2 14: ldx #$14
$FB0A C9 80: cmp #$80 ; Guess: When count=128, transfers the regular palette.
$FB0C F0 10: beq + ; $FB1E
$FB0E C9 40: cmp #$40
$FB10 D0 F3: bne - ; $FB05 -> rts
$FB12 A9 1C: lda #$1C ; When count=64, transfer the inverse palette for Simon.
$FB14 20 A7 C1: jsr PlaySFX ; And play the sound effect for phasing.
$FB17 A0 05: ldy #$05
$FB19 20 D6 C1: jsr SwitchBank_SaveNewBank ; HERE IS WHERE BANK 5 IS SELECTED
$FB1C A2 15: ldx #$15
+ $FB1E 8A: txa
$FB1F 4C 95 CC: jmp _func_1CC95 ; Reads PPU transfer data from ROM and copies into RAM, returns.
So, the problem here is that when the amphora causes Simon's palette to change, the routine will set a particular ROM bank and will not change it back.
Now the fun fact is that
GameLogic_GameRunning_ObjectAI is not only called from
GameLogic_DoorScrolling. It is also called from
GameLogic_Climbing,
GameLogic_EnterGateSlowly and of course
GameLogic_GameRunning_Body.
In
GameLogic_GameRunning_Body, the amphora bug cannot cause problems, because the routine explicitly switches to bank 6 after calling
GameLogic_GameRunning_ObjectAI.
However, there is potential that it might cause problems if the amphora switches colors while Simon is climbing (probably scrolling-related climbing only). This warrants study.
Now, the wrong code that it executes at $15458 (bank 5) is by chance some quite harmless code.
But $158B7 is where the mayhem happens.
This is the disassembly of code AROUND that region:
$98B3 DE 4C 05: dec ObjectUnknown54C,x
$98B6 F0 03: beq + ; $98BB
$98B8 4C A1 97: jmp _loc_157A1
+ $98BB 20 84 EF: jsr CalculateObjectXdistance
$98BE C9 30: cmp #$30
$98C0 B0 03: bcs + ; $98C5
- $98C2 4C C7 97: jmp _loc_157C7
+ $98C5 29 03: and #$03
$98C7 F0 F9: beq - ; $98C2 -> _loc_157C7
$98C9 A0 10: ldy #$10
$98CB A5 1A: lda FrameCounter
$98CD 29 01: and #$01
$98CF F0 02: beq + ; $98D3
$98D1 A0 28: ldy #$28
+ $98D3 98: tya
$98D4 A0 08: ldy #$08
$98D6 20 34 ED: jsr _func_1ED34
$98D9 4C A1 97: jmp _loc_157A1
However, because it begins executing at $98B7 and NOT $98B6 or $98B8, here's what it ENDS UP executing... Relatively quite harmless.
$98B7 03 4C: slo ($4C,x)
$98B9 A1 97: lda ($97,x)
$98BB 20 84 EF: jsr CalculateObjectXdistance
... and so on
EXCEPT that this "slo ($4C,x)" apparently ends up reprogramming the mapper.
The consecutive instructions starting from $98B9 will actually be read from bank 0.
Here's the disassembly from bank 0:
0098B9 F0 F8 beq $0098B3 <Lbl_000000+6323>
0098BB C3 F8 dcp ($F8,x)
0098BD F1 F6 sbc ($F6),y
0098BF 00 02 brk #$02
0098C1 F0 FC beq $0098BF <Lbl_000000+6335>
0098C3 C3 F8 dcp ($F8,x)
0098C5 F1 FA sbc ($FA),y
0098C7 00 09 brk #$09
0098C9 C0 91 cpy #$91
0098CB 03 F4 slo ($F4,x)
0098CD C1 91 cmp ($91,x)
0098CF FC C1 91 nop $91C1,x
0098D2 04 80 KIL $80
0098D4 E1 93 sbc ($93,x)
0098D6 09 C0 ora #$C0
0098D8 93 03 sha ($03),y
0098DA F4 C1 nop $C1,x
0098DC 93 FC sha ($FC),y
0098DE C1 93 cmp ($93,x)
0098E0 04 80 KIL $80
0098E2 F5 93 sbc $93,x
0098E4 09 C0 ora #$C0
0098E6 95 03 sta $03,x
0098E8 F4 C1 nop $C1,x
0098EA 95 FC sta $FC,x
0098EC C1 95 cmp ($95,x)
0098EE 04 80 KIL $80
0098F0 09 94 ora #$94
0098F2 09 D0 ora #$D0
0098F4 E4 43 cpx $43
0098F6 F8 sed
0098F7 D1 E2 cmp ($E2),y
0098F9 00 D1 brk #$D1
0098FB E0 08 cpx #$08
0098FD F1 E4 sbc ($E4),y
0098FF F8 sed
009900 F1 E6 sbc ($E6),y
009902 00 F1 brk #$F1
009904 E6 08 inc $08
009906 11 EA ora ($EA),y
009908 F8 sed
009909 11 E8 ora ($E8),y
00990B 00 11 brk #$11
00990D E8 inx
00990E 08 php
00990F 0C D0 F0 nop $F0D0
009912 43 F0 sre ($F0,x)
009914 D1 EE cmp ($EE),y
009916 F8 sed
009917 D1 EC cmp ($EC),y
009919 00 D1 brk #$D1
00991B E0 08 cpx #$08
00991D F1 F6 sbc ($F6),y
00991F F0 F1 beq $009912 <Lbl_000000+6418>
009921 F4 F8 nop $F8,x
009923 F1 F2 sbc ($F2),y
009925 00 F1 brk #$F1
009927 E6 08 inc $08
009929 11 FA ora ($FA),y
00992B F0 11 beq $00993E <Lbl_000000+6462>
00992D F4 F8 nop $F8,x
00992F 11 F8 ora ($F8),y
009931 00 11 brk #$11
009933 E6 08 inc $08
009935 01 F0 ora ($F0,x)
009937 FC 41 FC nop $FC41,x
00993A 02 F0 KIL #$F0
The "KIL" instructions here are invalid opcodes that kill the CPU. If the CPU encounters those instructions, it is unrecoverably hung and can only be reset.
Somehow Castlevania skillfully evades the minefield of KIL instructions in that listing until it finally hits $993A. At that point, the game is halted and nothing can be done anymore.
I did not do a complete trace, but that "slo ($4C,x)" appears to be somehow switching to bank 0. X is $B0 at that point, so what it does it reads a "pointer" from RAM address $FC, and does an ASL with the value at that absolute location.
If the value at $FC contains something else, what could happen? With ASL, the only possible outcomes for the value are even values, so we can't consider odd bank numbers.
Well, turns out that nothing much.
In bank 3 (which this method could not reach) there is some code that _might_ yield potential for arbitrary code execution...
0398B7 90 FE bcc $0398B7 <Lbl_000000+55479>
0398B9 E0 C0 cpx #$C0
0398BB C0 C0 cpy #$C0
0398BD A0 C0 ldy #$C0
0398BF A0 00 ldy #$00
0398C1 01 00 ora ($00,x)
0398C3 60 rts
But that's pretty much it.
Nevertheless, I would like to see what happens if the amphora bug is activated in one of those other contexts that I mentioned.