- C# 100%
| .forgejo/workflows | ||
| Outnumbered | ||
| Outnumbered.Tests | ||
| .editorconfig | ||
| .gitignore | ||
| Directory.Build.props | ||
| Directory.Build.targets | ||
| global.json | ||
| LICENSE | ||
| Outnumbered.slnx | ||
| README.md | ||
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. Linux-only. Licensed under AGPL-3.0 — see 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. Grab the latest tarball and
extract its addons/ folder into your server's game/csgo/ — that's the whole install, nothing to
compile:
<cs2>/game/csgo/addons/counterstrikesharp/plugins/outnumbered/
Then:
-
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 withmeta listandcss_plugins list. -
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). -
Launch with the mode, and a GSLT for a public server — e.g.:
./cs2 -dedicated -insecure +game_type 1 +game_mode 2 -maxplayers 25 +map ar_shoots \ -port 27015 +sv_setsteamaccount <your-token> -outnumbered_mode tdm
Notes
- Mode is
-outnumbered_mode tdm|gg|survival. - Public listing needs a GSLT (
+sv_setsteamaccount <token>, appid 730) and an open UDP game port; without a token the server is LAN-only. Mint tokens at https://steamcommunity.com/dev/managegameservers 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.
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.
# 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
topcommand 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/cmdlinebecause 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.