level 3 waterfall dragon crash documentation

This commit is contained in:
Michael Miceli 2023-11-19 20:30:42 -05:00
parent 46b25696e2
commit 92dce8d051
3 changed files with 105 additions and 29 deletions

View File

@ -150,4 +150,77 @@ boss_gemini_routine_02:
@calc_offset_set_pos: @calc_offset_set_pos:
... ...
``` ```
# 4. Dragon Crash
On 2020-09-08, user [aiqiyou](https://tasvideos.org/Users/Profile/aiqiyou) had
[posted](https://tasvideos.org/Forum/Topics/485?CurrentPage=7&Highlight=499433#499433)
a .fm2 file [glitch.fm2](https://tasvideos.org/userfiles/info/65950171267733022)
on the TASVideos forums. This showed a 2 player play through where the game
freezes on the level 3 - waterfall dragon boss. The next day
[feos](https://tasvideos.org/Users/Profile/feos) posted a
[video of the run](https://www.youtube.com/watch?v=4ffhI2J2dA8) on YouTube for
easier viewing. [Sand](https://tasvideos.org/Users/Profile/Sand) did some
initial investigation as to the cause of the freeze and noticed the game was in
a forever loop inside the `@enemy_orb_loop` code and it was looping forever due
to an invalid doubly linked list (`ENEMY_VAR_3` and `ENEMY_VAR_4` values).
The reason this freeze happens is due to a race condition where the left 'hand'
(red dragon arm orb) is destroyed, but before the next frame happens where the
'orb destroyed' routine is executed, another orb on the left arm changes the
routine of the left 'hand' to be a different routine. Since the expected
'orb destroyed' routine wasn't run, the rest of the arm didn't get the notice to
self-destruct. Then, a few frames later, the left shoulder creates a
projectile, which takes over the same slot where the left 'hand' was. Finally,
one frame later, when the left shoulder tries to animate the arm, the left
'hand' not having correct data (because it is now a projectile), causes the game
to get stuck in an infinite loop.
## Detailed Explanation
Below is a diagram of the dragon boss and its arm orbs. Each number below is
the enemy slot, i.e. the enemy number. #$06 and #$05 are the left and right
'hands' respectively, and are red. #$0d and #$0a are the left and right
'shoulders' respectively. () represents the dragon's mouth and is uninvolved in
this bug. In fact, only the left arm is involved in this bug.
```
06 08 0c 0f 0d () 0a 0e 0b 07 05
```
1. Frame #$aa - Enemy #$06 (the left 'hand') is destroyed, the memory address
specifying which routine to execute is updated to point to
`dragon_arm_orb_routine_04`.
2. Frame #$ab - Enemy #$0f has a timer elapse in `dragon_arm_orb_routine_02`.
Enemy #$0d updates the enemy routine for all orbs on the left arm. It does
this by incrementing a pointer. Usually, this updates the routine from
`dragon_arm_orb_routine_02` to `dragon_arm_orb_routine_03`. However, since
arm orb #$06 (the left 'hand') was no longer pointing to
`dragon_arm_orb_routine_02`, but instead to `dragon_arm_orb_routine_04`,
incrementing this pointer, set #$06's routine to
`enemy_routine_init_explosion`.
3. Frames #$ac-#$d1 - The animation for the left 'hand' explosion completes and
the 'hand' is removed from memory (`enemy_routine_remove_enemy`)
4. Frame #$d2 - The #$0d (left shoulder) decides that it should create a
projectile. The game logic finds an empty enemy slot where the left 'hand'
originally was (slot #$06). A bullet is created and initialized. This
initialization clears the data that linked the hand to the rest of the arm, in
particular `ENEMY_VAR_3` and `ENEMY_VAR_4`.
5. Frame #$d3 - When #$0d (left shoulder) executes, it animates the rest of the
orbs to make an attack pattern. It loops down to the hand by following the
links among the orbs. When it gets to the hand, it expects that the hand's
will have its `ENEMY_VAR_3` set to `#$ff` indicating there aren't any more
orbs to process. However, since the enemy at slot #$06 is no longer a hand,
but instead a projectile, the value at `ENEMY_VAR_3` has been cleared and is
#$00. This causes the logic to get stuck in `@arm_orb_loop` as an infinite
loop.
Step (2) caused `dragon_arm_orb_routine_04` to be skipped. Since this routine
was not executed as expected, the rest of the arm didn't get updated to know
that the 'hand' was destroyed. `dragon_arm_orb_routine_04` is responsible for
updating each orb on the arm to be begin its self destruct routine. However,
that never happens. So, the shoulder doesn't know to destroy itself. Instead
the shoulder operates as if it wasn't destroyed and when it decides that a
projectile should be created, that overwrites the hand with a different enemy
type, and clears all the links between the hand and the arm.

View File

@ -161,7 +161,7 @@ weapon_item_routine_00:
sta ENEMY_VAR_4,x sta ENEMY_VAR_4,x
lda #$fd lda #$fd
sta ENEMY_VAR_B,x sta ENEMY_VAR_B,x
jmp advance_enemy_routine ; advance to next routine jmp advance_enemy_routine ; advance enemy x to next routine
@set_velocity_outdoor: @set_velocity_outdoor:
ldy #$00 ; set weapon_item_init_vel_tbl to first set of entries ldy #$00 ; set weapon_item_init_vel_tbl to first set of entries
@ -4722,7 +4722,7 @@ dragon_arm_orb_routine_ptr_tbl:
.addr dragon_arm_orb_routine_01 ; CPU address $9ac5 .addr dragon_arm_orb_routine_01 ; CPU address $9ac5
.addr dragon_arm_orb_routine_02 ; CPU address $9b63 - dragon arms extending outward animation .addr dragon_arm_orb_routine_02 ; CPU address $9b63 - dragon arms extending outward animation
.addr dragon_arm_orb_routine_03 ; CPU address $9c03 - dragon arms attack patterns, only executes code for shoulder orbs .addr dragon_arm_orb_routine_03 ; CPU address $9c03 - dragon arms attack patterns, only executes code for shoulder orbs
.addr dragon_arm_orb_routine_04 ; CPU address $9edd .addr dragon_arm_orb_routine_04 ; CPU address $9edd - dragon arm orb destroyed routine
.addr enemy_routine_init_explosion ; CPU address $e74b from bank 7 .addr enemy_routine_init_explosion ; CPU address $e74b from bank 7
.addr enemy_routine_explosion ; CPU address $e7b0 from bank 7 .addr enemy_routine_explosion ; CPU address $e7b0 from bank 7
.addr enemy_routine_remove_enemy ; CPU address $e806 from bank 7 .addr enemy_routine_remove_enemy ; CPU address $e806 from bank 7
@ -4860,23 +4860,24 @@ dragon_arm_orb_routine_02:
bcc @exit2 bcc @exit2
lda #$ff ; a = #$ff lda #$ff ; a = #$ff
sta ENEMY_VAR_2,x sta ENEMY_VAR_2,x
ldy ENEMY_VAR_4,x ldy ENEMY_VAR_4,x ; load parent orb in y
lda #$01 ; a = #$01 lda #$01 ; a = #$01
sta ENEMY_VAR_2,y sta ENEMY_VAR_2,y
lda #$00 ; a = #$00 lda #$00 ; a = #$00
sta ENEMY_ANIMATION_DELAY,y sta ENEMY_ANIMATION_DELAY,y ; set animation delay for parent orb
lda ENEMY_VAR_4,y lda ENEMY_VAR_4,y ; load parent orb of parent orb
bpl @set_enemy_slot_exit bpl @set_enemy_slot_exit ; restore x to current enemy slot and exit
tya tya
tax tax
; arms fully extended, advance orb routines
@adv_routine_exit: @adv_routine_exit:
jsr advance_enemy_routine jsr advance_enemy_routine ; advance arm orb routine in slot x
lda #$00 ; a = #$00 lda #$00 ; a = #$00
sta ENEMY_VAR_2,x sta ENEMY_VAR_2,x
lda ENEMY_VAR_3,x lda ENEMY_VAR_3,x ; load child arm orb
tax tax
bpl @adv_routine_exit bpl @adv_routine_exit ; loop to advance arm orb routine of child
ldx ENEMY_CURRENT_SLOT ldx ENEMY_CURRENT_SLOT
lda #$00 ; a = #$00 lda #$00 ; a = #$00
sta ENEMY_FRAME,x ; set enemy animation frame number sta ENEMY_FRAME,x ; set enemy animation frame number
@ -5240,6 +5241,7 @@ dragon_arm_animate:
; only used for ENEMY_FRAME = #$01 ; only used for ENEMY_FRAME = #$01
rts rts
; dec animation timer and run @timer_logic
@check_delay_run_timer: @check_delay_run_timer:
lda ENEMY_ANIMATION_DELAY,x ; load enemy animation frame delay counter lda ENEMY_ANIMATION_DELAY,x ; load enemy animation frame delay counter
beq @timer_elapsed ; continue once animation timer has elapsed beq @timer_elapsed ; continue once animation timer has elapsed
@ -5253,7 +5255,7 @@ dragon_arm_animate:
bmi @negative_rotation_adjustment ; branch if dragon arm is rotating counterclockwise bmi @negative_rotation_adjustment ; branch if dragon arm is rotating counterclockwise
dec ENEMY_VAR_2,x ; rotating clockwise, decrement dragon arm rotation timer dec ENEMY_VAR_2,x ; rotating clockwise, decrement dragon arm rotation timer
lda #$01 ; a = #$01 lda #$01 ; a = #$01
bne @timer_logic bne @timer_logic ; always branch
@negative_rotation_adjustment: @negative_rotation_adjustment:
inc ENEMY_VAR_2,x inc ENEMY_VAR_2,x
@ -5393,23 +5395,24 @@ dragon_arm_orb_pos_tbl:
.byte $f1,$f1,$f1,$f1,$f2,$f2,$f3,$f4,$f5,$f6,$f8,$f9,$fa,$fc,$fd,$ff .byte $f1,$f1,$f1,$f1,$f2,$f2,$f3,$f4,$f5,$f6,$f8,$f9,$fa,$fc,$fd,$ff
.byte $00,$01,$03,$04,$06,$07,$08,$0a,$0b,$0c,$0d,$0e,$0e,$0f,$0f,$0f .byte $00,$01,$03,$04,$06,$07,$08,$0a,$0b,$0c,$0d,$0e,$0e,$0f,$0f,$0f
; dragon arm orb destroyed routine -
dragon_arm_orb_routine_04: dragon_arm_orb_routine_04:
lda ENEMY_VAR_3,x lda ENEMY_VAR_3,x ; load the child orb for current orb (farther from dragon)
bpl @adv_routine bpl @adv_routine ; if not the hand, then advance routine to show explosions
inc BOSS_SCREEN_ENEMIES_DESTROYED ; increase number of dragon arms destroyed inc BOSS_SCREEN_ENEMIES_DESTROYED ; current orb is the hand, increase number of dragon arms destroyed
@destroy_arm_part_loop: @destroy_arm_part_loop:
lda ENEMY_VAR_4,x lda ENEMY_VAR_4,x ; load the parent orb
bmi @set_slot_adv_routine bmi @set_slot_adv_routine ; branch if shoulder to exit, destroyed all orbs in arb
tax tax ; transfer parent orb index to x
jsr set_destroyed_enemy_routine ; update enemy's routine to the destroyed routine jsr set_destroyed_enemy_routine ; update enemy's routine to the destroyed routine (enemy_routine_init_explosion)
jmp @destroy_arm_part_loop jmp @destroy_arm_part_loop ; loop to update parent orb to the destroyed routine
@set_slot_adv_routine: @set_slot_adv_routine:
ldx ENEMY_CURRENT_SLOT ldx ENEMY_CURRENT_SLOT ; restore x to current enemy slot
@adv_routine: @adv_routine:
jmp advance_enemy_routine jmp advance_enemy_routine ; all arm orbs set to run explosions, advance routine to explode hand as well
; pointer table for boss gemini (#$7 * #$2 = #$e bytes) ; pointer table for boss gemini (#$7 * #$2 = #$e bytes)
boss_gemini_routine_ptr_tbl: boss_gemini_routine_ptr_tbl:
@ -7731,7 +7734,7 @@ boss_giant_soldier_routine_06:
sta $09 ; set relative x offset to #$00 sta $09 ; set relative x offset to #$00
jsr create_giant_boss_explosion ; create explosion at center of enemy jsr create_giant_boss_explosion ; create explosion at center of enemy
; $09 - relative x offset, $08 - relative y offset ; $09 - relative x offset, $08 - relative y offset
jmp advance_enemy_routine jmp advance_enemy_routine ; advance enemy in slot x to next routine
; create explosion animations ; create explosion animations
boss_giant_soldier_routine_07: boss_giant_soldier_routine_07:
@ -7935,7 +7938,7 @@ boss_giant_projectile_routine_00:
sta ENEMY_Y_VELOCITY_FRACT,x ; set spiked disk fractional y velocity to #$00 sta ENEMY_Y_VELOCITY_FRACT,x ; set spiked disk fractional y velocity to #$00
boss_giant_projectile_adv_routine: boss_giant_projectile_adv_routine:
jmp advance_enemy_routine jmp advance_enemy_routine ; advance spiked disk to next routine, boss_giant_projectile_routine_01 or remove_enemy
; update position, if animation delay has elapsed, update sprite so the disk rotates ; update position, if animation delay has elapsed, update sprite so the disk rotates
; remove enemy if off screen ; remove enemy if off screen

View File

@ -300,9 +300,9 @@ nmi_start:
pha ; push A on to the stack pha ; push A on to the stack
lda PPUSTATUS ; reset PPU latch lda PPUSTATUS ; reset PPU latch
ldy NMI_CHECK ; see if nmi interrupted previous frame's game loop ldy NMI_CHECK ; see if nmi interrupted previous frame's game loop
bne handle_sounds_set_ppu_scroll_rti ; branch if nmi occurred before game loop was completed bne handle_sounds_set_ppu_scroll_rti ; branch if nmi occurred before game loop was completed to skip game loop
; to skip game loop and instead just check for sounds to play and play them ; instead just continue playing sounds, set ppu scroll, and rti
; set ppu scroll, and rti ; previously unfinished frame will finish after rti and then itself rti
jsr clear_ppu ; first frame, so clear/init PPU jsr clear_ppu ; first frame, so clear/init PPU
sta OAMADDR ; set OAM address to #00 (DMA is used instead) sta OAMADDR ; set OAM address to #00 (DMA is used instead)
ldy #>OAMDMA_CPU_BUFFER ; setting OAMDMA to #$02 tells PPU to load sprite data from $0200-$02ff ldy #>OAMDMA_CPU_BUFFER ; setting OAMDMA to #$02 tells PPU to load sprite data from $0200-$02ff
@ -335,7 +335,7 @@ nmi_start:
jsr draw_sprites ; bank 1 jsr draw_sprites ; bank 1
jsr write_0_to_cpu_graphics_buffer jsr write_0_to_cpu_graphics_buffer
lda #$00 lda #$00
sta NMI_CHECK sta NMI_CHECK ; successfully rendered full frame before NMI, mark flag appropriately
remove_registers_from_stack_and_rti: remove_registers_from_stack_and_rti:
pla ; remove byte from stack pla ; remove byte from stack
@ -7003,7 +7003,7 @@ bullet_collision_logic:
lda ENEMY_HP,y ; load enemy hp lda ENEMY_HP,y ; load enemy hp
beq @exit ; exit if enemy HP already #$00 beq @exit ; exit if enemy HP already #$00
cmp #$f0 cmp #$f0
bcs @exit ; exit if enemy HP is negative bcs @exit ; exit if enemy HP is between #$f0 and #$ff
sbc #$00 ; subtract #$01 from enemy HP (carry is clear) sbc #$00 ; subtract #$01 from enemy HP (carry is clear)
bcs @continue bcs @continue
lda #$00 ; a = #$00 lda #$00 ; a = #$00
@ -9712,7 +9712,7 @@ mortar_shot_routine_02:
@advance_enemy_routine: @advance_enemy_routine:
ldx ENEMY_CURRENT_SLOT ; restore enemy slot ldx ENEMY_CURRENT_SLOT ; restore enemy slot
jmp advance_enemy_routine ; advance to next routine jmp advance_enemy_routine ; advance enemy x to next routine
; determines firing direction based on enemy position ($08, $09) and player position ($0b, $0a) ; determines firing direction based on enemy position ($08, $09) and player position ($0b, $0a)
; and creates bullet if appropriate ; and creates bullet if appropriate