level 3 waterfall dragon crash documentation
This commit is contained in:
parent
46b25696e2
commit
92dce8d051
73
docs/Bugs.md
73
docs/Bugs.md
|
@ -151,3 +151,76 @@ boss_gemini_routine_02:
|
|||
@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.
|
|
@ -161,7 +161,7 @@ weapon_item_routine_00:
|
|||
sta ENEMY_VAR_4,x
|
||||
lda #$fd
|
||||
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:
|
||||
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_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_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_explosion ; CPU address $e7b0 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
|
||||
lda #$ff ; a = #$ff
|
||||
sta ENEMY_VAR_2,x
|
||||
ldy ENEMY_VAR_4,x
|
||||
ldy ENEMY_VAR_4,x ; load parent orb in y
|
||||
lda #$01 ; a = #$01
|
||||
sta ENEMY_VAR_2,y
|
||||
lda #$00 ; a = #$00
|
||||
sta ENEMY_ANIMATION_DELAY,y
|
||||
lda ENEMY_VAR_4,y
|
||||
bpl @set_enemy_slot_exit
|
||||
sta ENEMY_ANIMATION_DELAY,y ; set animation delay for parent orb
|
||||
lda ENEMY_VAR_4,y ; load parent orb of parent orb
|
||||
bpl @set_enemy_slot_exit ; restore x to current enemy slot and exit
|
||||
tya
|
||||
tax
|
||||
|
||||
; arms fully extended, advance orb routines
|
||||
@adv_routine_exit:
|
||||
jsr advance_enemy_routine
|
||||
jsr advance_enemy_routine ; advance arm orb routine in slot x
|
||||
lda #$00 ; a = #$00
|
||||
sta ENEMY_VAR_2,x
|
||||
lda ENEMY_VAR_3,x
|
||||
lda ENEMY_VAR_3,x ; load child arm orb
|
||||
tax
|
||||
bpl @adv_routine_exit
|
||||
bpl @adv_routine_exit ; loop to advance arm orb routine of child
|
||||
ldx ENEMY_CURRENT_SLOT
|
||||
lda #$00 ; a = #$00
|
||||
sta ENEMY_FRAME,x ; set enemy animation frame number
|
||||
|
@ -5240,6 +5241,7 @@ dragon_arm_animate:
|
|||
; only used for ENEMY_FRAME = #$01
|
||||
rts
|
||||
|
||||
; dec animation timer and run @timer_logic
|
||||
@check_delay_run_timer:
|
||||
lda ENEMY_ANIMATION_DELAY,x ; load enemy animation frame delay counter
|
||||
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
|
||||
dec ENEMY_VAR_2,x ; rotating clockwise, decrement dragon arm rotation timer
|
||||
lda #$01 ; a = #$01
|
||||
bne @timer_logic
|
||||
bne @timer_logic ; always branch
|
||||
|
||||
@negative_rotation_adjustment:
|
||||
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 $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:
|
||||
lda ENEMY_VAR_3,x
|
||||
bpl @adv_routine
|
||||
inc BOSS_SCREEN_ENEMIES_DESTROYED ; increase number of dragon arms destroyed
|
||||
lda ENEMY_VAR_3,x ; load the child orb for current orb (farther from dragon)
|
||||
bpl @adv_routine ; if not the hand, then advance routine to show explosions
|
||||
inc BOSS_SCREEN_ENEMIES_DESTROYED ; current orb is the hand, increase number of dragon arms destroyed
|
||||
|
||||
@destroy_arm_part_loop:
|
||||
lda ENEMY_VAR_4,x
|
||||
bmi @set_slot_adv_routine
|
||||
tax
|
||||
jsr set_destroyed_enemy_routine ; update enemy's routine to the destroyed routine
|
||||
jmp @destroy_arm_part_loop
|
||||
lda ENEMY_VAR_4,x ; load the parent orb
|
||||
bmi @set_slot_adv_routine ; branch if shoulder to exit, destroyed all orbs in arb
|
||||
tax ; transfer parent orb index to x
|
||||
jsr set_destroyed_enemy_routine ; update enemy's routine to the destroyed routine (enemy_routine_init_explosion)
|
||||
jmp @destroy_arm_part_loop ; loop to update parent orb to the destroyed routine
|
||||
|
||||
@set_slot_adv_routine:
|
||||
ldx ENEMY_CURRENT_SLOT
|
||||
ldx ENEMY_CURRENT_SLOT ; restore x to current enemy slot
|
||||
|
||||
@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)
|
||||
boss_gemini_routine_ptr_tbl:
|
||||
|
@ -7731,7 +7734,7 @@ boss_giant_soldier_routine_06:
|
|||
sta $09 ; set relative x offset to #$00
|
||||
jsr create_giant_boss_explosion ; create explosion at center of enemy
|
||||
; $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
|
||||
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
|
||||
|
||||
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
|
||||
; remove enemy if off screen
|
||||
|
|
|
@ -300,9 +300,9 @@ nmi_start:
|
|||
pha ; push A on to the stack
|
||||
lda PPUSTATUS ; reset PPU latch
|
||||
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
|
||||
; to skip game loop and instead just check for sounds to play and play them
|
||||
; set ppu scroll, and rti
|
||||
bne handle_sounds_set_ppu_scroll_rti ; branch if nmi occurred before game loop was completed to skip game loop
|
||||
; instead just continue playing sounds, 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
|
||||
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
|
||||
|
@ -335,7 +335,7 @@ nmi_start:
|
|||
jsr draw_sprites ; bank 1
|
||||
jsr write_0_to_cpu_graphics_buffer
|
||||
lda #$00
|
||||
sta NMI_CHECK
|
||||
sta NMI_CHECK ; successfully rendered full frame before NMI, mark flag appropriately
|
||||
|
||||
remove_registers_from_stack_and_rti:
|
||||
pla ; remove byte from stack
|
||||
|
@ -7003,7 +7003,7 @@ bullet_collision_logic:
|
|||
lda ENEMY_HP,y ; load enemy hp
|
||||
beq @exit ; exit if enemy HP already #$00
|
||||
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)
|
||||
bcs @continue
|
||||
lda #$00 ; a = #$00
|
||||
|
@ -9712,7 +9712,7 @@ mortar_shot_routine_02:
|
|||
|
||||
@advance_enemy_routine:
|
||||
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)
|
||||
; and creates bullet if appropriate
|
||||
|
|
Loading…
Reference in New Issue