🔍
Building a Trackmania Community Dataset for Level Generation & More

Building a Trackmania Community Dataset for Level Generation & More

TL;DR: I packaged years of Trackmania community maps and replays from Mania Exchange into a data/ML people-friendly dataset: 850k+ maps across two games (Trackmania 2020 and Trackmania Nations Forever), about 30 GB of Parquet. It is on Kaggle and Hugging Face. I also used it to experiment with level generation (GPT-2 fine-tuning, Markov models, a TMTrackNN-style approach) and built a small Gradio app for anyone to explore levels and generate some Tracksmith.

In the past months, I have been working a lot to optimize and reactivate some of my personal data projects on AWS: a lot of devops/dataops, packaging, and so on. It was a bit boring (a coding assistant makes it less so for sure but still). So I wanted to switch to something else. An idea that had been sitting on my backlog for a long time was around the game Trackmania.

Disclaimer: yes, this is about a game made by my employer, Ubisoft, and yes, I work in the data teams supporting game production, but I have no link with Ubisoft Nadeo, the studio behind Trackmania. My work is on Canadian game productions, and this project is purely on my own time and my own money; most of the code will be made available so anyone can use it at home. #sharingiscaring

I will not be the first to explore this idea in the world: Donadigo made some noise in 2018 when he released his TMTrackNN project, where he developed deep learning models to produce Trackmania maps. He published his article/project more or less 6 months after I started to work at Ubisoft, and I was so impressed by what someone outside of Ubisoft managed to do on his own, without accessing the Ubisoft infrastructure.

TMTrackNN (https://donadigo.com/tmtracknn)

It stayed on my mind, but I never had time to dig fully into it. The release of the new Trackmania in 2020, or some papers like MarioGPT, a generative model of Super Mario Bros levels released in 2023, kept the sparkle alive in my backlog and I knew that at some point I would take some time to explore more in detail the data and the project.

MarioGPT timelapse generation (https://github.com/shyamsn97/mario-gpt)

These months of code optimization for my data pipeline were the perfect moment to do something different and explore Trackmania games (multiple games now) via its community and the levels/replays that they shared over the years. Before digging into the data and for those who don’t know Trackmania, let me do a quick tour of the franchise.

Trackmania mon amour

Trackmania is a racing game franchise developed in 2003 by Nadeo (which was acquired by Ubisoft in 2009), that is really somewhere between fast technical racing and user generated content (UGC). Here is a quick timeline of the main releases.

Trackmania Timeline from Wikipedia (AI generated)

On my end, Trackmania entered my life around 2005, as Trackmania Sunrise XL was my second video game on PC after Warcraft 3, and I loved it. After that I played a bit of Trackmania Nations, then Nations Forever, during my first university years. Before leaving for England I went back to Trackmania on console with Turbo (bought on PS4 first, and on PC afterwards when I left for England). Of course, being at Ubisoft pushed me back to the franchise with the release of Trackmania 2020, and I had my time to explore the first seasons.

My best season in terms of ranking

Trackmania can be defined as a driving easy to play but hard to master kind of game: for the controls you just have to accelerate or brake, turn right, turn left, and most of the core gameplay resides in the time attack, in order to beat the best score of a map.

As I said earlier, the other aspect beyond the racing aspect is the map creation, as the games are composed of a map editor that gives the ability in a few clicks (in a simple or advanced editor) to create a map from scratch by placing blocks together.

Trackmania 2020 simple track editor

At the end the editor is producing a binary file in a GBX format that can be shared easily with anyone. This same format also is used in replays that can be saved and shared in the same way.

Over the 20 years the franchise has existed, a community grew and started to share maps (and replays of the races) online, and one of the main platforms is Mania Exchange, an umbrella of multiple websites hosting maps and replays made by the communities.

My idea was to see how I could wrap and compile the work of the community into a dataset that was more “data/ML” friendly, and make it available to everybody, in and out of the community to experiment on various aspects from understanding video games, level design but also more experimental aspects with level generation.

Wrapping the community’s work into a dataset

As I said, the community is sharing GBX files that are not Python friendly but several community libraries exist to read or write GBX files, so I started building functions on top of them to interact with these files. My research pushed me to focus on two games specifically: Trackmania 2020 (TM2020) and Trackmania Nations Forever (TMNF), that are the most popular (for me) and that can rely easily on packages available.

I built a small and gentle scraper that collects the community maps and their replays that were available until mid July 2026. The goal of the scraper was to read each GBX file and transfer it into a JSON format, to be afterwards recompiled into Parquet to be easily manipulable by any Python scripts.

The datasets can be found in two places, Kaggle or Hugging Face, so it can fit more exploration interest or machine learning exploration (will reference Hugging Face for simplicity).

The dataset is structured with one folder per game and for each game, there is an overview of the organization of the folder:

  • blocks_details_{game}.parquet : A collection of the statistics around each block used in the set of maps for the game (mostly count of presence in maps and number of blocks in total)
  • tracks_details_{game}.parquet : per-map data (one row per map). Some columns come straight from the site (number of awards at upload time, the original link); others are metrics derived by functions that analyze the typology of each map (number of curves, elevation, speed). It also has a chunk_part column, which links each map to the blocks and traces files described below.
  • blocks : a folder of per-map block data, split into chunk files named blocks_{game}_partNNNN.parquet. Each file covers a chunk of maps, with one row per block: the block type/name, its orientation and its position
  • traces : a folder of per-map replay data, split into chunk files named traces_{game}_partNNNN.parquet. Each file covers a chunk of replays, with one row per state of the car during a run/replay: its acceleration, braking and turn state, its position, and the nearest placed block (already matched for you, and the code snippet later in this post reproduces that step so you can tune the matching rule).

In the blocks file you can find a field position_block_trace_order that gives you an idea of the order in which the block has been encountered by the player based on the replays so it can help to segment blocks that are more present for the “fluff” of the map versus the ones that are part of the trace on the map. You can find more details and a deeper overview of the files by looking at the data dictionary.

At the end, the dataset represents more than 850k maps across the two games (about 1/3 for TM2020, 2/3 for TMNF). Compiling everything on my laptop took around 3 weeks and produced roughly 30 GB. While that ran, I started exploring what it would take to build a level generator from the dataset (at first from a one-week sample) to see where I could start designing something useful for others working on the level generation topic.

What I learned trying to generate levels

Before going deep on level generation, let me be clear: this is just the beginning, so don’t expect a perfect level generator that produces the most enjoyable levels. I tried a few approaches. First, I mimicked the MarioGPT GPT-2 fine-tuning approach for Trackmania, with some interesting results but nothing conclusive. Then I went back to my first love of my final internship, a Markov-based model for block-to-block transitions, not bad, but far from perfect. Finally I merged my learnings and returned to the TMTrackNN approach, which I find pretty smart. All my learning is documented for this project so feel free to have a look more in detail.

From this experience and before digging into the generator that I built, I wanted to recap the main learnings / tips and tricks that came from this experiment and that can help those that want to work on this kind of application with this dataset.

To support the exploration of the dataset I built this notebook on Kaggle to have an overview of what is inside.

Curate the maps you train on

Use the tracks_details_{game}.parquet as much as possible to filter the maps that you are going to use to build a level generator. Being able to filter out any map without replays, or without awards, can help to apply some curation on the overall set of maps.

There are some distributions of some metrics related to the maps on the two games.

Comparison high level metrics

Comparison number of blocks

Notes: The two games are similar in terms of speed, elevation or configuration but we can clearly see a difference in terms of number of blocks.

x,y,z of maps are not x,y,z of traces

This matters: block positions and replay positions do not use the same reference frame. The blocks files use one referential and the traces table another. You can convert between them with a simple transformation.

Show the coordinate-conversion code
"""
Trackmania stores blocks on an integer grid; replays store the car as world-space
meters on the same axes. The grid cell size is the same for TMNF and TM2020:

    X (horizontal) : 32 m
    Y (vertical)   :  8 m
    Z (horizontal) : 32 m

The helpers below assume the two frames also share an origin. In practice a game
can add a constant offset (e.g. the editor's default ground sits at block Y = 9),
so check the offset against your own parsed data before trusting absolute positions.
"""

import numpy as np

BLOCK_SIZE = np.array([32.0, 8.0, 32.0])  # meters per grid cell (x, y, z)


def block_to_world(bx, by, bz):
    """Grid cell (from the `blocks` table) -> world-space center of that cell,
    in the same frame as the `x/y/z` columns of the `traces` table."""
    return (np.array([bx, by, bz]) + 0.5) * BLOCK_SIZE


def world_to_block(wx, wy, wz):
    """World-space point (from the `traces` table) -> the integer grid cell it
    sits in (the `x/y/z` columns of the `blocks` table)."""
    return np.floor(np.array([wx, wy, wz]) / BLOCK_SIZE).astype(int)


# --- Example: attach the nearest placed block to every replay sample of a track ---
# (this reproduces the dataset's precomputed nearest-block column, so you can
#  swap in your own matching rule)
#
# blocks_df : rows from blocks_{game}_partNNNN.parquet for one track_id
# traces_df : rows from traces_{game}_partNNNN.parquet  for the same track_id

block_centers = (blocks_df[["x", "y", "z"]].to_numpy() + 0.5) * BLOCK_SIZE
sample_pos    = traces_df[["x", "y", "z"]].to_numpy()

# NOTE: block_centers carry the +0.5 Y offset, so this match is biased vertically
#       (the drivable surface sits near a block's base, not its center)
# pairwise distances, then argmin per sample (fine for one track; use a KDTree at scale)
dist = np.linalg.norm(sample_pos[:, None, :] - block_centers[None, :, :], axis=2)
nearest_idx = dist.argmin(axis=1)

traces_df["nearest_block_name"] = blocks_df["name"].to_numpy()[nearest_idx]
traces_df["nearest_block_dist_m"] = dist[np.arange(len(traces_df)), nearest_idx]

Limit which blocks you generate with

The blocks_details_{game}.parquet file gives an idea of what’s popular, but each game has a lot of possible blocks: some come from Nadeo, some are built entirely by the community. I had to restrict generation to a curated list, because some blocks are no longer valid and crash the game on load (That took me days to diagnose).

Predict the car trace before the map

I explored this mostly with a Markov approach but my assumption is that being able to generate plausible states of the car (GOING UP, GOING DOWN, TURN RIGHT, TURN LEFT, ACCELERATE, DECELERATE) could help to orient the generation in a clearer direction. You can be very broad with high level status as I mentioned earlier or be more granular and predict the exact acceleration, brake, tilt value.

Inspect maps and replays together

Being able to quickly analyze a map that you built and a replay that you have done is essential, and can give you a lot of insight on the organization of a map. As an example, this is a screenshot of my Steam account page, and as you can see Trackmania Nations Forever has been open a lot recently (the time is important because the game was left open a lot of nights).

My steam recent activity page

Digging into your own data is essential, so play the game and with the editor.

Representing a map in 3D or as JSON with per-block ids also helps, because it lets you interact with the map and a replay together. When working with a coding agent, having the id of the block helps a lot as you can point at the exact block configurations that are wrong by referring to their block id that you can see in the 3D visualisation and in JSON.

Geometry is key

A Trackmania map is quite simple in itself: it is just a chain of connected blocks, that have position and orientation, so predicting the next good block is not so difficult; the difficulty is on how blocks can be connected together, as they all have various forms and entry points. My pain point was on the curves with their size and connection to other blocks.

Geometry illustration with curves

Beyond these learnings let see what I produced.

Tracksmith : A tool for exploration and level generation

So yes, if you are still here after all this reading, I have something for you. It is called Tracksmith (Trackmania + Blacksmith): a Gradio application where you can inspect a map and its associated replay and more importantly you can generate maps for TMNF or TM2020.

Tracksmith Generation page

The application is available on Hugging Face Spaces, and uses a piece of the code deployed to process GBX to build the dataset. For the level generation it’s a revised TMTrackNN, in PyTorch with more data and that can produce levels for both games.

Level generated

Once again the generation is far from being the most creative one but it’s a beginning that can give a kickstart to anyone who wants to start to dig in.

Where this goes next

This project was mostly about sharing the Mania Exchange community’s work, with that community but also with everyone else. I hope people find useful things in it, and that it puts a new spotlight on the community’s creativity and on how modular Trackmania games can be.

If I had more time, I’d explore how to automate map testing with bots as driving Trackmania from Python is already possible (see Yann Bouteiller or Yosh videos on YouTube), so wiring that into a test loop looks doable.

Once again this dataset is not only for level generation. Plenty of other things can come out of it: player understanding, level-design analysis, recommender systems, metadata generation, you name it. So feel free to use it, and to cite the dataset/article.

For now, the first iteration I set out to build is done: the dataset plus enough code to restart a level-generation project. On my end I have some ideas of exploration around level generation (some good old techniques versus more modern language model based ones) but nothing concrete yet.

I want to highlight once again the amazing work by Nadeo (and Ubisoft .. I know buttlicker :)) around the creation of this amazing franchise, the Mania Exchange community for all the work and dedication put into it and to all the code contributors like Donadigo that permitted me to make my entrance to this data easier.

References