From 7eba09905a55940a57fb03111e1219e6142b94d9 Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 00:20:33 +0100 Subject: [PATCH 1/3] AdminStealth 0.3.14: a team pick closes the team screen for hidden admins Telling only their client they joined spectators left the screen open, so the admin had to unhide to get out. Move them to spectators and back to no team, as css_hide does, with sv_disable_teamselect_menu on around it. --- AdminStealth.cs | 43 +++++++++++++++++++++++++++++++++---------- README.md | 5 +++-- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/AdminStealth.cs b/AdminStealth.cs index 317484c..f4ee8fb 100644 --- a/AdminStealth.cs +++ b/AdminStealth.cs @@ -23,7 +23,7 @@ namespace AdminStealth; public class AdminStealth : BasePlugin { public override string ModuleName => "AdminStealth"; - public override string ModuleVersion => "0.3.13"; + public override string ModuleVersion => "0.3.14"; public override string ModuleAuthor => "astra"; public override string ModuleDescription => "Makes CS2-SimpleAdmin's css_hide look like leaving, and keeps it"; @@ -80,6 +80,7 @@ public class AdminStealth : BasePlugin _hideTyped.Clear(); _restoreScheduled.Clear(); _restoring.Clear(); + _closingTeamScreen.Clear(); }); } @@ -136,20 +137,42 @@ public class AdminStealth : BasePlugin player!.PrintToChat($" You're hidden, so you can't join a team. Type {ChatAlias()} to show yourself."); - // M opens the team screen on the client, where the server can't block it, and a player with - // no team (as hidden admins are) can't close it without picking. Answer the pick by telling - // only their client they joined spectators, with the team screen disabled around it, so - // nobody else ever sees them on a team. + CloseTeamScreen(player); + return HookResult.Stop; + } + + // M opens the team screen on the client, where the server can't block it, and a player with no + // team (as hidden admins are) can't close it without picking. Telling only their client they + // joined spectators doesn't close it (tried live), so do what css_hide does: really move them + // to spectators and back to no team, with the team screen disabled around it. Their team + // events aren't broadcast while hidden (OnPlayerTeam), and our SimpleAdmin fork doesn't unhide + // for spectators. + private readonly HashSet _closingTeamScreen = []; + + private void CloseTeamScreen(CCSPlayerController player) + { var slot = player.Slot; + if (!_closingTeamScreen.Add(slot)) + return; + Server.ExecuteCommand("sv_disable_teamselect_menu 1"); - AddTimer(0.3f, () => + Server.NextFrame(() => { var p = Utilities.GetPlayerFromSlot(slot); - if (p is { IsValid: true } && IsHidden(p)) - SendTeamEvent(p, (int)CsTeam.Spectator, p.TeamNum); + if (p is { IsValid: true } && IsHidden(p) && p.TeamNum == (byte)CsTeam.None) + p.ChangeTeam(CsTeam.Spectator); + }); + AddTimer(0.15f, () => + { + var p = Utilities.GetPlayerFromSlot(slot); + if (p is { IsValid: true } && IsHidden(p) && p.TeamNum == (byte)CsTeam.Spectator) + p.ChangeTeam(CsTeam.None); + }, TimerFlags.STOP_ON_MAPCHANGE); + AddTimer(0.5f, () => + { + _closingTeamScreen.Remove(slot); + Server.ExecuteCommand("sv_disable_teamselect_menu 0"); }, TimerFlags.STOP_ON_MAPCHANGE); - AddTimer(0.6f, () => Server.ExecuteCommand("sv_disable_teamselect_menu 0"), TimerFlags.STOP_ON_MAPCHANGE); - return HookResult.Stop; } private HookResult OnTeamMenu(CCSPlayerController? player, CommandInfo command) diff --git a/README.md b/README.md index 5f82ae0..19b1e81 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ admin off the scoreboard. This plugin makes it look like they left the server: the team select screen doesn't open, until the restore hides them again. - The team select screen: the server's "team" menu message (`CS_UM_VGUIMenu`) is never sent to a hidden admin. M opens it on the client regardless, and a player with no team can't close it - without picking, so a pick is answered by telling only that client it joined spectators - (with `sv_disable_teamselect_menu` on around it); nobody else sees a team change. + without picking, so a pick moves them to spectators and straight back to no team, as `css_hide` + does (with `sv_disable_teamselect_menu` on around it), which closes it. The team events aren't + broadcast; for about 0.15 s they're on the spectator list of anyone looking at the scoreboard. - Restoring runs `css_hide` at most once: it toggles, so a second run could unhide them. Needs our SimpleAdmin fork (1.8.2b-zio2 or later), where moving to spectators doesn't unhide a From a74e9946c8f46320db925ef0051c99256234395a Mon Sep 17 00:00:00 2001 From: Astra Date: Sat, 3 Oct 2026 00:22:12 +0100 Subject: [PATCH 2/3] AdminStealth 0.3.15: no reply to css_hide from players who may not hide A catch-all command listener stops it before any handler, so CSS's missing-permission message isn't sent either. --- AdminStealth.cs | 13 ++++++++++++- README.md | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/AdminStealth.cs b/AdminStealth.cs index f4ee8fb..1351fd6 100644 --- a/AdminStealth.cs +++ b/AdminStealth.cs @@ -23,7 +23,7 @@ namespace AdminStealth; public class AdminStealth : BasePlugin { public override string ModuleName => "AdminStealth"; - public override string ModuleVersion => "0.3.14"; + public override string ModuleVersion => "0.3.15"; public override string ModuleAuthor => "astra"; public override string ModuleDescription => "Makes CS2-SimpleAdmin's css_hide look like leaving, and keeps it"; @@ -66,6 +66,9 @@ public class AdminStealth : BasePlugin // without ever calling us. foreach (var alias in _hideAliases) AddCommandListener(alias, (player, command) => OnHideTyped(player, command)); + // Players who may not hide get no reply at all, not even CSS's missing-permission one. Only + // a catch-all listener runs before every plugin's command handler, for console and chat. + AddCommandListener(null, OnAnyCommand); // Joining any team, spectators included, would make SimpleAdmin unhide them. AddCommandListener("jointeam", (player, command) => OnJoinTeam(player, command)); AddCommandListener("spectate", (player, command) => OnJoinTeam(player, command)); @@ -123,6 +126,14 @@ public class AdminStealth : BasePlugin return player == null ? HookResult.Continue : HookResult.Stop; } + private HookResult OnAnyCommand(CCSPlayerController? player, CommandInfo command) + { + if (player is not { IsValid: true, IsBot: false } || + !_hideAliases.Contains(command.GetArg(0), StringComparer.OrdinalIgnoreCase) || CanHide(player)) + return HookResult.Continue; + return HookResult.Stop; + } + private HookResult OnHideTyped(CCSPlayerController? player, CommandInfo command) { if (player is { IsValid: true } && !_restoring.Contains(player.Slot)) diff --git a/README.md b/README.md index 19b1e81..edd38d6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ admin off the scoreboard. This plugin makes it look like they left the server: does (with `sv_disable_teamselect_menu` on around it), which closes it. The team events aren't broadcast; for about 0.15 s they're on the spectator list of anyone looking at the scoreboard. - Restoring runs `css_hide` at most once: it toggles, so a second run could unhide them. +- Players who may not use `css_hide` (or its aliases) get no reply at all when they try it, from + console or chat: not even CSS's missing-permission message. Needs our SimpleAdmin fork (1.8.2b-zio2 or later), where moving to spectators doesn't unhide a hidden admin. Never hot-reload CS2-SimpleAdmin while this runs: CSS keeps handing out the old From b1dd58850eb134059f8e18127c031aa458f03d5f Mon Sep 17 00:00:00 2001 From: Astra Date: Sun, 4 Oct 2026 18:34:09 +0100 Subject: [PATCH 3/3] release.sh: read the Forgejo token from ../../.forgejo-token when FORGEJO_TOKEN isn't set --- release.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/release.sh b/release.sh index a3b6a11..3c7228f 100755 --- a/release.sh +++ b/release.sh @@ -19,8 +19,13 @@ set -euo pipefail FORGEJO_URL="https://git.zio.sh" OWNER="cs2" REPO="AdminStealth" -# Placeholder - replace, or set FORGEJO_TOKEN in the environment instead. Needs write:repository. -# Only uploading needs it; downloading from the public repo doesn't. Don't commit a real token. +# Needs write:repository. Only uploading needs it; downloading from the public repo doesn't. From the +# environment, or else from .forgejo-token in the cs2/ folder above every plugin (outside their +# repos, mode 600). Don't commit a real token. +TOKEN_FILE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../../.forgejo-token" +if [[ -z "${FORGEJO_TOKEN:-}" && -r "$TOKEN_FILE" ]]; then + FORGEJO_TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +fi FORGEJO_TOKEN="${FORGEJO_TOKEN:-CHANGE_ME_FORGEJO_TOKEN}" # Only these ship from the publish output. CounterStrikeSharp.API.dll is excluded by the csproj @@ -37,7 +42,7 @@ if [[ -z "$TAG" ]]; then exit 1 fi if [[ "$FORGEJO_TOKEN" == "CHANGE_ME_FORGEJO_TOKEN" ]]; then - echo "Set FORGEJO_TOKEN (edit release.sh or export it) first." >&2 + echo "Set FORGEJO_TOKEN (export it, or put it in $TOKEN_FILE) first." >&2 exit 1 fi for tool in podman jq curl git; do