level 3 waterfall dragon crash documentation
This commit is contained in:
parent
46b25696e2
commit
92dce8d051
75
docs/Bugs.md
75
docs/Bugs.md
|
@ -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.
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue