# Outnumbered **A players-vs-bots RPG mod for Counter-Strike 2** — a small human squad fights badly outnumbered against bot hordes while a full RPG layer (persistent levels, a passive perk tree, killstreak abilities, a quick-buy shop, and a hidden rubber-band balancer) rides on top of fast deathmatch combat. Because every enemy is a bot, the mod is **PvE by design** — there are no human opponents to cheat against, and everything works at `sv_cheats 0`. One shared RPG core runs under three match rulesets — **Team Deathmatch**, **Gun Game**, and **Wave Survival** — picked per server instance, so a single install + single config can power a whole fleet of differently-flavored servers. > Built on [CounterStrikeSharp](https://docs.cssharp.dev/). Linux-only. Licensed under **AGPL-3.0** — see [LICENSE](LICENSE). --- ## Requirements - A CS2 dedicated server with **Metamod:Source** and **CounterStrikeSharp** installed (compiled against CounterStrikeSharp API **1.0.369**; use a matching or newer server build). - **.NET 10** — provided by the CounterStrikeSharp runtime; only needed as an SDK if you build from source. - **Linux** — the damage path, native grenade spawning, and launch-flag parsing are Linux-specific. - A database: **PostgreSQL** for production (recommended, required for multi-instance), or **SQLite** for single-server/dev. --- ## Running a server ### Install from a release (no build required) Every tagged version is published to this repo's [**Releases**](https://git.lo.sh/kamal/cs2-outnumbered/releases). Grab the latest tarball and extract its `addons/` folder into your server's `game/csgo/` — that's the whole install, nothing to compile: ``` /game/csgo/addons/counterstrikesharp/plugins/outnumbered/ ``` Then: 1. Ensure **Metamod:Source + CounterStrikeSharp** are installed and the Metamod entry is present in `game/csgo/gameinfo.gi`. CS2 updates periodically strip that line, which silently disables every plugin — re-add it if plugins stop loading. Confirm with `meta list` and `css_plugins list`. 2. The release is **Postgres-only**, so set a **PostgreSQL** connection string in `outnumbered.json` (written on first load, in the CounterStrikeSharp configs folder). Want the zero-setup SQLite database instead? It isn't in the release — build from source with `-p:WithSqlite=true` (below). 3. Launch with the mode, and a GSLT for a public server — e.g.: ```sh ./cs2 -dedicated -insecure +game_type 1 +game_mode 2 -maxplayers 25 +map ar_shoots \ -port 27015 +sv_setsteamaccount -outnumbered_mode tdm ``` ### Notes - **Mode** is `-outnumbered_mode tdm|gg|survival`. - **Public listing** needs a **GSLT** (`+sv_setsteamaccount `, appid 730) and an open UDP game port; without a token the server is LAN-only. Mint tokens at with a **dedicated** Steam account (a game-server ban is account-wide). - The mod runs `-insecure` (VAC off — CounterStrikeSharp's memory writes would trip VAC); that's independent of being publicly listed. - Run it however you prefer — directly, under systemd, or any process supervisor; just pass the launch flags above. --- ## Building from source You need the **.NET 10 SDK**. CounterStrikeSharp is pulled from NuGet, so no local engine path is required. ```sh git clone https://git.lo.sh/kamal/cs2-outnumbered && cd cs2-outnumbered dotnet build Outnumbered.slnx -c Release # build everything dotnet test Outnumbered.slnx # run the Domain test suite ``` ### SQLite is a dev-only convenience The default build bundles **SQLite** (a zero-setup local file database) so you can get running without standing up Postgres. **It is for development / single-server use only.** Production — and especially any multi-instance fleet sharing one database — should use **PostgreSQL**. Pass `-p:WithSqlite=false` to produce the **Postgres-only** build. This is what ships: it drops the SQLite packages and their native library entirely (and with them a known dev-only SQLite advisory), leaving a lean plugin. ```sh # The shippable, Postgres-only artifact: dotnet publish Outnumbered/outnumbered.csproj -c Release -p:WithSqlite=false -o ./out ``` `dotnet publish` emits a ready-to-install plugin folder — host-provided assemblies are stripped automatically, so the output is just the plugin and its real dependencies. Drop it into your server at: ``` game/csgo/addons/counterstrikesharp/plugins/outnumbered/ ``` ### Configuration On first load the plugin writes `outnumbered.json` (in the CounterStrikeSharp configs folder) from its built-in defaults; edit it to tune nearly everything. Set the database provider (`sqlite` / `postgres`) and connection string there. Almost all tuning — abilities, stats, the handicap, the shop, HUD, ranks, and the full Survival economy — **hot-reloads at runtime** via the admin reload command, with no need to restart the match. --- ## Features ### Core: a PvE RPG shooter - **Heavily outnumbered, players-vs-bots.** Humans are forced onto CT and bots onto T at several bots per human, so you're always fighting outnumbered. - **One RPG core, three rulesets.** Shared persistence, stats, progression, handicap, abilities, and the shop ride under three distinct match modes selected per instance. - **Built on the Deathmatch base.** Every mode runs on the engine's deathmatch base for native respawn and weapon deployment, deliberately bypassing the built-in mode rules. Bomb sites, hostages, C4, money/buying, dropped weapons, and the deathmatch weapon prompt are all disabled — it's pure combat. - **Endless play.** Round-win conditions and the round timer are suppressed; a map ends only on a mode's kill goal or win/lose condition. - **Spawn protection** that grants brief immunity (to spend skill points) and drops the instant you fire. Everyone spawns with a knife and armor on top of their loadout. - **Configurable, fixed bot difficulty** (auto-adjust and chatter off). ### Game modes - **Team Deathmatch** — spawn with your chosen primary/secondary; the map ends and rotates when any one player hits the kill goal. - **Gun Game** — climb a shared weapon ladder by getting kills, with an optional knife finale. - *Dual inverse pacing:* kills-per-rung scale off each weapon's strength tier via inverse curves, so you grind the weak guns and breeze the strong ones — while bots do the opposite — and the ladder order zig-zags weapon types. - *Bots climb too,* in stable player-sized **batches** that share a rung and re-equip together, so the horde scales with player count and can actually reach the top and **win**. - *Headshot demotion:* a headshot kill knocks the victim back a step — the classic knife-steal, generalized — and rung weapons are handed over live, mid-life. - **Wave Survival** — a co-op campaign where a CT squad holds against escalating waves of vanilla bots; clear every wave to win, wipe or stall to lose (detailed below). - Shared mode plumbing: bot population scales with humans (clamped to a max), CT is kept human-only, the human cap is per-mode, and each mode can define its own map pool. ### Progression, prestige & ranks - **XP → levels → skill points** along a tunable, accelerating curve up to a level cap. - XP is earned **per hit, scaled by actual damage dealt**, plus flat bonuses for headshots, crits, and the killing blow; sub-point remainders carry over so nothing is lost. - **Prestige** at the cap: a full reset for a permanent, cumulative XP boost (it speeds the climb, never lowers difficulty), confirmed through a warning menu. Prestige tiers permanently unlock killstreak abilities (no streak needed) and re-lock level-gated weapons for the new climb. - **Ranks**: named titles by level, shown to others via the **scoreboard clan tag** and a **colored chat tag** (with a dead-player marker). High prestiges render as a flowing animated multi-color tag. Level-ups announce in chat, play a sound, and call out any weapons unlocking. ### Passive stat tree Skill points buy levels in a registry of passive perks, all resolved through one shared combat math core: - **Offense** — bonus weapon damage, bonus headshot damage (above the engine multiplier), crit chance, and crit damage. - **Survivability** — max HP and max armor, lifesteal and armor lifesteal (extra on crits, with a minimum-heal floor), and **always-on flat HP/armor regen** with no out-of-combat gate. - **Thorns** — reflects a portion of damage taken back at bots as *real, attributed* damage that can kill and credit you, scaling with your build and handicap. - **XP boost** — increases all XP gains. ### Killstreak abilities Five timed abilities unlock as your streak grows: - **No Reload** — refills your clip on every shot. - **Adrenaline** — reduces damage taken. - **Overcharge** — boosts damage dealt. - **Bloodthirst** — bonus HP + armor lifesteal. - **Berserk** — scales your damage and crit damage up as your health nears death. Abilities are cast with a clever **grenade-key trick** (the matching grenade is granted only while the ability is castable and instantly confiscated, so it can never be thrown), or via chat commands / custom key binds. Each has its own duration and cooldown (which keeps ticking through death), a ready sound cue, and lives in a fully configurable registry. Reserve ammo is topped up in code so you effectively never run dry. ### Hidden handicap (the rubber-band balancer) A silent per-player system keeps matches competitive without anyone seeing numbers: - A single hidden index — blended from your **K/D, headshot rate, killstreak, level, and mode-progress** — scales your **damage dealt, damage taken, and XP rate together**, so they hit their extremes at the same thresholds. - **Dominate** and you deal less, take more, but earn far more XP; **struggle** and you get a **comeback buff** (deal more, take less) for less XP. - A **monotonic escalation floor** lets a mode tighten difficulty one-way (Survival uses it to escalate via the handicap rather than tougher bots); the **mode-progress axis** feeds in things like Gun Game ladder position. Each mode can **override** only the handicap fields it wants, and an easing curve shapes how sharply it bites. ### Quick-buy shop & weapons - An **in-world, two-panel shop** opens by pressing the healthshot key and freezes you while browsing. Because CS2 gives the server no raw key input, the menu is driven entirely by **polling your active weapon** — a knife-neutral state machine between presses, the zeus used as a detectable third menu key, and grenade keys for the rest. Options are labeled by *item*, so they stay correct no matter how you've bound your slots. - The **skills browser** spends points on stats grouped into damage / defense / utility; the **weapon browser** (in weapon-enabled modes — Gun Game's shop is skills-only) picks a level-gated primary/secondary; an **info panel** shows your live stats, rank, K/D, and multipliers. A flat **numbered chat menu** is available too. - **Weapons** are level-gated, with the strongest unlocking at the highest levels; your chosen loadout is **saved per account** and reapplied on spawn (falling back to defaults if a pick is locked). Level-up messages even spell out the exact menu keystrokes to reach a newly unlocked gun. - The shop cleans up safely on death, respawn, disconnect, or map change so no one is left frozen. ### Wave Survival: draft & effect cards - A **finite campaign** of escalating waves; clear them all to win, wipe or genuinely stall to lose. - **Bots never get tougher** — they keep stock health; difficulty rises purely through *more bots* and the tightening handicap floor. Each wave sets both a live bot count (ramping to a CPU-protecting ceiling) and a separate kill budget scaled by wave and squad size. - Between waves: a **break** to revive (downed survivors come back at reduced HP — an optional hardcore mode makes death permanent for the run) and to **draft**. - A **roguelite draft** offers strong, run-scoped cards that **stack on your permanent stats** as deliberate counter-pressure to the rising floor. Picks **bank** (never wasted), the hand is **fixed** so you can't re-roll by stalling, and each card has a pick cap. - *Stat cards:* damage, headshot, crit chance/damage, max HP/armor, lifesteal, HP regen. - *Effect cards:* **Burn** (armor-skipping stacking damage-over-time), **Explode-on-Kill** (real HE blasts that chain), **squad-wide team buffs** (compounding damage up / damage-taken down), **Berserker** (always-on near-death damage), **ability cooldown reduction**, **run-XP boost**, and **headshot armor**. The whole catalog is data-driven — new cards need no code. - **Run XP** banks separately and converts once at run-end, scaled by depth under a depth-growing cap. It's deliberately **anti-launder**: it excludes the gameable handicap multiplier and counts only your own attributed damage, so leeching earns almost nothing and a win pays the same as a wipe at equal depth. - Stall nudges teleport straggler bots to the squad so waves never hang; mid-run joiners wait in spectator for the next run; run state is RAM-only (no mid-run reconnect restore). ### HUD, feel & feedback - An always-on per-player **HUD** shows level, prestige, points, streak, XP progress, and your live combat multipliers, plus a color-coded **state icon per ability** (ready / active+timer / cooling+timer / locked). It renders as a crisp center panel or a positionable 3D world-text entity, and is hidden from other players so everyone sees only their own. - While an ability is active the HUD recolors and an optional faint full-screen "movie filter" tint plays (blending when several are up). **Sound cues** fire for level-up, prestige, ability ready/activate, and crits. - Toggles for the HUD and for a **per-hit damage readout** (true final damage by hitgroup) as a tuning aid. Modes contribute their own status lines (Survival's wave/bots-left, Gun Game's rung). ### Persistence & leaderboard - A **pluggable database** behind one repository interface: SQLite (dev) or PostgreSQL (prod). - **Permanent progression and the leaderboard are global** across all servers, while **in-progress round state is scoped per server**, so many instances share one database cleanly. - **RAM-first** with crash insurance: state is cached and flushed periodically plus on disconnect/shutdown. A leaver's round stats are kept for a mid-match rejoin; when the last player leaves, round state is wiped so the next session starts fresh. A `top` command lists the highest players. ### Commands - **Players:** skills, prestige, abilities, weapon selection, rank/live-stats, leaderboard, help/about (which explains the hidden handicap), and HUD/damage toggles. - **Admins:** grant XP/points, set level/prestige, full player reset, and live config reload — targetable by name, `@me`, or `@all`. ### Under the hood A few things that make it robust and unusual: - A **pure, CounterStrikeSharp-free domain layer** holds all progression/stat/combat/handicap math, so it's **unit-tested without the engine** and a **centralized combat resolver** guarantees the HUD/shop readouts never drift from the damage that actually applies. Hot per-hit/per-tick math runs off immutable, allocation-free snapshots and a single shared per-tick roster walk. - A **pluggable mode-driver architecture** — each mode supplies only its ruleset variant points, so adding a mode needs no core rewrite — and a **centralized engine-name/offset/write layer** that makes a CS2 update a one-line fix. - **Real, attributed engine damage at `sv_cheats 0`:** thorns, burn, and explosions are dealt as genuine engine damage via direct memory writes, so they credit kills properly; HE blasts use the game's **native grenade-spawn** so they arm and chain like thrown nades (with a manual-blast fallback). - **Launch flags** (`-outnumbered_mode`, `-outnumbered_server`) are read from `/proc/self/cmdline` because the embedded .NET host hides process arguments. - Pervasive **slot + SteamID pinning** defends against within-frame slot reuse (a disconnect handing a bot a human's slot, etc.), and **load-time self-checks** validate mode aliases, card keys, handicap overrides, the stat registry, and ability/icon alignment so misconfigurations fail loud. --- ## License Outnumbered is free software under the **GNU Affero General Public License v3.0**. If you run a modified version on a network server, the AGPL requires you to offer your users the source of your modifications. See [LICENSE](LICENSE).