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)


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.


  • Advertises that a game has started
  • Transfers match parameter information


NameTypeSize (bytes)Description
Stage IDint2Indicates the stage the match is being played on
PlayersPlayerParams[]n * 4Array of player parameters. One for each character in game


NameTypeSize (bytes)Description
Port IDint1The port this player is using. This value is zero-indexed so port 1 = 0, port 2 = 1, etc
External Character IDint1Indicates the character ID of the character chosen for the match
Player Typeint10 for human player, 1 for CPU player
Costume IDint1ID number for the color costume chosen. 0 is default


This event is fired once for every non-freeze frame during the game.


  • Advertises all changes in game state


NameTypeSize (bytes)Description
Frame Countint4Number of frames into the match
Random Seedword4Random number used for calculating random events, required for replays
PlayersPlayerUpdate[]n * 57Array of player updates. One for each character in game


NameTypeSize (bytes)Description
Internal Character IDint1Indicates the character ID of the current character. This value should be constant for all characters except Zelda/Sheik
Action State IDint2Current action state ID of the character. Also known as animation ID
X Coordinatefloat4X position of the character on stage
Y Coordinatefloat4Y position of the character on stage
Stocksint1Number of stocks remaining
Percentfloat4Current percentage
Shield Sizefloat4Current size of shield
Last Move Connected IDint1ID of move last connected with
Combo Countint1The game's true combo count
Player Last Hit By IDint1ID of player to last hit this player
Joystick Xfloat4X position of the joystick used by the game
Joystick Yfloat4Y position of the joystick used by the game
C-Stick Xfloat4X position of the c-stick used by the game
C-Stick Yfloat4Y position of the c-stick used by the game
Triggerfloat4The analog trigger value used by the game. The game only uses the value of the trigger that is most pressed down
Buttonsword4The 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
PhysicalButtonshalf-word2These bits map better to physical button presses (1 button = 1 bit). Used for APM calculation
L-Triggerfloat4The value of the l-trigger. Used by APM calculation
R-Triggerfloat4The value of the r-trigger. Used by APM calculation


This event is fired on the first frame of the results screen.


  • Advertises that a game has ended


NameTypeSize (bytes)Description
Win Conditionint1Currently 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

Match LengthintframesTotal number of frames of the match
PlayersPlayerStats[]-Statistics for individual players

Player Stats

APMfloatactions/minuteActions per minute over the course of the match
Average Distance from CenterfloatdistanceHow far away the player is from 0,0 on average
Percent Time Closest CenterfloatpercentPercentage of time this player is closer to 0,0 than the opponent
Percent Time Above OthersfloatpercentPercentage of time this player is above the opponent
Percent Time in ShieldfloatpercentPercentage of the total match time this player is in shield
Seconds Without DamagefloatsecondsMost amount of time the player went without taking any damage
Roll CountintrollsAmount of times this player rolled
Spot Dodge Countintspot dodgesAmount of times this player spot dodged
Air Dodge Countintair dodgesAmount of times this player air dodged (includes wavedashes)
Recovery AttemptsintrecoveriesAmount of recoveries attempted
Successful RecoveriesintrecoveriesAmount of recoveries succeeded
Edgeguard ChancesintedgeguardsAmount of edgeguards attempted
Edgeguard ConversionsintedgeguardsAmount of edgeguards converted to kills
Number of OpeningsintopeningsNumber of openings found on opponent
Average Damage Per StringfloatdamageAmount of damage done on average per combo string
Average Time Per StringfloatframesAverage duration of combo strings
Average Hits Per StringfloathitsAverage number of hits per combo string
Most Damage StringfloatdamageMost amount of damage done with a single combo string
Most Time StringintframesMost amount of time a single combo string lasted
Most Hits StringinthitsMost amount of hits with a single combo string
StocksStockStats[]-Statistics for individual stocks

Stock Stats

Time SecondsfloatsecondsTime in seconds that this stock lasted
PercentfloatdamageDamage at which this stock ended
Move Last Hit Byintmove idMove ID of the opponent's move that last hit this player on this stock
Last Animationintanimation idLast animation ID of this stock (intended to determine death direction but actually kind of useless atm)
Openings AllowedintopeningsNumber of openings given up to the opponent on this stock
Is Stock Lostbool-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

Flowchart for detecting combo strings and openings

Flowchart for detecting combo strings and openings


Flowchart for detecting recoveries

Flowchart for detecting 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?”.


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.


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.


Stick regions

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.


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.


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!