Telemetry interface

From Mania Tech Wiki
Revision as of 15:43, 4 July 2022 by Electron (talk | contribs) (→‎maniaplanet_telemetry.h: Fixed comment for SGameState::GameplayVariant)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Maniaplanet 4, Trackmania Turbo and Trackmania 2020 provide telemetry data via a shared memory interface.

Interface

The data is written continuously to a non-persisted memory mapped file in virtual memory. External applications have direct access to it. The name of the file mapping object is ManiaPlanet_Telemetry.

The interface is active immediately after the start of the game. If Maniaplanet and Trackmania Turbo are running at the same time (or multiple instances of the same game), it is not possible to determine from which instance the data originates.

It is not known when or how often the data is updated.

An export of the telemetry data by UDP streaming is not available. The data can only be received on the PC on which the game is running.

The following information is provided:

  • Game state - Information about the current state of the game
  • Race state - Information about the current state of the race
  • Object state - State of the vehicle within the virtual world
  • Vehicle state - Current information about the vehicle itself
  • Device state - Information for virtual chairs
  • Player state - Information about the current player (TM2020)

The complete data structure can be taken from the header file maniaplanet_telemetry.h:

#ifndef _MANIAPLANET_TELEMETRY_H
#define _MANIAPLANET_TELEMETRY_H

#pragma once

namespace NManiaPlanet {

enum {
    ECurVersion = 3,
};

typedef unsigned int Nat32;
typedef unsigned int Bool;

struct Vec3 {
    float x,y,z;
};
struct Quat {
    float w,x,y,z;
};

struct STelemetry {
    struct SHeader {
        char        Magic[32];              //  "ManiaPlanet_Telemetry"
        Nat32       Version;
        Nat32       Size;                   // == sizeof(STelemetry)
    };
    enum EGameState {
        EState_Starting = 0,
        EState_Menus,
        EState_Running,
        EState_Paused,
    };
    enum ERaceState {
        ERaceState_BeforeState = 0,
        ERaceState_Running,
        ERaceState_Finished,
    };
    struct SGameState {
        EGameState  State;
        char        GameplayVariant[64];    // player model 'StadiumCar', 'CanyonCar', ....
        char        MapId[64];
        char        MapName[256];
        char        __future__[128];
    };
    struct SRaceState {
        ERaceState  State;
        Nat32       Time;
        Nat32       NbRespawns;
        Nat32       NbCheckpoints;
        Nat32       CheckpointTimes[125];
        Nat32       NbCheckpointsPerLap;
        Nat32       NbLapsPerRace;
        Nat32       Timestamp;
        Nat32       StartTimestamp;         // timestamp when the State will change to 'Running', or has changed when after the racestart.
        char        __future__[16];
    };
    struct SObjectState {
        Nat32       Timestamp;
        Nat32       DiscontinuityCount;     // the number changes everytime the object is moved not continuously (== teleported).
        Quat        Rotation;
        Vec3        Translation;            // +x is "left", +y is "up", +z is "front"
        Vec3        Velocity;               // (world velocity)
        Nat32       LatestStableGroundContactTime;
        char        __future__[32];
    };
    struct SVehicleState {
        Nat32       Timestamp;

        float       InputSteer;
        float       InputGasPedal;
        Bool        InputIsBraking;
        Bool        InputIsHorn;

        float       EngineRpm;              // 1500 -> 10000
        int         EngineCurGear;
        float       EngineTurboRatio;       // 1 turbo starting/full .... 0 -> finished
        Bool        EngineFreeWheeling;

        Bool        WheelsIsGroundContact[4];
        Bool        WheelsIsSliping[4];
        float       WheelsDamperLen[4];
        float       WheelsDamperRangeMin;
        float       WheelsDamperRangeMax;

        float       RumbleIntensity;

        Nat32       SpeedMeter;             // unsigned km/h
        Bool        IsInWater;
        Bool        IsSparkling;
        Bool        IsLightTrails;
        Bool        IsLightsOn;
        Bool        IsFlying;               // long time since touching ground.
        Bool        IsOnIce;

        Nat32       Handicap;               // bit mask: [reserved..] [NoGrip] [NoSteering] [NoBrakes] [EngineForcedOn] [EngineForcedOff]
        float       BoostRatio;             // 1 thrusters starting/full .... 0 -> finished

        char        __future__[20];
    };
    struct SDeviceState {   // VrChair state.
        Vec3        Euler;                  // yaw, pitch, roll  (order: pitch, roll, yaw)
        float       CenteredYaw;            // yaw accumulated + recentered to apply onto the device
        float       CenteredAltitude;       // Altitude accumulated + recentered

        char        __future__[32];
    };

    struct SPlayerState {
        Bool        IsLocalPlayer;          // Is the locally controlled player, or else it is a remote player we're spectating, or a replay.
        char        Trigram[4];             // 'TMN'
        char        DossardNumber[4];       // '01'
        float       Hue;
        char        UserName[256];
        char        __future__[28];
    };

    SHeader         Header;

    Nat32           UpdateNumber;
    SGameState      Game;
    SRaceState      Race;
    SObjectState    Object;
    SVehicleState   Vehicle;
    SDeviceState    Device;
    SPlayerState    Player;
};

}

// -----------------------------------------------
// Changelog:
//   Version 3 is a superset of Version 2.
//   New fields are:
//     Race.Timestamp, Race.StartTimestamp
//     Vehicle.IsOnIce, Vehicle.Handicap, Vehicle.BoostRatio
//     Player.*

#endif // _MANIAPLANET_TELEMETRY_H

Using the data

Nadeo has provided an example program (including source code) in the closed Maniaplanet 4 Beta forums. The tool displays all of the provided telemetry data items live in a window.

To process the data, we need to know when and how the individual data records are provided. The following C++ source code allows you to create a simple console application that will output some of the telemetry data live:

// maniaplanet_telemetry.cpp : TM² - TrackMania Telemetry Monitor
//
#include <windows.h>
#include <stdio.h>
#include <tchar.h>

#include "maniaplanet_telemetry.h"

bool done = false;

BOOL CtrlHandler(DWORD fdwCtrlType)
{
    done = true;
    return TRUE;
}

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hMapFile = NULL;
    void* pBufView = NULL;

    using namespace NManiaPlanet;
    const volatile STelemetry* Shared = NULL;
    Nat32 UpdateNumber = 0;

    // Set console title and control handler
    SetConsoleTitle(TEXT("TM² - TrackMania Telemetry Monitor"));
    SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE);

    // Main loop
    while (!done)
    {
        // Get access to the telemetry data
        if (Shared == NULL)
        {
            if (hMapFile == NULL)
                hMapFile = OpenFileMapping(FILE_MAP_READ, FALSE, TEXT("ManiaPlanet_Telemetry"));

            if (hMapFile != NULL)
            {
                if (pBufView == NULL)
                    pBufView = (void*)MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 4096);
            }

            // Show the wait status in the console title
            static bool show_waiting_title = true;
            if (show_waiting_title && pBufView == NULL)
            {
                show_waiting_title = false;
                SetConsoleTitle(TEXT("TM² - Waiting for the game..."));
            }

            Shared = (const STelemetry*)pBufView;
        }
        else
        {
            STelemetry S;

            for (;;)
            {
                Nat32 Before = Shared->UpdateNumber;
                memcpy(&S, (const STelemetry*)Shared, sizeof(S));
                Nat32 After = Shared->UpdateNumber;

                if (Before == After)
                    break;
                else
                    continue; // reading while the game is changing the values.. retry.
            }

            // Update the console title and output the header
            static bool write_header = true;
            if (write_header)
            {
                write_header = false;
                SetConsoleTitle(TEXT("TM² - TrackMania Telemetry Monitor"));
                puts("UpdateNumber,GameState,GameplayVariant,MapName,MapId,RaceState,RaceTime,"
                    "NbRespawns,NbCheckpoints,CheckpointTimes,Timestamp,SpeedMeter,InputSteer,InputGasPedal,"
                    "InputIsBraking,EngineRpm,EngineCurGear,EngineTurboRatio,RumbleIntensity");
            }

            // Check for updated telemetry data
            if (S.UpdateNumber != UpdateNumber)
            {
                UpdateNumber = S.UpdateNumber;

                // Output update number
                printf("%u,", S.UpdateNumber);

                // Output game data
                switch (S.Game.State)
                {
                    case STelemetry::EState_Starting:
                        printf("Starting,");
                        break;
                    case STelemetry::EState_Menus:
                        printf("Menus");
                        break;
                    case STelemetry::EState_Running:
                        printf("Running");
                        break;
                    case STelemetry::EState_Paused:
                        printf("Paused");
                        break;
                }

                printf(",%s,%s,%s,", S.Game.GameplayVariant, S.Game.MapName, S.Game.MapId);

                // Output race data
                switch (S.Race.State)
                {
                    case STelemetry::ERaceState_BeforeState:
                        printf("BeforeStart");
                        break;
                    case STelemetry::ERaceState_Running:
                        printf("Running");
                        break;
                    case STelemetry::ERaceState_Finished:
                        printf("Finished");
                        break;
                }

                printf(",%d,%u,%u,%u,", S.Race.Time, S.Race.NbRespawns, S.Race.NbCheckpoints,
                    S.Race.NbCheckpoints == 0 ? 0 : S.Race.CheckpointTimes[S.Race.NbCheckpoints - 1]);

                // Output some vehicle data
                printf("%u,%u,%f,%f,%s,%f,%d,%f,%f", S.Vehicle.Timestamp, S.Vehicle.SpeedMeter, S.Vehicle.InputSteer,
                    S.Vehicle.InputGasPedal, S.Vehicle.InputIsBraking ? "True" : "False", S.Vehicle.EngineRpm,
                    S.Vehicle.EngineCurGear, S.Vehicle.EngineTurboRatio, S.Vehicle.RumbleIntensity);

                printf("\n");
            }
        }

        // Suspend the busy wait loop
        Sleep(5);
    }

    // Cleanup
    if (pBufView)
        UnmapViewOfFile(pBufView);
    if (hMapFile)
        CloseHandle(hMapFile);

    return 0;
}

UpdateNumer is used to synchronize the data between the game and the receiver. The variable is incremented once before the values are changed and once thereafter.

For timing information, the object state and vehicle state structures each contain a Timestamp variable. The resolution of the included time is 10 milliseconds.

A few notes about the race data:

  • A new race starts when the race state changes from BeforeState to Running.
  • A race ends when the race state changes from Running to Finished or BeforeState. In order to distinguish between finish and restart, it's necessary to check whether the checkpoint count has increased.
  • It is not guaranteed that the checkpoint time has been updated at the same time as the checkpoint number is raised.
  • The current lap of a multi-lap race can be determined by dividing NbCheckpoints by NbCheckpointsPerLap.
  • The provided telemetry data records are not cleared after a race ends.

Hardware and software that use the telemetry interface