Telemetry interface

From Mania Tech Wiki
Revision as of 12:50, 7 August 2019 by Electron (talk | contribs) (→‎Hardware and software that use the telemetry interface: Added a link to a GitHub project)
Jump to navigation Jump to search

Maniaplanet 4 and Trackmania Turbo 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

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 = 2,
};

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];    // environment name 'stadium' 'canyon', ....
        char        MapId[64];
        char        MapName[256];
        char        __future__[128];
    };
    struct SRaceState {
        ERaceState  State;
        Nat32       Time;
        Nat32       NbRespawns;
        Nat32       NbCheckpoints;
        Nat32       CheckpointTimes[125];
        char        __future__[32];
    };
    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.

        char        __future__[32];
    };
    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];
    };

    SHeader         Header;

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

}

#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.
  • There is no a lap counter. In a multi-lap race, you can not detect if a lap has been completed.
  • The provided telemetry data records are not cleared after a race ends.

Hardware and software that use the telemetry interface