diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index f14ed8d..7192b94 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; 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) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index e2a2b30..4b2302e 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -507,26 +507,31 @@ public partial class CS2_SimpleAdmin Task.Run(async () => { await PermissionManager.CreateGroupsJsonFile(); - await PermissionManager.CreateAdminsJsonFile(); - - var adminsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/admins.json"); - var groupsFile = await File.ReadAllTextAsync(Instance.ModuleDirectory + "/data/groups.json"); - - await Server.NextWorldUpdateAsync(() => - { - AddTimer(1, () => - { - if (!string.IsNullOrEmpty(adminsFile)) - AddTimer(2.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); - if (!string.IsNullOrEmpty(groupsFile)) - AddTimer(3.0f, () => AdminManager.LoadAdminGroups(ModuleDirectory + "/data/groups.json")); - if (!string.IsNullOrEmpty(adminsFile)) - AddTimer(4.0f, () => AdminManager.LoadAdminData(ModuleDirectory + "/data/admins.json")); + var admins = await PermissionManager.CreateAdminsJsonFile(); + if (admins == null) return; - _logger?.LogInformation("Loaded admins!"); - }); + await Server.NextWorldUpdateAsync(() => + { + // 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, + // all in one tick so nobody is left without their flags in between. Groups can't be + // removed from CSS, so flags taken off a group still need a restart. + foreach (var steamId in PermissionManager.AdminCache.Keys.ToList()) + AdminManager.RemovePlayerAdminData(steamId); + PermissionManager.AdminCache.Clear(); + + 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); }); - }); + }); //_ = _adminManager.GiveAllGroupsFlags(); //_ = _adminManager.GiveAllFlags(); diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index 73d191c..4104a8a 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -17,8 +17,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// /// Retrieves all players' flags and associated data asynchronously. /// - /// A list of tuples containing player SteamID, name, flags, immunity, and expiration time. - private async Task, int, DateTime?)>> GetAllPlayersFlags() + /// A list of tuples containing player SteamID, name, flags, immunity, and expiration time, or null if the database query failed. + private async Task, int, DateTime?)>?> GetAllPlayersFlags() { if (databaseProvider == null) return new List<(ulong, string, List, int, DateTime?)>(); @@ -68,7 +68,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) catch (Exception ex) { 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) /// /// Creates a JSON file containing admins data asynchronously. /// + /// zio: every admin row loaded, for ReloadAdmins to rebuild AdminCache from, or null if the database query failed (the file is then left alone). [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - public async Task CreateAdminsJsonFile() + public async Task Flags)>?> CreateAdminsJsonFile() { - List<(ulong identity, string name, List flags, int immunity, DateTime? ends)> allPlayers = await GetAllPlayersFlags(); + var allPlayers = await GetAllPlayersFlags(); + if (allPlayers == null) return null; + 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(); - var jsonData = validPlayers - .GroupBy(player => player.name) - .ToDictionary( - group => group.Key, - object (group) => - { - var consolidatedData = group.Aggregate( - new - { - identity = string.Empty, - immunity = 0, - flags = new List(), - groups = new List() - }, - (acc, player) => - { - if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString())) - { - acc = acc with { identity = player.identity.ToString() }; - } + // zio: keyed by SteamID, not name: CSS ignores the key, and two admins with the same name were + // merged into one entry under the first one's SteamID. Upstream also cleared and refilled + // AdminCache here once per admin, so it only ever kept the last admin and a reload never + // removed anyone else; ReloadAdmins now does that once, from the list returned here. + var jsonData = validPlayers + .GroupBy(p => p.steamId!.SteamId64) + .ToDictionary( + group => group.Key.ToString(), + object (group) => + { + var rows = group.Select(p => p.player).ToList(); + var flags = rows.SelectMany(r => r.Item3).Distinct().ToList(); + return new + { + identity = group.Key.ToString(), + immunity = rows.Max(r => r.Item4), + flags = flags.Where(flag => flag.StartsWith($"@")).ToList(), + groups = flags.Where(flag => flag.StartsWith($"#")).ToList() + }; + }); - acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) }; + var options = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; - acc = acc with - { - 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(); - - 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 json = JsonSerializer.Serialize(jsonData, options); var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json"); await File.WriteAllTextAsync(filePath, json); + + return validPlayers.Select(p => (p.steamId!, p.player.Item5, p.player.Item3)).ToList(); } ///