# 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.