cs2-outnumbered/Outnumbered/Config/OutnumberedConfig.cs
Kamal Tufekcic d701598350
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s
initial commit
2026-07-05 13:28:35 +03:00

360 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core;
namespace Outnumbered.Config;
// Auto-loaded from configs/plugins/outnumbered/outnumbered.json on first load.
// NOTE: the config sub-types the pure Domain layer reads (AbilityDef/AbilitiesConfig/HandicapConfig/HandicapOverride/
// ProgressionConfig/SurvivalCardDef/SurvivalConfig/StatDef) live in Config/DomainConfig.cs — split out so they're
// CounterStrikeSharp-free and the test project can compile them with Domain/. This file holds OutnumberedConfig (which
// extends the CSSharp BasePluginConfig) + the engine-coupled / presentation blocks.
public sealed class OutnumberedConfig : BasePluginConfig
{
// Which match driver runs this instance: "tdm" (default), "gungame", or "survival" (Driver.NormalizeMode also accepts
// 0/1/2 and aliases gg/wave). A per-instance launch decision — the whole RPG core is shared; only the match ruleset
// changes. Source of truth for the accepted values: Driver.ModeRegistry + NormalizeMode. (Mode-driver split, see Modes.cs.)
[JsonPropertyName("Mode")]
public string Mode { get; set; } = "tdm";
[JsonPropertyName("Database")]
public DatabaseConfig Database { get; set; } = new();
[JsonPropertyName("Api")]
public ApiConfig Api { get; set; } = new();
[JsonPropertyName("Website")]
public WebsiteConfig Website { get; set; } = new();
[JsonPropertyName("Match")]
public MatchConfig Match { get; set; } = new();
[JsonPropertyName("GunGame")]
public GunGameConfig GunGame { get; set; } = new();
[JsonPropertyName("Survival")]
public SurvivalConfig Survival { get; set; } = new();
[JsonPropertyName("Stats")]
public StatsConfig Stats { get; set; } = new();
[JsonPropertyName("Progression")]
public ProgressionConfig Progression { get; set; } = new();
[JsonPropertyName("Handicap")]
public HandicapConfig Handicap { get; set; } = new();
[JsonPropertyName("Abilities")]
public AbilitiesConfig Abilities { get; set; } = new();
[JsonPropertyName("Hud")]
public HudConfig Hud { get; set; } = new();
[JsonPropertyName("Shop")]
public ShopConfig Shop { get; set; } = new();
[JsonPropertyName("Sounds")]
public SoundsConfig Sounds { get; set; } = new();
[JsonPropertyName("FlushIntervalSeconds")]
public float FlushIntervalSeconds { get; set; } = 90f;
}
// Per-player sound cues (spec §7), played via the client `play` command. Paths are built-in CS2 sounds
// (same style GG2 uses). Empty string = no cue. Swap freely; `play <path>` resolves under csgo/.
public sealed class SoundsConfig
{
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
[JsonPropertyName("LevelUp")] public string LevelUp { get; set; } = "sounds/ui/armsrace_level_up.wav";
[JsonPropertyName("Prestige")] public string Prestige { get; set; } = "sounds/ui/armsrace_level_up.wav";
[JsonPropertyName("AbilityReady")] public string AbilityReady { get; set; } = "sounds/ambient/office/tech_oneshot_08.wav";
[JsonPropertyName("AbilityActivate")] public string AbilityActivate { get; set; } = "sounds/training/pointscored.wav";
[JsonPropertyName("Crit")] public string Crit { get; set; } = ""; // off by default — per-crit can get spammy
}
// The local read-only status/balance/top API, served over a per-instance Unix domain socket (Api.cs). The companion
// website is the consumer; there is no network exposure — the socket is a filesystem path, access = file permissions.
public sealed class ApiConfig
{
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
// Directory holding the per-instance sockets (<SocketDir>/<ServerId>.sock). Created externally (systemd tmpfiles.d);
// when it's missing or unwritable the API disables itself with one log line instead of failing the plugin load.
[JsonPropertyName("SocketDir")] public string SocketDir { get; set; } = "/run/outnumbered";
}
// The companion-website plug: a periodic chat line + an !about line pointing players at the site. Url empty = the
// whole feature is OFF (the default — an operator without a site gets no dangling advert). Url is read live per
// announce (!og_reload applies); changing the interval needs a plugin reload to re-arm the timer.
public sealed class WebsiteConfig
{
[JsonPropertyName("Url")] public string Url { get; set; } = "";
[JsonPropertyName("AnnounceIntervalSeconds")] public float AnnounceIntervalSeconds { get; set; } = 300f;
}
// The always-on HUD (spec §7): handicap multipliers + streak + the 5 ability states.
public sealed class HudConfig
{
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
// "center" = center HTML panel (crisp, multi-color, 2D, no lag — fixed upper-center + width). DEFAULT.
// "world" = CPointWorldText entity (positionable/wide/sized, but single-color, 3D parallax, lags on movement
// since the viewmodel isn't accessible to parent to — kept as an option, not recommended).
[JsonPropertyName("Mode")] public string Mode { get; set; } = "center";
// Refresh cadence in ticks. Center HTML fades only after ~5s, so it does NOT need per-tick resends: 4 (=16 Hz at
// 64 tick) keeps countdowns smooth at a quarter of the per-human message + string-build cost of 1. Every refresh
// sends a reliable usermessage per human, so this dial is also the HUD's client/network footprint.
[JsonPropertyName("RefreshEveryTicks")] public int RefreshEveryTicks { get; set; } = 4;
// Ability icons for the center HUD (indexed 0..4: No Reload, Adrenaline, Overcharge, Bloodthirst, Berserk).
// Glyph rendering depends on the client font — if any show as a box, swap it here and `css_plugins reload`.
// Defaults kept in BMP symbol blocks that render on the CS2 panel font (astral emoji like 🔥💧 show as boxes).
[JsonPropertyName("AbilityIcons")]
public List<string> AbilityIcons { get; set; } = new()
{ "∞", "⛨", "⇈", "♥", "☠" };
// ---- world-text placement — LIVE-TUNABLE: edit JSON, then `css_plugins reload outnumbered` (no rebuild) ----
[JsonPropertyName("ForwardOffset")] public float ForwardOffset { get; set; } = 7f; // units in front of the eye (keep small so walls don't clip it)
[JsonPropertyName("RightOffset")] public float RightOffset { get; set; } = 0f; // + = right, - = left
[JsonPropertyName("UpOffset")] public float UpOffset { get; set; } = -2.6f; // - = lower on screen
[JsonPropertyName("FontSize")] public float FontSize { get; set; } = 26f;
[JsonPropertyName("WorldUnitsPerPx")] public float WorldUnitsPerPx { get; set; } = 0.0075f; // smaller = smaller/finer text
[JsonPropertyName("FontName")] public string FontName { get; set; } = "Arial Bold";
[JsonPropertyName("DrawBackground")] public bool DrawBackground { get; set; } = true;
[JsonPropertyName("ColorR")] public int ColorR { get; set; } = 255;
[JsonPropertyName("ColorG")] public int ColorG { get; set; } = 255;
[JsonPropertyName("ColorB")] public int ColorB { get; set; } = 255;
}
// The quick-buy shop world-text panel. Opened by switching to the healthshot (X), frozen while open.
// Placement is LIVE-TUNABLE: edit JSON, then `css_plugins reload outnumbered` (no rebuild) to reposition.
public sealed class ShopConfig
{
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
[JsonPropertyName("ForwardOffset")] public float ForwardOffset { get; set; } = 7f; // units in front of the eye
[JsonPropertyName("RightOffset")] public float RightOffset { get; set; } = 0f; // centre anchor: + = right, - = left
[JsonPropertyName("SplitOffset")] public float SplitOffset { get; set; } = 3f; // half-gap: left panel sits -this, right panel +this
[JsonPropertyName("CardSpread")] public float CardSpread { get; set; } = 6f; // survival draft: horizontal gap between the 3 cards (centre card at RightOffset)
[JsonPropertyName("UpOffset")] public float UpOffset { get; set; } = 0f; // + = higher, - = lower
[JsonPropertyName("FontSize")] public float FontSize { get; set; } = 40f;
[JsonPropertyName("WorldUnitsPerPx")] public float WorldUnitsPerPx { get; set; } = 0.011f; // smaller = finer
[JsonPropertyName("InfoFontSize")] public float InfoFontSize { get; set; } = 34f; // right (info) panel: smaller than shop, but readable
[JsonPropertyName("InfoWorldUnitsPerPx")] public float InfoWorldUnitsPerPx { get; set; } = 0.0085f;
// Server/about text shown atop the info panel (the !info / !about blurb) — edit per-server. Keep lines SHORT
// (this panel is narrow/tall by design); long lines run off-screen at lower resolutions.
[JsonPropertyName("InfoLines")]
public List<string> InfoLines { get; set; } = new()
{
"=== OUTNUMBERED ===",
"Kill bots -> XP -> level.",
"Spend points in Skills.",
"L100: !prestige resets you",
"for a permanent boost.",
"Streaks unlock abilities",
"(grenade keys 6-0).",
"Handicap scales bots",
"to your power.",
"",
};
[JsonPropertyName("FontName")] public string FontName { get; set; } = "Arial Bold";
[JsonPropertyName("DrawBackground")] public bool DrawBackground { get; set; } = true;
[JsonPropertyName("ColorR")] public int ColorR { get; set; } = 255;
[JsonPropertyName("ColorG")] public int ColorG { get; set; } = 255;
[JsonPropertyName("ColorB")] public int ColorB { get; set; } = 255;
}
// Separate file outnumbered.ranks.json (NOT auto-loaded by CSSharp — loaded manually, see Ranks.cs). Spec §9.
public sealed class RanksConfig
{
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
[JsonPropertyName("ShowClanTag")] public bool ShowClanTag { get; set; } = true; // scoreboard tag via m_szClan
[JsonPropertyName("ChatTag")] public bool ChatTag { get; set; } = true; // coloured rank tag in chat (reformats chat)
// Prestige tag colouring: I-V = the unlocked perk's tint; VI-VII 2-colour, VIII-IX 3-colour, X all-5 (HUD animated, chat static).
[JsonPropertyName("PrestigeColors")] public bool PrestigeColors { get; set; } = true;
[JsonPropertyName("TopCount")] public int TopCount { get; set; } = 10; // !top size
// {0} = Roman prestige; only shown when prestige > 0. e.g. "P {0}" -> "P III".
[JsonPropertyName("PrestigeTagFormat")] public string PrestigeTagFormat { get; set; } = "P{0}";
// Highest MinLevel that is <= the player's level wins. Levels are within a prestige (cap 100).
[JsonPropertyName("LevelRanks")]
public List<RankTier> LevelRanks { get; set; } = new()
{
new(1, "Recruit"), new(26, "Soldier"), new(51, "Veteran"), new(76, "Elite"), new(100, "Apex"),
};
}
public sealed class RankTier
{
[JsonPropertyName("MinLevel")] public int MinLevel { get; set; }
[JsonPropertyName("Name")] public string Name { get; set; } = "";
public RankTier() { }
public RankTier(int minLevel, string name) { MinLevel = minLevel; Name = name; }
}
public sealed class DatabaseConfig
{
// "sqlite" for dev; "postgres" is the final-step swap (no logic change — repository interface).
[JsonPropertyName("Provider")]
public string Provider { get; set; } = "sqlite";
// Relative to csgo/ . Defaults next to the plugin's own config.
[JsonPropertyName("SqliteFile")]
public string SqliteFile { get; set; } = "addons/counterstrikesharp/configs/plugins/outnumbered/outnumbered.db";
// Unused while Provider == "sqlite"; filled in for the Postgres phase.
[JsonPropertyName("PostgresConnectionString")]
public string PostgresConnectionString { get; set; } = "Host=localhost;Port=5432;Database=outnumbered;Username=outnumbered;Password=";
}
// The TDM match driver (spec §1). Humans on CT vs bots on T, continuous respawn, no objectives.
public sealed class MatchConfig
{
[JsonPropertyName("BotsPerHuman")] public int BotsPerHuman { get; set; } = 4;
[JsonPropertyName("MaxBots")] public int MaxBots { get; set; } = 12;
[JsonPropertyName("MaxHumansOnCt")] public int MaxHumansOnCt { get; set; } = 3;
[JsonPropertyName("RespawnDelaySeconds")] public float RespawnDelaySeconds { get; set; } = 1.0f; // DEPRECATED — native DM respawn handles timing; no longer read (kept so existing JSON doesn't error).
// 5s so you can spend skill points on respawn; it breaks the instant you fire, so it's not abusable (PvE anyway).
[JsonPropertyName("SpawnProtectionSeconds")] public float SpawnProtectionSeconds { get; set; } = 5.0f;
// Map ends when ANY single player reaches this many kills (spec §1).
[JsonPropertyName("KillGoal")] public int KillGoal { get; set; } = 250;
[JsonPropertyName("MapChangeDelaySeconds")] public float MapChangeDelaySeconds { get; set; } = 6.0f;
[JsonPropertyName("Maps")]
public List<string> Maps { get; set; } = new()
{ "cs_italy", "de_dust2", "de_vertigo", "de_nuke", "de_inferno" };
// bot_difficulty caps at 4 on this build (+ custom_bot_difficulty) — cookbook.
[JsonPropertyName("BotDifficulty")] public int BotDifficulty { get; set; } = 4;
// Base loadout = the only weapons free from level 1; everything else is level-gated (WeaponUnlockLevel).
// Prestige resets level → re-locks everything back to these. Persisted per-player choice falls back here when locked.
[JsonPropertyName("HumanPrimary")] public string HumanPrimary { get; set; } = "weapon_mp9";
[JsonPropertyName("HumanSecondary")] public string HumanSecondary { get; set; } = "weapon_usp_silencer";
// weapon -> level required to select it (absent/0 = free). Weak/cheap early, the cheese (AK/AWP/Negev/autos) late.
[JsonPropertyName("WeaponUnlockLevel")]
public Dictionary<string, int> WeaponUnlockLevel { get; set; } = new()
{
["weapon_glock"] = 2,
["weapon_p250"] = 4,
["weapon_mac10"] = 6,
["weapon_nova"] = 8,
["weapon_hkp2000"] = 10,
["weapon_bizon"] = 12,
["weapon_mp7"] = 14,
["weapon_galilar"] = 16,
["weapon_sawedoff"] = 18,
["weapon_elite"] = 20,
["weapon_famas"] = 22,
["weapon_ump45"] = 24,
["weapon_fiveseven"] = 27,
["weapon_tec9"] = 30,
["weapon_mp5sd"] = 33,
["weapon_mag7"] = 36,
["weapon_ssg08"] = 39,
["weapon_p90"] = 42,
["weapon_aug"] = 45,
["weapon_cz75a"] = 48,
["weapon_xm1014"] = 51,
["weapon_sg556"] = 55,
["weapon_deagle"] = 59,
["weapon_revolver"] = 63,
["weapon_m4a1"] = 67,
["weapon_m4a1_silencer"] = 71,
["weapon_ak47"] = 75,
["weapon_m249"] = 80,
["weapon_scar20"] = 85,
["weapon_g3sg1"] = 90,
["weapon_negev"] = 95,
["weapon_awp"] = 100,
};
// Free-selection weapon menu (!guns), split into categories (!rifles/!smgs/!snipers/!shotguns/!heavy/!pistols).
// Edit to taste; entries must be valid weapon_ item names. "Pistols" are the secondary slot; the rest are primary.
[JsonPropertyName("Rifles")]
public List<string> Rifles { get; set; } = new()
{ "weapon_ak47", "weapon_m4a1_silencer", "weapon_m4a1", "weapon_aug", "weapon_sg556", "weapon_galilar", "weapon_famas" };
[JsonPropertyName("Smgs")]
public List<string> Smgs { get; set; } = new()
{ "weapon_mp9", "weapon_mp7", "weapon_mp5sd", "weapon_ump45", "weapon_p90", "weapon_mac10", "weapon_bizon" };
[JsonPropertyName("Snipers")]
public List<string> Snipers { get; set; } = new()
{ "weapon_awp", "weapon_ssg08", "weapon_scar20", "weapon_g3sg1" };
[JsonPropertyName("Shotguns")]
public List<string> Shotguns { get; set; } = new()
{ "weapon_nova", "weapon_xm1014", "weapon_mag7", "weapon_sawedoff" };
[JsonPropertyName("Heavy")]
public List<string> Heavy { get; set; } = new()
{ "weapon_m249", "weapon_negev" };
[JsonPropertyName("Pistols")]
public List<string> Pistols { get; set; } = new()
{ "weapon_deagle", "weapon_revolver", "weapon_glock", "weapon_usp_silencer", "weapon_hkp2000",
"weapon_p250", "weapon_fiveseven", "weapon_tec9", "weapon_cz75a", "weapon_elite" };
// Bot squad pool — cycled per bot spawn for a mixed T side (full squad templates come later).
[JsonPropertyName("BotWeapons")]
public List<string> BotWeapons { get; set; } = new()
{ "weapon_ak47", "weapon_ak47", "weapon_awp", "weapon_nova" };
[JsonPropertyName("BotGrenades")]
public List<string> BotGrenades { get; set; } = new()
{ "weapon_hegrenade" };
}
// One Gun Game ladder rung: a weapon + its strength tier (1=weak/hard-to-use .. 5=cheese). The tier indexes the
// per-tier kill curves below — it does NOT affect the climb ORDER (that's this list's order, shared by players+bots).
public sealed class GunGameRung
{
[JsonPropertyName("Weapon")] public string Weapon { get; set; } = "";
[JsonPropertyName("Tier")] public int Tier { get; set; } = 1;
public GunGameRung() { }
public GunGameRung(string weapon, int tier) { Weapon = weapon; Tier = tier; }
}
// Gun Game mode (Mode="gungame", see Modes.cs). Climb a weapon ladder by kills; the full RPG kit rides along.
// DUAL pacing: kills-to-advance a rung is driven by the weapon's TIER, and players vs bots use INVERSE curves —
// players grind weak guns + breeze strong ones (so an HS-demotion into the grind stings); bots skip weak guns
// fast + linger on strong ones (so they reach dangerous weapons instead of being stuck on pistols). The ORDER is
// shared (bots have no muscle memory) and zig-zags weapon types so muscle memory never settles.
public sealed class GunGameConfig
{
// Kills to clear a rung, indexed by tier-1 (tier 1..5). The player curve grinds weak guns / breezes strong ones.
// Bots default to a FLAT 1 kill/rung — with BotSharedLadder (per-batch pooling) a batch then needs ~ladder-length
// collective kills to win, which only lands once a player's K/D falls into handicap territory.
[JsonPropertyName("PlayerKillsByTier")] public List<int> PlayerKillsByTier { get; set; } = new() { 10, 8, 5, 2, 1 };
[JsonPropertyName("BotKillsByTier")] public List<int> BotKillsByTier { get; set; } = new() { 1, 1, 1, 1, 1 };
// Pool bot ladder progress per BATCH of BotsPerHuman (true, default): bots are grouped by slot rank into batches
// of BotsPerHuman, and each batch shares ONE rung that advances on ANY of its members' kills — so a batch (≈ one
// player's worth of bots) climbs ~BotsPerHuman× faster and can actually win, and the horde scales with player
// count (1 player -> 1 batch, 2 -> 2, ... up to MaxBots). false = each bot climbs its own rung (rarely tops).
[JsonPropertyName("BotSharedLadder")] public bool BotSharedLadder { get; set; } = true;
// Max humans on CT for THIS mode (GG wants more than the "outnumbered" TDM default of 3). Per-mode so ONE shared
// config can run both; the driver feeds it to EnforceHumanTeam.
[JsonPropertyName("MaxHumansOnCt")] public int MaxHumansOnCt { get; set; } = 5;
// Append a final knife rung — the classic Gun Game capstone (the win is a knife kill; 1 kill for both sides).
[JsonPropertyName("KnifeFinale")] public bool KnifeFinale { get; set; } = true;
// Map pool for this mode (changelevel rotation on a win). EMPTY = fall back to Match.Maps. Put the small
// arms-race maps here once you've confirmed the exact names installed on your server (e.g. ar_shoots, ar_baggage).
[JsonPropertyName("Maps")] public List<string> Maps { get; set; } = new();
// Per-mode handicap override (null = inherit the base Handicap unchanged). GG: weights the ladder-position
// progress axis (climbing nerfs you; a bot-demotion eases it), runs a rougher sub-1 Curve (bites from the start),
// and DOUBLES MasterDifficulty so the nerf cap (0.1x deal / 8x taken) is actually reachable on small GG maps —
// it's hit at n≈0.5 (half-maxed factors) instead of needing K/D+HS+streak+level+ladder ALL maxed, so a good
// climber gets driven to the floor and can no longer headshot-suppress the horde. All live-tunable via !og_reload.
// (Heads-up: MasterDifficulty scales both sides, so a struggling low-K/D player also gets a stronger comeback buff.)
[JsonPropertyName("Handicap")] public HandicapOverride? Handicap { get; set; } = new() { MasterDifficulty = 2.0, ProgressWeight = 3.0, Curve = 0.8, MDealFloor = 0.1 };
// The ladder: weapon order (shared) + per-weapon tier. EMPTY -> code falls back to a knife-only ladder (safety).
// Order zig-zags categories (no two consecutive the same; cheese scattered, not clustered).
[JsonPropertyName("Ladder")]
public List<GunGameRung> Ladder { get; set; } = new()
{
new("weapon_usp_silencer", 1), new("weapon_famas", 4), new("weapon_mac10", 2), new("weapon_mag7", 4),
new("weapon_aug", 5), new("weapon_fiveseven", 3), new("weapon_ump45", 4), new("weapon_scar20", 5),
new("weapon_nova", 3), new("weapon_m4a1", 5), new("weapon_p250", 2), new("weapon_mp5sd", 4),
new("weapon_ssg08", 4), new("weapon_galilar", 4), new("weapon_mp7", 3), new("weapon_xm1014", 4),
new("weapon_deagle", 5), new("weapon_bizon", 2), new("weapon_sg556", 5), new("weapon_tec9", 4),
new("weapon_m249", 4), new("weapon_mp9", 1), new("weapon_g3sg1", 5), new("weapon_sawedoff", 4),
new("weapon_revolver", 4), new("weapon_m4a1_silencer", 5), new("weapon_p90", 3), new("weapon_negev", 4),
new("weapon_elite", 4), new("weapon_ak47", 5), new("weapon_hkp2000", 1), new("weapon_awp", 5),
};
}