Author Topic: [FF7 PSX] ATB Recommended/Wait Pause Logic  (Read 9850 times)

RoSoDude

  • *
  • Posts: 8
    • View Profile
[FF7 PSX] ATB Recommended/Wait Pause Logic
« on: 2025-02-09 00:23:38 »
I have become rather obsessed with the intricacies of the Active Time Battle system in Final Fantasy games. In particular, I find it very interesting how and when the games decide to pause ATB; FF4, FF5, and FF8 all pause during battle animations, while FF6 and FF9 let ATB run in the background (to rather deleterious effects). FF7 is in the middle; in Active mode it works like FF6/9, while in Recommended/Wait mode it works more like FF4/5/8... but with some notable quirks.

The FF7 manual states the following for ATB Recommended: "For moderately skilled players. Time stops while the screen effects are displayed when using Magic and Items."

Yeah, turns out it's much, much more complicated than this. In an earlier post on the issue, Sega Chief theorized that animation sequences had "hooks" to tell the battle engine to pause ATB, but these hooks were getting missed somehow due to menus. This seemed like a plausible theory since I myself had noticed that ATB pausing was inconsistent; sometimes ATB would pause after the startup phase of a spell command, while sometimes it would keep running. Limit Breaks usually fail to pause ATB, but if ATB was already paused going into a limit break, it would stay paused. I decided to go into the debugger to work it out myself, and see if I could fix any "bugs" in the code. It turns out this theory about animation sequences is totally wrong, and ATB pausing is broken by design rather than by any coding bugs.

The basic conditions for ATB pausing are:

  • ATB pauses during all summon animations (regardless of Active/Recommended/Wait)
  • ATB pauses when Wait mode is selected and the user is in a submenu
  • ATB pauses when Recommended/Wait is selected and the animation timer is 16,385 or greater

The animation timer is the tricky part here. First of all, the increment is determined by the Battle Speed, in particular Speed_mod = (65536/(2*(120+(480*Bat_Spd)/256)). Once each frame (at 60fps), this Speed_mod is added to the timer. For example, if the Battle Speed is 0 (fastest), the increment is 273 and so the timer takes 1 second to reach 16385; if Battle Speed is 128 (default), the increment is 91 and the timer takes 3 seconds; if Battle Speed is 255 (slowest), the increment is 54 and the timer takes 5 seconds. This is why you can sometimes see basic attacks pause ATB on the fastest Battle Speed, but you will never see this on slower settings.

More importantly, the timer only increments under specific conditions, and frequently resets before it can reach [1-5] seconds. Let's break it down further. The animation timer only ticks up if the variable I'm calling the "pause flag" is equal to 0x0003, and the timer is usually reset when the pause flag is changed. Here's how it works (the following applies only to Recommended/Wait mode):

  • When no turns are being executed, the pause flag is set to 0x0000
  • At the start of a command animation (excepting commands that jump the queue like counters and limit breaks), the pause flag is set as follows:
    • If there are no commands waiting on the queue, the pause flag is set to 0x0001 and the timer is reset
    • If there are commands waiting on the queue, the pause flag is set to 0x0003 and the timer is reset
  • If any character has received full ATB, the pause flag is set to pause_flag = pause_flag OR 0x0002, and the timer is not reset. This bitwise OR has the following effects on existing values of the pause flag:
    • pause_flag == 0x0000 becomes pause_flag == 0x0002
    • pause_flag == 0x0001 becomes pause_flag == 0x0003
    • pause_flag == 0x0002 becomes pause_flag == 0x0002
    • pause_flag == 0x0003 becomes pause_flag == 0x0003

Taking it all together, we can explain some common scenarios with ATB (assuming default Battle Speed):
  • Cloud casts Bolt when the queue is empty. The pause flag is set to 0x0001 but doesn't get bumped up to 0x0003 in time, so ATB never pauses during the attack
  • Cloud casts Bolt right before Barret gets full ATB. The pause flag is then set to 0x0003 and ATB pauses 3 seconds into the animation
  • Guard Scorpion queues Tail Laser during Cloud's attack, and then ATB is paused. ATB stays paused during Tail Laser
  • Tail Laser gives Cloud and Barret full limit gauges. Cloud activates Braver, but since limit breaks do not update the pause flag it is stuck at 0x0000. The Guard Scorpion gets full ATB during Braver which updates the pause flag to 0x0002. Barrett activates Big Shot (since limit breaks jump the queue), but the pause flag is now stuck at 0x0002 and ATB never pauses
  • Tail Laser gives Barret a full limit gauge. First, Cloud uses a potion, which sets the pause flag to 0x001. During the potion animation, the Guard Scorpion gets full ATB, updating the pause flag to 0x003. Barret activates Big Shot (since limit breaks jump the queue), and ATB pauses within 3 seconds of the animation
  • Cloud and Barret both queue up attacks at the same time, so there is one extra move waiting on the queue. Barret does a basic attack which sets the pause flag to 0x0003, but the animation is done before 3 seconds elapse so ATB never pauses, and the timer resets
  • There's nothing left on the queue. Cloud casts Bolt which sets the pause flag to 0x0001, and Barrett gets full ATB 3 seconds into the animation which sets the pause flag to 0x0003. However, the animation finishes before 3 more seconds can elapse, so ATB never pauses and the timer resets

We can glean several goals of this ATB design from the implementation. The primary goal is to keep the command queue from getting backlogged; if there are moves waiting, only a little bit of time will leak during the animation. A secondary goal is to maintain the flow of battle in contrast to the strict animation pausing of FF4/5/8; if the queue is empty, time will flow until at least one participant has full ATB. A final goal is to make this all scale with the Battle Speed setting, such that the ATB pauses more often at high Battle Speed but less often at low Battle Speed. However, despite these reasonable goals, the implementation is a bit of a failure in my view, since there are so many conditions that reset (or fail to set) the timer that the ATB pause ends up feeling random and unpredictable, which is not ideal for strategic gameplay.

I will be updating this thread later with commented MIPS disassembly that details the implementation. I already programmed a patch which simplifies the system; I removed the check for the queue position and set the pause flag on all attacks (whether player, enemy, or limit break) to 0x0001 always, and pause ATB as soon as the pause flag is updated to 0x0003 by a character getting full ATB. This puts a greater emphasis on the effect of Dexterity on character speed by pausing more consistently, while still letting ATB flow until the next move is ready to go onto the queue.

I originally thought the pause flag was set to 0x01 if the player and enemy turn counters were equal, but it is actually based on the current and total queue length
« Last Edit: 2025-11-19 21:51:24 by RoSoDude »

leorasy

  • *
  • Posts: 1
    • View Profile
    • smash karts
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #1 on: 2025-06-18 03:59:31 »
Makes it easier to plan — especially useful for beginners or during complex boss fights.


javisanchez1234567

  • *
  • Posts: 36
    • View Profile
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #2 on: 2025-06-26 09:39:41 »
This is very interesting. I'd love a mod for the Steam version that pauses the game on all animations.

The only problem would be that Haste and Slow would be too OP. Is there a way to make the speed +25% and -25% instead of doubling and dividing by two?

RoSoDude

  • *
  • Posts: 8
    • View Profile
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #3 on: 2025-08-02 16:17:06 »
This is very interesting. I'd love a mod for the Steam version that pauses the game on all animations.

The only problem would be that Haste and Slow would be too OP. Is there a way to make the speed +25% and -25% instead of doubling and dividing by two?

In my FF7 ATB hack for the PSX game I revised the system to pause ATB during animations as soon as anyone's turn comes up. This leads to less ATB "leaking" on faster battle speeds, so 2x haste does indeed become overpowered (0.5x slow is still pretty reasonable though). Thus I revised haste to 1.5x speed.

I don't know anything about modifying the PC version, so someone else would have to take a crack at it.

maruusa

  • *
  • Posts: 1
    • View Profile
    • Geometry Dash
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #4 on: 2025-08-07 03:34:56 »
If you perform an attack when no other character has full ATB, the pause flag is simply set to 0x0001. At this point, the timer will not increase, and ATB will continue to run in the background.

RoSoDude

  • *
  • Posts: 8
    • View Profile
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #5 on: 2025-08-13 20:29:06 »
If you perform an attack when no other character has full ATB, the pause flag is simply set to 0x0001. At this point, the timer will not increase, and ATB will continue to run in the background.

That is not true. When setting the pause flag at the start of an animation, the game does not check who has full ATB. Basic attacks can set the pause flag to 0x03 if there are moves waiting on the queue, otherwise it's set to 0x0001. A bitwise OR with 0x0002 is applied to the pause flag when any character reaches full ATB during the current animation.

Some examples in this video on Recommended/Wait mode with maximum Battle Speed: https://www.youtube.com/watch?v=Be655RetuM0&t=170s

  • Barret uses a phoenix down while no one has full ATB (pause flag is set to 0x0001), Tifa gets full ATB (pause flag is set to 0x03), and ATB pauses 1 second later
  • The Dark Dragon starts an attack while Tifa is already at full ATB (which is irrelevant, pause flag is set to 0x0003), and ATB pauses 1 second later
  • 1st Gargoyle starts an attack while all characters have full ATB (pause flag is set to 0x0003), and ATB pauses 1 second later
  • 2nd Gargoyle starts an attack while all characters have full ATB (pause flag is set to 0x0003), and ATB pauses 1 second later

It's really rare for zero characters to have full ATB on the fastest Battle Speed at the start of an animation. Time is always leaking a bit, so you'll usually have 1 or two characters waiting to execute a move, so it's difficult to come up with counterexamples where the pause flag is set to 0x0003 with no characters at full ATB. I know I'm right because I carefully traced the code and studied many different cases during battle. Speaking of which, I should put up my commented disassembly one of these days like I said I would.
« Last Edit: 2025-11-19 22:49:00 by RoSoDude »

RoSoDude

  • *
  • Posts: 8
    • View Profile
Re: [FF7 PSX] ATB Recommended/Wait Pause Logic
« Reply #6 on: Yesterday at 15:30:26 »
As I promised months ago, here's the relevant assembly code.

This code sets the pause flag during each loop of the battle module in Recommended/Wait mode. If no command is being executed, the pause flag is set to 0x00. If the command being executed is from a priority queue (e.g. counters and limit breaks), then we don't change the pause flag. If the command being executed is from the normal command queue, then we set the pause flag to 0x01 if there are no moves waiting on the queue, otherwise we set it to 0x03.
Code: [Select]
000a32c0: 3c02800f lui r2,0x800f ; BEGIN ATB PAUSE FLAG SUBROUTINE
000a32c4: 94427daa lhu r2,0x7daa(r2) ; r2 = $0x800F7DAA (0b01 = 0x01 if ATB Recommended, 0b10 = 0x02 if ATB Wait)
000a32c8: 27bdffe8 addiu r29,r29,0xffe8 ; r29 = r29 - 0x0018
000a32cc: 1040001d beq r2,r0,0x000a3344 ; skip ATB animation pause check if neither Recommended nor Wait
000a32d0: afbf0010 sw r31,0x0010(r29)
000a32d4: 10800012 beq r4,r0,0x000a3320 ; branch if r4 == 0... (no active command)
000a32d8: 34020006 ori r2,r0,0x0006 ; with r2 = 0x06
000a32dc: 3c03800f lui r3,0x800f
000a32e0: 94637dba lhu r3,0x7dba(r3) ; r3 = $0x800F7DBA (which queue are we resolving, counters and limits have priority)
000a32e4: 00000000 nop
000a32e8: 14620016 bne r3,r2,0x000a3344 ; branch if r3 != 0x06 (0x02 counterattack queue, 0x05 limit break queue, otherwise 0x06 for normal moves)
000a32ec: 00000000 nop
000a32f0: 3c03800f lui r3,0x800f
000a32f4: 90636b9a lbu r3,0x6b9a(r3) ; r3 = $0x800F6B9A (current queue position)
000a32f8: 3c02800f lui r2,0x800f
000a32fc: 90426ba1 lbu r2,0x6ba1(r2) ; r2 = $0x800F6BA1 (total queue length)
000a3300: 00000000 nop
000a3304: 10620002 beq r3,r2,0x000a3310 ; branch if r3 == r2...
000a3308: 34070001 ori r7,r0,0x0001 ; ...with r7 = 0x01     (if no moves waiting on queue, pause flag is 0x01)
000a330c: 34070003 ori r7,r0,0x0003 ; otherwise r7 = 0x03   (if moves waiting on queue, pause flag is 0x03)
000a3310: 00002021 addu r4,r0,r0 ; r4 = 0
000a3314: 00002821 addu r5,r0,r0 ; r5 = 0
000a3318: 08028ccf j 0x000a333c ; jump to set pause flag...
000a331c: 34060007 ori r6,r0,0x0007 ; ...with r6 = 0x07
000a3320: 0c028dfe jal 0x000a37f8 ; call to check pause flag...
000a3324: 2404ffff addiu r4,r0,0xffff ; ...with r4 = 0xFFFF (null)
000a3328: 10400006 beq r2,r0,0x000a3344 ; branch if r2 == 0 (zeroed pause flag)
000a332c: 00002021 addu r4,r0,r0
000a3330: 00002821 addu r5,r0,r0
000a3334: 34060007 ori r6,r0,0x0007
000a3338: 00003821 addu r7,r0,r0
000a333c: 0c029c95 jal 0x000a7254 ; call to set pause flag
000a3340: 00000000 nop
000a3344: 8fbf0010 lw r31,0x0010(r29)
000a3348: 27bd0018 addiu r29,r29,0x0018
000a334c: 03e00008 jr r31
000a3350: 00000000 nop

Here's the code that updates the pause flag whenever anyone gets full ATB, as part of the ATB increment subroutine.
Code: [Select]
000a3bac: 96620008 lhu r2,0x0008(r19) ; r2 = $(r19+0x0008)
000a3bb0: 3c03800f lui r3,0x800f
000a3bb4: 8c6339e0 lw r3,0x39e0(r3) ; r3 = $0x800F39E0 (pause flag)
000a3bb8: 3042fffe andi r2,r2,0xfffe ; r2 = r2 AND 0xFFFE i.e. 0b1111111111111110
000a3bbc: 34630002 ori r3,r3,0x0002 ; r3 = r3 OR 0x0002 (this converts 0x01 into 0x03, so animation timer starts incrementing!)
000a3bc0: a6620008 sh r2,0x0008(r19) ; store r2 at $(r19+0x0008)
000a3bc4: 3c01800f lui r1,0x800f
000a3bc8: ac2339e0 sw r3,0x39e0(r1) ; store r3 at $0x800F39E0 (pause flag, increments timer if equal to 0x03)

Here's the code that checks for whether to pause ATB on each frame. First, we check if the pause flag is 0x03, and if so we add Speed_mod to the animation timer. We pause if there's a submenu open in Wait mode, if there's a summon animation playing, the animation timer is greater than 0x4000 = 16385, plus an unknown condition that I haven't figured out (I believe it may be used to pause ATB when dialogue is appearing during battle?)
Code: [Select]
000a3828: 27bdffe8 addiu r29,r29,0xffe8 ; BEGIN ATB PAUSE CHECK SUBROUTINE
000a382c: afb00010 sw r16,0x0010(r29)
000a3830: 00008021 addu r16,r0,r0
000a3834: 3c03800f lui r3,0x800f
000a3838: 8c6339e0 lw r3,0x39e0(r3) ; r3 = $0x800F39E0 (pause flag)
000a383c: 34020003 ori r2,r0,0x0003 ; r2 = 0x0003
000a3840: 14620009 bne r3,r2,0x000a3868 ; branch if r3 != 0x0003, i.e. skip animation timer increment if pause flag is not equal to 0x0003
000a3844: afbf0014 sw r31,0x0014(r29)
000a3848: 3c03800f lui r3,0x800f
000a384c: 94637da6 lhu r3,0x7da6(r3) ; r3 = $0x800F7DA6 (Speed modifier: 273 on fastest Battle Speed, 91 on default, 54 on slowest)
000a3850: 3c02800f lui r2,0x800f
000a3854: 8c4239e4 lw r2,0x39e4(r2) ; r2 = $0x800F39E4 (animation timer)
000a3858: 00000000 nop
000a385c: 00431021 addu r2,r2,r3 ; r2 = r2 + r3
000a3860: 3c01800f lui r1,0x800f
000a3864: ac2239e4 sw r2,0x39e4(r1) ; store animation timer in $0x800F39E4, (pauses ATB if greater than 0x4001)000a3868: 3c03800f lui r3,0x800f ; BEGIN ATB PAUSE CHECK SUBROUTINE
000a386c: 94637daa lhu r3,0x7daa(r3) ; r3 = (r3+0x7DAA) = $0x800F7DAA (0b01 = 0x01 if ATB Recommended, 0b10 = 0x02 if ATB Wait)
000a3870: 34020002 ori r2,r0,0x0002 ; r2 = 0x02
000a3874: 1462000e bne r3,r2,0x000a38b0 ; skip menu check if r3 != r2, i.e. not ATB Wait
000a3878: 00000000 nop
000a387c: 3c03800f lui r3,0x800f ; r3 = 0x800F0000
000a3880: 84633896 lh r3,0x3896(r3) ; r3 = $(r3+0x3896) = $0x800F3896 (menu halfword)
000a3884: 00000000 nop
000a3888: 2c62001c sltiu r2,r3,0x001c ; r2 = r3 < 0x001C
000a388c: 10400008 beq r2,r0,0x000a38b0 ; branch if r2 == 0 (r2 >= 0x001C)...
000a3890: 00031080 sll r2,r3,0x02 ; ...with r2 = r3 >> 0x02
000a3894: 3c01800a lui r1,0x800a ; r1 = 0x800A0000
000a3898: 00220821 addu r1,r1,r2 ; r1 = r1 + r2
000a389c: 8c2201b0 lw r2,0x01b0(r1) ; r2 = $(r1+0x01B0)
000a38a0: 00000000 nop
000a38a4: 00400008 jr r2 ; jump to $r2 (e.g. 0x800A38AC for magic menu)
000a38a8: 00000000 nop
000a38ac: 34100001 ori r16,r0,0x0001 ; r16 = 0x0001 (set true if we got a pause from ATB Wait)
000a38b0: 0c0292a0 jal 0x000a4a80 ; check for summon byte at $0x800FAFDC
000a38b4: 00000000 nop
000a38b8: 02028025 or r16,r16,r2 ; r16 = r16 OR r2 (set true if we got a pause from ATB Wait OR summon)
000a38bc: 3c038010 lui r3,0x8010
000a38c0: 946383d0 lhu r3,-0x7c30(r3) ; r3 = $0x800F83D0
000a38c4: 3c02800f lui r2,0x800f
000a38c8: 8c4239e4 lw r2,0x39e4(r2) ; r2 = $0x800F39E4
000a38cc: 30630003 andi r3,r3,0x0003 ; r3 = r3 AND 0x0003
000a38d0: 0003182b sltu r3,r0,r3 ; r3 = ($0x800F83D0 AND 0x0003) != 0
000a38d4: 28424001 slti r2,r2,0x4001 ; r2 = $0x800F39E4 < 0x4001
000a38d8: 14400002 bne r2,r0,0x000a38e4 ; branch if r2 != 0, i.e. $800F39E4 < 0x4001...
000a38dc: 02038025 or r16,r16,r3 ; ...with r16 = r16 OR r3 (set true if we got a pause from ATB Wait OR summon OR animation pause)
000a38e0: 34100001 ori r16,r0,0x0001 ; r16 = 0x0001
000a38e4: 02001021 addu r2,r16,r0 ; r2 = r16
000a38e8: 8fbf0014 lw r31,0x0014(r29)
000a38ec: 8fb00010 lw r16,0x0010(r29) ; set true for pause from ATB Wait, summon, $0x800F83D0 AND 0x0003 != 0 (???), or animation timer is greater than 0x4001 = 16385
000a38f0: 27bd0018 addiu r29,r29,0x0018
000a38f4: 03e00008 jr r31 ; r31 = 0x800a3950
000a38f8: 00000000 nop

Here's the calculation for Speed_mod, which is used in ATB calculations as well as the animation timer increment each frame.
Code: [Select]
001b085c: 00041100 sll r2,r4,0x04 ; r2 = r4 << 0x04 (mult by 16) BEGIN SUBROUTINE: CALC SPEED MULTIPLIER FROM BATTLE SPEED
001b0860: 00441023 subu r2,r2,r4 ; r2 = r2 - r4 (subtract val, so overall mult 15)
001b0864: 00021140 sll r2,r2,0x05 ; r2 = r2 << 0x05 (mult by 32, so overall mult 480)
001b0868: 04410003 bgez r2,0x001b0878 ; branch if r2 >= 0...
001b086c: 00021a03 sra r3,r2,0x08 ; ...with r3 = r2 >> 0x08 (divide by 256)
001b0870: 244200ff addiu r2,r2,0x00ff ; r2 = r2 + 0x00FF (add 255, failsafe for negative number rounding which is irrelevant)
001b0874: 00021a03 sra r3,r2,0x08 ; r3 = r2 >> 0x08 (divide by 256)
001b0878: 24630078 addiu r3,r3,0x0078 ; r3 = r3 + 0x0078 (add 120)
001b087c: 00031840 sll r3,r3,0x01 ; r3 = r3 << 0x0001 (mult by 2)
001b0880: 3c020001 lui r2,0x0001 ; r2 = 0x00010000
001b0884: 0043001a div r2,r3 ; div r2/r3
001b0888: 14600002 bne r3,r0,0x001b0894 ; branch if not divide by 0
001b088c: 00000000 nop
001b0890: 0007000d break 0x00001c00
001b0894: 2401ffff addiu r1,r0,0xffff ; r1 = 0xFFFF
001b0898: 14610004 bne r3,r1,0x001b08ac ; branch if not divide by 0xFFFF
001b089c: 3c018000 lui r1,0x8000
001b08a0: 14410002 bne r2,r1,0x001b08ac ; branch if not divide from 0x80000000 (negative 32-bit int limit, -2147483648)
001b08a4: 00000000 nop
001b08a8: 0006000d break 0x00001800
001b08ac: 00001012 mflo r2 ; r2 = result of divide             (65536/(2*(120+(480*val)/256)) = 32768/(120+(15/8)*val)
001b08b0: 3c01800f lui r1,0x800f
001b08b4: a4227da6 sh r2,0x7da6(r1) ; store r2 at 0x800F7DA6
001b08b8: 03e00008 jr r31
001b08bc: 00000000 nop
« Last Edit: Yesterday at 16:41:16 by RoSoDude »