Compare commits

...

3 commits

Author SHA1 Message Date
b1dd58850e release.sh: read the Forgejo token from ../../.forgejo-token when FORGEJO_TOKEN isn't set 2026-10-04 18:34:09 +01:00
a74e9946c8 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.
2026-10-03 00:22:12 +01:00
7eba09905a 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.
2026-10-03 00:20:33 +01:00
3 changed files with 57 additions and 15 deletions

View file

@ -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.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));
@ -80,6 +83,7 @@ public class AdminStealth : BasePlugin
_hideTyped.Clear();
_restoreScheduled.Clear();
_restoring.Clear();
_closingTeamScreen.Clear();
});
}
@ -122,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))
@ -136,20 +148,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<int> _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)

View file

@ -20,9 +20,12 @@ 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.
- 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

View file

@ -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