FF7 PC Input Repeat Feature, and the DI bug fix

Table of Contents

This is currently a work in progress, expect edits and refinements. Currently (kinda) a first draft, in need of simplifying as much as possible, perhaps some rearranging and certainly cutting down on repetition.

TODO

The bugfix can be found here:
https://github.com/potus-barret/ff7-pc-di-fix/releases/latest/

Fun fact: chocobo racing game, specifically the betting screen, has its own double input like bug, that was correctly ported from PSX.

Intro


Just putting this here for no apparent reason.

Any and all questions/feedback is welcomed and appreciated.

Corrections, clarifications and additions will be made thanks to them so please do fire away. If something isnt clear its likely a failure on my part.

This was put together with bad notes, 4+ month old memory and re reversing the relevant parts. If I seem vague or uncertain at points, it is because of bad notes, not wanting to 100% trust my memory and not having the time to re read a metric s tone of assembly :D. Though I should patch up uncertainties as time allows.

Because the scope of knowledge of the games code and computer science concepts varies wildly from individual to individual in the speed running community and modding/developing community, (the two audiences I expect to be reading this,) I'll be doing my best to write in a way that caters to people with low and high levels of knowledge at the same time. I am not an expert but believe I can do decent job at that. Sorry if it gets annoying.

This explanation of the input repeat feature will cover:

  • Where it keeps track of timings and flags with regards to this input repeat feature.
  • What can and cant repeat in each 'menu context' that is covered and what a 'repeatable keys list' is.
  • When, where and how double input happens.
  • Where the fix is and why it's there.

Effectively an if else statement, (read as "a fork in the road",) where both paths "go to the same place", and the condition being checked, (read as "which path to take",) always being true (read as "only one path is ever taken"). I think I did a good job there explaining that the piece of code in question is nonsense, whether your are a layman or familiar with coding concepts. And the problems with that code dont end there.



Reproducing the glitch in game


Most commonly perceived by people when pressing a direction followed by Circle in a menu. The key is to press Circle immediately after pressing the direction. And without releasing the direction.

For example, easily the most common way of experiencing it, Down > Circle.

  • Enter the Main (Triangle) Menu.
  • The cursor is on Item.
  • Press and hold Down immediately followed by a press of Circle.

What is expected is, the menu cursor goes down 1 place and Circle confirms on Magic and enters that menu. As it does on PSX under the same circumstances.

What happens is the down button is processed, and the Circle press processes and additional 'down' before confirming on Materia and entering that menu(if materia has been unlocked of course).

This is where the 'Double Input' name comes from, because a 2nd input is being experienced.

The above steps can be repeated:

  • In any menu/submenu directions can repeat in.
  • With other buttons instead of Circle. Like Square or R2 or Triangle or etc.
  • With other directions (as long as they can repeat in that menu) including L1 and R1.



Not just directions can be 'doubled'


These steps will demonstrate that the input repeat feature does not act on buttons held in a previous menu/context. Not relevant to DI really, but will help delineate where the next test will be. And Illustrate that it is a repeatable input that is getting doubled.

  • Enter the Triangle > Item menu.
  • Select any item that can be used on one or all party members. A potion for example.
  • The cursor is now over a party member.
  • Hold X.
  • The cursor is now over the potion again, and there it remains.
  • Release X.
  • Hold X.
  • After 200ms you are in the triangle menu.
  • And there you remain.
  • Release X.

We are now going to skip that 200ms. Well we wont, but it looks that way, we actually reset it like the PSX version does when a new input is pressed, only we will play a 'doubled' X at the same time.

  • Press circle.
  • The cursor is back in the Item menu, pointing at the first item in the list.
  • Hold X and before 200ms, heck as quickly as possible press either Up or Down to skip the 200ms with a doubled input of X.

Like with Down > Circle the second button causes the first to be processed a 2nd time.

The previous three steps can be repeated over and over replacing Up or Down with:

  • Square
  • R1 or L1
  • Left or Right
  • Triangle
  • Select
  • etc

Those steps cannot be repeated in menu contexts where X is not a button that can repeat.



Some fun


A bonus bit of fun once the mind has been expanded by the above.

  • In the Triangle menu

This test expects the user to be holding a controller or be quick enough at alternating pressing any two buttons… or at least faster that they are at mashing one button.

Square or L2 and R2 can be switched out for any inputs that feel comfortable, and don't take effect on the menu.

  • Hold Down and mash Square as fast as you can.

Whats happening?

  • Now hold Down (or Up, doesn't matter) and alternate L2 and R2 faster than the Square mash.

Whats happening?

That's not air you're breathing…. i mean that's not down repeating, that's down getting double-input'd.

Whats expected, like on PSX, is each press of a new button should prevent Down from repeating, down should only move one place, and remain there as long as the new buttons are being pressed.



Testing with the mod


The mod can be used to increase the Input Repeat Rate. This can facilitate testing for "doubled" inputs after 200ms has passed. Which do happen, but are hard to perceive. Trying to spot an extra Down when Down is being processed every 50 milliseconds without using tools is bonkers.

Speed runners may find this last test illuminating as its been long believed that double input is only present when pressing a button shortly after pressing another. Rather than always possible as long as a repeatable button is held within a menu.

  • Load the mod into 7th heaven.
  • Enable it and click on the configure button.
  • Leave the fix disabled.
  • Leave Delay at default.
  • Set Rate to Infinity.
  • Load the game.
  • Get to a menu of your choosing.
  • Hold down a repeatable button you would like to test for double input.
  • It will process once, and then after 200ms a second time.
  • You are now in infinite rate. No other ticks of repeat will occur.
  • Press another button to see if it triggers a doubled input.

After reading below it will be obvious that rate is irrelevant, the instant a button is pressed rate is over, and a new cycle of input processing is underway.



Inputs, and their values


Here we have the hex, binary and decimal representations of the games values for buttons in the 'all held keys' and 'keys this frame' variables, single inputs lists and repeatable inputs lists.

The binary being the most illuminating, showing how one variable can hold and be checked for multiple inputs. These values take up no more than 2 bytes and match up with how the PSX version of the game does it.

I will be using the PSX's names for these buttons. This is the game logic's representation of these buttons and on PC it does not matter what keyboard keys, game pad buttons or dance mats these buttons are bound to. This is what the game will see them as by the time it gets to any of the code we are covering here.

| PC     | PSX      |  Hex |           Binary | Decimal |
|--------+----------+------+------------------+---------|
| Camera | L2       | 0001 | 0000000000000001 |       1 |
| Target | R2       | 0002 | 0000000000000010 |       2 |
| PgUp   | L1       | 0004 | 0000000000000100 |       4 |
| PgDn   | R1       | 0008 | 0000000000001000 |       8 |
|        |          |      |                  |         |
| Menu   | Triangle | 0010 | 0000000000010000 |      16 |
| OK     | Circle   | 0020 | 0000000000100000 |      32 |
| Cancel | X        | 0040 | 0000000001000000 |      64 |
| Switch | Square   | 0080 | 0000000010000000 |     128 |
|        |          |      |                  |         |
| Assist | Select   | 0100 | 0000000100000000 |     256 |
| Start  | Start    | 0800 | 0000100000000000 |    2048 |
|        |          |      |                  |         |
| Up     | Up       | 1000 | 0001000000000000 |    4096 |
| Right  | Right    | 2000 | 0010000000000000 |    8192 |
| Down   | Down     | 4000 | 0100000000000000 |   16384 |
| Left   | Left     | 8000 | 1000000000000000 |   32768 |

The values are stored in 2 bytes (16 bits), leaving 2 unused bits between select and start. This doesn't mean there are some secret hidden buttons, it just means they had room to spare.

So using the table above we can see that variables of 2 bytes in length can denote the state of every button in the game, pressed = 1, not pressed = 0. We need only watch for the buttons corresponding bit from the binary column in the above table.

For example the corresponding bit for L2 is the 1 (or 0 if the button is not pressed) the furthest to the right.

0000000000000001
               ^

While the corresponding bit for R2 is one space to the left of that.

0000000000000010
              ^

And so on and so forth for the rest of the buttons.

Because there is no bits overlapping, 2 bytes can hold the state of every button in the game. Like so:

Psx's reset combination would look like this

0000100100001111 (L2 + R2 + L1 + R1 + Select + Start)

Or a simple Circle + Down

0100000000100000



Definitions and Names


I am going to lay out terms I have used or come up with while trying to explain FF7's Input Repeat Feature and the Double Input glitch.

Fair warning, I tend to use keys and inputs interchangeably. I should fix that at some point.

IMPORTANT: Menu:
Includes all menus in the game apart from those that are part of minigames or are in dialogue boxes in fields.

UDLR:
up, down, left, right.

Input Repeat Feature, IRF:
A feature in FF7 PC and PSX that facilitates what many call auto fire, rapid fire etc. It is experienced when in menus certain buttons are held down and a moment later the game acts as though that button is being pressed rapidly.

In each menu or submenu only certain keys/inputs can repeat. For example Circle cannot repeat in the top level of Triangle/Main menu. But can in battle menu. R1 and L1 cannot repeat in top level of Triangle/Main menu. But can in many menus with lists, like the item selection sub menu under the Triangle/Main menu > Item Menu.

Repeatable Keys List, RKL:
When a menu parses inputs it iterates over a hard coded list of keys that can repeat in that menu, and checks whether each of those buttons are in a down state. If they are and the repeat gate/flag is set. The found keys are played in game. (with some exceptions #1)

Double Input glitch, DI glitch:
A glitch that happens in FF7, on PC, in menus, when:

  • one or more buttons that can repeat in the current menu are in a down/pressed state (not a up/released state) (DI Initial Input)
  • any other button/buttons is/are put into a down/pressed state (DI Trigger Input) (yes more than one button can get pressed in the same frame)

Causing all Initial Inputs to be processed by the game at the moment the Trigger Input is pressed. Most commonly perceived when two buttons are pressed in quick succession, giving the user the experience of a "doubled input".



Important Addresses


009A85C8 - aka C8 - Input Repeat Delay


An address that holds the input repeat delay value. 200. The time in ms until repeat/auto-fire begins when a key is held.

Back Links:
Why we don't get there



009A85E4 - aka E4 - Input repeat rate


An address that holds the input repeat rate value. 50. The time in ms between ticks/fires/executions of repeat/auto-fire.

Back Links:
Why we don't get there
0041B099 - aka 099 - check for repeatable key function



009A8714 - aka 14 - Current input repeat wait


An address that holds the 'current wait' in ms for repeat. It holds either 200 or 50 depending on if a button was recently pressed or is still held after 200ms. The value stored in input repeat delay or input repeat rate is put here depending on those circumstances.

Back Links:
Brass tacks
0041B108 - aka 108 - input timer and flag setting function



009A8730 - aka 30 - Timer


Keeps track of time passed since inputs were processed, either by press or a tick of repeat.

Gets set to 0 when the following is true:

  • E0 (keys this frame) holds a value greater than 0.

    OR the following is all true:

  • E0 (keys this frame) holds no value, 0.
  • D4 (all held keys) holds a value greater than 0.
  • The timer not less than the 'current wait'.

Back Links:
0041B108 - aka 108 - input timer and flag setting function



009A872C - aka 2C - Repeat gate/flag


An address that gets set to either 1 or 0. It gets set to 1 when the following is true:

  • E0 (keys this frame) holds a value greater than 0.

    OR the following is all true:

  • E0 (keys this frame) holds no value, 0.
  • D4 (all held keys) holds a value greater than 0.
  • The timer not less than the 'current wait'.

It is read from in one place.

| address  | bytes       | code               |
|----------+-------------+--------------------|
| 0041B0C3 | A1 2C879A00 | mov eax,[009A872C] |

More on this later.

Back Links:
0041B108 - aka 108 - input timer and flag setting function
Whats in there, and why the fix works.
Brass tacks
0041B099 - aka 099 - check for repeatable key function



009A8734 - aka 34 - Repeat is active flag


Reference Links:
009A8714 - aka 14 - Current input repeat wait
009A8730 - aka 30 - Timer
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame
009A85D4 - aka D4 - All held inputs/keys

A flag that is set to 0 when:

  • E0 (keys this frame) holds a value greater than 0.

It is set to 1 when the following are all true:

  • E0 holds no value (all 0's)
  • D4 (all held keys) holds a value greater than 0.
  • The timer is not less than 'current wait'.

In English, this flag = 1 when these three things are true:

  • No buttons were pressed this frame
  • One or more buttons are still held this frame
  • Either 200ms has passed since the last button was pressed or 50ms has passed since the last tick of repeat.

This flag, as written, is 1 when inputs should repeat. And 0 when only keys pressed this frame should be processed.

This flag is never read by the game at any point in time.

Back Links:
0041B108 - aka 108 - input timer and flag setting function
Whats in there, and why the fix works.
Brass tacks



Functions


In some cases showing the code is either unnecessary or stupid. Both cases just waste space, but the stupid one involves a few thousand lines. So if you want to read it, the addresses are there to look up.

Line breaks in tables are for readability, line breaks are placed directly above jump destinations.



0041B108 - aka 108 - input timer and flag setting function


This is the beating heart of menu inputs.

A lot of values get set here prior to the current frame/cycle of input processing/parsing.

If you to re read the following sections the code below is easier to follow. Use the 'back links' to return here. Remember 'aka 108'.

009A8714 - aka 14 - Current input repeat wait
009A8730 - aka 30 - Timer
009A872C - aka 2C - Repeat gate/flag
009A8734 - aka 34 - Repeat is active flag
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame
009A85D4 - aka D4 - All held inputs/keys

| address  | bytes                   | code                        | comments                                |
|----------+-------------------------+-----------------------------+-----------------------------------------|
| 0041B108 | 55                      | push ebp                    |                                         |
| 0041B109 | 8B EC                   | mov ebp,esp                 |                                         |
| 0041B10B | C7 05 2C879A00 00000000 | mov [009A872C],00000000     | "repeat gate" flag is zeroed            |
| 0041B115 | 83 3D E0859A00 00       | cmp dword ptr [009A85E0],00 | if any keys were not pressed this frame |
| 0041B11C | 74 2A                   | je 0041B148                 | jump to address 0041B148                |
| 0041B11E | C7 05 34879A00 00000000 | mov [009A8734],00000000     | zero "repeat is active" flag            |
| 0041B128 | A1 C8859A00             | mov eax,[009A85C8]          | load delay value (200)                  |
| 0041B12D | A3 14879A00             | mov [009A8714],eax          | put delay value in "current wait"       |
| 0041B132 | C7 05 2C879A00 01000000 | mov [009A872C],00000001     | set "repeat gate" flag to 1             |
| 0041B13C | C7 05 30879A00 00000000 | mov [009A8730],00000000     | set timer to 0                          |
| 0041B146 | EB 41                   | jmp 0041B189                | jump to address                         |
|----------+-------------------------+-----------------------------+-----------------------------------------|
| 0041B148 | 83 3D D4859A00 00       | cmp dword ptr [009A85D4],00 | if any keys are not held this frame     |
| 0041B14F | 74 38                   | je 0041B189                 | jump to address 0041B189                |
| 0041B151 | 8B 0D 30879A00          | mov ecx,[009A8730]          | load timer value                        |
| 0041B157 | 3B 0D 14879A00          | cmp ecx,[009A8714]          | if timer is less than "current wait"    |
| 0041B15D | 72 2A                   | jb 0041B189                 | jump to address 0041B189                |
| 0041B15F | 8B 15 E4859A00          | mov edx,[009A85E4]          | load rate value (50)                    |
| 0041B165 | 89 15 14879A00          | mov [009A8714],edx          | put rate value in "current wait"        |
| 0041B16B | C7 05 34879A00 01000000 | mov [009A8734],00000001     | set "repeat is active" flag to 1        |
| 0041B175 | C7 05 2C879A00 01000000 | mov [009A872C],00000001     | set "repeat gate" flag to 1             |
| 0041B17F | C7 05 30879A00 00000000 | mov [009A8730],00000000     | set timer to 0                          |
|----------+-------------------------+-----------------------------+-----------------------------------------|
| 0041B189 | 5D                      | pop ebp                     |                                         |
| 0041B18A | C3                      | ret                         |                                         |



0041AB67 - aka B67 -"check if key in 'all keys'(D4)" or 'in all keys' function


Reference Links:
009A85D4 - aka D4 - All held inputs/keys

The name says it all really. The function is passed a representational value of a button in the stack. 'See Inputs and their values' above.

Returns non zero value in eax register if a key value passed to it in the stack matches a bit in D4.

| address  | bytes       | code               | comments                                                              |
|----------+-------------+--------------------+-----------------------------------------------------------------------|
| 0041AB67 | 55          | push ebp           | start of all keys function, stores stack pointer                      |
| 0041AB68 | 8B EC       | mov ebp,esp        | sets pointer                                                          |
| 0041AB6A | A1 D4859A00 | mov eax,[009A85D4] | puts 'all keys' in eax register                                       |
| 0041AB6F | 23 45 08    | and eax,[ebp+08]   | checks if the bit for the key held in the stack is in eax(009A85D4)   |
| 0041AB72 | 5D          | pop ebp            | restores stored stack pointer                                         |
| 0041AB73 | C3          | ret                | return with the value of the button in eax, if found. 0 in eax if not |

Back Links:
Whats in there, and why the fix works.
0041B099 - aka 099 - check for repeatable key function



0041AB74 - aka B74 - "check if key in 'recent keys'(E0)" function


Reference Links:
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame

Exact same as the above function (B67) only it checks E0 (keys this frame).

Back Links:
Whats in there, and why the fix works.
Brass tacks
007212FB - aka 2FB - first menu main(-ish)



0041B099 - aka 099 - check for repeatable key function


Reference links:
009A872C - aka 2C - Repeat gate/flag
009A85D4 - aka D4 - All held inputs/keys
009A85E4 - aka E4 - Input repeat rate
0041AB67 - aka B67 -"check if key in 'all keys'(D4)" or 'in all keys' function

Gets passed a key value in the stack and checks for it in the D4 "all held keys" memory location by calling the 0041AB67 'in all keys' function.

Returns a non zero value in the eax register if the button is found in D4 and the repeat gate/flag is 1. If a non zero value is returned the found key is played in game later (with some exceptions #1).

This function is the only method menus use to check for inputs from keys that have the potential to repeat.

Used by menus to find repeatable keys and repeatable keys only. The problem is there is no distinction made between the initial presses of the repeatable keys and an already held key.

This function is called by a variety of what I call repeatable key wrapper functions, that either have a list of repeatable keys that suit the calling menu's context and are one at a time passed to this function (099) or are themselves passed keys to pass onto this function.

Whats more is the press of any button (with exceptions based on context - #2) causes an already held key to get played in game, even if that pressed key has no effect on that menu and no code to handle or acknowledge it in that menu.

A press of any key causes this function to return all repeatable keys that are in a down state down state as valid for processing. Regardless of whether or not it is time to play held repeatable keys.

I describe this as, in menus, every input triggers a tick of input repeat.

I cant be 100% sure whether this stems from fault or failure in the writing of this function or some other part of the code before or after each call of it. But I believe that this function is the cause of double input and was likely where a proper implementation of repeatable input parsing was being constructed.

But what I am sure of, is the fact that it, like other parts of the code, has a nonsense about it. A nonsense that can be leveraged to repair the bug. And that it is used for every menus processing of keys that have the potential to repeat and so is a single place where a fix can go that would mend all menus at once.

| address  | bytes             | code                        | comments                                        |
|----------+-------------------+-----------------------------+-------------------------------------------------|
| 0041B099 | 55                | push ebp                    |                                                 |
| 0041B09A | 8B EC             | mov ebp,esp                 |                                                 |
| 0041B09C | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 | is E4 (address that holds rate) equal to 0      |
| 0041B0A3 | 75 0E             | jne 0041B0B3                | jump if not, to 0041B0B3                        |
|          |                   |                             | BELOW, TILL THE BREAK IS AN UNREACHABLE SECTION |
| 0041B0A5 | 8B 45 08          | mov eax,[ebp+08]            | "load key to be checked" from stack             |
| 0041B0A8 | 50                | push eax                    | push it into the top of the stack               |
| 0041B0A9 | E8 B9FAFFFF       | call 0041AB67               | call "check if in all keys" function            |
| 0041B0AE | 83 C4 04          | add esp,04                  | shifts stack pointer (ignore)                   |
| 0041B0B1 | EB 19             | jmp 0041B0CC                | jmp to 0041B0CC                                 |
|----------+-------------------+-----------------------------+-------------------------------------------------|
| 0041B0B3 | 8B 4D 08          | mov ecx,[ebp+08]            | "load key to be checked" from stack             |
| 0041B0B6 | 51                | push ecx                    | push it into the top of the stack               |
| 0041B0B7 | E8 ABFAFFFF       | call 0041AB67               | call "check if in all keys" function            |
| 0041B0BC | 83 C4 04          | add esp,04                  | shifts stack pointer (ignore)                   |
| 0041B0BF | 85 C0             | test eax,eax                | AND's the eax register, w/o changing it         |
| 0041B0C1 | 74 07             | je 0041B0CA                 | jumps if eax was 0, to 0041B0CA                 |
| 0041B0C3 | A1 2C879A00       | mov eax,[009A872C]          | puts the "process keys" flag in eax             |
| 0041B0C8 | EB 02             | jmp 0041B0CC                | jmp to 0041B0CC                                 |
|----------+-------------------+-----------------------------+-------------------------------------------------|
| 0041B0CA | 33 C0             | xor eax,eax                 | sets eax to 0 .... yea that makes sense \s      |
|----------+-------------------+-----------------------------+-------------------------------------------------|
| 0041B0CC | 5D                | pop ebp                     |                                                 |
| 0041B0CD | C3                | ret                         | returns to caller                               |

From 0041B0A5 to 0041B0B1 will be referred to as 'Condition 1'.

From 0041B0B3 to 0041B0CA will be referred to as 'Condition 2'.

Back Links:
Whats in there, and why the fix works.
Brass tacks
006F4DB2 - aka DB2 - wrapper function that finds directions for menus
006F53B2 - aka 3B2 - used for UDLR
006F53F1 - aka 3F1 - simple wrapper
Why we don't get there



wrapper functions for 'check for repeatable key' (099)


Input repeat wrapper functions ultimately call 099 or a simpler wrapper function that in turn calls 099. They either take a key value (passed via the stack) or have their own list of key values baked in.

UDLR = up, down, left, right

006F53F1 - aka 3F1 - simple wrapper


Reference Links:
009A85D4 - aka D4 - All held inputs/keys
0041B099 - aka 099 - check for repeatable key function

Called by menus or wrapper functions to check just about any key, often directly by what I call a menus main function.

Gets passed a key value in the stack and checks for it in the D4 "all held keys" memory location by calling the 0041B099 function. (099).

If 099 returns with a non zero value in eax, this function returns with a non zero value in eax.

If 099 returns with a 0 in eax, this function will return with a 0 in eax.

| address  | bytes       | code             |
|----------+-------------+------------------|
| 006F53F1 | 55          | push ebp         |
| 006F53F2 | 8B EC       | mov ebp,esp      |
| 006F53F4 | 8B 45 08    | mov eax,[ebp+08] |
| 006F53F7 | 50          | push eax         |
| 006F53F8 | E8 9C5CD2FF | call 0041B099    |
| 006F53FD | 83 C4 04    | add esp,04       |
| 006F5400 | 5D          | pop ebp          |
| 006F5401 | C3          | ret              |

Back Links:
Whats in there, and why the fix works.
Brass tacks
007212FB - aka 2FB - first menu main(-ish)
007201E2 - aka 1E2 - Save slot selection (Load screen) repeatable keys wrapper
006F4DB2 - aka DB2 - wrapper function that finds directions for menus


006F53B2 - aka 3B2 - used for UDLR


Reference Links:
009A85D4 - aka D4 - All held inputs/keys
0041B099 - aka 099 - check for repeatable key function

Called by menus or wrapper functions to check up, down, left or right.

Does more than other smaller wrappers but all that matters for now is that it returns a non zero value in eax if the key was found and is valid for processing or another special case is met. And a zero if not.

I have theory's and little time to test them. May be for the tutorial sections as the code does seem to be overriding cases where no button is found and setting them as found returning a 1 in eax. Not related to DI as DI can occur on any key, not just UDLR. When time allows I'll pull this thread.

Gets passed a key value in the stack and checks for it in the D4 "all held keys" memory location by calling the 0041B099 function. (099).

If 099 returns with a non zero value in eax, this function returns with a non zero value in eax. Specifically, a '00000001'.

If 099 returns with a 0 in eax, this function will return with a 0 in eax unless a second value pulled from the stack does not match the key value that was checked for.

Only keys that we dont find in 099 have the potential to get overridden. Keys we do find will always result in eax returning a non 0 value.

| address  | bytes             | code                  | comments                                |
|----------+-------------------+-----------------------+-----------------------------------------|
| 006F53B2 | 55                | push ebp              |                                         |
| 006F53B3 | 8B EC             | mov ebp,esp           |                                         |
| 006F53B5 | 83 EC 08          | sub esp,08            |                                         |
| 006F53B8 | E8 115DD2FF       | call 0041B0CE         |                                         |
| 006F53BD | 89 45 FC          | mov [ebp-04],eax      |                                         |
| 006F53C0 | 8B 45 08          | mov eax,[ebp+08]      | "load key to be checked" from stack     |
| 006F53C3 | 50                | push eax              | push it into the top of the stack       |
| 006F53C4 | E8 D05CD2FF       | call 0041B099         | check for repeatable key function       |
| 006F53C9 | 83 C4 04          | add esp,04            |                                         |
| 006F53CC | 85 C0             | test eax,eax          | AND's the eax register, w/o changing it |
| 006F53CE | 75 13             | jne 006F53E3          | jumps if eax is not 0, to 006F53E3      |
| 006F53D0 | 8B 4D FC          | mov ecx,[ebp-04]      |                                         |
| 006F53D3 | 23 4D 0C          | and ecx,[ebp+0C]      |                                         |
| 006F53D6 | 85 C9             | test ecx,ecx          |                                         |
| 006F53D8 | 75 09             | jne 006F53E3          |                                         |
| 006F53DA | C7 45 F8 00000000 | mov [ebp-08],00000000 |                                         |
| 006F53E1 | EB 07             | jmp 006F53EA          |                                         |
|----------+-------------------+-----------------------+-----------------------------------------|
| 006F53E3 | C7 45 F8 01000000 | mov [ebp-08],00000001 |                                         |
|----------+-------------------+-----------------------+-----------------------------------------|
| 006F53EA | 8B 45 F8          | mov eax,[ebp-08]      | sets eax to 0 or 1                      |
| 006F53ED | 8B E5             | mov esp,ebp           |                                         |
| 006F53EF | 5D                | pop ebp               |                                         |
| 006F53F0 | C3                | ret                   | returns                                 |

Back Links:
Brass tacks
006F4DB2 - aka DB2 - wrapper function that finds directions for menus

006F4DB2 - aka DB2 - wrapper function that finds directions for menus


Reference Links:
006F53F1 - aka 3F1 - simple wrapper
006F53B2 - aka 3B2 - used for UDLR
0041B099 - aka 099 - check for repeatable key function

This wrapper is used by many menus to find UDLR.

Laid out in this block of code is a list of directions that it checks one at a time by pushing them into the stack and calling a wrapper function.

The order of the checks for inputs in this wrapper is:

  • up
  • down
  • left
  • right
  • L1
  • R1

They can be seen every time there is a push followed by a call to 006F53B2 aka 3B2… not to be confused with DB2, this function. Or when there is a push followed by a call to 006F53F1 (aka 3F1) in the case of R1 and L1.

BUT R1 and L1 often do not get reached. And are usually handled by the menus main calling 3F1 directly.

This function is 433 lines/instructions long, small parts will be shown for illustration purposes.

| address  | bytes         | code                      | comments                                  |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F4DC5 | 6A 01         | push 01                   | related to 3B2's other purpose?           |
| 006F4DC7 | 68 00100000   | push 00001000             | up                                        |
| 006F4DCC | E8 E1050000   | call 006F53B2             | 3B2 call                                  |
| 006F4DD1 | 83 C4 08      | add esp,08                |                                           |
| 006F4DD4 | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F4DD6 | 0F84 A4000000 | je 006F4E80               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F4E80 | 6A 02         | push 02                   | related to 3B2's other purpose?           |
| 006F4E82 | 68 00400000   | push 00004000             | down                                      |
| 006F4E87 | E8 26050000   | call 006F53B2             | 3B2 call                                  |
| 006F4E8C | 83 C4 08      | add esp,08                |                                           |
| 006F4E8F | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F4E91 | 0F84 AA000000 | je 006F4F41               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F4F41 | 6A 08         | push 08                   | related to 3B2's other purpose?           |
| 006F4F43 | 68 00800000   | push 00008000             | left                                      |
| 006F4F48 | E8 65040000   | call 006F53B2             | 3B2 call                                  |
| 006F4F4D | 83 C4 08      | add esp,08                |                                           |
| 006F4F50 | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F4F52 | 0F84 1D010000 | je 006F5075               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F5075 | 6A 04         | push 04                   | related to 3B2's other purpose?           |
| 006F5077 | 68 00200000   | push 00002000             | right                                     |
| 006F507C | E8 31030000   | call 006F53B2             | 3B2 call                                  |
| 006F5081 | 83 C4 08      | add esp,08                |                                           |
| 006F5084 | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F5086 | 0F84 62010000 | je 006F51EE               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F51EE | 8B 55 08      | mov edx,[ebp+08]          |                                           |
| 006F51F1 | 83 7A 34 00   | cmp dword ptr [edx+34],00 | makes a decision about skipping R1 and L1 |
| 006F51F5 | 0F84 93000000 | je 006F528E               | skip or move on                           |
| 006F51FB | 6A 08         | push 08                   | R1                                        |
| 006F51FD | E8 EF010000   | call 006F53F1             | 3F1 call                                  |
| 006F5202 | 83 C4 04      | add esp,04                |                                           |
| 006F5205 | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F5207 | 74 46         | je 006F524F               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |
|----------+---------------+---------------------------+-------------------------------------------|
| 006F524F | 6A 04         | push 04                   | L1                                        |
| 006F5251 | E8 9B010000   | call 006F53F1             | 3F1 call                                  |
| 006F5256 | 83 C4 04      | add esp,04                |                                           |
| 006F5259 | 85 C0         | test eax,eax              | AND's the eax register, w/o changing it   |
| 006F525B | 74 31         | je 006F528E               | jumps if 0                                |
| ........ | ...           | ...                       | otherwise, processes                      |

In order to save space they likely made this wrapper that's just for directions and reused it where menus needed to process UDLR. Even if not all keys mattered in that menu, like 'New Game / Continue?' where left and right make no sense and don't take effect on that menu, but still are processed and returned as valid by 099 (exceptions #1).

Whats important to take away from here is that eax is checked if it is 0 or non zero, and then processing of the input is queued/takes place if its non zero.

Back Links:
007212FB - aka 2FB - first menu main(-ish)



007201E2 - aka 1E2 - Save slot selection (Load screen) repeatable keys wrapper


Reference links:
006F53F1 - aka 3F1 - simple wrapper

This is a repeatable keys wrapper used for the save slot selection sub menu of the first menu the player interacts with.

I will not be covering it here. This is long enough and nothing new will be seen within it anyway. It is not used for saving, only loading. And perhaps other places. All the buttons it checks for, it does so with 3F1 (simple wrapper). The inputs it handles, in this order, are:

  • Up
  • Down
  • Left (i know, wtf)(exceptions #1)
  • Right (i know, wtf)(exceptions #1)
  • R1
  • L1

Back Links:
007212FB - aka 2FB - first menu main(-ish)


007212FB - aka 2FB - first menu main(-ish)


Reference links:
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame
0041AB74 - aka B74 - "check if key in 'recent keys'(E0)" function
006F53F1 - aka 3F1 - simple wrapper
006F4DB2 - aka DB2 - wrapper function that finds directions for menus
007201E2 - aka 1E2 - Save slot selection (Load screen) repeatable keys wrapper

This has input processing logic for keys in the first menu that loads (…. kinda the second.. or third, but nevermind). Its where you choose 'New Game' or 'Continue?'.

Not speaking authoritatively here on what is and isn't 'main'. It and other functions like it are as high up the chain I need to go to have all input parsing calls at the current functions level or in calls below it. So for now that name suits me, ish.

This menu like others has sub menus, so a total of 3 menus are covered by this code. They and the single inputs (buttons that don't repeat) that they are coded to handle are as follows:

  • New Game / Continue?
    • only circle
  • Save file selection
    • circle and X
  • Save slot selection
    • circle and X

This calls the repeatable keys logic for each menu, and uses the 0041AB74 (B74) (is key in recent list) function directly for all single inputs that each menu can handle… inputs that don't repeat.

The repeatable keys list, in order, for each menu are as follows:

  • New Game / Continue?
    • UDLR
  • Save file selection
    • UDLR
  • Save slot selection
    • UDLR
    • R1
    • L1

This one is 1105 lines/instructions, again relevant parts are cherry picked.

| address  | bytes         | code               | comments                                |
|----------+---------------+--------------------+-----------------------------------------|
| 00721E28 | 6A 20         | push 20            | circle (save file selection menu)       |
| 00721E2A | E8 458DCFFF   | call 0041AB74      | B74 call (finds single inputs in E0)    |
| 00721E2F | 83 C4 04      | add esp,04         |                                         |
| 00721E32 | 85 C0         | test eax,eax       | AND's the eax register, w/o changing it |
| 00721E34 | 0F84 28010000 | je 00721F62        | jumps if 0                              |
| ........ | ...           | ...                | otherwise, processes                    |
|----------+---------------+--------------------+-----------------------------------------|
| 00721F62 | 6A 40         | push 40            | X  (save file selection menu)           |
| 00721F64 | E8 0B8CCFFF   | call 0041AB74      | B74 call (finds single inputs in E0)    |
| 00721F69 | 83 C4 04      | add esp,04         |                                         |
| 00721F6C | 85 C0         | test eax,eax       | AND's the eax register, w/o changing it |
| 00721F6E | 74 16         | je 00721F86        | jumps if 0                              |
| ........ | ...           | ...                | otherwise, processes                    |
|----------+---------------+--------------------+-----------------------------------------|
| 00721F86 | 68 986DDD00   | push 00DD6D98      | Menu identification/criteria?           |
| 00721F8B | E8 222EFDFF   | call 006F4DB2      | DB2 call, for file selection            |
| 00721F90 | 83 C4 04      | add esp,04         |                                         |
| 00721F93 | E9 79030000   | jmp 00722311       | Jump to end of function                 |
| ........ | ...           | ...                | ...                                     |
|----------+---------------+--------------------+-----------------------------------------|
| 00721FA1 | 68 D06DDD00   | push 00DD6DD0      | Menu identification/criteria?           |
| 00721FA6 | E8 37E2FFFF   | call 007201E2      | 1E2 call, save slot selection           |
| ........ | ...           | ...                | ...                                     |
|----------+---------------+--------------------+-----------------------------------------|
| 00721FC5 | 6A 20         | push 20            | circle (save slot selection menu)       |
| 00721FC7 | E8 A88BCFFF   | call 0041AB74      | B74 call (finds single inputs in E0)    |
| 00721FCC | 83 C4 04      | add esp,04         |                                         |
| 00721FCF | 85 C0         | test eax,eax       | AND's the eax register, w/o changing it |
| 00721FD1 | 74 50         | je 00722023        | jumps if 0                              |
| ........ | ...           | ...                | otherwise, processes                    |
|----------+---------------+--------------------+-----------------------------------------|
| 00722023 | 6A 40         | push 40            | X (save slot selection)                 |
| 00722025 | E8 4A8BCFFF   | call 0041AB74      | B74 call (finds single inputs in E0)    |
| 0072202A | 83 C4 04      | add esp,04         |                                         |
| 0072202D | 85 C0         | test eax,eax       | AND's the eax register, w/o changing it |
| 0072202F | 74 19         | je 0072204A        | Jump to a jump to end of function if 0  |
| ........ | ...           | ...                | otherwise, processes                    |
|----------+---------------+--------------------+-----------------------------------------|
| 0072204A | E9 C2020000   | jmp 00722311       | Jump to end of function                 |
| ........ | ...           | ...                | ...                                     |
|----------+---------------+--------------------+-----------------------------------------|
| 0072225A | 6A 20         | push 20            | circle (New Game/Continue)              |
| 0072225C | E8 1389CFFF   | call 0041AB74      | B74 call (finds single inputs in E0)    |
| 00722261 | 83 C4 04      | add esp,04         |                                         |
| 00722264 | 85 C0         | test eax,eax       | AND's the eax register, w/o changing it |
| 00722266 | 0F84 98000000 | je 00722304        | jumps if 0                              |
| ........ | ...           | ...                | otherwise, processes                    |
|----------+---------------+--------------------+-----------------------------------------|
| 00722304 | 68 206FDD00   | push 00DD6F20      | Menu identification/criteria?           |
| 00722309 | E8 A42AFDFF   | call 006F4DB2      | DB2 call, for New Game screen           |
| 0072230E | 83 C4 04      | add esp,04         |                                         |
|----------+---------------+--------------------+-----------------------------------------|
| 00722311 | A1 3877DD00   | mov eax,[00DD7738] |                                         |
| 00722316 | 8B E5         | mov esp,ebp        |                                         |
| 00722318 | 5D            | pop ebp            |                                         |
| 00722319 | C3            | ret                |                                         |



What we can surmise


The fact that inputs can be doubled by the press of keys other than those in the list for single inputs or repeatable inputs highlight further that this is a bug.

For example in all three of these menus, inputs can be doubled by every key not included in the single inputs and repeatable inputs list. What business do these keys have taking effect on inputs in these menus when there is no code to use them in these contexts.

Keys that can repeat are all those in the repeatable keys logic list. And all those that can trigger them is every key.

Try: Up > Triangle in New Game / Continue?
Try: Right > Square in File selection.
etc etc


Brass tacks


Reference links:
009A8734 - aka 34 - Repeat is active flag
009A872C - aka 2C - Repeat gate/flag
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame
009A85D4 - aka D4 - All held inputs/keys
009A8714 - aka 14 - Current input repeat wait
0041AB74 - aka B74 - "check if key in 'recent keys'(E0)" function
006F53B2 - aka 3B2 - used for UDLR
006F53F1 - aka 3F1 - simple wrapper
0041B099 - aka 099 - check for repeatable key function

So what we have seen so far and some objective and subjective babbling…

  • When any input is cleared for processing in menus it ultimately is done so by testing eax and processing if eax is not 0.

    Seen following calls to:

    • B74 - "check if key in 'recent keys'(E0)" (for single inputs)
    • 3F1 - simple wrapper

    We also see other functions perform the eax test and their own additional checks/actions before sending the result up the chain to the calling function(s):

    • 3B2 - UDLR
    • 099 - check for repeatable key function (oh we'll get to you)
  • 34, the repeat is active flag, is set and never checked.

    And its value changes perfectly line up with when the 200ms delay should be respected.

  • 2C, what I call the repeat gate flag, not because of when it gets set, like why I call 34 what I do. But because of the one and only place it is checked. In 099.

    If forced to be only ever 1, repeat is constant and immediate. It is what keeps repeat firing on 50ms ticks, it is what keeps it from firing before the 200ms wait has passed. It gates repeat from occurring.

    Im of the firm belief that the only reason it is 1 when a value is found in E0 is because they janked up implementing the repeat feature and creating double input was the solution. It not being assigned 1 in 108 when E0 is occupied, without first finishing the implementation, means no input for repeatable keys until after 200ms.

    A band aid slapped in place because of time constraints or rotating someone off the job before it was finished or both. Or whatever.

  • 099, the place all roads lead to (other than B67) when parsing repeatable keys, is only ever used in menus and has an unreachable condition and only ever pulls from D4 (all held keys).

    I'll say, though I may need to do more work explaining it, that clearly it should be pulling from E0 (keys this frame) if/when there is a value in it. Keys never have any business repeating when E0 is occupied.

    In fact, the fix uses the 34 flag, but E0 would accomplish the same thing, 34 doesn't need to exist, 2C will do the rest of the job just fine. But it does… so why not use it. Its faster than using E0 thanks to the skipped unnecessary test and dumb extra xor. Which is likely 34's purpose. Guiding to a condition where unnecessary code does not take place. There is no point in checking for the next tick of repeat while delay is in 'current wait'.



The unreachable condition (condition 1 of 099)


Why we don't get there


Reference links:
009A85C8 - aka C8 - Input Repeat Delay
009A85E4 - aka E4 - Input repeat rate
0041B099 - aka 099 - check for repeatable key function

This line is the gate keeper of condition 1 in 099.

| address  | bytes             | code                        | comments                |
|----------+-------------------+-----------------------------+-------------------------|
| 0041B09C | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 | is E4 (rate) equal to 0 |

E4 is only ever 0, at one point in the game.

009A85C8 holds the delay 200
009A85E4 holds the rate 50

Right before the intros those vars get assigned their values by this function for the very first time. Their values never change after this.

0041B0D8 - Timings assignment function

| address  | bytes          | code               | comments |
|----------+----------------+--------------------+----------|
| 0041B0D8 | 55             | push ebp           |          |
| 0041B0D9 | 8B EC          | mov ebp,esp        |          |
| 0041B0DB | 8B 45 08       | mov eax,[ebp+08]   |          |
| 0041B0DE | A3 C8859A00    | mov [009A85C8],eax |      200 |
| 0041B0E3 | 8B 4D 0C       | mov ecx,[ebp+0C]   |          |
| 0041B0E6 | 89 0D E4859A00 | mov [009A85E4],ecx |       50 |
| 0041B0EC | 5D             | pop ebp            |          |
| 0041B0ED | C3             | ret                |          |

For right after the intros the origins of those values are dwords in the stack at 011D5104 (200) and 011D5108 (50) and they get there here just prior to calling the function.

|  address | bytes       | code          | comments |
|----------+-------------+---------------+----------|
| 007223D4 | 6A 32       | push 32       |       50 |
| 007223D6 | 68 C8000000 | push 000000C8 |      200 |
| 007223DB | E8 F88CCFFF | call 0041B0D8 |          |

This is a list of places that function gets called from (or is even at all referenced), and exactly like above, 50 and 200 get put in the stack just prior to calling, never 0 for either value.

| address  | bytes       | code          |
|----------+-------------+---------------|
| 00408A5E | E8 75260100 | call 0041B0D8 |
| 006CB584 | E8 4FFBD4FF | call 0041B0D8 |
| 006FFFEF | E8 E4B0D1FF | call 0041B0D8 |
| 00701FA3 | E8 3091D1FF | call 0041B0D8 |
| 00719C48 | E8 8B14D0FF | call 0041B0D8 |
| 0071FFE3 | E8 F0B0CFFF | call 0041B0D8 |
| 007223DB | E8 F88CCFFF | call 0041B0D8 |
| 0074BC25 | E8 AEF4CCFF | call 0041B0D8 |

It is possible that there are other calls that are done with relative pointers, but I haven't found them. And as best as I can tell, this function is used to set these values every time an menu module is loaded. If their are menu inputs to handle, this function is run before preparing to do so.

Checking the process memory the moment it spawns, shows that those values are in place after a fraction of a second. So there is a window of time when the process starts when they, or most importantly E4 (rate … 50) is 0. It is very small. Sending the game inputs in that window of time would take effort but is not impossible. Whether it is capable of receiving or processing inputs at this time is another question.

The moment a menu scene/module is loaded, the timing assignment function is called. So code in those modules checking for rate to be 0 is, dumb. Assuming the above function is never used to set the rate var to 0 with relative pointers that I missed. And assuming I am correct about the zeroing function below.



The zeroing function


This function zeros

  • the timer (30)
  • the repeat is active flag (34)
  • the delay holding variable (C8)
  • the rate holding variable (E4)
| address  | bytes                   | code                    |
|----------+-------------------------+-------------------------|
| 0041B18B | 55                      | push ebp                |
| 0041B18C | 8B EC                   | mov ebp,esp             |
| 0041B18E | C7 05 30879A00 00000000 | mov [009A8730],00000000 |
| 0041B198 | C7 05 C8859A00 00000000 | mov [009A85C8],00000000 |
| 0041B1A2 | A1 C8859A00             | mov eax,[009A85C8]      |
| 0041B1A7 | A3 E4859A00             | mov [009A85E4],eax      |
| 0041B1AC | C7 05 34879A00 00000000 | mov [009A8734],00000000 |
| 0041B1B6 | 5D                      | pop ebp                 |
| 0041B1B7 | C3                      | ret                     |

Nowhere in the code is this function ever explicitly referenced, at all. Neither as value or pointer.

Like the rate and delay assignment function there is the possibility that there are calls to this function using relative pointers, but I cant find this function executing at all with breakpoints, in menus or otherwise.

As best as I can tell this code is never reached, and at least is never reached during menus. When it could have had some effect on the repeat processing. Though double input would still exist and repeatable inputs would be insane…. its a tick rate of 0. See my mod for what that would be like.



All instructions that reference E4 explicitly


The search was over entirety of the assembly across all memory addresses (00000000 -> FFFFFFFF) for any reference to 009A85E4. This is as a value or a pointer. Only pointers were found.

COMPARES TO 0:
Mystery nonsense conditionals that are, as best as I can tell, in places that that are unreachable when E4 is 0 at the very beginning of the process.

|----------+-------------------+-----------------------------+------------------|
| 0041A766 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041AD81 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041AE98 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041AF12 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041AF88 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041B002 | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 |                  |
| 0041B09C | 83 3D E4859A00 00 | cmp dword ptr [009A85E4],00 | The one from 099 |
|----------+-------------------+-----------------------------+------------------|

PUTS A VALUE INTO AN ADDRESS RELATIVE TO E4'S POSITION:
To clarify, relative means it is putting a value into an address some number of places above or below E4. Not into E4 itself.

|----------+-------------------+------------------------------|
| 0041A81D | 89 8C 82 E4859A00 | mov [edx+eax*4+009A85E4],ecx |
|----------+-------------------+------------------------------|

READS THE VALUE OF E4:

|----------+----------------+--------------------|
| 0041AEC4 | 8B 0D E4859A00 | mov ecx,[009A85E4] |
| 0041AECD | 8B 15 E4859A00 | mov edx,[009A85E4] |
| 0041AF38 | 8B 0D E4859A00 | mov ecx,[009A85E4] |
| 0041AF41 | 8B 15 E4859A00 | mov edx,[009A85E4] |
| 0041AFB4 | 8B 0D E4859A00 | mov ecx,[009A85E4] |
| 0041AFBD | 8B 15 E4859A00 | mov edx,[009A85E4] |
| 0041B028 | 8B 0D E4859A00 | mov ecx,[009A85E4] |
| 0041B031 | 8B 15 E4859A00 | mov edx,[009A85E4] |
| 0041B0FF | A1 E4859A00    | mov eax,[009A85E4] |
| 0041B15F | 8B 15 E4859A00 | mov edx,[009A85E4] |
|----------+----------------+--------------------|

THE TWO AND ONLY INSTANCES IN THE ASSEMBLY WHERE THERE ARE WRITES TO E4:

|----------+----------------+--------------------+----------------------------------------|
| 0041B0E6 | 89 0D E4859A00 | mov [009A85E4],ecx | assignment function (where it gets 50) |
| 0041B1A7 | A3 E4859A00    | mov [009A85E4],eax | zeroing function                       |
|----------+----------------+--------------------+----------------------------------------|



Whats in there, and why the fix works.


Reference links:
009A8734 - aka 34 - Repeat is active flag
009A872C - aka 2C - Repeat gate/flag
009A85E0 - aka E0 - Recent inputs/keys, or keys this frame
009A85D4 - aka D4 - All held inputs/keys
0041AB67 - aka B67 -"check if key in 'all keys'(D4)" or 'in all keys' function
0041AB74 - aka B74 - "check if key in 'recent keys'(E0)" function
006F53F1 - aka 3F1 - simple wrapper
0041B099 - aka 099 - check for repeatable key function

When we compare condition 1 to condition 2, we see they are quite similar.

Condition 1, unreachable.

| code             | comments                             |
|------------------+--------------------------------------|
| mov eax,[ebp+08] | "load key to be checked" from stack  |
| push eax         | push it into the top of the stack    |
| call 0041AB67    | call "check if in all keys" function |
| add esp,04       | shifts stack pointer (ignore)        |
| jmp 0041B0CC     | jmp to 0041B0CC                      |

Condition 2.

| code               | comments                                           |
|--------------------+----------------------------------------------------|
| mov ecx,[ebp+08]   | "load key to be checked" from stack                |
| push ecx           | push it into the top of the stack                  |
| call 0041AB67      | call "check if in all keys" function               |
| add esp,04         | shifts stack pointer (ignore)                      |
| test eax,eax       | AND's the eax register                             |
| je 0041B0CA        | jumps if eax was 0, to set eax to 0, which is dumb |
| mov eax,[009A872C] | puts the "repeat gate" flag in eax                 |
| jmp 0041B0CC       | jmp to 0041B0CC                                    |
  • Both load the key value from the stack.
  • Both push it to the top of the stack.
  • Both call B67.

Condition 2:

  • Checks if the key was found (the 'test eax,eax').
  • Then moves the repeat gate flag into eax if it was.

We now have all the pieces.

We know that DI is not intentional, there is no code for handling keys that can trigger repeat in certain menus, like Up > Triangle in New Game/Continue menu, seriously…..

Their is an unused flag, perfectly in sync with when the 200ms wait should be respected.

The conditional in 099 (cmp dword ptr [009A85E4],00) is nonsense, checking if E4 is 0, when it only ever gets set to 50 every, single, time, you enter a menu.

We have the facility to pull inputs from E0 (keys this frame).

We know that from 099 all we need to do is pass a 0 or non zero in eax up the chain of calling functions depending on if a key is found and should be played in game.

We know that in this unique place (099) where the repeat features timing(2C) is kept in sync with keys getting processed when found.

There is a condition that does not get reached that simply calls a function to check if the key is pressed and then leaves ignoring 2C.



2C - repeat gate


As I've already covered the repeat gate being set while E0 is occupied makes little to no sense logically, other than to overcome an incomplete implementation of input repeat in menus. And this is the only place it is ever read from(loaded).

So for the sake of argument lets assume it is only set in 1 scenario, rather than 2. That scenario being the second of its two scenarios mentioned at the beginning of this:

  • E0 (keys this frame) holds no value, 0.
  • D4 (all held keys) holds a value greater than 0.
  • The timer not less than the 'current wait'.

Now we have no inputs from repeatable keys until 200ms have passed while the button(s) are held. But DI still exists.

Lets stick a pin in that.



cmp dword ptr [009A85E4],00 - Condition 1's unyielding gatekeeper


This fella makes no sense, so lets give it a purpose.

Now it checks the repeat is active flag (34).

And for thought experimentation purposes lets just say that condition one is blank (nop'ed). Does nothing, just jumps to the end of the function.

Now we have no inputs from repeatable keys until 200ms have passed while the button(s) are held. AND DI is gone, cant happen, period.

This is even if 2C is permitted to be 1 when E0 is occupied. It is now never loaded by 099 when E0 is occupied, or while the 200ms delay is active. It does not matter while 34 is 0.



Now lets look at condition 1.


Its effectively 3F1. It:

  • Makes a simple call to B67 (check if in 'all keys'(D4))
  • Receives the result
  • Pass it up the chain to the calling function.

2C does not matter to it.

If its call is to B74 (check if key in 'recent keys'(E0)) instead of B67 (check if in 'all keys'(D4)).

And if we change that gatekeeper to check repeat is active flag (34).

There is now inputs on key press for repeatable keys, double input is gone. Repeat after 200ms works like normal, as it should. And there are no undesirable effects. The DI fix.



The fix in place.


Compare to the original 099
0041B099 - aka 099 - check for repeatable key function

2C is of no consequence, so it is ignored and allowed to go on being set and never checked while E0 is occupied.

Only three bytes are changed:
0041B09E = 34 87
0041B0AA = C6

Giving us this version of 099:

| address  | bytes             | code                        | comments                                    |
|----------+-------------------+-----------------------------+---------------------------------------------|
| 0041B099 | 55                | push ebp                    |                                             |
| 0041B09A | 8B EC             | mov ebp,esp                 |                                             |
| 0041B09C | 83 3D 34879A00 00 | cmp dword ptr [009A8734],00 | is 34 (repeat is active flag) equal to 0    |
| 0041B0A3 | 75 0E             | jne 0041B0B3                | jump if not, to 0041B0B3                    |
| 0041B0A5 | 8B 45 08          | mov eax,[ebp+08]            | "load key to be checked" from stack         |
| 0041B0A8 | 50                | push eax                    | push it into the top of the stack           |
| 0041B0A9 | E8 C6FAFFFF       | call 0041AB74               | call "check if in keys this frame" function |
| 0041B0AE | 83 C4 04          | add esp,04                  | shifts stack pointer (ignore)               |
| 0041B0B1 | EB 19             | jmp 0041B0CC                | jmp to 0041B0CC                             |
|----------+-------------------+-----------------------------+---------------------------------------------|
| 0041B0B3 | 8B 4D 08          | mov ecx,[ebp+08]            | "load key to be checked" from stack         |
| 0041B0B6 | 51                | push ecx                    | push it into the top of the stack           |
| 0041B0B7 | E8 ABFAFFFF       | call 0041AB67               | call "check if in all keys" function        |
| 0041B0BC | 83 C4 04          | add esp,04                  | shifts stack pointer (ignore)               |
| 0041B0BF | 85 C0             | test eax,eax                | AND's the eax register                      |
| 0041B0C1 | 74 07             | je 0041B0CA                 | jumps if eax was 0, to 0041B0CA             |
| 0041B0C3 | A1 2C879A00       | mov eax,[009A872C]          | puts the "process keys" flag in eax         |
| 0041B0C8 | EB 02             | jmp 0041B0CC                | jmp to 0041B0CC                             |
|----------+-------------------+-----------------------------+---------------------------------------------|
| 0041B0CA | 33 C0             | xor eax,eax                 | sets eax to 0 .... yea that makes sense \s  |
|----------+-------------------+-----------------------------+---------------------------------------------|
| 0041B0CC | 5D                | pop ebp                     |                                             |
| 0041B0CD | C3                | ret                         | returns to caller                           |



I mentioned an exceptions #2 and never clarified


Reading over everything I realised I never explained exceptions #2. I don't recall if there are other examples but in battles the start button cant trigger DI. Also in battles, when the cursor is on a menu (not on a character or enemy), Square does not trigger DI.

This is because they either pause the game and thus prevent the processing of inputs, DI or otherwise. Or in the case of square, removes the menu, preventing processing of inputs, DI or otherwise.

Maybe not an extensive list of examples of this type of exception. But all I can remember right now. Might work this section into the flow of this doc in a version 2.

Return to:
0041B099 - aka 099 - check for repeatable key function



Detecting Cheaters in speed running


It should be very obvious how to go about detecting cheaters if concerned. Enforce input overlays and/or controller cams, and if a vod is suspected to have the fix applied, watch for DI where there should be DI. Rip vods, watch in slow motion. It is very detectable as long as the run benefited from the fix and even when it doesn't, its just a case of spotting it. And if it was used but the run did not benefit from it, it honestly doesn't matter now does it.

Hiding the use of di-fix will require such meticulous razor thin edits in parts of the vods where menuing is taking place to hide visual and audio ques of inputs that it honestly would be simpler to just splice in a whole menuing segment perfectly executed and changing the HP, MP, XP, AP, etc values. Which is its own problem I imagine. From where I'm sitting, if someone wanted to cheat and hide it, there are paths of less resistance with better benefits than using the di-fix.

These two clips (thank you zeg), while not a benefit per se because they are of a technique that relies on double input but causes delays ordinarily. Once the input overlay is understood its clear that a doubled circle would have occurred when the arrow is pressed.

When pressing left to cursor over 'Change':

Before the first command is fully confirmed:

So di-fix during an active repeat feature is very noticeable and probably undesirable in the case of this technique.

I can expand this section later if people ask. But I expect that those that read through this doc and have a vested interest in keeping cheaters out of the speed running community, already have tools and methods for analysis that they are familiar with. Leveraging those methods to detect DI fix where no DI fix is allowed should be trivial to them.

But I am always available for questions in case I failed to fully paint the picture in a way such methods require.

Date: 2024-07-01 Mon 20:04

Created: 2024-07-28 Sun 17:42

Validate