Ich habe mir ein Plugin geschrieben dafür sieht sehr manierlich aus
Beiträge von ddbmaster
-
-
Mein Name ist Walter ich bin hier der Cheff
-
Hier ein Bild
-
Mit einlesen von vorherige version.
admins.txt
ban-liste.txt
gruppen.txt
ban-zeiten.txt
ban-gruendeCode: Exemple
Code
Alles anzeigenadmins # ID : Name : SteamID64 : Gruppe : Immunitaet : ErstelltAm 1 : ddbmaster : xxxxxxxxxxxxxxx : #root : 100 : 2025-10-29 2 : Nixdorf : xxxxxxxxxxxxxxx : #Betreuer : 50 : 2025-10-29 gruppen // Gruppenname : Flags #root : @css/root,@css/adminmenu #Betreuer : @css/ban,@css/unban,@css/kick,@css/slay,@css/changemap,@css/listmaps,@css/reloadconfigs,@css/adminmenu ban-zeiten # Anzeigename : Minuten Permanent : 0 30 Minuten : 30 1 Stunde : 60 6 Stunden : 360 ban-gruende # Anzeigename : Grund Passt nicht : Passt nicht zu uns. Cheating : Cheating Beleidigung : Beleidigung ban-liste # ID : Name : SteamID64 : Minuten : Grund : Admin : ErstelltAm 1 : Name : xxxxxxxxxxxxxxx : 0 : Umgehung eines Bans : Admin-Name :xxxxxxxxxxxxxxx : 2025-10-29 18:15:00, 2 : Name : xxxxxxxxxxxxxxx : 0 : Passt nicht zu uns : Admin-Name : xxxxxxxxxxxxxxx : 2025-10-29 18:30:00Weniger anzeigen
-
Das Plugin in ist installiert
Und auch das Plugin in woltlab Bannliste
Zusätzlich noch das css für das scrollen rechts linksCode
Alles anzeigen/* Bannliste horizontal scrollbar machen */ .table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; } /* Tabelle darf breiter sein */ .table thead, .table tbody { display: table; width: max-content; } /* verhindert Zusammenquetschen */ .table th, .table td { white-space: nowrap; } -
de.txt
Code
Alles anzeigenUsage_AddGroup = Benutzung: css_addgroup <#gruppenname> <flag1,flag2,...> Usage_DelGroup = Benutzung: css_delgroup <#gruppenname> GroupAdded = Gruppe {0} mit Flags hinzugefügt: {1} GroupExists = Gruppe {0} existiert bereits. GroupDeleted = Gruppe {0} wurde gelöscht. GroupNotExist = Gruppe {0} existiert nicht. NoGroups = Keine Gruppen vorhanden. Usage_AddAdmin = Benutzung: css_addadmin <name> <steamid64> <#gruppe> <immunitaet> Usage_DelAdmin = Benutzung: css_deladmin <steamid64> Usage_SetGroup = Benutzung: css_setgroup <steamid64> <#gruppe> Usage_SetImmunity = Benutzung: css_setimmunity <steamid64> <wert> Usage_SetAdminName = Benutzung: css_setadminname <steamid64> <name> AdminAdded = Admin {0} hinzugefügt zur Gruppe {1} mit Immunität {2}. AdminRemoved = Admin {0} wurde entfernt. AdminNotFound = Kein Admin mit SteamID {0} gefunden. AdminUpdatedGroup = Admin {0} wurde Gruppe {1} zugewiesen. AdminUpdatedImmunity = Admin {0} hat jetzt Immunität {1}. AdminUpdatedName = Admin {0} wurde umbenannt in {1}. NoAdmins = Keine Admins vorhanden. AdminSelfGroup = {0} Du bist jetzt {1} AdminSelfGroupCss = {0} Du bist jetzt CSS-Admin Usage_Ban = Benutzung: css_ban oder css_ban <steamid64> <minuten> <grund> Usage_BanId = Benutzung: css_banid <steamid64> <minuten> <grund> Usage_Unban = Benutzung: css_unban <steamid64> Usage_EditBan = Benutzung: css_editban <steamid64> <minuten> <grund> Usage_ReloadBanReasons = Benutzung: css_reloadbanreasons Usage_ReloadBanTimes = Benutzung: css_reloadbantimes Usage_ReloadGroups = Benutzung: css_reloadgroups BanAdded = Spieler {0} ({1}) wurde gebannt. Dauer: {2}. Grund: {3} von {4} BanEdited = Ban für Spieler {0} ({1}) wurde geändert. Neue Dauer: {2}. Neuer Grund: {3}. Bearbeitet von: {4} BanRemoved = Ban entfernt: {0} NoBanFound = Kein aktiver Ban für SteamID {0} gefunden. NoBans = Keine Bans vorhanden. NoActiveBans = Keine aktiven Bans vorhanden. NoBanTimes = Keine Ban-Zeiten vorhanden. NoBanReasons = Keine Ban-Gründe vorhanden. BanAlreadyActive = Für SteamID {0} existiert bereits ein aktiver Ban. BanMenuTitlePlayers = Spieler für Ban auswählen BanMenuTitleTimes = Ban-Zeit für {0} auswählen BanMenuTitleReasons = Ban-Grund für {0} auswählen EditBanMenuTitleBans = Aktiven Ban auswählen EditBanMenuTitleTimes = Neue Ban-Zeit für {0} auswählen EditBanMenuTitleReasons = Neuen Ban-Grund für {0} auswählen EditBanMenuEntry = {0}. {1} ({2}) EditBanCustomReasonHint = Eigener Grund: Benutze css_editban {0} {1} <dein grund> EditBanUseCommand = Eigenen Grund per Befehl BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} Minuten) BanMenuReasonEntry = {0} BanMenuNoPlayers = Keine Spieler gefunden. BanJoinDenied = Du bist gebannt. Grund: {0}. Bitte melde dich bei {1} BanContact = ddbmaster.de BanStatusActive = Aktiv BanStatusRemoved = Entbannt BanStatusExpired = Abgelaufen BanDeletedBy = entfernt von {0} BanEditedBy = bearbeitet von {0} BanListEntry = {0} | {1} | {2} | {3} | {4} BanDurationPermanent = Permanent BanDurationMinutes = {0} Minuten KickDenied = Du kannst diesen Spieler nicht kicken: Ziel hat gleiche oder höhere Immunität. KickDone = Spieler {0} wurde gekickt. Usage_Kick = Benutzung: css_kick oder css_kick <steamid64> KickMenuTitlePlayers = Spieler für Kick auswählen KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = Keine Spieler gefunden. SlayDenied = Du kannst diesen Spieler nicht töten: Ziel hat gleiche oder höhere Immunität. SlayDone = Spieler {0} wurde getötet. TargetNotFound = Zielspieler nicht gefunden. Usage_Slay = Benutzung: css_slay oder css_slay <steamid64> SlayMenuTitlePlayers = Spieler für Slay auswählen SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = Keine Spieler gefunden. Usage_Map = Benutzung: css_map oder css_map <mapname|workshopid> Usage_WsMap = Benutzung: css_wsmap oder css_wsmap <mapname|workshopid> MapChanged = {0} wechselt die Map nach {1}. MapNotFound = Die Map oder Workshop-Map ""{0}"" wurde nicht gefunden. NoMaps = Keine Maps in changemap.txt vorhanden. MapListHeader = Verfügbare Maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Standard Map Menü MapMenuTitleWorkshop = Workshop Map Menü MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = Keine Standard-Maps gefunden. MapMenuNoWorkshopMaps = Keine Workshop-Maps gefunden. Usage_ListBanTimes = Benutzung: css_listbantimes BanTimeHeader = Verfügbare Ban-Zeiten: BanTimeEntry = {0} = {1} Minuten Usage_ListBanReasons = Benutzung: css_listbanreasons BanReasonHeader = Verfügbare Ban-Gründe: BanReasonEntry = {0} = {1} Usage_AdminMenu = Benutzung: css_adminmenu AdminMenuTitle = BZ Admin Menü AdminMenuBan = Ban Menü AdminMenuEditBan = Ban bearbeiten AdminMenuKick = Kick Menü AdminMenuSlay = Slay Menü AdminMenuMap = Standard Map Menü AdminMenuWsMap = Workshop Map Menü AdminMenuListMaps = Mapliste anzeigen AdminMenuListBans = Bannliste anzeigen AdminMenuListBanTimes = Ban-Zeiten anzeigen AdminMenuListBanReasons = Ban-Gründe anzeigen MenuMain = Hauptmenü ConfigsReloaded = BZ_Admin Konfigurationen wurden neu geladen. ReloadBanReasonsDone = Ban-Gründe wurden neu geladen: {0} ReloadBanTimesDone = Ban-Zeiten wurden neu geladen: {0} ReloadGroupsDone = Gruppen wurden neu geladen: {0} LanguageLoaded = Sprache geladen: {0} InvalidSteamId64 = Ungültige SteamID64. InvalidImmunity = Ungültiger Immunitätswert. InvalidBanMinutes = Ungültige Ban-Dauer in Minuten. PlayerOnlyCommand = Dieser Befehl muss von einem Spieler ausgeführt werden. BanDenied = Du kannst diesen Spieler nicht bannen: Ziel hat gleiche oder höhere Immunität. NoPermission = Du hast keine Berechtigung für diesen Befehl.en.txt
Code
Alles anzeigenUsage_AddGroup = Usage: css_addgroup <#groupname> <flag1,flag2,...> Usage_DelGroup = Usage: css_delgroup <#groupname> GroupAdded = Group {0} added with flags: {1} GroupExists = Group {0} already exists. GroupDeleted = Group {0} has been deleted. GroupNotExist = Group {0} does not exist. NoGroups = No groups defined. Usage_AddAdmin = Usage: css_addadmin <name> <steamid64> <#group> <immunity> Usage_DelAdmin = Usage: css_deladmin <steamid64> Usage_SetGroup = Usage: css_setgroup <steamid64> <#group> Usage_SetImmunity = Usage: css_setimmunity <steamid64> <value> Usage_SetAdminName = Usage: css_setadminname <steamid64> <name> AdminAdded = Admin {0} added to {1} with immunity {2}. AdminRemoved = Admin {0} has been removed. AdminNotFound = Admin with SteamID {0} not found. AdminUpdatedGroup = Admin {0} has been assigned to group {1}. AdminUpdatedImmunity = Admin {0} now has immunity {1}. AdminUpdatedName = Admin {0} has been renamed to {1}. NoAdmins = No admins defined. AdminSelfGroup = {0} You are now {1} AdminSelfGroupCss = {0} You are now a CSS admin Usage_Ban = Usage: css_ban or css_ban <steamid64> <minutes> <reason> Usage_BanId = Usage: css_banid <steamid64> <minutes> <reason> Usage_Unban = Usage: css_unban <steamid64> Usage_EditBan = Usage: css_editban <steamid64> <minutes> <reason> Usage_ReloadBanReasons = Usage: css_reloadbanreasons Usage_ReloadBanTimes = Usage: css_reloadbantimes Usage_ReloadGroups = Usage: css_reloadgroups BanAdded = Player {0} ({1}) has been banned. Duration: {2}. Reason: {3} by {4} BanEdited = Ban for player {0} ({1}) has been changed. New duration: {2}. New reason: {3}. Edited by: {4} BanRemoved = Ban removed: {0} NoBanFound = No active ban found for SteamID {0}. NoBans = No bans recorded. NoActiveBans = No active bans found. NoBanTimes = No ban times found. NoBanReasons = No ban reasons found. BanAlreadyActive = There is already an active ban for SteamID {0}. BanMenuTitlePlayers = Select player to ban BanMenuTitleTimes = Select ban time for {0} BanMenuTitleReasons = Select ban reason for {0} EditBanMenuTitleBans = Select active ban EditBanMenuTitleTimes = Select new ban time for {0} EditBanMenuTitleReasons = Select new ban reason for {0} EditBanMenuEntry = {0}. {1} ({2}) EditBanCustomReasonHint = Custom reason: Use css_editban {0} {1} <your reason> EditBanUseCommand = Custom reason via command BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} minutes) BanMenuReasonEntry = {0} BanMenuNoPlayers = No players found. BanJoinDenied = You are banned. Reason: {0}. Please contact {1} BanContact = ddbmaster.de BanStatusActive = Active BanStatusRemoved = Unbanned BanStatusExpired = Expired BanDeletedBy = removed by {0} BanEditedBy = edited by {0} BanListEntry = {0} | {1} | {2} | {3} | {4} BanDurationPermanent = Permanent BanDurationMinutes = {0} minutes KickDenied = You cannot kick this player: target has equal or higher immunity. KickDone = Player {0} has been kicked. Usage_Kick = Usage: css_kick or css_kick <steamid64> KickMenuTitlePlayers = Select player to kick KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = No players found. SlayDenied = You cannot slay this player: target has equal or higher immunity. SlayDone = Player {0} has been slain. TargetNotFound = Target player not found. Usage_Slay = Usage: css_slay or css_slay <steamid64> SlayMenuTitlePlayers = Select player to slay SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = No players found. Usage_Map = Usage: css_map or css_map <mapname|workshopid> Usage_WsMap = Usage: css_wsmap or css_wsmap <mapname|workshopid> MapChanged = {0} changed the map to {1}. MapNotFound = Map or workshop map ""{0}"" not found. NoMaps = No maps found in changemap.txt. MapListHeader = Available maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Standard Map Menu MapMenuTitleWorkshop = Workshop Map Menu MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = No standard maps found. MapMenuNoWorkshopMaps = No workshop maps found. Usage_ListBanTimes = Usage: css_listbantimes BanTimeHeader = Available ban times: BanTimeEntry = {0} = {1} minutes Usage_ListBanReasons = Usage: css_listbanreasons BanReasonHeader = Available ban reasons: BanReasonEntry = {0} = {1} Usage_AdminMenu = Usage: css_adminmenu AdminMenuTitle = BZ Admin Menu AdminMenuBan = Ban Menu AdminMenuEditBan = Edit Ban AdminMenuKick = Kick Menu AdminMenuSlay = Slay Menu AdminMenuMap = Standard Map Menu AdminMenuWsMap = Workshop Map Menu AdminMenuListMaps = Show map list AdminMenuListBans = Show ban list AdminMenuListBanTimes = Show ban times AdminMenuListBanReasons = Show ban reasons MenuMain = Main menu ConfigsReloaded = BZ_Admin configurations reloaded. ReloadBanReasonsDone = Ban reasons reloaded: {0} ReloadBanTimesDone = Ban times reloaded: {0} ReloadGroupsDone = Groups reloaded: {0} LanguageLoaded = Language loaded: {0} InvalidSteamId64 = Invalid SteamID64. InvalidImmunity = Invalid immunity value. InvalidBanMinutes = Invalid ban duration in minutes. PlayerOnlyCommand = This command must be executed by a player. BanDenied = You cannot ban this player: target has equal or higher immunity. NoPermission = You do not have permission for this command. -
ist Komplett
C#: BZ_Admin.csproj
Alles anzeigen<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <AssemblyName>BZ_Admin</AssemblyName> <RootNamespace>BZ_Admin</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <DebugType>none</DebugType> <DebugSymbols>false</DebugSymbols> </PropertyGroup> <ItemGroup> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.363"> <PrivateAssets>none</PrivateAssets> <ExcludeAssets>runtime</ExcludeAssets> <IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="MySqlConnector" Version="2.4.0" /> </ItemGroup> <ItemGroup> <None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> </Project>C#
Alles anzeigenusing CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Menu; using Microsoft.Extensions.Logging; using MySqlConnector; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; namespace BZ_Admin { [MinimumApiVersion(80)] public class BZ_AdminPlugin : BasePlugin { public override string ModuleName => "BZ_Admin"; public override string ModuleVersion => "1.6.4"; public override string ModuleAuthor => "ddbmaster"; public override string ModuleDescription => "Admin, Gruppen, Bans und Mapwechsel für CS2 mit MySQL."; private readonly Dictionary<string, GroupInfo> _groups = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, AdminInfo> _admins = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _lang = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, MapEntry> _maps = new(StringComparer.OrdinalIgnoreCase); private readonly List<BanTimeEntry> _banTimes = new(); private readonly List<BanReasonEntry> _banReasons = new(); private readonly List<BanInfo> _bans = new(); private string CurrentLanguage = "de"; private DatabaseConfig _databaseConfig = new(); private string PluginDir => ModuleDirectory; private string LangDir => Path.Combine(PluginDir, "lang"); private string LangDeFilePath => Path.Combine(LangDir, "de.txt"); private string LangEnFilePath => Path.Combine(LangDir, "en.txt"); private string ConfigBaseDir => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", ModuleName); private string DatabaseConfigFilePath => Path.Combine(ConfigBaseDir, "database.json"); private string LegacyAdminsFilePath => Path.Combine(ConfigBaseDir, "admins.txt"); private string LegacyGroupsFilePath => Path.Combine(ConfigBaseDir, "gruppen.txt"); private string LegacyBansFilePath => Path.Combine(ConfigBaseDir, "ban-liste.txt"); private string LegacyBanTimesFilePath => Path.Combine(ConfigBaseDir, "ban-zeiten.txt"); private string LegacyBanReasonsFilePath => Path.Combine(ConfigBaseDir, "ban-gruende.txt"); private string ChangeMapFilePath => Path.Combine(ConfigBaseDir, "changemap.txt"); public override void Load(bool hotReload) { Directory.CreateDirectory(PluginDir); Directory.CreateDirectory(LangDir); Directory.CreateDirectory(ConfigBaseDir); EnsureDefaultFiles(); LoadDatabaseConfig(); InitializeDatabase(); MigrateDatabase(); SeedOrMigrateDatabaseIfNeeded(); LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); ExpireElapsedBans(); RegisterListener<Listeners.OnClientPutInServer>(OnClientPutInServer); Logger.LogInformation($"[BZ_Admin] Loaded (Lang={CurrentLanguage})"); Logger.LogInformation($"[BZ_Admin] PluginDir: {PluginDir}"); Logger.LogInformation($"[BZ_Admin] ConfigDir: {ConfigBaseDir}"); Logger.LogInformation($"[BZ_Admin] LanguageDir: {LangDir}"); Logger.LogInformation($"[BZ_Admin] DatabaseConfig: {DatabaseConfigFilePath}"); Logger.LogInformation($"[BZ_Admin] MySQL: {_databaseConfig.Host}:{_databaseConfig.Port}/{_databaseConfig.Database}"); } private void EnsureDefaultFiles() { if (!File.Exists(ChangeMapFilePath)) { File.WriteAllLines(ChangeMapFilePath, new[] { "# Standard Maps", "de_dust2", "de_mirage", "de_inferno", "de_nuke", "de_train", "de_anubis", "de_overpass", "", "# Workshop Maps", "# Format: workshopid:mapname", "3671053668:de_rats_teatre", "3075012302:de_blackgold_d_prefab", "3070219079:de_santo", "3328169568:de_edin", "" }); } if (!File.Exists(LangDeFilePath)) File.WriteAllText(LangDeFilePath, GetDefaultGermanLanguageFile()); if (!File.Exists(LangEnFilePath)) File.WriteAllText(LangEnFilePath, GetDefaultEnglishLanguageFile()); if (!File.Exists(DatabaseConfigFilePath)) { var defaultConfig = new DatabaseConfig { Host = "127.0.0.1", Port = 3306, Database = "bz_admin", Username = "bz_admin", Password = "DEIN_PASSWORT", Charset = "utf8mb4" }; File.WriteAllText( DatabaseConfigFilePath, JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true }) ); } } private void LoadDatabaseConfig() { if (!File.Exists(DatabaseConfigFilePath)) throw new FileNotFoundException($"MySQL config not found: {DatabaseConfigFilePath}"); string json = File.ReadAllText(DatabaseConfigFilePath); _databaseConfig = JsonSerializer.Deserialize<DatabaseConfig>(json) ?? new DatabaseConfig(); if (string.IsNullOrWhiteSpace(_databaseConfig.Host) || string.IsNullOrWhiteSpace(_databaseConfig.Database) || string.IsNullOrWhiteSpace(_databaseConfig.Username)) { throw new InvalidOperationException($"Invalid MySQL config: {DatabaseConfigFilePath}"); } } private string BuildConnectionString() { var builder = new MySqlConnectionStringBuilder { Server = _databaseConfig.Host, Port = (uint)_databaseConfig.Port, Database = _databaseConfig.Database, UserID = _databaseConfig.Username, Password = _databaseConfig.Password, CharacterSet = string.IsNullOrWhiteSpace(_databaseConfig.Charset) ? "utf8mb4" : _databaseConfig.Charset, SslMode = MySqlSslMode.Preferred, AllowUserVariables = true }; return builder.ConnectionString; } private MySqlConnection GetConnection() { var connection = new MySqlConnection(BuildConnectionString()); connection.Open(); return connection; } private string GetDefaultGermanLanguageFile() { return @"Usage_AddGroup = Benutzung: css_addgroup <#gruppenname> <flag1,flag2,...> Usage_DelGroup = Benutzung: css_delgroup <#gruppenname> GroupAdded = Gruppe {0} mit Flags hinzugefügt: {1} GroupExists = Gruppe {0} existiert bereits. GroupDeleted = Gruppe {0} wurde gelöscht. GroupNotExist = Gruppe {0} existiert nicht. NoGroups = Keine Gruppen vorhanden. Usage_AddAdmin = Benutzung: css_addadmin <name> <steamid64> <#gruppe> <immunitaet> Usage_DelAdmin = Benutzung: css_deladmin <steamid64> Usage_SetGroup = Benutzung: css_setgroup <steamid64> <#gruppe> Usage_SetImmunity = Benutzung: css_setimmunity <steamid64> <wert> Usage_SetAdminName = Benutzung: css_setadminname <steamid64> <name> AdminAdded = Admin {0} hinzugefügt zur Gruppe {1} mit Immunität {2}. AdminRemoved = Admin {0} wurde entfernt. AdminNotFound = Kein Admin mit SteamID {0} gefunden. AdminUpdatedGroup = Admin {0} wurde Gruppe {1} zugewiesen. AdminUpdatedImmunity = Admin {0} hat jetzt Immunität {1}. AdminUpdatedName = Admin {0} wurde umbenannt in {1}. NoAdmins = Keine Admins vorhanden. AdminSelfGroup = {0} Du bist jetzt {1} AdminSelfGroupCss = {0} Du bist jetzt CSS-Admin Usage_Ban = Benutzung: css_ban oder css_ban <steamid64> <minuten> <grund> Usage_BanId = Benutzung: css_banid <steamid64> <minuten> <grund> Usage_Unban = Benutzung: css_unban <steamid64> Usage_EditBan = Benutzung: css_editban <steamid64> <minuten> <grund> Usage_ReloadBanReasons = Benutzung: css_reloadbanreasons Usage_ReloadBanTimes = Benutzung: css_reloadbantimes Usage_ReloadGroups = Benutzung: css_reloadgroups BanAdded = Spieler {0} ({1}) wurde gebannt. Dauer: {2}. Grund: {3} von {4} BanEdited = Ban für Spieler {0} ({1}) wurde geändert. Neue Dauer: {2}. Neuer Grund: {3}. Bearbeitet von: {4} BanRemoved = Ban entfernt: {0} NoBanFound = Kein aktiver Ban für SteamID {0} gefunden. NoBans = Keine Bans vorhanden. NoActiveBans = Keine aktiven Bans vorhanden. NoBanTimes = Keine Ban-Zeiten vorhanden. NoBanReasons = Keine Ban-Gründe vorhanden. BanAlreadyActive = Für SteamID {0} existiert bereits ein aktiver Ban. BanMenuTitlePlayers = Spieler für Ban auswählen BanMenuTitleTimes = Ban-Zeit für {0} auswählen BanMenuTitleReasons = Ban-Grund für {0} auswählen EditBanMenuTitleBans = Aktiven Ban auswählen EditBanMenuTitleTimes = Neue Ban-Zeit für {0} auswählen EditBanMenuTitleReasons = Neuen Ban-Grund für {0} auswählen EditBanMenuEntry = {0}. {1} ({2}) EditBanCustomReasonHint = Eigener Grund: Benutze css_editban {0} {1} <dein grund> EditBanUseCommand = Eigenen Grund per Befehl BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} Minuten) BanMenuReasonEntry = {0} BanMenuNoPlayers = Keine Spieler gefunden. BanJoinDenied = Du bist gebannt. Grund: {0}. Bitte melde dich bei {1} BanContact = ddbmaster.de BanStatusActive = Aktiv BanStatusRemoved = Entbannt BanStatusExpired = Abgelaufen BanDeletedBy = entfernt von {0} BanEditedBy = bearbeitet von {0} BanListEntry = {0} | {1} | {2} | {3} | {4} BanDurationPermanent = Permanent BanDurationMinutes = {0} Minuten KickDenied = Du kannst diesen Spieler nicht kicken: Ziel hat gleiche oder höhere Immunität. KickDone = Spieler {0} wurde gekickt. Usage_Kick = Benutzung: css_kick oder css_kick <steamid64> KickMenuTitlePlayers = Spieler für Kick auswählen KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = Keine Spieler gefunden. SlayDenied = Du kannst diesen Spieler nicht töten: Ziel hat gleiche oder höhere Immunität. SlayDone = Spieler {0} wurde getötet. TargetNotFound = Zielspieler nicht gefunden. Usage_Slay = Benutzung: css_slay oder css_slay <steamid64> SlayMenuTitlePlayers = Spieler für Slay auswählen SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = Keine Spieler gefunden. Usage_Map = Benutzung: css_map oder css_map <mapname|workshopid> Usage_WsMap = Benutzung: css_wsmap oder css_wsmap <mapname|workshopid> MapChanged = {0} wechselt die Map nach {1}. MapNotFound = Die Map oder Workshop-Map ""{0}"" wurde nicht gefunden. NoMaps = Keine Maps in changemap.txt vorhanden. MapListHeader = Verfügbare Maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Standard Map Menü MapMenuTitleWorkshop = Workshop Map Menü MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = Keine Standard-Maps gefunden. MapMenuNoWorkshopMaps = Keine Workshop-Maps gefunden. Usage_ListBanTimes = Benutzung: css_listbantimes BanTimeHeader = Verfügbare Ban-Zeiten: BanTimeEntry = {0} = {1} Minuten Usage_ListBanReasons = Benutzung: css_listbanreasons BanReasonHeader = Verfügbare Ban-Gründe: BanReasonEntry = {0} = {1} Usage_AdminMenu = Benutzung: css_adminmenu AdminMenuTitle = BZ Admin Menü AdminMenuBan = Ban Menü AdminMenuEditBan = Ban bearbeiten AdminMenuKick = Kick Menü AdminMenuSlay = Slay Menü AdminMenuMap = Standard Map Menü AdminMenuWsMap = Workshop Map Menü AdminMenuListMaps = Mapliste anzeigen AdminMenuListBans = Bannliste anzeigen AdminMenuListBanTimes = Ban-Zeiten anzeigen AdminMenuListBanReasons = Ban-Gründe anzeigen MenuMain = Hauptmenü ConfigsReloaded = BZ_Admin Konfigurationen wurden neu geladen. ReloadBanReasonsDone = Ban-Gründe wurden neu geladen: {0} ReloadBanTimesDone = Ban-Zeiten wurden neu geladen: {0} ReloadGroupsDone = Gruppen wurden neu geladen: {0} LanguageLoaded = Sprache geladen: {0} InvalidSteamId64 = Ungültige SteamID64. InvalidImmunity = Ungültiger Immunitätswert. InvalidBanMinutes = Ungültige Ban-Dauer in Minuten. PlayerOnlyCommand = Dieser Befehl muss von einem Spieler ausgeführt werden. BanDenied = Du kannst diesen Spieler nicht bannen: Ziel hat gleiche oder höhere Immunität. NoPermission = Du hast keine Berechtigung für diesen Befehl."; } private string GetDefaultEnglishLanguageFile() { return @"Usage_AddGroup = Usage: css_addgroup <#groupname> <flag1,flag2,...> Usage_DelGroup = Usage: css_delgroup <#groupname> GroupAdded = Group {0} added with flags: {1} GroupExists = Group {0} already exists. GroupDeleted = Group {0} has been deleted. GroupNotExist = Group {0} does not exist. NoGroups = No groups defined. Usage_AddAdmin = Usage: css_addadmin <name> <steamid64> <#group> <immunity> Usage_DelAdmin = Usage: css_deladmin <steamid64> Usage_SetGroup = Usage: css_setgroup <steamid64> <#group> Usage_SetImmunity = Usage: css_setimmunity <steamid64> <value> Usage_SetAdminName = Usage: css_setadminname <steamid64> <name> AdminAdded = Admin {0} added to {1} with immunity {2}. AdminRemoved = Admin {0} has been removed. AdminNotFound = Admin with SteamID {0} not found. AdminUpdatedGroup = Admin {0} has been assigned to group {1}. AdminUpdatedImmunity = Admin {0} now has immunity {1}. AdminUpdatedName = Admin {0} has been renamed to {1}. NoAdmins = No admins defined. AdminSelfGroup = {0} You are now {1} AdminSelfGroupCss = {0} You are now a CSS admin Usage_Ban = Usage: css_ban or css_ban <steamid64> <minutes> <reason> Usage_BanId = Usage: css_banid <steamid64> <minutes> <reason> Usage_Unban = Usage: css_unban <steamid64> Usage_EditBan = Usage: css_editban <steamid64> <minutes> <reason> Usage_ReloadBanReasons = Usage: css_reloadbanreasons Usage_ReloadBanTimes = Usage: css_reloadbantimes Usage_ReloadGroups = Usage: css_reloadgroups BanAdded = Player {0} ({1}) has been banned. Duration: {2}. Reason: {3} by {4} BanEdited = Ban for player {0} ({1}) has been changed. New duration: {2}. New reason: {3}. Edited by: {4} BanRemoved = Ban removed: {0} NoBanFound = No active ban found for SteamID {0}. NoBans = No bans recorded. NoActiveBans = No active bans found. NoBanTimes = No ban times found. NoBanReasons = No ban reasons found. BanAlreadyActive = There is already an active ban for SteamID {0}. BanMenuTitlePlayers = Select player to ban BanMenuTitleTimes = Select ban time for {0} BanMenuTitleReasons = Select ban reason for {0} EditBanMenuTitleBans = Select active ban EditBanMenuTitleTimes = Select new ban time for {0} EditBanMenuTitleReasons = Select new ban reason for {0} EditBanMenuEntry = {0}. {1} ({2}) EditBanCustomReasonHint = Custom reason: Use css_editban {0} {1} <your reason> EditBanUseCommand = Custom reason via command BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} minutes) BanMenuReasonEntry = {0} BanMenuNoPlayers = No players found. BanJoinDenied = You are banned. Reason: {0}. Please contact {1} BanContact = ddbmaster.de BanStatusActive = Active BanStatusRemoved = Unbanned BanStatusExpired = Expired BanDeletedBy = removed by {0} BanEditedBy = edited by {0} BanListEntry = {0} | {1} | {2} | {3} | {4} BanDurationPermanent = Permanent BanDurationMinutes = {0} minutes KickDenied = You cannot kick this player: target has equal or higher immunity. KickDone = Player {0} has been kicked. Usage_Kick = Usage: css_kick or css_kick <steamid64> KickMenuTitlePlayers = Select player to kick KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = No players found. SlayDenied = You cannot slay this player: target has equal or higher immunity. SlayDone = Player {0} has been slain. TargetNotFound = Target player not found. Usage_Slay = Usage: css_slay or css_slay <steamid64> SlayMenuTitlePlayers = Select player to slay SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = No players found. Usage_Map = Usage: css_map or css_map <mapname|workshopid> Usage_WsMap = Usage: css_wsmap or css_wsmap <mapname|workshopid> MapChanged = {0} changed the map to {1}. MapNotFound = Map or workshop map ""{0}"" not found. NoMaps = No maps found in changemap.txt. MapListHeader = Available maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Standard Map Menu MapMenuTitleWorkshop = Workshop Map Menu MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = No standard maps found. MapMenuNoWorkshopMaps = No workshop maps found. Usage_ListBanTimes = Usage: css_listbantimes BanTimeHeader = Available ban times: BanTimeEntry = {0} = {1} minutes Usage_ListBanReasons = Usage: css_listbanreasons BanReasonHeader = Available ban reasons: BanReasonEntry = {0} = {1} Usage_AdminMenu = Usage: css_adminmenu AdminMenuTitle = BZ Admin Menu AdminMenuBan = Ban Menu AdminMenuEditBan = Edit Ban AdminMenuKick = Kick Menu AdminMenuSlay = Slay Menu AdminMenuMap = Standard Map Menu AdminMenuWsMap = Workshop Map Menu AdminMenuListMaps = Show map list AdminMenuListBans = Show ban list AdminMenuListBanTimes = Show ban times AdminMenuListBanReasons = Show ban reasons MenuMain = Main menu ConfigsReloaded = BZ_Admin configurations reloaded. ReloadBanReasonsDone = Ban reasons reloaded: {0} ReloadBanTimesDone = Ban times reloaded: {0} ReloadGroupsDone = Groups reloaded: {0} LanguageLoaded = Language loaded: {0} InvalidSteamId64 = Invalid SteamID64. InvalidImmunity = Invalid immunity value. InvalidBanMinutes = Invalid ban duration in minutes. PlayerOnlyCommand = This command must be executed by a player. BanDenied = You cannot ban this player: target has equal or higher immunity. NoPermission = You do not have permission for this command."; } private void LoadLanguage(string langCode) { string langFile = Path.Combine(LangDir, $"{langCode}.txt"); _lang.Clear(); Logger.LogInformation($"[BZ_Admin] Lade Sprache '{langCode}' aus: {langFile}"); if (!File.Exists(langFile)) { Logger.LogError($"[BZ_Admin] Sprachdatei NICHT gefunden: {langFile}"); return; } foreach (var rawLine in File.ReadAllLines(langFile)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split('=', 2); if (parts.Length == 2) { string key = parts[0].Trim(); string value = parts[1].Trim(); _lang[key] = value; } } Logger.LogInformation($"[BZ_Admin] Sprachkeys geladen: {_lang.Count}"); } private string T(string key, params object[] args) { if (_lang.TryGetValue(key, out var fmt)) return args.Length > 0 ? string.Format(fmt, args) : fmt; return key; } private string FormatBanDuration(int minutes) { return minutes <= 0 ? T("BanDurationPermanent") : T("BanDurationMinutes", minutes); } private void OnClientPutInServer(int slot) { var player = Utilities.GetPlayerFromSlot(slot); if (player == null || !player.IsValid || player.IsBot) return; string steamId = player.SteamID.ToString(); var activeBan = GetEffectiveActiveBanBySteamId(steamId); if (activeBan != null) { Server.NextFrame(() => { if (player == null || !player.IsValid) return; string kickMessage = T("BanJoinDenied", activeBan.Reason, T("BanContact")); string escaped = EscapeServerCommandArgument(kickMessage); Server.ExecuteCommand($"kickid {steamId} \"{escaped}\""); }); return; } if (_admins.TryGetValue(steamId, out var localAdmin)) { Server.NextFrame(() => { if (player == null || !player.IsValid) return; player.PrintToChat($" {T("AdminSelfGroup", localAdmin.Name, localAdmin.GroupName)}"); }); return; } if (AdminManager.PlayerHasPermissions(player, "@css/root") || AdminManager.GetPlayerImmunity(player) > 0) { Server.NextFrame(() => { if (player == null || !player.IsValid) return; player.PrintToChat($" {T("AdminSelfGroupCss", player.PlayerName)}"); }); } } private void InitializeDatabase() { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" CREATE TABLE IF NOT EXISTS bz_groups ( group_name VARCHAR(191) NOT NULL, flags TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (group_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS bz_admins ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, steamid64 VARCHAR(32) NOT NULL, group_name VARCHAR(191) NOT NULL, immunity INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_bz_admins_steamid64 (steamid64), KEY idx_bz_admins_group_name (group_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS bz_bans ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, steamid64 VARCHAR(32) NOT NULL, duration_minutes INT NOT NULL, reason VARCHAR(255) NOT NULL, admin_name VARCHAR(255) NOT NULL, admin_steamid64 VARCHAR(32) NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, is_active TINYINT(1) NOT NULL DEFAULT 1, deleted_at DATETIME NULL, deleted_by VARCHAR(255) NULL, edited_by VARCHAR(255) NULL, edited_by_steamid64 VARCHAR(32) NULL, edited_at DATETIME NULL, source VARCHAR(32) NOT NULL DEFAULT 'server', PRIMARY KEY (id), KEY idx_bz_bans_steamid64 (steamid64), KEY idx_bz_bans_active (steamid64, is_active), KEY idx_bz_bans_updated_at (updated_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS bz_ban_times ( display_name VARCHAR(191) NOT NULL, minutes INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (display_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS bz_ban_reasons ( display_name VARCHAR(191) NOT NULL, reason VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (display_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "; command.ExecuteNonQuery(); } private void MigrateDatabase() { using var connection = GetConnection(); EnsureColumnExists(connection, "bz_bans", "admin_steamid64", "ALTER TABLE bz_bans ADD COLUMN admin_steamid64 VARCHAR(32) NULL AFTER admin_name;"); EnsureColumnExists(connection, "bz_bans", "edited_by", "ALTER TABLE bz_bans ADD COLUMN edited_by VARCHAR(255) NULL AFTER deleted_by;"); EnsureColumnExists(connection, "bz_bans", "edited_by_steamid64", "ALTER TABLE bz_bans ADD COLUMN edited_by_steamid64 VARCHAR(32) NULL AFTER edited_by;"); EnsureColumnExists(connection, "bz_bans", "edited_at", "ALTER TABLE bz_bans ADD COLUMN edited_at DATETIME NULL AFTER edited_by_steamid64;"); EnsureColumnExists(connection, "bz_bans", "source", "ALTER TABLE bz_bans ADD COLUMN source VARCHAR(32) NOT NULL DEFAULT 'server' AFTER edited_at;"); } private void EnsureColumnExists(MySqlConnection connection, string tableName, string columnName, string alterSql) { using var checkCommand = connection.CreateCommand(); checkCommand.CommandText = @" SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND COLUMN_NAME = @column;"; checkCommand.Parameters.AddWithValue("@schema", _databaseConfig.Database); checkCommand.Parameters.AddWithValue("@table", tableName); checkCommand.Parameters.AddWithValue("@column", columnName); int count = Convert.ToInt32(checkCommand.ExecuteScalar()); if (count > 0) return; using var alterCommand = connection.CreateCommand(); alterCommand.CommandText = alterSql; alterCommand.ExecuteNonQuery(); Logger.LogInformation($"[BZ_Admin] Migration ausgeführt: Spalte '{columnName}' zu Tabelle '{tableName}' hinzugefügt."); } private string NormalizeDate(string input) { if (DateTime.TryParseExact(input, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); if (DateTime.TryParseExact(input, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var d)) return d.ToString("yyyy-MM-dd 00:00:00", CultureInfo.InvariantCulture); return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); } private void SeedOrMigrateDatabaseIfNeeded() { using var connection = GetConnection(); SeedOrMigrateGroups(connection); SeedOrMigrateAdmins(connection); SeedOrMigrateBanTimes(connection); SeedOrMigrateBanReasons(connection); SeedOrMigrateBans(connection); } private void SeedOrMigrateGroups(MySqlConnection connection) { if (GetTableCount(connection, "bz_groups") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyGroupsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyGroupsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var groupName = parts[0].Trim(); var flagsPart = parts[1].Trim(); if (!groupName.StartsWith("#")) continue; InsertGroup(connection, groupName, flagsPart, transaction); imported = true; } } if (!imported) { InsertGroup(connection, "#root", "@css/root", transaction); InsertGroup(connection, "#coadmin", "@css/ban,@css/unban,@css/kick,@css/slay,@css/changemap,@css/listmaps,@css/reloadconfigs,@css/adminmenu", transaction); InsertGroup(connection, "#moderator", "@css/kick,@css/slay,@css/adminmenu", transaction); InsertGroup(connection, "#supporter", "@css/listmaps", transaction); } transaction.Commit(); } private void SeedOrMigrateAdmins(MySqlConnection connection) { if (GetTableCount(connection, "bz_admins") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyAdminsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyAdminsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 6) continue; if (!int.TryParse(parts[0], out var id)) continue; if (!int.TryParse(parts[4], out var immunity)) continue; InsertAdmin(connection, new AdminInfo { Id = id, Name = parts[1], SteamId = parts[2], GroupName = parts[3], Immunity = immunity, CreatedAt = NormalizeDate(parts[5]), UpdatedAt = NormalizeDate(parts[5]) }, transaction); imported = true; } } if (!imported) { InsertAdmin(connection, new AdminInfo { Id = 1, Name = "ddbmaster", SteamId = "76561197960398110", GroupName = "#root", Immunity = 100, CreatedAt = "2025-10-29 00:00:00", UpdatedAt = "2025-10-29 00:00:00" }, transaction); InsertAdmin(connection, new AdminInfo { Id = 2, Name = "Nixdorf", SteamId = "76561197965022135", GroupName = "#coadmin", Immunity = 50, CreatedAt = "2025-10-29 00:00:00", UpdatedAt = "2025-10-29 00:00:00" }, transaction); } transaction.Commit(); } private void SeedOrMigrateBanTimes(MySqlConnection connection) { if (GetTableCount(connection, "bz_ban_times") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyBanTimesFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBanTimesFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; if (!int.TryParse(parts[1], out var minutes)) continue; InsertBanTime(connection, parts[0].Trim(), minutes, transaction); imported = true; } } if (!imported) { InsertBanTime(connection, "Permanent", 0, transaction); InsertBanTime(connection, "30 Minuten", 30, transaction); InsertBanTime(connection, "1 Stunde", 60, transaction); InsertBanTime(connection, "6 Stunden", 360, transaction); InsertBanTime(connection, "12 Stunden", 720, transaction); InsertBanTime(connection, "1 Tag", 1440, transaction); InsertBanTime(connection, "3 Tage", 4320, transaction); InsertBanTime(connection, "1 Woche", 10080, transaction); InsertBanTime(connection, "2 Wochen", 20160, transaction); } transaction.Commit(); } private void SeedOrMigrateBanReasons(MySqlConnection connection) { if (GetTableCount(connection, "bz_ban_reasons") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyBanReasonsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBanReasonsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; InsertBanReason(connection, parts[0].Trim(), parts[1].Trim(), transaction); imported = true; } } if (!imported) { InsertBanReason(connection, "Cheating", "Cheating", transaction); InsertBanReason(connection, "Beleidigung", "Beleidigung", transaction); InsertBanReason(connection, "Teamkill", "Teamkill", transaction); InsertBanReason(connection, "Griefing", "Griefing", transaction); InsertBanReason(connection, "AFK", "AFK", transaction); InsertBanReason(connection, "Spam", "Spam", transaction); InsertBanReason(connection, "Sonstiges", "Sonstiges", transaction); } transaction.Commit(); } private void SeedOrMigrateBans(MySqlConnection connection) { if (GetTableCount(connection, "bz_bans") > 0) return; using var transaction = connection.BeginTransaction(); if (File.Exists(LegacyBansFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBansFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 7) continue; if (!int.TryParse(parts[0], out var id)) continue; if (!int.TryParse(parts[3], out var minutes)) continue; string createdAt; string? adminSteamId = null; if (parts.Length >= 8) { adminSteamId = string.IsNullOrWhiteSpace(parts[6]) ? null : parts[6]; createdAt = NormalizeDate(parts[7]); } else { createdAt = NormalizeDate(parts[6]); } InsertBan(connection, new BanInfo { Id = id, Name = parts[1], SteamId = parts[2], DurationMinutes = minutes, Reason = parts[4], AdminName = parts[5], AdminSteamId = adminSteamId, CreatedAt = createdAt, UpdatedAt = createdAt, IsActive = true, DeletedAt = null, DeletedBy = null, EditedBy = null, EditedBySteamId = null, EditedAt = null, Source = "legacy" }, transaction); } } transaction.Commit(); } private int GetTableCount(MySqlConnection connection, string tableName) { using var command = connection.CreateCommand(); command.CommandText = $"SELECT COUNT(*) FROM {tableName};"; return Convert.ToInt32(command.ExecuteScalar()); } private void LoadGroups() { _groups.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT group_name, flags FROM bz_groups ORDER BY group_name ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { var groupName = reader.GetString(0); var flagsPart = reader.GetString(1); var flags = flagsPart .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToList(); _groups[groupName] = new GroupInfo { GroupName = groupName, Flags = flags }; } Logger.LogInformation($"[BZ_Admin] Loaded groups: {_groups.Count}"); } private void LoadAdmins() { _admins.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT id, name, steamid64, group_name, immunity, created_at, updated_at FROM bz_admins ORDER BY id ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { var admin = new AdminInfo { Id = reader.GetInt32(0), Name = reader.GetString(1), SteamId = reader.GetString(2), GroupName = reader.GetString(3), Immunity = reader.GetInt32(4), CreatedAt = reader.GetDateTime(5).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), UpdatedAt = reader.GetDateTime(6).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) }; _admins[admin.SteamId] = admin; } } private void LoadBans() { _bans.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT id, name, steamid64, duration_minutes, reason, admin_name, admin_steamid64, created_at, updated_at, is_active, deleted_at, deleted_by, edited_by, edited_by_steamid64, edited_at, source FROM bz_bans ORDER BY id ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _bans.Add(new BanInfo { Id = reader.GetInt32(0), Name = reader.GetString(1), SteamId = reader.GetString(2), DurationMinutes = reader.GetInt32(3), Reason = reader.GetString(4), AdminName = reader.GetString(5), AdminSteamId = reader.IsDBNull(6) ? null : reader.GetString(6), CreatedAt = reader.GetDateTime(7).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), UpdatedAt = reader.GetDateTime(8).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), IsActive = reader.GetBoolean(9), DeletedAt = reader.IsDBNull(10) ? null : reader.GetDateTime(10).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), DeletedBy = reader.IsDBNull(11) ? null : reader.GetString(11), EditedBy = reader.IsDBNull(12) ? null : reader.GetString(12), EditedBySteamId = reader.IsDBNull(13) ? null : reader.GetString(13), EditedAt = reader.IsDBNull(14) ? null : reader.GetDateTime(14).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), Source = reader.IsDBNull(15) ? "server" : reader.GetString(15) }); } } private void LoadBanTimes() { _banTimes.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT display_name, minutes FROM bz_ban_times ORDER BY minutes ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _banTimes.Add(new BanTimeEntry { DisplayName = reader.GetString(0), Minutes = reader.GetInt32(1) }); } } private void LoadBanReasons() { _banReasons.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT display_name, reason FROM bz_ban_reasons ORDER BY display_name ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _banReasons.Add(new BanReasonEntry { DisplayName = reader.GetString(0), Reason = reader.GetString(1) }); } } private void LoadMaps() { _maps.Clear(); if (!File.Exists(ChangeMapFilePath)) { Logger.LogWarning($"[BZ_Admin] changemap.txt nicht gefunden: {ChangeMapFilePath}"); return; } foreach (var rawLine in File.ReadAllLines(ChangeMapFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; int colonIndex = line.IndexOf(':'); if (colonIndex > 0) { string workshopId = line.Substring(0, colonIndex).Trim(); string mapName = line.Substring(colonIndex + 1).Trim().ToLowerInvariant(); if (!ulong.TryParse(workshopId, out _)) { Logger.LogWarning($"[BZ_Admin] Ungültige Workshop-Zeile in changemap.txt: {line}"); continue; } if (string.IsNullOrWhiteSpace(mapName)) { Logger.LogWarning($"[BZ_Admin] Leerer Workshop-Mapname in changemap.txt: {line}"); continue; } var entry = new MapEntry { IsWorkshop = true, WorkshopId = workshopId, MapName = mapName }; _maps[workshopId] = entry; _maps[mapName] = entry; Logger.LogInformation($"[BZ_Admin] Workshop-Map geladen: {workshopId} -> {mapName}"); } else { string mapName = line.ToLowerInvariant(); if (string.IsNullOrWhiteSpace(mapName)) continue; _maps[mapName] = new MapEntry { IsWorkshop = false, MapName = mapName }; Logger.LogInformation($"[BZ_Admin] Standard-Map geladen: {mapName}"); } } int standardCount = _maps.Values .Where(m => !m.IsWorkshop) .Select(m => m.MapName) .Distinct(StringComparer.OrdinalIgnoreCase) .Count(); int workshopCount = _maps.Values .Where(m => m.IsWorkshop) .Select(m => m.WorkshopId) .Distinct(StringComparer.OrdinalIgnoreCase) .Count(); Logger.LogInformation($"[BZ_Admin] Maps geladen: Standard={standardCount}, Workshop={workshopCount}"); } private AdminInfo? GetAdminInfo(string steamId) { return _admins.TryGetValue(steamId, out var admin) ? admin : null; } private string GetConfiguredAdminName(CCSPlayerController? player) { if (player == null) return "Console"; var steamId = player.SteamID.ToString(); var admin = GetAdminInfo(steamId); return admin?.Name ?? player.PlayerName; } private bool HasPermission(CCSPlayerController? player, string permission) { if (player == null) return true; if (AdminManager.PlayerHasPermissions(player, "@css/root")) return true; if (AdminManager.PlayerHasPermissions(player, permission)) return true; var steamId = player.SteamID.ToString(); if (!_admins.TryGetValue(steamId, out var admin)) return false; if (!_groups.TryGetValue(admin.GroupName, out var group)) return false; if (group.Flags.Any(x => x.Equals("@css/root", StringComparison.OrdinalIgnoreCase))) return true; return group.Flags.Any(x => x.Equals(permission, StringComparison.OrdinalIgnoreCase)); } private int GetLocalImmunity(string steamId) { var admin = GetAdminInfo(steamId); return admin?.Immunity ?? 0; } private int GetEffectiveImmunity(CCSPlayerController? player) { if (player == null) return 0; int local = GetLocalImmunity(player.SteamID.ToString()); int css = (int)AdminManager.GetPlayerImmunity(player); return Math.Max(local, css); } private bool CanTarget(CCSPlayerController? executor, CCSPlayerController? target) { if (executor == null) return false; if (target == null) return true; bool cssCanTarget = AdminManager.CanPlayerTarget(executor, target); int execImmunity = GetEffectiveImmunity(executor); int targetImmunity = GetEffectiveImmunity(target); return cssCanTarget || execImmunity > targetImmunity; } private bool CanTarget(string executorId, string targetId) { if (!ulong.TryParse(executorId, out var executorSteamId)) return false; if (!ulong.TryParse(targetId, out var targetSteamId)) return false; var executorPlayer = Utilities.GetPlayerFromSteamId(executorSteamId); var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (executorPlayer != null) return CanTarget(executorPlayer, targetPlayer); var exec = GetAdminInfo(executorId); var targ = GetAdminInfo(targetId); if (exec == null) return false; if (targ == null) return true; return exec.Immunity > targ.Immunity; } private bool EnsurePermission(CCSPlayerController? player, CommandInfo commandInfo, string permission) { if (HasPermission(player, permission)) return true; commandInfo.ReplyToCommand(T("NoPermission")); return false; } [ConsoleCommand("css_adminmenu", "Open BZ admin menu")] public void CmdAdminMenu(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/adminmenu")) return; if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenAdminMenu(player); } [ConsoleCommand("css_reloadbanreasons", "Reload ban reasons from txt without duplicates")] public void CmdReloadBanReasons(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; int count = ReloadBanReasonsFromTxt(); LoadBanReasons(); commandInfo.ReplyToCommand(T("ReloadBanReasonsDone", count)); } [ConsoleCommand("css_reloadbantimes", "Reload ban times from txt without duplicates")] public void CmdReloadBanTimes(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; int count = ReloadBanTimesFromTxt(); LoadBanTimes(); commandInfo.ReplyToCommand(T("ReloadBanTimesDone", count)); } [ConsoleCommand("css_reloadgroups", "Reload groups from txt without duplicates")] public void CmdReloadGroups(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; int count = ReloadGroupsFromTxt(); LoadGroups(); commandInfo.ReplyToCommand(T("ReloadGroupsDone", count)); } private void OpenAdminMenu(CCSPlayerController admin) { var menu = new ChatMenu(T("AdminMenuTitle")); if (HasPermission(admin, "@css/ban")) { menu.AddMenuOption(T("AdminMenuBan"), (player, option) => OpenBanPlayerMenu(player)); menu.AddMenuOption(T("AdminMenuEditBan"), (player, option) => OpenEditBanMenu(player)); } if (HasPermission(admin, "@css/kick")) menu.AddMenuOption(T("AdminMenuKick"), (player, option) => OpenKickMenu(player)); if (HasPermission(admin, "@css/slay")) menu.AddMenuOption(T("AdminMenuSlay"), (player, option) => OpenSlayMenu(player)); if (HasPermission(admin, "@css/changemap")) { menu.AddMenuOption(T("AdminMenuMap"), (player, option) => OpenMapMenu(player, false)); menu.AddMenuOption(T("AdminMenuWsMap"), (player, option) => OpenMapMenu(player, true)); } if (HasPermission(admin, "@css/listmaps")) menu.AddMenuOption(T("AdminMenuListMaps"), (player, option) => RunListMapsToPlayer(player)); if (HasPermission(admin, "@css/ban")) { menu.AddMenuOption(T("AdminMenuListBans"), (player, option) => RunListBansToPlayer(player)); menu.AddMenuOption(T("AdminMenuListBanTimes"), (player, option) => RunListBanTimesToPlayer(player)); menu.AddMenuOption(T("AdminMenuListBanReasons"), (player, option) => RunListBanReasonsToPlayer(player)); } MenuManager.OpenChatMenu(admin, menu); } private void RunListMapsToPlayer(CCSPlayerController player) { if (_maps.Count == 0) { player.PrintToChat($" {T("NoMaps")}"); return; } player.PrintToChat($" {T("MapListHeader")}"); var printedStandard = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var printedWorkshop = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var kvp in _maps.OrderBy(x => x.Key)) { var map = kvp.Value; if (map.IsWorkshop) { if (printedWorkshop.Contains(map.WorkshopId)) continue; printedWorkshop.Add(map.WorkshopId); player.PrintToChat($" {T("MapEntryWorkshop", map.MapName, map.WorkshopId)}"); } else { if (printedStandard.Contains(map.MapName)) continue; printedStandard.Add(map.MapName); player.PrintToChat($" {T("MapEntryStandard", map.MapName)}"); } } } private void RunListBansToPlayer(CCSPlayerController player) { ExpireElapsedBans(); if (_bans.Count == 0) { player.PrintToChat($" {T("NoBans")}"); return; } foreach (var ban in _bans.OrderBy(x => x.Id)) player.PrintToChat($" {FormatBanListEntry(ban)}"); } private void RunListBanTimesToPlayer(CCSPlayerController player) { if (_banTimes.Count == 0) { player.PrintToChat($" {T("NoBanTimes")}"); return; } player.PrintToChat($" {T("BanTimeHeader")}"); foreach (var entry in _banTimes) player.PrintToChat($" {T("BanTimeEntry", entry.DisplayName, entry.Minutes)}"); } private void RunListBanReasonsToPlayer(CCSPlayerController player) { if (_banReasons.Count == 0) { player.PrintToChat($" {T("NoBanReasons")}"); return; } player.PrintToChat($" {T("BanReasonHeader")}"); foreach (var entry in _banReasons) player.PrintToChat($" {T("BanReasonEntry", entry.DisplayName, entry.Reason)}"); } [ConsoleCommand("css_ban", "Ban menu or direct ban")] public void CmdBan(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenBanPlayerMenu(player); return; } if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_Ban")); return; } if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } string executorName = GetConfiguredAdminName(player); string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (!CanTarget(player, targetPlayer)) { commandInfo.ReplyToCommand(T("BanDenied")); return; } if (HasActiveBan(targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanAlreadyActive", targetSteamId.ToString())); return; } string targetName = targetPlayer?.PlayerName ?? "OhneName"; var ban = AddBan(targetName, targetSteamId.ToString(), minutes, reason, executorName, player.SteamID.ToString()); commandInfo.ReplyToCommand(T("BanAdded", ban.Name, ban.SteamId, FormatBanDuration(ban.DurationMinutes), ban.Reason, ban.AdminName)); try { Server.ExecuteCommand($"kickid {ban.SteamId}"); } catch { } } [ConsoleCommand("css_banid", "Ban offline or online by steamid64")] public void CmdBanId(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_BanId")); return; } string executorName = GetConfiguredAdminName(player); string? executorSteamId = player?.SteamID.ToString(); string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); if (player != null) { var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (!CanTarget(player, targetPlayer)) { commandInfo.ReplyToCommand(T("BanDenied")); return; } } if (HasActiveBan(targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanAlreadyActive", targetSteamId.ToString())); return; } var maybeTarget = Utilities.GetPlayerFromSteamId(targetSteamId); string targetName = maybeTarget?.PlayerName ?? "OhneName"; var ban = AddBan(targetName, targetSteamId.ToString(), minutes, reason, executorName, executorSteamId); commandInfo.ReplyToCommand(T("BanAdded", ban.Name, ban.SteamId, FormatBanDuration(ban.DurationMinutes), ban.Reason, ban.AdminName)); try { Server.ExecuteCommand($"kickid {ban.SteamId}"); } catch { } } [ConsoleCommand("css_editban", "Edit an active ban")] public void CmdEditBan(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_EditBan")); return; } string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out _)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); var ban = GetLatestActiveBanBySteamId(steamIdArg); if (ban == null) { commandInfo.ReplyToCommand(T("NoBanFound", steamIdArg)); return; } ApplyBanEdit(ban, minutes, reason, player); commandInfo.ReplyToCommand(T("BanEdited", ban.Name, ban.SteamId, FormatBanDuration(ban.DurationMinutes), ban.Reason, GetConfiguredAdminName(player))); } [ConsoleCommand("css_unban", "Unban a player")] public void CmdUnban(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/unban")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Unban")); return; } string steamIdArg = commandInfo.GetArg(1); string deletedBy = GetConfiguredAdminName(player); var ban = GetLatestActiveBanBySteamId(steamIdArg); if (ban == null) { commandInfo.ReplyToCommand(T("NoBanFound", steamIdArg)); return; } UnbanInternal(ban, deletedBy); commandInfo.ReplyToCommand(T("BanRemoved", steamIdArg)); } [ConsoleCommand("css_kick", "Kick a player")] public void CmdKick(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/kick")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenKickMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Kick")); return; } string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (player != null && !CanTarget(player, targetPlayer)) { commandInfo.ReplyToCommand(T("KickDenied")); return; } commandInfo.ReplyToCommand(T("KickDone", targetSteamId)); Server.ExecuteCommand($"kickid {targetSteamId}"); } [ConsoleCommand("css_slay", "Slay a player")] public void CmdSlay(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/slay")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenSlayMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Slay")); return; } string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (player != null && !CanTarget(player, targetPlayer)) { commandInfo.ReplyToCommand(T("SlayDenied")); return; } if (targetPlayer == null || !targetPlayer.IsValid || targetPlayer.Pawn == null || !targetPlayer.Pawn.IsValid) { commandInfo.ReplyToCommand(T("TargetNotFound")); return; } targetPlayer.Pawn.Value?.CommitSuicide(false, true); commandInfo.ReplyToCommand(T("SlayDone", targetSteamId)); } [ConsoleCommand("css_map", "Change map")] public void CmdMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, false); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, false); } [ConsoleCommand("css_wsmap", "Change workshop map")] public void CmdWorkshopMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, true); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, true); } [ConsoleCommand("css_reloadconfigs", "Reload BZ_Admin configs")] public void CmdReloadConfigs(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; LoadDatabaseConfig(); MigrateDatabase(); LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); ExpireElapsedBans(); commandInfo.ReplyToCommand(T("ConfigsReloaded")); } private void OpenBanPlayerMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("BanMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("BanMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("BanMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("BanDenied")}"); return; } if (HasActiveBan(targetId)) { player.PrintToChat($" {T("BanAlreadyActive", targetId)}"); return; } OpenBanTimeMenu(player, target); }); index++; } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenEditBanMenu(CCSPlayerController admin) { ExpireElapsedBans(); var activeBans = _bans .Where(x => x.IsActive) .OrderByDescending(x => x.Id) .GroupBy(x => x.SteamId, StringComparer.OrdinalIgnoreCase) .Select(g => g.First()) .OrderBy(x => x.Name) .ToList(); if (activeBans.Count == 0) { admin.PrintToChat($" {T("NoActiveBans")}"); return; } var menu = new ChatMenu(T("EditBanMenuTitleBans")); int index = 1; foreach (var ban in activeBans) { string display = T("EditBanMenuEntry", index, ban.Name, FormatBanDuration(ban.DurationMinutes)); menu.AddMenuOption(display, (player, option) => { OpenEditBanTimeMenu(player, ban); }); index++; } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenBanTimeMenu(CCSPlayerController admin, CCSPlayerController target) { if (_banTimes.Count == 0) { admin.PrintToChat($" {T("NoBanTimes")}"); return; } var menu = new ChatMenu(T("BanMenuTitleTimes", target.PlayerName)); foreach (var time in _banTimes) { string display = T("BanMenuTimeEntry", time.DisplayName, time.Minutes); menu.AddMenuOption(display, (player, option) => OpenBanReasonMenu(player, target, time)); } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenEditBanTimeMenu(CCSPlayerController admin, BanInfo ban) { if (_banTimes.Count == 0) { admin.PrintToChat($" {T("NoBanTimes")}"); return; } var menu = new ChatMenu(T("EditBanMenuTitleTimes", ban.Name)); foreach (var time in _banTimes) { string display = T("BanMenuTimeEntry", time.DisplayName, time.Minutes); menu.AddMenuOption(display, (player, option) => OpenEditBanReasonMenu(player, ban, time.Minutes)); } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenBanReasonMenu(CCSPlayerController admin, CCSPlayerController target, BanTimeEntry time) { if (_banReasons.Count == 0) { admin.PrintToChat($" {T("NoBanReasons")}"); return; } var menu = new ChatMenu(T("BanMenuTitleReasons", target.PlayerName)); foreach (var reason in _banReasons) { string display = T("BanMenuReasonEntry", reason.DisplayName); menu.AddMenuOption(display, (player, option) => { string adminName = GetConfiguredAdminName(player); string targetName = string.IsNullOrWhiteSpace(target.PlayerName) ? "OhneName" : target.PlayerName; string targetSteamId = target.SteamID.ToString(); if (HasActiveBan(targetSteamId)) { player.PrintToChat($" {T("BanAlreadyActive", targetSteamId)}"); return; } var ban = AddBan(targetName, targetSteamId, time.Minutes, reason.Reason, adminName, player.SteamID.ToString()); player.PrintToChat($" {T("BanAdded", ban.Name, ban.SteamId, FormatBanDuration(ban.DurationMinutes), ban.Reason, ban.AdminName)}"); try { Server.ExecuteCommand($"kickid {targetSteamId}"); } catch { } }); } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenEditBanReasonMenu(CCSPlayerController admin, BanInfo ban, int minutes) { if (_banReasons.Count == 0) { admin.PrintToChat($" {T("NoBanReasons")}"); return; } var menu = new ChatMenu(T("EditBanMenuTitleReasons", ban.Name)); foreach (var reason in _banReasons) { string display = T("BanMenuReasonEntry", reason.DisplayName); menu.AddMenuOption(display, (player, option) => { ApplyBanEdit(ban, minutes, reason.Reason, player); player.PrintToChat($" {T("BanEdited", ban.Name, ban.SteamId, FormatBanDuration(ban.DurationMinutes), ban.Reason, GetConfiguredAdminName(player))}"); }); } menu.AddMenuOption(T("EditBanUseCommand"), (player, option) => { player.PrintToChat($" {T("EditBanCustomReasonHint", ban.SteamId, minutes)}"); }); menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenKickMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("KickMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("KickMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("KickMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("KickDenied")}"); return; } Server.ExecuteCommand($"kickid {target.SteamID}"); player.PrintToChat($" {T("KickDone", target.SteamID)}"); }); index++; } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenSlayMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("SlayMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("SlayMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("SlayMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("SlayDenied")}"); return; } if (!target.IsValid || target.Pawn == null || !target.Pawn.IsValid) { player.PrintToChat($" {T("TargetNotFound")}"); return; } target.Pawn.Value?.CommitSuicide(false, true); player.PrintToChat($" {T("SlayDone", target.SteamID)}"); }); index++; } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); } private void OpenMapMenu(CCSPlayerController admin, bool workshopOnly) { if (workshopOnly) { var workshopMaps = _maps.Values .Where(m => m.IsWorkshop) .GroupBy(m => m.WorkshopId) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (workshopMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoWorkshopMaps")}"); return; } var menu = new ChatMenu(T("MapMenuTitleWorkshop")); int index = 1; foreach (var map in workshopMaps) { string display = T("MapMenuWorkshopEntry", index, map.MapName, map.WorkshopId); menu.AddMenuOption(display, (player, option) => { Server.ExecuteCommand($"host_workshop_map {map.WorkshopId}"); AnnounceMapChange(player, $"{map.MapName} ({map.WorkshopId})"); }); index++; } menu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, menu); return; } var standardMaps = _maps.Values .Where(m => !m.IsWorkshop) .GroupBy(m => m.MapName) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (standardMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoStandardMaps")}"); return; } var standardMenu = new ChatMenu(T("MapMenuTitle")); int standardIndex = 1; foreach (var map in standardMaps) { string display = T("MapMenuStandardEntry", standardIndex, map.MapName); standardMenu.AddMenuOption(display, (player, option) => { if (!Server.IsMapValid(map.MapName)) { player.PrintToChat($" {T("MapNotFound", map.MapName)}"); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(player, map.MapName); }); standardIndex++; } standardMenu.AddMenuOption($"9. {T("MenuMain")}", (player, option) => OpenAdminMenu(player)); MenuManager.OpenChatMenu(admin, standardMenu); } private void ChangeMap(CCSPlayerController? caller, string input, CommandInfo commandInfo, bool workshopOnly) { if (!_maps.TryGetValue(input, out var map)) { if (ulong.TryParse(input, out _)) { Server.ExecuteCommand($"host_workshop_map {input}"); AnnounceMapChange(caller, input); return; } if (workshopOnly) { commandInfo.ReplyToCommand(T("MapNotFound", input)); return; } if (Server.IsMapValid(input)) { Server.ExecuteCommand($"changelevel {input}"); AnnounceMapChange(caller, input); return; } commandInfo.ReplyToCommand(T("MapNotFound", input)); return; } if (map.IsWorkshop) { Server.ExecuteCommand($"host_workshop_map {map.WorkshopId}"); AnnounceMapChange(caller, $"{map.MapName} ({map.WorkshopId})"); return; } if (!Server.IsMapValid(map.MapName)) { commandInfo.ReplyToCommand(T("MapNotFound", map.MapName)); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(caller, map.MapName); } private void AnnounceMapChange(CCSPlayerController? caller, string mapDisplayName) { string callerName = GetConfiguredAdminName(caller); Server.PrintToChatAll(T("MapChanged", callerName, mapDisplayName)); Logger.LogInformation($"[BZ_Admin] {callerName} changed map to {mapDisplayName}"); } private List<CCSPlayerController> GetConnectedHumanPlayers() { return Utilities.GetPlayers() .Where(p => p != null && p.IsValid && !p.IsBot && p.Connected == PlayerConnectedState.PlayerConnected) .ToList(); } private string EscapeServerCommandArgument(string value) { return value.Replace("\\", "\\\\").Replace("\"", "\\\""); } private int GetNextAdminId() => _admins.Count == 0 ? 1 : _admins.Values.Max(x => x.Id) + 1; private BanInfo AddBan(string targetName, string targetSteamId, int minutes, string reason, string executorName, string? executorSteamId) { string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); var ban = new BanInfo { Id = InsertBanPlaceholder(), Name = string.IsNullOrWhiteSpace(targetName) ? "OhneName" : targetName, SteamId = targetSteamId, DurationMinutes = minutes, Reason = reason, AdminName = executorName, AdminSteamId = executorSteamId, CreatedAt = now, UpdatedAt = now, IsActive = true, DeletedAt = null, DeletedBy = null, EditedBy = null, EditedBySteamId = null, EditedAt = null, Source = "server" }; UpdateBanFull(ban); _bans.Add(ban); return ban; } private void ApplyBanEdit(BanInfo ban, int minutes, string reason, CCSPlayerController? editor) { string editorName = GetConfiguredAdminName(editor); string? editorSteamId = editor?.SteamID.ToString(); string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DurationMinutes = minutes; ban.Reason = reason; ban.UpdatedAt = now; ban.EditedBy = editorName; ban.EditedBySteamId = editorSteamId; ban.EditedAt = now; UpdateBanFull(ban); } private int InsertBanPlaceholder() { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @"INSERT INTO bz_bans (name, steamid64, duration_minutes, reason, admin_name, created_at, updated_at, is_active, source) VALUES ('', '', 0, '', '', NOW(), NOW(), 0, 'server'); SELECT LAST_INSERT_ID();"; return Convert.ToInt32(command.ExecuteScalar()); } private void UpdateBanFull(BanInfo ban) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" UPDATE bz_bans SET name = @name, steamid64 = @steamid64, duration_minutes = @duration_minutes, reason = @reason, admin_name = @admin_name, admin_steamid64 = @admin_steamid64, created_at = @created_at, updated_at = @updated_at, is_active = @is_active, deleted_at = @deleted_at, deleted_by = @deleted_by, edited_by = @edited_by, edited_by_steamid64 = @edited_by_steamid64, edited_at = @edited_at, source = @source WHERE id = @id;"; command.Parameters.AddWithValue("@name", ban.Name); command.Parameters.AddWithValue("@steamid64", ban.SteamId); command.Parameters.AddWithValue("@duration_minutes", ban.DurationMinutes); command.Parameters.AddWithValue("@reason", ban.Reason); command.Parameters.AddWithValue("@admin_name", ban.AdminName); command.Parameters.AddWithValue("@admin_steamid64", (object?)ban.AdminSteamId ?? DBNull.Value); command.Parameters.AddWithValue("@created_at", ban.CreatedAt); command.Parameters.AddWithValue("@updated_at", ban.UpdatedAt); command.Parameters.AddWithValue("@is_active", ban.IsActive); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.Parameters.AddWithValue("@edited_by", (object?)ban.EditedBy ?? DBNull.Value); command.Parameters.AddWithValue("@edited_by_steamid64", (object?)ban.EditedBySteamId ?? DBNull.Value); command.Parameters.AddWithValue("@edited_at", (object?)ban.EditedAt ?? DBNull.Value); command.Parameters.AddWithValue("@source", ban.Source); command.Parameters.AddWithValue("@id", ban.Id); command.ExecuteNonQuery(); } private void UnbanInternal(BanInfo ban, string deletedBy) { ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = deletedBy; ban.UpdatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); UpdateBanStatus(ban); } private bool HasActiveBan(string steamId) => GetEffectiveActiveBanBySteamId(steamId) != null; private BanInfo? GetLatestActiveBanBySteamId(string steamId) { return _bans .Where(x => x.SteamId.Equals(steamId, StringComparison.OrdinalIgnoreCase) && x.IsActive) .OrderByDescending(x => x.Id) .FirstOrDefault(); } private BanInfo? GetEffectiveActiveBanBySteamId(string steamId) { var activeBan = GetLatestActiveBanBySteamId(steamId); if (activeBan == null) return null; if (IsBanExpired(activeBan)) { ExpireBan(activeBan); return null; } return activeBan; } private bool IsBanExpired(BanInfo ban) { if (!ban.IsActive || ban.DurationMinutes <= 0) return false; if (!DateTime.TryParseExact(ban.CreatedAt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var createdAt)) return false; return DateTime.Now >= createdAt.AddMinutes(ban.DurationMinutes); } private void ExpireBan(BanInfo ban) { ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = "AUTO"; ban.UpdatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); UpdateBanStatus(ban); } private void ExpireElapsedBans() { foreach (var ban in _bans.Where(x => x.IsActive).ToList()) { if (!IsBanExpired(ban)) continue; ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = "AUTO"; ban.UpdatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); UpdateBanStatus(ban); } } private string FormatBanListEntry(BanInfo ban) { string status = ban.IsActive ? T("BanStatusActive") : string.Equals(ban.DeletedBy, "AUTO", StringComparison.OrdinalIgnoreCase) ? T("BanStatusExpired") : T("BanStatusRemoved"); return T("BanListEntry", ban.Name, ban.SteamId, ban.Reason, ban.AdminName, status); } private int ReloadBanReasonsFromTxt() { if (!File.Exists(LegacyBanReasonsFilePath)) return 0; int count = 0; using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); foreach (var rawLine in File.ReadAllLines(LegacyBanReasonsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_ban_reasons (display_name, reason, created_at, updated_at) VALUES (@display_name, @reason, NOW(), NOW()) ON DUPLICATE KEY UPDATE reason = VALUES(reason), updated_at = NOW();"; command.Parameters.AddWithValue("@display_name", parts[0].Trim()); command.Parameters.AddWithValue("@reason", parts[1].Trim()); count += command.ExecuteNonQuery() > 0 ? 1 : 0; } transaction.Commit(); return count; } private int ReloadBanTimesFromTxt() { if (!File.Exists(LegacyBanTimesFilePath)) return 0; int count = 0; using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); foreach (var rawLine in File.ReadAllLines(LegacyBanTimesFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; if (!int.TryParse(parts[1], out int minutes)) continue; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_ban_times (display_name, minutes, created_at, updated_at) VALUES (@display_name, @minutes, NOW(), NOW()) ON DUPLICATE KEY UPDATE minutes = VALUES(minutes), updated_at = NOW();"; command.Parameters.AddWithValue("@display_name", parts[0].Trim()); command.Parameters.AddWithValue("@minutes", minutes); count += command.ExecuteNonQuery() > 0 ? 1 : 0; } transaction.Commit(); return count; } private int ReloadGroupsFromTxt() { if (!File.Exists(LegacyGroupsFilePath)) return 0; int count = 0; using var connection = GetConnection(); using var transaction = connection.BeginTransaction(); foreach (var rawLine in File.ReadAllLines(LegacyGroupsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var groupName = parts[0].Trim(); var flags = parts[1].Trim(); if (!groupName.StartsWith("#")) continue; using var command = connection.CreateCommand(); command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_groups (group_name, flags, created_at, updated_at) VALUES (@group_name, @flags, NOW(), NOW()) ON DUPLICATE KEY UPDATE flags = VALUES(flags), updated_at = NOW();"; command.Parameters.AddWithValue("@group_name", groupName); command.Parameters.AddWithValue("@flags", flags); count += command.ExecuteNonQuery() > 0 ? 1 : 0; } transaction.Commit(); return count; } private void InsertGroup(MySqlConnection connection, string groupName, string flagsCsv, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @"INSERT INTO bz_groups (group_name, flags, created_at, updated_at) VALUES (@group_name, @flags, NOW(), NOW());"; command.Parameters.AddWithValue("@group_name", groupName); command.Parameters.AddWithValue("@flags", flagsCsv); command.ExecuteNonQuery(); } private void InsertAdmin(MySqlConnection connection, AdminInfo admin, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @"INSERT INTO bz_admins (id, name, steamid64, group_name, immunity, created_at, updated_at) VALUES (@id, @name, @steamid64, @group_name, @immunity, @created_at, @updated_at);"; command.Parameters.AddWithValue("@id", admin.Id); command.Parameters.AddWithValue("@name", admin.Name); command.Parameters.AddWithValue("@steamid64", admin.SteamId); command.Parameters.AddWithValue("@group_name", admin.GroupName); command.Parameters.AddWithValue("@immunity", admin.Immunity); command.Parameters.AddWithValue("@created_at", admin.CreatedAt); command.Parameters.AddWithValue("@updated_at", admin.UpdatedAt); command.ExecuteNonQuery(); } private void InsertBanTime(MySqlConnection connection, string displayName, int minutes, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @"INSERT INTO bz_ban_times (display_name, minutes, created_at, updated_at) VALUES (@display_name, @minutes, NOW(), NOW());"; command.Parameters.AddWithValue("@display_name", displayName); command.Parameters.AddWithValue("@minutes", minutes); command.ExecuteNonQuery(); } private void InsertBanReason(MySqlConnection connection, string displayName, string reason, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @"INSERT INTO bz_ban_reasons (display_name, reason, created_at, updated_at) VALUES (@display_name, @reason, NOW(), NOW());"; command.Parameters.AddWithValue("@display_name", displayName); command.Parameters.AddWithValue("@reason", reason); command.ExecuteNonQuery(); } private void InsertBan(MySqlConnection connection, BanInfo ban, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @"INSERT INTO bz_bans (id, name, steamid64, duration_minutes, reason, admin_name, admin_steamid64, created_at, updated_at, is_active, deleted_at, deleted_by, edited_by, edited_by_steamid64, edited_at, source) VALUES (@id, @name, @steamid64, @duration_minutes, @reason, @admin_name, @admin_steamid64, @created_at, @updated_at, @is_active, @deleted_at, @deleted_by, @edited_by, @edited_by_steamid64, @edited_at, @source);"; command.Parameters.AddWithValue("@id", ban.Id); command.Parameters.AddWithValue("@name", ban.Name); command.Parameters.AddWithValue("@steamid64", ban.SteamId); command.Parameters.AddWithValue("@duration_minutes", ban.DurationMinutes); command.Parameters.AddWithValue("@reason", ban.Reason); command.Parameters.AddWithValue("@admin_name", ban.AdminName); command.Parameters.AddWithValue("@admin_steamid64", (object?)ban.AdminSteamId ?? DBNull.Value); command.Parameters.AddWithValue("@created_at", ban.CreatedAt); command.Parameters.AddWithValue("@updated_at", ban.UpdatedAt); command.Parameters.AddWithValue("@is_active", ban.IsActive); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.Parameters.AddWithValue("@edited_by", (object?)ban.EditedBy ?? DBNull.Value); command.Parameters.AddWithValue("@edited_by_steamid64", (object?)ban.EditedBySteamId ?? DBNull.Value); command.Parameters.AddWithValue("@edited_at", (object?)ban.EditedAt ?? DBNull.Value); command.Parameters.AddWithValue("@source", ban.Source); command.ExecuteNonQuery(); } private void UpdateBanStatus(BanInfo ban) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @"UPDATE bz_bans SET is_active=@is_active, deleted_at=@deleted_at, deleted_by=@deleted_by, updated_at=@updated_at WHERE id=@id;"; command.Parameters.AddWithValue("@is_active", ban.IsActive); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.Parameters.AddWithValue("@updated_at", ban.UpdatedAt); command.Parameters.AddWithValue("@id", ban.Id); command.ExecuteNonQuery(); } private class DatabaseConfig { public string Host { get; set; } = "127.0.0.1"; public int Port { get; set; } = 3306; public string Database { get; set; } = "bz_admin"; public string Username { get; set; } = "bz_admin"; public string Password { get; set; } = "DEIN_PASSWORT"; public string Charset { get; set; } = "utf8mb4"; } private class GroupInfo { public string GroupName { get; set; } = ""; public List<string> Flags { get; set; } = new(); } private class AdminInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public string GroupName { get; set; } = ""; public int Immunity { get; set; } public string CreatedAt { get; set; } = ""; public string UpdatedAt { get; set; } = ""; } private class BanInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public int DurationMinutes { get; set; } public string Reason { get; set; } = ""; public string AdminName { get; set; } = ""; public string? AdminSteamId { get; set; } public string CreatedAt { get; set; } = ""; public string UpdatedAt { get; set; } = ""; public bool IsActive { get; set; } = true; public string? DeletedAt { get; set; } public string? DeletedBy { get; set; } public string? EditedBy { get; set; } public string? EditedBySteamId { get; set; } public string? EditedAt { get; set; } public string Source { get; set; } = "server"; } private class BanTimeEntry { public string DisplayName { get; set; } = ""; public int Minutes { get; set; } } private class BanReasonEntry { public string DisplayName { get; set; } = ""; public string Reason { get; set; } = ""; } private class MapEntry { public bool IsWorkshop { get; set; } public string MapName { get; set; } = ""; public string WorkshopId { get; set; } = ""; } } } -
Anleitung
- Gehe zu ACP ➜ Inhalt Boxen / Seiten ➜ + Box / Seite hinzufügen oder editiere eine vorhandene Box / Seite.
⚠ Hinweis: Beachte, dass die Box / Seite zwingend den Typ HTML entsprechen muss! - Kopiere den o.g. HTML-Quellcode und füge diesen dort ein und speichere ab.
- Gehe zu ACP ➜ Anpassung ➜ Stile ➜ DEIN STIL ➜ Tab: Erweiterte Einstellungen
- Kopiere den o.g. CSS-Quellcode und füge diesen dort ein und speichere ab.
Fertig!
HTML: HTML
Alles anzeigen<!-- START: CODE --> <section id="timeline"> <ul> <li> <i class="fa fa-html5"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>HTML Language</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-css3"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>CSS Language</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-jsfiddle"></i> <div class="box"> <h3 class="title"> <span class="year">2024</span>JavaScript Programming </h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-database"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>SQL Server</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-code"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>C# Programming</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-code"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>PHP Programming</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-code"></i> <div class="box"> <h3 class="title"><span class="year">20240</span>React JS</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> <li> <i class="fa fa-code"></i> <div class="box"> <h3 class="title"><span class="year">2024</span>Node JS</h3> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quo possimus in ipsum iure eaque atque esse commodi molestiae, et odio!</p> <button class="button">Mehr</button> </div> </li> </ul> </section> <script> "use strict"; const boxes = document.querySelectorAll(".box"); window.addEventListener("scroll", DisplayContent); DisplayContent(); function DisplayContent() { const TriggerBottom = (window.innerHeight / 5) * 4; boxes.forEach((box) => { const topBox = box.getBoundingClientRect().top; if (topBox < TriggerBottom) { box.classList.add("show"); } else { box.classList.remove("show"); } }); } </script> <!-- ENDE: CODE -->
CSS: CSS
Alles anzeigen<!-- START: CODE --> .title { box-shadow: 10px 5px 10px rgba(0, 0, 0, 0.5); padding: 1rem 0rem 1rem 0.7rem; border-top-right-radius: 5px; border-top-left-radius: 5px; color: #fff; font-size: 1.3rem; background-color: #B51836; } .year { background-color: #fff; padding: 0.2rem 0.8rem; border-radius: 10px; color: #2c3e50; font-size: 0.9rem; margin: 0 0.5rem; } #timeline p { padding: 1rem 0 1rem 1rem; color: #000; } #timeline ul { padding: 50px 0; } #timeline ul li { list-style: none; position: relative; width: 7px; margin: 0 auto; padding-top: 50px; background-color: #B51836; } #timeline ul li .box { position: relative; bottom: 0; width: 450px; background-color: #fff !important; box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); border-radius: 5px; transform: translateX(400%); transition: all 0.5s ease-in-out; } #timeline ul li:nth-child(odd) .box { left: 50px; } #timeline ul li:nth-child(even) .box { left: -500px; transform: translateX(-400%); } #timeline ul li .box.show { transform: translateX(0%); transition: all 0.5s ease-in-out; } #timeline ul li i { position: absolute; left: 50%; top: 80px; width: 45px; height: 45px; background: #2c3e50; transform: translateX(-50%); border-radius: 50%; } #timeline ul li i::before { font-size: 2.2em; } #timeline button { margin: 0.5rem 0rem 1rem 1rem; padding: 0.5rem 1rem; } #timeline .box:before { content: ""; position: absolute; top: 5px; width: 0; right: 0; border-style: solid; } #timeline ul li:nth-child(odd) .box:before { left: -15px; border-width: 8px 16px 8px 0; border-color: transparent #B51836; } #timeline ul li:nth-child(even) .box:before { right: -15px; border-width: 8px 0px 8px 16px; border-color: transparent #B51836; } @media (max-width: 900px) { #timeline ul li .box { width: calc(70vw); left: 45px; } #timeline ul li:nth-child(even) .box:before { left: -15px; border-width: 8px 16px 8px 0px; border-color: transparent #B51836; } #timeline ul li:nth-child(odd) .box { left: 40px; } #timeline ul li:nth-child(even) .box { left: -390px; } } @media (max-width: 768px) { #timeline ul li { margin-left: 30px; } #timeline ul li .box { width: calc(70vw); left: 45px; } #timeline ul li:nth-child(even) .box { left: 40px; } #timeline ul li:nth-child(even) .box:before { left: -15px; border-width: 8px 16px 8px 0px; border-color: transparent #de3163; } } ::-webkit-scrollbar:horizontal { display: none; } <!-- ENDE: CODE --> - Gehe zu ACP ➜ Inhalt Boxen / Seiten ➜ + Box / Seite hinzufügen oder editiere eine vorhandene Box / Seite.
-
Test Datei
Smarty: Persönliche Box
Alles anzeigen{if !$__wcf->getUser()->userID != 0} <div class="personalBox boxContent"> <h2 class="boxTitle"> <script type="text/javascript"> //<![CDATA[ function displayTitle() { var date = new Date(); var hours = date.getHours(); if (hours >= 0) { dayTime = "{lang}wcf.wbbsupport.night{/lang}"; } if (hours >= 6) { dayTime = "{lang}wcf.wbbsupport.morning{/lang}"; } if ((hours >= 12) && (hours < 18)) { dayTime = "{lang}wcf.wbbsupport.afternoon{/lang}"; } if (hours >= 18) { dayTime = "{lang}wcf.wbbsupport.evening{/lang}"; } document.write('' + dayTime + ''); } displayTitle(); //]]> </script> </h2> <ul class="sidebarItemList"> <div class="userPersonalBoxBackground" style="background-image: url({@$__wcf->getUserProfileHandler()->getCoverPhoto()->getURL()});"></div> <div class="guestBox"> <div class="personalBoxAvatar" style="display: flex; justify-content: center;margin-top: -45px;"> <span>{@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(128)}</span> </div> {if !$__disableLoginLink|isset} <!-- login box --> <section class="box" style="text-align:center;"> {lang}wcf.wbbsupport.registerBox{/lang} {if !REGISTER_DISABLED} <div> <a href="{link controller='Register'}{/link}" class="button">{lang}wcf.user.button.register{/lang}</a> </div> {else} <div> <a href="#" class="button">{lang}wcf.wbbsupport.register{/lang}</a> </div> {/if} <hr> {lang}wcf.wbbsupport.loginBox{/lang} <div> <a class="button" href="{link controller='Login'}{/link}">{lang}wcf.user.login{/lang}</a> </div> </section> {/if} </div> </ul> </div> {else} {if $__wcf->getUser()->userID && $__wcf->getSession()->getPermission('user.profile.coverPhoto.canUploadCoverPhoto')} <div class="personalBox boxContent"> <h2 class="boxTitle"> <script type="text/javascript"> //<![CDATA[ function displayTitle() { var date = new Date(); var hours = date.getHours(); if (hours >= 0) { dayTime = "{lang}wcf.wbbsupport.night{/lang}"; } if (hours >= 6) { dayTime = "{lang}wcf.wbbsupport.morning{/lang}"; } if ((hours >= 12) && (hours < 18)) { dayTime = "{lang}wcf.wbbsupport.afternoon{/lang}"; } if (hours >= 18) { dayTime = "{lang}wcf.wbbsupport.evening{/lang}"; } document.write('' + dayTime + ''); } displayTitle(); //]]> </script> </h2> <ul class="sidebarItemList"> <div class="userPersonalBoxBackground" style="background-image: url({@$__wcf->getUserProfileHandler()->getCoverPhoto()->getURL()});"></div> <div class="messageAuthorBox"> <div class="personalBoxAvatar" style="display: flex; justify-content: center;margin-top: -45px;"> <a href="{link controller='User' object=$__wcf->user}editOnInit=true#about{/link}" class="jsUserPanelEditProfile jsTooltip" title="{lang}Profil bearbeiten{/lang}"> {@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(128)} </a> </div> {if $__wcf->getUserProfileHandler()->getUserTitle()} <div class="userTitle" style="display: flex; justify-content: center;margin-top: 15px;"> <span class="badge userTitleBadge{if $__wcf->getUserProfileHandler()->getRank() && $__wcf->getUserProfileHandler()->getRank()->cssClassName} {@$__wcf->getUserProfileHandler()->getRank()->cssClassName}{/if}">{$__wcf->getUserProfileHandler()->getUserTitle()}</span> </div> {/if} {if $__wcf->getUserProfileHandler()->getRank() && $__wcf->getUserProfileHandler()->getRank()->rankImage} <div class="userRank">{@$__wcf->getUserProfileHandler()->getRank()->getImage()}</div> {/if} {/if} {if MODULE_TROPHY && $__wcf->session->getPermission('user.profile.trophy.canSeeTrophies') && ($__wcf->getUserProfileHandler()->isAccessible('canViewTrophies') || $__wcf->getUserProfileHandler()->userID == $__wcf->session->userID) && $__wcf->getUserProfileHandler()->getSpecialTrophies()|count} <div class="specialTrophyContainer"> <ul> {foreach from=$__wcf->getUserProfileHandler()->getSpecialTrophies() item=trophy} <li><a href="{@$trophy->getLink()}">{@$trophy->renderTrophy(32, true)}</a></li> {/foreach} </ul> </div> {/if} {event name='personalBoxAvatar'} </div> <div class="personalBoxInfo"> <dl class="plain dataList containerContent small"> {if MESSAGE_SIDEBAR_ENABLE_ARTICLES && $__wcf->getUserProfileHandler()->articles} <dt><a href="{link controller='ArticleList' userID=$__wcf->getUserProfileHandler()->userID}{/link}" class="jsTooltip" title="{lang user=$__wcf->getUserProfileHandler()}wcf.article.showArticlesWritten{/lang}">{lang}wcf.user.articles{/lang}</a></dt> <dd>{#$__wcf->getUserProfileHandler()->articles}</dd> {/if} {include file='userInformationStatistics' user=$__wcf->getUserProfileHandler()->getUserProfile()} </dl> <dl class="plain dataList containerContent small"> <dt>{lang}wcf.user.lastActivityTime{/lang}</dt> <span style="float:right;">{@$__wcf->getUser()->lastActivityTime|time}</span> </dl> {event name='personalBoxStatistics'} <ul class="personalInfo"> <li> {if $__wcf->getUserProfileHandler()->canEditOwnProfile()}{icon size=16 name='pencil'} <a href="{link controller='User' object=$__wcf->user}editOnInit=true#about{/link}" class="jsUserPanelEditProfile"><span class="personalInfoLinkText">{lang}wcf.user.editProfile{/lang}</span></a>{/if} </li> <li> {if $__wcf->session->getPermission('admin.general.canUseAcp')}{icon size=16 name='wrench'} <a href="{link isACP=true}{/link}"><span class="personalInfoLinkText">{lang}wcf.global.acp.short{/lang}</span></a>{/if} </li> <li> {icon size=16 name='user'} <a href="{link}{/link}index.php?account-management/"><span class="personalInfoLinkText">{lang}wcf.user.menu.profile{/lang}</span></a> </li> <li> {icon size=16 name='cog'} <a href="{link}{/link}index.php?settings/"><span class="personalInfoLinkText">{lang}wcf.user.menu.settings{/lang}</span></a> </li> <li> {icon size=16 name='globe'} <a href="{link}{/link}index.php?notification-list/"><span class="personalInfoLinkText">{lang}wcf.user.menu.community{/lang}</span></a> </li> <li> <a href="{link controller='NotificationList'}{/link}">{icon size=16 name='bell'} <span class="personalInfoLinkText">{lang}wcf.user.notification.notifications{/lang}</span>{if $__wcf->getUserNotificationHandler()->getNotificationCount()} <span class="badge badgeUpdate">{#$__wcf->getUserNotificationHandler()->getNotificationCount()}</span>{/if}</a> </li> <li> {if $__wcf->user->userID && $__wcf->session->getPermission('mod.general.canUseModeration')} <a href="{link controller='ModerationList'}{/link}"> {icon size=16 name='exclamation-triangle'} <span class="personalInfoLinkText">{lang}wcf.moderation.moderation{/lang}</span> {if $__wcf->getModerationQueueManager()->getOutstandingModerationCount()}<span class="badge badgeUpdate">{#$__wcf->getModerationQueueManager()->getOutstandingModerationCount()}</span>{/if} </a> {/if} </li> {event name='personalBoxPanel'} <li> {if MODULE_CONVERSATION && $__wcf->user->userID && $__wcf->session->getPermission('user.conversation.canUseConversation')} <a href="{link controller='ConversationList'}{/link}">{icon size=16 name='comments'} <span class="personalInfoLinkText">{lang}wcf.conversation.conversations{/lang}</span> {if $__wcf->getConversationHandler()->getUnreadConversationCount()}<span class="badge badgeUpdate">{#$__wcf->getConversationHandler()->getUnreadConversationCount()}</span>{/if}</a> </li> <li> <legend><span style="font-size:0.8em;">{lang}wcf.conversation.quota{/lang} {lang}wcf.conversation.conversations{/lang}</span></legend> <div class="conversationCount"> {assign var='conversationCount' value=$__wcf->getConversationHandler()->getConversationCount()} {assign var='maxConversationCount' value=$__wcf->session->getPermission('user.conversation.maxConversations')} {assign var='conversationCountValue' value=$conversationCount/$maxConversationCount*100} <meter class="conversationQuotaMeter" min="0" max="100" low="90" high="99" value="{$conversationCountValue|ceil}" aria-label="{lang}wcf.conversation.quota{/lang}"> {#$conversationCountValue} % </meter> <p><small>{lang}wcf.conversation.quota.description{/lang}</small></p> </div> {/if} </li> </ul> <div class="userPersonalBoxFooter"> <a href="{link controller='Logout'}t={csrfToken type=url}{/link}" onclick="WCF.Dropdown.Interactive.Handler.close('userMenu'); WCF.System.Confirmation.show('{jslang}{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du dich{else}Wollen Sie sich{/if} wirklich abmelden?{/jslang}', $.proxy(function (action) { if (action == 'confirm') window.location.href = $(this).attr('href'); }, this)); return false;">{lang}wcf.user.logout{/lang}</a> </div> </div> </ul> </div> {/if} <style> .personalInfo fa-icon { color: var(--wcfContentText)!important; } </style> -
Einen Menü Punkt im ACP erstellen ein Test Menü in der Konfiguration (Configuration)
XML: Package.xml
Alles anzeigen<?xml version="1.0" encoding="UTF-8"?> <package xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd" name="com.example.testacpmenu"> <packageinformation> <packagename><![CDATA[Test ACP Menü]]></packagename> <packagedescription><![CDATA[Erstellt eine ACP-Kategorie mit zwei Unterpunkten.]]></packagedescription> <version>1.0.0</version> <date>2026-04-09</date> </packageinformation> <authorinformation> <author><![CDATA[ddbmaster]]></author> <authorurl><![CDATA[https://ddbmaster.de]]></authorurl> </authorinformation> <requiredpackages> <requiredpackage minversion="6.2.0">com.woltlab.wcf</requiredpackage> </requiredpackages> <instructions type="install"> <instruction type="file" /> <instruction type="acpMenu" /> <instruction type="acpTemplate" /> <instruction type="language" /> </instructions> </package>XML: acpMenu.xml
Alles anzeigen<?xml version="1.0" encoding="UTF-8"?> <data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/acpMenu.xsd"> <import> <acpmenuitem name="test.acp.menu.link.testkategorien"> <parent>wcf.acp.menu.link.content</parent> </acpmenuitem> <acpmenuitem name="test.acp.menu.link.test1"> <controller>wcf\acp\page\Test1Page</controller> <parent>test.acp.menu.link.testkategorien</parent> <permissions>admin.general.canUseAcp</permissions> <showorder>1</showorder> </acpmenuitem> <acpmenuitem name="test.acp.menu.link.test2"> <controller>wcf\acp\page\Test2Page</controller> <parent>test.acp.menu.link.testkategorien</parent> <permissions>admin.general.canUseAcp</permissions> <showorder>2</showorder> </acpmenuitem> </import> </data>Ordner Language
XML: de
Alles anzeigen<?xml version="1.0" encoding="UTF-8"?> <language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/2019/language.xsd" languagecode="de"> <import> <category name="test.acp.menu"> <item name="test.acp.menu.link.testkategorien"><![CDATA[Testkategorien]]></item> <item name="test.acp.menu.link.test1"><![CDATA[Test1]]></item> <item name="test.acp.menu.link.test2"><![CDATA[Test2]]></item> </category> </import> </language>acptemplates.tar
PHP: test1.tpl
Alles anzeigen{include file='header' pageTitle='Test1'} <header class="contentHeader"> <div class="contentHeaderTitle"> <h1>Test1</h1> </div> </header> <div class="section"> <p>Das ist die Test1-Seite.</p> </div> {include file='footer'}PHP: test2
Alles anzeigen{include file='header' pageTitle='Test2'} <header class="contentHeader"> <div class="contentHeaderTitle"> <h1>Test2</h1> </div> </header> <div class="section"> <p>Das ist die Test2-Seite.</p> </div> {include file='footer'}files.tar
PHP: lib/acp/page/Test1Page.class.php
Alles anzeigen<?php namespace wcf\acp\page; use wcf\page\AbstractPage; class Test1Page extends AbstractPage { public $activeMenuItem = 'test.acp.menu.link.test1'; public $templateName = 'test1'; }PHP: lib/acp/page/Test2Page.class.php
Alles anzeigen<?php namespace wcf\acp\page; use wcf\page\AbstractPage; class Test2Page extends AbstractPage { public $activeMenuItem = 'test.acp.menu.link.test2'; public $templateName = 'test2'; }Wichtig ist sollte dieses Menü an anderer Stelle sein als unter Konfiguration muss lediglich der Selektor geändert werden in der acpMenu.xml
Auswahl ist configuration, application, managment, customization, content, user das sind die Selektoren zur Auswahl.
-
Diese Box 📦 funktioniert sehr gut
-
-
1. Composer installieren (frisches Ubuntu)
sudo apt update
sudo apt install composer -y2. Projektordner erstellen
mkdir -p /var/www/html/serverview
cd /var/www/html/serverview3. xPaw installieren
composer require xpaw/php-source-query-class4.Template-Ordner erstellen
mkdir -p /var/www/html/serverview/template5. Template-Datei erstell
nano /var/www/html/serverview/template/cs2.html.phpPHP: cs2.html.php
Alles anzeigen<?php $SQ_TIMEOUT = 5; $Query = new \xPaw\SourceQuery\SourceQuery(); $Info = []; $Players = []; $Bots = 0; try { $Query->Connect($SQ_SERVER_ADDR, $SQ_SERVER_PORT, $SQ_TIMEOUT, $SQ_ENGINE); $Info = $Query->GetInfo(); $Players = $Query->GetPlayers(); } catch( Exception $e ) { return "CS2 Server derzeit nicht erreichbar"; } finally { $Query->Disconnect(); } if( empty($Info) ) { return "CS2 Server derzeit nicht erreichbar"; } $Bots = array_key_exists('Bots', $Info) ? (int)$Info['Bots'] : 0; $mapWithoutPath = explode("/", $Info["Map"]); $map = $mapWithoutPath[count($mapWithoutPath) - 1]; $imageFile = $map . ".png"; $relMapUrl = $REL_URL_IMAGES . $imageFile; $absMapPath = $ABS_PATH_IMAGES . $imageFile; $timestamp = date("Y-m-d H:i:s"); if (is_array($Players)) { foreach ($Players as &$player) { if (!isset($player["TimeF"])) { $seconds = isset($player["Time"]) ? (int)$player["Time"] : 0; $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); $secs = $seconds % 60; $player["TimeF"] = sprintf('%02d:%02d:%02d', $hours, $minutes, $secs); } } unset($player); } ob_start(); ?> <a href="http://store.steampowered.com/app/730/" target="_blank"> <img height="16" width="16" src="/images/icons/cs2.png"/> </a> <a href="steam://connect/<?php echo $SQ_CONNECT_ADDR; ?>" target="_top"> <?php echo htmlspecialchars($Info["HostName"]); ?> </a><br/> <?php if(file_exists($absMapPath)): ?> <div style="text-align:center;"> <img src="<?php echo $relMapUrl; ?>" width="100%"> </div> <?php endif; ?> <table style="width:100%;"> <tr> <td>Map</td> <td style="text-align:right;"><?php echo htmlspecialchars($map); ?></td> </tr> <tr> <td>Spieler</td> <td style="text-align:right;"> <?php echo $Info["Players"]; ?> (<?php echo $Bots; ?> Bots) / <?php echo $Info["MaxPlayers"]; ?> </td> </tr> </table> <?php if(!empty($Players)): ?> <table style="width:100%;"> <tr><th>Name</th><th>Kills</th><th>Zeit</th></tr> <?php foreach($Players as $p): ?> <tr> <td><?php echo htmlspecialchars($p["Name"]); ?></td> <td><?php echo (int)$p["Frags"]; ?></td> <td><?php echo htmlspecialchars($p["TimeF"]); ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> <?php return ob_get_clean();
6. Loader erstellen
nano /var/www/html/serverview/server_view_load_HL2.phpInhalt
PHP: server_view_load_HL2.php
Alles anzeigen<?php $ABS_PATH_IMAGES = "/var/www/html/images/maps_cs2/"; $REL_URL_IMAGES = "/images/maps_cs2/"; $templateFile = __DIR__ . "/template/cs2.html.php"; $outputDir = "/var/www/html/cache/serverview/"; mkdir -p($outputDir); require_once __DIR__ . '/vendor/autoload.php'; $SQ_SERVER_ADDR = "127.0.0.1"; $SQ_ENGINE = \xPaw\SourceQuery\SourceQuery::SOURCE; /* SERVER 1 */ $SQ_SERVER_PORT = 27015; $SQ_CONNECT_ADDR = "84.118.93.219:27015"; $html = include $templateFile; file_put_contents($outputDir . "cs2.html", $html); /* SERVER 2 */ $SQ_SERVER_PORT = 27016; $SQ_CONNECT_ADDR = "84.118.93.219:27016"; $html = include $templateFile; file_put_contents($outputDir . "cs2--2.html", $html);
7. Cache-Ordner erstellen
mkdir -p /var/www/html/cache/serverview
chmod -R 775 /var/www/html/cache8. Script einmal ausführen
php /var/www/html/serverview/server_view_load_HL2.php9. Im Browser öffnen
https://deine-domain/cache/serverview/cs2.html10. Cronjob (automatisch alle 3 Minuten)
crontab -e
*/3 * * * * /usr/bin/php /var/www/html/serverview/server_view_load_HL2.php
Fertig
Du hast jetzt:
✔ Composer installiert
✔ xPaw installiert
✔ Template-System
✔ mehrere Server
✔ Cache-System
✔ automatische Updates -
Anleitung
BZ_Admin
BZ_Admin ist ein CounterStrikeSharp Plugin für Counter-Strike 2, das ein komplettes Admin-System mit Gruppen, Bans, Menüs und Mapverwaltung bereitstellt.
🔧 Installation
- Stelle sicher, dass folgende Komponenten installiert sind:
- Counter-Strike 2 Dedicated Server
- Metamod
- CounterStrikeSharp
- Kopiere die Plugin-Dateien nach:
- Starte den Server neu.
📁 Wichtige Dateien
Nach dem ersten Start werden automatisch erstellt:
Darin befinden sich:
- bz_admin.sqlite → Datenbank (Admins, Gruppen, Bans)
- changemap.txt → Mapliste
- Sprachdateien (de/en)
👑 Admin-System
Admins werden in Gruppen eingeteilt.
Standard-Gruppen
- #root → Vollzugriff
- #coadmin
- #moderator
- #supporter
🔐 Rechte (Flags)
Beispiele:
- @css/root → alles
- @css/ban
- @css/unban
- @css/kick
- @css/slay
- @css/changemap
- @css/adminmenu
⚙️ Admin-Befehle
Gruppen
Admins
Codecss_addadmin <name> <steamid64> <#gruppe> <immunitaet> css_deladmin <steamid64> css_setgroup <steamid64> <#gruppe> css_setimmunity <steamid64> <wert> css_setadminname <steamid64> <name> css_listadmins📋 Admin-Menü
Öffnet das komplette Admin-Menü im Chat:
- Ban Menü
- Kick Menü
- Slay Menü
- Map Menü
- Bannliste
- Mapliste
🔨 Spieler-Befehle
Kick
Slay
🔒 Bann-System
Spieler bannen
oder direkt:
Offline bannen
Entbannen
Listen
🗺️ Map-System
Standard Maps
Workshop Maps
Mapliste anzeigen
🔁 Konfiguration neu laden
📌 Hinweise
- Adminrechte basieren auf Gruppen und Flags
- Höhere Immunität kann niedrigere überschreiben
- Daten werden in SQLite gespeichert
- Alte Textdateien werden automatisch übernommen (falls vorhanden)
👤 Autor
ddbmaster
Mit einlesen von vorherige version.
admins.txt
ban-liste.txt
gruppen.txt
ban-zeiten.txt
ban-gruendeCode: Exemple
Alles anzeigenadmins # ID : Name : SteamID64 : Gruppe : Immunitaet : ErstelltAm 1 : ddbmaster : xxxxxxxxxxxxxxx : #root : 100 : 2025-10-29 2 : Nixdorf : xxxxxxxxxxxxxxx : #Betreuer : 50 : 2025-10-29 gruppen // Gruppenname : Flags #root : @css/root,@css/adminmenu #Betreuer : @css/ban,@css/unban,@css/kick,@css/slay,@css/changemap,@css/listmaps,@css/reloadconfigs,@css/adminmenu ban-zeiten # Anzeigename : Minuten Permanent : 0 30 Minuten : 30 1 Stunde : 60 6 Stunden : 360 ban-gruende # Anzeigename : Grund Passt nicht : Passt nicht zu uns. Cheating : Cheating Beleidigung : Beleidigung ban-liste # ID : Name : SteamID64 : Minuten : Grund : Admin : ErstelltAm 1 : Name : xxxxxxxxxxxxxxx : 0 : Umgehung eines Bans : Admin-Name :xxxxxxxxxxxxxxx : 2025-10-29 18:15:00, 2 : Name : xxxxxxxxxxxxxxx : 0 : Passt nicht zu uns : Admin-Name : xxxxxxxxxxxxxxx : 2025-10-29 18:30:00 - Stelle sicher, dass folgende Komponenten installiert sind:
-
BZ_Admin.csproj
C#: BZ_Admin.csproj
Alles anzeigen<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <AssemblyName>BZ_Admin</AssemblyName> <RootNamespace>BZ_Admin</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <DebugType>none</DebugType> <DebugSymbols>false</DebugSymbols> </PropertyGroup> <ItemGroup> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.363"> <PrivateAssets>none</PrivateAssets> <ExcludeAssets>runtime</ExcludeAssets> <IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> </ItemGroup> <ItemGroup> <None Update="lang\**\*.*" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> </Project>BZ_Admin.cs
C#: BZ_Admin.cs
Alles anzeigenusing CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Menu; using Microsoft.Extensions.Logging; using CounterStrikeSharp.API.Core.Attributes.Registration; using System; using System.Collections.Generic; using System.Data.SQLite; using System.Globalization; using System.IO; using System.Linq; namespace BZ_Admin { [MinimumApiVersion(80)] public class BZ_AdminPlugin : BasePlugin { public override string ModuleName => "BZ_Admin"; public override string ModuleVersion => "1.5.2"; public override string ModuleAuthor => "ddbmaster"; public override string ModuleDescription => "Admin, Gruppen, Bans und Mapwechsel für CS2 mit SQLite."; private readonly Dictionary<string, GroupInfo> _groups = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, AdminInfo> _admins = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _lang = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, MapEntry> _maps = new(StringComparer.OrdinalIgnoreCase); private readonly List<BanTimeEntry> _banTimes = new(); private readonly List<BanReasonEntry> _banReasons = new(); private readonly List<BanInfo> _bans = new(); private string CurrentLanguage = "de"; private string PluginDir => ModuleDirectory; private string LangDir => Path.Combine(PluginDir, "lang"); private string LangDeFilePath => Path.Combine(LangDir, "de.txt"); private string LangEnFilePath => Path.Combine(LangDir, "en.txt"); private string ConfigBaseDir => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", ModuleName); private string DatabaseFilePath => Path.Combine(ConfigBaseDir, "bz_admin.sqlite"); private string DatabaseConnectionString => $"Data Source={DatabaseFilePath};Version=3;"; private string LegacyAdminsFilePath => Path.Combine(ConfigBaseDir, "admins.txt"); private string LegacyGroupsFilePath => Path.Combine(ConfigBaseDir, "gruppen.txt"); private string LegacyBansFilePath => Path.Combine(ConfigBaseDir, "ban-liste.txt"); private string LegacyBanTimesFilePath => Path.Combine(ConfigBaseDir, "ban-zeiten.txt"); private string LegacyBanReasonsFilePath => Path.Combine(ConfigBaseDir, "ban-gruende.txt"); private string ChangeMapFilePath => Path.Combine(ConfigBaseDir, "changemap.txt"); public override void Load(bool hotReload) { Directory.CreateDirectory(PluginDir); Directory.CreateDirectory(LangDir); Directory.CreateDirectory(ConfigBaseDir); EnsureDefaultFiles(); InitializeDatabase(); SeedOrMigrateDatabaseIfNeeded(); LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); ExpireElapsedBans(); RegisterListener<Listeners.OnClientPutInServer>(OnClientPutInServer); Logger.LogInformation($"[BZ_Admin] Loaded (Lang={CurrentLanguage})"); Logger.LogInformation($"[BZ_Admin] PluginDir: {PluginDir}"); Logger.LogInformation($"[BZ_Admin] ConfigDir: {ConfigBaseDir}"); Logger.LogInformation($"[BZ_Admin] Database: {DatabaseFilePath}"); } private void EnsureDefaultFiles() { if (!File.Exists(ChangeMapFilePath)) { File.WriteAllLines(ChangeMapFilePath, new[] { "# Standard Maps", "de_dust2", "de_mirage", "de_inferno", "de_nuke", "de_train", "de_anubis", "de_overpass", "", "# Workshop Maps", "# Format: workshopid:mapname", "3671053668:de_rats_teatre", "3075012302:de_blackgold_d_prefab", "3070219079:de_santo", "3328169568:de_edin", "" }); } if (!File.Exists(LangDeFilePath)) { File.WriteAllText(LangDeFilePath, GetDefaultGermanLanguageFile()); } if (!File.Exists(LangEnFilePath)) { File.WriteAllText(LangEnFilePath, GetDefaultEnglishLanguageFile()); } } private string GetDefaultGermanLanguageFile() { return @"Usage_AddGroup = Benutzung: css_addgroup <#gruppenname> <flag1,flag2,...> Usage_DelGroup = Benutzung: css_delgroup <#gruppenname> GroupAdded = Gruppe {0} mit Flags hinzugefügt: {1} GroupExists = Gruppe {0} existiert bereits. GroupDeleted = Gruppe {0} wurde gelöscht. GroupNotExist = Gruppe {0} existiert nicht. NoGroups = Keine Gruppen vorhanden. Usage_AddAdmin = Benutzung: css_addadmin <name> <steamid64> <#gruppe> <immunitaet> Usage_DelAdmin = Benutzung: css_deladmin <steamid64> Usage_SetGroup = Benutzung: css_setgroup <steamid64> <#gruppe> Usage_SetImmunity = Benutzung: css_setimmunity <steamid64> <wert> Usage_SetAdminName = Benutzung: css_setadminname <steamid64> <name> AdminAdded = Admin {0} hinzugefügt zur Gruppe {1} mit Immunität {2}. AdminRemoved = Admin {0} wurde entfernt. AdminNotFound = Kein Admin mit SteamID {0} gefunden. AdminUpdatedGroup = Admin {0} wurde Gruppe {1} zugewiesen. AdminUpdatedImmunity = Admin {0} hat jetzt Immunität {1}. AdminUpdatedName = Admin {0} wurde umbenannt in {1}. NoAdmins = Keine Admins vorhanden. AdminSelfGroup = {0} Du bist jetzt {1} Usage_Ban = Benutzung: css_ban oder css_ban <steamid64> <minuten> <grund> Usage_BanId = Benutzung: css_banid <steamid64> <minuten> <grund> Usage_Unban = Benutzung: css_unban <steamid64> BanAdded = Spieler {0} ({1}) wurde für {2} Minuten gebannt. Grund: {3} von {4} BanRemoved = Ban entfernt: {0} NoBanFound = Kein aktiver Ban für SteamID {0} gefunden. NoBans = Keine Bans vorhanden. NoBanTimes = Keine Ban-Zeiten vorhanden. NoBanReasons = Keine Ban-Gründe vorhanden. BanAlreadyActive = Für SteamID {0} existiert bereits ein aktiver Ban. BanMenuTitlePlayers = Spieler für Ban auswählen BanMenuTitleTimes = Ban-Zeit für {0} auswählen BanMenuTitleReasons = Ban-Grund für {0} auswählen BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} Minuten) BanMenuReasonEntry = {0} BanMenuNoPlayers = Keine Spieler gefunden. BanJoinDenied = Du bist gebannt. Grund: {0}. Bitte melde dich bei {1} BanContact = ddbmaster.de BanStatusActive = Aktiv BanStatusRemoved = Entbannt BanStatusExpired = Abgelaufen BanDeletedBy = entfernt von {0} BanListEntry = {0} | {1} | {2} | {3} | {4} KickDenied = Du kannst diesen Spieler nicht kicken: Ziel hat gleiche oder höhere Immunität. KickDone = Spieler {0} wurde gekickt. Usage_Kick = Benutzung: css_kick oder css_kick <steamid64> KickMenuTitlePlayers = Spieler für Kick auswählen KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = Keine Spieler gefunden. SlayDenied = Du kannst diesen Spieler nicht töten: Ziel hat gleiche oder höhere Immunität. SlayDone = Spieler {0} wurde getötet. TargetNotFound = Zielspieler nicht gefunden. Usage_Slay = Benutzung: css_slay oder css_slay <steamid64> SlayMenuTitlePlayers = Spieler für Slay auswählen SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = Keine Spieler gefunden. Usage_Map = Benutzung: css_map oder css_map <mapname|workshopid> Usage_WsMap = Benutzung: css_wsmap oder css_wsmap <mapname|workshopid> MapChanged = {0} wechselt die Map nach {1}. MapNotFound = Die Map oder Workshop-Map ""{0}"" wurde nicht gefunden. NoMaps = Keine Maps in changemap.txt vorhanden. MapListHeader = Verfügbare Maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Map auswählen MapMenuTitleWorkshop = Workshop-Map auswählen MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = Keine Standard-Maps gefunden. MapMenuNoWorkshopMaps = Keine Workshop-Maps gefunden. Usage_ListBanTimes = Benutzung: css_listbantimes BanTimeHeader = Verfügbare Ban-Zeiten: BanTimeEntry = {0} = {1} Minuten Usage_ListBanReasons = Benutzung: css_listbanreasons BanReasonHeader = Verfügbare Ban-Gründe: BanReasonEntry = {0} = {1} Usage_AdminMenu = Benutzung: css_adminmenu AdminMenuTitle = BZ Admin Menü AdminMenuBan = Ban Menü AdminMenuKick = Kick Menü AdminMenuSlay = Slay Menü AdminMenuMap = Standard Map Menü AdminMenuWsMap = Workshop Map Menü AdminMenuListMaps = Mapliste anzeigen AdminMenuListBans = Bannliste anzeigen AdminMenuListBanTimes = Ban-Zeiten anzeigen AdminMenuListBanReasons = Ban-Gründe anzeigen ConfigsReloaded = BZ_Admin Konfigurationen wurden neu geladen. LanguageLoaded = Sprache geladen: {0} InvalidSteamId64 = Ungültige SteamID64. InvalidImmunity = Ungültiger Immunitätswert. InvalidBanMinutes = Ungültige Ban-Dauer in Minuten. PlayerOnlyCommand = Dieser Befehl muss von einem Spieler ausgeführt werden. BanDenied = Du kannst diesen Spieler nicht bannen: Ziel hat gleiche oder höhere Immunität. NoPermission = Du hast keine Berechtigung für diesen Befehl."; } private string GetDefaultEnglishLanguageFile() { return @"Usage_AddGroup = Usage: css_addgroup <#groupname> <flag1,flag2,...> Usage_DelGroup = Usage: css_delgroup <#groupname> GroupAdded = Group {0} added with flags: {1} GroupExists = Group {0} already exists. GroupDeleted = Group {0} has been deleted. GroupNotExist = Group {0} does not exist. NoGroups = No groups defined. Usage_AddAdmin = Usage: css_addadmin <name> <steamid64> <#group> <immunity> Usage_DelAdmin = Usage: css_deladmin <steamid64> Usage_SetGroup = Usage: css_setgroup <steamid64> <#group> Usage_SetImmunity = Usage: css_setimmunity <steamid64> <value> Usage_SetAdminName = Usage: css_setadminname <steamid64> <name> AdminAdded = Admin {0} added to {1} with immunity {2}. AdminRemoved = Admin {0} has been removed. AdminNotFound = Admin with SteamID {0} not found. AdminUpdatedGroup = Admin {0} has been assigned to group {1}. AdminUpdatedImmunity = Admin {0} now has immunity {1}. AdminUpdatedName = Admin {0} has been renamed to {1}. NoAdmins = No admins defined. AdminSelfGroup = {0} You are now {1} Usage_Ban = Usage: css_ban or css_ban <steamid64> <minutes> <reason> Usage_BanId = Usage: css_banid <steamid64> <minutes> <reason> Usage_Unban = Usage: css_unban <steamid64> BanAdded = Player {0} ({1}) has been banned for {2} minutes. Reason: {3} by {4} BanRemoved = Ban removed: {0} NoBanFound = No active ban found for SteamID {0}. NoBans = No bans recorded. NoBanTimes = No ban times found. NoBanReasons = No ban reasons found. BanAlreadyActive = There is already an active ban for SteamID {0}. BanMenuTitlePlayers = Select player to ban BanMenuTitleTimes = Select ban time for {0} BanMenuTitleReasons = Select ban reason for {0} BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} minutes) BanMenuReasonEntry = {0} BanMenuNoPlayers = No players found. BanJoinDenied = You are banned. Reason: {0}. Please contact {1} BanContact = ddbmaster.de BanStatusActive = Active BanStatusRemoved = Unbanned BanStatusExpired = Expired BanDeletedBy = removed by {0} BanListEntry = {0} | {1} | {2} | {3} | {4} KickDenied = You cannot kick this player: target has equal or higher immunity. KickDone = Player {0} has been kicked. Usage_Kick = Usage: css_kick or css_kick <steamid64> KickMenuTitlePlayers = Select player to kick KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = No players found. SlayDenied = You cannot slay this player: target has equal or higher immunity. SlayDone = Player {0} has been slain. TargetNotFound = Target player not found. Usage_Slay = Usage: css_slay or css_slay <steamid64> SlayMenuTitlePlayers = Select player to slay SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = No players found. Usage_Map = Usage: css_map or css_map <mapname|workshopid> Usage_WsMap = Usage: css_wsmap or css_wsmap <mapname|workshopid> MapChanged = {0} changed the map to {1}. MapNotFound = Map or workshop map ""{0}"" not found. NoMaps = No maps found in changemap.txt. MapListHeader = Available maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Select map MapMenuTitleWorkshop = Select workshop map MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = No standard maps found. MapMenuNoWorkshopMaps = No workshop maps found. Usage_ListBanTimes = Usage: css_listbantimes BanTimeHeader = Available ban times: BanTimeEntry = {0} = {1} minutes Usage_ListBanReasons = Usage: css_listbanreasons BanReasonHeader = Available ban reasons: BanReasonEntry = {0} = {1} Usage_AdminMenu = Usage: css_adminmenu AdminMenuTitle = BZ Admin Menu AdminMenuBan = Ban Menu AdminMenuKick = Kick Menu AdminMenuSlay = Slay Menu AdminMenuMap = Standard Map Menu AdminMenuWsMap = Workshop Map Menu AdminMenuListMaps = Show map list AdminMenuListBans = Show ban list AdminMenuListBanTimes = Show ban times AdminMenuListBanReasons = Show ban reasons ConfigsReloaded = BZ_Admin configurations reloaded. LanguageLoaded = Language loaded: {0} InvalidSteamId64 = Invalid SteamID64. InvalidImmunity = Invalid immunity value. InvalidBanMinutes = Invalid ban duration in minutes. PlayerOnlyCommand = This command must be executed by a player. BanDenied = You cannot ban this player: target has equal or higher immunity. NoPermission = You do not have permission for this command."; } private void LoadLanguage(string langCode) { string langFile = Path.Combine(LangDir, $"{langCode}.txt"); _lang.Clear(); if (!File.Exists(langFile)) { Logger.LogWarning($"[BZ_Admin] Language file missing: {langFile}"); return; } foreach (var rawLine in File.ReadAllLines(langFile)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split('=', 2); if (parts.Length == 2) _lang[parts[0].Trim()] = parts[1].Trim(); } Logger.LogInformation($"[BZ_Admin] {T("LanguageLoaded", langCode)}"); } private string T(string key, params object[] args) { if (_lang.TryGetValue(key, out var fmt)) return args.Length > 0 ? string.Format(fmt, args) : fmt; return key; } private void OnClientPutInServer(int slot) { var player = Utilities.GetPlayerFromSlot(slot); if (player == null || !player.IsValid || player.IsBot) return; string steamId = player.SteamID.ToString(); var activeBan = GetEffectiveActiveBanBySteamId(steamId); if (activeBan != null) { Server.NextFrame(() => { if (player == null || !player.IsValid) return; string kickMessage = T("BanJoinDenied", activeBan.Reason, T("BanContact")); string escaped = EscapeServerCommandArgument(kickMessage); Server.ExecuteCommand($"kickid {steamId} \"{escaped}\""); }); return; } if (!_admins.TryGetValue(steamId, out var admin)) return; Server.NextFrame(() => { if (player == null || !player.IsValid) return; player.PrintToChat($" {T("AdminSelfGroup", admin.Name, admin.GroupName)}"); }); } private SQLiteConnection GetConnection() { var connection = new SQLiteConnection(DatabaseConnectionString); connection.Open(); return connection; } private void InitializeDatabase() { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" CREATE TABLE IF NOT EXISTS bz_groups ( group_name TEXT PRIMARY KEY, flags TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS bz_admins ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, steamid64 TEXT NOT NULL UNIQUE, group_name TEXT NOT NULL, immunity INTEGER NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS bz_bans ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, steamid64 TEXT NOT NULL, duration_minutes INTEGER NOT NULL, reason TEXT NOT NULL, admin_name TEXT NOT NULL, created_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, deleted_at TEXT NULL, deleted_by TEXT NULL ); CREATE TABLE IF NOT EXISTS bz_ban_times ( display_name TEXT PRIMARY KEY, minutes INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS bz_ban_reasons ( display_name TEXT PRIMARY KEY, reason TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_bz_bans_steamid64 ON bz_bans (steamid64); CREATE INDEX IF NOT EXISTS idx_bz_bans_active ON bz_bans (steamid64, is_active); "; command.ExecuteNonQuery(); } private void SeedOrMigrateDatabaseIfNeeded() { using var connection = GetConnection(); SeedOrMigrateGroups(connection); SeedOrMigrateAdmins(connection); SeedOrMigrateBanTimes(connection); SeedOrMigrateBanReasons(connection); SeedOrMigrateBans(connection); } private void SeedOrMigrateGroups(SQLiteConnection connection) { if (GetTableCount(connection, "bz_groups") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyGroupsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyGroupsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var groupName = parts[0].Trim(); var flagsPart = parts[1].Trim(); if (!groupName.StartsWith("#")) continue; InsertGroup(connection, groupName, flagsPart, transaction); imported = true; } } if (!imported) { InsertGroup(connection, "#root", "@css/root", transaction); InsertGroup(connection, "#coadmin", "@css/ban,@css/unban,@css/kick,@css/slay,@css/changemap,@css/listmaps,@css/reloadconfigs,@css/adminmenu", transaction); InsertGroup(connection, "#moderator", "@css/kick,@css/slay,@css/adminmenu", transaction); InsertGroup(connection, "#supporter", "@css/listmaps", transaction); } transaction.Commit(); } private void SeedOrMigrateAdmins(SQLiteConnection connection) { if (GetTableCount(connection, "bz_admins") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyAdminsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyAdminsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 6) continue; if (!int.TryParse(parts[0], out var id)) continue; if (!int.TryParse(parts[4], out var immunity)) continue; InsertAdmin(connection, new AdminInfo { Id = id, Name = parts[1], SteamId = parts[2], GroupName = parts[3], Immunity = immunity, CreatedAt = parts[5] }, transaction); imported = true; } } if (!imported) { InsertAdmin(connection, new AdminInfo { Id = 1, Name = "ddbmaster", SteamId = "76561197960398110", GroupName = "#root", Immunity = 100, CreatedAt = "2025-10-29" }, transaction); InsertAdmin(connection, new AdminInfo { Id = 2, Name = "Nixdorf", SteamId = "76561197965022135", GroupName = "#coadmin", Immunity = 50, CreatedAt = "2025-10-29" }, transaction); } transaction.Commit(); } private void SeedOrMigrateBanTimes(SQLiteConnection connection) { if (GetTableCount(connection, "bz_ban_times") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyBanTimesFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBanTimesFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; if (!int.TryParse(parts[1], out var minutes)) continue; InsertBanTime(connection, parts[0].Trim(), minutes, transaction); imported = true; } } if (!imported) { InsertBanTime(connection, "Permanent", 0, transaction); InsertBanTime(connection, "30 Minuten", 30, transaction); InsertBanTime(connection, "1 Stunde", 60, transaction); InsertBanTime(connection, "6 Stunden", 360, transaction); InsertBanTime(connection, "12 Stunden", 720, transaction); InsertBanTime(connection, "1 Tag", 1440, transaction); InsertBanTime(connection, "3 Tage", 4320, transaction); InsertBanTime(connection, "1 Woche", 10080, transaction); InsertBanTime(connection, "2 Wochen", 20160, transaction); } transaction.Commit(); } private void SeedOrMigrateBanReasons(SQLiteConnection connection) { if (GetTableCount(connection, "bz_ban_reasons") > 0) return; using var transaction = connection.BeginTransaction(); bool imported = false; if (File.Exists(LegacyBanReasonsFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBanReasonsFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; InsertBanReason(connection, parts[0].Trim(), parts[1].Trim(), transaction); imported = true; } } if (!imported) { InsertBanReason(connection, "Cheating", "Cheating", transaction); InsertBanReason(connection, "Beleidigung", "Beleidigung", transaction); InsertBanReason(connection, "Teamkill", "Teamkill", transaction); InsertBanReason(connection, "Griefing", "Griefing", transaction); InsertBanReason(connection, "AFK", "AFK", transaction); InsertBanReason(connection, "Spam", "Spam", transaction); InsertBanReason(connection, "Sonstiges", "Sonstiges", transaction); } transaction.Commit(); } private void SeedOrMigrateBans(SQLiteConnection connection) { if (GetTableCount(connection, "bz_bans") > 0) return; using var transaction = connection.BeginTransaction(); if (File.Exists(LegacyBansFilePath)) { foreach (var rawLine in File.ReadAllLines(LegacyBansFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 7) continue; if (!int.TryParse(parts[0], out var id)) continue; if (!int.TryParse(parts[3], out var minutes)) continue; InsertBan(connection, new BanInfo { Id = id, Name = parts[1], SteamId = parts[2], DurationMinutes = minutes, Reason = parts[4], AdminName = parts[5], CreatedAt = parts[6], IsActive = true }, transaction); } } transaction.Commit(); } private int GetTableCount(SQLiteConnection connection, string tableName) { using var command = connection.CreateCommand(); command.CommandText = $"SELECT COUNT(*) FROM {tableName};"; return Convert.ToInt32(command.ExecuteScalar()); } private void LoadGroups() { _groups.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT group_name, flags FROM bz_groups ORDER BY group_name ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { var groupName = reader.GetString(0); var flagsPart = reader.GetString(1); var flags = flagsPart .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToList(); _groups[groupName] = new GroupInfo { GroupName = groupName, Flags = flags }; } Logger.LogInformation($"[BZ_Admin] Loaded groups: {_groups.Count}"); } private void LoadAdmins() { _admins.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT id, name, steamid64, group_name, immunity, created_at FROM bz_admins ORDER BY id ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { var admin = new AdminInfo { Id = reader.GetInt32(0), Name = reader.GetString(1), SteamId = reader.GetString(2), GroupName = reader.GetString(3), Immunity = reader.GetInt32(4), CreatedAt = reader.GetString(5) }; _admins[admin.SteamId] = admin; } } private void LoadBans() { _bans.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" SELECT id, name, steamid64, duration_minutes, reason, admin_name, created_at, is_active, deleted_at, deleted_by FROM bz_bans ORDER BY id ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _bans.Add(new BanInfo { Id = reader.GetInt32(0), Name = reader.GetString(1), SteamId = reader.GetString(2), DurationMinutes = reader.GetInt32(3), Reason = reader.GetString(4), AdminName = reader.GetString(5), CreatedAt = reader.GetString(6), IsActive = reader.GetInt32(7) == 1, DeletedAt = reader.IsDBNull(8) ? null : reader.GetString(8), DeletedBy = reader.IsDBNull(9) ? null : reader.GetString(9) }); } } private void LoadBanTimes() { _banTimes.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT display_name, minutes FROM bz_ban_times ORDER BY minutes ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _banTimes.Add(new BanTimeEntry { DisplayName = reader.GetString(0), Minutes = reader.GetInt32(1) }); } } private void LoadBanReasons() { _banReasons.Clear(); using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "SELECT display_name, reason FROM bz_ban_reasons ORDER BY display_name ASC;"; using var reader = command.ExecuteReader(); while (reader.Read()) { _banReasons.Add(new BanReasonEntry { DisplayName = reader.GetString(0), Reason = reader.GetString(1) }); } } private void LoadMaps() { _maps.Clear(); if (!File.Exists(ChangeMapFilePath)) return; foreach (var rawLine in File.ReadAllLines(ChangeMapFilePath)) { var line = rawLine.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; if (line.Contains(':')) { var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var workshopId = parts[0].Trim(); var mapName = parts[1].Trim().ToLowerInvariant(); if (!ulong.TryParse(workshopId, out _)) continue; var entry = new MapEntry { IsWorkshop = true, WorkshopId = workshopId, MapName = mapName }; _maps[workshopId] = entry; _maps[mapName] = entry; } else { var mapName = line.ToLowerInvariant(); _maps[mapName] = new MapEntry { IsWorkshop = false, MapName = mapName }; } } } private AdminInfo? GetAdminInfo(string steamId) { return _admins.TryGetValue(steamId, out var admin) ? admin : null; } private string GetConfiguredAdminName(CCSPlayerController? player) { if (player == null) return "Console"; var steamId = player.SteamID.ToString(); var admin = GetAdminInfo(steamId); return admin?.Name ?? player.PlayerName; } private bool HasPermission(CCSPlayerController? player, string permission) { if (player == null) return true; var steamId = player.SteamID.ToString(); if (!_admins.TryGetValue(steamId, out var admin)) return false; if (!_groups.TryGetValue(admin.GroupName, out var group)) return false; if (group.Flags.Any(x => x.Equals("@css/root", StringComparison.OrdinalIgnoreCase))) return true; return group.Flags.Any(x => x.Equals(permission, StringComparison.OrdinalIgnoreCase)); } private bool CanTarget(string executorId, string targetId) { var exec = GetAdminInfo(executorId); var targ = GetAdminInfo(targetId); if (exec == null) return false; if (targ == null) return true; return exec.Immunity > targ.Immunity; } private bool EnsurePermission(CCSPlayerController? player, CommandInfo commandInfo, string permission) { if (HasPermission(player, permission)) return true; commandInfo.ReplyToCommand(T("NoPermission")); return false; } [ConsoleCommand("css_addgroup", "Add a new admin group")] public void CmdAddGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_AddGroup")); return; } string groupName = commandInfo.GetArg(1); string flagsCsv = commandInfo.GetArg(2); if (!groupName.StartsWith("#")) { commandInfo.ReplyToCommand("Group names must start with '#'."); return; } if (_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupExists", groupName)); return; } var flags = flagsCsv .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); var group = new GroupInfo { GroupName = groupName, Flags = flags }; InsertGroup(group); _groups[groupName] = group; commandInfo.ReplyToCommand(T("GroupAdded", groupName, string.Join(", ", flags))); } [ConsoleCommand("css_delgroup", "Delete a group")] public void CmdDelGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_DelGroup")); return; } string groupName = commandInfo.GetArg(1); if (!_groups.Remove(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } DeleteGroup(groupName); commandInfo.ReplyToCommand(T("GroupDeleted", groupName)); } [ConsoleCommand("css_listgroups", "List groups")] public void CmdListGroups(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (_groups.Count == 0) { commandInfo.ReplyToCommand(T("NoGroups")); return; } foreach (var group in _groups.OrderBy(x => x.Key)) { string flags = group.Value.Flags.Count > 0 ? string.Join(", ", group.Value.Flags) : "(no flags)"; commandInfo.ReplyToCommand($"{group.Key}: {flags}"); } } [ConsoleCommand("css_addadmin", "Add an admin")] public void CmdAddAdmin(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 5) { commandInfo.ReplyToCommand(T("Usage_AddAdmin")); return; } string adminName = commandInfo.GetArg(1); string steamId = commandInfo.GetArg(2); string groupName = commandInfo.GetArg(3); string immunityArg = commandInfo.GetArg(4); if (!ulong.TryParse(steamId, out _)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (!_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } if (!int.TryParse(immunityArg, out int immunity)) { commandInfo.ReplyToCommand(T("InvalidImmunity")); return; } int nextId; string createdAt; if (_admins.TryGetValue(steamId, out var existing)) { nextId = existing.Id; createdAt = existing.CreatedAt; } else { nextId = GetNextAdminId(); createdAt = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } var admin = new AdminInfo { Id = nextId, Name = adminName, SteamId = steamId, GroupName = groupName, Immunity = immunity, CreatedAt = createdAt }; UpsertAdmin(admin); _admins[steamId] = admin; commandInfo.ReplyToCommand(T("AdminAdded", adminName, groupName, immunity)); } [ConsoleCommand("css_deladmin", "Delete an admin")] public void CmdDelAdmin(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_DelAdmin")); return; } string steamId = commandInfo.GetArg(1); if (!_admins.Remove(steamId)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } DeleteAdmin(steamId); commandInfo.ReplyToCommand(T("AdminRemoved", steamId)); } [ConsoleCommand("css_setgroup", "Set admin group")] public void CmdSetGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetGroup")); return; } string steamId = commandInfo.GetArg(1); string groupName = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } if (!_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } admin.GroupName = groupName; UpsertAdmin(admin); commandInfo.ReplyToCommand(T("AdminUpdatedGroup", admin.Name, groupName)); } [ConsoleCommand("css_setimmunity", "Set admin immunity")] public void CmdSetImmunity(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetImmunity")); return; } string steamId = commandInfo.GetArg(1); string immunityArg = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } if (!int.TryParse(immunityArg, out int immunity)) { commandInfo.ReplyToCommand(T("InvalidImmunity")); return; } admin.Immunity = immunity; UpsertAdmin(admin); commandInfo.ReplyToCommand(T("AdminUpdatedImmunity", admin.Name, immunity)); } [ConsoleCommand("css_setadminname", "Set admin display name")] public void CmdSetAdminName(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetAdminName")); return; } string steamId = commandInfo.GetArg(1); string newName = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } string oldName = admin.Name; admin.Name = newName; UpsertAdmin(admin); commandInfo.ReplyToCommand(T("AdminUpdatedName", oldName, newName)); } [ConsoleCommand("css_listadmins", "List admins")] public void CmdListAdmins(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (_admins.Count == 0) { commandInfo.ReplyToCommand(T("NoAdmins")); return; } foreach (var admin in _admins.Values.OrderBy(x => x.Id)) { commandInfo.ReplyToCommand( $"{admin.Id} : {admin.Name} : {admin.SteamId} : {admin.GroupName} : {admin.Immunity} : {admin.CreatedAt}" ); } } [ConsoleCommand("css_adminmenu", "Open BZ admin menu")] public void CmdAdminMenu(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/adminmenu")) return; if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenAdminMenu(player); } private void OpenAdminMenu(CCSPlayerController admin) { var menu = new ChatMenu(T("AdminMenuTitle")); if (HasPermission(admin, "@css/ban")) menu.AddMenuOption(T("AdminMenuBan"), (player, option) => OpenBanPlayerMenu(player)); if (HasPermission(admin, "@css/kick")) menu.AddMenuOption(T("AdminMenuKick"), (player, option) => OpenKickMenu(player)); if (HasPermission(admin, "@css/slay")) menu.AddMenuOption(T("AdminMenuSlay"), (player, option) => OpenSlayMenu(player)); if (HasPermission(admin, "@css/changemap")) { menu.AddMenuOption(T("AdminMenuMap"), (player, option) => OpenMapMenu(player, false)); menu.AddMenuOption(T("AdminMenuWsMap"), (player, option) => OpenMapMenu(player, true)); } if (HasPermission(admin, "@css/listmaps")) { menu.AddMenuOption(T("AdminMenuListMaps"), (player, option) => RunListMapsToPlayer(player)); } if (HasPermission(admin, "@css/ban")) { menu.AddMenuOption(T("AdminMenuListBans"), (player, option) => RunListBansToPlayer(player)); menu.AddMenuOption(T("AdminMenuListBanTimes"), (player, option) => RunListBanTimesToPlayer(player)); menu.AddMenuOption(T("AdminMenuListBanReasons"), (player, option) => RunListBanReasonsToPlayer(player)); } MenuManager.OpenChatMenu(admin, menu); } private void RunListMapsToPlayer(CCSPlayerController player) { if (_maps.Count == 0) { player.PrintToChat($" {T("NoMaps")}"); return; } player.PrintToChat($" {T("MapListHeader")}"); var printedStandard = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var printedWorkshop = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var kvp in _maps.OrderBy(x => x.Key)) { var map = kvp.Value; if (map.IsWorkshop) { if (printedWorkshop.Contains(map.WorkshopId)) continue; printedWorkshop.Add(map.WorkshopId); player.PrintToChat($" {T("MapEntryWorkshop", map.MapName, map.WorkshopId)}"); } else { if (printedStandard.Contains(map.MapName)) continue; printedStandard.Add(map.MapName); player.PrintToChat($" {T("MapEntryStandard", map.MapName)}"); } } } private void RunListBansToPlayer(CCSPlayerController player) { ExpireElapsedBans(); if (_bans.Count == 0) { player.PrintToChat($" {T("NoBans")}"); return; } foreach (var ban in _bans.OrderBy(x => x.Id)) { player.PrintToChat($" {FormatBanListEntry(ban)}"); } } private void RunListBanTimesToPlayer(CCSPlayerController player) { if (_banTimes.Count == 0) { player.PrintToChat($" {T("NoBanTimes")}"); return; } player.PrintToChat($" {T("BanTimeHeader")}"); foreach (var entry in _banTimes) { player.PrintToChat($" {T("BanTimeEntry", entry.DisplayName, entry.Minutes)}"); } } private void RunListBanReasonsToPlayer(CCSPlayerController player) { if (_banReasons.Count == 0) { player.PrintToChat($" {T("NoBanReasons")}"); return; } player.PrintToChat($" {T("BanReasonHeader")}"); foreach (var entry in _banReasons) { player.PrintToChat($" {T("BanReasonEntry", entry.DisplayName, entry.Reason)}"); } } [ConsoleCommand("css_ban", "Ban menu or direct ban")] public void CmdBan(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenBanPlayerMenu(player); return; } if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_Ban")); return; } if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } string executorSteamId = player.SteamID.ToString(); string executorName = GetConfiguredAdminName(player); string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); if (!CanTarget(executorSteamId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanDenied")); return; } if (HasActiveBan(targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanAlreadyActive", targetSteamId.ToString())); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); string targetName = targetPlayer?.PlayerName ?? "OhneName"; var ban = AddBan(targetName, targetSteamId.ToString(), minutes, reason, executorName); commandInfo.ReplyToCommand(T("BanAdded", ban.Name, ban.SteamId, ban.DurationMinutes, ban.Reason, ban.AdminName)); try { Server.ExecuteCommand($"kickid {ban.SteamId}"); } catch { } } [ConsoleCommand("css_banid", "Ban offline or online by steamid64")] public void CmdBanId(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_BanId")); return; } string executorName = GetConfiguredAdminName(player); string executorSteamId = player?.SteamID.ToString() ?? string.Empty; string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); if (player != null && !CanTarget(executorSteamId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanDenied")); return; } if (HasActiveBan(targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanAlreadyActive", targetSteamId.ToString())); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); string targetName = targetPlayer?.PlayerName ?? "OhneName"; var ban = AddBan(targetName, targetSteamId.ToString(), minutes, reason, executorName); commandInfo.ReplyToCommand(T("BanAdded", ban.Name, ban.SteamId, ban.DurationMinutes, ban.Reason, ban.AdminName)); try { Server.ExecuteCommand($"kickid {ban.SteamId}"); } catch { } } [ConsoleCommand("css_unban", "Unban a player")] public void CmdUnban(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/unban")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Unban")); return; } string steamIdArg = commandInfo.GetArg(1); string deletedBy = GetConfiguredAdminName(player); var ban = GetLatestActiveBanBySteamId(steamIdArg); if (ban == null) { commandInfo.ReplyToCommand(T("NoBanFound", steamIdArg)); return; } UnbanInternal(ban, deletedBy); commandInfo.ReplyToCommand(T("BanRemoved", steamIdArg)); } [ConsoleCommand("css_listbans", "List bans")] public void CmdListBans(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; ExpireElapsedBans(); if (_bans.Count == 0) { commandInfo.ReplyToCommand(T("NoBans")); return; } foreach (var ban in _bans.OrderBy(x => x.Id)) { commandInfo.ReplyToCommand(FormatBanListEntry(ban)); } } [ConsoleCommand("css_listbantimes", "List configured ban times")] public void CmdListBanTimes(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (_banTimes.Count == 0) { commandInfo.ReplyToCommand(T("NoBanTimes")); return; } commandInfo.ReplyToCommand(T("BanTimeHeader")); foreach (var entry in _banTimes) { commandInfo.ReplyToCommand(T("BanTimeEntry", entry.DisplayName, entry.Minutes)); } } [ConsoleCommand("css_listbanreasons", "List configured ban reasons")] public void CmdListBanReasons(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (_banReasons.Count == 0) { commandInfo.ReplyToCommand(T("NoBanReasons")); return; } commandInfo.ReplyToCommand(T("BanReasonHeader")); foreach (var entry in _banReasons) { commandInfo.ReplyToCommand(T("BanReasonEntry", entry.DisplayName, entry.Reason)); } } private void OpenBanPlayerMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("BanMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("BanMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("BanMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("BanDenied")}"); return; } if (HasActiveBan(targetId)) { player.PrintToChat($" {T("BanAlreadyActive", targetId)}"); return; } OpenBanTimeMenu(player, target); }); index++; } MenuManager.OpenChatMenu(admin, menu); } private void OpenBanTimeMenu(CCSPlayerController admin, CCSPlayerController target) { if (_banTimes.Count == 0) { admin.PrintToChat($" {T("NoBanTimes")}"); return; } var menu = new ChatMenu(T("BanMenuTitleTimes", target.PlayerName)); foreach (var time in _banTimes) { string display = T("BanMenuTimeEntry", time.DisplayName, time.Minutes); menu.AddMenuOption(display, (player, option) => OpenBanReasonMenu(player, target, time)); } MenuManager.OpenChatMenu(admin, menu); } private void OpenBanReasonMenu(CCSPlayerController admin, CCSPlayerController target, BanTimeEntry time) { if (_banReasons.Count == 0) { admin.PrintToChat($" {T("NoBanReasons")}"); return; } var menu = new ChatMenu(T("BanMenuTitleReasons", target.PlayerName)); foreach (var reason in _banReasons) { string display = T("BanMenuReasonEntry", reason.DisplayName); menu.AddMenuOption(display, (player, option) => { string adminName = GetConfiguredAdminName(player); string targetName = string.IsNullOrWhiteSpace(target.PlayerName) ? "OhneName" : target.PlayerName; string targetSteamId = target.SteamID.ToString(); if (HasActiveBan(targetSteamId)) { player.PrintToChat($" {T("BanAlreadyActive", targetSteamId)}"); return; } var ban = AddBan(targetName, targetSteamId, time.Minutes, reason.Reason, adminName); player.PrintToChat($" {T("BanAdded", ban.Name, ban.SteamId, ban.DurationMinutes, ban.Reason, ban.AdminName)}"); try { Server.ExecuteCommand($"kickid {targetSteamId}"); } catch { } }); } MenuManager.OpenChatMenu(admin, menu); } private BanInfo AddBan(string targetName, string targetSteamId, int minutes, string reason, string executorName) { var ban = new BanInfo { Id = GetNextBanId(), Name = string.IsNullOrWhiteSpace(targetName) ? "OhneName" : targetName, SteamId = targetSteamId, DurationMinutes = minutes, Reason = reason, AdminName = executorName, CreatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture), IsActive = true }; InsertBan(ban); _bans.Add(ban); return ban; } private void UnbanInternal(BanInfo ban, string deletedBy) { ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = deletedBy; UpdateBanStatus(ban); } private bool HasActiveBan(string steamId) { return GetEffectiveActiveBanBySteamId(steamId) != null; } private BanInfo? GetLatestActiveBanBySteamId(string steamId) { return _bans .Where(x => x.SteamId.Equals(steamId, StringComparison.OrdinalIgnoreCase) && x.IsActive) .OrderByDescending(x => x.Id) .FirstOrDefault(); } private BanInfo? GetEffectiveActiveBanBySteamId(string steamId) { var activeBan = GetLatestActiveBanBySteamId(steamId); if (activeBan == null) return null; if (IsBanExpired(activeBan)) { ExpireBan(activeBan); return null; } return activeBan; } private bool IsBanExpired(BanInfo ban) { if (!ban.IsActive || ban.DurationMinutes <= 0) return false; if (!DateTime.TryParseExact( ban.CreatedAt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var createdAt)) return false; var expiresAt = createdAt.AddMinutes(ban.DurationMinutes); return DateTime.Now >= expiresAt; } private void ExpireBan(BanInfo ban) { ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = "AUTO"; UpdateBanStatus(ban); } private void ExpireElapsedBans() { foreach (var ban in _bans.Where(x => x.IsActive).ToList()) { if (!IsBanExpired(ban)) continue; ban.IsActive = false; ban.DeletedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); ban.DeletedBy = "AUTO"; UpdateBanStatus(ban); } } private string FormatBanListEntry(BanInfo ban) { string status = ban.IsActive ? T("BanStatusActive") : string.Equals(ban.DeletedBy, "AUTO", StringComparison.OrdinalIgnoreCase) ? T("BanStatusExpired") : T("BanStatusRemoved"); return T("BanListEntry", ban.Name, ban.SteamId, ban.Reason, ban.AdminName, status); } [ConsoleCommand("css_kick", "Kick a player")] public void CmdKick(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/kick")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenKickMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Kick")); return; } string executorId = player?.SteamID.ToString() ?? ""; string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (player != null && !CanTarget(executorId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("KickDenied")); return; } commandInfo.ReplyToCommand(T("KickDone", targetSteamId)); Server.ExecuteCommand($"kickid {targetSteamId}"); } [ConsoleCommand("css_slay", "Slay a player")] public void CmdSlay(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/slay")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenSlayMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Slay")); return; } string executorId = player?.SteamID.ToString() ?? ""; string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (player != null && !CanTarget(executorId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("SlayDenied")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (targetPlayer == null || !targetPlayer.IsValid || targetPlayer.Pawn == null || !targetPlayer.Pawn.IsValid) { commandInfo.ReplyToCommand(T("TargetNotFound")); return; } targetPlayer.Pawn.Value?.CommitSuicide(false, true); commandInfo.ReplyToCommand(T("SlayDone", targetSteamId)); } private void OpenKickMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("KickMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("KickMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("KickMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("KickDenied")}"); return; } Server.ExecuteCommand($"kickid {target.SteamID}"); player.PrintToChat($" {T("KickDone", target.SteamID)}"); }); index++; } MenuManager.OpenChatMenu(admin, menu); } private void OpenSlayMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("SlayMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("SlayMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("SlayMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("SlayDenied")}"); return; } if (!target.IsValid || target.Pawn == null || !target.Pawn.IsValid) { player.PrintToChat($" {T("TargetNotFound")}"); return; } target.Pawn.Value?.CommitSuicide(false, true); player.PrintToChat($" {T("SlayDone", target.SteamID)}"); }); index++; } MenuManager.OpenChatMenu(admin, menu); } [ConsoleCommand("css_map", "Change map")] public void CmdMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, false); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, false); } [ConsoleCommand("css_wsmap", "Change workshop map")] public void CmdWorkshopMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, true); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, true); } [ConsoleCommand("css_listmaps", "List maps from changemap.txt")] public void CmdListMaps(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/listmaps")) return; if (_maps.Count == 0) { commandInfo.ReplyToCommand(T("NoMaps")); return; } commandInfo.ReplyToCommand(T("MapListHeader")); var printedStandard = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var printedWorkshop = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var kvp in _maps.OrderBy(x => x.Key)) { var map = kvp.Value; if (map.IsWorkshop) { if (printedWorkshop.Contains(map.WorkshopId)) continue; printedWorkshop.Add(map.WorkshopId); commandInfo.ReplyToCommand(T("MapEntryWorkshop", map.MapName, map.WorkshopId)); } else { if (printedStandard.Contains(map.MapName)) continue; printedStandard.Add(map.MapName); commandInfo.ReplyToCommand(T("MapEntryStandard", map.MapName)); } } } [ConsoleCommand("css_reloadconfigs", "Reload BZ_Admin configs")] public void CmdReloadConfigs(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); ExpireElapsedBans(); commandInfo.ReplyToCommand(T("ConfigsReloaded")); } private void OpenMapMenu(CCSPlayerController admin, bool workshopOnly) { if (workshopOnly) { var workshopMaps = _maps.Values .Where(m => m.IsWorkshop) .GroupBy(m => m.WorkshopId) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (workshopMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoWorkshopMaps")}"); return; } var menu = new ChatMenu(T("MapMenuTitleWorkshop")); int index = 1; foreach (var map in workshopMaps) { string display = T("MapMenuWorkshopEntry", index, map.MapName, map.WorkshopId); menu.AddMenuOption(display, (player, option) => { Server.ExecuteCommand($"ds_workshop_changelevel {map.MapName}"); AnnounceMapChange(player, map.MapName); }); index++; } MenuManager.OpenChatMenu(admin, menu); return; } var standardMaps = _maps.Values .Where(m => !m.IsWorkshop) .GroupBy(m => m.MapName) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (standardMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoStandardMaps")}"); return; } var standardMenu = new ChatMenu(T("MapMenuTitle")); int standardIndex = 1; foreach (var map in standardMaps) { string display = T("MapMenuStandardEntry", standardIndex, map.MapName); standardMenu.AddMenuOption(display, (player, option) => { if (!Server.IsMapValid(map.MapName)) { player.PrintToChat($" {T("MapNotFound", map.MapName)}"); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(player, map.MapName); }); standardIndex++; } MenuManager.OpenChatMenu(admin, standardMenu); } private void ChangeMap(CCSPlayerController? caller, string input, CommandInfo commandInfo, bool workshopOnly) { if (!_maps.TryGetValue(input, out var map)) { if (ulong.TryParse(input, out _)) { Server.ExecuteCommand($"host_workshop_map {input}"); AnnounceMapChange(caller, input); return; } if (workshopOnly) { Server.ExecuteCommand($"ds_workshop_changelevel {input}"); AnnounceMapChange(caller, input); return; } if (Server.IsMapValid(input)) { Server.ExecuteCommand($"changelevel {input}"); AnnounceMapChange(caller, input); return; } commandInfo.ReplyToCommand(T("MapNotFound", input)); return; } if (map.IsWorkshop) { if (input.Equals(map.WorkshopId, StringComparison.OrdinalIgnoreCase)) Server.ExecuteCommand($"host_workshop_map {map.WorkshopId}"); else Server.ExecuteCommand($"ds_workshop_changelevel {map.MapName}"); AnnounceMapChange(caller, map.MapName); return; } if (!Server.IsMapValid(map.MapName)) { commandInfo.ReplyToCommand(T("MapNotFound", map.MapName)); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(caller, map.MapName); } private void AnnounceMapChange(CCSPlayerController? caller, string mapDisplayName) { string callerName = GetConfiguredAdminName(caller); Server.PrintToChatAll(T("MapChanged", callerName, mapDisplayName)); Logger.LogInformation($"[BZ_Admin] {callerName} changed map to {mapDisplayName}"); } private List<CCSPlayerController> GetConnectedHumanPlayers() { return Utilities.GetPlayers() .Where(p => p != null && p.IsValid && !p.IsBot && p.Connected == PlayerConnectedState.PlayerConnected) .ToList(); } private string EscapeServerCommandArgument(string value) { return value.Replace("\\", "\\\\").Replace("\"", "\\\""); } private int GetNextAdminId() => _admins.Count == 0 ? 1 : _admins.Values.Max(x => x.Id) + 1; private int GetNextBanId() => _bans.Count == 0 ? 1 : _bans.Max(x => x.Id) + 1; private void InsertGroup(GroupInfo group) { using var connection = GetConnection(); InsertGroup(connection, group.GroupName, string.Join(",", group.Flags), null); } private void InsertGroup(SQLiteConnection connection, string groupName, string flagsCsv, SQLiteTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_groups (group_name, flags) VALUES (@group_name, @flags);"; command.Parameters.AddWithValue("@group_name", groupName); command.Parameters.AddWithValue("@flags", flagsCsv); command.ExecuteNonQuery(); } private void DeleteGroup(string groupName) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM bz_groups WHERE group_name = @group_name;"; command.Parameters.AddWithValue("@group_name", groupName); command.ExecuteNonQuery(); } private void InsertAdmin(SQLiteConnection connection, AdminInfo admin, SQLiteTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_admins (id, name, steamid64, group_name, immunity, created_at) VALUES (@id, @name, @steamid64, @group_name, @immunity, @created_at);"; command.Parameters.AddWithValue("@id", admin.Id); command.Parameters.AddWithValue("@name", admin.Name); command.Parameters.AddWithValue("@steamid64", admin.SteamId); command.Parameters.AddWithValue("@group_name", admin.GroupName); command.Parameters.AddWithValue("@immunity", admin.Immunity); command.Parameters.AddWithValue("@created_at", admin.CreatedAt); command.ExecuteNonQuery(); } private void UpsertAdmin(AdminInfo admin) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO bz_admins (id, name, steamid64, group_name, immunity, created_at) VALUES (@id, @name, @steamid64, @group_name, @immunity, @created_at) ON CONFLICT(steamid64) DO UPDATE SET id = excluded.id, name = excluded.name, group_name = excluded.group_name, immunity = excluded.immunity, created_at = excluded.created_at;"; command.Parameters.AddWithValue("@id", admin.Id); command.Parameters.AddWithValue("@name", admin.Name); command.Parameters.AddWithValue("@steamid64", admin.SteamId); command.Parameters.AddWithValue("@group_name", admin.GroupName); command.Parameters.AddWithValue("@immunity", admin.Immunity); command.Parameters.AddWithValue("@created_at", admin.CreatedAt); command.ExecuteNonQuery(); } private void DeleteAdmin(string steamId) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM bz_admins WHERE steamid64 = @steamid64;"; command.Parameters.AddWithValue("@steamid64", steamId); command.ExecuteNonQuery(); } private void InsertBanTime(SQLiteConnection connection, string displayName, int minutes, SQLiteTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_ban_times (display_name, minutes) VALUES (@display_name, @minutes);"; command.Parameters.AddWithValue("@display_name", displayName); command.Parameters.AddWithValue("@minutes", minutes); command.ExecuteNonQuery(); } private void InsertBanReason(SQLiteConnection connection, string displayName, string reason, SQLiteTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_ban_reasons (display_name, reason) VALUES (@display_name, @reason);"; command.Parameters.AddWithValue("@display_name", displayName); command.Parameters.AddWithValue("@reason", reason); command.ExecuteNonQuery(); } private void InsertBan(SQLiteConnection connection, BanInfo ban, SQLiteTransaction? transaction) { using var command = connection.CreateCommand(); if (transaction != null) command.Transaction = transaction; command.CommandText = @" INSERT INTO bz_bans ( id, name, steamid64, duration_minutes, reason, admin_name, created_at, is_active, deleted_at, deleted_by ) VALUES ( @id, @name, @steamid64, @duration_minutes, @reason, @admin_name, @created_at, @is_active, @deleted_at, @deleted_by );"; command.Parameters.AddWithValue("@id", ban.Id); command.Parameters.AddWithValue("@name", ban.Name); command.Parameters.AddWithValue("@steamid64", ban.SteamId); command.Parameters.AddWithValue("@duration_minutes", ban.DurationMinutes); command.Parameters.AddWithValue("@reason", ban.Reason); command.Parameters.AddWithValue("@admin_name", ban.AdminName); command.Parameters.AddWithValue("@created_at", ban.CreatedAt); command.Parameters.AddWithValue("@is_active", ban.IsActive ? 1 : 0); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.ExecuteNonQuery(); } private void InsertBan(BanInfo ban) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO bz_bans ( id, name, steamid64, duration_minutes, reason, admin_name, created_at, is_active, deleted_at, deleted_by ) VALUES ( @id, @name, @steamid64, @duration_minutes, @reason, @admin_name, @created_at, @is_active, @deleted_at, @deleted_by );"; command.Parameters.AddWithValue("@id", ban.Id); command.Parameters.AddWithValue("@name", ban.Name); command.Parameters.AddWithValue("@steamid64", ban.SteamId); command.Parameters.AddWithValue("@duration_minutes", ban.DurationMinutes); command.Parameters.AddWithValue("@reason", ban.Reason); command.Parameters.AddWithValue("@admin_name", ban.AdminName); command.Parameters.AddWithValue("@created_at", ban.CreatedAt); command.Parameters.AddWithValue("@is_active", ban.IsActive ? 1 : 0); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.ExecuteNonQuery(); } private void UpdateBanStatus(BanInfo ban) { using var connection = GetConnection(); using var command = connection.CreateCommand(); command.CommandText = @" UPDATE bz_bans SET is_active = @is_active, deleted_at = @deleted_at, deleted_by = @deleted_by WHERE id = @id;"; command.Parameters.AddWithValue("@is_active", ban.IsActive ? 1 : 0); command.Parameters.AddWithValue("@deleted_at", (object?)ban.DeletedAt ?? DBNull.Value); command.Parameters.AddWithValue("@deleted_by", (object?)ban.DeletedBy ?? DBNull.Value); command.Parameters.AddWithValue("@id", ban.Id); command.ExecuteNonQuery(); } private class GroupInfo { public string GroupName { get; set; } = ""; public List<string> Flags { get; set; } = new(); } private class AdminInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public string GroupName { get; set; } = ""; public int Immunity { get; set; } public string CreatedAt { get; set; } = ""; } private class BanInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public int DurationMinutes { get; set; } public string Reason { get; set; } = ""; public string AdminName { get; set; } = ""; public string CreatedAt { get; set; } = ""; public bool IsActive { get; set; } = true; public string? DeletedAt { get; set; } public string? DeletedBy { get; set; } } private class BanTimeEntry { public string DisplayName { get; set; } = ""; public int Minutes { get; set; } } private class BanReasonEntry { public string DisplayName { get; set; } = ""; public string Reason { get; set; } = ""; } private class MapEntry { public bool IsWorkshop { get; set; } public string MapName { get; set; } = ""; public string WorkshopId { get; set; } = ""; } } } -
C#: BZ_Admin.csproj
Alles anzeigen<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <AssemblyName>BZ_Admin</AssemblyName> <RootNamespace>BZ_Admin</RootNamespace> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> <PackageReference Include="CounterStrikeSharp.API" Version="1.0.363" /> </ItemGroup> </Project>C#: BZ_Admin.cs
Alles anzeigenusing CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Menu; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; namespace BZ_Admin { [MinimumApiVersion(80)] public class BZ_AdminPlugin : BasePlugin { public override string ModuleName => "BZ_Admin"; public override string ModuleVersion => "1.3.0"; public override string ModuleAuthor => "OpenAI + ddbmaster"; public override string ModuleDescription => "Admin, Gruppen, Bans und Mapwechsel für CS2."; private readonly Dictionary<string, GroupInfo> _groups = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, AdminInfo> _admins = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, BanInfo> _bans = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _lang = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, MapEntry> _maps = new(StringComparer.OrdinalIgnoreCase); private readonly List<BanTimeEntry> _banTimes = new(); private readonly List<BanReasonEntry> _banReasons = new(); private string CurrentLanguage = "de"; private string PluginDir => ModuleDirectory; private string LangDir => Path.Combine(PluginDir, "lang"); private string LangDeFilePath => Path.Combine(LangDir, "de.txt"); private string LangEnFilePath => Path.Combine(LangDir, "en.txt"); private string ConfigBaseDir => Path.Combine(Server.GameDirectory, "csgo", "addons", "counterstrikesharp", "configs", "plugins", ModuleName); private string AdminsFilePath => Path.Combine(ConfigBaseDir, "admins.txt"); private string GroupsFilePath => Path.Combine(ConfigBaseDir, "gruppen.txt"); private string BansFilePath => Path.Combine(ConfigBaseDir, "ban-liste.txt"); private string BanTimesFilePath => Path.Combine(ConfigBaseDir, "ban-zeiten.txt"); private string BanReasonsFilePath => Path.Combine(ConfigBaseDir, "ban-gruende.txt"); private string ChangeMapFilePath => Path.Combine(ConfigBaseDir, "changemap.txt"); public override void Load(bool hotReload) { Directory.CreateDirectory(PluginDir); Directory.CreateDirectory(LangDir); Directory.CreateDirectory(ConfigBaseDir); EnsureDefaultFiles(); LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); Logger.LogInformation($"[BZ_Admin] Loaded (Lang={CurrentLanguage})"); Logger.LogInformation($"[BZ_Admin] PluginDir: {PluginDir}"); Logger.LogInformation($"[BZ_Admin] ConfigDir: {ConfigBaseDir}"); } // ========================================================= // Default-Dateien // ========================================================= private void EnsureDefaultFiles() { if (!File.Exists(GroupsFilePath)) { File.WriteAllLines(GroupsFilePath, new[] { "# Gruppenname : Flags", "#root : @css/root", "#coadmin : @css/ban,@css/unban,@css/kick,@css/slay,@css/changemap,@css/listmaps,@css/reloadconfigs", "#moderator : @css/kick,@css/slay", "#supporter : @css/listmaps", "" }); } if (!File.Exists(AdminsFilePath)) { File.WriteAllLines(AdminsFilePath, new[] { "# ID : Name : SteamID64 : Gruppe : Immunitaet : ErstelltAm", "1 : ddbmaster : 76561197960398110 : #root : 100 : 2025-10-29", "2 : Nixdorf : 76561197965022135 : #coadmin : 50 : 2025-10-29", "3 : NIT : 76561198413643807 : #coadmin : 50 : 2025-10-29", "4 : Stefan : 76561197960306744 : #coadmin : 50 : 2025-10-29", "5 : schlaubi : 76561197961449233 : #coadmin : 50 : 2025-10-29", "" }); } if (!File.Exists(BansFilePath)) { File.WriteAllLines(BansFilePath, new[] { "# ID : Name : SteamID64 : Minuten : Grund : Admin : ErstelltAm", "1 : ddbmaster : 76561197960398110 : 0 : Cheating : Nixdorf : 2025-10-29 18:15:00", "2 : OhneName : 76561197965022135 : 1440 : Beleidigung : Stefan : 2025-10-29 18:30:00", "" }); } if (!File.Exists(BanTimesFilePath)) { File.WriteAllLines(BanTimesFilePath, new[] { "# Anzeigename : Minuten", "Permanent : 0", "30 Minuten : 30", "1 Stunde : 60", "6 Stunden : 360", "12 Stunden : 720", "1 Tag : 1440", "3 Tage : 4320", "1 Woche : 10080", "2 Wochen : 20160", "" }); } if (!File.Exists(BanReasonsFilePath)) { File.WriteAllLines(BanReasonsFilePath, new[] { "# Anzeigename : Grund", "Cheating : Cheating", "Beleidigung : Beleidigung", "Teamkill : Teamkill", "Griefing : Griefing", "AFK : AFK", "Spam : Spam", "Sonstiges : Sonstiges", "" }); } if (!File.Exists(ChangeMapFilePath)) { File.WriteAllLines(ChangeMapFilePath, new[] { "# Standard Maps", "de_dust2", "de_mirage", "de_inferno", "de_nuke", "de_train", "de_anubis", "de_overpass", "", "# Workshop Maps", "# Format: workshopid:mapname", "3671053668:de_rats_teatre", "3075012302:de_blackgold_d_prefab", "3070219079:de_santo", "3328169568:de_edin", "" }); } if (!File.Exists(LangDeFilePath)) { File.WriteAllText(LangDeFilePath, GetDefaultGermanLanguageFile()); } if (!File.Exists(LangEnFilePath)) { File.WriteAllText(LangEnFilePath, GetDefaultEnglishLanguageFile()); } } private string GetDefaultGermanLanguageFile() { return @"Usage_AddGroup = Benutzung: css_addgroup <#gruppenname> <flag1,flag2,...> Usage_DelGroup = Benutzung: css_delgroup <#gruppenname> GroupAdded = Gruppe {0} mit Flags hinzugefügt: {1} GroupExists = Gruppe {0} existiert bereits. GroupDeleted = Gruppe {0} wurde gelöscht. GroupNotExist = Gruppe {0} existiert nicht. NoGroups = Keine Gruppen vorhanden. Usage_AddAdmin = Benutzung: css_addadmin <name> <steamid64> <#gruppe> <immunitaet> Usage_DelAdmin = Benutzung: css_deladmin <steamid64> Usage_SetGroup = Benutzung: css_setgroup <steamid64> <#gruppe> Usage_SetImmunity = Benutzung: css_setimmunity <steamid64> <wert> Usage_SetAdminName = Benutzung: css_setadminname <steamid64> <name> AdminAdded = Admin {0} hinzugefügt zur Gruppe {1} mit Immunität {2}. AdminRemoved = Admin {0} wurde entfernt. AdminNotFound = Kein Admin mit SteamID {0} gefunden. AdminUpdatedGroup = Admin {0} wurde Gruppe {1} zugewiesen. AdminUpdatedImmunity = Admin {0} hat jetzt Immunität {1}. AdminUpdatedName = Admin {0} wurde umbenannt in {1}. NoAdmins = Keine Admins vorhanden. Usage_Ban = Benutzung: css_ban oder css_ban <steamid64> <minuten> <grund> Usage_BanId = Benutzung: css_banid <steamid64> <minuten> <grund> Usage_Unban = Benutzung: css_unban <steamid64> BanAdded = Spieler {0} ({1}) wurde für {2} Minuten gebannt. Grund: {3} von {4} BanRemoved = Ban entfernt: {0} NoBanFound = Kein Ban für SteamID {0} gefunden. NoBans = Keine Bans vorhanden. NoBanTimes = Keine Ban-Zeiten in ban-zeiten.txt vorhanden. NoBanReasons = Keine Ban-Gründe in ban-gruende.txt vorhanden. BanMenuTitlePlayers = Spieler für Ban auswählen BanMenuTitleTimes = Ban-Zeit für {0} auswählen BanMenuTitleReasons = Ban-Grund für {0} auswählen BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} Minuten) BanMenuReasonEntry = {0} BanMenuNoPlayers = Keine Spieler gefunden. KickDenied = Du kannst diesen Spieler nicht kicken: Ziel hat gleiche oder höhere Immunität. KickDone = Spieler {0} wurde gekickt. Usage_Kick = Benutzung: css_kick oder css_kick <steamid64> KickMenuTitlePlayers = Spieler für Kick auswählen KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = Keine Spieler gefunden. SlayDenied = Du kannst diesen Spieler nicht töten: Ziel hat gleiche oder höhere Immunität. SlayDone = Spieler {0} wurde getötet. TargetNotFound = Zielspieler nicht gefunden. Usage_Slay = Benutzung: css_slay oder css_slay <steamid64> SlayMenuTitlePlayers = Spieler für Slay auswählen SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = Keine Spieler gefunden. Usage_Map = Benutzung: css_map oder css_map <mapname|workshopid> Usage_WsMap = Benutzung: css_wsmap oder css_wsmap <mapname|workshopid> MapChanged = {0} wechselt die Map nach {1}. MapNotFound = Die Map oder Workshop-Map ""{0}"" wurde nicht gefunden. NoMaps = Keine Maps in changemap.txt vorhanden. MapListHeader = Verfügbare Maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Map auswählen MapMenuTitleWorkshop = Workshop-Map auswählen MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = Keine Standard-Maps gefunden. MapMenuNoWorkshopMaps = Keine Workshop-Maps gefunden. Usage_ListBanTimes = Benutzung: css_listbantimes BanTimeHeader = Verfügbare Ban-Zeiten: BanTimeEntry = {0} = {1} Minuten Usage_ListBanReasons = Benutzung: css_listbanreasons BanReasonHeader = Verfügbare Ban-Gründe: BanReasonEntry = {0} = {1} ConfigsReloaded = BZ_Admin Konfigurationen wurden neu geladen. LanguageLoaded = Sprache geladen: {0} InvalidSteamId64 = Ungültige SteamID64. InvalidImmunity = Ungültiger Immunitätswert. InvalidBanMinutes = Ungültige Ban-Dauer in Minuten. PlayerOnlyCommand = Dieser Befehl muss von einem Spieler ausgeführt werden. BanDenied = Du kannst diesen Spieler nicht bannen: Ziel hat gleiche oder höhere Immunität. NoPermission = Du hast keine Berechtigung für diesen Befehl. "; } private string GetDefaultEnglishLanguageFile() { return @"Usage_AddGroup = Usage: css_addgroup <#groupname> <flag1,flag2,...> Usage_DelGroup = Usage: css_delgroup <#groupname> GroupAdded = Group {0} added with flags: {1} GroupExists = Group {0} already exists. GroupDeleted = Group {0} has been deleted. GroupNotExist = Group {0} does not exist. NoGroups = No groups defined. Usage_AddAdmin = Usage: css_addadmin <name> <steamid64> <#group> <immunity> Usage_DelAdmin = Usage: css_deladmin <steamid64> Usage_SetGroup = Usage: css_setgroup <steamid64> <#group> Usage_SetImmunity = Usage: css_setimmunity <steamid64> <value> Usage_SetAdminName = Usage: css_setadminname <steamid64> <name> AdminAdded = Admin {0} added to {1} with immunity {2}. AdminRemoved = Admin {0} has been removed. AdminNotFound = Admin with SteamID {0} not found. AdminUpdatedGroup = Admin {0} has been assigned to group {1}. AdminUpdatedImmunity = Admin {0} now has immunity {1}. AdminUpdatedName = Admin {0} has been renamed to {1}. NoAdmins = No admins defined. Usage_Ban = Usage: css_ban or css_ban <steamid64> <minutes> <reason> Usage_BanId = Usage: css_banid <steamid64> <minutes> <reason> Usage_Unban = Usage: css_unban <steamid64> BanAdded = Player {0} ({1}) has been banned for {2} minutes. Reason: {3} by {4} BanRemoved = Ban removed: {0} NoBanFound = No ban found for SteamID {0}. NoBans = No bans recorded. NoBanTimes = No ban times found in ban-zeiten.txt. NoBanReasons = No ban reasons found in ban-gruende.txt. BanMenuTitlePlayers = Select player to ban BanMenuTitleTimes = Select ban time for {0} BanMenuTitleReasons = Select ban reason for {0} BanMenuPlayerEntry = {0}. {1} BanMenuTimeEntry = {0} ({1} minutes) BanMenuReasonEntry = {0} BanMenuNoPlayers = No players found. KickDenied = You cannot kick this player: target has equal or higher immunity. KickDone = Player {0} has been kicked. Usage_Kick = Usage: css_kick or css_kick <steamid64> KickMenuTitlePlayers = Select player to kick KickMenuPlayerEntry = {0}. {1} KickMenuNoPlayers = No players found. SlayDenied = You cannot slay this player: target has equal or higher immunity. SlayDone = Player {0} has been slain. TargetNotFound = Target player not found. Usage_Slay = Usage: css_slay or css_slay <steamid64> SlayMenuTitlePlayers = Select player to slay SlayMenuPlayerEntry = {0}. {1} SlayMenuNoPlayers = No players found. Usage_Map = Usage: css_map or css_map <mapname|workshopid> Usage_WsMap = Usage: css_wsmap or css_wsmap <mapname|workshopid> MapChanged = {0} changed the map to {1}. MapNotFound = Map or workshop map ""{0}"" not found. NoMaps = No maps found in changemap.txt. MapListHeader = Available maps: MapEntryStandard = {0} MapEntryWorkshop = {0} ({1}) MapMenuTitle = Select map MapMenuTitleWorkshop = Select workshop map MapMenuStandardEntry = {0}. {1} MapMenuWorkshopEntry = {0}. {1} ({2}) MapMenuNoStandardMaps = No standard maps found. MapMenuNoWorkshopMaps = No workshop maps found. Usage_ListBanTimes = Usage: css_listbantimes BanTimeHeader = Available ban times: BanTimeEntry = {0} = {1} minutes Usage_ListBanReasons = Usage: css_listbanreasons BanReasonHeader = Available ban reasons: BanReasonEntry = {0} = {1} ConfigsReloaded = BZ_Admin configurations reloaded. LanguageLoaded = Language loaded: {0} InvalidSteamId64 = Invalid SteamID64. InvalidImmunity = Invalid immunity value. InvalidBanMinutes = Invalid ban duration in minutes. PlayerOnlyCommand = This command must be executed by a player. BanDenied = You cannot ban this player: target has equal or higher immunity. NoPermission = You do not have permission for this command. "; } // ========================================================= // Sprache // ========================================================= private void LoadLanguage(string langCode) { string langFile = Path.Combine(LangDir, $"{langCode}.txt"); _lang.Clear(); if (!File.Exists(langFile)) { Logger.LogWarning($"[BZ_Admin] Language file missing: {langFile}"); return; } foreach (var raw in File.ReadAllLines(langFile)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split('=', 2); if (parts.Length == 2) _lang[parts[0].Trim()] = parts[1].Trim(); } Logger.LogInformation($"[BZ_Admin] {T("LanguageLoaded", langCode)}"); } private string T(string key, params object[] args) { if (_lang.TryGetValue(key, out var fmt)) return args.Length > 0 ? string.Format(fmt, args) : fmt; return key; } // ========================================================= // Loader // ========================================================= private void LoadGroups() { _groups.Clear(); if (!File.Exists(GroupsFilePath)) return; foreach (var raw in File.ReadAllLines(GroupsFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var groupName = parts[0].Trim(); var flagsPart = parts[1].Trim(); if (!groupName.StartsWith("#")) continue; var flags = flagsPart .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToList(); _groups[groupName] = new GroupInfo { GroupName = groupName, Flags = flags }; } } private void LoadAdmins() { _admins.Clear(); if (!File.Exists(AdminsFilePath)) return; foreach (var raw in File.ReadAllLines(AdminsFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 6) continue; if (!int.TryParse(parts[0], out int id)) continue; var name = parts[1]; var steamId = parts[2]; var groupName = parts[3]; if (!int.TryParse(parts[4], out int immunity)) continue; var createdAt = parts[5]; _admins[steamId] = new AdminInfo { Id = id, Name = name, SteamId = steamId, GroupName = groupName, Immunity = immunity, CreatedAt = createdAt }; } } private void LoadBans() { _bans.Clear(); if (!File.Exists(BansFilePath)) return; foreach (var raw in File.ReadAllLines(BansFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', StringSplitOptions.TrimEntries); if (parts.Length < 7) continue; if (!int.TryParse(parts[0], out int id)) continue; var name = parts[1]; var steamId = parts[2]; if (!int.TryParse(parts[3], out int minutes)) continue; var reason = parts[4]; var admin = parts[5]; var createdAt = parts[6]; _bans[steamId] = new BanInfo { Id = id, Name = name, SteamId = steamId, DurationMinutes = minutes, Reason = reason, AdminName = admin, CreatedAt = createdAt }; } } private void LoadBanTimes() { _banTimes.Clear(); if (!File.Exists(BanTimesFilePath)) return; foreach (var raw in File.ReadAllLines(BanTimesFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var displayName = parts[0].Trim(); if (!int.TryParse(parts[1], out int minutes)) continue; _banTimes.Add(new BanTimeEntry { DisplayName = displayName, Minutes = minutes }); } } private void LoadBanReasons() { _banReasons.Clear(); if (!File.Exists(BanReasonsFilePath)) return; foreach (var raw in File.ReadAllLines(BanReasonsFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; _banReasons.Add(new BanReasonEntry { DisplayName = parts[0].Trim(), Reason = parts[1].Trim() }); } } private void LoadMaps() { _maps.Clear(); if (!File.Exists(ChangeMapFilePath)) return; foreach (var raw in File.ReadAllLines(ChangeMapFilePath)) { var line = raw.Trim(); if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; if (line.Contains(':')) { var parts = line.Split(':', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2) continue; var workshopId = parts[0].Trim(); var mapName = parts[1].Trim().ToLowerInvariant(); if (!ulong.TryParse(workshopId, out _)) continue; var entry = new MapEntry { IsWorkshop = true, WorkshopId = workshopId, MapName = mapName }; _maps[workshopId] = entry; _maps[mapName] = entry; } else { var mapName = line.ToLowerInvariant(); _maps[mapName] = new MapEntry { IsWorkshop = false, MapName = mapName }; } } } // ========================================================= // Permission / Admin Helper // ========================================================= private AdminInfo? GetAdminInfo(string steamId) { return _admins.TryGetValue(steamId, out var admin) ? admin : null; } private string GetConfiguredAdminName(CCSPlayerController? player) { if (player == null) return "Console"; var steamId = player.SteamID.ToString(); var admin = GetAdminInfo(steamId); return admin?.Name ?? player.PlayerName; } private bool HasPermission(CCSPlayerController? player, string permission) { if (player == null) return true; var steamId = player.SteamID.ToString(); if (!_admins.TryGetValue(steamId, out var admin)) return false; if (!_groups.TryGetValue(admin.GroupName, out var group)) return false; if (group.Flags.Any(x => x.Equals("@css/root", StringComparison.OrdinalIgnoreCase))) return true; return group.Flags.Any(x => x.Equals(permission, StringComparison.OrdinalIgnoreCase)); } private bool CanTarget(string executorId, string targetId) { var exec = GetAdminInfo(executorId); var targ = GetAdminInfo(targetId); if (exec == null) return false; if (targ == null) return true; return exec.Immunity > targ.Immunity; } private bool EnsurePermission(CCSPlayerController? player, CommandInfo commandInfo, string permission) { if (HasPermission(player, permission)) return true; commandInfo.ReplyToCommand(T("NoPermission")); return false; } // ========================================================= // Gruppen // ========================================================= [ConsoleCommand("css_addgroup", "Add a new admin group")] public void CmdAddGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_AddGroup")); return; } string groupName = commandInfo.GetArg(1); string flagsCsv = commandInfo.GetArg(2); if (!groupName.StartsWith("#")) { commandInfo.ReplyToCommand("Group names must start with '#'."); return; } if (_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupExists", groupName)); return; } var flags = flagsCsv .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); _groups[groupName] = new GroupInfo { GroupName = groupName, Flags = flags }; SaveGroupsToFile(); commandInfo.ReplyToCommand(T("GroupAdded", groupName, string.Join(", ", flags))); } [ConsoleCommand("css_delgroup", "Delete a group")] public void CmdDelGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_DelGroup")); return; } string groupName = commandInfo.GetArg(1); if (!_groups.Remove(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } SaveGroupsToFile(); commandInfo.ReplyToCommand(T("GroupDeleted", groupName)); } [ConsoleCommand("css_listgroups", "List groups")] public void CmdListGroups(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (_groups.Count == 0) { commandInfo.ReplyToCommand(T("NoGroups")); return; } foreach (var group in _groups.OrderBy(x => x.Key)) { string flags = group.Value.Flags.Count > 0 ? string.Join(", ", group.Value.Flags) : "(no flags)"; commandInfo.ReplyToCommand($"{group.Key}: {flags}"); } } // ========================================================= // Admins // ========================================================= [ConsoleCommand("css_addadmin", "Add an admin")] public void CmdAddAdmin(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 5) { commandInfo.ReplyToCommand(T("Usage_AddAdmin")); return; } string adminName = commandInfo.GetArg(1); string steamId = commandInfo.GetArg(2); string groupName = commandInfo.GetArg(3); string immunityArg = commandInfo.GetArg(4); if (!ulong.TryParse(steamId, out _)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (!_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } if (!int.TryParse(immunityArg, out int immunity)) { commandInfo.ReplyToCommand(T("InvalidImmunity")); return; } int nextId; string createdAt; if (_admins.TryGetValue(steamId, out var existing)) { nextId = existing.Id; createdAt = existing.CreatedAt; } else { nextId = _admins.Count == 0 ? 1 : _admins.Values.Max(x => x.Id) + 1; createdAt = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } _admins[steamId] = new AdminInfo { Id = nextId, Name = adminName, SteamId = steamId, GroupName = groupName, Immunity = immunity, CreatedAt = createdAt }; SaveAdminsToFile(); commandInfo.ReplyToCommand(T("AdminAdded", adminName, groupName, immunity)); } [ConsoleCommand("css_deladmin", "Delete an admin")] public void CmdDelAdmin(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_DelAdmin")); return; } string steamId = commandInfo.GetArg(1); if (!_admins.Remove(steamId)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } SaveAdminsToFile(); commandInfo.ReplyToCommand(T("AdminRemoved", steamId)); } [ConsoleCommand("css_setgroup", "Set admin group")] public void CmdSetGroup(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetGroup")); return; } string steamId = commandInfo.GetArg(1); string groupName = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } if (!_groups.ContainsKey(groupName)) { commandInfo.ReplyToCommand(T("GroupNotExist", groupName)); return; } admin.GroupName = groupName; SaveAdminsToFile(); commandInfo.ReplyToCommand(T("AdminUpdatedGroup", admin.Name, groupName)); } [ConsoleCommand("css_setimmunity", "Set admin immunity")] public void CmdSetImmunity(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetImmunity")); return; } string steamId = commandInfo.GetArg(1); string immunityArg = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } if (!int.TryParse(immunityArg, out int immunity)) { commandInfo.ReplyToCommand(T("InvalidImmunity")); return; } admin.Immunity = immunity; SaveAdminsToFile(); commandInfo.ReplyToCommand(T("AdminUpdatedImmunity", admin.Name, immunity)); } [ConsoleCommand("css_setadminname", "Set admin display name")] public void CmdSetAdminName(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (commandInfo.ArgCount < 3) { commandInfo.ReplyToCommand(T("Usage_SetAdminName")); return; } string steamId = commandInfo.GetArg(1); string newName = commandInfo.GetArg(2); if (!_admins.TryGetValue(steamId, out var admin)) { commandInfo.ReplyToCommand(T("AdminNotFound", steamId)); return; } string oldName = admin.Name; admin.Name = newName; SaveAdminsToFile(); commandInfo.ReplyToCommand(T("AdminUpdatedName", oldName, newName)); } [ConsoleCommand("css_listadmins", "List admins")] public void CmdListAdmins(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/root")) return; if (_admins.Count == 0) { commandInfo.ReplyToCommand(T("NoAdmins")); return; } foreach (var admin in _admins.Values.OrderBy(x => x.Id)) { commandInfo.ReplyToCommand( $"{admin.Id} : {admin.Name} : {admin.SteamId} : {admin.GroupName} : {admin.Immunity} : {admin.CreatedAt}" ); } } // ========================================================= // Bans // ========================================================= [ConsoleCommand("css_ban", "Ban menu or direct ban")] public void CmdBan(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenBanPlayerMenu(player); return; } if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_Ban")); return; } if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } string executorSteamId = player.SteamID.ToString(); string executorName = GetConfiguredAdminName(player); string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); if (!CanTarget(executorSteamId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanDenied")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); string targetName = targetPlayer?.PlayerName ?? "OhneName"; AddOrUpdateBan(targetName, targetSteamId.ToString(), minutes, reason, executorName); commandInfo.ReplyToCommand(T("BanAdded", targetName, targetSteamId, minutes, reason, executorName)); } [ConsoleCommand("css_banid", "Ban offline or online by steamid64")] public void CmdBanId(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (commandInfo.ArgCount < 4) { commandInfo.ReplyToCommand(T("Usage_BanId")); return; } string executorName = GetConfiguredAdminName(player); string executorSteamId = player?.SteamID.ToString() ?? string.Empty; string steamIdArg = commandInfo.GetArg(1); if (!ulong.TryParse(steamIdArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } string minutesArg = commandInfo.GetArg(2); if (!int.TryParse(minutesArg, out int minutes)) { commandInfo.ReplyToCommand(T("InvalidBanMinutes")); return; } string reason = string.Join(" ", Enumerable.Range(3, commandInfo.ArgCount - 3).Select(commandInfo.GetArg)); if (player != null && !CanTarget(executorSteamId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("BanDenied")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); string targetName = targetPlayer?.PlayerName ?? "OhneName"; AddOrUpdateBan(targetName, targetSteamId.ToString(), minutes, reason, executorName); commandInfo.ReplyToCommand(T("BanAdded", targetName, targetSteamId, minutes, reason, executorName)); } [ConsoleCommand("css_unban", "Unban a player")] public void CmdUnban(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/unban")) return; if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Unban")); return; } string steamIdArg = commandInfo.GetArg(1); if (!_bans.Remove(steamIdArg)) { commandInfo.ReplyToCommand(T("NoBanFound", steamIdArg)); return; } SaveBansToFile(); commandInfo.ReplyToCommand(T("BanRemoved", steamIdArg)); } [ConsoleCommand("css_listbans", "List bans")] public void CmdListBans(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (_bans.Count == 0) { commandInfo.ReplyToCommand(T("NoBans")); return; } foreach (var ban in _bans.Values.OrderBy(x => x.Id)) { commandInfo.ReplyToCommand( $"{ban.Id} : {ban.Name} : {ban.SteamId} : {ban.DurationMinutes} : {ban.Reason} : {ban.AdminName} : {ban.CreatedAt}" ); } } [ConsoleCommand("css_listbantimes", "List configured ban times")] public void CmdListBanTimes(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (_banTimes.Count == 0) { commandInfo.ReplyToCommand(T("NoBanTimes")); return; } commandInfo.ReplyToCommand(T("BanTimeHeader")); foreach (var entry in _banTimes) { commandInfo.ReplyToCommand(T("BanTimeEntry", entry.DisplayName, entry.Minutes)); } } [ConsoleCommand("css_listbanreasons", "List configured ban reasons")] public void CmdListBanReasons(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/ban")) return; if (_banReasons.Count == 0) { commandInfo.ReplyToCommand(T("NoBanReasons")); return; } commandInfo.ReplyToCommand(T("BanReasonHeader")); foreach (var entry in _banReasons) { commandInfo.ReplyToCommand(T("BanReasonEntry", entry.DisplayName, entry.Reason)); } } private void OpenBanPlayerMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("BanMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("BanMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("BanMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("BanDenied")}"); return; } OpenBanTimeMenu(player, target); }); index++; } MenuManager.OpenChatMenu(admin, menu); } private void OpenBanTimeMenu(CCSPlayerController admin, CCSPlayerController target) { if (_banTimes.Count == 0) { admin.PrintToChat($" {T("NoBanTimes")}"); return; } var menu = new ChatMenu(T("BanMenuTitleTimes", target.PlayerName)); foreach (var time in _banTimes) { string display = T("BanMenuTimeEntry", time.DisplayName, time.Minutes); menu.AddMenuOption(display, (player, option) => { OpenBanReasonMenu(player, target, time); }); } MenuManager.OpenChatMenu(admin, menu); } private void OpenBanReasonMenu(CCSPlayerController admin, CCSPlayerController target, BanTimeEntry time) { if (_banReasons.Count == 0) { admin.PrintToChat($" {T("NoBanReasons")}"); return; } var menu = new ChatMenu(T("BanMenuTitleReasons", target.PlayerName)); foreach (var reason in _banReasons) { string display = T("BanMenuReasonEntry", reason.DisplayName); menu.AddMenuOption(display, (player, option) => { string adminName = GetConfiguredAdminName(player); string targetName = string.IsNullOrWhiteSpace(target.PlayerName) ? "OhneName" : target.PlayerName; string targetSteamId = target.SteamID.ToString(); AddOrUpdateBan(targetName, targetSteamId, time.Minutes, reason.Reason, adminName); player.PrintToChat($" {T("BanAdded", targetName, targetSteamId, time.Minutes, reason.Reason, adminName)}"); try { Server.ExecuteCommand($"kickid {targetSteamId}"); } catch { } }); } MenuManager.OpenChatMenu(admin, menu); } private void AddOrUpdateBan(string targetName, string targetSteamId, int minutes, string reason, string executorName) { int nextId; if (_bans.TryGetValue(targetSteamId, out var existing)) nextId = existing.Id; else nextId = _bans.Count == 0 ? 1 : _bans.Values.Max(x => x.Id) + 1; string createdAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); _bans[targetSteamId] = new BanInfo { Id = nextId, Name = string.IsNullOrWhiteSpace(targetName) ? "OhneName" : targetName, SteamId = targetSteamId, DurationMinutes = minutes, Reason = reason, AdminName = executorName, CreatedAt = createdAt }; SaveBansToFile(); } // ========================================================= // Kick / Slay // ========================================================= [ConsoleCommand("css_kick", "Kick a player")] public void CmdKick(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/kick")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenKickMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Kick")); return; } string executorId = player?.SteamID.ToString() ?? ""; string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (player != null && !CanTarget(executorId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("KickDenied")); return; } commandInfo.ReplyToCommand(T("KickDone", targetSteamId)); Server.ExecuteCommand($"kickid {targetSteamId}"); } [ConsoleCommand("css_slay", "Slay a player")] public void CmdSlay(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/slay")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenSlayMenu(player); return; } if (commandInfo.ArgCount < 2) { commandInfo.ReplyToCommand(T("Usage_Slay")); return; } string executorId = player?.SteamID.ToString() ?? ""; string targetArg = commandInfo.GetArg(1); if (!ulong.TryParse(targetArg, out ulong targetSteamId)) { commandInfo.ReplyToCommand(T("InvalidSteamId64")); return; } if (player != null && !CanTarget(executorId, targetSteamId.ToString())) { commandInfo.ReplyToCommand(T("SlayDenied")); return; } var targetPlayer = Utilities.GetPlayerFromSteamId(targetSteamId); if (targetPlayer == null || !targetPlayer.IsValid || targetPlayer.Pawn == null || !targetPlayer.Pawn.IsValid) { commandInfo.ReplyToCommand(T("TargetNotFound")); return; } targetPlayer.Pawn.Value?.CommitSuicide(false, true); commandInfo.ReplyToCommand(T("SlayDone", targetSteamId)); } private void OpenKickMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("KickMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("KickMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("KickMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("KickDenied")}"); return; } Server.ExecuteCommand($"kickid {target.SteamID}"); player.PrintToChat($" {T("KickDone", target.SteamID)}"); }); index++; } MenuManager.OpenChatMenu(admin, menu); } private void OpenSlayMenu(CCSPlayerController admin) { var players = GetConnectedHumanPlayers() .Where(p => p.SteamID != admin.SteamID) .OrderBy(p => p.PlayerName) .ToList(); if (players.Count == 0) { admin.PrintToChat($" {T("SlayMenuNoPlayers")}"); return; } var menu = new ChatMenu(T("SlayMenuTitlePlayers")); int index = 1; foreach (var target in players) { string display = T("SlayMenuPlayerEntry", index, target.PlayerName); menu.AddMenuOption(display, (player, option) => { string executorId = player.SteamID.ToString(); string targetId = target.SteamID.ToString(); if (!CanTarget(executorId, targetId)) { player.PrintToChat($" {T("SlayDenied")}"); return; } if (target == null || !target.IsValid || target.Pawn == null || !target.Pawn.IsValid) { player.PrintToChat($" {T("TargetNotFound")}"); return; } target.Pawn.Value?.CommitSuicide(false, true); player.PrintToChat($" {T("SlayDone", target.SteamID)}"); }); index++; } MenuManager.OpenChatMenu(admin, menu); } // ========================================================= // Maps // ========================================================= [ConsoleCommand("css_map", "Change map")] public void CmdMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, workshopOnly: false); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, workshopOnly: false); } [ConsoleCommand("css_wsmap", "Change workshop map")] public void CmdWorkshopMap(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/changemap")) return; if (commandInfo.ArgCount == 1) { if (player == null) { commandInfo.ReplyToCommand(T("PlayerOnlyCommand")); return; } OpenMapMenu(player, workshopOnly: true); return; } string input = commandInfo.GetArg(1).Trim().ToLowerInvariant(); ChangeMap(player, input, commandInfo, workshopOnly: true); } [ConsoleCommand("css_listmaps", "List maps from changemap.txt")] public void CmdListMaps(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/listmaps")) return; if (_maps.Count == 0) { commandInfo.ReplyToCommand(T("NoMaps")); return; } commandInfo.ReplyToCommand(T("MapListHeader")); var printedStandard = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var printedWorkshop = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var kvp in _maps.OrderBy(x => x.Key)) { var map = kvp.Value; if (map.IsWorkshop) { if (printedWorkshop.Contains(map.WorkshopId)) continue; printedWorkshop.Add(map.WorkshopId); commandInfo.ReplyToCommand(T("MapEntryWorkshop", map.MapName, map.WorkshopId)); } else { if (printedStandard.Contains(map.MapName)) continue; printedStandard.Add(map.MapName); commandInfo.ReplyToCommand(T("MapEntryStandard", map.MapName)); } } } [ConsoleCommand("css_reloadconfigs", "Reload BZ_Admin configs")] public void CmdReloadConfigs(CCSPlayerController? player, CommandInfo commandInfo) { if (!EnsurePermission(player, commandInfo, "@css/reloadconfigs")) return; LoadLanguage(CurrentLanguage); LoadGroups(); LoadAdmins(); LoadBans(); LoadBanTimes(); LoadBanReasons(); LoadMaps(); commandInfo.ReplyToCommand(T("ConfigsReloaded")); } private void OpenMapMenu(CCSPlayerController admin, bool workshopOnly) { if (workshopOnly) { var workshopMaps = _maps.Values .Where(m => m.IsWorkshop) .GroupBy(m => m.WorkshopId) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (workshopMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoWorkshopMaps")}"); return; } var menu = new ChatMenu(T("MapMenuTitleWorkshop")); int index = 1; foreach (var map in workshopMaps) { string display = T("MapMenuWorkshopEntry", index, map.MapName, map.WorkshopId); menu.AddMenuOption(display, (player, option) => { Server.ExecuteCommand($"ds_workshop_changelevel {map.MapName}"); AnnounceMapChange(player, map.MapName); }); index++; } MenuManager.OpenChatMenu(admin, menu); return; } var standardMaps = _maps.Values .Where(m => !m.IsWorkshop) .GroupBy(m => m.MapName) .Select(g => g.First()) .OrderBy(m => m.MapName) .ToList(); if (standardMaps.Count == 0) { admin.PrintToChat($" {T("MapMenuNoStandardMaps")}"); return; } var standardMenu = new ChatMenu(T("MapMenuTitle")); int standardIndex = 1; foreach (var map in standardMaps) { string display = T("MapMenuStandardEntry", standardIndex, map.MapName); standardMenu.AddMenuOption(display, (player, option) => { if (!Server.IsMapValid(map.MapName)) { player.PrintToChat($" {T("MapNotFound", map.MapName)}"); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(player, map.MapName); }); standardIndex++; } MenuManager.OpenChatMenu(admin, standardMenu); } private void ChangeMap(CCSPlayerController? caller, string input, CommandInfo commandInfo, bool workshopOnly = false) { if (!_maps.TryGetValue(input, out var map)) { if (ulong.TryParse(input, out _)) { Server.ExecuteCommand($"host_workshop_map {input}"); AnnounceMapChange(caller, input); return; } if (workshopOnly) { Server.ExecuteCommand($"ds_workshop_changelevel {input}"); AnnounceMapChange(caller, input); return; } if (Server.IsMapValid(input)) { Server.ExecuteCommand($"changelevel {input}"); AnnounceMapChange(caller, input); return; } commandInfo.ReplyToCommand(T("MapNotFound", input)); return; } if (map.IsWorkshop) { if (input.Equals(map.WorkshopId, StringComparison.OrdinalIgnoreCase)) Server.ExecuteCommand($"host_workshop_map {map.WorkshopId}"); else Server.ExecuteCommand($"ds_workshop_changelevel {map.MapName}"); AnnounceMapChange(caller, map.MapName); return; } if (!Server.IsMapValid(map.MapName)) { commandInfo.ReplyToCommand(T("MapNotFound", map.MapName)); return; } Server.ExecuteCommand($"changelevel {map.MapName}"); AnnounceMapChange(caller, map.MapName); } private void AnnounceMapChange(CCSPlayerController? caller, string mapDisplayName) { string callerName = GetConfiguredAdminName(caller); Server.PrintToChatAll(T("MapChanged", callerName, mapDisplayName)); Logger.LogInformation($"[BZ_Admin] {callerName} changed map to {mapDisplayName}"); } // ========================================================= // Helpers // ========================================================= private List<CCSPlayerController> GetConnectedHumanPlayers() { return Utilities.GetPlayers() .Where(p => p != null && p.IsValid && !p.IsBot && p.Connected == PlayerConnectedState.PlayerConnected) .ToList(); } // ========================================================= // Speichern // ========================================================= private void SaveGroupsToFile() { using var sw = new StreamWriter(GroupsFilePath, false); sw.WriteLine("# Gruppenname : Flags"); foreach (var group in _groups.Values.OrderBy(x => x.GroupName)) { sw.WriteLine($"{group.GroupName} : {string.Join(",", group.Flags)}"); } } private void SaveAdminsToFile() { using var sw = new StreamWriter(AdminsFilePath, false); sw.WriteLine("# ID : Name : SteamID64 : Gruppe : Immunitaet : ErstelltAm"); foreach (var admin in _admins.Values.OrderBy(x => x.Id)) { sw.WriteLine($"{admin.Id} : {admin.Name} : {admin.SteamId} : {admin.GroupName} : {admin.Immunity} : {admin.CreatedAt}"); } } private void SaveBansToFile() { using var sw = new StreamWriter(BansFilePath, false); sw.WriteLine("# ID : Name : SteamID64 : Minuten : Grund : Admin : ErstelltAm"); foreach (var ban in _bans.Values.OrderBy(x => x.Id)) { sw.WriteLine($"{ban.Id} : {ban.Name} : {ban.SteamId} : {ban.DurationMinutes} : {ban.Reason} : {ban.AdminName} : {ban.CreatedAt}"); } } // ========================================================= // DTOs // ========================================================= private class GroupInfo { public string GroupName { get; set; } = ""; public List<string> Flags { get; set; } = new(); } private class AdminInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public string GroupName { get; set; } = ""; public int Immunity { get; set; } public string CreatedAt { get; set; } = ""; } private class BanInfo { public int Id { get; set; } public string Name { get; set; } = ""; public string SteamId { get; set; } = ""; public int DurationMinutes { get; set; } public string Reason { get; set; } = ""; public string AdminName { get; set; } = ""; public string CreatedAt { get; set; } = ""; } private class BanTimeEntry { public string DisplayName { get; set; } = ""; public int Minutes { get; set; } } private class BanReasonEntry { public string DisplayName { get; set; } = ""; public string Reason { get; set; } = ""; } private class MapEntry { public bool IsWorkshop { get; set; } public string MapName { get; set; } = ""; public string WorkshopId { get; set; } = ""; } } } -
2. Für später besser: symbolische Links statt Kopien
Kopieren ist unschön, weil bei Updates Dateien auseinanderlaufen können.
Sauberer wäre später eher:
Codeln -sf /home/cs2server/cs2/game/bin/linuxsteamrt64/libv8.so /home/cs2server/cs2/game/csgo/bin/linuxsteamrt64/libv8.so ln -sf /home/cs2server/cs2/game/bin/linuxsteamrt64/libtier0.so /home/cs2server/cs2/game/csgo/bin/linuxsteamrt64/libtier0.so ln -sf /home/cs2server/cs2/game/bin/linuxsteamrt64/libvstdlib_s.so /home/cs2server/cs2/game/csgo/bin/linuxsteamrt64/libvstdlib_s.soAber erst später, nicht mitten im funktionierenden Test.
3. Noch besser: über Wrapper-Script mit sauberem Pfad starten
Dann kann man LD_LIBRARY_PATH fest setzen und muss weniger basteln.
Zum Beispiel später:
-
-
Game server Steuerung
🧾 Alle Befehle (All Commands)
Eine vollständige Liste aller Befehle erhältst du mit:
Unten findest du die wichtigsten Befehle.
▶️ Server starten / stoppen (Running)
Starten
Stoppen
Neustarten
🖥 Konsole
Die Konsole erlaubt dir:
- die Live-Konsole des Servers zu sehen
- Befehle einzugeben (falls unterstützt)
👉 Konsole verlassen:
- CTRL + b dann d
- NICHT CTRL + c → das würde den Server beenden!
🔄 Updates (Updating)
Update prüfen & installieren
👉 Prüft auf Updates und führt sie aus
👉 Server startet nur neu, wenn nötigDirektes Update erzwingen
👉 Überspringt die Prüfung und nutzt direkt SteamCMD
Dateien validieren
👉 Überprüft Server-Dateien auf Fehler
🐞 Debugging
Details anzeigen
Zeigt wichtige Infos wie:
- Ports
- Passwörter
- Config-Dateien
Debug-Modus
👉 Zeigt die Server-Ausgabe direkt im Terminal
👉 Sehr hilfreich bei FehlernLogs
Logs findest du hier:
Dort sind enthalten:
- Script-Logs
- Konsolen-Logs
- Game-Server-Logs
💾 Backup
Erstellt ein komplettes Backup des Servers:
👉 Archiv wird als tar.bz2 gespeichert
📡 Monitoring
LinuxGSM kann deinen Server überwachen:
👉 Prüft:
- läuft der Server noch?
- antwortet er?
👉 Wenn nicht:
- automatischer Neustart
- optional Benachrichtigung
⚙️ LinuxGSM konfigurieren
Weitere Infos zu Einstellungen:
👉 LinuxGSM Config Files
📚 Dokumentation
👉 Offizielle LinuxGSM Dokumentation
⏰ Cronjobs (Automatisierung)
Du kannst Befehle automatisch ausführen lassen.
Crontab bearbeiten:
✅ Empfohlene Cronjobs
Alle 5 Minuten: Server überwachen
Alle 30 Minuten: Updates prüfen
Jeden Sonntag um 00:00: LinuxGSM selbst updaten
🧠 Kurz erklärt
- monitor → Server läuft? Wenn nicht → neu starten
- update → Game Updates
- update-lgsm → Script selbst aktualisieren
-
🧾 Installation Schritt für Schritt (Deutsch)
👤 1. Benutzer erstellen und einloggen
👉 Erstellt einen neuen Benutzer für den Server
💡 Wichtig:
- Verwende ein starkes Passwort
- Beispiel (zufällig): TI1NjMzNDM5N
Danach zum Benutzer wechseln:
👉 Jetzt arbeitest du als cs2server User (wichtig für Sicherheit)
📥 2. linuxgsm.sh herunterladen
👉 Das macht:
- Script herunterladen
- ausführbar machen
- Installation starten
⚙️ 3. Installer ausführen
👉 Installiert den kompletten CS2 Server
👉 Folge einfach den Anweisungen im Terminal🎫 Game Server Login Token (GSLT)
👉 Dieser Token ist Pflicht, damit dein Server:
- in der Steam Serverliste erscheint
- öffentlich gefunden wird
💡 Kurz erklärt:
Ohne GSLT:
- ❌ Server nicht sichtbar in CS2
- ❌ keine öffentliche Listung
Mit GSLT:
- ✔ Server wird angezeigt
- ✔ Spieler können ihn finden