Skip to content

Repository files navigation

Progression

Missions, achievements and a season pass for CS2 — one counting engine, driven by whatever your gamemodes emit.

ModSharp CS2


Players earn progress by playing. Objectives are defined as an event plus a filter — "10 AK headshots", "kills in Long A", "3 bomb plants" — so a new mission is authored, not coded. Daily, weekly, monthly, all-time achievements and a seasonal pass are all the same machinery with different reset rules.

🧩 How it fits together

Piece Responsibility
Progression.Core Pulls definitions, matches events locally, batches progress, runs reward commands. Owns buffering, retry and idempotency
Progression.Shared IProgressionShared (what gamemodes reference) and IProgressionStore (what storage implements)
Progression.Database Storage: MySQL. Install it and you need no backend at all
Progression.Ui !missions menu and completion announcements. Delete it if you want a different UI
Progression.Events.CS2 Standard CS2 events. The first satellite, not part of Core

Core contains no game knowledge. It does not know what a kill is. Every event source is a satellite — including the CS2 one — so adding a gamemode needs no change to Core. Deleting Progression.Events.CS2 leaves Core building and running.

🔌 Emitting from your gamemode

Reference YappersHQ.Progression.Shared with ExcludeAssets="runtime", resolve in OnAllModulesLoaded, and declare what you can emit:

var progression = moduleManager
    .GetOptionalSharpModuleInterface<IProgressionShared>(IProgressionShared.Identity)?.Instance;

progression?.RegisterEvent("duels.arena_won",
[
    new ProgField("arena",  ProgFieldKind.Int),
    new ProgField("weapon", ProgFieldKind.String),
]);

// later, when it happens
progression?.Emit("duels.arena_won", steamId, new Dictionary<string, object?>
{
    ["arena"]  = arenaId,
    ["weapon"] = weaponName,
});

Missions filtering on duels.arena_won can then be authored on the website with no code change — that is the entire point of the split.

Event keys are permanent. They are stored in mission definitions that live in a database for months, so namespace them by owner and never derive one from a runtime id.

Deploying it for the first time: docs/DEPLOY.md — ordered steps, each with a way to tell whether it worked, because several failures here are silent by construction (a wrong catalogue is empty, not an error).

💾 Storage — pick one

Progression stores nothing itself. Choose how it persists:

Option What you run When
Progression.Database A MySQL server You just want it to work. Owns its schema, migrates on load
Your own store One class You already have a database, or want Postgres/Redis/an existing API
Built-in HTTP client A web service You have a website and want authoring UI and a shared fleet

With Progression.Database, docs/example-missions.sql gives you eight working missions to start from — run it after the first boot, since objectives are only served for events a server has reported it can emit.

Writing your own is one project implementing IProgressionStore, registered in PostInit. Core resolves it in OnAllModulesLoaded and prefers it over HTTP.

You do not implement delivery. Buffering, idempotency keys, disk spill and retry stay in Core — a store is asked to write deltas and say what completed, never to re-derive exactly-once semantics. What it must guarantee is in docs/BACKEND_PROTOCOL.md.

⚙️ Configuration

Copy the examples in .assets/configs/ to <sharp>/configs/ and edit.

progression.json — Core:

Key Default Meaning
backend-url "" Backend base URL. Ignored when a store module is installed
server-key "" Sent as Authorization: Bearer. Not shared between servers
flush-seconds 10 How often buffered progress is sent
catalog-refresh-seconds 900 Backstop for definition changes
state-refresh-seconds 60 How often the in-game mission list refreshes

progression-database.json — only if using Progression.Database:

Key Default Meaning
connection-string "" MySQL. Needs CREATE TABLE on first run
gamemode "" Scopes which objectives this server is offered. Empty = all
timezone "UTC" Zone daily/weekly boundaries are measured in

🔧 How it works

Servers match filters locally and post only progress deltas — posting raw events would turn every kill on every server into an HTTP call. Gameplay never blocks on the network: emissions land in a buffer, a timer flushes, and a backend outage spills to disk and replays rather than losing progress.

Rewards are decided by the backend but must run on a server, so pending grants ride back in the response to the flush the server already made. No RCON, no inbound connection, no port to open.

Every delta carries an idempotency key generated at enqueue time, so replaying a batch that partially landed is safe rather than merely unlikely to double-count.

⌨️ Commands

Command What it does
!missions · !mission · !progress · !daily Your current missions and progress

Uses MenuManager when present and falls back to a chat listing when not. Mission titles resolve as locale keys, so a mission set can ship translations; a plain literal title still works.

📦 Build

dotnet build -c Release

Outputs .build/modules/ and .build/shared/Progression.Shared/.

To verify the MySQL store against a throwaway database (it writes rows — never point it at production):

dotnet run --project tools/StoreSelfTest -- "Server=127.0.0.1;User ID=u;Password=p;Database=throwaway"

29 checks covering idempotent replay, compare-and-swap progress, single-claim completion, grant leasing and period boundaries.

Made with ❤️ by yappershq

About

Missions, achievements and season-pass progression engine for CS2 servers.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages