Site banner

Implementing Jump Cancels in MUGEN with Variable Expansion

tl;dr

If you just want to use this yourself (Currently MUGEN 1.0+ only, and not tested to work on 1.0 yet):

  1. Download this example KFM edit.
  2. Copy the files Supernull.st, LoadASM10.bin, LoadASM11a4.bin, LoadASM11b1.bin into your char's folder
  3. Edit your character's .def to have "st = Supernull.st". Move your characters other state file(s) as stx files, i.e. st0, st1, etc.
  4. Add the following lines to the bottom of your .cmd file:
    [State -1, Air Jump Cancel]
    type = ChangeState
    value = 45 + (Var(-809) := Var(-809)-1)*0
    trigger1 = var(-809) > 0
    trigger1 = command = "tapup"
    trigger1 = stateno = 600 || stateno = 630 ;replace with air states to jump cancel
    trigger1 = movecontact
    
    [State -1, Ground Jump Cancel]
    type = ChangeState
    value = 40 + (Var(-809) := const(movement.airjump.num))*0
    trigger1 = command = "holdup"
    trigger1 = stateno = 200 || stateno = 230 ;replace with ground states to jump cancel
    trigger1 = movecontact
    
    And this at the bottom of the "Hold Dirs" command section (right below "holddown"):
    [Command]
    name = "tapup"
    command = $U, >~$U
    time = 10
    
    Performing an air jump cancel will require a flick upwards (Hold for <10 frames). The reason for this is explained in the full writeup.

Preface

A few days ago, one of my friends was complaining about how jump cancels are a pain to implement in MUGEN. This is because directly changing state to a prejump does not modify the amount of air jumps remaining, which causes various issues, not limited to but including:

MUGEN also doesn't provide an interface to modify/view the amount of air jumps directly. Considering these limitations, he (along with most other MUGEN creators, as far as I know) have settled on forgoing the usage of MUGEN's builtin airjump system entirely and reimplementing it using variables in order to have a more flexible interface.

I realized however that there may be another way that is simpler for creators to use. While MUGEN does not provide an interface to modify the amount of air jumps directly, it's possible to create our own interface through Variable Expansion[1].

What is Variable Expansion?

What variable expansion does is remove the 0-59 range limitation on variable references (Like Var(1)). Because player variables are stored in the same data structure as other inaccessible player data, being able to index out of bounds allows us to read/write to otherwise unexposed player data, including the amount of air jumps remaining. Enabling this requires the usage of some exploits, which is what the state file and the binary files mentioned in the tl;dr are for. For more details, see the appendix below; I felt it was unnecessary to cover the implementation in detail as part of the main writeup because I didn't write the exploit myself.

However, this doesn't mean that all of the work was done for me. As mentioned before, variable expansion just gives us the means to write out of bounds. I still needed to write the jump cancel state. And before that, I actually needed to figure out where the amount of air jumps was located in the player data structure.

Finding where air jumps are stored

As mentioned before, MUGEN stores all player data, including variables, in a large data structure. Indexing outside of the bounds of the array of variables gives us a relatively much more convenient [2] way of accessing other values in this data structure.

Unfortunately, while there is a resource for player offsets for WinMUGEN that provides this offset (and even a column for the variable to index if using variable expansion), there is no such resource for MUGEN 1.0+. So, I had to first find the offset myself.

To do this, I opened up MUGEN 1.1b and started up a game in training mode, and attached a debugger (x64dbg, though Cheat Engine could have also worked) to the MUGEN process.

I then needed to go to the player data structure in the debugger's memory dump. Fortunately, while I lacked the offset for air jumps, a MUGEN debugger written by Ziddia has an offset database for MUGEN 1.1 with most other relevant offsets.

To get to the player data structure, I first had to find where the MUGEN gamestate data structure was located in memory. The aforementioned database states that a pointer to this data structure is always provided at 0x5040E8.

Following that pointer to the base address of the game state, I then had to find the base address of the player data structure. The database states that the offset from the game state for the data structure is 0x12278, so adding this number to the game states' address will bring us to another pointer containing the address of the data structure for player 1's data.

Fortunately, in x64dbg that whole process can be simply jumped to as an expression: [[5040E8]+12278]

Now that I had the right area of memory to look at, I just did a bunch of jumping around in MUGEN as KFM while scrolling through the memory region until I found a number that changed as I did that to the values I was expecting (0 or 1, as KFM has 1 air jump). Eventually I found where the value was located in memory.

Translating the memory location for variable expansion

WARNING: The following paragraph is a word salad that I really need to rephrase to be more readable, due to consantly referencing both var array-relative offsets and player data-relative offsets.

However, since we're using variable expansion, I needed to convert this memory address into a Var() index. To do this, I first needed to find what the memory offset of the air jump value is relative to the base address of the var array. The aforementioned database has the offset for the var array relative to the player data structure as 0xF1C, so if I subtracted the player data structure offset for air jumps from this, I would get the var-relative offset I needed. I found that the player data offset for airjumps was 0x278, so 0xF1C - 0x278 = 0xCA4, or in decimal 3236 bytes prior. Since vars are stored as integers which take up 4 bytes each, our var index should be the byte offset divided by 4 to get the right memory value, which finally gets us the magic variable number we need of var(-809).

Writing the jump cancel state using Variable Expansion

Fortunately, now equipped with knowledge of the magic variable number, writing the jump cancel states is extremely simple. We just put this inside our cmd file:

[State -1, Air Jump Cancel]
type = ChangeState
value = 45 + (Var(-809) := Var(-809)-1)*0
trigger1 = var(-809) > 0
trigger1 = command = "tapup"
trigger1 = stateno = 600 || stateno = 630 ;jump_x or jump_a
trigger1 = movecontact

[State -1, Ground Jump Cancel]
type = ChangeState
value = 40 + (Var(-809) := const(movement.airjump.num))*0
trigger1 = command = "holdup"
trigger1 = stateno = 200 || stateno = 230 ;stand_x or stand_a
trigger1 = movecontact

If you're familiar with writing MUGEN states, this should all be fairly intuitive, we just access the air jump count like any other variable using var(-809). The one bit that may be confusing is the assignments to var(-809) inside of the value field. This is done because I had found no other way of modifying the var after the triggers had been processed without having to use a variable, which I wanted to avoid at all costs since the point of this all is a simpler solution, which involves using less variables.

You may notice however that the air jump cancel state uses a "tapup" command instead of "holdup". "tapup" is not a default command, but rather another command I wrote for this, placed below the hold commands:

[Command]
name = "tapup"
command = $U, >~$U
time = 10

This is because if holdup is used for the air jump cancel, it produces the undesirable effect that a jump cancel is automatically done if the player is holding up as they hit an air normal, rather than having to deliberately press up afterwards.

You may wonder why the command isn't just simply $U, and the reason for that is that for some reason when using $U, going from straight up to up-right counts as two separate tap inputs. Doing $U, >~$U avoids this, however it does introduce its own undesirable behavior: the stick must be flicked up for the jump cancel, as the tap input will only be processed on the release. Also, if you wiggle the stick holding up, that will also trigger a jump cancel, though this is hard to do on accident. While this implementation is still flawed, I've found nothing better so far. I may try figuring out something better in the future; if you've found one better yourself let me know. If this behavior really bothers you, it's possible to do a more correct solution yourself using a variable to track if the stick was held prior.

Conclusion, and further plans

While the imperfect tapup implementation is annoying, that issue is inherent to writing jump cancels even without variable expansion, and I think that this approach still significantly reduces the complexity for authors. Instead of having to reimplement the air jump system yourself and use up multiple variables solely for that purpose, an interface close to the ideal is provided instead. I also learned a lot from doing this, in particular about the MUGEN game data structure and MUGEN 1.1 offsets. I also learned a fair bit about how to write a variable expansion exploit, as that was my original plan before I decided to just use Ziddia's 1.0+ code.

However, there is still some more work that could be done. Aside from the aforementioned resolving the command issue, at the moment I've only figured this out for MUGEN 1.0+, as WinMUGEN requires both its own variable expansion payload and has a different var() offset. If I implement it for WinMUGEN, it will likely be its own article due to the majority of the work being writing a variable expansion payload rather than making use of it. I also haven't actually tested the code on MUGEN 1.0 yet, only 1.1b. 1.0 may have air jumps at a different var offset, which would break the code. I haven't tested it yet since AFAIK nobody really uses 1.0.


Appendix: How is variable expansion implemented?

For an in-depth explanation, refer to the documentation provided in Z-Yuuka 2nd's code. Not only is it documenting the exact same code I use, but Ziddia's explanation is far better than what I could write.

In summary, variable expansion requires us to remove the code from MUGEN that prevents us from indexing out-of-bounds. However, MUGEN obviously doesn't provide us a means to modify this. Instead, a buffer overflow exploit in the AssertSpecial flag parameter is used to initiate a series of further exploits that allow us to gain write access to the code, and then from there we modify the code to remove the out of bounds checks (Along with some other features that don't matter for our use case).

Footnotes

[1] I've also seen this technique sometimes referred to as "Fake Variable Expansion" or "Var Range Removal", but Variable Expansion seems the most common term.

[2] Elaboration: The alternative to doing variable expansion would be writing a pure assembly exploit to read/write to the air jump count. This would introduce the following additional complexities: