zio fork 1.8.2b-zio11: reloading admins removes deleted and demoted admins
Some checks are pending
Build and Publish / build (push) Waiting to run
Build and Publish / publish (push) Blocked by required conditions

CSS's admin loaders only add, and the per-admin cleanup in CreateAdminsJsonFile
reset AdminCache once per admin, so it only ever tracked the last one and a
reload never took anyone else out of memory. ReloadAdmins now removes every
admin it loaded last time and loads the new set in one tick. admins.json is
keyed by SteamID instead of name, so admins sharing a name aren't merged, and a
failed admins query keeps the loaded admins instead of removing them.
This commit is contained in:
Astra 2026-10-02 21:21:09 +01:00
parent f46ccf18ce
commit d042aab1c0
3 changed files with 63 additions and 103 deletions

View file

@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy"; public override string ModuleAuthor => "daffyy";
public override string ModuleVersion => "1.8.2b-zio10"; public override string ModuleVersion => "1.8.2b-zio11";
public override void Load(bool hotReload) public override void Load(bool hotReload)
{ {

View file

@ -507,24 +507,29 @@ public partial class CS2_SimpleAdmin
Task.Run(async () => Task.Run(async () =>
{ {
await PermissionManager.CreateGroupsJsonFile(); await PermissionManager.CreateGroupsJsonFile();
await PermissionManager.CreateAdminsJsonFile(); var admins = await PermissionManager.CreateAdminsJsonFile();
if (admins == null) return;
var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json"); await Server.NextWorldUpdateAsync(() =>
var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json");
await Server.NextWorldUpdateAsync(() =>
{ {
AddTimer(1, () => // zio: CSS's loaders only ever add, so first remove everyone loaded last time (removed or
{ // demoted admins kept their old rank until a restart otherwise), then load the new set,
if (!string.IsNullOrEmpty(adminsFile)) // all in one tick so nobody is left without their flags in between. Groups can't be
AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); // removed from CSS, so flags taken off a group still need a restart.
if (!string.IsNullOrEmpty(groupsFile)) foreach (var steamId in PermissionManager.AdminCache.Keys.ToList())
AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json")); AdminManager.RemovePlayerAdminData(steamId);
if (!string.IsNullOrEmpty(adminsFile)) PermissionManager.AdminCache.Clear();
AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json"));
_logger?.LogInformation("Loaded admins!"); foreach (var admin in admins.GroupBy(a => a.SteamId))
}); {
var ends = admin.Any(a => a.Ends == null) ? null : admin.Max(a => a.Ends);
PermissionManager.AdminCache[admin.Key] = (ends, admin.SelectMany(a => a.Flags).Distinct().ToList());
}
AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json");
AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json");
_logger?.LogInformation("Loaded {count} admins!", PermissionManager.AdminCache.Count);
}); });
}); });

View file

@ -17,8 +17,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <summary> /// <summary>
/// Retrieves all players' flags and associated data asynchronously. /// Retrieves all players' flags and associated data asynchronously.
/// </summary> /// </summary>
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time.</returns> /// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time, or null if the database query failed.</returns>
private async Task<List<(ulong, string ,List<string>, int, DateTime?)>> GetAllPlayersFlags() private async Task<List<(ulong, string ,List<string>, int, DateTime?)>?> GetAllPlayersFlags()
{ {
if (databaseProvider == null) if (databaseProvider == null)
return new List<(ulong, string, List<string>, int, DateTime?)>(); return new List<(ulong, string, List<string>, int, DateTime?)>();
@ -68,7 +68,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
catch (Exception ex) catch (Exception ex)
{ {
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message); CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
return []; // zio: null rather than empty, so ReloadAdmins keeps the loaded admins instead of removing them all.
return null;
} }
} }
@ -150,96 +151,50 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <summary> /// <summary>
/// Creates a JSON file containing admins data asynchronously. /// Creates a JSON file containing admins data asynchronously.
/// </summary> /// </summary>
/// <returns>zio: every admin row loaded, for ReloadAdmins to rebuild AdminCache from, or null if the database query failed (the file is then left alone).</returns>
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CreateAdminsJsonFile() public async Task<List<(SteamID SteamId, DateTime? Ends, List<string> Flags)>?> CreateAdminsJsonFile()
{ {
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags(); var allPlayers = await GetAllPlayersFlags();
if (allPlayers == null) return null;
var validPlayers = allPlayers var validPlayers = allPlayers
.Where(player => SteamID.TryParse(player.identity.ToString(), out _)) .Select(player => (player, steamId: SteamID.TryParse(player.Item1.ToString(), out var steamId) ? steamId : null))
.Where(p => p.steamId != null)
.ToList(); .ToList();
var jsonData = validPlayers // zio: keyed by SteamID, not name: CSS ignores the key, and two admins with the same name were
.GroupBy(player => player.name) // merged into one entry under the first one's SteamID. Upstream also cleared and refilled
.ToDictionary( // AdminCache here once per admin, so it only ever kept the last admin and a reload never
group => group.Key, // removed anyone else; ReloadAdmins now does that once, from the list returned here.
object (group) => var jsonData = validPlayers
{ .GroupBy(p => p.steamId!.SteamId64)
var consolidatedData = group.Aggregate( .ToDictionary(
new group => group.Key.ToString(),
{ object (group) =>
identity = string.Empty, {
immunity = 0, var rows = group.Select(p => p.player).ToList();
flags = new List<string>(), var flags = rows.SelectMany(r => r.Item3).Distinct().ToList();
groups = new List<string>() return new
}, {
(acc, player) => identity = group.Key.ToString(),
{ immunity = rows.Max(r => r.Item4),
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString())) flags = flags.Where(flag => flag.StartsWith($"@")).ToList(),
{ groups = flags.Where(flag => flag.StartsWith($"#")).ToList()
acc = acc with { identity = player.identity.ToString() }; };
} });
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) }; var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
acc = acc with var json = JsonSerializer.Serialize(jsonData, options);
{
flags = acc.flags.Concat(player.flags.Where(flag => flag.StartsWith($"@"))).Distinct().ToList(),
groups = acc.groups.Concat(player.flags.Where(flag => flag.StartsWith($"#"))).Distinct().ToList()
};
return acc;
});
Server.NextWorldUpdate(() =>
{
var keysToRemove = new List<SteamID>();
foreach (var steamId in AdminCache.Keys.ToList())
{
var data = AdminManager.GetPlayerAdminData(steamId);
if (data != null)
{
var flagsArray = AdminCache[steamId].Flags.ToArray();
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
}
keysToRemove.Add(steamId);
}
foreach (var steamId in keysToRemove)
{
if (!AdminCache.TryRemove(steamId, out _)) continue;
var data = AdminManager.GetPlayerAdminData(steamId);
if (data == null) continue;
if (data.Flags.Count != 0 && data.Groups.Count != 0) continue;
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
}
foreach (var player in group)
{
if (SteamID.TryParse(player.identity.ToString(), out var steamId) && steamId != null)
{
AdminCache.TryAdd(steamId, (player.ends, player.flags));
}
}
});
return consolidatedData;
});
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json"); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
await File.WriteAllTextAsync(filePath, json); await File.WriteAllTextAsync(filePath, json);
return validPlayers.Select(p => (p.steamId!, p.player.Item5, p.player.Item3)).ToList();
} }
/// <summary> /// <summary>