AdminStealth 0.3.12: keep hidden admins off the team select screen
Hidden admins skip the team screen on reconnect (moved to spectators until re-hidden), never receive the server's team menu message, and a pick from the screen M opens is answered by telling only their client they joined spectators. The restore runs css_hide at most once, since it toggles. Each command listener gets its own delegate: CSS keys listeners by handler, and reusing one orphaned the first registration, which then swallowed its command.
This commit is contained in:
parent
b60f508a7b
commit
33feda8179
2 changed files with 133 additions and 6 deletions
124
AdminStealth.cs
124
AdminStealth.cs
|
|
@ -7,6 +7,7 @@ using CounterStrikeSharp.API.Modules.Admin;
|
||||||
using CounterStrikeSharp.API.Modules.Commands;
|
using CounterStrikeSharp.API.Modules.Commands;
|
||||||
using CounterStrikeSharp.API.Modules.Entities;
|
using CounterStrikeSharp.API.Modules.Entities;
|
||||||
using CounterStrikeSharp.API.Modules.Timers;
|
using CounterStrikeSharp.API.Modules.Timers;
|
||||||
|
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||||
using CounterStrikeSharp.API.Modules.Utils;
|
using CounterStrikeSharp.API.Modules.Utils;
|
||||||
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
using CounterStrikeSharp.API.ValveConstants.Protobuf;
|
||||||
using CS2_SimpleAdminApi;
|
using CS2_SimpleAdminApi;
|
||||||
|
|
@ -22,7 +23,7 @@ namespace AdminStealth;
|
||||||
public class AdminStealth : BasePlugin
|
public class AdminStealth : BasePlugin
|
||||||
{
|
{
|
||||||
public override string ModuleName => "AdminStealth";
|
public override string ModuleName => "AdminStealth";
|
||||||
public override string ModuleVersion => "0.2.0";
|
public override string ModuleVersion => "0.3.12";
|
||||||
public override string ModuleAuthor => "astra";
|
public override string ModuleAuthor => "astra";
|
||||||
public override string ModuleDescription => "Makes CS2-SimpleAdmin's css_hide look like leaving, and keeps it";
|
public override string ModuleDescription => "Makes CS2-SimpleAdmin's css_hide look like leaving, and keeps it";
|
||||||
|
|
||||||
|
|
@ -30,6 +31,11 @@ public class AdminStealth : BasePlugin
|
||||||
private const string HidePermission = "@css/kick";
|
private const string HidePermission = "@css/kick";
|
||||||
private const float RestoreDelay = 2f;
|
private const float RestoreDelay = 2f;
|
||||||
private const int RestoreAttempts = 5;
|
private const int RestoreAttempts = 5;
|
||||||
|
// How soon after connecting a hidden admin is moved to spectators, past the team select screen.
|
||||||
|
private const float SkipTeamSelectDelay = 0.1f;
|
||||||
|
// CS_UM_VGUIMenu: how the server opens the team select screen, as name "team". CS2 opens it
|
||||||
|
// whatever the message's "show" says (the server doesn't even set it), so it can't close one.
|
||||||
|
private const int VguiMenuMessage = 301;
|
||||||
|
|
||||||
private static readonly PluginCapability<ICS2_SimpleAdminApi> SimpleAdminCapability = new("simpleadmin:api");
|
private static readonly PluginCapability<ICS2_SimpleAdminApi> SimpleAdminCapability = new("simpleadmin:api");
|
||||||
private ICS2_SimpleAdminApi? _simpleAdmin;
|
private ICS2_SimpleAdminApi? _simpleAdmin;
|
||||||
|
|
@ -54,12 +60,19 @@ public class AdminStealth : BasePlugin
|
||||||
{
|
{
|
||||||
LoadHidden();
|
LoadHidden();
|
||||||
_hideAliases = ReadHideAliases();
|
_hideAliases = ReadHideAliases();
|
||||||
|
// Each listener needs its own delegate: CSS keys a plugin's listeners by handler, so adding
|
||||||
|
// the same method twice orphans the first registration, which then blocks its command
|
||||||
|
// without ever calling us.
|
||||||
foreach (var alias in _hideAliases)
|
foreach (var alias in _hideAliases)
|
||||||
AddCommandListener(alias, OnHideTyped);
|
AddCommandListener(alias, (player, command) => OnHideTyped(player, command));
|
||||||
// Joining any team, spectators included, would make SimpleAdmin unhide them.
|
// Joining any team, spectators included, would make SimpleAdmin unhide them.
|
||||||
AddCommandListener("jointeam", OnJoinTeam);
|
AddCommandListener("jointeam", (player, command) => OnJoinTeam(player, command));
|
||||||
AddCommandListener("spectate", OnJoinTeam);
|
AddCommandListener("spectate", (player, command) => OnJoinTeam(player, command));
|
||||||
|
// M. Hidden admins have no team, and the game won't let a player with no team close the
|
||||||
|
// team screen without picking one, so don't let it open.
|
||||||
|
AddCommandListener("teammenu", OnTeamMenu);
|
||||||
AddCommandListener("status", OnStatus);
|
AddCommandListener("status", OnStatus);
|
||||||
|
HookUserMessage(VguiMenuMessage, OnVguiMenu);
|
||||||
|
|
||||||
RegisterListener<Listeners.OnMapStart>(_ =>
|
RegisterListener<Listeners.OnMapStart>(_ =>
|
||||||
{
|
{
|
||||||
|
|
@ -121,9 +134,67 @@ public class AdminStealth : BasePlugin
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
|
|
||||||
player!.PrintToChat($" You're hidden, so you can't join a team. Type {ChatAlias()} to show yourself.");
|
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.
|
||||||
|
var slot = player.Slot;
|
||||||
|
Server.ExecuteCommand("sv_disable_teamselect_menu 1");
|
||||||
|
AddTimer(0.3f, () =>
|
||||||
|
{
|
||||||
|
var p = Utilities.GetPlayerFromSlot(slot);
|
||||||
|
if (p is { IsValid: true } && IsHidden(p))
|
||||||
|
SendTeamEvent(p, (int)CsTeam.Spectator, p.TeamNum);
|
||||||
|
}, TimerFlags.STOP_ON_MAPCHANGE);
|
||||||
|
AddTimer(0.6f, () => Server.ExecuteCommand("sv_disable_teamselect_menu 0"), TimerFlags.STOP_ON_MAPCHANGE);
|
||||||
return HookResult.Stop;
|
return HookResult.Stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HookResult OnTeamMenu(CCSPlayerController? player, CommandInfo command)
|
||||||
|
{
|
||||||
|
if (!IsHidden(player))
|
||||||
|
return HookResult.Continue;
|
||||||
|
|
||||||
|
Logger.LogInformation("Blocked teammenu from hidden admin {Name}", player!.PlayerName);
|
||||||
|
player.PrintToChat($" You're hidden, so you can't join a team. Type {ChatAlias()} to show yourself.");
|
||||||
|
return HookResult.Stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps the team select screen from ever opening for a hidden admin.
|
||||||
|
private HookResult OnVguiMenu(UserMessage message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (message.ReadString("name") != "team")
|
||||||
|
return HookResult.Continue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WarnMenuOnce($"Couldn't read the team menu message ({message.DebugString}): {ex.Message}");
|
||||||
|
return HookResult.Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var player in message.Recipients.ToList())
|
||||||
|
{
|
||||||
|
if (!IsHidden(player))
|
||||||
|
continue;
|
||||||
|
message.Recipients.Remove(player);
|
||||||
|
Logger.LogInformation("Kept the team menu from hidden admin {Name}", player.PlayerName);
|
||||||
|
}
|
||||||
|
return HookResult.Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _menuWarned;
|
||||||
|
|
||||||
|
private void WarnMenuOnce(string warning)
|
||||||
|
{
|
||||||
|
if (_menuWarned)
|
||||||
|
return;
|
||||||
|
_menuWarned = true;
|
||||||
|
Logger.LogWarning("{Warning}", warning);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnAdminToggleSilent(int slot, bool hidden)
|
private void OnAdminToggleSilent(int slot, bool hidden)
|
||||||
{
|
{
|
||||||
// The typed command's listener may run before or after SimpleAdmin's handler, which fires
|
// The typed command's listener may run before or after SimpleAdmin's handler, which fires
|
||||||
|
|
@ -216,6 +287,18 @@ public class AdminStealth : BasePlugin
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// css_hide toggles, so running it again after it didn't seem to take could unhide them. That
|
||||||
|
// happens when SimpleAdmin was hot-reloaded: CSS keeps handing out the old instance's API.
|
||||||
|
if (_restoring.Contains(slot))
|
||||||
|
{
|
||||||
|
_restoreScheduled.Remove(slot);
|
||||||
|
_restoring.Remove(slot);
|
||||||
|
Logger.LogWarning("Ran css_hide for {Name} ({SteamId}) but SimpleAdmin doesn't report them hidden. " +
|
||||||
|
"If CS2-SimpleAdmin was reloaded without a server restart, restart the server.",
|
||||||
|
player.PlayerName, steamId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt > RestoreAttempts)
|
if (attempt > RestoreAttempts)
|
||||||
{
|
{
|
||||||
_restoreScheduled.Remove(slot);
|
_restoreScheduled.Remove(slot);
|
||||||
|
|
@ -269,6 +352,21 @@ public class AdminStealth : BasePlugin
|
||||||
|
|
||||||
info.DontBroadcast = true;
|
info.DontBroadcast = true;
|
||||||
ScheduleRestore(player, RestoreDelay);
|
ScheduleRestore(player, RestoreDelay);
|
||||||
|
|
||||||
|
// Put them in spectators straight away so they skip the team select screen, rather than
|
||||||
|
// waiting for the restore. SimpleAdmin doesn't count them as hidden yet, so this doesn't
|
||||||
|
// unhide them; the restore then moves them from spectators to hidden.
|
||||||
|
var slot = player.Slot;
|
||||||
|
var steamId = player.SteamID;
|
||||||
|
AddTimer(SkipTeamSelectDelay, () =>
|
||||||
|
{
|
||||||
|
var p = Utilities.GetPlayerFromSlot(slot);
|
||||||
|
if (p is { IsValid: true } && p.SteamID == steamId && _hidden.Contains(steamId) &&
|
||||||
|
p.TeamNum == (byte)CsTeam.None && _simpleAdmin?.IsAdminSilent(p) != true)
|
||||||
|
{
|
||||||
|
p.ChangeTeam(CsTeam.Spectator);
|
||||||
|
}
|
||||||
|
}, TimerFlags.STOP_ON_MAPCHANGE);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,11 +378,29 @@ public class AdminStealth : BasePlugin
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
|
|
||||||
info.DontBroadcast = true;
|
info.DontBroadcast = true;
|
||||||
|
// Still tell the admin themselves: their client closes the team screen on it.
|
||||||
|
SendTeamEvent(player!, @event.Team, @event.Oldteam);
|
||||||
if (@event.Team > (int)CsTeam.Spectator && _hidden.Contains(player!.SteamID))
|
if (@event.Team > (int)CsTeam.Spectator && _hidden.Contains(player!.SteamID))
|
||||||
ScheduleRestore(player, 0.1f);
|
ScheduleRestore(player, 0.1f);
|
||||||
return HookResult.Continue;
|
return HookResult.Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SendTeamEvent(CCSPlayerController player, int team, int oldTeam)
|
||||||
|
{
|
||||||
|
var copy = new EventPlayerTeam(true)
|
||||||
|
{
|
||||||
|
Userid = player,
|
||||||
|
Team = team,
|
||||||
|
Oldteam = oldTeam,
|
||||||
|
Disconnect = false,
|
||||||
|
Silent = true,
|
||||||
|
Isbot = false,
|
||||||
|
Name = player.PlayerName,
|
||||||
|
};
|
||||||
|
copy.FireEventToClient(player);
|
||||||
|
copy.Free();
|
||||||
|
}
|
||||||
|
|
||||||
[GameEventHandler(HookMode.Pre)]
|
[GameEventHandler(HookMode.Pre)]
|
||||||
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -15,6 +15,17 @@ admin off the scoreboard. This plugin makes it look like they left the server:
|
||||||
- A hidden admin can't join a team or go to spectators (`jointeam` and `spectate` are blocked
|
- A hidden admin can't join a team or go to spectators (`jointeam` and `spectate` are blocked
|
||||||
with a chat message). If the game or another plugin puts them on a team anyway, which makes
|
with a chat message). If the game or another plugin puts them on a team anyway, which makes
|
||||||
SimpleAdmin unhide them, they're hidden again straight away.
|
SimpleAdmin unhide them, they're hidden again straight away.
|
||||||
|
- On reconnect or map change, a hidden admin is moved to spectators 0.1 s after connecting, so
|
||||||
|
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.
|
||||||
|
- 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
|
||||||
|
hidden admin. Never hot-reload CS2-SimpleAdmin while this runs: CSS keeps handing out the old
|
||||||
|
instance's API until the server restarts.
|
||||||
- `status` from a player's console is blocked and prints nothing. It still works from the server
|
- `status` from a player's console is blocked and prints nothing. It still works from the server
|
||||||
console and RCON.
|
console and RCON.
|
||||||
|
|
||||||
|
|
@ -37,8 +48,8 @@ the `status` block works.
|
||||||
- Unhiding doesn't announce the admin as joining again.
|
- Unhiding doesn't announce the admin as joining again.
|
||||||
- The server browser and trackers get the player list from a separate server query, which this
|
- The server browser and trackers get the player list from a separate server query, which this
|
||||||
doesn't touch, so they still list a hidden admin.
|
doesn't touch, so they still list a hidden admin.
|
||||||
- Reloading CS2-SimpleAdmin on its own leaves this plugin listening to the old instance. Reload
|
- Reloading CS2-SimpleAdmin on its own leaves this plugin (and WebPanelBridge) talking to the old
|
||||||
AdminStealth after it.
|
instance until a server restart.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue