using System.Text.Json; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using Microsoft.Extensions.Logging; using Outnumbered.Config; using Outnumbered.Data; namespace Outnumbered; // Admin command suite (spec ยง13). Gated by @outnumbered/admin โ€” add your SteamID to // addons/counterstrikesharp/configs/admins.json with flag "@outnumbered/admin" (or "@css/root" for everything). // Target arg: a name substring, "@me", or "@all". Console (no caller) is always allowed by CSSharp. public sealed partial class OutnumberedPlugin { // PdOf(CCSPlayerController) lives in the resolution seam (Players.cs); the admin suite just uses it. private static List ResolveTargets(string pattern, CCSPlayerController? caller) { var humans = Humans().ToList(); if (pattern is "@all" or "@a") return humans; if ((pattern is "@me" or "@self") && caller is { IsValid: true }) return [caller]; return humans.Where(p => p.PlayerName.Contains(pattern, StringComparison.OrdinalIgnoreCase)).ToList(); } // Apply an action to every matched target's PlayerData, then persist + refresh HP/armor + clantag. private int ApplyToTargets(string pattern, CCSPlayerController? caller, Action action) { int n = 0; foreach (var t in ResolveTargets(pattern, caller)) { var pd = PdOf(t); if (pd is null) continue; action(t, pd); pd.Dirty = true; DeferReapplyCaps(t); // re-apply caps next frame (slot+SteamID-pinned) so a slot-reuse can't inherit this grant ApplyClan(t, pd); n++; } return n; } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_givexp", "[admin] grant XP")] [CommandHelper(2, " ", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_GiveXp(CCSPlayerController? player, CommandInfo info) { if (!long.TryParse(info.GetArg(2), out var amt)) { info.ReplyToCommand("[Outnumbered] amount must be a number."); return; } int n = ApplyToTargets(info.GetArg(1), player, (c, pd) => GrantXp(pd, amt, c)); info.ReplyToCommand($"[Outnumbered] gave {amt} XP to {n} player(s)."); } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_givepoints", "[admin] grant skill points")] [CommandHelper(2, " ", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_GivePoints(CCSPlayerController? player, CommandInfo info) { if (!int.TryParse(info.GetArg(2), out var amt)) { info.ReplyToCommand("[Outnumbered] amount must be a number."); return; } int n = ApplyToTargets(info.GetArg(1), player, (_, pd) => pd.Points = Math.Max(0, pd.Points + amt)); info.ReplyToCommand($"[Outnumbered] gave {amt} point(s) to {n} player(s)."); } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_setlevel", "[admin] set level")] [CommandHelper(2, " ", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_SetLevel(CCSPlayerController? player, CommandInfo info) { if (!int.TryParse(info.GetArg(2), out var lvl)) { info.ReplyToCommand("[Outnumbered] level must be a number."); return; } lvl = Math.Clamp(lvl, 1, Config.Progression.LevelCap); int n = ApplyToTargets(info.GetArg(1), player, (_, pd) => { pd.Level = lvl; pd.Xp = 0; }); info.ReplyToCommand($"[Outnumbered] set level {lvl} on {n} player(s)."); } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_setprestige", "[admin] set prestige")] [CommandHelper(2, " ", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_SetPrestige(CCSPlayerController? player, CommandInfo info) { if (!int.TryParse(info.GetArg(2), out var pres)) { info.ReplyToCommand("[Outnumbered] prestige must be a number."); return; } pres = Math.Clamp(pres, 0, Config.Progression.PrestigeCap); int n = ApplyToTargets(info.GetArg(1), player, (_, pd) => pd.Prestige = pres); info.ReplyToCommand($"[Outnumbered] set prestige {pres} on {n} player(s)."); } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_resetplayer", "[admin] full progression reset")] [CommandHelper(1, "", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_ResetPlayer(CCSPlayerController? player, CommandInfo info) { int n = ApplyToTargets(info.GetArg(1), player, (_, pd) => { pd.Xp = 0; pd.Level = 1; pd.Points = 1; pd.Prestige = 0; pd.Upgrades.Clear(); }); info.ReplyToCommand($"[Outnumbered] reset {n} player(s) to L1."); } [RequiresPermissions("@outnumbered/admin")] [ConsoleCommand("css_og_reload", "[admin] live-reload outnumbered.json + ranks.json (no session reset)")] [CommandHelper(0, "", CommandUsage.CLIENT_AND_SERVER)] public void Cmd_ReloadConfig(CCSPlayerController? player, CommandInfo info) { try { string path = ConfigPath("outnumbered.json"); // assembly-name-derived (Ranks.cs) โ€” no hardcoded folder literal if (File.Exists(path)) { var c = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true }); if (c is not null) Config = c; // sub-sections have new() defaults, so missing keys never null out } RebuildEffectiveHandicap(); // re-resolve base Handicap + the active mode's override from the new config RebuildStatDefs(); // re-resolve the key->StatDef registry from the new Config.Stats LoadRanksConfig(); RebuildBalancePayload(); // the API's balance verb serves the EFFECTIVE config โ€” must track every reload info.ReplyToCommand("[Outnumbered] reloaded outnumbered.json + ranks.json."); } catch (Exception ex) { info.ReplyToCommand("[Outnumbered] reload failed: " + ex.Message); Logger.LogError(ex, "Outnumbered og_reload failed"); } } }