initial commit
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s

This commit is contained in:
Kamal Tufekcic 2026-07-05 13:28:35 +03:00
commit d701598350
67 changed files with 9351 additions and 0 deletions

300
README.md Normal file
View file

@ -0,0 +1,300 @@
# 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:
```
<cs2>/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 <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.
```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).