|
| 1 | +# CounterStrikeProgression site design |
| 2 | + |
| 3 | +## Intent |
| 4 | + |
| 5 | +The CS2 server's players want to see their progression outside the game. The site at |
| 6 | +`cs2-progression.ktsu.dev` shows what is happening on the server right now, and lets anyone look up |
| 7 | +the server-wide ranks and any player's full record. It's public and read-only. Players open it on a |
| 8 | +second screen or a phone while they play, or afterwards to see how they're doing. |
| 9 | + |
| 10 | +## Goals |
| 11 | + |
| 12 | +- **Live match:** the current map and game mode, the player count, and a scoreboard of everyone on the |
| 13 | + server with their name, rank, kills, deaths, and XP earned this map. It updates as it happens. |
| 14 | +- **XP feed:** skill shots and rank-ups scroll in as players earn them, for example |
| 15 | + "Katsu · Noscope · AWP +40". When the page opens, the feed already shows the last 50 entries. |
| 16 | +- **Server ranks:** every human who has played, ordered by rank XP, with their rank, total XP, and when |
| 17 | + they were last on. Players on the server right now show "online". Each name links to their player page. |
| 18 | +- **Player page:** one player's rank, a progress bar to the next rank, total XP, first seen and last |
| 19 | + seen, a table with one row per weapon (level, kills, headshot percentage, accuracy), and each weapon's |
| 20 | + challenge tiers, with finished tiers highlighted. |
| 21 | +- **Every page is live.** XP and stats climb on screen while the player is playing, without a reload. |
| 22 | + Lifetime totals, weapon stats, and challenge tiers update within a few seconds, not on the plugin's |
| 23 | + database save. |
| 24 | + |
| 25 | +## Non-goals |
| 26 | + |
| 27 | +- No login and no accounts. Every page is public. |
| 28 | +- No admin actions. The CSS-BANS panel at `cs2-admin.ktsu.dev` covers administration. |
| 29 | +- No match history beyond the current map. The scoreboard resets on map change. |
| 30 | +- No graphs or history over time in the first version. |
| 31 | + |
| 32 | +## Anti-goals |
| 33 | + |
| 34 | +- The site must never slow down or break the game server. If the site is down, slow, or |
| 35 | + misconfigured, the plugin carries on exactly as it does today. |
| 36 | +- The site must never be able to change progression data. It reads the database with a read-only |
| 37 | + user. |
| 38 | +- Nothing shown may reveal anything a player couldn't already see in game (no SteamIDs beyond what a |
| 39 | + Steam profile link needs, no IPs). |
| 40 | + |
| 41 | +## Look |
| 42 | + |
| 43 | +The game HUD style, chosen from mockups: dark backgrounds (#0d0f12, #15181d), a monospace face, orange |
| 44 | +accents (#f2a33a) for headers and rank badges, and gold (#f2c94c) for highlights such as skill shots, |
| 45 | +rank-ups, and finished challenge tiers. It matches the in-game floaties, whose highlight color is the |
| 46 | +same gold. Online players are marked in green. It must work on a phone. The mockups are in |
| 47 | +`.superpowers/brainstorm/` in this repo and are a guide, not a pixel spec. |
| 48 | + |
| 49 | +## How it's built |
| 50 | + |
| 51 | +These were decided in conversation. |
| 52 | + |
| 53 | +- **An ASP.NET Core app in this repo**, built the ktsu way under `ktsu.Sdk`, alongside the plugin. Follow |
| 54 | + GitLfsCache's pattern (`C:/dev/ktsu-dev/GitLfsCache`): an ASP.NET project under plain |
| 55 | + `Microsoft.NET.Sdk` plus `ktsu.Sdk` with a `FrameworkReference`, `net10.0` only, and a container image |
| 56 | + through SDK container support (`EnableSdkContainerSupport`, the chiseled `aspnet:10.0` base, linux-x64 |
| 57 | + and linux-arm64). The image is published to GHCR by CI, the way GitLfsCache's is. |
| 58 | +- **Ranks and player pages read the MariaDB** the plugin already writes: `csp_players`, `csp_weapons`, |
| 59 | + and `csp_challenges` in the `cs2` database that SimpleAdmin also uses. |
| 60 | +- **Live data comes from the plugin.** The database only holds lifetime totals, so the plugin sends |
| 61 | + events to the site as they happen, and the site pushes them on to open browsers. |
| 62 | +- **It runs as its own pod in the homelab**, deployed by Terraform in `matt-edmondson/homelab`. |
| 63 | + |
| 64 | +## Constraints |
| 65 | + |
| 66 | +- The plugin builds against CounterStrikeSharp v1.0.376 on .NET 10 with `ktsu.Sdk`. Warnings are errors |
| 67 | + and code style is strict (see `CLAUDE.md`). |
| 68 | +- All game state is read on the game thread. Anything that leaves the plugin must be captured there and |
| 69 | + sent from the thread pool. |
| 70 | +- The Core library holds the rank ladder, the weapon catalog, the skill shot names, and the challenge |
| 71 | + definitions. The site uses them from Core rather than copying them, so names and thresholds can't |
| 72 | + drift. Core is `internal`, so it needs another `InternalsVisibleTo` for the site. |
| 73 | +- The site project must not be packed to NuGet, like the plugin. See `Directory.Build.targets`. |
| 74 | +- The release zip for the server stays plugin-only. The site ships as an image. |
| 75 | + |
| 76 | +## Plugin side |
| 77 | + |
| 78 | +- A new optional `Site` section in `PluginConfig`, with the site's URL and a shared token. With no URL |
| 79 | + set, nothing is sent, which is the default. |
| 80 | +- The plugin sends two kinds of message: |
| 81 | + - **A live snapshot** of the server: map, game mode, the player list with each player's slot, name, |
| 82 | + SteamID, rank, kills, deaths, XP this map, and whether they're a bot. It's sent every couple of |
| 83 | + seconds and straight after a map change. |
| 84 | + - **Lifetime progress for online humans,** so player pages and the ranks page are live within |
| 85 | + seconds rather than waiting for the plugin's once-a-minute database save (the user's choice). This |
| 86 | + covers rank XP, rank, and each weapon's and challenge's rows. Sending only what changed since the |
| 87 | + last message keeps it small. The site overlays these on what it reads from the database, and the |
| 88 | + newer value wins. |
| 89 | + - **Feed events:** a skill shot (player, skill shot name, weapon, XP) and a rank-up (player, new rank). |
| 90 | + Weapon level-ups and finished challenge tiers are good candidates for the feed too, in the |
| 91 | + highlight color. |
| 92 | +- Sending is fire and forget: a bounded queue drained on the thread pool, with a short timeout. When the |
| 93 | + queue is full or the site doesn't answer, messages are dropped and a failure is logged at most once a |
| 94 | + minute. A save never waits on the site. |
| 95 | +- Kills, deaths, and XP this map need counting per player per map, since nothing tracks them today |
| 96 | + outside the database totals. |
| 97 | +- The rank ladder, weapon level curve, and challenge tiers can be changed in the plugin's |
| 98 | + `Progression` settings. The plugin sends the values it's using, and the site draws progress bars and |
| 99 | + "next tier" targets from them. Until it has heard from the plugin, the site uses Core's defaults. This |
| 100 | + was the design author's call. |
| 101 | + |
| 102 | +## Site side |
| 103 | + |
| 104 | +- An endpoint for the plugin's messages, which rejects any request without the shared token. It's |
| 105 | + reachable inside the cluster only, never through the public route. |
| 106 | +- The latest snapshot is kept in memory. The feed's history is kept so a page opened, or a site |
| 107 | + restart, still shows the last 50 entries. |
| 108 | +- Browsers get live updates over a server push channel. Snapshots and feed entries go to every open |
| 109 | + page. The ranks page and player pages refresh what changed, so an online player's XP climbs. |
| 110 | +- When no snapshot has arrived for a while, the live page shows the server as offline instead of a |
| 111 | + frozen scoreboard. |
| 112 | +- Player pages are addressed by SteamID64, so a link survives a name change. |
| 113 | + |
| 114 | +## Homelab side |
| 115 | + |
| 116 | +- Terraform in the homelab repo next to `cs2.tf`: a Deployment, a ClusterIP Service, a public Traefik |
| 117 | + route at `cs2-progression.<domain>` behind crowdsec only (no oauth), the `.local` bypass route, and an |
| 118 | + Azure DNS record, like the other public sites. |
| 119 | +- A read-only MariaDB user for the site with `SELECT` on the `csp_*` tables, created and kept in step by |
| 120 | + Terraform, with its password from `terraform.tfvars`. |
| 121 | +- The plugin config's `Site` section is rendered by Terraform into the existing |
| 122 | + `CounterStrikeProgression.json` secret, pointing at the site's in-cluster Service. |
| 123 | +- A homepage tile, like the CS2 server and admin panel tiles. |
| 124 | +- The homelab `CLAUDE.md` gets an entry for the new file. |
| 125 | + |
| 126 | +## Left to the builder |
| 127 | + |
| 128 | +- The push channel (server-sent events or SignalR), and the wire format between the plugin and the site. |
| 129 | +- Where the feed history lives (a site-owned table in the MariaDB, or the site's own storage), as long as |
| 130 | + it survives a site restart. |
| 131 | +- Whether the pages are server-rendered with live patches or a small client-side app. Keep the |
| 132 | + front-end free of a build step if that's practical. |
| 133 | +- Caching of ranks and player pages, sized for a 16-player server and a few hundred players in total. |
| 134 | +- Page and endpoint names. |
| 135 | +- Tests: MSTest for the parts that can be tested without a game server, such as snapshot and feed |
| 136 | + handling, ordering, and the token check. |
0 commit comments