-
-
Notifications
You must be signed in to change notification settings - Fork 21
Extracting input data from ghosts
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.
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 ofSteer
. -
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.
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.
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.
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
, orRight
[CharacterPilot only] -
Walk
[IInput
] -None
,Forward
, orBackward
[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());
}
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 pressRespawn
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}");
}
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.
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
.
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
, orRight
-
Walk
[IInput
] -None
,Forward
, orBackward
-
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.
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
, orRight
) -
Walk
(None
,Forward
, orBackward
) -
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)
- Beginner
- Extracting basic map data
- Extracting ghosts from replays
- Extracting input data from ghosts
- Extracting checkpoints from ghosts (soon)
- Builders - create new nodes extremely easily (soon)
- Intermediate
- Basic map modification (soon)
- Embedding custom items to a map
- Working with script metadata
- Extracting samples from ghosts (later)
- Fast header reading and writing (later)
- Advanced
- Advanced map modification (soon)
- Creating a MediaTracker clip from scratch (soon)
- Lightmap modification (later)
- Integrate GBX.NET with other languages (later)
-
TimeInt32
andTimeSingle
(soon) - Chunks in depth - why certain properties lag? (soon)
- High-performance parsing (later)
- Purpose of Async methods (soon)
- Compatibility, class ID remapping (soon)
- Class structure (soon)
- Class verification (soon)
- Class documentation
- Gbx from noob to master
- Reading chunks in your parser