# Kacky Docs
> How Kacky works — concepts, architecture, and the API. For humans and for LLMs.
---
# Kacky Docs
# Kacky Docs
How the Kacky platform works — written once, for **humans and for LLMs**.
Kacky is a community event series for [Trackmania](https://www.trackmania.com/) built around
extremely difficult maps. This site explains the concepts (what a *record* is, how *editions* and
*hunting* work), the architecture (how the in-game servers, the backend, and the frontends fit
together), and the API.
## Start here
- **[Concepts](concepts/index.md)** — the vocabulary. Finishes vs records, KR/KX, editions, hunting,
difficulty, world records. Start here if a term confused you.
- **[Architecture](architecture/overview.md)** — how the pieces connect: in-game controllers →
Redis Streams → backend → website & Discord.
- **[API](api/index.md)** — the REST surface, rendered from the OpenAPI spec (the source of truth).
## For LLMs / agents
This site is also machine-readable. Every page is plain markdown, and the build publishes the
[`llms.txt`](https://llmstxt.org/) convention:
- **[/llms.txt](/llms.txt)** — a flat index of every page.
- **[/llms-full.txt](/llms-full.txt)** — the entire docs concatenated into one file, for a single fetch.
There is no separate "LLM documentation" — the same markdown is rendered for people and concatenated
for machines. One source, two surfaces, so the two can never drift apart.
## Why this exists
Kacky knowledge lives in a lot of heads and scattered files. This site is the shared, un-siloed
reference — the place a new community member, a new contributor, or a coding agent can go to
understand how the thing actually works.
---
# API reference
# API reference
The Kacky backend exposes a REST API. **The OpenAPI spec is the single source of truth** — the
frontends generate their typed clients from it, and this page renders from the same spec, so the docs
can't drift from the code.
!!! note "How this spec stays current"
The `openapi.json` beside this page is refreshed from the running backend
(`/openapi/v1.json`) as a build step — never hand-edited. To refresh locally:
```bash
dotnet run --project src/KackyGG.Api # serves the spec at /openapi/v1.json
curl -s http://localhost:5213/openapi/v1.json \
-o docs-site/docs/api/openapi.json
```
In CI, a job runs the backend, exports the spec to this path, and then builds the site — so a
deployed API page always matches the deployed backend.
---
# The backend and the data
# The backend and the data
One service holds all of Kacky's truth: the backend, a .NET / ASP.NET Core app (`src/KackyGG.Api`).
Everything a player sees — a record, a rank, whether a server is live — is a value the backend
decided and stored. Nothing else gets to be the authority.
## What it stores
The backend keeps the core things in a **PostgreSQL** database, through EF Core. The handful of
entities you actually need to know:
- **Event** — one edition's batch of maps, with its type, start, and end.
- **Map** — a single Kacky map. Its real identity is the map's UID, not its number (numbers aren't
unique across families).
- **Player** — someone who plays in-game, optionally linked to a website account.
- **Finish** — every completion. One row per time crossed.
- **Record** — a player's *best* online time on a map in an event.
How they hang together:
```
Event ─* EventMap *─ Map (an event's maps)
Map ─* Finish *─ Player (every completion)
Map ─* Record *─ Player (best online time per event)
```
Finish and Record look similar and aren't — that split is the thing people trip on, and
[Records vs Finishes](../concepts/records-vs-finishes.md) owns the full story.
## How it serves everything
The backend talks to the world three ways:
- **REST** — the website and admin panel read and write over a normal HTTP API.
- **SignalR** — live pushes (server status, updates) so the site doesn't have to keep asking.
- **Redis Streams** — it consumes what the in-game controllers report and writes it to the database.
The frontends don't hand-write their API code. They **generate** it from the backend's OpenAPI spec,
so when the backend's shape changes, the clients regenerate to match — they can't quietly drift out
of sync.
## Where the rules live
Rank cutoffs, what counts toward which edition, which times are trustworthy — that logic lives in the
backend, in one place, not scattered across each client. A client asks; the backend answers with the
one true version. That's the whole point of a single source of truth: there's no second opinion to
disagree with.
---
# The game servers
# The game servers
Every time you're hunting a Kacky map, you're connected to a **Trackmania 2020 dedicated server** —
the same kind anyone can run, just loaded with the Kacky map pool and a lot of custom logic bolted on.
A dedicated server on its own is dumb: it runs the maps and reports raw events. It doesn't know what a
"finish count" is, or how to draw the in-game record list. That's a second program's job.
## kontrol: the controller next to each server
Next to every Trackmania server runs a controller called **kontrol**. One kontrol per server, paired
up. kontrol is the in-game brain: it talks to the game server over a local protocol (XML-RPC), renders
the in-game UI you see — the record widgets, the `/records` list — and reports what happens back to the
Kacky backend.
kontrol is a fork of an open-source controller (MiniControl) with the Kacky-specific features layered
on top. It's written in TypeScript. When you cross the line, kontrol turns the server's raw event into
a clean "player finished" message and publishes it over Redis Streams to the backend. You can follow
that [whole finish journey](redis-streams.md) end to end, or see the
[architecture overview](overview.md) for where it fits.
## Where all this physically runs
None of it runs on some player's PC. Everything lives on **Hetzner Cloud** — a hosting provider — as
rented virtual servers (VPS).
- **Game hosts** run the Trackmania server and its kontrol side by side, as a pair of Docker
containers sharing a local network. The permanent hunting servers and the event servers each get
their own host.
- A separate **utility host** runs Redis, the pipe kontrol uses to reach the backend.
- The **app host** runs the backend and the websites.
The whole fleet is set up and updated with **Ansible** (infrastructure-as-code — the servers are
described in files, not clicked together by hand). Container images are built in CI and pulled from a
registry. Admins can trigger a deploy from the admin panel. When kontrol updates, it reloads on its
own without kicking players — you stay connected mid-hunt.
Kacky also runs some older Trackmania Nations Forever servers, but those don't have kontrol next to
them yet.
---
# Architecture overview
# Architecture overview
How the pieces fit together — from a player finishing a map in-game to a number on the website.
## The integration spine
```mermaid
flowchart LR
subgraph game[In-game]
TM[Trackmania dedicated servers]
K[kontrol controllers]
TM <--> K
end
subgraph platform[Kacky platform]
API[Backend API
.NET / ASP.NET Core]
DB[(PostgreSQL)]
API <--> DB
end
subgraph clients[Clients]
WEB[Website]
ADMIN[Admin panel]
BOT[Discord bot]
end
K -- "Redis Streams
player.* server.* map.*" --> API
API -- "REST + SignalR" --> WEB
API -- "REST" --> ADMIN
API -- "notification.* streams" --> BOT
```
- **kontrol** — the in-game controllers (a MiniControl fork plus Kacky plugins) drive the Trackmania
dedicated servers and report what happens to the backend.
- **Redis Streams** — the sole message bus between kontrol and the backend. Subjects are namespaced
(`player.*`, `server.*`, `map.*`, `admin.*`), with request/reply correlated by id.
- **Backend API** — a .NET / ASP.NET Core service. It consumes the streams, persists to PostgreSQL,
and serves the clients over REST + SignalR. It is the owner of all truth.
- **Clients** — the website (player stats, rankings, live status), the admin panel (event/user/server
management), and the Discord bot (which also consumes `notification.*` streams).
## Why a message bus, not direct calls
The game servers and the backend have independent lifecycles — servers restart, deploy, and go
offline; the backend deploys on its own cadence. A message bus decouples them: kontrol publishes what
happened and the backend consumes at its own pace, with automatic recovery if either side restarts.
It also means the contract between them is a **set of message shapes**, not a synchronous API — which
is powerful but loosely typed, so the two repos must change their payloads in lockstep.
## The contract is loosely typed — treat it like a versioned API
The stream payloads are validated for *shape* but not *meaning*. A field renamed on one side and not
the other fails **silently at runtime** rather than erroring. So the discipline is:
- Meaning-bearing field names — never overload one field for two concepts.
- Breaking changes ship backend-first, additively (expand/contract), never as a hard rename.
- Each stream handler is the source of truth for its payload; the consumer's schema mirrors it.
## Source of truth, by layer
| Layer | Source of truth |
|-------|-----------------|
| API shape (for the frontends) | The backend **OpenAPI spec** — clients are generated, never hand-written. See [API](../api/index.md). |
| Ranks / world records / personal bests | The online-only `Record` ledger → `mv_map_rankings`. See [Records vs Finishes](../concepts/records-vs-finishes.md). |
| Finish counts / history | Raw `Finish` rows. |
| Game-server liveness | The `server.sync` stream — a server that syncs is alive; no sync for >5 min marks it offline. |
## Where the code lives
The platform is a monorepo. The backend is `src/KackyGG.Api/` (.NET, EF Core, MediatR/CQRS,
PostgreSQL, Redis Streams); the website and admin panel are React apps; the Discord bot is a
Discord.js/TypeScript app. Infrastructure (Hetzner, Cloudflare, Ansible) lives in a sibling repo.
!!! note "Deeper contributor docs"
The operational, agent-facing detail (how to build, test, and work in each component) lives in
the repo's `AGENTS.md` and `.claude/` rules. This site is the shared *reference*; those are the
*operating manual*. They link to each other and deliberately don't duplicate.
---
# How the game talks to the platform
# How the game talks to the platform
A Trackmania 2020 server doesn't call the backend directly. When you cross the finish line, the news
travels as a **message** — dropped onto a queue, picked up whenever the backend is ready. That queue is
**Redis Streams**, and it's the one road between the game servers and the platform.
The controller running next to each server is **kontrol** (a MiniControl fork with Kacky plugins). It
watches the game, and when something happens it publishes a message. The backend reads those messages
and writes them down. Neither side talks to the other directly.
## Subjects: every message has a name
Messages are namespaced by **subject** — a short name that says what happened. They group by topic:
- `player.*` — `player.connect`, `player.finish`, `player.sync`, nickname changes.
- `server.*` — server roster sync, map start/end/switch, the event map list.
- `map.*` — karma votes, map records, analytics.
- plus `admin.*`, `leaderboard.get`, `notification.*`, `achievement.*`.
Each subject has exactly one handler on the backend. `player.finish` is written down as a finish;
`server.sync` proves a server is alive. (Finishes aren't the same as records — see
[Records vs Finishes](../concepts/records-vs-finishes.md) for why.)
Most messages flow one way, game → backend. A few need an answer: subjects like `map.getRecords`,
`leaderboard.get`, and `server.getEventMaps` carry a `correlationId` and a `replyTo`, so the backend
can send its reply back to the exact kontrol instance that asked.
## Following a finish
```mermaid
sequenceDiagram
participant TM as Trackmania server
participant K as kontrol
participant R as Redis Streams
participant API as Backend
participant C as Website / Discord bot
TM->>K: you crossed the line
K->>R: publish player.finish
R->>API: backend consumes it
API->>API: write the finish to PostgreSQL
API-->>C: rankings over REST, live status over SignalR
```
## Why a queue instead of a phone call
The game servers and the backend live on their own schedules. Servers restart, redeploy, and drop
offline; the backend deploys whenever it deploys. A queue lets them ignore each other's downtime —
kontrol drops the message and moves on, the backend picks it up when it's back. Streams have no
expiry, so a message waits until it's read.
## The catch: the contract is loosely typed
Here's the part that bites. A stream message is just JSON. The backend checks its *shape*, not its
*meaning*. There's no shared type system spanning the two codebases — kontrol writes fields by name,
the backend reads fields by name, and nothing forces them to agree.
!!! warning "A renamed field fails silently"
Rename a field on one side and forget the other, and nothing errors. The message still arrives,
still looks valid — the value just quietly goes missing. No exception, no log, no red flag. It
only shows up as wrong numbers on the website, later. Treat the message shapes like a versioned
API: **kontrol and the backend change together, or not at all.**
The same trap hides in values. `gameType` on player and server messages is read case-insensitively
and **falls back to `TRACKMANIA` when it doesn't recognise the string** — so a typo doesn't crash, it
just files the data under the wrong game.
The bigger picture — who owns what, and how the pieces connect — is in the
[Architecture overview](overview.md).
---
# Events and editions
# Events and editions
Everything in Kacky hangs off events. This page goes a level deeper than
[How Kacky works](how-kacky-works.md) on what an event actually is, how editions work, and what
happens to the maps once the event is over.
## An event is a batch of maps with a month in the spotlight
An event is a set of brand-new maps released together. For about a month it's the main thing
happening — the maps are fresh, everyone's playing them, and there's a live leaderboard just for that
batch. That month-long run is the event's moment in the sun.
## Two series: Kacky Reloaded and Kacky Remixed
Kacky runs two ongoing series, **Kacky Reloaded** and **Kacky Remixed**. Both are Trackmania 2020,
and both follow the same rhythm — they're just separate map pools with their own history and their own
numbering. Each new event in a series gets the next number, and that number is the **edition**. So
"the newest Kacky Reloaded" or a particular edition is never ambiguous — the number pins it down.
## After the month: hunting
When an event's month ends, its maps don't disappear. They roll onto a permanent **hunting server**
that keeps the whole series' back catalogue online. That's where the long game happens — people are
still chipping away at old maps years after the event that introduced them.
The only thing that changes is **where your finish counts**:
- Finish a map while its event is live → it counts toward that event.
- Finish the same map later, on the hunting server → it counts toward the all-time record for that map.
Same map, same finish line — just a different bucket depending on when you did it. This split is also
why the site is careful about which "best time" it shows you; the details are on
[Records vs Finishes](records-vs-finishes.md).
## Why it's built this way
Kacky isn't a one-and-done event series. The whole point is that a map you couldn't beat two years ago
is still sitting on the hunting server waiting for you. Editions give the community a steady drip of
new challenges; the hunting servers make sure none of it is ever lost.
---
# Glossary
# Glossary
The Kacky vocabulary in one place. For the most important distinction — *finish* vs *record* — see
[Records vs Finishes](records-vs-finishes.md).
## Terms you'll see around Kacky
| Term | What it means |
|------|--------------|
| **Kacky** | A community that plays some of the hardest maps in Trackmania 2020. Run by a non-profit, for the players. |
| **Kacky Reloaded** | One of the two ongoing map series. New editions drop, get played for about a month, then live on the hunting server. |
| **Kacky Remixed** | The other ongoing series. Same rhythm as Kacky Reloaded — a separate pool of maps with its own history. |
| **Edition** | One event's batch of maps, identified by its number (each new event in a series gets the next one). |
| **Hunting server** | A permanent server that keeps a series' whole back catalogue online, long after each event ends. Where the long game happens. |
| **Finish** | Completing a map — crossing the line. Every completion counts as a finish. See [Records vs Finishes](records-vs-finishes.md). |
| **Record** | Your *best* time on a map. Ranks and world records are built from records, never from raw finishes. |
| **World Record (WR)** | The fastest record on a map, across everyone. |
| **First blood** | The first player to finish a brand-new map after its event starts. Only one person per map ever gets it. |
| **Hunter** | A player who replays maps over and over — finishing new ones and improving old times. |
| **Casual** | Someone who watches, or finishes a few maps during an event, without hunting the whole catalogue. |
| **Rank** | How far you got in an event, by how many of its maps you finished: **Plastic → Bronze → Silver → Gold → Kacky** (100%). See [Ranks and difficulty](ranks-and-difficulty.md). |
| **Difficulty** | A rough measure of how hard a map is to finish. |
| **Karma** | Community thumbs-up / thumbs-down voting on how good a map is. |
| **Map number** | A map's number within its series (like "map 123"). Handy for talking about a map, but not a bulletproof ID — see *Map UID* below. |
| **Author time** | The map maker's own reference time on the map. |
| **Clip** | A gameplay video someone submitted (Twitch, YouTube, Medal.tv). |
## Under the hood
The technical names, for the curious and for people building on Kacky. You won't need these to play.
| Term | What it means |
|------|--------------|
| **kontrol** | The controller program that runs next to each Trackmania 2020 server, renders the in-game UI, and reports events to the backend. See [The game servers](../architecture/game-servers.md). |
| **Redis Streams** | The message bus the game servers use to talk to the backend. See [How the game talks to the platform](../architecture/redis-streams.md). |
| **Backend** | The single service that owns all the data and rules, backed by a PostgreSQL database. See [The backend and the data](../architecture/backend-and-data.md). |
| **Map UID** | A map's true, unique identifier baked into the file — the reliable ID when a map number isn't enough (map numbers aren't unique across series). |
| **Event family** | The internal grouping behind a series (Kacky Reloaded / Kacky Remixed), including its separate hunting event. |
| **Contributing event** | An event whose finishes also count toward another, bigger tally. Most events are standalone. |
---
# How Kacky works
# How Kacky works
Kacky is a community built around some of the hardest maps in Trackmania 2020. This page is the whole
thing in one read — what an event is, why people are still grinding maps from years ago, and where a
"time" actually comes from. No code, no jargon you haven't met yet.
## Events, and the maps that never really end
Kacky runs **events**. An event is a fresh batch of new maps, dropped together and played hard for
about a month. They come in two flavours: **Kacky Reloaded** and **Kacky Remixed** (KR and KX, if you
hang around the Discord). Each new batch gets its own number — that's the **edition** — so everyone
knows exactly which set of maps they mean.
Here's the bit that catches people off guard: the maps don't disappear when the event ends. Both
Kacky Reloaded and Kacky Remixed keep a permanent **hunting server** running the entire back
catalogue — so long after an edition's month is done, players are still on there grinding maps from
years ago.
So Kacky has a rhythm:
- During an **edition** (the ~month it's live), your finish counts toward that edition.
- On the **hunting server**, any time after, it counts toward the all-time record instead.
One map, two lives — its edition, and forever after.
## What people are actually doing: hunting
Kacky maps are hard enough that just *finishing* one is the flex. **Hunters** are the players who run
maps over and over, chasing two things:
- Finishing maps they haven't beaten yet — the pool is massive, so "which of these have I still not
done?" is a real question, and the site answers it.
- Beating their own time on maps they've already cleared.
A **casual** player might watch, or finish a few maps during an event and call it a day. A hunter
treats the whole back catalogue as a mountain. Most of what Kacky.gg does is for that second group:
your records, what's left to finish, how you stack up against everyone else.
## Where a "time" comes from — and the one thing people mix up
Every time you cross the finish line, that's a **finish**. Your best time on a map is your
**record**. The fastest record on a map, across everyone, is the **world record**.
Finishes and records aren't the same data, and mixing them up is the classic mistake:
- A **finish** is every completion — including old times imported from Kacky's earlier systems. Good
for "how much has this map been played," useless for "who's fastest," because an imported time
can't be trusted next to a live one.
- A **record** is your best online time — the trustworthy kind. Ranks, leaderboards, and world
records are built only from records.
If you take one thing from this page: **ranks and world records never come from raw finishes.** The
full story's on [Records vs Finishes](records-vs-finishes.md) — worth a read if you care where the
numbers come from.
## Ranks: how far you got
Kacky ranks you by **how much of an event you finished** — not how fast. Clear a chunk of the maps and
you earn a tier:
| Tier | Roughly |
|------|--------|
| Plastic | ~10% of the maps |
| Bronze | ~30% |
| Silver | ~50% |
| Gold | ~80% |
| Kacky | 100% — every single map |
The exact cutoffs are worked out per event, but the idea never changes: finish more maps, climb
higher. Getting **Kacky** — every map in an event — is the one everyone's really chasing.
## Who runs it
Kacky is **Kacky Organisation e.V.**, a non-profit. There's no company behind it — the servers, the
events, and this site are run by the community, for the community. That's why it's built for the few
hundred people who actually hunt these maps, not for some imaginary million.
## Want to go deeper?
That's the whole player side. If you want to see how it's built — the game servers, how your finish
travels from the server to your profile, the API — start with the
[Architecture overview](../architecture/overview.md). The tech's only there if you want it.
---
# Concepts
# Concepts
The Kacky vocabulary, from plain-language to the details that actually bite.
- **[Records vs Finishes](records-vs-finishes.md)** — the single most important distinction on the
platform. If you only read one page, read this: it's the difference between a *fun fact* and a
*record*, and getting it wrong silently corrupts stats.
- **[Glossary](glossary.md)** — every term in one table (KR, KX, KRHX, edition, hunting, WR, karma,
rank tiers, …).
## The 30-second model
- **Kacky** runs **events**. An event belongs to a **family** — **KR** (Kacky Reloaded) and
**KX** (Kacky Remixed) are the two live families today.
- A family releases maps in **editions** (roughly month-long main events, numbered — "KR 8").
- Each family also has a **permanent hunting server** (**KRHX**, **KXHX**) where people keep
replaying the whole map pool long after an edition ends.
- Players **finish** maps; their best online time on a map is their **record**; the fastest record
anywhere is the **world record (WR)**.
Every one of those bolded terms has a precise meaning — the pages above pin them down.
---
# Ranks and difficulty
# Ranks and difficulty
Two different questions people ask about Kacky: how far did *I* get, and how hard is *this map*?
Ranks answer the first. Difficulty answers the second.
## Ranks: how much of an event you finished
Kacky ranks you by **how many of an event's maps you've finished** — not by your times. Clear enough
of them and you earn a tier:
| Tier | Roughly |
|------|--------|
| Plastic | ~10% of the maps |
| Bronze | ~30% |
| Silver | ~50% |
| Gold | ~80% |
| Kacky | 100% — every single map |
The exact cutoffs are worked out **per event**, because events don't all have the same number of maps.
The percentages stay about the same; the actual number of maps behind each tier is rounded to something
clean for that event. The idea never changes: finish more maps, climb higher.
Getting **Kacky** — every single map in an event — is the tier people chase hardest. It means you beat
the whole thing.
## First blood
The first person to finish a brand-new map after an event starts gets **first blood** on it. It's a
small bragging right, and only one player per map can ever have it.
## Difficulty: how hard a map is
Every map also carries a **difficulty** — a rough measure of how tough it is to finish. The harder a
map, the fewer people manage to clear it, so a map's difficulty and its finisher count tend to move
together: brutal maps have short finisher lists.
---
# Records vs Finishes
# Records vs Finishes
This is the most important distinction on the platform, and the one that most often gets used
wrong. **A finish and a record are not the same thing, and they do not come from the same place.**
## The short version
| | **Finish** | **Record** |
|---|---|---|
| What it is | *Every* time a player crosses the finish line | A player's *best online* time on a map |
| How many per player/map | Many (every attempt that finishes) | One (their best), per event |
| Includes offline / imported times? | **Yes** | **No — online only** |
| Use it for | Finish **counts** and history | Best times, **ranks**, **world records**, leaderboards |
| Stored in | The `Finish` table | The `Record` ledger (→ `mv_map_rankings`) |
If you remember one rule:
!!! danger "The one rule"
**Never compute a best time, a rank, or a world record from `Finish` rows.**
`Finish` includes offline and imported times with no flag to tell them apart. Best-times,
ranks, and WRs come only from the online **`Record`** ledger.
## Why the split exists
Kacky's data comes from two kinds of play:
- **Online play** on the live game servers — fully trusted, every time is attributable to a real
session on a real server.
- **Offline / imported times** — historical data migrated from old databases, or times that never
went through the live online pipeline.
Both are real finishes, so both belong in the **finish history**. But only the online times are
trustworthy enough to rank people against each other. Mixing an imported offline time into a
leaderboard would let an unverifiable time claim a world record — so the platform keeps two ledgers.
## What each one answers
**`Finish` answers "how much has this map been played?"**
- How many finishes does map KR 412 have? (all of them — online, offline, repeats)
- Has this player ever finished this map?
- Finish history / activity over time.
**`Record` answers "who is fastest, and where do I rank?"**
- What's the world record on KR 412?
- What's my personal best, and my rank?
- The hunting leaderboards, the website's records widget, the in-game `/records` list.
## The trap in the numbers
Two counts look similar but mean different things:
- **`finishCount`** — the raw number of `Finish` rows for a map. Includes online, offline/imported,
**and repeat** finishes by the same player. It's a "how much action has this map seen" number.
- **`uniqueFinishers`** — how many distinct players have an **online** record on the map. It comes
from the `Record` ledger (one row per player), **not** from counting distinct players in `Finish`.
This is the number the in-game map list and the records widget show.
Because `uniqueFinishers` is drawn from the online-only ledger, it can never over-report by counting
an imported offline finisher. If you instead counted distinct players in `Finish`, offline/imported
players would inflate it.
## For contributors (the technical edge)
- The materialized view **`mv_map_rankings`** is built from the online-only `Record` ledger and is
the single source for ranks and WRs (rank-1 = the WR). The website and the in-game HUD read the
same view, so they always agree.
- The hunting rankings rank the cross-event online best per **logical map number**, and they filter
`Map.IsActive` so a replaced map version doesn't create a phantom second entry.
- Repositories that need a finish *count* read `Finish`; anything that needs a *time/rank* reads
`Record` / `mv_map_rankings`. The two are never interchangeable — a code comment in the map
read-repository literally warns against using the ranking view as a finish counter.
!!! tip "If you're building a feature that shows a time"
Ask: *is this a count, or a ranking?* Counts → `Finish`. Rankings/WRs/personal-bests → `Record` /
`mv_map_rankings`. When in doubt, it's a ranking, and it's online-only.