Skip to content

Extracting input data from ghosts

Petr Pivoňka edited this page May 26, 2024 · 31 revisions

Replay and ghost contain validation data that is used to, obviously, validate the run - to automatically check if the physics engine hasn't been modified before or during the run.

This automatic validation method does not work for slow motion, so in these cases, you would rather wanna see the inputs visually to see if they are humanly doable. Or, you just want to know how people drive certain tracks or utilize the input list in different ways.

Extracting inputs from TMU/TMNF/TMUF/TM2 ghosts

Inputs in these games are stored in the object of the CGameCtnGhost class. If you're working with a replay, you need to get to the ghost object first.

The inputs can be seen inside Inputs collection. Each element is an IInput representing a change of state. The new API added in 1.1.3 offers strongly-typed structs and you can differentiate them with pattern matching (see example below).

Here is a list of all the possible input (struct) names for these games:

  • FakeDontInverseAxis [IInputState] - Likely corresponds to the setting to invert the input directions.
  • FakeIsRaceRunning [IInputState] - Tells the point in time where the acceleration/brake becomes active.
  • FakeFinishLine [IInputState] - Tells the point in time where the brake becomes active and acceleration inactive.
  • Accelerate [IInputState] - Digital acceleration aka the Up key (true/false means on/off).
  • AccelerateReal [IInputReal] - Analog acceleration (range 0-1).
  • Brake [IInputState] - Digital brake aka the Down key (true/false means on/off).
  • BrakeReal [IInputReal] - Analog brake (range 0-1).
  • Gas [IInputReal] - Analog acceleration/brake, where 1 is full acceleration and -1 is full brake.
  • Horn [IInputState] - Horn key aka Num 0 by default.
  • Respawn [IInputState] - Respawn key aka Enter/Backspace by default.
  • Steer [IInputSteer + IInputReal] - Analog steering.
  • SteerOld [IInputSteer] - Old variant of Steer.
  • SteerLeft [IInputState] - Digital left steering aka the Left key (true/false means press/release).
  • SteerRight [IInputState] - Digital right steering aka the Right key (true/false means press/release).

You may know different names of these states, but the ones above are derived from the official names. Personal choice was minified.

Inputs of analog gas is tracked precisely, but the game always represents acceleration as either 0 (off) or 1 (on) in the physics engine.

There are two types of steers - Steer and SteerOld. They are wrapped under IInputSteer (which also wraps TM2020 input steer) so it is recommended to use IInputSteer for steer filtering.

This is the rough approach to go about reading inputs:

var replay = Gbx.ParseNode<CGameCtnReplayRecord>("Path/To/My/Replay.Replay.Gbx");

// Taking just the first ghost makes thing cleanerfor this example
var ghost = replay.GetGhosts().FirstOrDefault();

if (ghost is null)
{
    throw new Exception("No ghost found.");
}

if (ghost.Inputs is not null)
{
    // Loop through all inputs
    foreach (IInput input in ghost.Inputs)
    {
        // Do something with specific inputs

        // Pattern matching via switch
        switch (input)
        {
            case Accelerate accel:
                var pressed = accel.Pressed;
                break;
            case IInputSteer steer:
                var pureSteer = steer.Value; // represents -65536 to 65536 steer range
                var normalizedSteer = steer.GetValue(); // represents -1 to 1 steer range
                break;
        }

        // Pattern matching via if statement
        if (input is Respawn respawn)
        {
            // special Respawn logic
        }

        // Advanced pattern matching
        if (input is IInputSteer { Value < 10000 } steerUnder10000)
        {
            // something
        }
        
        // Print out the input string representation
        Console.WriteLine(input.ToString());
    }
}

The inputs are pretty much always ordered by timestamp, but it's something to not completely rely on. In older games, this may be much less often the case.

You can also filter inputs with handy LINQ methods like OfType<T>().

The ghost also has a member called EventDuration which tells the length of the captured period of input events. It is presented in TimeInt32 format.

Extracting inputs from TM1.0/TMO/TMS/TMNESWC replays

Inputs in these games are stored in the object of the CGameCtnReplayRecord class.

Getting the inputs from these kinds of replays is almost exactly the same as in TMU/TMUF/TM2, except you don't access the CGameCtnGhost.Inputs, but the CGameCtnReplayRecord.Inputs.

var replay = Gbx.ParseNode<CGameCtnReplayRecord>("Path/To/My/Replay.Replay.Gbx");

if (replay.Inputs is not null)
{
    // Loop through all inputs
    foreach (IInput input in replay.Inputs)
    {
        // For filtering options, see code example above

        // Print out the input string representation
        Console.WriteLine(input.ToString());
    }
}

If the replay has multiple ghosts inside, there's still only one member to use to store inputs. This has changed since TMU due to its flaws.

Extracting inputs from TM2020 replays

Since GBX.NET 1.1.0, the CGameCtnGhost 0x01D chunk has been a major focus and many behaviors of TM2020 inputs have been recognized.

It is possible to read inputs from all sorts of runs since GBX.NET 1.1.1.

Not all TM2020 kinds of inputs may be supported at the moment - the focused version at the moment is _2020_07_20. All runs before this date have a high chance of being unstable.

The focused property is the CGameCtnGhost.PlayerInputs (class PlayerInputData).

The storage of data is tick-based. The number of counted ticks can be seen with the Ticks property, which is used to determine the correct length of data, as the data is stored via bits, not bytes.

It is worth noting that Trackmania 2020 also has a player type called CharacterPilot which has its own dedicated inputs, which on the other hand vehicle doesn't use.

Inputs approach

You can read the inputs with the Inputs property.

Here is a list of all the currently recognized input (struct) names:

  • SteerTM2020 [IInputSteer] - Steer represented from -127 to 127. [Vehicle only]
  • Accelerate [IInputState] - [Vehicle only]
  • Brake [IInputState] - [Vehicle only]
  • Strafe [IInput] - None, Left, or Right [CharacterPilot only]
  • Walk [IInput] - None, Forward, or Backward [CharacterPilot only]
  • Vertical [IInput] - Unknown (values: 0, 1, 3) [CharacterPilot only]
  • Horizontal [IInput] - Unknown (values: 0, 1, 3) [CharacterPilot only]
  • MouseAccu [IInput] - Some form of mouse position presented in subpixels.
  • Horn [IInputState] - Horn key aka Num 0 by default. [Vehicle only]
  • GunTrigger [IInputState] - [CharacterPilot only]
  • Action [IInputState] - [CharacterPilot only]
  • Camera2 [IInputState] - [CharacterPilot only?]
  • Jump [IInputState] - [CharacterPilot only?]
  • FreeLook [IInputState] - [CharacterPilot only?]
  • ActionSlot [IInputState] - 0 to 9 where with the latest version with action keys:
    • 1 -> 2
    • 2 -> 4
    • 3 -> 6
    • 4 -> 8
    • 5 -> 0
  • RespawnTM2020 [IInput] - Does not include key release event, one-way press.
  • SecondaryRespawn [IInput] - Does not include key release event, one-way press.

Example:

var replay = Gbx.ParseNode<CGameCtnReplayRecord>("Path/To/My/Replay.Replay.Gbx");

// Taking just the first ghost makes thing cleaner
var ghost = replay.GetGhosts().FirstOrDefault();

if (ghost is null)
{
    throw new Exception("No ghost found.");
}

// There could be multiple sets of inputs, but I haven't seen more than one just yet
var playerInputs = ghost.PlayerInputs.FirstOrDefault();

if (playerInputs is null)
{
    throw new Exception("No player inputs found.");
}

var startOffset = playerInputs.StartOffset ?? TimeInt32.Zero;

var tempGas = default(bool?);
var tempBrake = default(bool?);
var tempHorn = default(bool?);

foreach (IInput input in playerInputs.Inputs)
{
    // Do something with specific inputs

    // NOTE: StartOffset is not currently included in IInput structs and needs to be adjusted
    // This is no longer needed since 1.2.0
    var timestamp = input.Timestamp + startOffset;

    // Pattern matching via switch
    switch (input)
    {
        case Accelerate accel:
            var pressed = accel.Pressed;
            break;
        case IInputSteer steer:
            var pureSteer = steer.Value; // represents -65536 to 65536 steer range
            var normalizedSteer = steer.GetValue(); // represents -1 to 1 steer range
            break;
    }

    // Pattern matching via if statement
    if (input is RespawnTM2020 respawn)
    {
        // special Respawn logic
    }

    // Advanced pattern matching
    if (input is IInputSteer { Value < 10 } steerUnder10)
    {
        // something
    }
    
    // Print out the input string representation, using a timestamp relative to 0
    Console.WriteLine(input.ToString());
}

InputChanges approach

The GBX.NET API also includes the InputChanges list which behaves similarly, but the possible state changes are all merged into a single IInputChange interface (in case of TM2020, TrackmaniaInputChange struct). This class is kept until all input types are recognized. After that, it will become obsolete and you should use Inputs property in production.

An input change includes Tick as integer and Timestamp in TimeInt32 format.

An input change can include the change of any of these (the property won't be null in that case):

  • Steer - the range will be between 127 to -127 (sbyte).
  • Gas - The acceleration button/key was pressed (true) or released (false)
  • Brake - The brake button/key was pressed (true) or released (false)
  • Respawn - The respawn button/key was pressed at that moment
  • Horn - The horn button/key was pressed (true) or released (false)
  • MouseAccuX - (ushort)
  • MouseAccuY - (ushort)
  • ActionSlot1 - Removed Action key
  • ActionSlot2 - New Action key 1
  • ActionSlot3 - Removed Action key
  • ActionSlot4 - New Action key 2
  • ActionSlot5 - Removed Action key
  • ActionSlot6 - New Action key 3
  • ActionSlot7 - Removed Action key
  • ActionSlot8 - New Action key 4
  • ActionSlot9 - Removed Action key
  • ActionSlot0 - New Action key 5
  • FreeLook - Look back key
  • SecondaryRespawn - Respawn key that resets your velocity without having to double press Respawn

Example:

var replay = Gbx.ParseNode<CGameCtnReplayRecord>("Path/To/My/Replay.Replay.Gbx");

// Taking just the first ghost makes thing cleaner
var ghost = replay.GetGhosts().FirstOrDefault();

if (ghost is null)
{
    throw new Exception("No ghost found.");
}

// There could be multiple sets of inputs, but I haven't seen more than one just yet
var playerInputs = ghost.PlayerInputs.FirstOrDefault();

if (playerInputs is null)
{
    throw new Exception("No player inputs found.");
}

var startOffset = playerInputs.StartOffset ?? TimeInt32.Zero;

var tempGas = default(bool?);
var tempBrake = default(bool?);
var tempHorn = default(bool?);

foreach (var inputChange in playerInputs.InputChanges.OfType<CGameCtnGhost.PlayerInputData.TrackmaniaInputChange>())
{
    var timestamp = inputChange.Timestamp + startOffset;

    if (inputChange.Horn.HasValue && inputChange.Horn != tempHorn)
    {
        Console.WriteLine($"{inputChange.Timestamp} Horn {inputChange.Horn}");
        tempHorn = inputChange.Horn.Value;
    }

    if (inputChange.Respawn)
    {
        Console.WriteLine($"{inputChange.Timestamp} Respawn");
    }

    if (inputChange.MouseAccuX.HasValue)
    {
        Console.WriteLine($"{inputChange.Timestamp} Mouse X: {inputChange.MouseAccuX}");
    }

    if (inputChange.MouseAccuY.HasValue)
    {
        Console.WriteLine($"{inputChange.Timestamp} Mouse Y: {inputChange.MouseAccuY}");
    }

    if (inputChange.Steer is null)
    {
        continue;
    }

    if (inputChange.Gas.HasValue && inputChange.Gas != tempGas)
    {
        Console.WriteLine($"{inputChange.Timestamp} Gas {inputChange.Gas}");
        tempGas = inputChange.Gas.Value;
    }

    if (inputChange.Brake.HasValue && inputChange.Brake != tempBrake)
    {
        Console.WriteLine($"{inputChange.Timestamp} Brake {inputChange.Brake}");
        tempBrake = inputChange.Brake.Value;
    }

    if (inputChange.Steer == 0)
    {
        continue;
    }
        
    var steer = inputChange.Steer.Value / 127f;
    
    Console.WriteLine($"{inputChange.Timestamp} Steer {steer}");
}

Additional notes

It is also possible to read ghost inputs from ghost visual samples in some way, however, the precision is decreased from 100 ticks to 20 ticks.

Extracting inputs from Shootmania

Shootmania follows a (roughly?) similar data structure to TM2020, but most importantly, the GBX.NET API is similar. The current latest version is _2017_09_12.

Inputs approach

You can read the inputs with the Inputs property.

See above how you can handle them as an example. One difference is that StartOffset is not utilized in Shootmania.

Here is a list of all the currently recognized input (struct) names:

  • Strafe [IInput] - None, Left, or Right
  • Walk [IInput] - None, Forward, or Backward
  • Vertical [IInput] - Unknown (values: 0, 1, 3)
  • MouseAccu [IInput] - Some form of mouse position presented in subpixels.
  • GunTrigger [IInputState]
  • Action [IInputState]
  • FreeLook [IInputState]
  • Fly [IInputState] - The F key in map editor.
  • Camera2 [IInputState] - The only tracked camera key recognized at the moment.
  • Jump [IInputState]
  • ActionSlot [IInputState] - 1 to 4.
  • Use [IInputState] - 1 and 2.
  • Menu [IInputState] - Esc key.
  • Horn [IInputState]
  • Respawn [IInputState] - Backspace key.
  • GiveUp [IInputState] - Delete key.

Input called 'Horizontal' is not included.

InputChanges approach

The struct to distinguish Shootmania changes is called ShootmaniaInputChange.

foreach (var inputChange in playerInputs.InputChanges.OfType<CGameCtnGhost.PlayerInputData.ShootmaniaInputChange>())
{
   // look for some changes...
}

See above how you can handle the changes as an example. One difference is that StartOffset is not utilized in Shootmania.

An input change can include the change of any of these (the property won't be null in that case):

  • MouseAccuX - Some form of mouse position on the X axis (ushort)
  • MouseAccuY - Some form of mouse position on the Y axis (ushort)
  • Strafe (None, Left, or Right)
  • Walk (None, Forward, or Backward)
  • Vertical (unknown; values: 0, 1, 3)
  • Jump
  • Horn
  • IsGunTrigger (primary fire)
  • IsAction (secondary fire? right click basically)
  • Respawn (the Backspace variant)
  • FreeLook
  • Fly (the F key in map editor)
  • Camera2 (the only tracked camera key recognized at the moment)
  • ActionSlot1
  • ActionSlot2
  • ActionSlot3
  • ActionSlot4
  • Use1
  • Use2
  • Menu (Esc key)

GBX.NET

Practical

Theoretical

  • TimeInt32 and TimeSingle (soon)
  • Chunks in depth - why certain properties lag? (soon)
  • High-performance parsing (later)
  • Purpose of Async methods (soon)
  • Compatibility, class ID remapping (soon)

Internal

External

  • Gbx from noob to master
  • Reading chunks in your parser
Clone this wiki locally