Introduction
Part 1 of this series outlined the journey from idea to trial run for implementing statistics in Melee. This part will focus more on the data being extracted out of the game and how that data is converted into complex statistics. This entry is much more documentation than it is article. I realize that there’s no great love for documentation in general but hopefully the topic is interesting enough that people enjoy at least looking at it.
The Raw Data
The game stores information galore in the console’s internal memory. The state of the memory is constantly changing as the game progresses and to get relevant data the memory must be read at exactly the right time. Three events were identified as critical to best understand game state. These events will be referred to as OnStart, OnFrameUpdate, and OnEnd.
In order to read memory, code must be added to the game and an injection point must be found that serves the need required. In this case, the need is to service the three events mentioned. A single injection point was found that can service all three events. The injection point is at the end of a function that runs once per frame per player either while a game is in progress or during the results screen.
Pre-Transfer Checks
Given there is a single injection point, some checks must be made at the start of the injected function that determine both if and what data should be transferred. Under the following conditions, no data will be transferred:
- There are more than two players in the game (statistics capturing is currently not supported for anything other than singles)
- The game is currently in single-player mode (training mode, story mode, etc)
- This is not the last character update for this frame (data should only be transferred once per frame, if this condition was not here data would be transferred twice per frame)
OnStart
This event is fired on the first freeze-frame of a game. The freeze frames are the ready-go frames at the start of the game where players have no control over their character.
Purpose
- Advertises that a game has started
- Transfers match parameter information
MatchParams
Name | Type | Size (bytes) | Description |
---|---|---|---|
Stage ID | int | 2 | Indicates the stage the match is being played on |
Players | PlayerParams[] | n * 4 | Array of player parameters. One for each character in game |
PlayerParams
Name | Type | Size (bytes) | Description |
---|---|---|---|
Port ID | int | 1 | The port this player is using. This value is zero-indexed so port 1 = 0, port 2 = 1, etc |
External Character ID | int | 1 | Indicates the character ID of the character chosen for the match |
Player Type | int | 1 | 0 for human player, 1 for CPU player |
Costume ID | int | 1 | ID number for the color costume chosen. 0 is default |
OnFrameUpdate
This event is fired once for every non-freeze frame during the game.
Purpose
- Advertises all changes in game state
FrameUpdate
Name | Type | Size (bytes) | Description |
---|---|---|---|
Frame Count | int | 4 | Number of frames into the match |
Random Seed | word | 4 | Random number used for calculating random events, required for replays |
Players | PlayerUpdate[] | n * 57 | Array of player updates. One for each character in game |
PlayerUpdate
Name | Type | Size (bytes) | Description |
---|---|---|---|
Internal Character ID | int | 1 | Indicates the character ID of the current character. This value should be constant for all characters except Zelda/Sheik |
Action State ID | int | 2 | Current action state ID of the character. Also known as animation ID |
X Coordinate | float | 4 | X position of the character on stage |
Y Coordinate | float | 4 | Y position of the character on stage |
Stocks | int | 1 | Number of stocks remaining |
Percent | float | 4 | Current percentage |
Shield Size | float | 4 | Current size of shield |
Last Move Connected ID | int | 1 | ID of move last connected with |
Combo Count | int | 1 | The game's true combo count |
Player Last Hit By ID | int | 1 | ID of player to last hit this player |
Joystick X | float | 4 | X position of the joystick used by the game |
Joystick Y | float | 4 | Y position of the joystick used by the game |
C-Stick X | float | 4 | X position of the c-stick used by the game |
C-Stick Y | float | 4 | Y position of the c-stick used by the game |
Trigger | float | 4 | The analog trigger value used by the game. The game only uses the value of the trigger that is most pressed down |
Buttons | word | 4 | The calculated button presses used by the game. For this value, one button press can actually equate to multiple bits being set. For example, z sets 3 different bits |
PhysicalButtons | half-word | 2 | These bits map better to physical button presses (1 button = 1 bit). Used for APM calculation |
L-Trigger | float | 4 | The value of the l-trigger. Used by APM calculation |
R-Trigger | float | 4 | The value of the r-trigger. Used by APM calculation |
OnEnd
This event is fired on the first frame of the results screen.
Purpose
- Advertises that a game has ended
MatchSummary
Name | Type | Size (bytes) | Description |
---|---|---|---|
Win Condition | int | 1 | Currently only supports two values. 0 = rage quit, 3 = anything else. Will likely add specific values for timeout, tie, and win by stock in the future |
The Statistics
The above section defined all the data being passed out from the game. At this point, something must receive and do something with it. In the current architecture, this is the task of the microprocessor. This section will define how the raw data is converted to interesting statistics.
Match Stats
Name | Type | Units | Description |
---|---|---|---|
Match Length | int | frames | Total number of frames of the match |
Players | PlayerStats[] | - | Statistics for individual players |
Player Stats
Name | Type | Units | Description |
---|---|---|---|
APM | float | actions/minute | Actions per minute over the course of the match |
Average Distance from Center | float | distance | How far away the player is from 0,0 on average |
Percent Time Closest Center | float | percent | Percentage of time this player is closer to 0,0 than the opponent |
Percent Time Above Others | float | percent | Percentage of time this player is above the opponent |
Percent Time in Shield | float | percent | Percentage of the total match time this player is in shield |
Seconds Without Damage | float | seconds | Most amount of time the player went without taking any damage |
Roll Count | int | rolls | Amount of times this player rolled |
Spot Dodge Count | int | spot dodges | Amount of times this player spot dodged |
Air Dodge Count | int | air dodges | Amount of times this player air dodged (includes wavedashes) |
Recovery Attempts | int | recoveries | Amount of recoveries attempted |
Successful Recoveries | int | recoveries | Amount of recoveries succeeded |
Edgeguard Chances | int | edgeguards | Amount of edgeguards attempted |
Edgeguard Conversions | int | edgeguards | Amount of edgeguards converted to kills |
Number of Openings | int | openings | Number of openings found on opponent |
Average Damage Per String | float | damage | Amount of damage done on average per combo string |
Average Time Per String | float | frames | Average duration of combo strings |
Average Hits Per String | float | hits | Average number of hits per combo string |
Most Damage String | float | damage | Most amount of damage done with a single combo string |
Most Time String | int | frames | Most amount of time a single combo string lasted |
Most Hits String | int | hits | Most amount of hits with a single combo string |
Stocks | StockStats[] | - | Statistics for individual stocks |
Stock Stats
Name | Type | Units | Description |
---|---|---|---|
Time Seconds | float | seconds | Time in seconds that this stock lasted |
Percent | float | damage | Damage at which this stock ended |
Move Last Hit By | int | move id | Move ID of the opponent's move that last hit this player on this stock |
Last Animation | int | animation id | Last animation ID of this stock (intended to determine death direction but actually kind of useless atm) |
Openings Allowed | int | openings | Number of openings given up to the opponent on this stock |
Is Stock Lost | bool | - | Indicates whether this stock was lost |
Complex Statistics
Some statistics require a bit more effort to extract. The statistics in this section are computed via monitoring the game state changes in specific ways to garner interesting information.
Combo Strings and Openings
Recoveries
Actions Per Minute (APM)
APM is a metric that indicates how fast a player is. It is what it says it is: actions per minute, how many actions are made every minute. The question to answer here is “what is an action?”.
Buttons
Button presses are simple, every time a button is pressed, the action count is incremented. The action count is not incremented when a button is released.
Sticks
Melee, however, is very much an analog game and converting the analog inputs of the game to actions is a bit more subjective. Kadano came up with a method that was both simple and effective to calculate the actions made using the control stick.
The image above divides the sticks into nine individual regions. The exact positional values of the regions are defined by the following:Northeast: x >= 0.2875 and y >= 0.2875
Southeast: x >= 0.2875 and y <= -0.2875
Southwest: x <= -0.2875 and y <= -0.2875
Northwest: x <= -0.2875 and y >= 0.2875
North: y >= 0.2875
East: x >= 0.2875
South: y <= -0.2875
West: x <= -0.2875
Dead zone: anything else
Whenever a player moves the stick from one region to another, the action count is incremented. The only exception to this rule is when transitioning back to the dead zone region. Any transition from a region to the dead zone region does not count as an action. This is done such that releasing the stick does not count as an action.
Triggers
The last remaining analog inputs are the triggers. These are a little trickier and some improvement could definitely be made here. Currently the only action counted is when the stick crosses the light shield threshold on the way down. Remember that at the bottom of the analog portion of the trigger is a digital button press, this press is already taken into account as described by the buttons section. This means that this approach has a few problems:
- When a player slams a trigger all the way down from neutral, it counts as two actions
- When a player changes from a full shield to a light shield, or does any adjustment to the light shield amount, it does not count as an action
Suggestions are welcome to improve this metric.
Closing
Once again I’d like to thank everyone that helped me along with this project. The full list can be found in part 1 of the series.
This document has compiled a list of what has been done so far. There is much more work to be done and as mentioned in the last section, suggestions are welcome.
Look forward to an analysis of all the data collected at HTC Throwdown written by someone you should all know quite well in part 3!
Great write up !
For the trigger problem, is it possible to write à rule such as “if the digital button is pressed directly one frame after the analog, then it count as one action”? When you slam the trigger it’s pretty much always at maximum speed
Yeah I guess a timer between the analog input to the digital input could work. I dunno if it’d be one frame though, might take longer than that to travel from > 0.3 to digital.
Do you have any information about the addresses of the functions you’re using as benchmarks for when certain checks are made?