Migrate menus and votes to Panorama UI Kit

Replace MenuManagerCS2 with Panorama UI Kit for the admin menus and
css_vote. Layout names (menu, vote, banner, toast) are configurable
under MenuConfig and preloaded on startup; MenuType is removed and
ConfigVersion bumped to 26.

- Drop MenuManagerApi.dll and the old chat menu classes
- Reference PanoramaUiKit.Shared.dll, bump CounterStrikeSharp to 1.0.376
- Add .editorconfig and CODING_CONVENTIONS.md, apply them codebase-wide
- Remove the CS2-SimpleAdmin-docs site
- Rewrite README for the fork
This commit is contained in:
Salvatore 2026-10-21 18:51:18 +02:00
parent 03a454b1b5
commit 460d8b2b85
112 changed files with 5559 additions and 36371 deletions

Binary file not shown.

Binary file not shown.

View file

@ -18,9 +18,12 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public PlayerInfo GetPlayerInfo(CCSPlayerController player)
{
return !player.UserId.HasValue
? throw new KeyNotFoundException("Player with specific UserId not found")
: CS2_SimpleAdmin.PlayersInfo[player.SteamID];
if (!player.UserId.HasValue)
{
throw new KeyNotFoundException("Player with specific UserId not found");
}
return CS2_SimpleAdmin.PlayersInfo[player.SteamID];
}
public string GetConnectionString() => CS2_SimpleAdmin.Instance.DbConnectionString;
@ -57,35 +60,35 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
switch (penaltyType)
{
case PenaltyType.Ban:
{
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason);
break;
}
case PenaltyType.Kick:
{
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Kick(admin, player, reason);
break;
}
case PenaltyType.Gag:
{
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
break;
}
case PenaltyType.Mute:
{
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
break;
}
case PenaltyType.Silence:
{
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
break;
}
case PenaltyType.Warn:
{
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
}
@ -97,30 +100,30 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
switch (penaltyType)
{
case PenaltyType.Ban:
{
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.AddBan(admin, steamid, duration, reason);
break;
}
case PenaltyType.Gag:
{
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.AddGag(admin, steamid, duration, reason);
break;
}
case PenaltyType.Mute:
{
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.AddMute(admin, steamid, duration, reason);
break;
}
case PenaltyType.Silence:
{
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.AddSilence(admin, steamid, duration, reason);
break;
}
case PenaltyType.Warn:
{
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
break;
}
{
CS2_SimpleAdmin.Instance.AddWarn(admin, steamid, duration, reason);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(penaltyType), penaltyType, null);
}
@ -149,27 +152,31 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public void RegisterCommand(string name, string? description, CommandInfo.CommandCallback callback)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Command name cannot be null or empty.", nameof(name));
}
ArgumentNullException.ThrowIfNull(callback);
var definition = new CommandDefinition(name, description ?? "No description", callback);
if (!RegisterCommands._commandDefinitions.TryGetValue(name, out var list))
CommandDefinition definition = new CommandDefinition(name, description ?? "No description", callback);
if (!RegisterCommands._commandDefinitions.TryGetValue(name, out IList<CommandDefinition>? list))
{
list = new List<CommandDefinition>();
RegisterCommands._commandDefinitions[name] = list;
}
list.Add(definition);
}
public void UnRegisterCommand(string commandName)
{
var definitions = RegisterCommands._commandDefinitions[commandName];
IList<CommandDefinition> definitions = RegisterCommands._commandDefinitions[commandName];
if (definitions.Count == 0)
{
return;
}
foreach (var definition in definitions)
foreach (CommandDefinition definition in definitions)
{
CS2_SimpleAdmin.Instance.RemoveCommand(commandName, definition.Callback);
}
@ -196,7 +203,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
bool dontPublish = false, params object[] messageArgs)
{
if (moduleLocalizer is not IStringLocalizer localizer)
{
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
}
Helper.ShowAdminActivityLocalized(localizer, messageKey, callerName, dontPublish, messageArgs);
}
@ -209,7 +218,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public void RegisterMenuCategory(string categoryId, string categoryNameKey, string permission, object moduleLocalizer)
{
if (moduleLocalizer is not IStringLocalizer localizer)
{
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
}
Menus.MenuManager.Instance.RegisterCategory(categoryId, categoryNameKey, permission, localizer);
}
@ -223,22 +234,11 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
MenuBuilder BuilderFactory(CCSPlayerController player)
{
if (menuFactory(player) is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu factory must return MenuBuilder");
// Dodaj automatyczną obsługę przycisku 'Wróć'
menuBuilder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
throw new InvalidOperationException("Menu factory must return MenuBuilder");
}
return menuBuilder;
return menuBuilder.WithBackButton();
}
}
@ -250,25 +250,14 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
MenuBuilder BuilderFactory(CCSPlayerController player)
{
var context = new MenuContext(categoryId, menuId, menuName, permission, commandName);
MenuContext context = new MenuContext(categoryId, menuId, menuName, permission, commandName);
if (menuFactory(player, context) is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu factory must return MenuBuilder");
// Dodaj automatyczną obsługę przycisku 'Wróć'
menuBuilder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
throw new InvalidOperationException("Menu factory must return MenuBuilder");
}
return menuBuilder;
return menuBuilder.WithBackButton();
}
}
@ -276,32 +265,23 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
Func<CCSPlayerController, MenuContext, object> menuFactory, string? permission, string? commandName, object moduleLocalizer)
{
if (moduleLocalizer is not IStringLocalizer localizer)
{
throw new InvalidOperationException("moduleLocalizer must be an IStringLocalizer instance");
}
Menus.MenuManager.Instance.RegisterMenu(categoryId, menuId, menuNameKey, BuilderFactory, permission, commandName, localizer);
return;
MenuBuilder BuilderFactory(CCSPlayerController player)
{
var context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName);
MenuContext context = new MenuContext(categoryId, menuId, menuNameKey, permission, commandName);
if (menuFactory(player, context) is not MenuBuilder menuBuilder)
throw new InvalidOperationException("Menu factory must return MenuBuilder");
// Dodaj automatyczną obsługę przycisku 'Wróć'
menuBuilder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
throw new InvalidOperationException("Menu factory must return MenuBuilder");
}
return menuBuilder;
return menuBuilder.WithBackButton();
}
}
@ -313,20 +293,7 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public object CreateMenuWithBack(string title, string categoryId, CCSPlayerController player)
{
var builder = new MenuBuilder(title);
builder.WithBackAction(p =>
{
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(categoryId, out var category))
{
Menus.MenuManager.Instance.CreateCategoryMenuPublic(category, p).OpenMenu(p);
}
else
{
Menus.MenuManager.Instance.OpenMainMenu(p);
}
});
return builder;
return new MenuBuilder(title).WithBackButton();
}
public object CreateMenuWithBack(MenuContext context, CCSPlayerController player)
@ -334,10 +301,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
// Get translated title if module has localizer
string title = context.MenuTitle;
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out MenuCategory? category))
{
// Check if this specific menu has a localizer
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
if (category.MenuLocalizers.TryGetValue(context.MenuId, out IStringLocalizer? menuLocalizer))
{
using (new WithTemporaryCulture(player.GetLanguage()))
{
@ -365,12 +332,12 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public object CreateMenuWithPlayers(string title, string categoryId, CCSPlayerController admin,
Func<CCSPlayerController, bool> filter, Action<CCSPlayerController, CCSPlayerController> onSelect)
{
var menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin);
var players = Helper.GetValidPlayers().Where(filter);
MenuBuilder menu = (MenuBuilder)CreateMenuWithBack(title, categoryId, admin);
IEnumerable<CCSPlayerController> players = Helper.GetValidPlayers().Where(filter);
foreach (var player in players)
foreach (CCSPlayerController? player in players)
{
var playerName = player.PlayerName.Length > 26 ? player.PlayerName[..26] : player.PlayerName;
string playerName = Helper.Truncate(player.PlayerName, 26);
menu.AddOption(playerName, _ =>
{
if (player.IsValid)
@ -389,10 +356,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
// Get translated title if module has localizer
string title = context.MenuTitle;
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out var category))
if (Menus.MenuManager.Instance.GetMenuCategories().TryGetValue(context.CategoryId, out MenuCategory? category))
{
// Check if this specific menu has a localizer
if (category.MenuLocalizers.TryGetValue(context.MenuId, out var menuLocalizer))
if (category.MenuLocalizers.TryGetValue(context.MenuId, out IStringLocalizer? menuLocalizer))
{
using (new WithTemporaryCulture(admin.GetLanguage()))
{
@ -416,7 +383,9 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
string? permission = null)
{
if (menu is not MenuBuilder menuBuilder)
{
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
}
menuBuilder.AddOption(name, action, disabled, permission);
}
@ -425,13 +394,18 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
bool disabled = false, string? permission = null)
{
if (menu is not MenuBuilder menuBuilder)
{
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
}
menuBuilder.AddSubMenu(name, player =>
{
var subMenu = subMenuFactory(player);
object subMenu = subMenuFactory(player);
if (subMenu is not MenuBuilder builder)
{
throw new InvalidOperationException("SubMenu factory must return MenuBuilder");
}
return builder;
}, disabled, permission);
}
@ -439,8 +413,10 @@ public class CS2_SimpleAdminApi : ICS2_SimpleAdminApi
public void OpenMenu(object menu, CCSPlayerController player)
{
if (menu is not MenuBuilder menuBuilder)
{
throw new InvalidOperationException("Menu must be a MenuBuilder instance");
}
menuBuilder.OpenMenu(player);
}
}
}

View file

@ -19,11 +19,22 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
{
internal static CS2_SimpleAdmin Instance { get; private set; } = new();
public override string ModuleName => "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)");
public override string ModuleName
{
get
{
if (Helper.IsDebugBuild)
{
return "CS2-SimpleAdmin (DEBUG)";
}
return "CS2-SimpleAdmin (RELEASE)";
}
}
public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)";
public override string ModuleAuthor => "daffyy";
public override string ModuleVersion => "1.9.1";
public override void Load(bool hotReload)
{
Instance = this;
@ -31,30 +42,38 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
{
ServerLoaded = false;
_serverLoading = false;
CacheManager?.Dispose();
CacheManager = new CacheManager();
// OnGameServerSteamAPIActivated();
OnMapStart(string.Empty);
AddTimer(6.0f, () =>
{
if (DatabaseProvider == null) return;
if (DatabaseProvider == null)
{
return;
}
PlayersInfo.Clear();
CachedPlayers.Clear();
BotPlayers.Clear();
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray())
foreach (CCSPlayerController? player in Utilities.GetPlayers().Where(p => p.IsValid && p is { Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToArray())
{
if (!player.IsBot)
{
PlayerManager.LoadPlayerData(player, true);
}
else
{
BotPlayers.Add(player);
};
}
}
;
});
}
_cBasePlayerControllerSetPawnFunc = new MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>(GameData.GetSignature("CBasePlayerController_SetPawn"));
@ -64,22 +83,14 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
PlayersTimer?.Kill();
PlayersTimer = null;
PlayerManager.CheckPlayersTimer();
Menus.MenuManager.Instance.InitializeDefaultCategories();
BasicMenu.Initialize();
}
public override void OnAllPluginsLoaded(bool hotReload)
{
try
{
MenuApi = MenuCapability.Get();
}
catch (Exception ex)
{
Logger.LogError("Unable to load required plugins ... \n{exception}", ex.Message);
Unload(false);
}
UiKit.Preload(Logger);
RegisterEvents();
AddTimer(0.5f, RegisterCommands.InitializeCommands);
@ -87,7 +98,10 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
// Loading the plugin mid-map (css_plugins load) fires no map start, so kick server loading off ourselves.
AddTimer(5.0f, () =>
{
if (!ServerLoaded) OnGameServerSteamAPIActivated();
if (!ServerLoaded)
{
OnGameServerSteamAPIActivated();
}
});
if (!CoreConfig.UnlockConCommands)
@ -107,16 +121,17 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
_logger = Logger;
Config = config;
UiKit.Configure(config.MenuConfigs);
bool missing = false;
var cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
var pluginsPath = Path.Combine(cssPath, "plugins");
var sharedPath = Path.Combine(cssPath, "shared");
string cssPath = Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp");
string pluginsPath = Path.Combine(cssPath, "plugins");
string sharedPath = Path.Combine(cssPath, "shared");
foreach (var plugin in _requiredPlugins)
foreach (string plugin in _requiredPlugins)
{
var pluginDirPath = Path.Combine(pluginsPath, plugin);
var pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
string pluginDirPath = Path.Combine(pluginsPath, plugin);
string pluginDllPath = Path.Combine(pluginDirPath, $"{plugin}.dll");
if (!Directory.Exists(pluginDirPath))
{
@ -135,10 +150,10 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
}
}
foreach (var shared in _requiredShared)
foreach (string shared in _requiredShared)
{
var sharedDirPath = Path.Combine(sharedPath, shared);
var sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
string sharedDirPath = Path.Combine(sharedPath, shared);
string sharedDllPath = Path.Combine(sharedDirPath, $"{shared}.dll");
if (!Directory.Exists(sharedDirPath))
{
@ -156,7 +171,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
missing = true;
}
}
if (missing)
{
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
@ -164,54 +179,59 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
}
Instance = this;
if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase))
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser))
if (Config.DatabaseConfig.DatabaseType.Contains("mysql", StringComparison.CurrentCultureIgnoreCase))
{
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseHost) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseName) ||
string.IsNullOrWhiteSpace(config.DatabaseConfig.DatabaseUser))
{
throw new Exception("[CS2-SimpleAdmin] You need to setup MySQL credentials in config!");
}
if (!Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode))
{
sslMode = MySqlSslMode.Preferred;
}
MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder()
{
Server = config.DatabaseConfig.DatabaseHost,
Database = config.DatabaseConfig.DatabaseName,
UserID = config.DatabaseConfig.DatabaseUser,
Password = config.DatabaseConfig.DatabasePassword,
Port = (uint)config.DatabaseConfig.DatabasePort,
SslMode = sslMode,
Pooling = true,
};
DbConnectionString = builder.ConnectionString;
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
}
else
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
{
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
}
DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
}
var builder = new MySqlConnectionStringBuilder()
(bool success, string? exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult();
if (!success)
{
Server = config.DatabaseConfig.DatabaseHost,
Database = config.DatabaseConfig.DatabaseName,
UserID = config.DatabaseConfig.DatabaseUser,
Password = config.DatabaseConfig.DatabasePassword,
Port = (uint)config.DatabaseConfig.DatabasePort,
SslMode = Enum.TryParse(config.DatabaseConfig.DatabaseSSlMode, true, out MySqlSslMode sslMode)
? sslMode
: MySqlSslMode.Preferred,
Pooling = true,
};
if (exception != null)
{
Logger.LogError("Problem with database connection! \n{exception}", exception);
}
DbConnectionString = builder.ConnectionString;
DatabaseProvider = new MySqlDatabaseProvider(DbConnectionString);
}
else
{
if (string.IsNullOrWhiteSpace(config.DatabaseConfig.SqliteFilePath))
{
throw new Exception("[CS2-SimpleAdmin] You need to specify SQLite file path in config!");
Unload(false);
return;
}
DatabaseProvider = new SqliteDatabaseProvider(ModuleDirectory + "/" + config.DatabaseConfig.SqliteFilePath);
}
var (success, exception) = Task.Run(() => DatabaseProvider.CheckConnectionAsync()).GetAwaiter().GetResult();
if (!success)
{
if (exception != null)
Logger.LogError("Problem with database connection! \n{exception}", exception);
Unload(false);
return;
}
Task.Run(() => DatabaseProvider.DatabaseMigrationAsync());
if (!Directory.Exists(ModuleDirectory + "/data"))
{
Directory.CreateDirectory(ModuleDirectory + "/data");
@ -220,11 +240,15 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
_localizer = Localizer;
if (!string.IsNullOrEmpty(Config.Discord.DiscordLogWebhook))
{
DiscordWebhookClientLog = new DiscordManager(Config.Discord.DiscordLogWebhook);
}
if (Config.EnableUpdateCheck)
{
Task.Run(async () => await PluginInfo.CheckVersion(ModuleVersion, Logger));
}
PermissionManager = new PermissionManager(DatabaseProvider);
BanManager = new BanManager(DatabaseProvider);
MuteManager = new MuteManager(DatabaseProvider);
@ -233,7 +257,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
internal static TargetResult? GetTarget(CommandInfo command, int argument = 1)
{
var matches = command.GetArgTargetResult(argument);
TargetResult matches = command.GetArgTargetResult(argument);
if (!matches.Any())
{
@ -242,10 +266,14 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
}
if (command.GetArg(argument).StartsWith('@'))
{
return matches;
}
if (matches.Count() == 1)
{
return matches;
}
command.ReplyToCommand($"Multiple targets found for \"{command.GetArg(argument)}\".");
return null;
@ -257,12 +285,16 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig<CS2_SimpleAdmin
CacheManager = null;
PlayersTimer?.Kill();
PlayersTimer = null;
UnregisterEvents();
if (hotReload)
{
PlayersInfo.Clear();
}
else
{
Server.ExecuteCommand($"css_plugins unload {ModuleDirectory}");
}
}
}
}

View file

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369">
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.376">
<PrivateAssets>none</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -47,8 +47,8 @@
</ItemGroup>
<ItemGroup>
<Reference Include="MenuManagerApi">
<HintPath>3rd_party\MenuManagerApi.dll</HintPath>
<Reference Include="PanoramaUiKit.Shared">
<HintPath>3rd_party\PanoramaUiKit.Shared.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>

View file

@ -11,9 +11,9 @@ public static class RegisterCommands
{
internal static readonly Dictionary<string, IList<CommandDefinition>> _commandDefinitions =
new(StringComparer.InvariantCultureIgnoreCase);
private delegate void CommandCallback(CCSPlayerController? caller, CommandInfo.CommandCallback callback);
private static readonly string CommandsPath = Path.Combine(CS2_SimpleAdmin.ConfigDirectory, "Commands.json");
private static readonly List<CommandMapping> CommandMappings =
[
@ -101,7 +101,7 @@ public static class RegisterCommands
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static void CreateConfig()
{
var commands = new CommandsConfig
CommandsConfig commands = new CommandsConfig
{
Commands = new Dictionary<string, Command>
{
@ -161,14 +161,14 @@ public static class RegisterCommands
{ "css_adminvoice", new Command { Aliases = ["css_adminvoice", "css_listenall"] } }
}
};
var options = new JsonSerializerOptions
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(commands, options);
string json = JsonSerializer.Serialize(commands, options);
File.WriteAllText(CommandsPath, json);
}
@ -179,46 +179,52 @@ public static class RegisterCommands
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static void Register()
{
var json = File.ReadAllText(CommandsPath);
var commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
string json = File.ReadAllText(CommandsPath);
CommandsConfig? commandsConfig = JsonSerializer.Deserialize<CommandsConfig>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (commandsConfig?.Commands != null)
{
foreach (var command in commandsConfig.Commands)
foreach (KeyValuePair<string, Command> command in commandsConfig.Commands)
{
if (command.Value.Aliases == null) continue;
if (command.Value.Aliases == null)
{
continue;
}
CS2_SimpleAdmin._logger?.LogInformation(
$"Registering command: `{command.Key}` with aliases: `{string.Join(", ", command.Value.Aliases)}`");
var mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key);
if (mapping == null || command.Value.Aliases.Length == 0) continue;
foreach (var alias in command.Value.Aliases)
CommandMapping? mapping = CommandMappings.FirstOrDefault(m => m.CommandKey == command.Key);
if (mapping == null || command.Value.Aliases.Length == 0)
{
continue;
}
foreach (string alias in command.Value.Aliases)
{
CS2_SimpleAdmin.Instance.AddCommand(alias, "", mapping.Callback);
}
}
// Commands added after Commands.json was generated are registered under their default name
foreach (var mapping in CommandMappings.Where(m => !commandsConfig.Commands.ContainsKey(m.CommandKey)))
foreach (CommandMapping? mapping in CommandMappings.Where(m => !commandsConfig.Commands.ContainsKey(m.CommandKey)))
{
CS2_SimpleAdmin._logger?.LogInformation($"Registering command: `{mapping.CommandKey}` (not in Commands.json, using default alias)");
CS2_SimpleAdmin.Instance.AddCommand(mapping.CommandKey, "", mapping.Callback);
}
}
foreach (var (name, definitions) in _commandDefinitions)
foreach ((string? name, IList<CommandDefinition>? definitions) in _commandDefinitions)
{
foreach (var definition in definitions)
foreach (CommandDefinition definition in definitions)
{
CS2_SimpleAdmin._logger?.LogInformation($"Registering custom command: `{name}`");
CS2_SimpleAdmin.Instance.AddCommand(name, definition.Description, definition.Callback);
}
}
}
/// <summary>
/// Represents the JSON configuration structure for commands.
/// </summary>
@ -226,7 +232,7 @@ public static class RegisterCommands
{
public Dictionary<string, Command>? Commands { get; init; }
}
/// <summary>
/// Represents a command definition containing a list of aliases.
/// </summary>
@ -234,7 +240,7 @@ public static class RegisterCommands
{
public string[]? Aliases { get; init; }
}
/// <summary>
/// Maps a command key to its respective command callback handler.
/// </summary>
@ -243,4 +249,4 @@ public static class RegisterCommands
public string CommandKey { get; } = commandKey;
public CommandInfo.CommandCallback Callback { get; } = callback;
}
}
}

View file

@ -2,6 +2,7 @@ using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using CS2_SimpleAdmin.Managers;
@ -21,34 +22,38 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<#userid or name> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
string callerName = Helper.GetCallerName(caller);
if (command.ArgCount < 2)
{
return;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList();
}
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, Connected: PlayerConnectedState.Connected, IsHLTV: false }).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
var time = Helper.ParsePenaltyTime(command.GetArg(2));
string reason = Helper.GetArgsReason(command, 3);
int time = Helper.ParsePenaltyTime(command.GetArg(2));
playersToTarget.ForEach(player =>
{
if (!caller.CanTarget(player)) return;
if (!caller.CanTarget(player))
{
return;
}
if (time < 0 && caller != null && caller.IsValid && Config.OtherSettings.ShowBanMenuIfNoTime)
{
DurationMenu.OpenMenu(caller, $"{_localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player,
ManagePlayersMenu.BanMenu);
BasicMenu.CreatePenaltyMenu(caller, player, PenaltyType.Ban).OpenMenu(caller);
return;
}
@ -69,18 +74,27 @@ public partial class CS2_SimpleAdmin
/// <param name="silent">If true, suppresses command logging.</param>
internal void Ban(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, BanManager? banManager = null, CommandInfo? command = null, bool silent = false)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return;
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue)
{
return;
}
if (!caller.CanTarget(player))
{
return;
}
if (!CheckValidBan(caller, time))
{
return;
}
// Set default caller name if not provided
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
callerName = Helper.GetCallerName(caller);
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
PlayerInfo playerInfo = PlayersInfo[player.SteamID];
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
// Asynchronously handle banning logic
Task.Run(async () =>
@ -93,13 +107,25 @@ public partial class CS2_SimpleAdmin
});
// Determine message keys and arguments based on ban time
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
? ("sa_player_ban_message_perm", "sa_admin_ban_message_perm",
[reason, "CALLER"],
["CALLER", player.PlayerName, reason])
: ("sa_player_ban_message_time", "sa_admin_ban_message_time",
new object[] { reason, time, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason, time });
string? messageKey;
string? activityMessageKey;
object[]? centerArgs;
object[]? adminActivityArgs;
if (time == 0)
{
messageKey = "sa_player_ban_message_perm";
activityMessageKey = "sa_admin_ban_message_perm";
centerArgs = [reason, "CALLER"];
adminActivityArgs = ["CALLER", player.PlayerName, reason];
}
else
{
messageKey = "sa_player_ban_message_time";
activityMessageKey = "sa_admin_ban_message_time";
centerArgs = new object[] { reason, time, "CALLER" };
adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason, time };
}
// Display center message to the player
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
@ -125,11 +151,15 @@ public partial class CS2_SimpleAdmin
if (!silent)
{
if (command == null)
Helper.LogCommand(caller, $"css_ban {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
{
Helper.LogCommand(caller, $"css_ban {Helper.GetLogName(player)} {time} {reason}");
}
else
{
Helper.LogCommand(caller, command);
}
}
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Ban, _localizer);
}
@ -144,23 +174,26 @@ public partial class CS2_SimpleAdmin
internal void AddBan(CCSPlayerController? caller, SteamID steamid, int time, string reason, BanManager? banManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
string callerName = Helper.GetCallerName(caller);
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
{
return;
}
Ban(caller, player, time, reason, callerName, silent: true);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
}
else
{
if (!caller.CanTarget(steamid))
{
return;
}
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
@ -171,7 +204,7 @@ public partial class CS2_SimpleAdmin
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Ban, _localizer);
}
}
@ -185,34 +218,41 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<steamid> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnAddBanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
if (!Helper.ValidateSteamId(command.GetArg(1), out var steamId) || steamId == null)
if (DatabaseProvider == null)
{
return;
}
string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1)))
{
return;
}
if (!Helper.ValidateSteamId(command.GetArg(1), out SteamID? steamId) || steamId == null)
{
command.ReplyToCommand("Invalid SteamID64.");
return;
}
var steamid = steamId.SteamId64;
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
ulong steamid = steamId.SteamId64;
string reason = Helper.GetArgsReason(command, 3);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time)) return;
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time))
{
return;
}
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.SteamID]
: null;
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
var player = Helper.GetPlayerFromSteamid64(steamid);
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
{
return;
}
Ban(caller, player, time, reason, callerName, silent: true);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
@ -220,8 +260,10 @@ public partial class CS2_SimpleAdmin
else
{
if (!caller.CanTarget(new SteamID(steamId.SteamId64)))
{
return;
}
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
@ -232,7 +274,7 @@ public partial class CS2_SimpleAdmin
penaltyId);
});
});
Helper.SendDiscordPenaltyMessage(caller, steamid.ToString(), reason, time, PenaltyType.Ban, _localizer);
command.ReplyToCommand($"Player with steamid {steamid} is not online. Ban has been added offline.");
@ -241,7 +283,9 @@ public partial class CS2_SimpleAdmin
Helper.LogCommand(caller, command);
if (UnlockedCommands)
{
Server.ExecuteCommand($"banid 1 {steamId.SteamId3}");
}
}
/// <summary>
@ -253,10 +297,18 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<ip> [time in minutes/0 perm] [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnBanIpCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1))) return;
var ipAddress = command.GetArg(1);
if (DatabaseProvider == null)
{
return;
}
string callerName = caller?.PlayerName ?? _localizer?["sa_console"] ?? "Console";
if (command.ArgCount < 2 || string.IsNullOrEmpty(command.GetArg(1)))
{
return;
}
string ipAddress = command.GetArg(1);
if (!Helper.IsValidIp(ipAddress))
{
@ -264,28 +316,31 @@ public partial class CS2_SimpleAdmin
return;
}
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
string reason = Helper.GetArgsReason(command, 3);
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
if (!CheckValidBan(caller, time)) return;
if (!CheckValidBan(caller, time))
{
return;
}
var adminInfo = caller != null && caller.UserId.HasValue
? PlayersInfo[caller.SteamID]
: null;
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
var players = Helper.GetPlayerFromIp(ipAddress);
List<CCSPlayerController> players = Helper.GetPlayerFromIp(ipAddress);
if (players.Count >= 1)
{
foreach (var player in players)
foreach (CCSPlayerController player in players)
{
if (player == null || !player.IsValid) continue;
if (player == null || !player.IsValid)
{
continue;
}
if (!caller.CanTarget(player))
{
return;
}
Ban(caller, player, time, reason, callerName, silent: true);
}
@ -312,9 +367,12 @@ public partial class CS2_SimpleAdmin
/// <returns>True if ban duration is valid; otherwise, false.</returns>
private bool CheckValidBan(CCSPlayerController? caller, int duration)
{
if (caller == null) return true;
if (caller == null)
{
return true;
}
var canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
bool canPermBan = AdminManager.PlayerHasPermissions(new SteamID(caller.SteamID), "@css/permban");
if (duration <= 0 && !canPermBan)
{
@ -322,7 +380,10 @@ public partial class CS2_SimpleAdmin
return false;
}
if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan) return true;
if (duration <= Config.OtherSettings.MaxBanDuration || canPermBan)
{
return true;
}
caller.PrintToChat($"{_localizer!["sa_prefix"]} {_localizer["sa_ban_max_duration_exceeded", Config.OtherSettings.MaxBanDuration]}");
return false;
@ -337,20 +398,20 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip> [reason]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnbanCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
var callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
if (DatabaseProvider == null)
{
return;
}
string callerSteamId = caller?.SteamID.ToString() ?? _localizer?["sa_console"] ?? "Console";
if (command.GetArg(1).Length <= 1)
{
command.ReplyToCommand($"Too short pattern to search.");
return;
}
var pattern = command.GetArg(1);
var reason = command.ArgCount >= 2
? string.Join(" ", Enumerable.Range(2, command.ArgCount - 2).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
string pattern = command.GetArg(1);
string reason = Helper.GetArgsReason(command, 2);
Task.Run(async () => await BanManager.UnbanPlayer(pattern, callerSteamId, reason));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unbanned player with pattern {pattern}.");
@ -366,26 +427,31 @@ public partial class CS2_SimpleAdmin
public void OnWarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null)
{
return;
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
}
string callerName = Helper.GetCallerName(caller);
if (command.ArgCount < 2)
{
return;
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList();
}
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player.Connected == PlayerConnectedState.Connected && !player.IsHLTV).ToList();
if (playersToTarget.Count > 1 && Config.OtherSettings.DisableDangerousCommands || playersToTarget.Count == 0)
{
return;
}
var time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
var reason = command.ArgCount >= 3
? string.Join(" ", Enumerable.Range(3, command.ArgCount - 3).Select(command.GetArg)).Trim()
: _localizer?["sa_unknown"] ?? "Unknown";
reason = string.IsNullOrWhiteSpace(reason) ? _localizer?["sa_unknown"] ?? "Unknown" : reason;
int time = Math.Max(0, Helper.ParsePenaltyTime(command.GetArg(2)));
string reason = Helper.GetArgsReason(command, 3);
playersToTarget.ForEach(player =>
{
@ -407,14 +473,23 @@ public partial class CS2_SimpleAdmin
/// <param name="command">Optional command info for logging.</param>
internal void Warn(CCSPlayerController? caller, CCSPlayerController player, int time, string reason, string? callerName = null, CommandInfo? command = null)
{
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue) return;
if (!caller.CanTarget(player)) return;
if (!CheckValidBan(caller, time)) return;
if (DatabaseProvider == null || !player.IsValid || !player.UserId.HasValue)
{
return;
}
if (!caller.CanTarget(player))
{
return;
}
if (!CheckValidBan(caller, time))
{
return;
}
// Set default caller name if not provided
callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
callerName = Helper.GetCallerName(caller);
// Freeze player pawn if alive
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE)
@ -424,8 +499,8 @@ public partial class CS2_SimpleAdmin
}
// Get player and admin information
var playerInfo = PlayersInfo[player.SteamID];
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
PlayerInfo playerInfo = PlayersInfo[player.SteamID];
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
// Asynchronously handle warning logic
Task.Run(async () =>
@ -438,16 +513,20 @@ public partial class CS2_SimpleAdmin
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
int totalWarns = await WarnManager.GetPlayerWarnsCount(player.SteamID);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
var lastKey = Config.WarnThreshold.Keys.Max();
int lastKey = Config.WarnThreshold.Keys.Max();
if (totalWarns >= lastKey)
{
punishCommand = Config.WarnThreshold[lastKey];
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
}
else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value))
{
punishCommand = value;
}
if (!string.IsNullOrEmpty(punishCommand))
{
@ -460,13 +539,25 @@ public partial class CS2_SimpleAdmin
});
// Determine message keys and arguments based on warning time
var (messageKey, activityMessageKey, centerArgs, adminActivityArgs) = time == 0
? ("sa_player_warn_message_perm", "sa_admin_warn_message_perm",
new object[] { reason, "CALLER" },
new object[] { "CALLER", player.PlayerName, reason })
: ("sa_player_warn_message_time", "sa_admin_warn_message_time",
[reason, time, "CALLER"],
["CALLER", player.PlayerName, reason, time]);
string? messageKey;
string? activityMessageKey;
object[]? centerArgs;
object[]? adminActivityArgs;
if (time == 0)
{
messageKey = "sa_player_warn_message_perm";
activityMessageKey = "sa_admin_warn_message_perm";
centerArgs = new object[] { reason, "CALLER" };
adminActivityArgs = new object[] { "CALLER", player.PlayerName, reason };
}
else
{
messageKey = "sa_player_warn_message_time";
activityMessageKey = "sa_admin_warn_message_time";
centerArgs = [reason, time, "CALLER"];
adminActivityArgs = ["CALLER", player.PlayerName, reason, time];
}
// Display center message to the playser
Helper.DisplayCenterMessage(player, messageKey, callerName, centerArgs);
@ -479,14 +570,18 @@ public partial class CS2_SimpleAdmin
// Log the warning command
if (command == null)
Helper.LogCommand(caller, $"css_warn {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time} {reason}");
{
Helper.LogCommand(caller, $"css_warn {Helper.GetLogName(player)} {time} {reason}");
}
else
{
Helper.LogCommand(caller, command);
}
// Send Discord notification for the warning
Helper.SendDiscordPenaltyMessage(caller, player, reason, time, PenaltyType.Warn, _localizer);
}
/// <summary>
/// Adds a warning to a player by their SteamID, including support for offline players.
/// </summary>
@ -498,18 +593,18 @@ public partial class CS2_SimpleAdmin
internal void AddWarn(CCSPlayerController? caller, SteamID steamid, int time, string reason, WarnManager? warnManager = null)
{
// Set default caller name if not provided
var callerName = !string.IsNullOrEmpty(caller?.PlayerName)
? caller.PlayerName
: (_localizer?["sa_console"] ?? "Console");
var adminInfo = caller != null && caller.UserId.HasValue ? PlayersInfo[caller.SteamID] : null;
string callerName = Helper.GetCallerName(caller);
var player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
PlayerInfo? adminInfo = Helper.GetAdminInfo(caller);
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid.SteamId64);
if (player != null && player.IsValid)
{
if (!caller.CanTarget(player))
{
return;
}
Warn(caller, player, time, reason, callerName);
//command.ReplyToCommand($"Banned player {player.PlayerName}.");
@ -517,8 +612,10 @@ public partial class CS2_SimpleAdmin
else
{
if (!caller.CanTarget(steamid))
{
return;
}
// Asynchronous ban operation if player is not online or not found
Task.Run(async () =>
{
@ -530,16 +627,20 @@ public partial class CS2_SimpleAdmin
});
// Check for warn thresholds and execute punish command if applicable
var totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
int totalWarns = await WarnManager.GetPlayerWarnsCount(steamid.SteamId64);
if (Config.WarnThreshold.Count > 0)
{
string? punishCommand = null;
var lastKey = Config.WarnThreshold.Keys.Max();
int lastKey = Config.WarnThreshold.Keys.Max();
if (totalWarns >= lastKey)
{
punishCommand = Config.WarnThreshold[lastKey];
else if (Config.WarnThreshold.TryGetValue(totalWarns, out var value))
}
else if (Config.WarnThreshold.TryGetValue(totalWarns, out string? value))
{
punishCommand = value;
}
if (!string.IsNullOrEmpty(punishCommand))
{
@ -550,11 +651,11 @@ public partial class CS2_SimpleAdmin
}
}
});
Helper.SendDiscordPenaltyMessage(caller, steamid.SteamId64.ToString(), reason, time, PenaltyType.Warn, _localizer);
}
}
/// <summary>
/// Handles removing a warning (unwarn) by a pattern string.
/// </summary>
@ -564,7 +665,10 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<steamid or name or ip>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnUnwarnCommand(CCSPlayerController? caller, CommandInfo command)
{
if (DatabaseProvider == null) return;
if (DatabaseProvider == null)
{
return;
}
if (command.GetArg(1).Length <= 1)
{
@ -572,9 +676,9 @@ public partial class CS2_SimpleAdmin
return;
}
var pattern = command.GetArg(1);
string pattern = command.GetArg(1);
Task.Run(async () => await WarnManager.UnwarnPlayer(pattern));
Helper.LogCommand(caller, command);
command.ReplyToCommand($"Unwarned player with pattern {pattern}.");
}
}
}

View file

@ -1,16 +1,21 @@
using CounterStrikeSharp.API.Core;
using System.Text;
using System.Text.RegularExpressions;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using System.Text;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Menus;
using PanoramaUiKit.Shared.Api;
using PanoramaUiKit.Shared.Common;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
private const float AnnouncementSeconds = 7f;
/// <summary>
/// Sends a chat message only to admins that have chat permission.
/// The message is encoded properly to handle UTF-8 characters.
@ -23,16 +28,18 @@ public partial class CS2_SimpleAdmin
{
Helper.LogCommand(caller, command);
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
foreach (var player in Helper.GetValidPlayers()
foreach (CCSPlayerController? player in Helper.GetValidPlayers()
.Where(p => AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
if (_localizer != null)
{
player.PrintToChat(_localizer["sa_adminchat_template_admin",
caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName,
Helper.GetCallerName(caller),
utf8String]);
}
}
}
@ -45,14 +52,17 @@ public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/chat")]
public void OnAdminCustomSayCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0)
{
return;
}
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers())
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
player.PrintToChat(utf8String.ReplaceColorTags());
}
@ -67,14 +77,17 @@ public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/chat")]
public void OnAdminSayCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0) return;
if (command.GetCommandString[command.GetCommandString.IndexOf(' ')..].Length == 0)
{
return;
}
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
var callerName = caller?.PlayerName;
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
string? callerName = caller?.PlayerName;
Helper.LogCommand(caller, command);
foreach (var player in Helper.GetValidPlayers())
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
// The message always goes out (it is a broadcast, not an activity notice),
// but the admin's name follows ShowActivityType like every other activity message.
@ -93,19 +106,23 @@ public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/chat")]
public void OnAdminPrivateSayCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
string callerName = Helper.GetCallerName(caller);
var targets = GetTarget(command);
if (targets == null) return;
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
//Helper.LogCommand(caller, command);
var range = command.GetArg(0).Length + command.GetArg(1).Length + 2;
var message = command.GetCommandString[range..];
int range = command.GetArg(0).Length + command.GetArg(1).Length + 2;
string message = command.GetCommandString[range..];
var utf8BytesString = Encoding.UTF8.GetBytes(message);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
byte[] utf8BytesString = Encoding.UTF8.GetBytes(message);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
playersToTarget.ForEach(player =>
{
@ -116,7 +133,7 @@ public partial class CS2_SimpleAdmin
}
/// <summary>
/// Broadcasts a center-screen message to all players.
/// Broadcasts a message to all players in a Panorama UI Kit banner.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
@ -124,15 +141,15 @@ public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/chat")]
public void OnAdminCenterSayCommand(CCSPlayerController? caller, CommandInfo command)
{
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
Helper.PrintToCenterAll(utf8String.ReplaceColorTags());
UiKit.Instance?.Banner(UiKit.BannerLayout, StripColorTags(utf8String), UiStyle.Info, AnnouncementSeconds);
}
/// <summary>
/// Sends a HUD alert message to all players.
/// Sends a message to all players in a Panorama UI Kit toast, titled with the admin's name.
/// </summary>
/// <param name="caller">The admin or console sending the message.</param>
/// <param name="command">The command input containing the message.</param>
@ -140,14 +157,31 @@ public partial class CS2_SimpleAdmin
[RequiresPermissions("@css/chat")]
public void OnAdminHudSayCommand(CCSPlayerController? caller, CommandInfo command)
{
var utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
var utf8String = Encoding.UTF8.GetString(utf8BytesString);
byte[] utf8BytesString = Encoding.UTF8.GetBytes(command.GetCommandString[command.GetCommandString.IndexOf(' ')..]);
string utf8String = Encoding.UTF8.GetString(utf8BytesString);
Helper.LogCommand(caller, command);
VirtualFunctions.ClientPrintAll(
HudDestination.Alert,
utf8String.ReplaceColorTags(),
0, 0, 0, 0, 0);
IPanoramaUiKit? kit = UiKit.Instance;
if (kit == null)
{
return;
}
string message = StripColorTags(utf8String);
string? callerName = caller?.PlayerName;
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
kit.Toast(player, UiKit.ToastLayout, Helper.ResolveActivityName(player, callerName), message, UiStyle.Info, AnnouncementSeconds);
}
}
}
/// <summary>
/// Panorama does not render chat color tags such as {red}: drop them.
/// </summary>
private static string StripColorTags(string message)
{
return Regex.Replace(message, @"\{[A-Za-z]+\}", string.Empty).Trim();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,21 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Timers;
using CS2_SimpleAdmin.Menus;
using PanoramaUiKit.Shared.Api;
using PanoramaUiKit.Shared.Votes;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
private const float VoteDurationSeconds = 30f;
/// <summary>
/// Handles the vote command, creates voting menu for players, and collects answers.
/// Displays results after timeout and resets voting state.
/// Handles the vote command: shows the vote screen to every player, collects one answer each
/// and prints the results when the countdown ends.
/// </summary>
/// <param name="caller">The player/admin who initiated the vote, or null for console.</param>
/// <param name="command">Command object containing question and options.</param>
@ -19,80 +24,123 @@ public partial class CS2_SimpleAdmin
public void OnVoteCommand(CCSPlayerController? caller, CommandInfo command)
{
if (command.ArgCount < 2 || _localizer == null)
{
return;
}
Helper.LogCommand(caller, command);
VoteAnswers.Clear();
var question = command.GetArg(1);
var answersCount = command.ArgCount;
if (caller == null || !SilentPlayers.Contains(caller.Slot))
if (caller != null && SilentPlayers.Contains(caller.Slot))
{
for (var i = 2; i <= answersCount - 1; i++)
{
VoteAnswers.Add(command.GetArg(i), 0);
}
foreach (var player in Helper.GetValidPlayers())
{
using (new WithTemporaryCulture(player.GetLanguage()))
{
IMenu? voteMenu = Helper.CreateMenu(_localizer["sa_admin_vote_menu_title", question]);
if (voteMenu == null)
return;
//ChatMenu voteMenu = new(_localizer!["sa_admin_vote_menu_title", question]);
for (var i = 2; i <= answersCount - 1; i++)
{
voteMenu.AddMenuOption(command.GetArg(i), Helper.HandleVotes);
}
voteMenu.PostSelectAction = PostSelectAction.Close;
Helper.PrintToCenterAll(_localizer["sa_admin_vote_message", caller == null ? _localizer["sa_console"] : caller.PlayerName, question]);
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message",
caller == null ? _localizer["sa_console"] : caller.PlayerName,
question);
voteMenu.Open(player);
//MenuManager.OpenChatMenu(player, voteMenu);
}
}
VoteInProgress = true;
return;
}
if (VoteInProgress)
IPanoramaUiKit? kit = UiKit.Instance;
if (kit == null)
{
AddTimer(30, () =>
return;
}
string question = command.GetArg(1);
string callerName = Helper.GetCallerName(caller);
List<string> answers = [];
for (int i = 2; i < command.ArgCount; i++)
{
answers.Add(command.GetArg(i));
}
int[] counts = new int[answers.Count];
Dictionary<ulong, int> ballots = [];
VoteInProgress = true;
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
kit.Votes.Show(player, BuildVote(player, question, answers, counts, null, OnVote));
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message", callerName, question);
}
Helper.PrintToCenterAll(_localizer["sa_admin_vote_message", callerName, question]);
AddTimer(VoteDurationSeconds, () =>
{
UiKit.Instance?.Votes.HideAll(UiKit.VoteLayout);
VoteInProgress = false;
if (_localizer == null)
{
foreach (var player in Helper.GetValidPlayers())
return;
}
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message_results", question);
for (int i = 0; i < answers.Count; i++)
{
if (_localizer != null)
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message_results",
question);
player.SendLocalizedMessage(_localizer, "sa_admin_vote_message_results_answer", answers[i], counts[i]);
}
}
}, TimerFlags.STOP_ON_MAPCHANGE);
return;
void OnVote(VoteEvent vote)
{
if (!VoteInProgress || ballots.ContainsKey(vote.Player.SteamID))
{
return;
}
ballots[vote.Player.SteamID] = vote.Index;
counts[vote.Index]++;
IPanoramaUiKit? currentKit = UiKit.Instance;
if (currentKit == null)
{
return;
}
foreach (CCSPlayerController player in Helper.GetValidPlayers())
{
int? selected = null;
if (ballots.TryGetValue(player.SteamID, out int index))
{
selected = index;
}
foreach (var (key, value) in VoteAnswers)
{
foreach (var player in Helper.GetValidPlayers())
{
if (_localizer != null)
player.SendLocalizedMessage(_localizer,
"sa_admin_vote_message_results_answer",
key,
value);
}
}
VoteAnswers.Clear();
VoteInProgress = false;
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
currentKit.Votes.Update(player, BuildVote(player, question, answers, counts, selected, OnVote));
}
}
}
}
/// <summary>
/// The vote screen for one player, locked once that player has voted.
/// </summary>
private static VoteOptions BuildVote(CCSPlayerController player, string question, List<string> answers, int[] counts,
int? selected, Action<VoteEvent> onVote)
{
string title;
using (new WithTemporaryCulture(player.GetLanguage()))
{
title = _localizer?["sa_admin_vote_menu_title", question] ?? question;
}
List<VoteChoice> choices = [];
for (int i = 0; i < answers.Count; i++)
{
choices.Add(new VoteChoice(answers[i], counts[i]));
}
return new VoteOptions
{
Layout = UiKit.VoteLayout,
Title = title,
Choices = choices,
Selected = selected,
Locked = selected.HasValue,
Seconds = VoteDurationSeconds,
OnVote = onVote
};
}
}

View file

@ -49,7 +49,7 @@
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// callerName ??= Helper.GetCallerName(caller);
//
// // Toggle no-clip mode for the player
// player.Pawn.Value?.ToggleNoclip();
@ -67,7 +67,7 @@
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_noclip {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Helper.LogCommand(caller, $"css_noclip {Helper.GetLogName(player)}");
// }
//
// /// <summary>
@ -80,7 +80,7 @@
// [CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
// public void OnGodCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// var callerName = Helper.GetCallerName(caller);
// var targets = GetTarget(command);
// if (targets == null) return;
//
@ -111,7 +111,7 @@
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// var callerName = Helper.GetCallerName(caller);
//
// // Toggle god mode for the player
// if (!GodPlayers.Add(player.Slot))
@ -121,7 +121,7 @@
//
// // Log the command
// if (command == null)
// Helper.LogCommand(caller, $"css_god {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Helper.LogCommand(caller, $"css_god {Helper.GetLogName(player)}");
//
// // Determine message key and arguments for the god mode notification
// var (activityMessageKey, adminActivityArgs) =
@ -144,7 +144,7 @@
// [RequiresPermissions("@css/slay")]
// public void OnFreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// var callerName = Helper.GetCallerName(caller);
// int.TryParse(command.GetArg(2), out var time);
//
// var targets = GetTarget(command);
@ -171,7 +171,7 @@
// [RequiresPermissions("@css/slay")]
// public void OnResizeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// var callerName = Helper.GetCallerName(caller);
// float.TryParse(command.GetArg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var size);
//
// var targets = GetTarget(command);
@ -221,7 +221,7 @@
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// callerName ??= Helper.GetCallerName(caller);
//
// // Freeze player pawn
// player.Pawn.Value?.Freeze();
@ -245,7 +245,7 @@
//
// // Log the command and send Discord notification
// if (command == null)
// Helper.LogCommand(caller, $"css_freeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {time}");
// Helper.LogCommand(caller, $"css_freeze {Helper.GetLogName(player)} {time}");
// }
//
// /// <summary>
@ -257,7 +257,7 @@
// [RequiresPermissions("@css/slay")]
// public void OnUnfreezeCommand(CCSPlayerController? caller, CommandInfo command)
// {
// var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
// var callerName = Helper.GetCallerName(caller);
//
// var targets = GetTarget(command);
// if (targets == null) return;
@ -284,7 +284,7 @@
// if (!caller.CanTarget(player)) return;
//
// // Set default caller name if not provided
// callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
// callerName ??= Helper.GetCallerName(caller);
//
// // Unfreeze player pawn
// player.Pawn.Value?.Unfreeze();
@ -302,6 +302,6 @@
//
// // Log the command and send Discord notification
// if (command == null)
// Helper.LogCommand(caller, $"css_unfreeze {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
// Helper.LogCommand(caller, $"css_unfreeze {Helper.GetLogName(player)}");
// }
// }
// }

View file

@ -1,10 +1,9 @@
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
namespace CS2_SimpleAdmin;
@ -21,17 +20,20 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<#userid or name>", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlayCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var targets = GetTarget(command);
if (targets == null) return;
string callerName = Helper.GetCallerName(caller);
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
var playersToTarget = targets.Players.Where(player => player.IsValid && player is {IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
playersToTarget.ForEach(player =>
{
Slay(caller, player, callerName, command);
});
Helper.LogCommand(caller, command);
}
@ -44,18 +46,25 @@ public partial class CS2_SimpleAdmin
/// <param name="command">Optional command info for logging.</param>
internal static void Slay(CCSPlayerController? caller, CCSPlayerController player, string? callerName = null, CommandInfo? command = null)
{
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected) return;
if (!caller.CanTarget(player)) return;
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
{
return;
}
if (!caller.CanTarget(player))
{
return;
}
// Set default caller name if not provided
callerName ??= caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
callerName ??= Helper.GetCallerName(caller);
// Make the player commit suicide
player.CommitSuicide(false, true);
player.EmitSound("Player.Death");
// Determine message keys and arguments for the slay notification
var (activityMessageKey, adminActivityArgs) =
(string? activityMessageKey, object[]? adminActivityArgs) =
("sa_admin_slay_message",
new object[] { "CALLER", player.PlayerName });
@ -67,7 +76,9 @@ public partial class CS2_SimpleAdmin
// Log the command and send Discord notification
if (command == null)
Helper.LogCommand(caller, $"css_slay {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)}");
{
Helper.LogCommand(caller, $"css_slay {Helper.GetLogName(player)}");
}
}
/// <summary>
@ -79,12 +90,15 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 1, usage: "<#userid or name> [damage]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnSlapCommand(CCSPlayerController? caller, CommandInfo command)
{
var damage = 0;
int damage = 0;
var targets = GetTarget(command);
if (targets == null) return;
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
var playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player.IsValid && player is { IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE }).ToList();
if (command.ArgCount >= 2)
{
@ -94,14 +108,16 @@ public partial class CS2_SimpleAdmin
playersToTarget.ForEach(player =>
{
if (player.Connected != PlayerConnectedState.Connected)
{
return;
}
if (caller!.CanTarget(player))
{
Slap(caller, player, damage, command);
}
});
Helper.LogCommand(caller, command);
}
@ -114,26 +130,34 @@ public partial class CS2_SimpleAdmin
/// <param name="command">Optional command info for logging.</param>
internal static void Slap(CCSPlayerController? caller, CCSPlayerController player, int damage, CommandInfo? command = null)
{
if (!caller.CanTarget(player)) return;
if (!caller.CanTarget(player))
{
return;
}
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
string callerName = Helper.GetCallerName(caller);
// Apply slap damage to the player
player.Pawn.Value?.Slap(damage);
player.EmitSound("Player.DamageFall");
// Log the command
if (command == null)
Helper.LogCommand(caller, $"css_slap {(string.IsNullOrEmpty(player.PlayerName) ? player.SteamID.ToString() : player.PlayerName)} {damage}");
{
Helper.LogCommand(caller, $"css_slap {Helper.GetLogName(player)} {damage}");
}
// Determine message key and arguments for the slap notification
var (activityMessageKey, adminActivityArgs) =
(string? activityMessageKey, object[]? adminActivityArgs) =
("sa_admin_slap_message",
new object[] { "CALLER", player.PlayerName });
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
if (caller != null && SilentPlayers.Contains(caller.Slot))
{
return;
}
if (_localizer != null)
{
@ -150,15 +174,18 @@ public partial class CS2_SimpleAdmin
[CommandHelper(minArgs: 2, usage: "<#userid or name> [<ct/tt/spec>] [-k]", whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
public void OnTeamCommand(CCSPlayerController? caller, CommandInfo command)
{
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
var teamName = command.GetArg(2).ToLower();
string callerName = Helper.GetCallerName(caller);
string teamName = command.GetArg(2).ToLower();
string _teamName;
var teamNum = CsTeam.Spectator;
CsTeam teamNum = CsTeam.Spectator;
var targets = GetTarget(command);
if (targets == null) return;
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
switch (teamName)
{
@ -185,13 +212,13 @@ public partial class CS2_SimpleAdmin
break;
}
var kill = command.GetArg(3).ToLower().Equals("-k");
bool kill = command.GetArg(3).ToLower().Equals("-k");
playersToTarget.ForEach(player =>
{
ChangeTeam(caller, player, _teamName, teamNum, kill, command);
});
Helper.LogCommand(caller, command);
}
@ -208,45 +235,79 @@ public partial class CS2_SimpleAdmin
{
// Check if the player is valid and connected
if (!player.IsValid || player.Connected != PlayerConnectedState.Connected)
{
return;
}
// Ensure the caller can target the player
if (!caller.CanTarget(player)) return;
if (!caller.CanTarget(player))
{
return;
}
// Set default caller name if not provided
var callerName = caller != null ? caller.PlayerName : _localizer?["sa_console"] ?? "Console";
string callerName = Helper.GetCallerName(caller);
// Change team based on the provided teamName and conditions
if (!teamName.Equals("swap", StringComparison.OrdinalIgnoreCase))
{
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && teamNum != CsTeam.Spectator && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
{
player.SwitchTeam(teamNum);
}
else
{
player.ChangeTeam(teamNum);
}
}
else
{
if (player.TeamNum != (byte)CsTeam.Spectator)
{
var _teamNum = (CsTeam)player.TeamNum == CsTeam.Terrorist ? CsTeam.CounterTerrorist : CsTeam.Terrorist;
teamName = _teamNum == CsTeam.Terrorist ? "TT" : "CT";
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
player.SwitchTeam(_teamNum);
CsTeam _teamNum;
if ((CsTeam)player.TeamNum == CsTeam.Terrorist)
{
_teamNum = CsTeam.CounterTerrorist;
}
else
{
_teamNum = CsTeam.Terrorist;
}
if (_teamNum == CsTeam.Terrorist)
{
teamName = "TT";
}
else
{
teamName = "CT";
}
if (player.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE && !kill && Instance.Config.OtherSettings.TeamSwitchType == 1)
{
player.SwitchTeam(_teamNum);
}
else
{
player.ChangeTeam(_teamNum);
}
}
}
// Log the command
if (command == null)
{
Helper.LogCommand(caller, $"css_team {player.PlayerName} {teamName}");
}
// Determine message key and arguments for the team change notification
var activityMessageKey = "sa_admin_team_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, teamName };
string activityMessageKey = "sa_admin_team_message";
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, teamName };
// Display admin activity message to other players
if (caller != null && SilentPlayers.Contains(caller.Slot)) return;
if (caller != null && SilentPlayers.Contains(caller.Slot))
{
return;
}
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
@ -261,21 +322,26 @@ public partial class CS2_SimpleAdmin
public void OnRenameCommand(CCSPlayerController? caller, CommandInfo command)
{
// Set default caller name if not provided
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
string callerName = Helper.GetCallerName(caller);
// Get the new name from the command arguments
var newName = command.GetArg(2);
string newName = command.GetArg(2);
// Check if the new name is valid
if (string.IsNullOrEmpty(newName))
{
return;
}
// Retrieve the targets based on the command
var targets = GetTarget(command);
if (targets == null) return;
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
// Filter out valid players from the targets
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
// Log the command
Helper.LogCommand(caller, command);
@ -285,15 +351,19 @@ public partial class CS2_SimpleAdmin
{
// Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
{
return;
}
// Determine message key and arguments for the rename notification
var activityMessageKey = "sa_admin_rename_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
string activityMessageKey = "sa_admin_rename_message";
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
// Display admin activity message to other players
if (caller != null && !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Rename the player
player.Rename(newName);
@ -310,17 +380,20 @@ public partial class CS2_SimpleAdmin
public void OnPrenameCommand(CCSPlayerController? caller, CommandInfo command)
{
// Set default caller name if not provided
var callerName = caller == null ? _localizer?["sa_console"] ?? "Console" : caller.PlayerName;
string callerName = Helper.GetCallerName(caller);
// Get the new name from the command arguments
var newName = command.GetArg(2);
string newName = command.GetArg(2);
// Retrieve the targets based on the command
var targets = GetTarget(command);
if (targets == null) return;
TargetResult? targets = GetTarget(command);
if (targets == null)
{
return;
}
// Filter out valid players from the targets
var playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
List<CCSPlayerController> playersToTarget = targets.Players.Where(player => player is { IsValid: true, IsHLTV: false }).ToList();
// Log the command
Helper.LogCommand(caller, command);
@ -330,20 +403,22 @@ public partial class CS2_SimpleAdmin
{
// Check if the player is connected and can be targeted
if (player.Connected != PlayerConnectedState.Connected || !caller!.CanTarget(player))
{
return;
}
// Determine message key and arguments for the rename notification
var activityMessageKey = "sa_admin_rename_message";
var adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
string activityMessageKey = "sa_admin_rename_message";
object[] adminActivityArgs = new object[] { "CALLER", player.PlayerName, newName };
// Display admin activity message to other players
if (caller != null && !SilentPlayers.Contains(caller.Slot))
{
Helper.ShowAdminActivity(activityMessageKey, callerName, false, adminActivityArgs);
}
// Determine if the new name is valid and update the renamed players list
var steamId = player.SteamID;
ulong steamId = player.SteamID;
if (!string.IsNullOrEmpty(newName))
{
RenamedPlayers[steamId] = newName;
@ -370,51 +445,65 @@ public partial class CS2_SimpleAdmin
IEnumerable<CCSPlayerController> playersToTeleport;
CCSPlayerController? destinationPlayer;
var targets = GetTarget(command);
TargetResult? targets = GetTarget(command);
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
{
return;
}
if (targets == null || targets.Count() != 1)
{
return;
}
destinationPlayer = targets.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null || !caller.CanTarget(destinationPlayer) || caller.PlayerPawn.Value == null)
{
return;
}
playersToTeleport = [caller];
}
else
{
var destination = GetTarget(command, 2);
TargetResult? destination = GetTarget(command, 2);
if (targets == null || destination == null || destination.Count() != 1)
{
return;
}
destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null)
{
return;
}
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller.CanTarget(p))
.ToList();
if (!playersToTeleport.Any())
{
return;
}
}
// Log command
Helper.LogCommand(caller, command);
foreach (var player in playersToTeleport)
foreach (CCSPlayerController player in playersToTeleport)
{
if (player.PlayerPawn?.Value == null || destinationPlayer?.PlayerPawn?.Value == null)
{
continue;
}
player.TeleportPlayer(destinationPlayer);
@ -453,7 +542,7 @@ public partial class CS2_SimpleAdmin
}
}
}
/// <summary>
/// Brings targeted player(s) to the caller or specified destination player's location.
/// </summary>
@ -469,11 +558,15 @@ public partial class CS2_SimpleAdmin
if (command.ArgCount < 3)
{
if (caller == null || caller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
{
return;
}
var targets = GetTarget(command);
TargetResult? targets = GetTarget(command);
if (targets == null || !targets.Any())
{
return;
}
destinationPlayer = caller;
@ -483,20 +576,26 @@ public partial class CS2_SimpleAdmin
}
else
{
var destination = GetTarget(command);
TargetResult? destination = GetTarget(command);
if (destination == null || destination.Count() != 1)
{
return;
}
destinationPlayer = destination.Players.FirstOrDefault(p =>
p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE });
if (destinationPlayer == null)
{
return;
}
// Rest args = targets to teleport
var targets = GetTarget(command, 2);
TargetResult? targets = GetTarget(command, 2);
if (targets == null || !targets.Any())
{
return;
}
playersToTeleport = targets.Players
.Where(p => p is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.Connected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE } && caller!.CanTarget(p))
@ -504,15 +603,19 @@ public partial class CS2_SimpleAdmin
}
if (destinationPlayer == null || !playersToTeleport.Any())
{
return;
}
// Log command
Helper.LogCommand(caller, command);
foreach (var player in playersToTeleport)
foreach (CCSPlayerController player in playersToTeleport)
{
if (player.PlayerPawn?.Value == null || destinationPlayer.PlayerPawn?.Value == null)
{
continue;
}
// Teleport
player.TeleportPlayer(destinationPlayer);
@ -624,4 +727,4 @@ public partial class CS2_SimpleAdmin
// }
// }
// }
}
}

View file

@ -1,5 +1,5 @@
using CounterStrikeSharp.API.Core;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using CounterStrikeSharp.API.Core;
namespace CS2_SimpleAdmin;
@ -89,7 +89,7 @@ public class Discord
new() { Name = "Footer", Value = "" },
new() { Name = "Time", Value = "{relative}" },
];
[JsonPropertyName("DiscordAssociatedAccountsSettings")]
public DiscordPenaltySetting[] DiscordAssociatedAccountsSettings { get; set; } =
[
@ -119,7 +119,13 @@ public class CustomServerCommandData
public class MenuConfig
{
[JsonPropertyName("MenuType")] public string MenuType { get; set; } = "selectable";
[JsonPropertyName("MenuLayout")] public string MenuLayout { get; set; } = "uikit_menu";
[JsonPropertyName("VoteLayout")] public string VoteLayout { get; set; } = "uikit_vote";
[JsonPropertyName("BannerLayout")] public string BannerLayout { get; set; } = "uikit_banner";
[JsonPropertyName("ToastLayout")] public string ToastLayout { get; set; } = "uikit_toast";
[JsonPropertyName("Durations")]
public DurationItem[] Durations { get; set; } =
@ -214,7 +220,7 @@ public class OtherSettings
[JsonPropertyName("MaxBanDuration")]
public int MaxBanDuration { get; set; } = 60 * 24 * 7;
[JsonPropertyName("MaxMuteDuration")]
public int MaxMuteDuration { get; set; } = 60 * 24 * 7;
@ -226,16 +232,16 @@ public class OtherSettings
[JsonPropertyName("DisconnectedPlayersHistoryCount")]
public int DisconnectedPlayersHistoryCount { get; set; } = 10;
[JsonPropertyName("NotifyPenaltiesToAdminOnConnect")]
public bool NotifyPenaltiesToAdminOnConnect { get; set; } = true;
[JsonPropertyName("ShowBanMenuIfNoTime")]
public bool ShowBanMenuIfNoTime { get; set; } = true;
[JsonPropertyName("UserMessageGagChatType")]
public bool UserMessageGagChatType { get; set; } = false;
[JsonPropertyName("CheckMultiAccountsByIp")]
public bool CheckMultiAccountsByIp { get; set; } = true;
@ -247,11 +253,11 @@ public class OtherSettings
public class CS2_SimpleAdminConfig : BasePluginConfig
{
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 25;
[JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 26;
[JsonPropertyName("DatabaseConfig")]
[JsonPropertyName("DatabaseConfig")]
public DatabaseConfig DatabaseConfig { get; set; } = new();
[JsonPropertyName("OtherSettings")]
public OtherSettings OtherSettings { get; set; } = new();
@ -295,7 +301,7 @@ public class DatabaseConfig
{
[JsonPropertyName("DatabaseType")]
public string DatabaseType { get; set; } = "SQLite";
[JsonPropertyName("SqliteFilePath")]
public string SqliteFilePath { get; set; } = "cs2-simpleadmin.sqlite";
@ -313,7 +319,7 @@ public class DatabaseConfig
[JsonPropertyName("DatabaseName")]
public string DatabaseName { get; set; } = "";
[JsonPropertyName("DatabaseSSlMode")]
public string DatabaseSSlMode { get; set; } = "preferred";
}

View file

@ -9,13 +9,13 @@ public class Database(string dbConnectionString)
{
try
{
var connection = new MySqlConnection(dbConnectionString);
MySqlConnection connection = new MySqlConnection(dbConnectionString);
connection.Open();
// using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// cmd.ExecuteNonQueryAsync();
return connection;
}
catch (Exception ex)
@ -29,13 +29,13 @@ public class Database(string dbConnectionString)
{
try
{
var connection = new MySqlConnection(dbConnectionString);
MySqlConnection connection = new MySqlConnection(dbConnectionString);
await connection.OpenAsync();
// await using var cmd = connection.CreateCommand();
// cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
// await cmd.ExecuteNonQueryAsync();
return connection;
}
catch (Exception ex)
@ -53,9 +53,9 @@ public class Database(string dbConnectionString)
public bool CheckDatabaseConnection(out string? exception)
{
using var connection = GetConnection();
using MySqlConnection connection = GetConnection();
exception = null;
try
{
return connection.Ping();
@ -66,4 +66,4 @@ public class Database(string dbConnectionString)
return false;
}
}
}
}

View file

@ -7,7 +7,7 @@ public interface IDatabaseProvider
Task<DbConnection> CreateConnectionAsync();
Task<(bool Success, string? Exception)> CheckConnectionAsync();
Task DatabaseMigrationAsync();
// CacheManager
string GetBanSelectQuery(bool multiServer);
string GetIpHistoryQuery();
@ -15,7 +15,7 @@ public interface IDatabaseProvider
// PlayerManager
string GetUpsertPlayerIpQuery();
// PermissionManager
string GetAdminsQuery();
string GetDeleteAdminQuery(bool globalDelete);
@ -29,7 +29,7 @@ public interface IDatabaseProvider
string GetAddGroupServerQuery();
string GetDeleteGroupQuery();
string GetDeleteOldAdminsQuery();
// BanManager
string GetAddBanQuery();
string GetAddBanBySteamIdQuery();
@ -58,7 +58,7 @@ public interface IDatabaseProvider
string GetInsertUnmuteQuery(bool includeReason);
string GetUpdateMuteStatusQuery();
string GetExpireMutesQuery(bool multiServer, int timeMode);
// WarnManager
string GetAddWarnQuery(bool includePlayerName);
string GetPlayerWarnsQuery(bool multiServer, bool active);
@ -69,4 +69,4 @@ public interface IDatabaseProvider
// Penalty history (css_history)
string GetPenaltyHistoryQuery(bool multiServer);
}
}

View file

@ -12,12 +12,19 @@ public class Migration(string migrationsPath)
/// </summary>
public async Task ExecuteMigrationsAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
var files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
if (files.Count == 0) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using (var cmd = connection.CreateCommand())
List<string> files = Directory.GetFiles(migrationsPath, "*.sql").OrderBy(f => f).ToList();
if (files.Count == 0)
{
return;
}
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using (DbCommand cmd = connection.CreateCommand())
{
if (migrationsPath.Contains("sqlite", StringComparison.CurrentCultureIgnoreCase))
{
@ -42,19 +49,21 @@ public class Migration(string migrationsPath)
await cmd.ExecuteNonQueryAsync();
}
var lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
string lastAppliedVersion = await GetLastAppliedVersionAsync(connection);
foreach (var file in files)
foreach (string file in files)
{
var version = Path.GetFileNameWithoutExtension(file);
string version = Path.GetFileNameWithoutExtension(file);
if (string.Compare(version, lastAppliedVersion, StringComparison.OrdinalIgnoreCase) <= 0)
{
continue;
}
try
{
var sqlScript = await File.ReadAllTextAsync(file);
string sqlScript = await File.ReadAllTextAsync(file);
await using (var cmdMigration = connection.CreateCommand())
await using (DbCommand cmdMigration = connection.CreateCommand())
{
cmdMigration.CommandText = sqlScript;
await cmdMigration.ExecuteNonQueryAsync();
@ -79,9 +88,9 @@ public class Migration(string migrationsPath)
/// <returns>The version string of the last applied migration, or empty string if none.</returns>
private static async Task<string> GetLastAppliedVersionAsync(DbConnection connection)
{
await using var cmd = connection.CreateCommand();
await using DbCommand cmd = connection.CreateCommand();
cmd.CommandText = "SELECT version FROM sa_migrations ORDER BY id DESC LIMIT 1;";
var result = await cmd.ExecuteScalarAsync();
object? result = await cmd.ExecuteScalarAsync();
return result?.ToString() ?? string.Empty;
}
@ -92,10 +101,10 @@ public class Migration(string migrationsPath)
/// <param name="version">The version string of the migration applied.</param>
private static async Task UpdateLastAppliedVersionAsync(DbConnection connection, string version)
{
await using var cmd = connection.CreateCommand();
await using DbCommand cmd = connection.CreateCommand();
cmd.CommandText = "INSERT INTO sa_migrations (version) VALUES (@Version);";
var param = cmd.CreateParameter();
DbParameter param = cmd.CreateParameter();
param.ParameterName = "@Version";
param.Value = version;
cmd.Parameters.Add(param);

View file

@ -7,14 +7,14 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
{
public async Task<DbConnection> CreateConnectionAsync()
{
var connection = new MySqlConnection(connectionString);
MySqlConnection connection = new MySqlConnection(connectionString);
await connection.OpenAsync();
await using var cmd = connection.CreateCommand();
await using MySqlCommand cmd = connection.CreateCommand();
cmd.CommandText = "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_general_ci';";
await cmd.ExecuteNonQueryAsync();
// cmd.CommandText = "SET time_zone = '+00:00';";
await cmd.ExecuteNonQueryAsync();
@ -25,7 +25,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
{
try
{
await using var conn = await CreateConnectionAsync();
await using DbConnection conn = await CreateConnectionAsync();
return (true, null);
}
catch (Exception ex)
@ -33,52 +33,62 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
return (false, ex.Message);
}
}
public Task DatabaseMigrationAsync()
{
var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Mysql");
Migration migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Mysql");
return migration.ExecuteMigrationsAsync();
}
public string GetBanSelectQuery(bool multiServer)
{
return multiServer ? """
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
""" : """
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""";
}
if (multiServer)
{
return """
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
""";
}
return """
SELECT
id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""";
}
public static string GetBanUpdatedSelectQuery(bool multiServer)
{
return multiServer ? """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""" : """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
""";
if (multiServer)
{
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
""";
}
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
""";
}
public string GetIpHistoryQuery()
@ -99,25 +109,30 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
public string GetBanUpdateQuery(bool multiServer)
{
return multiServer ? """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
""" : """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId
""";
if (multiServer)
{
return """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
""";
}
return """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId
""";
}
public string GetAdminsQuery()
@ -131,12 +146,17 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
ORDER BY sa_admins.player_steamid
""";
}
public string GetDeleteAdminQuery(bool globalDelete) =>
globalDelete
? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID"
: "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
public string GetDeleteAdminQuery(bool globalDelete)
{
if (globalDelete)
{
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID";
}
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
}
public string GetAddAdminQuery() =>
"INSERT INTO sa_admins (player_steamid, player_name, immunity, ends, created, server_id) " +
"VALUES (@playerSteamId, @playerName, @immunity, @ends, @created, @serverid); SELECT LAST_INSERT_ID();";
@ -160,7 +180,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
public string GetAddGroupQuery() =>
"INSERT INTO sa_groups (name, immunity) VALUES (@groupName, @immunity); SELECT LAST_INSERT_ID();";
public string GetGroupIdByNameQuery() =>
"""
SELECT sgs.group_id
@ -181,7 +201,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
public string GetDeleteOldAdminsQuery() =>
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
public string GetAddBanQuery()
{
return """
@ -192,7 +212,7 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
SELECT LAST_INSERT_ID();
""";
}
public string GetAddBanBySteamIdQuery()
{
return """
@ -213,12 +233,15 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
(@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
""";
}
public string GetUnbanRetrieveBansQuery(bool multiServer)
{
return multiServer
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
if (multiServer)
{
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'";
}
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetUnbanAdminIdQuery()
@ -228,9 +251,12 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
public string GetInsertUnbanQuery(bool includeReason)
{
return includeReason
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();"
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
if (includeReason)
{
return "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();";
}
return "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
}
public string GetUpdateBanStatusQuery()
@ -249,145 +275,245 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
public string GetExpireBansQuery(bool multiServer)
{
return multiServer
? "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
: "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
if (multiServer)
{
return "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime";
}
return "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
}
public string GetExpireIpBansQuery(bool multiServer)
{
return multiServer
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
if (multiServer)
{
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime";
}
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
}
public string GetExpireOldPlayerIpsQuery()
{
return "DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
}
public string GetAddMuteQuery(bool includePlayerName) =>
includePlayerName
? """
public string GetAddMuteQuery(bool includePlayerName)
{
if (includePlayerName)
{
return """
INSERT INTO `sa_mutes`
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT LAST_INSERT_ID();
"""
: """
INSERT INTO `sa_mutes`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetIsMutedQuery(bool multiServer, int timeMode) =>
multiServer
? (timeMode == 1
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)"
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))")
: (timeMode == 1
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid"
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid");
return """
INSERT INTO `sa_mutes`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetMuteStatsQuery(bool multiServer) =>
multiServer
? """
public string GetIsMutedQuery(bool multiServer, int timeMode)
{
if (multiServer)
{
if (timeMode == 1)
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)";
}
else
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))";
}
}
if (timeMode == 1)
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid";
}
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid";
}
public string GetMuteStatsQuery(bool multiServer)
{
if (multiServer)
{
return """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID;
"""
: """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
""";
}
public string GetUpdateMutePassedQuery(bool multiServer) =>
multiServer
? "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'"
: "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
return """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
""";
}
public string GetCheckExpiredMutesQuery(bool multiServer) =>
multiServer
? "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'"
: "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
public string GetUpdateMutePassedQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'";
}
public string GetRetrieveMutesQuery(bool multiServer) =>
multiServer
? "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'"
: "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
return "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetCheckExpiredMutesQuery(bool multiServer)
{
if (multiServer)
{
return "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'";
}
return "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetRetrieveMutesQuery(bool multiServer)
{
if (multiServer)
{
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'";
}
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetUnmuteAdminIdQuery() =>
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
public string GetInsertUnmuteQuery(bool includeReason) =>
includeReason
? "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();"
: "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
public string GetInsertUnmuteQuery(bool includeReason)
{
if (includeReason)
{
return "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();";
}
return "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
}
public string GetUpdateMuteStatusQuery() =>
"UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
public string GetExpireMutesQuery(bool multiServer, int timeMode) =>
multiServer
? (timeMode == 1
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`")
: (timeMode == 1
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid"
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid");
public string GetAddWarnQuery(bool includePlayerName) =>
includePlayerName
? """
public string GetExpireMutesQuery(bool multiServer, int timeMode)
{
if (multiServer)
{
if (timeMode == 1)
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
}
else
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`";
}
}
if (timeMode == 1)
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
}
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid";
}
public string GetAddWarnQuery(bool includePlayerName)
{
if (includePlayerName)
{
return """
INSERT INTO `sa_warns`
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @playerName, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
"""
: """
INSERT INTO `sa_warns`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetPlayerWarnsQuery(bool multiServer, bool active) =>
multiServer
? active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC"
: active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
return """
INSERT INTO `sa_warns`
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT LAST_INSERT_ID();
""";
}
public string GetPlayerWarnsCountQuery(bool multiServer, bool active) =>
multiServer
? active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
: active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
public string GetPlayerWarnsQuery(bool multiServer, bool active)
{
if (multiServer)
{
if (active)
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC";
}
else
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
}
}
public string GetUnwarnByIdQuery(bool multiServer) =>
multiServer
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
if (active)
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC";
}
public string GetUnwarnLastQuery(bool multiServer) =>
multiServer
? """
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
}
public string GetPlayerWarnsCountQuery(bool multiServer, bool active)
{
if (multiServer)
{
if (active)
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'";
}
else
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID";
}
}
if (active)
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'";
}
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
}
public string GetUnwarnByIdQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId";
}
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
}
public string GetUnwarnLastQuery(bool multiServer)
{
if (multiServer)
{
return """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
@ -396,45 +522,66 @@ public class MySqlDatabaseProvider(string connectionString) : IDatabaseProvider
) AS subquery ON sa_warns.id = subquery.max_id
SET sa_warns.status = 'EXPIRED'
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid;
"""
: """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
FROM sa_warns
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
) AS subquery ON sa_warns.id = subquery.max_id
SET sa_warns.status = 'EXPIRED'
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
""";
}
public string GetExpireWarnsQuery(bool multiServer) =>
multiServer
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
return """
UPDATE sa_warns
JOIN (
SELECT MAX(id) AS max_id
FROM sa_warns
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
) AS subquery ON sa_warns.id = subquery.max_id
SET sa_warns.status = 'EXPIRED'
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
""";
}
public string GetPenaltyHistoryQuery(bool multiServer) =>
$"""
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
FROM sa_bans b
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
UNION ALL
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
FROM sa_mutes m
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
UNION ALL
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
FROM sa_warns w
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
ORDER BY created DESC
""";
public string GetExpireWarnsQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
}
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
}
public string GetPenaltyHistoryQuery(bool multiServer)
{
string banServerFilter = string.Empty;
string muteServerFilter = string.Empty;
string warnServerFilter = string.Empty;
if (!multiServer)
{
banServerFilter = "AND b.server_id = @serverid";
muteServerFilter = "AND m.server_id = @serverid";
warnServerFilter = "AND w.server_id = @serverid";
}
return $"""
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
FROM sa_bans b
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
WHERE b.player_steamid = @PlayerSteamID {banServerFilter}
UNION ALL
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
FROM sa_mutes m
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
WHERE m.player_steamid = @PlayerSteamID {muteServerFilter}
UNION ALL
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
FROM sa_warns w
WHERE w.player_steamid = @PlayerSteamID {warnServerFilter}
ORDER BY created DESC
""";
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

View file

@ -9,7 +9,7 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
public async Task<DbConnection> CreateConnectionAsync()
{
var conn = new SQLiteConnection(_connectionString);
SQLiteConnection conn = new SQLiteConnection(_connectionString);
await conn.OpenAsync();
return conn;
}
@ -18,8 +18,8 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
{
try
{
await using var conn = await CreateConnectionAsync();
await using var cmd = conn.CreateCommand();
await using DbConnection conn = await CreateConnectionAsync();
await using DbCommand cmd = conn.CreateCommand();
cmd.CommandText = "SELECT 1";
await cmd.ExecuteScalarAsync();
return (true, null);
@ -32,33 +32,40 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
public Task DatabaseMigrationAsync()
{
var migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite");
Migration migration = new Migration(CS2_SimpleAdmin.Instance.ModuleDirectory + "/Database/Migrations/Sqlite");
return migration.ExecuteMigrationsAsync();
}
public string GetBanSelectQuery(bool multiServer) =>
multiServer
? """
public string GetBanSelectQuery(bool multiServer)
{
if (multiServer)
{
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
"""
: """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""";
}
public static string GetBanUpdatedSelectQuery(bool multiServer) =>
multiServer
? """
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""";
}
public static string GetBanUpdatedSelectQuery(bool multiServer)
{
if (multiServer)
{
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
@ -67,18 +74,21 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
FROM sa_bans
WHERE updated_at > @lastUpdate OR created > @lastUpdate
ORDER BY updated_at DESC
"""
: """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE (updated_at > @lastUpdate OR created > @lastUpdate)
AND server_id = @serverId
ORDER BY updated_at DESC
""";
}
return """
SELECT id AS Id,
player_name AS PlayerName,
player_steamid AS PlayerSteamId,
player_ip AS PlayerIp,
status AS Status
FROM sa_bans
WHERE (updated_at > @lastUpdate OR created > @lastUpdate)
AND server_id = @serverId
ORDER BY updated_at DESC
""";
}
public string GetIpHistoryQuery() =>
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC";
@ -94,25 +104,30 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
""";
}
public string GetBanUpdateQuery(bool multiServer) =>
multiServer
? """
public string GetBanUpdateQuery(bool multiServer)
{
if (multiServer)
{
return """
UPDATE sa_bans
SET player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
"""
: """
UPDATE sa_bans
SET player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
AND server_id = @ServerId
""";
}
return """
UPDATE sa_bans
SET player_ip = COALESCE(player_ip, @PlayerIP),
player_name = COALESCE(player_name, @PlayerName)
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
AND status = 'ACTIVE'
AND (duration = 0 OR ends > @CurrentTime)
AND server_id = @ServerId
""";
}
public string GetAddBanQuery() =>
"""
@ -141,32 +156,52 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
SELECT last_insert_rowid();
""";
public string GetUnbanRetrieveBansQuery(bool multiServer) =>
multiServer
? "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
: "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
public string GetUnbanRetrieveBansQuery(bool multiServer)
{
if (multiServer)
{
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'";
}
return "SELECT id, player_steamid FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetUnbanAdminIdQuery() =>
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
public string GetInsertUnbanQuery(bool includeReason) =>
includeReason
? "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();"
: "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
public string GetInsertUnbanQuery(bool includeReason)
{
if (includeReason)
{
return "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT last_insert_rowid();";
}
return "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT last_insert_rowid();";
}
// SQLite has no ON UPDATE CURRENT_TIMESTAMP, so updated_at must be set explicitly or the cache refresh never sees the change
public string GetUpdateBanStatusQuery() =>
"UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId, updated_at = CURRENT_TIMESTAMP WHERE id = @banId";
public string GetExpireBansQuery(bool multiServer) =>
multiServer
? "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime"
: "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
public string GetExpireBansQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime";
}
public string GetExpireIpBansQuery(bool multiServer) =>
multiServer
? "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime"
: "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
return "UPDATE sa_bans SET status = 'EXPIRED', updated_at = CURRENT_TIMESTAMP WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @currentTime AND server_id = @serverid";
}
public string GetExpireIpBansQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime";
}
return "UPDATE sa_bans SET player_ip = NULL WHERE status = 'ACTIVE' AND ends <= @ipBansTime AND server_id = @serverid";
}
public string GetExpireOldPlayerIpsQuery() =>
"DELETE FROM sa_players_ips WHERE used_at <= @ipBansTime";
@ -190,10 +225,15 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
ORDER BY sa_admins.player_steamid
""";
public string GetDeleteAdminQuery(bool globalDelete) =>
globalDelete
? "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID"
: "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
public string GetDeleteAdminQuery(bool globalDelete)
{
if (globalDelete)
{
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID";
}
return "DELETE FROM sa_admins WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId";
}
public string GetAddAdminQuery() =>
"""
@ -244,170 +284,285 @@ public class SqliteDatabaseProvider(string filePath) : IDatabaseProvider
public string GetDeleteOldAdminsQuery() =>
"DELETE FROM sa_admins WHERE ends IS NOT NULL AND ends <= @CurrentTime;";
public string GetAddMuteQuery(bool includePlayerName) =>
includePlayerName
? """
public string GetAddMuteQuery(bool includePlayerName)
{
if (includePlayerName)
{
return """
INSERT INTO sa_mutes
(player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
VALUES (@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT last_insert_rowid();
"""
: """
INSERT INTO sa_mutes
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT last_insert_rowid();
""";
}
public string GetIsMutedQuery(bool multiServer, int timeMode) =>
multiServer
? (timeMode == 1
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)"
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))")
: (timeMode == 1
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid"
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid");
return """
INSERT INTO sa_mutes
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, type, server_id)
VALUES (@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
SELECT last_insert_rowid();
""";
}
public string GetMuteStatsQuery(bool multiServer) =>
multiServer
? """
public string GetIsMutedQuery(bool multiServer, int timeMode)
{
if (multiServer)
{
if (timeMode == 1)
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)";
}
else
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))";
}
}
if (timeMode == 1)
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid";
}
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid";
}
public string GetMuteStatsQuery(bool multiServer)
{
if (multiServer)
{
return """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID;
"""
: """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
""";
}
public string GetUpdateMutePassedQuery(bool multiServer) =>
multiServer
? "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'"
: "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
return """
SELECT
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
FROM sa_mutes
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
""";
}
public string GetCheckExpiredMutesQuery(bool multiServer) =>
multiServer
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'"
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
public string GetUpdateMutePassedQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'";
}
public string GetRetrieveMutesQuery(bool multiServer) =>
multiServer
? "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'"
: "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
return "UPDATE sa_mutes SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetCheckExpiredMutesQuery(bool multiServer)
{
if (multiServer)
{
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'";
}
return "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetRetrieveMutesQuery(bool multiServer)
{
if (multiServer)
{
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE'";
}
return "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
}
public string GetUnmuteAdminIdQuery() =>
"SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
public string GetInsertUnmuteQuery(bool includeReason) =>
includeReason
? "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT last_insert_rowid();"
: "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT last_insert_rowid();";
public string GetInsertUnmuteQuery(bool includeReason)
{
if (includeReason)
{
return "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT last_insert_rowid();";
}
return "INSERT INTO sa_unmutes (mute_id, admin_id) VALUES (@muteId, @adminId); SELECT last_insert_rowid();";
}
public string GetUpdateMuteStatusQuery() =>
"UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
public string GetExpireMutesQuery(bool multiServer, int timeMode) =>
multiServer
? (timeMode == 1
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration")
: (timeMode == 1
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid"
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration AND server_id = @serverid");
public string GetAddWarnQuery(bool includePlayerName) =>
includePlayerName
? """
public string GetExpireMutesQuery(bool multiServer, int timeMode)
{
if (multiServer)
{
if (timeMode == 1)
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime";
}
else
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration";
}
}
if (timeMode == 1)
{
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
}
return "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND passed >= duration AND server_id = @serverid";
}
public string GetAddWarnQuery(bool includePlayerName)
{
if (includePlayerName)
{
return """
INSERT INTO sa_warns
(player_steamid, player_name, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerSteamid, @playerName, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
"""
: """
INSERT INTO sa_warns
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
}
public string GetPlayerWarnsQuery(bool multiServer, bool active) =>
multiServer
? active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC"
: active
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
return """
INSERT INTO sa_warns
(player_steamid, admin_steamid, admin_name, reason, duration, ends, created, server_id)
VALUES
(@playerSteamid, @adminSteamid, @adminName, @warnReason, @duration, @ends, @created, @serverid);
SELECT last_insert_rowid();
""";
}
public string GetPlayerWarnsCountQuery(bool multiServer, bool active) =>
multiServer
? active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
: active
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
public string GetPlayerWarnsQuery(bool multiServer, bool active)
{
if (multiServer)
{
if (active)
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC";
}
else
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
}
}
public string GetUnwarnByIdQuery(bool multiServer) =>
multiServer
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
if (active)
{
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC";
}
public string GetUnwarnLastQuery(bool multiServer) =>
multiServer
? """
return "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
}
public string GetPlayerWarnsCountQuery(bool multiServer, bool active)
{
if (multiServer)
{
if (active)
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'";
}
else
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID";
}
}
if (active)
{
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'";
}
return "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid";
}
public string GetUnwarnByIdQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId";
}
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
}
public string GetUnwarnLastQuery(bool multiServer)
{
if (multiServer)
{
return """
UPDATE sa_warns
SET status = 'EXPIRED'
WHERE status = 'ACTIVE'
AND player_steamid = @steamid
ORDER BY id DESC
LIMIT 1
"""
: """
UPDATE sa_warns
SET status = 'EXPIRED'
WHERE status = 'ACTIVE'
AND player_steamid = @steamid
AND server_id = @serverid
ORDER BY id DESC
LIMIT 1
""";
}
public string GetExpireWarnsQuery(bool multiServer) =>
multiServer
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime"
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
return """
UPDATE sa_warns
SET status = 'EXPIRED'
WHERE status = 'ACTIVE'
AND player_steamid = @steamid
AND server_id = @serverid
ORDER BY id DESC
LIMIT 1
""";
}
public string GetPenaltyHistoryQuery(bool multiServer) =>
$"""
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
FROM sa_bans b
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
WHERE b.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND b.server_id = @serverid")}
UNION ALL
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
FROM sa_mutes m
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
WHERE m.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND m.server_id = @serverid")}
UNION ALL
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
FROM sa_warns w
WHERE w.player_steamid = @PlayerSteamID {(multiServer ? "" : "AND w.server_id = @serverid")}
ORDER BY created DESC
""";
public string GetExpireWarnsQuery(bool multiServer)
{
if (multiServer)
{
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime";
}
return "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND duration > 0 AND ends <= @CurrentTime AND server_id = @serverid";
}
public string GetPenaltyHistoryQuery(bool multiServer)
{
string banServerFilter = string.Empty;
string muteServerFilter = string.Empty;
string warnServerFilter = string.Empty;
if (!multiServer)
{
banServerFilter = "AND b.server_id = @serverid";
muteServerFilter = "AND m.server_id = @serverid";
warnServerFilter = "AND w.server_id = @serverid";
}
return $"""
SELECT b.id, 'BAN' AS type, b.player_name, b.admin_name, b.reason, b.duration, b.created, b.ends, b.status,
ub.reason AS lift_reason, ub.date AS lift_date, ua.player_name AS lift_admin
FROM sa_bans b
LEFT JOIN sa_unbans ub ON ub.id = b.unban_id
LEFT JOIN sa_admins ua ON ua.id = ub.admin_id
WHERE b.player_steamid = @PlayerSteamID {banServerFilter}
UNION ALL
SELECT m.id, m.type, m.player_name, m.admin_name, m.reason, m.duration, m.created, m.ends, m.status,
um.reason AS lift_reason, um.date AS lift_date, ua.player_name AS lift_admin
FROM sa_mutes m
LEFT JOIN sa_unmutes um ON um.id = m.unmute_id
LEFT JOIN sa_admins ua ON ua.id = um.admin_id
WHERE m.player_steamid = @PlayerSteamID {muteServerFilter}
UNION ALL
SELECT w.id, 'WARN' AS type, w.player_name, w.admin_name, w.reason, w.duration, w.created, w.ends, w.status,
NULL AS lift_reason, NULL AS lift_date, NULL AS lift_admin
FROM sa_warns w
WHERE w.player_steamid = @PlayerSteamID {warnServerFilter}
ORDER BY created DESC
""";
}
}

View file

@ -1,24 +1,25 @@
using System.Numerics;
using System.Text;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.UserMessages;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Models;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Logging;
using System.Text;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.UserMessages;
namespace CS2_SimpleAdmin;
public partial class CS2_SimpleAdmin
{
private bool _serverLoading;
private void RegisterEvents()
{
RegisterListener<Listeners.OnMapStart>(OnMapStart);
@ -27,14 +28,16 @@ public partial class CS2_SimpleAdmin
RegisterListener<Listeners.OnClientConnected>(OnClientConnected);
RegisterListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
if (Config.OtherSettings.UserMessageGagChatType)
{
HookUserMessage(118, HookUmChat);
}
AddCommandListener(null, ComamndListenerHandler);
// AddCommandListener("callvote", OnCommandCallVote);
// AddCommandListener("say", OnCommandSay);
// AddCommandListener("say_team", OnCommandTeamSay);
}
private void UnregisterEvents()
{
RemoveListener<Listeners.OnMapStart>(OnMapStart);
@ -42,15 +45,17 @@ public partial class CS2_SimpleAdmin
RemoveListener<Listeners.OnClientConnected>(OnClientConnected);
RemoveListener<Listeners.OnGameServerSteamAPIActivated>(OnGameServerSteamAPIActivated);
if (Config.OtherSettings.UserMessageGagChatType)
{
UnhookUserMessage(118, HookUmChat);
}
RemoveCommandListener(null!, ComamndListenerHandler, HookMode.Pre);
// AddCommandListener("callvote", OnCommandCallVote);
// AddCommandListener("say", OnCommandSay);
// AddCommandListener("say_team", OnCommandTeamSay);
}
// private HookResult OnCommandCallVote(CCSPlayerController? caller, CommandInfo info)
// {
// var voteType = info.GetArg(1).ToLower();
@ -71,8 +76,10 @@ public partial class CS2_SimpleAdmin
private void OnGameServerSteamAPIActivated()
{
if (ServerLoaded || _serverLoading)
{
return;
}
_serverLoading = true;
new ServerManager().LoadServerData();
}
@ -81,17 +88,21 @@ public partial class CS2_SimpleAdmin
public HookResult OnClientDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
if (@event.Reason is 149 or 6)
{
info.DontBroadcast = true;
}
var player = @event.Userid;
CCSPlayerController? player = @event.Userid;
#if DEBUG
Logger.LogCritical("[OnClientDisconnect] Before");
#endif
if (player == null || !player.IsValid || player.IsHLTV)
{
return HookResult.Continue;
}
CachedPlayers.Remove(player);
BotPlayers.Remove(player);
SilentPlayers.Remove(player.Slot);
@ -107,10 +118,12 @@ public partial class CS2_SimpleAdmin
try
{
if (DisconnectedPlayers.Count >= Config.OtherSettings.DisconnectedPlayersHistoryCount)
{
DisconnectedPlayers.RemoveAt(0);
}
var steamId = new SteamID(player.SteamID);
var disconnectedPlayer = DisconnectedPlayers.FirstOrDefault(p => p.SteamId == steamId);
SteamID steamId = new SteamID(player.SteamID);
DisconnectedPlayer? disconnectedPlayer = DisconnectedPlayers.FirstOrDefault(p => p.SteamId == steamId);
if (disconnectedPlayer != null)
{
@ -123,13 +136,15 @@ public partial class CS2_SimpleAdmin
DisconnectedPlayers.Add(new DisconnectedPlayer(steamId, player.PlayerName,
player.IpAddress?.Split(":")[0], Time.ActualDateTime()));
}
PlayerPenaltyManager.RemoveAllPenalties(player.Slot);
if (player.UserId.HasValue)
PlayersInfo.TryRemove(player.SteamID, out _);
if (!PermissionManager.AdminCache.TryGetValue(steamId, out var data)
PlayerPenaltyManager.RemoveAllPenalties(player.Slot);
if (player.UserId.HasValue)
{
PlayersInfo.TryRemove(player.SteamID, out _);
}
if (!PermissionManager.AdminCache.TryGetValue(steamId, out (DateTime? ExpirationTime, List<string> Flags) data)
|| !(data.ExpirationTime <= Time.ActualDateTime()))
{
return HookResult.Continue;
@ -137,10 +152,12 @@ public partial class CS2_SimpleAdmin
AdminManager.RemovePlayerPermissions(steamId, PermissionManager.AdminCache[steamId].Flags.ToArray());
AdminManager.RemovePlayerFromGroup(steamId, true, PermissionManager.AdminCache[steamId].Flags.ToArray());
var adminData = AdminManager.GetPlayerAdminData(steamId);
AdminData? adminData = AdminManager.GetPlayerAdminData(steamId);
if (adminData == null || data.Flags.ToList().Count != 0 && adminData.Groups.ToList().Count != 0)
{
return HookResult.Continue;
}
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
@ -160,64 +177,70 @@ public partial class CS2_SimpleAdmin
Logger.LogCritical("[OnClientConnect]");
#endif
var player = Utilities.GetPlayerFromSlot(playerslot);
CCSPlayerController? player = Utilities.GetPlayerFromSlot(playerslot);
if (player == null || !player.IsValid || player.IsBot)
{
return;
}
PlayerManager.LoadPlayerData(player);
}
private void OnClientConnected(int playerslot)
{
#if DEBUG
Logger.LogCritical("[OnClientConnected]");
#endif
var player = Utilities.GetPlayerFromSlot(playerslot);
CCSPlayerController? player = Utilities.GetPlayerFromSlot(playerslot);
if (player == null || !player.IsValid || player.IsBot)
{
return;
}
if (!CachedPlayers.Contains(player))
{
CachedPlayers.Add(player);
}
PlayerManager.LoadPlayerData(player);
}
// private void OnClientConnect(int playerslot, string name, string ipaddress)
// {
// #if DEBUG
// Logger.LogCritical("[OnClientConnect]");
// #endif
// if (Config.OtherSettings.BanType == 0)
// return;
//
// if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0]))
// return;
//
// var testPlayer = Utilities.GetPlayerFromSlot(playerslot);
// if (testPlayer == null)
// return;
// Logger.LogInformation($"Gracz {testPlayer.PlayerName} ({testPlayer.SteamID.ToString()}) Czas: {DateTime.Now}");
//
// Server.NextFrame((() =>
// {
// var player = Utilities.GetPlayerFromSlot(playerslot);
// if (player == null || !player.IsValid || player.IsBot)
// return;
//
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
// }));
//
// // Server.NextFrame(() =>
// // {
// // var player = Utilities.GetPlayerFromSlot(playerslot);
// //
// // if (player == null || !player.IsValid || player.IsBot)
// // return;
// //
// // new PlayerManager().LoadPlayerData(player);
// // });
// }
// private void OnClientConnect(int playerslot, string name, string ipaddress)
// {
// #if DEBUG
// Logger.LogCritical("[OnClientConnect]");
// #endif
// if (Config.OtherSettings.BanType == 0)
// return;
//
// if (Instance.CacheManager != null && !Instance.CacheManager.IsPlayerBanned(null, ipaddress.Split(":")[0]))
// return;
//
// var testPlayer = Utilities.GetPlayerFromSlot(playerslot);
// if (testPlayer == null)
// return;
// Logger.LogInformation($"Gracz {testPlayer.PlayerName} ({testPlayer.SteamID.ToString()}) Czas: {DateTime.Now}");
//
// Server.NextFrame((() =>
// {
// var player = Utilities.GetPlayerFromSlot(playerslot);
// if (player == null || !player.IsValid || player.IsBot)
// return;
//
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
// }));
//
// // Server.NextFrame(() =>
// // {
// // var player = Utilities.GetPlayerFromSlot(playerslot);
// //
// // if (player == null || !player.IsValid || player.IsBot)
// // return;
// //
// // new PlayerManager().LoadPlayerData(player);
// // });
// }
[GameEventHandler]
public HookResult OnPlayerFullConnect(EventPlayerConnectFull @event, GameEventInfo info)
@ -226,10 +249,12 @@ public partial class CS2_SimpleAdmin
Logger.LogCritical("[OnPlayerFullConnect]");
#endif
var player = @event.Userid;
CCSPlayerController? player = @event.Userid;
if (player == null || !player.IsValid)
{
return HookResult.Continue;
}
if (player is { IsBot: true, IsHLTV: false })
{
@ -248,22 +273,26 @@ public partial class CS2_SimpleAdmin
Logger.LogCritical("[OnRoundStart]");
#endif
foreach (var player in PlayersInfo.Values)
foreach (PlayerInfo player in PlayersInfo.Values)
{
player.DiePosition = null;
}
AddTimer(0.5f, () =>
{
foreach (var list in RenamedPlayers)
foreach (KeyValuePair<ulong, string> list in RenamedPlayers)
{
var player = Utilities.GetPlayerFromSteamId(list.Key);
CCSPlayerController? player = Utilities.GetPlayerFromSteamId(list.Key);
if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.Connected)
{
continue;
}
if (player.PlayerName.Equals(list.Value))
{
continue;
}
player.Rename(list.Value);
}
@ -271,32 +300,41 @@ public partial class CS2_SimpleAdmin
return HookResult.Continue;
}
private HookResult HookUmChat(UserMessage um)
{
var author = Utilities.GetPlayerFromIndex(um.ReadInt("entityindex"));
CCSPlayerController? author = Utilities.GetPlayerFromIndex(um.ReadInt("entityindex"));
if (author == null || !author.IsValid || author.IsBot)
{
return HookResult.Continue;
}
if (!PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Gag, out DateTime? endDateTime) &&
!PlayerPenaltyManager.IsPenalized(author.Slot, PenaltyType.Silence, out endDateTime))
{
return HookResult.Continue;
if (_localizer == null || endDateTime == null)
return HookResult.Continue;
}
var message = um.ReadString("param2");
var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
if (!triggers.Any(trigger => message.StartsWith(trigger))) return HookResult.Stop;
for (var i = um.Recipients.Count - 1; i >= 0; i--)
if (_localizer == null || endDateTime == null)
{
return HookResult.Continue;
}
string message = um.ReadString("param2");
IEnumerable<string> triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
if (!triggers.Any(trigger => message.StartsWith(trigger)))
{
return HookResult.Stop;
}
for (int i = um.Recipients.Count - 1; i >= 0; i--)
{
if (um.Recipients[i] != author)
{
um.Recipients.RemoveAt(i);
}
}
return HookResult.Continue;
// author.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", author.GetLanguage()));
@ -305,12 +343,16 @@ public partial class CS2_SimpleAdmin
private HookResult ComamndListenerHandler(CCSPlayerController? player, CommandInfo info)
{
if (player == null || !player.IsValid || player.IsBot)
{
return HookResult.Continue;
}
var command = info.GetArg(0).ToLower();
string command = info.GetArg(0).ToLower();
if (Config.OtherSettings.AdditionalCommandsToLog.Contains(command))
{
Helper.LogCommand(player, info);
}
switch (command)
{
@ -318,59 +360,85 @@ public partial class CS2_SimpleAdmin
AddTimer(1.0f, () => ReloadAdmins(null));
return HookResult.Continue;
case "callvote":
{
var voteType = info.GetArg(1).ToLower();
if (voteType != "kick")
return HookResult.Continue;
{
string voteType = info.GetArg(1).ToLower();
if (voteType != "kick")
{
return HookResult.Continue;
}
CCSPlayerController? target;
if (int.TryParse(info.GetArg(2), out int userId))
{
target = Utilities.GetPlayerFromUserid(userId);
}
else
{
target = null;
}
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
{
return HookResult.Continue;
}
if (!player.CanTarget(target))
{
return HookResult.Stop;
}
var target = int.TryParse(info.GetArg(2), out var userId)
? Utilities.GetPlayerFromUserid(userId)
: null;
if (target == null || !target.IsValid || target.Connected != PlayerConnectedState.Connected)
return HookResult.Continue;
return !player.CanTarget(target) ? HookResult.Stop : HookResult.Continue;
}
}
}
if (!command.Contains("say"))
{
return HookResult.Continue;
}
if (info.GetArg(1).Length == 0)
{
return HookResult.Stop;
var triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
}
IEnumerable<string> triggers = CoreConfig.PublicChatTrigger.Concat(CoreConfig.SilentChatTrigger);
if (triggers.Any(trigger => info.GetArg(1).StartsWith(trigger)))
{
return HookResult.Continue;
}
// if (!Config.OtherSettings.UserMessageGagChatType)
// {
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime))
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out DateTime? endDateTime) ||
PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out endDateTime))
{
if (_localizer != null && endDateTime is not null)
{
if (_localizer != null && endDateTime is not null)
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
return HookResult.Stop;
player.SendLocalizedMessage(_localizer, "sa_player_penalty_chat_active", endDateTime.Value.ToString("g", player.GetLanguage()));
}
return HookResult.Stop;
}
// }
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat") && command == "say" && info.GetArg(1).StartsWith($"@"))
{
player.ExecuteClientCommandFromServer($"css_say {info.GetArg(1).Remove(0, 1)}");
return HookResult.Stop;
}
if (command != "say_team" || !info.GetArg(1).StartsWith($"@")) return HookResult.Continue;
if (command != "say_team" || !info.GetArg(1).StartsWith($"@"))
{
return HookResult.Continue;
}
StringBuilder sb = new();
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat"))
{
sb.Append(_localizer!["sa_adminchat_template_admin", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
foreach (var p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
p.PrintToChat(sb.ToString());
}
@ -379,7 +447,7 @@ public partial class CS2_SimpleAdmin
{
sb.Append(_localizer!["sa_adminchat_template_player", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
player.PrintToChat(sb.ToString());
foreach (var p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
p.PrintToChat(sb.ToString());
}
@ -409,26 +477,37 @@ public partial class CS2_SimpleAdmin
public HookResult OnCommandTeamSay(CCSPlayerController? player, CommandInfo info)
{
if (player == null || !player.IsValid || player.IsBot)
{
return HookResult.Continue;
}
if (info.GetArg(1).StartsWith($"/")
|| info.GetArg(1).StartsWith($"!"))
{
return HookResult.Continue;
}
if (info.GetArg(1).Length == 0)
{
return HookResult.Handled;
}
if (PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _) || PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _))
{
return HookResult.Stop;
}
if (!info.GetArg(1).StartsWith($"@")) return HookResult.Continue;
if (!info.GetArg(1).StartsWith($"@"))
{
return HookResult.Continue;
}
StringBuilder sb = new();
if (AdminManager.PlayerHasPermissions(new SteamID(player.SteamID), "@css/chat"))
{
sb.Append(_localizer!["sa_adminchat_template_admin", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
foreach (var p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p.IsValid && p is { IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
p.PrintToChat(sb.ToString());
}
@ -437,7 +516,7 @@ public partial class CS2_SimpleAdmin
{
sb.Append(_localizer!["sa_adminchat_template_player", player.PlayerName, info.GetArg(1).Remove(0, 1)]);
player.PrintToChat(sb.ToString());
foreach (var p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
foreach (CCSPlayerController? p in Utilities.GetPlayers().Where(p => p is { IsValid: true, IsBot: false, IsHLTV: false } && AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/chat")))
{
p.PrintToChat(sb.ToString());
}
@ -449,12 +528,16 @@ public partial class CS2_SimpleAdmin
private void OnMapStart(string mapName)
{
if (Config.OtherSettings.ReloadAdminsEveryMapChange && ServerLoaded && ServerId != null)
{
ReloadAdmins(null);
}
AddTimer(1.0f, ServerManager.CheckHibernationStatus);
if (!ServerLoaded || ServerId == null)
{
AddTimer(1.5f, OnGameServerSteamAPIActivated);
}
// AddTimer(34, () =>
// {
@ -470,15 +553,17 @@ public partial class CS2_SimpleAdmin
[GameEventHandler]
public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
CCSPlayerController? player = @event.Userid;
if (player?.UserId == null || !player.IsValid || player.IsHLTV ||
player.Connected != PlayerConnectedState.Connected || !PlayersInfo.ContainsKey(player.SteamID) ||
@event.Attacker == null)
{
return HookResult.Continue;
}
var playerPosition = player.PlayerPawn.Value?.AbsOrigin;
var playerRotation = player.PlayerPawn.Value?.AbsRotation;
CounterStrikeSharp.API.Modules.Utils.Vector? playerPosition = player.PlayerPawn.Value?.AbsOrigin;
QAngle? playerRotation = player.PlayerPawn.Value?.AbsRotation;
PlayersInfo[player.SteamID].DiePosition = new DiePosition(
new Vector3(
playerPosition?.X ?? 0,
@ -498,12 +583,17 @@ public partial class CS2_SimpleAdmin
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerTeam(EventPlayerTeam @event, GameEventInfo info)
{
var player = @event.Userid;
CCSPlayerController? player = @event.Userid;
if (player == null || !player.IsValid || player.IsBot || !SilentPlayers.Contains(player.Slot))
{
return HookResult.Continue;
}
if (@event is not { Oldteam: <= 1, Team: >= 1 })
{
return HookResult.Continue;
}
if (@event is not { Oldteam: <= 1, Team: >= 1 }) return HookResult.Continue;
SilentPlayers.Remove(player.Slot);
SimpleAdminApi?.OnAdminToggleSilentEvent(player.Slot, false);
@ -531,13 +621,22 @@ public partial class CS2_SimpleAdmin
/// </summary>
private void EnforceRename(CCSPlayerController? player)
{
if (player is null || !player.IsValid || player.IsBot) return;
if (!RenamedPlayers.TryGetValue(player.SteamID, out var name)) return;
if (player is null || !player.IsValid || player.IsBot)
{
return;
}
if (!RenamedPlayers.TryGetValue(player.SteamID, out string? name))
{
return;
}
AddTimer(0.5f, () =>
{
if (player.IsValid && !player.PlayerName.Equals(name))
{
player.Rename(name);
}
});
}
}
}

View file

@ -9,4 +9,4 @@ public static class EnumerableExtensions
.GroupBy(x => x.Index / chunkSize)
.Select(x => x.Select(v => v.Value));
}
}
}

View file

@ -1,15 +1,14 @@
using System.Drawing;
using System.Numerics;
using System.Text;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Memory;
using Microsoft.Extensions.Localization;
using System.Text;
using CounterStrikeSharp.API.Modules.UserMessages;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin;
@ -45,12 +44,19 @@ public static class PlayerExtensions
/// <returns>True if targeting is allowed, false otherwise.</returns>
public static bool CanTarget(this CCSPlayerController? controller, CCSPlayerController? target)
{
if (controller is null || target is null) return true;
if (target.IsBot) return true;
if (controller is null || target is null)
{
return true;
}
if (target.IsBot)
{
return true;
}
return AdminManager.CanPlayerTarget(controller, target) ||
AdminManager.CanPlayerTarget(new SteamID(controller.SteamID),
new SteamID(target.SteamID)) ||
new SteamID(target.SteamID)) ||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(target);
}
@ -62,9 +68,12 @@ public static class PlayerExtensions
/// <returns>True if targeting is permitted, false otherwise.</returns>
public static bool CanTarget(this CCSPlayerController? controller, SteamID steamId)
{
if (controller is null) return true;
if (controller is null)
{
return true;
}
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
return AdminManager.CanPlayerTarget(new SteamID(controller.SteamID), steamId) ||
AdminManager.GetPlayerImmunity(controller) >= AdminManager.GetPlayerImmunity(steamId);
}
@ -75,8 +84,11 @@ public static class PlayerExtensions
/// <param name="speed">The speed modifier value.</param>
public static void SetSpeed(this CCSPlayerController? controller, float speed)
{
var playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null) return;
CCSPlayerPawn? playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null)
{
return;
}
playerPawnValue.VelocityModifier = speed;
}
@ -88,8 +100,11 @@ public static class PlayerExtensions
/// <param name="gravity">The gravity scale.</param>
public static void SetGravity(this CCSPlayerController? controller, float gravity)
{
var playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null) return;
CCSPlayerPawn? playerPawnValue = controller?.PlayerPawn.Value;
if (playerPawnValue == null)
{
return;
}
playerPawnValue.ActualGravityScale = gravity;
}
@ -101,12 +116,18 @@ public static class PlayerExtensions
/// <param name="money">The amount of money to set.</param>
public static void SetMoney(this CCSPlayerController? controller, int money)
{
var moneyServices = controller?.InGameMoneyServices;
if (moneyServices == null) return;
CCSPlayerController_InGameMoneyServices? moneyServices = controller?.InGameMoneyServices;
if (moneyServices == null)
{
return;
}
moneyServices.Account = money;
if (controller != null) Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
if (controller != null)
{
Utilities.SetStateChanged(controller, "CCSPlayerController", "m_pInGameMoneyServices");
}
}
/// <summary>
@ -116,8 +137,15 @@ public static class PlayerExtensions
/// <param name="health">The health value, default is 100.</param>
public static void SetHp(this CCSPlayerController? controller, int health = 100)
{
if (controller == null) return;
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE) return;
if (controller == null)
{
return;
}
if (health <= 0 || controller.PlayerPawn.Value == null || controller.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE)
{
return;
}
controller.PlayerPawn.Value.Health = health;
@ -136,10 +164,10 @@ public static class PlayerExtensions
/// <param name="depth">The depth offset (default 10 units).</param>
public static void Bury(this CBasePlayerPawn pawn, float depth = 10f)
{
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
Vector3 newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
pawn.AbsOrigin!.Z - depth);
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
Vector3 newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
Vector3 newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
pawn.Teleport(newPos, newRotation, newVelocity);
}
@ -151,10 +179,10 @@ public static class PlayerExtensions
/// <param name="depth">The depth offset (default 15 units).</param>
public static void Unbury(this CBasePlayerPawn pawn, float depth = 15f)
{
var newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
Vector3 newPos = new Vector3(pawn.AbsOrigin!.X, pawn.AbsOrigin.Y,
pawn.AbsOrigin!.Z + depth);
var newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
var newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
Vector3 newRotation = new Vector3(pawn.AbsRotation!.X, pawn.AbsRotation.Y, pawn.AbsRotation.Z);
Vector3 newVelocity = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
pawn.Teleport(newPos, newRotation, newVelocity);
}
@ -169,7 +197,7 @@ public static class PlayerExtensions
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 11); // invalid
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
/// <summary>
/// Unfreezes the player pawn, enabling movement.
/// </summary>
@ -180,7 +208,7 @@ public static class PlayerExtensions
Schema.SetSchemaValue(pawn.Handle, "CBaseEntity", "m_nActualMoveType", 2); // walk
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_MoveType");
}
/// <summary>
/// Changes the player's color tint to specified RGBA values.
/// </summary>
@ -226,7 +254,7 @@ public static class PlayerExtensions
if (controller != null)
{
var playerName = new SchemaString<CBasePlayerController>(controller, "m_iszPlayerName");
SchemaString<CBasePlayerController> playerName = new SchemaString<CBasePlayerController>(controller, "m_iszPlayerName");
playerName.Set(newName + " ");
CS2_SimpleAdmin.Instance.AddTimer(0.25f, () =>
@ -243,7 +271,10 @@ public static class PlayerExtensions
CS2_SimpleAdmin.Instance.AddTimer(0.4f, () =>
{
if (controller != null) Utilities.SetStateChanged(controller, "CBasePlayerController", "m_iszPlayerName");
if (controller != null)
{
Utilities.SetStateChanged(controller, "CBasePlayerController", "m_iszPlayerName");
}
});
}
@ -255,7 +286,9 @@ public static class PlayerExtensions
public static void TeleportPlayer(this CCSPlayerController? controller, CCSPlayerController? target)
{
if (controller?.PlayerPawn.Value == null && target?.PlayerPawn.Value == null)
{
return;
}
if (
controller?.PlayerPawn.Value is { AbsOrigin: not null, AbsRotation: not null } &&
@ -278,17 +311,19 @@ public static class PlayerExtensions
private static void PerformSlap(CBasePlayerPawn pawn, int damage = 0)
{
if (pawn.LifeState != (int)LifeState_t.LIFE_ALIVE)
{
return;
}
var controller = pawn.Controller.Value?.As<CCSPlayerController>();
CCSPlayerController? controller = pawn.Controller.Value?.As<CCSPlayerController>();
/* Teleport in a random direction - thank you, Mani!*/
/* Thank you AM & al!*/
var random = new Random();
var vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
Random random = new Random();
Vector3 vel = new Vector3(pawn.AbsVelocity.X, pawn.AbsVelocity.Y, pawn.AbsVelocity.Z);
vel.X += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
vel.Y += (random.Next(180) + 50) * (random.Next(2) == 1 ? -1 : 1);
vel.X += (random.Next(180) + 50) * RandomSign(random);
vel.Y += (random.Next(180) + 50) * RandomSign(random);
vel.Z += random.Next(200) + 100;
pawn.AbsVelocity.X = vel.X;
@ -297,7 +332,7 @@ public static class PlayerExtensions
if (controller != null && controller.IsValid)
{
var shakeMessage = UserMessage.FromPartialName("Shake");
UserMessage shakeMessage = UserMessage.FromPartialName("Shake");
shakeMessage.SetFloat("duration", 1);
shakeMessage.SetFloat("amplitude", 10);
shakeMessage.SetFloat("frequency", 1f);
@ -307,13 +342,17 @@ public static class PlayerExtensions
}
if (damage <= 0)
{
return;
}
pawn.Health -= damage;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
if (pawn.Health <= 0)
{
pawn.CommitSuicide(true, true);
}
}
/// <summary>
@ -329,16 +368,19 @@ public static class PlayerExtensions
public static void SendLocalizedMessage(this CCSPlayerController? controller, IStringLocalizer? localizer,
string messageKey, params object[] messageArgs)
{
if (controller == null || localizer == null) return;
if (controller == null || localizer == null)
{
return;
}
using (new WithTemporaryCulture(controller.GetLanguage()))
{
StringBuilder sb = new();
sb.Append(localizer[messageKey, messageArgs]);
foreach (var part in Helper.SeparateLines(sb.ToString()))
foreach (string part in Helper.SeparateLines(sb.ToString()))
{
var lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
string lineWithPrefix = (CS2_SimpleAdmin._localizer?["sa_prefix"] ?? "") + part.Trim();
controller.PrintToChat(lineWithPrefix);
}
}
@ -357,20 +399,33 @@ public static class PlayerExtensions
public static void SendLocalizedMessageCenter(this CCSPlayerController? controller, IStringLocalizer? localizer,
string messageKey, params object[] messageArgs)
{
if (controller == null || localizer == null) return;
if (controller == null || localizer == null)
{
return;
}
using (new WithTemporaryCulture(controller.GetLanguage()))
{
StringBuilder sb = new();
sb.Append(localizer[messageKey, messageArgs]);
foreach (var part in Helper.SeparateLines(sb.ToString()))
foreach (string part in Helper.SeparateLines(sb.ToString()))
{
string _part;
_part = Helper.CenterMessage(part);
var lineWithPrefix = localizer["sa_prefix"] + _part;
string lineWithPrefix = localizer["sa_prefix"] + _part;
controller.PrintToChat(lineWithPrefix);
}
}
}
}
private static int RandomSign(Random random)
{
if (random.Next(2) == 1)
{
return -1;
}
return 1;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,10 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.ValveConstants.Protobuf;
using System.Data.Common;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using System.Text;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
@ -22,19 +20,29 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <returns>The newly created ban ID if successful, otherwise null.</returns>
public async Task<int?> BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
try
{
var sql = databaseProvider.GetAddBanQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
string? playerIp = null;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1)
{
playerIp = player.IpAddress;
}
string sql = databaseProvider.GetAddBanQuery();
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
playerIp,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
banReason = reason,
@ -46,7 +54,7 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
return banId;
}
catch(Exception ex)
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
@ -63,16 +71,19 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <returns>The ID of the newly created ban if successful, otherwise null.</returns>
public async Task<int?> AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanBySteamIdQuery();
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddBanBySteamIdQuery();
int? banId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -83,10 +94,10 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
created = now,
serverid = CS2_SimpleAdmin.ServerId
});
return banId;
}
catch(Exception ex)
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
return null;
@ -102,18 +113,24 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerIp)) return;
if (string.IsNullOrEmpty(playerIp))
{
return;
}
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddBanByIpQuery();
string sql = databaseProvider.GetAddBanByIpQuery();
await connection.ExecuteAsync(sql, new
{
playerIp,
@ -129,179 +146,179 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
catch { }
}
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
// {
// if (database == null) return false;
//
// if (player.IpAddress == null)
// {
// return false;
// }
//
// #if DEBUG
// if (CS2_SimpleAdmin._logger != null)
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
// #endif
//
// int banCount;
//
// DateTime currentTime = Time.ActualDateTime();
//
// try
// {
// string sql;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// )
// ), 0) AS TotalBanCount;
// """ : """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// )
// ), 0) AS TotalBanCount;
// """;
// }
// else
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
// """ : """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
// """;
// }
//
// await using var connection = await database.GetConnectionAsync();
//
// var parameters = new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
// string.IsNullOrEmpty(player.IpAddress) ||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
// ? null
// : player.IpAddress,
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
// CurrentTime = currentTime,
// CS2_SimpleAdmin.ServerId
// };
//
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
// player.Name, ex.Message);
// return false;
// }
//
// return banCount > 0;
// }
//
// public async Task<int> GetPlayerBans(PlayerInfo player)
// {
// if (database == null) return 0;
//
// try
// {
// string sql;
//
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
//
// int banCount;
//
// await using var connection = await database.GetConnectionAsync();
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = player.IpAddress,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
// else
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = DBNull.Value,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
//
// return banCount;
// }
// catch { }
//
// return 0;
// }
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
// {
// if (database == null) return false;
//
// if (player.IpAddress == null)
// {
// return false;
// }
//
// #if DEBUG
// if (CS2_SimpleAdmin._logger != null)
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
// #endif
//
// int banCount;
//
// DateTime currentTime = Time.ActualDateTime();
//
// try
// {
// string sql;
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// )
// ), 0) AS TotalBanCount;
// """ : """
// SELECT COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// ), 0)
// +
// COALESCE((
// SELECT COUNT(*)
// FROM sa_bans
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
// WHERE sa_bans.status = 'ACTIVE'
// AND sa_players_ips.address = @PlayerIP
// AND NOT EXISTS (
// SELECT 1
// FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime)
// AND server_id = @ServerId
// )
// ), 0) AS TotalBanCount;
// """;
// }
// else
// {
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime);
// """ : """
// UPDATE sa_bans
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
//
// SELECT COUNT(*) FROM sa_bans
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
// AND status = 'ACTIVE'
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
// """;
// }
//
// await using var connection = await database.GetConnectionAsync();
//
// var parameters = new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
// string.IsNullOrEmpty(player.IpAddress) ||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
// ? null
// : player.IpAddress,
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
// CurrentTime = currentTime,
// CS2_SimpleAdmin.ServerId
// };
//
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
// }
// catch (Exception ex)
// {
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
// player.Name, ex.Message);
// return false;
// }
//
// return banCount > 0;
// }
//
// public async Task<int> GetPlayerBans(PlayerInfo player)
// {
// if (database == null) return 0;
//
// try
// {
// string sql;
//
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
//
// int banCount;
//
// await using var connection = await database.GetConnectionAsync();
//
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = player.IpAddress,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
// else
// {
// banCount = await connection.ExecuteScalarAsync<int>(sql,
// new
// {
// PlayerSteamID = player.SteamId.SteamId64,
// PlayerIP = DBNull.Value,
// serverid = CS2_SimpleAdmin.ServerId
// });
// }
//
// return banCount;
// }
// catch { }
//
// return 0;
// }
/// <summary>
/// Unbans a player based on a pattern match of SteamID or IP address.
@ -309,46 +326,51 @@ internal class BanManager(IDatabaseProvider? databaseProvider)
/// <param name="playerPattern">Pattern to match against player identifiers (e.g., partial SteamID).</param>
/// <param name="adminSteamId">SteamID64 of the admin performing the unban.</param>
/// <param name="reason">Optional reason for the unban. If null or empty, the unban reason is not stored.</param>
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
if (databaseProvider == null) return;
if (playerPattern is not { Length: > 1 })
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
var bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
return;
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (var ban in bansList)
if (databaseProvider == null)
{
int banId = ban.id;
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
return;
}
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
if (CS2_SimpleAdmin.UnlockedCommands)
if (playerPattern is not { Length: > 1 })
{
var steamIds = bansList
return;
}
try
{
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
IEnumerable<dynamic> bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
dynamic[] bansList = bans as dynamic[] ?? bans.ToArray();
if (bansList.Length == 0)
{
return;
}
string sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
int adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
foreach (dynamic ban in bansList)
{
int banId = ban.id;
string sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
int unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
string sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
}
// Apply immediately; the periodic cache refresh would otherwise keep rejecting the player for up to a minute
CS2_SimpleAdmin.Instance.CacheManager?.SetBanStatus(bansList.Select(b => (int)b.id), Models.BanStatus.UNBANNED);
// css_ban also issues a native banid when UnlockedCommands is on; clear it so the engine stops
// rejecting the player. Done here, after the rows are updated, and for whichever pattern matched them.
if (CS2_SimpleAdmin.UnlockedCommands)
{
List<ulong> steamIds = bansList
.Select(b => (object?)b.player_steamid)
.Where(id => id != null)
.Select(id => Convert.ToUInt64(id))
@ -356,19 +378,23 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
.Distinct()
.ToList();
if (steamIds.Count > 0)
await Server.NextWorldUpdateAsync(() =>
if (steamIds.Count > 0)
{
foreach (var steamId in steamIds)
await Server.NextWorldUpdateAsync(() =>
{
foreach (ulong steamId in steamIds)
{
Server.ExecuteCommand($"removeid {new SteamID(steamId).SteamId3}");
}
});
}
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
}
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to unban player: {exception}", ex.Message);
}
}
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
// {
@ -445,17 +471,21 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
/// </summary>
public async Task ExpireOldBans()
{
if (databaseProvider == null) return;
var currentTime = Time.ActualDateTime();
if (databaseProvider == null)
{
return;
}
DateTime currentTime = Time.ActualDateTime();
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId });
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0)
{
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
DateTime ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
@ -468,4 +498,4 @@ public async Task UnbanPlayer(string playerPattern, string adminSteamId, string
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired bans");
}
}
}
}

View file

@ -1,13 +1,12 @@
using System.Collections.Concurrent;
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Models;
using Dapper;
using Microsoft.Extensions.Logging;
using ZLinq;
namespace CS2_SimpleAdmin.Managers;
internal class CacheManager: IDisposable
internal class CacheManager : IDisposable
{
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
// Rebuilt into fresh instances and swapped, so unlocked readers on the game thread never see a half-built index
@ -17,22 +16,33 @@ internal class CacheManager: IDisposable
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
private HashSet<uint> _cachedIgnoredIps = [];
private DateTime _lastUpdateTime = DateTime.MinValue;
private DateTime? _lastDatabaseTime = null; // Track actual time from database
private bool _isInitialized;
private bool _disposed;
/// <summary>
/// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings.
/// </summary>
/// <returns>Asynchronous task representing the initialization process.</returns>
public async Task InitializeCacheAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!CS2_SimpleAdmin.ServerLoaded) return;
if (_isInitialized) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
if (!CS2_SimpleAdmin.ServerLoaded)
{
return;
}
if (_isInitialized)
{
return;
}
try
{
Clear();
@ -41,9 +51,9 @@ internal class CacheManager: IDisposable
.Select(IpHelper.IpToUint)
.ToHashSet();
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
List<BanRecord> bans;
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
bans = (await connection.QueryAsync<BanRecord>(
@ -69,21 +79,21 @@ internal class CacheManager: IDisposable
status AS Status
FROM sa_bans
WHERE server_id = @serverId
""", new {serverId = CS2_SimpleAdmin.ServerId})).ToList();
""", new { serverId = CS2_SimpleAdmin.ServerId })).ToList();
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
// Optimization: Load IP history and build cache in single pass
var ipHistory = await connection.QueryAsync<IpHistoryRow>(
IEnumerable<IpHistoryRow> ipHistory = await connection.QueryAsync<IpHistoryRow>(
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY steamid, address, used_at DESC");
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var currentSteamId = 0UL;
var currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
var latestIpTimestamps = new Dictionary<uint, DateTime>();
string unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
ulong currentSteamId = 0UL;
HashSet<IpRecord> currentIpSet = new HashSet<IpRecord>(new IpRecordComparer());
Dictionary<uint, DateTime> latestIpTimestamps = new Dictionary<uint, DateTime>();
foreach (var record in ipHistory)
foreach (IpHistoryRow record in ipHistory)
{
// When we encounter a new steamid, save the previous one
if ((ulong)record.Steamid != currentSteamId && currentSteamId != 0)
@ -96,14 +106,14 @@ internal class CacheManager: IDisposable
currentSteamId = (ulong)record.Steamid;
// Only keep the latest timestamp for each IP
if (!latestIpTimestamps.TryGetValue(record.Address, out var existingTimestamp) ||
if (!latestIpTimestamps.TryGetValue(record.Address, out DateTime existingTimestamp) ||
record.Used_at > existingTimestamp)
{
latestIpTimestamps[record.Address] = record.Used_at;
currentIpSet.Add(new IpRecord(
record.Address,
record.Used_at,
string.IsNullOrEmpty(record.Name) ? unknownName : record.Name
NameOrDefault(record.Name, unknownName)
));
}
}
@ -115,11 +125,13 @@ internal class CacheManager: IDisposable
}
}
foreach (var ban in bans.AsValueEnumerable())
foreach (BanRecord? ban in bans.AsValueEnumerable())
{
_banCache.TryAdd(ban.Id, ban);
}
RebuildIndexes();
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
_isInitialized = true;
}
@ -128,7 +140,7 @@ internal class CacheManager: IDisposable
Console.WriteLine(e.ToString());
}
}
/// <summary>
/// Clears all cached data and reinitializes the cache from the database.
/// </summary>
@ -136,15 +148,15 @@ internal class CacheManager: IDisposable
public async Task ForceReInitializeCacheAsync()
{
_isInitialized = false;
_banCache.Clear();
_playerIpsCache.Clear();
_cachedIgnoredIps = [];
_lastUpdateTime = DateTime.MinValue;
await InitializeCacheAsync();
}
/// <summary>
/// Refreshes the in-memory cache with updated or new data from the database since the last update time.
/// Also updates multi-account IP history if enabled.
@ -152,16 +164,23 @@ internal class CacheManager: IDisposable
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshCacheAsync()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (!_isInitialized) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
if (!_isInitialized)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<BanRecord> updatedBans;
// Get current time from database in local timezone (CURRENT_TIMESTAMP uses session timezone, not UTC)
var currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
DateTime currentDatabaseTime = await connection.QueryFirstAsync<DateTime>("SELECT CURRENT_TIMESTAMP");
// Optimization: Only get IDs for comparison if we need to check for deletions
// Most of the time bans are just added/updated, not deleted
@ -170,10 +189,10 @@ internal class CacheManager: IDisposable
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
{
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
@ -186,10 +205,10 @@ internal class CacheManager: IDisposable
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
List<BanRecord> updatedList = new List<BanRecord>();
foreach (BanRecord ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
{
// New ban
updatedList.Add(ban);
@ -213,10 +232,10 @@ internal class CacheManager: IDisposable
else
{
// Use previous database time or start from far past if first run
var lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
DateTime lastCheckTime = _lastDatabaseTime ?? DateTime.MinValue;
// Get recently updated bans for this server by timestamp (using database time to avoid timezone issues)
var updatedBans_Query = (await connection.QueryAsync<BanRecord>(
List<BanRecord> updatedBans_Query = (await connection.QueryAsync<BanRecord>(
"""
SELECT id AS Id,
player_name AS PlayerName,
@ -229,10 +248,10 @@ internal class CacheManager: IDisposable
)).ToList();
// Detect changes: new bans or status changes
var updatedList = new List<BanRecord>();
foreach (var ban in updatedBans_Query)
List<BanRecord> updatedList = new List<BanRecord>();
foreach (BanRecord ban in updatedBans_Query)
{
if (!_banCache.TryGetValue(ban.Id, out var cachedBan))
if (!_banCache.TryGetValue(ban.Id, out BanRecord? cachedBan))
{
// New ban
updatedList.Add(ban);
@ -260,49 +279,56 @@ internal class CacheManager: IDisposable
// Optimization: Only process deletions if we have the full ID list
if (allIds != null)
{
foreach (var id in _banCache.Keys)
foreach (int id in _banCache.Keys)
{
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
if (allIds.Contains(id) || !_banCache.TryRemove(id, out BanRecord? ban))
{
continue;
}
if (ban.PlayerSteamId != null &&
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out List<BanRecord>? steamBans))
{
steamBans.RemoveAll(b => b.Id == id);
if (steamBans.Count == 0)
{
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
}
}
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipBans))
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBans))
{
continue;
}
ipBans.RemoveAll(b => b.Id == id);
if (ipBans.Count == 0)
{
_ipIndex.TryRemove(ipUInt, out _);
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
var ipHistory = (await connection.QueryAsync<IpHistoryRow>(
IEnumerable<IpHistoryRow> ipHistory = (await connection.QueryAsync<IpHistoryRow>(
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
new { lastUpdate = _lastUpdateTime }));
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
foreach (IGrouping<long, IpHistoryRow>? group in ipHistory.AsValueEnumerable().GroupBy(x => x.Steamid))
{
var ipSet = new HashSet<IpRecord>(
HashSet<IpRecord> ipSet = new HashSet<IpRecord>(
group
.GroupBy(x => x.Address)
.Select(g =>
{
var latest = g.MaxBy(x => x.Used_at)!; // group is never empty
IpHistoryRow latest = g.MaxBy(x => x.Used_at)!; // group is never empty
return new IpRecord(
g.Key,
latest.Used_at,
!string.IsNullOrEmpty(latest.Name)
? latest.Name
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
NameOrDefault(latest.Name, CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown")
);
}),
new IpRecordComparer()
@ -313,7 +339,7 @@ internal class CacheManager: IDisposable
_ => ipSet,
(_, existingSet) =>
{
foreach (var newEntry in ipSet)
foreach (IpRecord newEntry in ipSet)
{
existingSet.Remove(newEntry);
existingSet.Add(newEntry);
@ -325,10 +351,10 @@ internal class CacheManager: IDisposable
}
// Update cache with new/modified bans
var needsRebuild = false;
foreach (var ban in updatedBans)
bool needsRebuild = false;
foreach (BanRecord ban in updatedBans)
{
if (_banCache.TryGetValue(ban.Id, out var oldBan) && oldBan.Status != ban.Status)
if (_banCache.TryGetValue(ban.Id, out BanRecord? oldBan) && oldBan.Status != ban.Status)
{
// Ban status changed (e.g., ACTIVE -> EXPIRED/UNBANNED), need to rebuild indexes
needsRebuild = true;
@ -370,36 +396,43 @@ internal class CacheManager: IDisposable
/// <param name="status">New status.</param>
public void SetBanStatus(IEnumerable<int> banIds, BanStatus status)
{
var changed = false;
foreach (var id in banIds)
bool changed = false;
foreach (int id in banIds)
{
if (!_banCache.TryGetValue(id, out var ban) || ban.StatusEnum == status) continue;
if (!_banCache.TryGetValue(id, out BanRecord? ban) || ban.StatusEnum == status)
{
continue;
}
_banCache[id] = ban with { Status = status.ToString() };
changed = true;
}
if (changed) RebuildIndexes();
if (changed)
{
RebuildIndexes();
}
}
private void RebuildIndexesCore()
{
var steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
var ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
ConcurrentDictionary<ulong, List<BanRecord>> steamIdIndex = new ConcurrentDictionary<ulong, List<BanRecord>>();
ConcurrentDictionary<uint, List<BanRecord>> ipIndex = new ConcurrentDictionary<uint, List<BanRecord>>();
// Optimization: Cache config value to avoid repeated property access
var banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
var checkIpBans = banType != 0;
int banType = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType;
bool checkIpBans = banType != 0;
// Optimization: Pre-filter only ACTIVE bans to avoid checking status in loop
var activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
IEnumerable<BanRecord> activeBans = _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE);
foreach (var ban in activeBans)
foreach (BanRecord? ban in activeBans)
{
// Index by Steam ID
if (ban.PlayerSteamId.HasValue)
{
var steamId = ban.PlayerSteamId.Value;
if (!steamIdIndex.TryGetValue(steamId, out var steamList))
ulong steamId = ban.PlayerSteamId.Value;
if (!steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamList))
{
steamList = new List<BanRecord>();
steamIdIndex[steamId] = steamList;
@ -409,9 +442,9 @@ internal class CacheManager: IDisposable
// Index by IP (only if IP bans are enabled)
if (checkIpBans && !string.IsNullOrEmpty(ban.PlayerIp) &&
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
IpHelper.TryConvertIpToUint(ban.PlayerIp, out uint ipUInt))
{
if (!ipIndex.TryGetValue(ipUInt, out var ipList))
if (!ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipList))
{
ipList = new List<BanRecord>();
ipIndex[ipUInt] = ipList;
@ -424,19 +457,19 @@ internal class CacheManager: IDisposable
Volatile.Write(ref _steamIdIndex, steamIdIndex);
Volatile.Write(ref _ipIndex, ipIndex);
}
/// <summary>
/// Retrieves all ban records currently stored in the cache.
/// </summary>
/// <returns>List of all <see cref="BanRecord"/> objects.</returns>
public List<BanRecord> GetAllBans() => _banCache.Values.ToList();
/// <summary>
/// Retrieves only active ban records from the cache.
/// </summary>
/// <returns>List of active <see cref="BanRecord"/> objects.</returns>
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList();
/// <summary>
/// Retrieves all ban records for a specific player by their Steam ID.
/// </summary>
@ -446,7 +479,7 @@ internal class CacheManager: IDisposable
// ponytail: O(n) over all cached bans per call; add an all-status index if this gets hot.
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) =>
_banCache.Values.Where(b => b.PlayerSteamId == steamId).ToList();
/// <summary>
/// Gets all known Steam accounts that have used the specified IP address.
/// </summary>
@ -456,16 +489,16 @@ internal class CacheManager: IDisposable
/// </returns>
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
{
var ipAsUint = IpHelper.IpToUint(ipAddress);
var results = new List<(ulong, DateTime, string)>();
uint ipAsUint = IpHelper.IpToUint(ipAddress);
List<(ulong, DateTime, string)> results = new List<(ulong, DateTime, string)>();
// Optimization: Direct lookup using HashSet.Contains instead of TryGetValue
var searchRecord = new IpRecord(ipAsUint, default, null!);
IpRecord searchRecord = new IpRecord(ipAsUint, default, null!);
foreach (var (steamId, ipSet) in _playerIpsCache)
foreach ((ulong steamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
{
// Optimization: Single pass through the set
foreach (var entry in ipSet)
foreach (IpRecord entry in ipSet)
{
if (entry.Ip == ipAsUint)
{
@ -488,11 +521,15 @@ internal class CacheManager: IDisposable
private bool IsIpBanned(string ipAddress)
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
var ipUInt = IpHelper.IpToUint(ipAddress);
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
{
return false;
}
uint ipUInt = IpHelper.IpToUint(ipAddress);
return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
}
// public bool IsPlayerBanned(ulong? steamId, string? ipAddress)
// {
// if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value))
@ -518,13 +555,13 @@ internal class CacheManager: IDisposable
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
{
BanRecord? record;
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
{
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record != null)
{
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(record.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
if (_banCache.TryGetValue(record.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue))
@ -536,21 +573,31 @@ internal class CacheManager: IDisposable
}
}
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
{
return false;
}
if (string.IsNullOrEmpty(ipAddress) ||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt) ||
_cachedIgnoredIps.Contains(ipUInt) ||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
!_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
{
return false;
}
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (record == null) return false;
if (record == null)
{
return false;
}
// Double-check the ban is still active in cache (handle race conditions)
if (!_banCache.TryGetValue(record.Id, out var cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
if (!_banCache.TryGetValue(record.Id, out BanRecord? cachedBanIp) || cachedBanIp.StatusEnum != BanStatus.ACTIVE)
{
return false;
}
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
@ -560,7 +607,7 @@ internal class CacheManager: IDisposable
return true;
}
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
// {
// if (_steamIdIndex.ContainsKey(steamId))
@ -638,16 +685,18 @@ internal class CacheManager: IDisposable
/// <returns>True if the player or their known IPs are banned, otherwise false.</returns>
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress)
{
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
if (_steamIdIndex.TryGetValue(steamId, out List<BanRecord>? steamBans))
{
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
BanRecord? activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan != null)
{
// Double-check the ban is still active in cache (handle race conditions)
if (_banCache.TryGetValue(activeBan.Id, out var cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
if (_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) && cachedBan.StatusEnum == BanStatus.ACTIVE)
{
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
{
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
}
return true;
}
@ -655,25 +704,34 @@ internal class CacheManager: IDisposable
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 || string.IsNullOrEmpty(ipAddress))
{
return false;
}
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt))
{
return false;
}
if (_cachedIgnoredIps.Contains(ipUInt))
{
return false;
}
// Direct ip ban (ban record has player_ip set)
if (_ipIndex.TryGetValue(ipUInt, out var ipBanRecords))
if (_ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipBanRecords))
{
var ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out var cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
BanRecord? ipBan = ipBanRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
if (ipBan != null && _banCache.TryGetValue(ipBan.Id, out BanRecord? cachedIpBan) && cachedIpBan.StatusEnum == BanStatus.ACTIVE)
{
var expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
int expireOldIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans;
if (expireOldIpBans <= 0 || ipBan.Created >= Time.ActualDateTime().AddDays(-expireOldIpBans))
{
if (string.IsNullOrEmpty(ipBan.PlayerName))
{
ipBan.PlayerName = playerName;
}
ipBan.PlayerSteamId ??= steamId;
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
@ -684,19 +742,32 @@ internal class CacheManager: IDisposable
// Multiaccount ban - check if other accounts using current ip are banned
if (!_playerIpsCache.IsEmpty)
{
foreach (var (otherSteamId, ipSet) in _playerIpsCache)
foreach ((ulong otherSteamId, HashSet<IpRecord>? ipSet) in _playerIpsCache)
{
// Skip current player
if (otherSteamId == steamId)
{
continue;
}
// Check if this ip is in the other accounts ip history
if (ipSet.All(record => record.Ip != ipUInt)) continue;
if (ipSet.All(record => record.Ip != ipUInt))
{
continue;
}
// Found another account using this ip - check if its banned
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans)) continue;
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE) continue;
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
{
continue;
}
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE)
{
continue;
}
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
}
@ -705,32 +776,44 @@ internal class CacheManager: IDisposable
// Multiaccount ban - check if this player used any ip where other banned accounts are connected
// Search sa_players_ips for all accounts sharing the same ips as current player
if (!CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
{
return false;
}
if (!_playerIpsCache.TryGetValue(steamId, out var playerIps))
if (!_playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? playerIps))
{
return false;
}
// For each ip the player used (current or historical)
foreach (var playerIpRecord in playerIps)
foreach (IpRecord playerIpRecord in playerIps)
{
// Search sa_players_ips for other accounts using this same ip (as uint)
foreach (var (otherSteamId, otherIpSet) in _playerIpsCache)
foreach ((ulong otherSteamId, HashSet<IpRecord>? otherIpSet) in _playerIpsCache)
{
if (otherSteamId == steamId)
{
continue;
}
// Check if this other account used the player ip
if (otherIpSet.All(record => record.Ip != playerIpRecord.Ip))
{
continue;
}
// Check if this other account is banned
if (!_steamIdIndex.TryGetValue(otherSteamId, out var otherSteamBans))
if (!_steamIdIndex.TryGetValue(otherSteamId, out List<BanRecord>? otherSteamBans))
{
continue;
}
var activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out var cachedBan) ||
BanRecord? activeBan = otherSteamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
if (activeBan == null || !_banCache.TryGetValue(activeBan.Id, out BanRecord? cachedBan) ||
cachedBan.StatusEnum != BanStatus.ACTIVE)
{
continue;
}
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
return true;
@ -749,15 +832,19 @@ internal class CacheManager: IDisposable
public bool HasIpForPlayer(ulong steamId, string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
{
return false;
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint))
return false;
}
return _playerIpsCache.TryGetValue(steamId, out var ipData) &&
if (!IpHelper.TryConvertIpToUint(ipAddress, out uint ipUint))
{
return false;
}
return _playerIpsCache.TryGetValue(steamId, out HashSet<IpRecord>? ipData) &&
ipData.Contains(new IpRecord(ipUint, default, null!));
}
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
// {
// if (string.IsNullOrWhiteSpace(ipAddress))
@ -778,9 +865,11 @@ internal class CacheManager: IDisposable
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
{
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
var baseSql = """
string baseSql = """
UPDATE sa_bans
SET
player_ip = COALESCE(player_ip, @PlayerIP),
@ -796,44 +885,56 @@ internal class CacheManager: IDisposable
baseSql += " AND server_id = @ServerId;";
}
string? playerIp = ipAddress;
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|| string.IsNullOrEmpty(ipAddress)
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress))
{
playerIp = null;
}
var parameters = new
{
PlayerSteamID = steamId,
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|| string.IsNullOrEmpty(ipAddress)
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)
? null
: ipAddress,
PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName,
PlayerIP = playerIp,
PlayerName = playerName ?? string.Empty,
CurrentTime = Time.ActualDateTime(),
CS2_SimpleAdmin.ServerId
};
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await connection.ExecuteAsync(baseSql, parameters);
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out List<BanRecord>? steamRecords))
{
foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
foreach (BanRecord? rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
{
rec.PlayerIp = ipAddress;
}
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
{
rec.PlayerName = playerName;
}
}
}
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)
&& _ipIndex.TryGetValue(ipUInt, out var ipRecords))
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out uint ipUInt)
&& _ipIndex.TryGetValue(ipUInt, out List<BanRecord>? ipRecords))
{
foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
foreach (BanRecord? rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
{
if (!rec.PlayerSteamId.HasValue && steamId.HasValue)
{
rec.PlayerSteamId = steamId;
}
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
{
rec.PlayerName = playerName;
}
}
}
}
@ -847,16 +948,30 @@ internal class CacheManager: IDisposable
_playerIpsCache.Clear();
_cachedIgnoredIps.Clear();
}
/// <summary>
/// Clears and disposes of all cached data and marks the object as disposed.
/// </summary>
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
Clear();
_disposed = true;
}
private static string NameOrDefault(string? name, string defaultName)
{
if (string.IsNullOrEmpty(name))
{
return defaultName;
}
return name;
}
}
public class IpRecordComparer : IEqualityComparer<IpRecord>
@ -866,4 +981,4 @@ public class IpRecordComparer : IEqualityComparer<IpRecord>
public int GetHashCode(IpRecord obj)
=> obj.Ip.GetHashCode();
}
}

View file

@ -7,7 +7,7 @@ namespace CS2_SimpleAdmin.Managers;
public class DiscordManager(string webhookUrl)
{
/// <summary>
/// Sends a plain text message asynchronously to the configured Discord webhook URL.
/// </summary>
@ -16,23 +16,23 @@ public class DiscordManager(string webhookUrl)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task SendMessageAsync(string message)
{
var client = CS2_SimpleAdmin.HttpClient;
var payload = new
HttpClient client = CS2_SimpleAdmin.HttpClient;
Dictionary<string, object> payload = new Dictionary<string, object>
{
content = message
["content"] = message
};
var options = new JsonSerializerOptions
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = false
};
var json = JsonSerializer.Serialize(payload, options);
var content = new StringContent(json, Encoding.UTF8, "application/json");
string json = JsonSerializer.Serialize(payload, options);
StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
var response = await client.PostAsync(webhookUrl, content);
HttpResponseMessage response = await client.PostAsync(webhookUrl, content);
if (!response.IsSuccessStatusCode)
{
@ -54,48 +54,75 @@ public class DiscordManager(string webhookUrl)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task SendEmbedAsync(Embed embed)
{
var httpClient = CS2_SimpleAdmin.HttpClient;
HttpClient httpClient = CS2_SimpleAdmin.HttpClient;
var payload = new
// Same JSON as before: every key is written, null when the part is empty
Dictionary<string, object?> embedPayload = new Dictionary<string, object?>
{
embeds = new[]
{
new
{
color = embed.Color,
title = !string.IsNullOrEmpty(embed.Title) ? embed.Title : null,
description = !string.IsNullOrEmpty(embed.Description) ? embed.Description : null,
thumbnail = !string.IsNullOrEmpty(embed.ThumbnailUrl) ? new { url = embed.ThumbnailUrl } : null,
image = !string.IsNullOrEmpty(embed.ImageUrl) ? new { url = embed.ImageUrl } : null,
footer = !string.IsNullOrEmpty(embed.Footer?.Text) ? new { text = embed.Footer.Text, icon_url = embed.Footer.IconUrl } : null,
timestamp = embed.Timestamp,
fields = embed.Fields.Count > 0 ? embed.Fields.Select(field => new
{
name = field.Name,
value = field.Value,
inline = field.Inline
}).ToArray() : null
}
}
["color"] = embed.Color,
["title"] = null,
["description"] = null,
["thumbnail"] = null,
["image"] = null,
["footer"] = null,
["timestamp"] = embed.Timestamp,
["fields"] = null
};
var options = new JsonSerializerOptions
if (!string.IsNullOrEmpty(embed.Title))
{
embedPayload["title"] = embed.Title;
}
if (!string.IsNullOrEmpty(embed.Description))
{
embedPayload["description"] = embed.Description;
}
if (!string.IsNullOrEmpty(embed.ThumbnailUrl))
{
embedPayload["thumbnail"] = new Dictionary<string, object?> { ["url"] = embed.ThumbnailUrl };
}
if (!string.IsNullOrEmpty(embed.ImageUrl))
{
embedPayload["image"] = new Dictionary<string, object?> { ["url"] = embed.ImageUrl };
}
if (embed.Footer != null && !string.IsNullOrEmpty(embed.Footer.Text))
{
embedPayload["footer"] = new Dictionary<string, object?> { ["text"] = embed.Footer.Text, ["icon_url"] = embed.Footer.IconUrl };
}
if (embed.Fields.Count > 0)
{
embedPayload["fields"] = embed.Fields
.Select(field => new Dictionary<string, object?> { ["name"] = field.Name, ["value"] = field.Value, ["inline"] = field.Inline })
.ToArray();
}
Dictionary<string, object> payload = new Dictionary<string, object>
{
["embeds"] = new[] { embedPayload }
};
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = false
};
var jsonPayload = JsonSerializer.Serialize(payload, options);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
string jsonPayload = JsonSerializer.Serialize(payload, options);
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(webhookUrl, content);
HttpResponseMessage response = await httpClient.PostAsync(webhookUrl, content);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await response.Content.ReadAsStringAsync();
string errorMessage = await response.Content.ReadAsStringAsync();
CS2_SimpleAdmin._logger?.LogError($"Failed to send embed: {response.StatusCode} - {errorMessage}");
}
}
/// <summary>
/// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation.
/// </summary>
@ -135,13 +162,13 @@ public class Embed
/// <param name="inline">Whether the field should be displayed inline with other fields.</param>
public void AddField(string name, string value, bool inline)
{
var field = new EmbedField
EmbedField field = new EmbedField
{
Name = name,
Value = value,
Inline = inline
};
Fields.Add(field);
}
}

View file

@ -1,4 +1,5 @@
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
@ -18,12 +19,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Mute ID if successfully added, otherwise null.</returns>
public async Task<int?> MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
var muteType = type switch
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
@ -32,10 +36,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(true);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddMuteQuery(true);
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
@ -69,12 +73,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Mute ID if successful, otherwise null.</returns>
public async Task<int?> AddMuteBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
var muteType = type switch
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
@ -83,10 +90,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddMuteQuery(false);
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddMuteQuery(false);
int? muteId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -114,7 +121,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>List of active mute records; empty list if none or on error.</returns>
public async Task<List<dynamic>> IsPlayerMuted(string steamId)
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
if (string.IsNullOrEmpty(steamId))
{
@ -123,18 +133,20 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
#if DEBUG
if (CS2_SimpleAdmin._logger != null)
{
CS2_SimpleAdmin._logger.LogCritical($"IsPlayerMuted for {steamId}");
}
#endif
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var currentTime = Time.ActualDateTime();
var sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
DateTime currentTime = Time.ActualDateTime();
string sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
var parameters = new { PlayerSteamID = steamId, CurrentTime = currentTime, serverid = CS2_SimpleAdmin.ServerId };
var activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
List<dynamic> activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
return activeMutes;
}
catch (Exception)
@ -153,14 +165,17 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// </returns>
public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo)
{
if (databaseProvider == null) return (0,0,0);
if (databaseProvider == null)
{
return (0, 0, 0);
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
string sql = databaseProvider.GetMuteStatsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
(int TotalMutes, int TotalGags, int TotalSilences) result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
{
PlayerSteamID = playerInfo.SteamId.SteamId64,
CS2_SimpleAdmin.ServerId
@ -173,7 +188,7 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
return (0, 0, 0);
}
}
/// <summary>
/// Processes a batch of online players to update their mute status and remove expired penalties.
/// </summary>
@ -181,21 +196,24 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous operation.</returns>
public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
const int batchSize = 20;
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
for (var i = 0; i < players.Count; i += batchSize)
for (int i = 0; i < players.Count; i += batchSize)
{
var batch = players.Skip(i).Take(batchSize);
var parametersList = new List<object>();
IEnumerable<(ulong SteamID, int? UserId, int Slot)> batch = players.Skip(i).Take(batchSize);
List<object> parametersList = new List<object>();
foreach (var (steamId, _, _) in batch)
foreach ((ulong steamId, int? _, int _) in batch)
{
parametersList.Add(new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
}
@ -205,11 +223,11 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
foreach (var (steamId, _, slot) in players)
foreach ((ulong steamId, int? _, int slot) in players)
{
var muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
IEnumerable<dynamic> muteRecords = await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
foreach (var muteRecord in muteRecords)
foreach (dynamic muteRecord in muteRecords)
{
DateTime endDateTime = muteRecord.ends;
PlayerPenaltyManager.RemovePenaltiesByDateTime(slot, endDateTime);
@ -230,7 +248,10 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous operation.</returns>
public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (playerPattern.Length <= 1)
{
@ -239,36 +260,38 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var muteType = type switch
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string muteType = type switch
{
1 => "MUTE",
2 => "SILENCE",
_ => "GAG"
};
var sqlRetrieveMutes =
string sqlRetrieveMutes =
databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
IEnumerable<dynamic> mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
var mutesList = mutes as dynamic[] ?? mutes.ToArray();
dynamic[] mutesList = mutes as dynamic[] ?? mutes.ToArray();
if (mutesList.Length == 0)
{
return;
}
var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
string sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
string sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
var adminId = sqlAdminId ?? 0;
int? sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
int adminId = sqlAdminId ?? 0;
foreach (var mute in mutesList)
foreach (dynamic mute in mutesList)
{
int muteId = mute.id;
int? unmuteId =
await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
string sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId });
}
}
@ -284,12 +307,15 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
/// <returns>Task representing the asynchronous expiration operation.</returns>
public async Task ExpireOldMutes()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception)
@ -297,4 +323,4 @@ internal class MuteManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical("Unable to remove expired mutes");
}
}
}
}

View file

@ -1,12 +1,13 @@
using System.Collections.Concurrent;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CS2_SimpleAdmin.Database;
using Dapper;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text.Json;
using CounterStrikeSharp.API.Modules.Admin;
using System.Diagnostics.CodeAnalysis;
using CS2_SimpleAdmin.Database;
namespace CS2_SimpleAdmin.Managers;
@ -18,58 +19,60 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// Retrieves all players' flags and associated data asynchronously.
/// </summary>
/// <returns>A list of tuples containing player SteamID, name, flags, immunity, and expiration time.</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)
return new List<(ulong, string, List<string>, int, DateTime?)>();
if (databaseProvider == null)
{
return new List<(ulong, string, List<string>, int, DateTime?)>();
}
var now = Time.ActualDateTime();
DateTime now = Time.ActualDateTime();
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAdminsQuery();
var admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
try
{
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAdminsQuery();
List<dynamic> admins = (await connection.QueryAsync(sql, new { CurrentTime = now, serverid = CS2_SimpleAdmin.ServerId })).ToList();
var groupedPlayers = admins
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
.Select(g =>
{
ulong steamId = g.Key.playerSteamId switch
{
long l => (ulong)l,
int i => (ulong)i,
string s when ulong.TryParse(s, out var parsed) => parsed,
_ => 0UL
};
List<(ulong steamId, string playerName, List<string> flags, int immunity, DateTime? ends)> groupedPlayers = admins
.GroupBy(r => new { playerSteamId = r.player_steamid, playerName = r.player_name, r.immunity, r.ends })
.Select(g =>
{
ulong steamId = g.Key.playerSteamId switch
{
long l => (ulong)l,
int i => (ulong)i,
string s when ulong.TryParse(s, out ulong parsed) => parsed,
_ => 0UL
};
int immunity = g.Key.immunity switch
{
int i => i,
string s when int.TryParse(s, out var parsed) => parsed,
_ => 0
};
int immunity = g.Key.immunity switch
{
int i => i,
string s when int.TryParse(s, out int parsed) => parsed,
_ => 0
};
DateTime? ends = g.Key.ends as DateTime?;
DateTime? ends = g.Key.ends as DateTime?;
string playerName = g.Key.playerName as string ?? string.Empty;
string playerName = g.Key.playerName as string ?? string.Empty;
// Dapper returns string here, not dynamic
var flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct()
.ToList();
// Dapper returns string here, not dynamic
List<string> flags = g.Select(r => r.flag as string ?? string.Empty)
.Distinct()
.ToList();
return (steamId, playerName, flags, immunity, ends);
})
.ToList();
return (steamId, playerName, flags, immunity, ends);
})
.ToList();
return groupedPlayers;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
}
return groupedPlayers;
}
catch (Exception ex)
{
CS2_SimpleAdmin._logger?.LogError("Unable to load admins from database! {exception}", ex.Message);
throw; // an empty list would strip every admin; abort the reload and keep the current permissions
}
}
/// <summary>
@ -78,24 +81,27 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <returns>A dictionary with group names as keys and tuples of flags and immunity as values.</returns>
private async Task<Dictionary<string, (List<string>, int)>> GetAllGroupsData()
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetGroupsQuery();
var groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetGroupsQuery();
List<dynamic> groupData = connection.Query(sql, new { serverid = CS2_SimpleAdmin.ServerId }).ToList();
if (groupData.Count == 0)
{
return [];
}
var groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
foreach (var row in groupData)
Dictionary<string, (List<string>, int)> groupInfoDictionary = new Dictionary<string, (List<string>, int)>();
foreach (dynamic row in groupData)
{
var groupName = (string)row.group_name;
var flag = (string)row.flag;
var immunity = (int)row.immunity;
string groupName = (string)row.group_name;
string flag = (string)row.flag;
int immunity = (int)row.immunity;
if (!groupInfoDictionary.TryGetValue(groupName, out (List<string>, int) value))
{
@ -121,12 +127,12 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public async Task CreateGroupsJsonFile()
{
var groupsData = await GetAllGroupsData();
var jsonData = new Dictionary<string, object>();
Dictionary<string, (List<string>, int)> groupsData = await GetAllGroupsData();
Dictionary<string, object> jsonData = new Dictionary<string, object>();
foreach (var kvp in groupsData)
foreach (KeyValuePair<string, (List<string>, int)> kvp in groupsData)
{
var groupData = new Dictionary<string, object>
Dictionary<string, object> groupData = new Dictionary<string, object>
{
["flags"] = kvp.Value.Item1,
["immunity"] = kvp.Value.Item2
@ -135,14 +141,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
jsonData[kvp.Key] = groupData;
}
var options = new JsonSerializerOptions
JsonSerializerOptions options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(jsonData, options);
var filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
string json = JsonSerializer.Serialize(jsonData, options);
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "groups.json");
await File.WriteAllTextAsync(filePath, json);
}
@ -153,55 +159,55 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
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 validPlayers = allPlayers
List<(ulong identity, string name, List<string> flags, int immunity, DateTime? ends)> validPlayers = allPlayers
.Where(player => SteamID.TryParse(player.identity.ToString(), out _))
.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<string>(),
groups = new List<string>()
},
(acc, player) =>
{
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{
acc = acc with { identity = player.identity.ToString() };
}
Dictionary<string, object> 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<string>(),
groups = new List<string>()
},
(acc, player) =>
{
if (string.IsNullOrEmpty(acc.identity) && !string.IsNullOrEmpty(player.identity.ToString()))
{
acc = acc with { identity = player.identity.ToString() };
}
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
acc = acc with { immunity = Math.Max(acc.immunity, player.immunity) };
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()
};
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;
});
return acc;
});
return consolidatedData;
});
return consolidatedData;
});
var newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
List<(SteamID, DateTime? ends, List<string> flags)> newCache = validPlayers.Select(player => (new SteamID(player.identity), player.ends, player.flags)).ToList();
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
JsonSerializerOptions 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");
string json = JsonSerializer.Serialize(jsonData, options);
string filePath = Path.Combine(CS2_SimpleAdmin.Instance.ModuleDirectory, "data", "admins.json");
await File.WriteAllTextAsync(filePath, json);
return newCache;
@ -214,25 +220,34 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="admins">Admins loaded from the database.</param>
public static void ApplyAdminCache(List<(SteamID steamId, DateTime? ends, List<string> flags)> admins)
{
foreach (var steamId in AdminCache.Keys.ToList())
foreach (SteamID? steamId in AdminCache.Keys.ToList())
{
if (!AdminCache.TryRemove(steamId, out var cached)) continue;
if (!AdminCache.TryRemove(steamId, out (DateTime? ExpirationTime, List<string> Flags) cached))
{
continue;
}
var data = AdminManager.GetPlayerAdminData(steamId);
if (data == null) continue;
AdminData? data = AdminManager.GetPlayerAdminData(steamId);
if (data == null)
{
continue;
}
var flagsArray = cached.Flags.ToArray();
string[] flagsArray = cached.Flags.ToArray();
AdminManager.RemovePlayerPermissions(steamId, flagsArray);
AdminManager.RemovePlayerFromGroup(steamId, true, flagsArray);
data = AdminManager.GetPlayerAdminData(steamId);
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0)) continue;
if (data == null || (data.Flags.Count != 0 && data.Groups.Count != 0))
{
continue;
}
AdminManager.ClearPlayerPermissions(steamId);
AdminManager.RemovePlayerAdminData(steamId);
}
foreach (var (steamId, ends, flags) in admins)
foreach ((SteamID? steamId, DateTime? ends, List<string>? flags) in admins)
{
AdminCache.TryAdd(steamId, (ends, flags));
}
@ -245,13 +260,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalDelete">Whether to delete the admin globally or only for the current server.</param>
public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false)
{
if (databaseProvider == null) return;
if (string.IsNullOrEmpty(playerSteamId)) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerSteamId))
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetDeleteAdminQuery(globalDelete);
await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -271,31 +293,52 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalAdmin">Whether the admin is global or server-specific.</param>
public async Task AddAdminBySteamId(string playerSteamId, string playerName, List<string> flagsList, int immunity = 0, int time = 0, bool globalAdmin = false)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0) return;
if (string.IsNullOrEmpty(playerSteamId) || flagsList.Count == 0)
{
return;
}
var now = Time.ActualDateTime();
DateTime? futureTime = time != 0 ? now.AddMinutes(time) : null;
DateTime now = Time.ActualDateTime();
DateTime? futureTime;
if (time != 0)
{
futureTime = now.AddMinutes(time);
}
else
{
futureTime = null;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var insertAdminSql = databaseProvider.GetAddAdminQuery();
var adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
int? serverId = CS2_SimpleAdmin.ServerId;
if (globalAdmin)
{
serverId = null;
}
string insertAdminSql = databaseProvider.GetAddAdminQuery();
int adminId = await connection.ExecuteScalarAsync<int>(insertAdminSql, new
{
playerSteamId,
playerName,
immunity,
ends = futureTime,
created = now,
serverid = globalAdmin ? null : CS2_SimpleAdmin.ServerId
serverid = serverId
});
foreach (var flag in flagsList)
foreach (string flag in flagsList)
{
var insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
string insertFlagsSql = databaseProvider.GetAddAdminFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
adminId,
@ -323,24 +366,30 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="globalGroup">Whether the group is global or server-specific.</param>
public async Task AddGroup(string groupName, List<string> flagsList, int immunity = 0, bool globalGroup = false)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0) return;
if (string.IsNullOrEmpty(groupName) || flagsList.Count == 0)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var insertGroup = databaseProvider.GetAddGroupQuery();
var groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
string insertGroup = databaseProvider.GetAddGroupQuery();
int groupId = await connection.ExecuteScalarAsync<int>(insertGroup, new
{
groupName,
immunity
});
foreach (var flag in flagsList)
foreach (string flag in flagsList)
{
var insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
string insertFlagsSql = databaseProvider.GetAddGroupFlagsQuery();
await connection.ExecuteAsync(insertFlagsSql, new
{
groupId,
@ -348,8 +397,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
});
}
var insertGroupServer = databaseProvider.GetAddGroupServerQuery();
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = globalGroup ? null : CS2_SimpleAdmin.ServerId });
string insertGroupServer = databaseProvider.GetAddGroupServerQuery();
int? groupServerId = CS2_SimpleAdmin.ServerId;
if (globalGroup)
{
groupServerId = null;
}
await connection.ExecuteAsync(insertGroupServer, new { groupId, server_id = groupServerId });
await Server.NextWorldUpdateAsync(() =>
{
CS2_SimpleAdmin.Instance.ReloadAdmins(null);
@ -367,14 +422,20 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// <param name="groupName">Name of the group to delete.</param>
public async Task DeleteGroup(string groupName)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
if (string.IsNullOrEmpty(groupName)) return;
if (string.IsNullOrEmpty(groupName))
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteGroupQuery();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetDeleteGroupQuery();
await connection.ExecuteAsync(sql, new { groupName });
}
catch (Exception ex)
@ -388,13 +449,16 @@ public class PermissionManager(IDatabaseProvider? databaseProvider)
/// </summary>
public async Task DeleteOldAdmins()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetDeleteOldAdminsQuery();
string sql = databaseProvider.GetDeleteOldAdminsQuery();
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime() });
}
catch (Exception)

View file

@ -1,3 +1,4 @@
using System.Data.Common;
using System.Globalization;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
@ -37,20 +38,30 @@ internal class PlayerManager
return;
}
var userId = player.UserId.Value;
var slot = player.Slot;
var steamId = player.SteamID;
var playerName = !string.IsNullOrEmpty(player.PlayerName)
? player.PlayerName
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var ipAddress = player.IpAddress?.Split(":")[0];
int userId = player.UserId.Value;
int slot = player.Slot;
ulong steamId = player.SteamID;
string playerName;
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out var renamedTo))
if (!string.IsNullOrEmpty(player.PlayerName))
{
playerName = player.PlayerName;
}
else
{
playerName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
}
string? ipAddress = player.IpAddress?.Split(":")[0];
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(steamId, out string? renamedTo))
{
player.Rename(renamedTo);
}
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null)
{
return;
}
Task.Run(async () =>
{
@ -62,14 +73,8 @@ internal class PlayerManager
await SavePlayerIpAddress(steamId, playerName, ipAddress);
// Always check bans first, regardless of PlayersInfo state
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
{
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
ipAddress)
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
};
bool isBanned = IsBanned(CS2_SimpleAdmin.Instance.CacheManager, CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType,
CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp, playerName, steamId, ipAddress);
CS2_SimpleAdmin._logger?.LogInformation($"[BAN CHECK] Player {playerName} ({steamId}) IP: {ipAddress} - BanType: {CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType} - CheckMultiAccounts: {CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp} - isBanned: {isBanned}");
@ -87,7 +92,7 @@ internal class PlayerManager
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
{
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
PlayerInfo playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
@ -109,8 +114,8 @@ internal class PlayerManager
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip
{
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
var (totalMutes, totalGags, totalSilences) =
List<dynamic> warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
(int totalMutes, int totalGags, int totalSilences) =
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
@ -120,13 +125,13 @@ internal class PlayerManager
value.TotalSilences = totalSilences;
value.TotalWarns = warns.Count;
var activeMutes =
List<dynamic> activeMutes =
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
.ToString());
if (activeMutes.Count > 0)
{
foreach (var mute in activeMutes)
foreach (dynamic mute in activeMutes)
{
string muteType = mute.type;
DateTime ends = mute.ends;
@ -171,7 +176,7 @@ internal class PlayerManager
{
await Server.NextWorldUpdateAsync(() =>
{
foreach (var admin in Helper.GetValidPlayers()
foreach (CCSPlayerController? admin in Helper.GetValidPlayers()
.Where(p => (AdminManager.PlayerHasPermissions(
new SteamID(p.SteamID),
"@css/kick") ||
@ -182,7 +187,11 @@ internal class PlayerManager
!CS2_SimpleAdmin.AdminDisabledJoinComms
.Contains(p.SteamID)))
{
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
if (CS2_SimpleAdmin._localizer == null || admin == player)
{
continue;
}
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_penalty_info",
player.PlayerName,
@ -195,10 +204,10 @@ internal class PlayerManager
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
{
var associatedAcccountsChunks =
List<IEnumerable<(ulong SteamId, string PlayerName)>> associatedAcccountsChunks =
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
.ToList();
foreach (var chunk in associatedAcccountsChunks)
foreach (IEnumerable<(ulong SteamId, string PlayerName)> chunk in associatedAcccountsChunks)
{
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
"sa_admin_associated_accounts",
@ -235,17 +244,22 @@ internal class PlayerManager
/// <param name="type">Optional filter: bans, gags, mutes, silences or warns.</param>
public async Task<List<dynamic>> GetPenaltyHistory(ulong steamId, string? type = null)
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return [];
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return [];
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
var rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
string sql = CS2_SimpleAdmin.DatabaseProvider.GetPenaltyHistoryQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
List<dynamic> rows = (await connection.QueryAsync(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId })).ToList();
// Filter word is the plural (bans, gags, mutes, silences, warns); rows carry BAN/GAG/MUTE/SILENCE/WARN
if (type != null)
{
rows.RemoveAll(r => !type.Equals((string)r.type + "s", StringComparison.OrdinalIgnoreCase));
}
return rows;
}
@ -263,7 +277,7 @@ internal class PlayerManager
value switch
{
DateTime d => d,
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) => parsed,
string s when DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed) => parsed,
_ => null
};
@ -272,17 +286,22 @@ internal class PlayerManager
/// </summary>
public async Task LoadRenamedPlayers()
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
IEnumerable<(long steamId, string name)> rows = await connection.QueryAsync<(long steamId, string name)>(CS2_SimpleAdmin.DatabaseProvider.GetRenamesQuery());
await Server.NextWorldUpdateAsync(() =>
{
foreach (var (steamId, name) in rows)
foreach ((long steamId, string? name) in rows)
{
CS2_SimpleAdmin.RenamedPlayers[(ulong)steamId] = name;
}
});
}
catch (Exception ex)
@ -298,15 +317,22 @@ internal class PlayerManager
/// <param name="name">Forced name, or null/empty to remove the rename.</param>
public async Task SaveRenamedPlayer(ulong steamId, string? name)
{
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
if (string.IsNullOrEmpty(name))
{
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetDeleteRenameQuery(), new { steamId });
}
else
{
await connection.ExecuteAsync(CS2_SimpleAdmin.DatabaseProvider.GetUpsertRenameQuery(), new { steamId, name });
}
}
catch (Exception ex)
{
@ -321,16 +347,18 @@ internal class PlayerManager
private async Task SavePlayerIpAddress(ulong steamId, string playerName, string? ipAddress)
{
if (!_config.OtherSettings.CheckMultiAccountsByIp || ipAddress == null || CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
var steamId64 = steamId;
var ipUint = IpHelper.IpToUint(ipAddress);
ulong steamId64 = steamId;
uint ipUint = IpHelper.IpToUint(ipAddress);
var upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
string upsertQuery = CS2_SimpleAdmin.DatabaseProvider.GetUpsertPlayerIpQuery();
await connection.ExecuteAsync(upsertQuery, new
{
@ -365,40 +393,46 @@ internal class PlayerManager
{
CS2_SimpleAdmin.Instance.AddTimer(5f, () =>
{
foreach (var (steamid, name) in CS2_SimpleAdmin.RenamedPlayers)
foreach ((ulong steamid, string? name) in CS2_SimpleAdmin.RenamedPlayers)
{
var player = Helper.GetPlayerFromSteamid64(steamid);
if (player == null || !player.IsValid || player.PlayerName == name) continue;
CCSPlayerController? player = Helper.GetPlayerFromSteamid64(steamid);
if (player == null || !player.IsValid || player.PlayerName == name)
{
continue;
}
player.Rename(name);
}
}, TimerFlags.REPEAT);
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
{
#if DEBUG
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
#endif
if (CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
// Optimization: Get players once and avoid allocating anonymous types
var validPlayers = Helper.GetValidPlayers();
List<CCSPlayerController> validPlayers = Helper.GetValidPlayers();
// Use ValueTuple instead of anonymous type - better performance and less allocations
var tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (var p in validPlayers)
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> tempPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>(validPlayers.Count);
foreach (CCSPlayerController p in validPlayers)
{
tempPlayers.Add((p.PlayerName, p.SteamID, p.IpAddress, p.UserId, p.Slot));
}
var pluginInstance = CS2_SimpleAdmin.Instance;
var config = _config.OtherSettings; // Cache config access
CS2_SimpleAdmin pluginInstance = CS2_SimpleAdmin.Instance;
OtherSettings config = _config.OtherSettings; // Cache config access
_ = Task.Run(async () =>
{
try
{
// Run all expire tasks in parallel
var expireTasks = new[]
Task[] expireTasks = new[]
{
pluginInstance.BanManager.ExpireOldBans(),
pluginInstance.MuteManager.ExpireOldMutes(),
@ -415,7 +449,7 @@ internal class PlayerManager
if (ex is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
foreach (Exception inner in aggregate.InnerExceptions)
{
CS2_SimpleAdmin._logger?.LogError($"Inner exception: {inner.Message}");
}
@ -423,28 +457,24 @@ internal class PlayerManager
}
if (pluginInstance.CacheManager == null)
{
return;
}
// Optimization: Cache ban type and multi-account check to avoid repeated config access
var banType = config.BanType;
var checkMultiAccounts = config.CheckMultiAccountsByIp;
int banType = config.BanType;
bool checkMultiAccounts = config.CheckMultiAccountsByIp;
var bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)> bannedPlayers = new List<(string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot)>();
// Manual loop instead of LINQ - better performance
foreach (var player in tempPlayers)
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
{
var playerName = player.PlayerName;
var steamId = player.SteamID;
var ip = player.IpAddress?.Split(':')[0];
string playerName = player.PlayerName;
ulong steamId = player.SteamID;
string? ip = player.IpAddress?.Split(':')[0];
bool isBanned = banType switch
{
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
_ => checkMultiAccounts
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
};
bool isBanned = IsBanned(pluginInstance.CacheManager, banType, checkMultiAccounts, playerName, steamId, ip);
if (isBanned)
{
@ -454,23 +484,29 @@ internal class PlayerManager
if (bannedPlayers.Count > 0)
{
foreach (var player in bannedPlayers)
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in bannedPlayers)
{
if (!player.UserId.HasValue) continue;
if (!player.UserId.HasValue)
{
continue;
}
await Server.NextWorldUpdateAsync(() =>
{
if (Helper.GetPlayerFromSteamid64(player.SteamID) != null)
{
Helper.KickPlayer((int)player.UserId,
NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
}
});
}
}
if (config.TimeMode == 0)
{
// Optimization: Manual projection instead of LINQ
var onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
foreach (var player in tempPlayers)
List<(ulong, int?, int)> onlinePlayers = new List<(ulong, int?, int)>(tempPlayers.Count);
foreach ((string PlayerName, ulong SteamID, string? IpAddress, int? UserId, int Slot) player in tempPlayers)
{
onlinePlayers.Add((player.SteamID, player.UserId, player.Slot));
}
@ -485,14 +521,16 @@ internal class PlayerManager
try
{
// Optimization: Process penalties without LINQ allocations
var players = Helper.GetValidPlayers();
foreach (var player in players)
List<CCSPlayerController> players = Helper.GetValidPlayers();
foreach (CCSPlayerController player in players)
{
if (!PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
{
continue;
}
var isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
var isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
bool isMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _);
bool isSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _);
// Only reset voice flags if not muted or silenced
if (!isMuted && !isSilenced)
@ -509,4 +547,22 @@ internal class PlayerManager
}
}, TimerFlags.REPEAT);
}
}
/// <summary>
/// BanType 0 checks the SteamID only; otherwise the IP too, and every IP the player used when multi-account checks are on.
/// </summary>
private static bool IsBanned(CacheManager cacheManager, int banType, bool checkMultiAccounts, string playerName, ulong steamId, string? ipAddress)
{
if (banType == 0)
{
return cacheManager.IsPlayerBanned(playerName, steamId, null);
}
if (checkMultiAccounts)
{
return cacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ipAddress);
}
return cacheManager.IsPlayerBanned(playerName, steamId, ipAddress);
}
}

View file

@ -1,5 +1,5 @@
using CS2_SimpleAdminApi;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Managers;
@ -20,7 +20,7 @@ public static class PlayerPenaltyManager
Penalties.AddOrUpdate(slot,
(_) =>
{
var dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
Dictionary<PenaltyType, List<(DateTime, int, bool)>> dict = new Dictionary<PenaltyType, List<(DateTime, int, bool)>>
{
[penaltyType] = [(endDateTime, durationInMinutes, false)]
};
@ -28,7 +28,7 @@ public static class PlayerPenaltyManager
},
(_, existingDict) =>
{
if (!existingDict.TryGetValue(penaltyType, out var value))
if (!existingDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? value))
{
value = new List<(DateTime, int, bool)>();
existingDict[penaltyType] = value;
@ -50,21 +50,27 @@ public static class PlayerPenaltyManager
{
endDateTime = null;
if (!Penalties.TryGetValue(slot, out var penaltyDict) ||
!penaltyDict.TryGetValue(penaltyType, out var penaltiesList)) return false;
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) ||
!penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
return false;
}
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
{
if (penaltiesList.Count == 0) return false;
if (penaltiesList.Count == 0)
{
return false;
}
endDateTime = penaltiesList.First().EndDateTime;
return true;
}
var now = Time.ActualDateTime();
DateTime now = Time.ActualDateTime();
// Check if any active penalties exist
foreach (var penalty in penaltiesList.ToList())
foreach ((DateTime EndDateTime, int Duration, bool Passed) penalty in penaltiesList.ToList())
{
// Check if the penalty is still active
if (penalty.Duration > 0 && now >= penalty.EndDateTime)
@ -95,14 +101,14 @@ public static class PlayerPenaltyManager
/// <returns>A list of penalties if found, otherwise an empty list.</returns>
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType)
{
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
penaltyDict.TryGetValue(penaltyType, out var penaltiesList))
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
penaltyDict.TryGetValue(penaltyType, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
return penaltiesList;
}
return [];
}
/// <summary>
/// Retrieves all penalties for a player across multiple penalty types.
/// </summary>
@ -113,11 +119,11 @@ public static class PlayerPenaltyManager
{
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
if (Penalties.TryGetValue(slot, out var penaltyDict))
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
foreach (var type in penaltyType)
foreach (PenaltyType type in penaltyType)
{
if (penaltyDict.TryGetValue(type, out var penaltiesList))
if (penaltyDict.TryGetValue(type, out List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList))
{
result.AddRange(penaltiesList);
}
@ -126,7 +132,7 @@ public static class PlayerPenaltyManager
return result;
}
/// <summary>
/// Retrieves all penalties for a player across all penalty types.
/// </summary>
@ -134,12 +140,12 @@ public static class PlayerPenaltyManager
/// <returns>A dictionary with penalty types as keys and lists of penalties as values.</returns>
public static Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetAllPlayerPenalties(int slot)
{
// Check if the player has any penalties in the dictionary
return Penalties.TryGetValue(slot, out var penaltyDict) ?
// Return all penalty types and their respective penalties for the player
penaltyDict :
// If the player has no penalties, return an empty dictionary
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
return penaltyDict;
}
return new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
}
/// <summary>
@ -179,7 +185,7 @@ public static class PlayerPenaltyManager
/// <param name="penaltyType">The penalty type to remove.</param>
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
{
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
if (Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) &&
penaltyDict.ContainsKey(penaltyType))
{
penaltyDict.Remove(penaltyType);
@ -193,15 +199,21 @@ public static class PlayerPenaltyManager
/// <param name="dateTime">The end datetime of penalties to mark as passed.</param>
public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime)
{
if (!Penalties.TryGetValue(slot, out var penaltyDict)) return;
foreach (var penaltiesList in penaltyDict.Values)
if (!Penalties.TryGetValue(slot, out Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict))
{
for (var i = 0; i < penaltiesList.Count; i++)
return;
}
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)> penaltiesList in penaltyDict.Values)
{
for (int i = 0; i < penaltiesList.Count; i++)
{
if (penaltiesList[i].EndDateTime != dateTime) continue;
if (penaltiesList[i].EndDateTime != dateTime)
{
continue;
}
// Create a copy of the penalty
var penalty = penaltiesList[i];
(DateTime EndDateTime, int Duration, bool Passed) penalty = penaltiesList[i];
// Update the end datetime of the copied penalty to the current datetime
penalty.Passed = true;
@ -223,10 +235,10 @@ public static class PlayerPenaltyManager
{
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
{
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
{
// Remove expired penalties for the player
foreach (var penaltiesList in penaltyDict.Values)
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
{
penaltiesList.RemoveAll(p => p is { Duration: > 0, Passed: true });
}
@ -241,10 +253,10 @@ public static class PlayerPenaltyManager
return;
}
var now = Time.ActualDateTime();
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
DateTime now = Time.ActualDateTime();
foreach ((int playerSlot, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>? penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
{
foreach (var penaltiesList in penaltyDict.Values)
foreach (List<(DateTime EndDateTime, int Duration, bool Passed)>? penaltiesList in penaltyDict.Values)
{
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
}
@ -255,4 +267,4 @@ public static class PlayerPenaltyManager
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using CounterStrikeSharp.API;
using System.Data.Common;
using CounterStrikeSharp.API.Modules.Cvars;
using Dapper;
using Microsoft.Extensions.Logging;
@ -15,13 +15,15 @@ public class ServerManager
/// </summary>
public static void CheckHibernationStatus()
{
var convar = ConVar.Find("sv_hibernate_when_empty");
ConVar? convar = ConVar.Find("sv_hibernate_when_empty");
if (convar == null || !convar.GetPrimitiveValue<bool>())
{
return;
}
CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly");
}
/// <summary>
/// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password.
/// Handles retry attempts if IP address is not immediately available.
@ -33,11 +35,14 @@ public class ServerManager
{
CS2_SimpleAdmin.Instance.AddTimer(1.0f, () =>
{
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null)
{
return;
}
// Optimization: Get server IP once and reuse
var serverIp = Helper.GetServerIp();
var isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
string serverIp = Helper.GetServerIp();
bool isInvalidIp = string.IsNullOrEmpty(serverIp) || serverIp.StartsWith("0.0.0");
// Check if we've exceeded retry limit with invalid IP
if (_getIpTryCount > 32 && isInvalidIp)
@ -49,8 +54,8 @@ public class ServerManager
}
// Optimization: Cache ConVar lookups
var ipConVar = ConVar.Find("ip");
var ipAddress = ipConVar?.StringValue;
ConVar? ipConVar = ConVar.Find("ip");
string? ipAddress = ipConVar?.StringValue;
// Use Helper IP if ConVar IP is invalid
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
@ -67,20 +72,20 @@ public class ServerManager
}
// Optimization: Cache remaining ConVar lookups
var hostportConVar = ConVar.Find("hostport");
var hostnameConVar = ConVar.Find("hostname");
var rconPasswordConVar = ConVar.Find("rcon_password");
ConVar? hostportConVar = ConVar.Find("hostport");
ConVar? hostnameConVar = ConVar.Find("hostname");
ConVar? rconPasswordConVar = ConVar.Find("rcon_password");
var address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
var hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
var rconPassword = rconPasswordConVar?.StringValue ?? "";
string address = $"{ipAddress}:{hostportConVar?.GetPrimitiveValue<int>()}";
string hostname = hostnameConVar?.StringValue ?? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
string rconPassword = rconPasswordConVar?.StringValue ?? "";
CS2_SimpleAdmin.IpAddress = address;
Task.Run(async () =>
{
try
{
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
await using DbConnection connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
int? serverId = await connection.ExecuteScalarAsync<int?>(
"SELECT id FROM sa_servers WHERE address = @address",
new { address });
@ -126,8 +131,8 @@ public class ServerManager
if (CS2_SimpleAdmin.Instance.Config.EnableMetrics)
{
var queryString = $"?address={address}&hostname={hostname}";
var client = CS2_SimpleAdmin.HttpClient;
string queryString = $"?address={address}&hostname={hostname}";
HttpClient client = CS2_SimpleAdmin.HttpClient;
try
{
@ -139,8 +144,8 @@ public class ServerManager
}
}
});
CS2_SimpleAdmin.SimpleAdminApi?.OnSimpleAdminReadyEvent();
});
}
}
}

View file

@ -1,4 +1,5 @@
using CS2_SimpleAdmin.Database;
using System.Data.Common;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdminApi;
using Dapper;
using Microsoft.Extensions.Logging;
@ -17,17 +18,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(true);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddWarnQuery(true);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = player.SteamId.SteamId64,
playerName = player.Name,
@ -42,7 +46,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
return warnId;
}
catch(Exception)
catch (Exception)
{
return null;
}
@ -58,17 +62,20 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
public async Task<int?> AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
{
if (databaseProvider == null) return null;
if (databaseProvider == null)
{
return null;
}
var now = Time.ActualDateTime();
var futureTime = now.AddMinutes(time);
DateTime now = Time.ActualDateTime();
DateTime futureTime = now.AddMinutes(time);
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetAddWarnQuery(false);
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
string sql = databaseProvider.GetAddWarnQuery(false);
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
int? warnId = await connection.ExecuteScalarAsync<int?>(sql, new
{
playerSteamid = playerSteamId,
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
@ -96,15 +103,18 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A list of dynamic objects representing warnings, or an empty list if none found or on failure.</returns>
public async Task<List<dynamic>> GetPlayerWarns(PlayerInfo player, bool active = true)
{
if (databaseProvider == null) return [];
if (databaseProvider == null)
{
return [];
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
string sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId };
var warns = await connection.QueryAsync<dynamic>(sql, parameters);
IEnumerable<dynamic> warns = await connection.QueryAsync<dynamic>(sql, parameters);
return warns.ToList();
}
@ -122,14 +132,17 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>The count of warnings as an integer, or 0 if none found or on failure.</returns>
public async Task<int> GetPlayerWarnsCount(ulong steamId, bool active = true)
{
if (databaseProvider == null) return 0;
if (databaseProvider == null)
{
return 0;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
string sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
int warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
return warnsCount;
}
catch (Exception)
@ -146,13 +159,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -160,7 +176,7 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove warn + {ex}");
}
}
/// <summary>
/// Removes the most recent warning matching a player pattern (usually SteamID string).
/// </summary>
@ -168,13 +184,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnwarnPlayer(string playerPattern)
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -189,13 +208,16 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
/// <returns>A task representing the asynchronous operation.</returns>
public async Task ExpireOldWarns()
{
if (databaseProvider == null) return;
if (databaseProvider == null)
{
return;
}
try
{
await using var connection = await databaseProvider.CreateConnectionAsync();
await using DbConnection connection = await databaseProvider.CreateConnectionAsync();
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
string sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
}
catch (Exception ex)
@ -203,4 +225,4 @@ internal class WarnManager(IDatabaseProvider? databaseProvider)
CS2_SimpleAdmin._logger?.LogCritical($"Unable to remove expired warns + {ex}");
}
}
}
}

View file

@ -1,75 +0,0 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
namespace CS2_SimpleAdmin.Menus;
public static class AdminMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
MenuManager.Instance.OpenMainMenu(admin);
}
public static IMenu? CreateMenu(string title, Action<CCSPlayerController>? backAction = null)
{
return Helper.CreateMenu(title, backAction);
// return CS2_SimpleAdmin.Instance.Config.UseChatMenu ? new ChatMenu(title) : new CenterHtmlMenu(title, CS2_SimpleAdmin.Instance);
}
public static void OpenMenu(CCSPlayerController player, IMenu menu)
{
menu.Open(player);
// switch (menu)
// {
// case CenterHtmlMenu centerHtmlMenu:
// MenuManager.OpenCenterHtmlMenu(CS2_SimpleAdmin.Instance, player, centerHtmlMenu);
// break;
// case ChatMenu chatMenu:
// MenuManager.OpenChatMenu(player, chatMenu);
// break;
// }
}
// public static void OpenMenu(CCSPlayerController admin)
// {
// if (admin.IsValid == false)
// return;
//
// var localizer = CS2_SimpleAdmin._localizer;
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic") == false)
// {
// admin.PrintToChat(localizer?["sa_prefix"] ??
// "[SimpleAdmin] " +
// (localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
// );
// return;
// }
//
// var menu = CreateMenu(localizer?["sa_title"] ?? "SimpleAdmin");
// List<ChatMenuOptionData> options =
// [
// new(localizer?["sa_menu_players_manage"] ?? "Players Manage", () => ManagePlayersMenu.OpenMenu(admin)),
// new(localizer?["sa_menu_server_manage"] ?? "Server Manage", () => ManageServerMenu.OpenMenu(admin)),
// new(localizer?["sa_menu_fun_commands"] ?? "Fun Commands", () => FunActionsMenu.OpenMenu(admin)),
// ];
//
// var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
// if (customCommands.Count > 0)
// {
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_custom_commands"] ?? "Custom Commands", () => CustomCommandsMenu.OpenMenu(admin)));
// }
//
// if (AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
// options.Add(new ChatMenuOptionData(localizer?["sa_menu_admins_manage"] ?? "Admins Manage", () => ManageAdminsMenu.OpenMenu(admin)));
//
// foreach (var menuOptionData in options)
// {
// var menuName = menuOptionData.Name;
// menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
// }
//
// if (menu != null) OpenMenu(admin, menu);
// }
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
namespace CS2_SimpleAdmin.Menus;
public class ChatMenuOptionData(string name, Action action, bool disabled = false)
{
public readonly string Name = name;
public readonly Action Action = action;
public readonly bool Disabled = disabled;
}

View file

@ -1,51 +0,0 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public static class CustomCommandsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_custom_commands"] ?? "Custom Commands");
List<ChatMenuOptionData> options = [];
var customCommands = CS2_SimpleAdmin.Instance.Config.CustomServerCommands;
options.AddRange(from customCommand in customCommands
where !string.IsNullOrEmpty(customCommand.DisplayName) && !string.IsNullOrEmpty(customCommand.Command)
let hasRights = AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), customCommand.Flag)
where hasRights
select new ChatMenuOptionData(customCommand.DisplayName, () =>
{
Helper.TryLogCommandOnDiscord(admin, customCommand.Command);
if (customCommand.ExecuteOnClient)
admin.ExecuteClientCommandFromServer(customCommand.Command);
else
Server.ExecuteCommand(customCommand.Command);
}));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
}

View file

@ -1,46 +0,0 @@
using CounterStrikeSharp.API.Core;
using CS2_SimpleAdmin.Models;
namespace CS2_SimpleAdmin.Menus;
public static class DurationMenu
{
public static void OpenMenu(CCSPlayerController admin, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, int> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
if (menu == null)
return;
var durations = CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations;
// Capture admin and player to avoid closure issues
var capturedAdmin = admin;
var capturedPlayer = player;
var capturedAction = onSelectAction;
foreach (var durationItem in durations)
{
var duration = durationItem.Duration; // Capture in local variable
var name = durationItem.Name;
menu.AddMenuOption(name, (controller, option) =>
{
capturedAction(capturedAdmin, capturedPlayer, duration);
});
}
AdminMenu.OpenMenu(admin, menu);
}
public static void OpenMenu(CCSPlayerController admin, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, int> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
foreach (var durationItem in CS2_SimpleAdmin.Instance.Config.MenuConfigs.Durations)
{
menu?.AddMenuOption(durationItem.Name, (_, _) => { onSelectAction(admin, player, durationItem.Duration); });
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
}

View file

@ -264,4 +264,4 @@
// {
// CS2_SimpleAdmin.SetMoney(admin, player, money);
// }
// }
// }

View file

@ -1,72 +0,0 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public static class ManageAdminsMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_admins_manage"] ?? "Admins Manage");
List<ChatMenuOptionData> options =
[
new(localizer?["sa_admin_add"] ?? "Add Admin",
() => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_admin_add"] ?? "Add Admin", AddAdminMenu)),
new(localizer?["sa_admin_remove"] ?? "Remove Admin",
() => PlayersMenu.OpenAdminPlayersMenu(admin, localizer?["sa_admin_remove"] ?? "Remove Admin", RemoveAdmin,
player => player != admin && admin.CanTarget(player))),
new(localizer?["sa_admin_reload"] ?? "Reload Admins", () => ReloadAdmins(admin))
];
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void AddAdminMenu(CCSPlayerController admin, CCSPlayerController player)
{
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_admin_add"] ?? "Add Admin"}: {player.PlayerName}");
foreach (var adminFlag in CS2_SimpleAdmin.Instance.Config.MenuConfigs.AdminFlags)
{
var disabled = AdminManager.PlayerHasPermissions(player, adminFlag.Flag);
menu?.AddMenuOption(adminFlag.Name, (_, _) => { AddAdmin(admin, player, adminFlag.Flag); }, disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void AddAdmin(CCSPlayerController admin, CCSPlayerController player, string flag)
{
// TODO: Change default immunity?
CS2_SimpleAdmin.AddAdmin(admin, player.SteamID.ToString(), player.PlayerName, flag, 10);
}
private static void RemoveAdmin(CCSPlayerController admin, CCSPlayerController player)
{
CS2_SimpleAdmin.Instance.RemoveAdmin(admin, player.SteamID.ToString());
}
private static void ReloadAdmins(CCSPlayerController admin)
{
CS2_SimpleAdmin.Instance.ReloadAdmins(admin);
}
}

View file

@ -1,325 +0,0 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Utils;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Menus;
public static class ManagePlayersMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_players_manage"] ?? "Manage Players");
List<ChatMenuOptionData> options = [];
// permissions
var hasSlay = AdminManager.CommandIsOverriden("css_slay") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_slay")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/slay");
var hasKick = AdminManager.CommandIsOverriden("css_kick") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_kick")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick");
var hasBan = AdminManager.CommandIsOverriden("css_ban") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_ban")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/ban");
var hasChat = AdminManager.CommandIsOverriden("css_gag") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat");
// TODO: Localize options
// options added in order
if (hasSlay)
{
options.Add(new ChatMenuOptionData(localizer?["sa_slap"] ?? "Slap", () => PlayersMenu.OpenMenu(admin, localizer?["sa_slap"] ?? "Slap", SlapMenu)));
options.Add(new ChatMenuOptionData(localizer?["sa_slay"] ?? "Slay", () => PlayersMenu.OpenMenu(admin, localizer?["sa_slay"] ?? "Slay", Slay)));
}
if (hasKick)
{
options.Add(new ChatMenuOptionData(localizer?["sa_kick"] ?? "Kick", () => PlayersMenu.OpenMenu(admin, localizer?["sa_kick"] ?? "Kick", KickMenu)));
}
if (AdminManager.CommandIsOverriden("css_warn")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_warn"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick"))
options.Add(new ChatMenuOptionData(localizer?["sa_warn"] ?? "Warn", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_warn"] ?? "Warn", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_warn"] ?? "Warn"}: {p.PlayerName}", p, WarnMenu))));
if (hasBan)
options.Add(new ChatMenuOptionData(localizer?["sa_ban"] ?? "Ban", () =>
PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_ban"] ?? "Ban", (a, p) =>
DurationMenu.OpenMenu(a, $"{localizer?["sa_ban"] ?? "Ban"}: {p.PlayerName}", p, BanMenu))));
if (hasChat)
{
if (AdminManager.CommandIsOverriden("css_gag")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_gag"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
options.Add(new ChatMenuOptionData(localizer?["sa_gag"] ?? "Gag", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_gag"] ?? "Gag", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_gag"] ?? "Gag"}: {p.PlayerName}", p, GagMenu))));
if (AdminManager.CommandIsOverriden("css_mute")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_mute"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
options.Add(new ChatMenuOptionData(localizer?["sa_mute"] ?? "Mute", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_mute"] ?? "Mute", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_mute"] ?? "Mute"}: {p.PlayerName}", p, MuteMenu))));
if (AdminManager.CommandIsOverriden("css_silence")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_silence"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/chat"))
options.Add(new ChatMenuOptionData(localizer?["sa_silence"] ?? "Silence", () => PlayersMenu.OpenRealPlayersMenu(admin, localizer?["sa_silence"] ?? "Silence", (a, p) => DurationMenu.OpenMenu(a, $"{localizer?["sa_silence"] ?? "Silence"}: {p.PlayerName}", p, SilenceMenu))));
}
if (AdminManager.CommandIsOverriden("css_team")
? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_team"))
: AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/kick"))
options.Add(new ChatMenuOptionData(localizer?["sa_team_force"] ?? "Force Team", () => PlayersMenu.OpenMenu(admin, localizer?["sa_team_force"] ?? "Force Team", ForceTeamMenu)));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void SlapMenu(CCSPlayerController admin, CCSPlayerController player)
{
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_slap"] ?? "Slap"}: {player.PlayerName}");
List<ChatMenuOptionData> options =
[
// options added in order
new("0 hp", () => ApplySlapAndKeepMenu(admin, player, 0)),
new("1 hp", () => ApplySlapAndKeepMenu(admin, player, 1)),
new("5 hp", () => ApplySlapAndKeepMenu(admin, player, 5)),
new("10 hp", () => ApplySlapAndKeepMenu(admin, player, 10)),
new("50 hp", () => ApplySlapAndKeepMenu(admin, player, 50)),
new("100 hp", () => ApplySlapAndKeepMenu(admin, player, 100)),
];
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void ApplySlapAndKeepMenu(CCSPlayerController admin, CCSPlayerController player, int damage)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Slap(admin, player, damage);
SlapMenu(admin, player);
}
private static void Slay(CCSPlayerController admin, CCSPlayerController player)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Slay(admin, player);
}
private static void KickMenu(CCSPlayerController admin, CCSPlayerController player)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Kick,
$"{CS2_SimpleAdmin._localizer?["sa_kick"] ?? "Kick"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Kick(admin, player, reason);
});
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_kick"] ?? "Kick"}: {player?.PlayerName}");
//
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons)
// {
// menu?.AddMenuOption(option, (_, _) =>
// {
// if (player is { IsValid: true })
// Kick(admin, player, option);
// });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void Kick(CCSPlayerController admin, CCSPlayerController player, string? reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Kick(admin, player, reason, admin.PlayerName);
}
internal static void BanMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Ban,
$"{CS2_SimpleAdmin._localizer?["sa_ban"] ?? "Ban"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Ban(admin, player, duration, reason);
CS2_SimpleAdmin.MenuApi?.CloseMenu(admin);
});
}
private static void Ban(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Ban(admin, player, duration, reason, admin.PlayerName);
}
private static void WarnMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Warn,
$"{CS2_SimpleAdmin._localizer?["sa_warn"] ?? "Warn"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Warn(admin, player, duration, reason);
});
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_warn"] ?? "Warn"}: {player?.PlayerName}");
//
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons)
// {
// menu?.AddMenuOption(option, (_, _) =>
// {
// if (player is { IsValid: true })
// Warn(admin, player, duration, option);
// });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void Warn(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Warn(admin, player, duration, reason, admin.PlayerName);
}
internal static void GagMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Gag,
$"{CS2_SimpleAdmin._localizer?["sa_gag"] ?? "Gag"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Gag(admin, player, duration, reason);
});
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_gag"] ?? "Gag"}: {player?.PlayerName}");
//
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
// {
// menu?.AddMenuOption(option, (_, _) =>
// {
// if (player is { IsValid: true })
// Gag(admin, player, duration, option);
// });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void Gag(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Gag(admin, player, duration, reason);
}
internal static void MuteMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Mute,
$"{CS2_SimpleAdmin._localizer?["sa_mute"] ?? "mute"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Mute(admin, player, duration, reason);
});
// // TODO: Localize and make options in config?
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_mute"] ?? "Mute"}: {player?.PlayerName}");
//
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
// {
// menu?.AddMenuOption(option, (_, _) =>
// {
// if (player is { IsValid: true })
// Mute(admin, player, duration, option);
// });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void Mute(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Mute(admin, player, duration, reason);
}
internal static void SilenceMenu(CCSPlayerController admin, CCSPlayerController player, int duration)
{
ReasonMenu.OpenMenu(admin, PenaltyType.Silence,
$"{CS2_SimpleAdmin._localizer?["sa_silence"] ?? "Silence"}: {player.PlayerName}", player, (_, _, reason) =>
{
if (player is { IsValid: true })
Silence(admin, player, duration, reason);
});
// // TODO: Localize and make options in config?
// var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_silence"] ?? "Silence"}: {player?.PlayerName}");
//
// foreach (var option in CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons)
// {
// menu?.AddMenuOption(option, (_, _) =>
// {
// if (player is { IsValid: true })
// Silence(admin, player, duration, option);
// });
// }
//
// if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void Silence(CCSPlayerController admin, CCSPlayerController player, int duration, string reason)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.Instance.Silence(admin, player, duration, reason);
}
private static void ForceTeamMenu(CCSPlayerController admin, CCSPlayerController player)
{
// TODO: Localize
var menu = AdminMenu.CreateMenu($"{CS2_SimpleAdmin._localizer?["sa_team_force"] ?? "Force Team"} {player.PlayerName}");
List<ChatMenuOptionData> options =
[
new(CS2_SimpleAdmin._localizer?["sa_team_ct"] ?? "CT", () => ForceTeam(admin, player, "ct", CsTeam.CounterTerrorist)),
new(CS2_SimpleAdmin._localizer?["sa_team_t"] ?? "T", () => ForceTeam(admin, player, "t", CsTeam.Terrorist)),
new(CS2_SimpleAdmin._localizer?["sa_team_swap"] ?? "Swap", () => ForceTeam(admin, player, "swap", CsTeam.Spectator)),
new(CS2_SimpleAdmin._localizer?["sa_team_spec"] ?? "Spec", () => ForceTeam(admin, player, "spec", CsTeam.Spectator)),
];
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void ForceTeam(CCSPlayerController admin, CCSPlayerController player, string teamName, CsTeam teamNum)
{
if (player is not { IsValid: true }) return;
CS2_SimpleAdmin.ChangeTeam(admin, player, teamName, teamNum, true);
}
}

View file

@ -1,83 +0,0 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
namespace CS2_SimpleAdmin.Menus;
public static class ManageServerMenu
{
public static void OpenMenu(CCSPlayerController admin)
{
if (!admin.IsValid)
return;
var localizer = CS2_SimpleAdmin._localizer;
if (!AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/generic"))
{
admin.PrintToChat(localizer?["sa_prefix"] ??
"[SimpleAdmin] " +
(localizer?["sa_no_permission"] ?? "You do not have permissions to use this command")
);
return;
}
var menu = AdminMenu.CreateMenu(localizer?["sa_menu_server_manage"] ?? "Server Manage");
List<ChatMenuOptionData> options = [];
// permissions
var hasMap = AdminManager.CommandIsOverriden("css_map") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_map")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/changemap");
var hasPlugins = AdminManager.CommandIsOverriden("css_pluginsmanager") ? AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), AdminManager.GetPermissionOverrides("css_pluginsmanager")) : AdminManager.PlayerHasPermissions(new SteamID(admin.SteamID), "@css/root");
//bool hasMap = AdminManager.PlayerHasPermissions(admin, "@css/changemap");
// options added in order
if (hasPlugins)
{
options.Add(new ChatMenuOptionData(localizer?["sa_menu_pluginsmanager_title"] ?? "Manage Plugins", () => admin.ExecuteClientCommandFromServer("css_pluginsmanager")));
}
if (hasMap)
{
options.Add(new ChatMenuOptionData(localizer?["sa_changemap"] ?? "Change Map", () => ChangeMapMenu(admin)));
}
options.Add(new ChatMenuOptionData(localizer?["sa_restart_game"] ?? "Restart Game", () => CS2_SimpleAdmin.RestartGame(admin)));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void ChangeMapMenu(CCSPlayerController admin)
{
var menu = AdminMenu.CreateMenu(CS2_SimpleAdmin._localizer?["sa_changemap"] ?? "Change Map");
List<ChatMenuOptionData> options = [];
var maps = CS2_SimpleAdmin.Instance.Config.DefaultMaps;
options.AddRange(maps.Select(map => new ChatMenuOptionData(map, () => ExecuteChangeMap(admin, map, false))));
var wsMaps = CS2_SimpleAdmin.Instance.Config.WorkshopMaps;
options.AddRange(wsMaps.Select(map => new ChatMenuOptionData($"{map.Key} (WS)", () => ExecuteChangeMap(admin, map.Value?.ToString() ?? map.Key, true))));
foreach (var menuOptionData in options)
{
var menuName = menuOptionData.Name;
menu?.AddMenuOption(menuName, (_, _) => { menuOptionData.Action.Invoke(); }, menuOptionData.Disabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
private static void ExecuteChangeMap(CCSPlayerController admin, string mapName, bool workshop)
{
if (workshop)
CS2_SimpleAdmin.Instance.ChangeWorkshopMap(admin, mapName);
else
CS2_SimpleAdmin.Instance.ChangeMap(admin, mapName);
}
}

View file

@ -1,17 +1,29 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
using PanoramaUiKit.Shared.Api;
using PanoramaUiKit.Shared.Menus;
namespace CS2_SimpleAdmin.Menus;
/// <summary>
/// Describes a menu and renders it through the Panorama UI Kit.
/// Sub-menus are pushed on top of their parent, the kit draws the back button.
/// </summary>
public class MenuBuilder
{
/// <summary>
/// Bumped every time a menu is opened for a player, so an option can tell whether its action opened another one.
/// </summary>
private static readonly Dictionary<ulong, int> OpenCounters = [];
private readonly string _title;
private readonly CCSPlayerController? _player;
private readonly IStringLocalizer? _localizer;
private readonly List<MenuOption> _options = [];
private MenuBuilder? _parentMenu;
private Action<CCSPlayerController>? _backAction;
private Action<CCSPlayerController>? _resetAction;
private bool _isSubMenu;
/// <summary>
/// Constructor for player-localized menu with translation key
@ -24,7 +36,7 @@ public class MenuBuilder
}
/// <summary>
/// Constructor for static title (backward compatibility)
/// Constructor for static title
/// </summary>
public MenuBuilder(string title)
{
@ -33,21 +45,6 @@ public class MenuBuilder
_localizer = null;
}
/// <summary>
/// Gets the localized title for the player
/// </summary>
private string GetLocalizedTitle()
{
if (_player != null && _localizer != null)
{
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[_title];
}
}
return _title;
}
/// <summary>
/// Adds a menu option with an action.
/// </summary>
@ -56,7 +53,9 @@ public class MenuBuilder
/// <param name="disabled">Whether the option is disabled</param>
/// <param name="permission">Required permission</param>
/// <param name="isTranslationKey">If true, name is a translation key to be localized</param>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null, bool isTranslationKey = false)
/// <param name="keepOpen">If true, the menu stays open after the action</param>
public MenuBuilder AddOption(string name, Action<CCSPlayerController> action, bool disabled = false, string? permission = null,
bool isTranslationKey = false, bool keepOpen = false)
{
_options.Add(new MenuOption
{
@ -64,46 +63,18 @@ public class MenuBuilder
Action = action,
Disabled = disabled,
Permission = permission,
IsTranslationKey = isTranslationKey
IsTranslationKey = isTranslationKey,
KeepOpen = keepOpen
});
return this;
}
/// <summary>
/// Gets the localized name for a menu option
/// </summary>
private string GetLocalizedOptionName(MenuOption option)
{
if (option.IsTranslationKey && _player != null && _localizer != null)
{
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[option.Name];
}
}
return option.Name;
}
/// <summary>
/// Adds a menu option that opens a submenu.
/// </summary>
public MenuBuilder AddSubMenu(string name, Func<MenuBuilder> subMenuFactory, bool disabled = false, string? permission = null)
{
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory();
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
Disabled = disabled,
Permission = permission
});
return this;
return AddSubMenu(name, _ => subMenuFactory(), disabled, permission);
}
/// <summary>
@ -114,14 +85,7 @@ public class MenuBuilder
_options.Add(new MenuOption
{
Name = name,
Action = player =>
{
var subMenu = subMenuFactory(player);
subMenu.SetParent(this);
// Automatically add back button to submenu
subMenu.WithBackButton();
subMenu.OpenMenu(player);
},
SubMenuFactory = subMenuFactory,
Disabled = disabled,
Permission = permission
});
@ -129,66 +93,50 @@ public class MenuBuilder
}
/// <summary>
/// Adds a back button to return to the previous menu.
/// Opens this menu on top of the current one, with a back button.
/// </summary>
public MenuBuilder WithBackButton()
{
if (_parentMenu != null)
{
_backAction = player => _parentMenu.OpenMenu(player);
// Add back option at the end of menu
// AddOption(backButtonText, _backAction);
}
_isSubMenu = true;
return this;
}
/// <summary>
/// Sets the parent menu (for navigation).
/// </summary>
private void SetParent(MenuBuilder parent)
{
_parentMenu = parent;
_backAction = player => _parentMenu.OpenMenu(player);
}
/// <summary>
/// Opens the menu for a player.
/// </summary>
/// <param name="player">The player to open the menu for.</param>
public void OpenMenu(CCSPlayerController player)
{
if (!player.IsValid) return;
// Get localized title
var localizedTitle = GetLocalizedTitle();
// Use MenuManager dependency
var menu = Helper.CreateMenu(localizedTitle, _backAction);
if (menu == null) return;
foreach (var option in _options)
if (!player.IsValid)
{
// Check permissions if required
if (!string.IsNullOrEmpty(option.Permission))
{
var steamId = new CounterStrikeSharp.API.Modules.Entities.SteamID(player.SteamID);
if (!CounterStrikeSharp.API.Modules.Admin.AdminManager.PlayerHasPermissions(steamId, option.Permission))
{
continue; // Skip option if player doesn't have permission
}
}
// Get localized option name
var localizedName = GetLocalizedOptionName(option);
menu.AddMenuOption(localizedName, (menuPlayer, menuOption) =>
{
option.Action?.Invoke(menuPlayer);
}, option.Disabled);
return;
}
menu.Open(player);
IPanoramaUiKit? kit = UiKit.Instance;
if (kit == null)
{
return;
}
MenuOptions menu = Build(player);
BumpOpenCounter(player);
if (_isSubMenu)
{
kit.Menus.Push(player, menu);
}
else
{
kit.Menus.Open(player, menu);
}
}
/// <summary>
/// Closes any SimpleAdmin menu open for the player.
/// </summary>
public static void CloseMenu(CCSPlayerController player)
{
UiKit.Instance?.Menus.Close(player, UiKit.MenuLayout);
}
/// <summary>
@ -201,26 +149,101 @@ public class MenuBuilder
return this;
}
/// <summary>
/// Sets a reset action for the menu.
/// </summary>
/// <param name="resetAction">The action to execute on reset.</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithResetAction(Action<CCSPlayerController> resetAction)
private MenuOptions Build(CCSPlayerController player)
{
_resetAction = resetAction;
return this;
List<MenuEntry> entries = [];
SteamID steamId = new SteamID(player.SteamID);
foreach (MenuOption option in _options)
{
if (!string.IsNullOrEmpty(option.Permission) && !AdminManager.PlayerHasPermissions(steamId, option.Permission))
{
continue;
}
string label = GetLocalizedOptionName(option);
MenuEntry entry;
if (option.SubMenuFactory != null)
{
Func<CCSPlayerController, MenuBuilder> subMenuFactory = option.SubMenuFactory;
entry = MenuEntry.Open(label, menuPlayer =>
{
BumpOpenCounter(menuPlayer);
return subMenuFactory(menuPlayer).Build(menuPlayer);
});
}
else
{
entry = MenuEntry.Button(label, menuPlayer => RunOption(option, menuPlayer));
}
if (option.Disabled)
{
entry = entry with { Disabled = true };
}
entries.Add(entry);
}
return new MenuOptions
{
Layout = UiKit.MenuLayout,
Title = GetLocalizedTitle(),
Entries = entries
};
}
/// <summary>
/// Sets a custom back action for the menu.
/// Runs the option, then closes the menu unless the option keeps it open or its action opened another menu.
/// </summary>
/// <param name="backAction">The action to execute when going back (nullable).</param>
/// <returns>This MenuBuilder instance for chaining.</returns>
public MenuBuilder WithBackAction(Action<CCSPlayerController>? backAction)
private static void RunOption(MenuOption option, CCSPlayerController player)
{
_backAction = backAction;
return this;
int counterBefore = GetOpenCounter(player);
option.Action?.Invoke(player);
if (!option.KeepOpen && player.IsValid && GetOpenCounter(player) == counterBefore)
{
CloseMenu(player);
}
}
private static int GetOpenCounter(CCSPlayerController player)
{
OpenCounters.TryGetValue(player.SteamID, out int counter);
return counter;
}
private static void BumpOpenCounter(CCSPlayerController player)
{
OpenCounters[player.SteamID] = GetOpenCounter(player) + 1;
}
private string GetLocalizedTitle()
{
if (_player == null || _localizer == null)
{
return _title;
}
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[_title];
}
}
private string GetLocalizedOptionName(MenuOption option)
{
if (!option.IsTranslationKey || _player == null || _localizer == null)
{
return option.Name;
}
using (new WithTemporaryCulture(_player.GetLanguage()))
{
return _localizer[option.Name];
}
}
}
@ -231,8 +254,9 @@ public class MenuOption
{
public string Name { get; set; } = string.Empty;
public Action<CCSPlayerController>? Action { get; set; }
public Func<CCSPlayerController, MenuBuilder>? SubMenuFactory { get; set; }
public bool Disabled { get; set; }
public string? Permission { get; set; }
public bool IsTranslationKey { get; set; }
public bool KeepOpen { get; set; }
}

View file

@ -2,6 +2,7 @@ using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
namespace CS2_SimpleAdmin.Menus;
@ -125,7 +126,11 @@ public class MenuManager
/// <param name="menuId">The menu to unregister.</param>
public void UnregisterMenu(string categoryId, string menuId)
{
if (!_menuCategories.TryGetValue(categoryId, out var category)) return;
if (!_menuCategories.TryGetValue(categoryId, out MenuCategory? category))
{
return;
}
category.MenuFactories.Remove(menuId);
_menuCategories[categoryId].MenuNames.Remove(menuId);
_menuCategories[categoryId].MenuPermissions.Remove(menuId);
@ -139,16 +144,21 @@ public class MenuManager
/// <returns>A MenuBuilder instance for the main menu.</returns>
public MenuBuilder CreateMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var mainMenu = new MenuBuilder("sa_title", player, localizer);
IStringLocalizer? localizer = CS2_SimpleAdmin._localizer;
MenuBuilder mainMenu = new MenuBuilder("sa_title", player, localizer);
foreach (var category in _menuCategories.Values)
foreach (MenuCategory category in _menuCategories.Values)
{
if (category.MenuFactories.Count <= 0) continue;
// Check category permissions
var steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
if (category.MenuFactories.Count <= 0)
{
continue;
}
// Check category permissions
SteamID steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, category.Permission))
{
continue;
}
// Get localized category name for this player
// If category has a module localizer, use it; otherwise use main plugin localizer
@ -181,7 +191,7 @@ public class MenuManager
/// <returns>A MenuBuilder instance for the category menu.</returns>
private MenuBuilder CreateCategoryMenu(MenuCategory category, CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
IStringLocalizer? localizer = CS2_SimpleAdmin._localizer;
// Get localized category name for this player
// If category has a module localizer, use it; otherwise use main plugin localizer
@ -198,18 +208,22 @@ public class MenuManager
}
}
var categoryMenu = new MenuBuilder(localizedCategoryName);
MenuBuilder categoryMenu = new MenuBuilder(localizedCategoryName);
foreach (var kvp in category.MenuFactories)
foreach (KeyValuePair<string, Func<CCSPlayerController, MenuBuilder>> kvp in category.MenuFactories)
{
var menuId = kvp.Key;
var menuFactory = kvp.Value;
var menuName = category.MenuNames.TryGetValue(menuId, out var name) ? name : menuId;
var permission = category.MenuPermissions.TryGetValue(menuId, out var perm) ? perm : null;
var commandName = category.MenuCommandNames.TryGetValue(menuId, out var cmd) ? cmd : null;
string menuId = kvp.Key;
Func<CCSPlayerController, MenuBuilder> menuFactory = kvp.Value;
if (!category.MenuNames.TryGetValue(menuId, out string? menuName))
{
menuName = menuId;
}
category.MenuPermissions.TryGetValue(menuId, out string? permission);
category.MenuCommandNames.TryGetValue(menuId, out string? commandName);
// Check permissions with command override support
var steamId = new SteamID(player.SteamID);
SteamID steamId = new SteamID(player.SteamID);
// If commandName is provided, check for permission overrides
if (!string.IsNullOrEmpty(commandName))
@ -219,7 +233,7 @@ public class MenuManager
// Check if command has overridden permissions
if (AdminManager.CommandIsOverriden(commandName))
{
var overriddenPermission = AdminManager.GetPermissionOverrides(commandName);
string[] overriddenPermission = AdminManager.GetPermissionOverrides(commandName);
hasPermission = AdminManager.PlayerHasPermissions(steamId, overriddenPermission);
}
else if (!string.IsNullOrEmpty(permission))
@ -234,13 +248,17 @@ public class MenuManager
}
if (!hasPermission)
{
continue;
}
}
// Fallback to standard permission check if no commandName provided
else if (!string.IsNullOrEmpty(permission))
{
if (!AdminManager.PlayerHasPermissions(steamId, permission))
{
continue;
}
}
// Get localized menu name for this player
@ -248,7 +266,7 @@ public class MenuManager
string localizedMenuName;
using (new WithTemporaryCulture(player.GetLanguage()))
{
if (category.MenuLocalizers.TryGetValue(menuId, out var menuLocalizer))
if (category.MenuLocalizers.TryGetValue(menuId, out IStringLocalizer? menuLocalizer))
{
// Menu has its own module localizer
localizedMenuName = menuLocalizer[menuName] ?? menuName;
@ -278,9 +296,9 @@ public class MenuManager
/// <param name="player">The player to open the menu for.</param>
public void OpenMainMenu(CCSPlayerController player)
{
var localizer = CS2_SimpleAdmin._localizer;
var steamId = new SteamID(player.SteamID);
IStringLocalizer? localizer = CS2_SimpleAdmin._localizer;
SteamID steamId = new SteamID(player.SteamID);
if (!AdminManager.PlayerHasPermissions(steamId, "@css/generic"))
{
player.PrintToChat(localizer?["sa_prefix"] ?? "[SimpleAdmin] " +
@ -303,17 +321,6 @@ public class MenuManager
// RegisterCategory("fun", "sa_menu_fun_commands", "@css/generic");
RegisterCategory("admin", "sa_menu_admins_manage", "@css/root");
}
/// <summary>
/// Public method for creating category menus (for API usage).
/// </summary>
/// <param name="category">The menu category to create.</param>
/// <param name="player">The player to create the menu for.</param>
/// <returns>A MenuBuilder instance for the category menu.</returns>
public MenuBuilder CreateCategoryMenuPublic(MenuCategory category, CCSPlayerController player)
{
return CreateCategoryMenu(category, player);
}
}
/// <summary>

View file

@ -1,56 +0,0 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Admin;
using System.Web;
namespace CS2_SimpleAdmin.Menus;
public static class PlayersMenu
{
public static void OpenRealPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => !p.IsBot);
}
public static void OpenAdminPlayersMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController?, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => AdminManager.GetPlayerAdminData(p)?.Flags.Count > 0);
}
public static void OpenAliveMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.PlayerPawn?.Value?.LifeState == (int)LifeState_t.LIFE_ALIVE);
}
public static void OpenDeadMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController?, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
OpenMenu(admin, menuName, onSelectAction, p => p.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE);
}
public static void OpenMenu(CCSPlayerController admin, string menuName, Action<CCSPlayerController, CCSPlayerController> onSelectAction, Func<CCSPlayerController, bool>? enableFilter = null)
{
var menu = AdminMenu.CreateMenu(menuName);
var players = Helper.GetValidPlayersWithBots();
foreach (var player in players)
{
var playerName = player != null && player.PlayerName.Length > 26 ? player.PlayerName[..26] : player?.PlayerName;
var optionName = HttpUtility.HtmlEncode(playerName);
if (player != null && enableFilter != null && !enableFilter(player))
continue;
var enabled = admin.CanTarget(player);
var capturedPlayer = player; // Capture in local variable to avoid closure issues
if (optionName != null)
menu?.AddMenuOption(optionName, (controller, option) =>
{
if (capturedPlayer != null) onSelectAction.Invoke(admin, capturedPlayer);
},
!enabled);
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
}

View file

@ -1,52 +0,0 @@
using CounterStrikeSharp.API.Core;
using CS2_SimpleAdmin.Models;
using CS2_SimpleAdminApi;
namespace CS2_SimpleAdmin.Menus;
public static class ReasonMenu
{
public static void OpenMenu(CCSPlayerController admin, PenaltyType penaltyType, string menuName, CCSPlayerController player, Action<CCSPlayerController, CCSPlayerController, string> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
var reasons = penaltyType switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons,
PenaltyType.Kick => CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons,
PenaltyType.Gag => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
PenaltyType.Silence => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
_ => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons
};
foreach (var reason in reasons)
{
menu?.AddMenuOption(reason, (_, _) => onSelectAction(admin, player, reason));
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
public static void OpenMenu(CCSPlayerController admin, PenaltyType penaltyType, string menuName, DisconnectedPlayer player, Action<CCSPlayerController, DisconnectedPlayer, string> onSelectAction)
{
var menu = AdminMenu.CreateMenu(menuName);
var reasons = penaltyType switch
{
PenaltyType.Ban => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons,
PenaltyType.Kick => CS2_SimpleAdmin.Instance.Config.MenuConfigs.KickReasons,
PenaltyType.Mute => CS2_SimpleAdmin.Instance.Config.MenuConfigs.MuteReasons,
PenaltyType.Warn => CS2_SimpleAdmin.Instance.Config.MenuConfigs.WarnReasons,
_ => CS2_SimpleAdmin.Instance.Config.MenuConfigs.BanReasons
};
foreach (var reason in reasons)
{
menu?.AddMenuOption(reason, (_, _) => onSelectAction(admin, player, reason));
}
if (menu != null) AdminMenu.OpenMenu(admin, menu);
}
}

View file

@ -0,0 +1,81 @@
using CounterStrikeSharp.API.Core.Capabilities;
using Microsoft.Extensions.Logging;
using PanoramaUiKit.Shared.Api;
using PanoramaUiKit.Shared.Banners;
using PanoramaUiKit.Shared.Common;
using PanoramaUiKit.Shared.Menus;
using PanoramaUiKit.Shared.Toasts;
using PanoramaUiKit.Shared.Votes;
namespace CS2_SimpleAdmin.Menus;
/// <summary>
/// Access to the Panorama UI Kit plugin and the layouts SimpleAdmin draws on.
/// </summary>
internal static class UiKit
{
private static readonly PluginCapability<IPanoramaUiKit> Capability = new(IPanoramaUiKit.CapabilityName);
internal static MenuLayout MenuLayout { get; private set; } = new() { Name = "uikit_menu" };
internal static VoteLayout VoteLayout { get; private set; } = new() { Name = "uikit_vote" };
internal static BannerLayout BannerLayout { get; private set; } = new() { Name = "uikit_banner" };
internal static ToastLayout ToastLayout { get; private set; } = new() { Name = "uikit_toast" };
/// <summary>
/// Resolved on every use so a hot reload of the kit is picked up.
/// </summary>
internal static IPanoramaUiKit? Instance
{
get
{
try
{
return Capability.Get();
}
catch (Exception)
{
return null;
}
}
}
internal static void Configure(MenuConfig config)
{
MenuLayout = new MenuLayout { Name = config.MenuLayout };
VoteLayout = new VoteLayout { Name = config.VoteLayout };
BannerLayout = new BannerLayout { Name = config.BannerLayout };
ToastLayout = new ToastLayout { Name = config.ToastLayout };
}
/// <summary>
/// Spawns the layouts up front: creating one on first use sends the client one big burst.
/// </summary>
internal static bool Preload(ILogger logger)
{
IPanoramaUiKit? kit = Instance;
if (kit == null)
{
logger.LogError("Panorama UI Kit not found: install PanoramaUiKit and shared/PanoramaUiKit.Shared.");
return false;
}
if (kit.Version != IPanoramaUiKit.ApiVersion)
{
logger.LogWarning("Panorama UI Kit API v{Running}, SimpleAdmin built against v{Built}.", kit.Version, IPanoramaUiKit.ApiVersion);
}
UiLayout[] layouts = [MenuLayout, VoteLayout, BannerLayout, ToastLayout];
foreach (UiLayout layout in layouts)
{
if (!kit.Preload(layout))
{
logger.LogError("Layout {Layout} was refused, see the Panorama UI Kit log.", layout);
}
}
return true;
}
}

View file

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace CS2_SimpleAdmin.Models;
public enum BanStatus
public enum BanStatus
{
[Description("ACTIVE")] ACTIVE,
[Description("UNBANNED")] UNBANNED,
@ -15,13 +15,13 @@ public record BanRecord
{
[Column("id")]
public int Id { get; init; }
[Column("player_name")]
public string? PlayerName { get; set; }
[Column("player_steamid")]
public ulong? PlayerSteamId { get; set; }
[Column("player_ip")]
public string? PlayerIp { get; set; }

View file

@ -12,4 +12,4 @@ public class DisconnectedPlayer(
public string Name { get; set; } = name;
public string? IpAddress { get; set; } = ipAddress;
public DateTime DisconnectTime = disconnectTime;
}
}

View file

@ -1,14 +1,12 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Capabilities;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CS2_SimpleAdmin.Models;
using CS2_SimpleAdminApi;
using MenuManager;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CS2_SimpleAdmin.Database;
using CS2_SimpleAdmin.Managers;
using CS2_SimpleAdmin.Models;
using CS2_SimpleAdminApi;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;
namespace CS2_SimpleAdmin;
@ -29,7 +27,6 @@ public partial class CS2_SimpleAdmin
public static IStringLocalizer? _localizer;
// Voting System
public static readonly Dictionary<string, int> VoteAnswers = [];
public static bool VoteInProgress;
// Command and Server Settings
@ -62,10 +59,6 @@ public partial class CS2_SimpleAdmin
private static MemoryFunctionVoid<CBasePlayerController, CCSPlayerPawn, bool, bool>?
_cBasePlayerControllerSetPawnFunc;
// Menu API and Capabilities
internal static IMenuApi? MenuApi;
private static readonly PluginCapability<IMenuApi> MenuCapability = new("menu:nfcore");
// Shared API
internal static Api.CS2_SimpleAdminApi? SimpleAdminApi { get; private set; }
@ -79,8 +72,8 @@ public partial class CS2_SimpleAdmin
// Timers
internal Timer? PlayersTimer = null;
// Funny list
private readonly List<string> _requiredPlugins = ["MenuManagerCore", "PlayerSettings"];
private readonly List<string> _requiredShared = ["MenuManagerApi", "PlayerSettingsApi", "AnyBaseLib", "CS2-SimpleAdminApi"];
}
private readonly List<string> _requiredPlugins = ["PanoramaUiKit"];
private readonly List<string> _requiredShared = ["PanoramaUiKit.Shared", "CS2-SimpleAdminApi"];
}