diff --git a/docs/Bugs.md b/docs/Bugs.md index fe4dcfc..ed41b4e 100644 --- a/docs/Bugs.md +++ b/docs/Bugs.md @@ -150,4 +150,77 @@ boss_gemini_routine_02: @calc_offset_set_pos: ... -``` \ No newline at end of file +``` + +# 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. \ No newline at end of file diff --git a/src/bank0.asm b/src/bank0.asm index 7022753..e103374 100644 --- a/src/bank0.asm +++ b/src/bank0.asm @@ -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 diff --git a/src/bank7.asm b/src/bank7.asm index eafd3f1..f0d3f84 100644 --- a/src/bank7.asm +++ b/src/bank7.asm @@ -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