A tutorial mini-site by Choon_2i
Based roughly on "Simply FPU" by Raymond Filiatreault
Prerequisite Knowledge
Throughout this section, I will be using certain characters
(SOMETHING)?indicates thatSOMETHINGcan be omitted.
Internal Notes
State Processing/Branching
Whenever a player is processed, a small flowchart is referred to for determining which state to execute.
- If the player isn’t a helper or it has helpertype=player, as long as the player isn’t thrown, execute Def -3.
- If the player isn’t a helper or it has helpertype=player, execute Def -2.
- If the player has keyctrl=1 (non-helper players have this by default), execute Def -1.
- Execute the Def corresponding to the player’s current stateno. Whenever a state branch occurs, repeat this step. If this step happens 2500 times consecutively, the game will crash.
- If the player has hitpausetime<=0, stateno=140, and statetime>=animlength, ChangeState to a statetype-dependent state and return to step 4※S sends to 0. C sends to 11. A sends to 51. Any other statetype will not branch, but the engine will still treat it as if it did, which can result in an infinite loop if not handled..
- If the player has physics=A and they’re about to hit the ground, ChangeState to 52 and return to step 4※As you’d expect, this can result in an infinite loop if physics isn’t changed in Def 52, or if Def 52 doesn’t exist..
“Branching” refers to the process of changing one’s state, or restarting state processing somehow. Using only standard state controllers, there are either 2 or 4 ways to execute it, depending on version, which are:
- ChangeState, which changes the user’s stateno to the
valueparameter and prevstateno to their stateno before execution and anim/ctrl to their respective parameters if specified, in addition to turning them if certain conditions are met. - SelfState, which does the above and frees the user from any throws they may be subject to.
- TagOut, a WinMUGEN-exclusive state controller which does nothing else except set the user’s standby to 1.
- TagIn, a WinMUGEN-exclusive state controller which does nothing else, unless there’s a partner, in which case it will set the partner’s standby to 0, change the user’s stateno to the
statenoparameter and their partner’s stateno to thepartnerstatenoparameter, and turn the partner to match the user’s facing value※Most current nuke tournaments are 1v1, meaning this is only relevant in certain niche cases. Team battles were slightly more common in the past, so you’ll see code in older nukes, like ONI-MIKO-X, that relies on TagIn for state callback..
IgnoreHitPause, Persistent, and the 512 State Controller Limit
There are 2 non-expressive※The parser will only read the first number present after persistent= parameters that every state controller can have, which are ignorehitpause and persistent. When ignorehitpause is omitted/0 and the player’s hitpausetime value is greater than 0, that state controller is completely ignored. In all other cases, it’s executed as normal.
persistent is a parameter that allows a state controller to be executed every Nth time until a state is branched to, or just once if N happens to be 0. It defaults to 1 if unspecified. This is implemented as each controller having a byte corresponding to it that stores its “persistence” value within player memory. The flowchart of processing these bytes is as follows:
- If the persistence byte is non-zero, skip to step 4.
- If all
triggeralls (if they exist) are truthy, and all of anytrigger#are also truthy, execute the state controller. Otherwise, skip to step 4. - Set the persistence byte to the controller’s
persistentvalue※As this is a byte, the value written is actually (value&255)., or 1 if the value is zero. - If a state branch occurred, ignore steps 5-6.
- If the controller’s
persistentvalue is non-zero, decrease the value of the persistence byte by 1. - If the resulting byte is less than 0, set it to 0.
Because there are only 512 persistent bytes allocated, there is a maximum of 512 state controllers that can be used per state, as any more would result in unintended memory regions being modified by the persistence system. This check is performed on initially branching to a state without hitpausetime (in other words, if (hitpausetime<=0 && time=0)), right as every byte is cleared. In other words, if this check isn’t performed, you can have more than 512 state controllers in a state. Exploiting this oversight is referred to as “State Controller Overflow,” “Null Overflow※As it’s commonly accomplished by using the Null state controller, which does nothing on execution.,” and “512 Overflow.”
By extension, since persistent bytes aren’t reset when a character has hitpausetime and state changes don’t decrement their respective persistence byte, keeping a state branch’s persistence byte at 0 is incredibly important to prevent weird inconsistencies involving state controllers simply not being executed at all. Since persistent=0 will still write 1, persistent=256 is used to accomplish this※(256&255) = 0, and since 256 != 0, it will properly write 0.
Since states -3~-1 are meant to (almost) always be executed, they do not※(except for when your StateNo happens to be one of these special values) rely on any persistence-based system, and are exempt from this limit.
HitAttr
This lists the types of attacks belonging to a particular HitDef/Projectile, reversible by a particular ReversalDef, a character can/can’t be hit by when a HitBy/NotHitBy is executed, and are overriden by a particular HitOverride. These are written in the format of PRIMARY,SECONDARY.
The primary field can be any combination of the following:
S, which indicates Standing attacks.C, which indicates Crouching attacks.A, which indicates Aerial attacks.
The secondary field can be any comma-delimited combination of the following:
NA,SA, andHA, which indicates physical Attacks used during a Normal, Special, and Hyper move respectively.NT,ST, andHT, which indicates Throws used during a Normal, Special, and Hyper move respectively.NP,SP, andHP, which indicates Projecties used during a Normal, Special, and Hyper move respectively.AA, which is equivalent toNA,SA,HA.AT, which is equivalent toNT,ST,HT.AP, which is equivalent toNP,SP,HP.N, which is equivalent toNA,NT,NP.S, which is equivalent toSA,ST,SP.H, which is equivalent toHA,HT,HP.A, which, due to a programming error, is equivalent toH.
When 2 HitAttrs need to interact with one another for whatever reason, if both attrs can “mask each other,” the interaction is considered a success. In other words, if the combination of a component in each field is present in both attrs, it’s successful.
For example, a HitOverride with attr S,NA will override Projectiles with attrs SC,NA and SCA,AA,AT,AP because each of the latter contains both S in their primary fields and NA in their secondary fields, but not A,NA or S,NT as one field contains something missing from the other.
Similarly, a ReversalDef with reversal.attr SCA,AA,AT will reverse HitDefs with attrs C,HA and S,AA,AT because each field is contained within the former, but not SCA,AP because the ,AP component is not.
Because of this masking property, if either field is empty, any interaction relying on attrs will always fail. Keeping the primary field empty is the mechanism behind “unreversible HitDefs” and is part of the reason why NotHitBy with a value of SCA is actually optimal.