From 95f1d5a873fe9d82d3b508e799354ab56e36087c Mon Sep 17 00:00:00 2001 From: WiruWiru <61034981+wiruwiru@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:22:59 -0300 Subject: [PATCH 1/2] feat: implement storage system with PlayerSettings and MySQL support --- Configs/BaseConfigs.cs | 13 +- Managers/DisplayManager.cs | 137 +++++++------------- PlayerSettingsApi.dll | Bin 0 -> 4096 bytes README.md | 53 ++++++-- Services/DatabaseService.cs | 199 +++++++++++++++++++++--------- Services/IStorageService.cs | 16 +++ Services/MemoryStorage.cs | 64 ++++++++++ Services/PlayerSettingsStorage.cs | 89 +++++++++++++ Services/StorageFactory.cs | 52 ++++++++ SpectatorList.cs | 45 ++++++- SpectatorList.csproj | 1 + 11 files changed, 498 insertions(+), 171 deletions(-) create mode 100644 PlayerSettingsApi.dll create mode 100644 Services/IStorageService.cs create mode 100644 Services/MemoryStorage.cs create mode 100644 Services/PlayerSettingsStorage.cs create mode 100644 Services/StorageFactory.cs diff --git a/Configs/BaseConfigs.cs b/Configs/BaseConfigs.cs index 13a6396..d04a506 100644 --- a/Configs/BaseConfigs.cs +++ b/Configs/BaseConfigs.cs @@ -20,8 +20,8 @@ public class SpectatorConfig : BasePluginConfig [JsonPropertyName("DisplaySettings")] public DisplaySettings Display { get; set; } = new(); - [JsonPropertyName("Database")] - public DatabaseConfig Database { get; set; } = new DatabaseConfig(); + [JsonPropertyName("StorageSettings")] + public StorageSettings Storage { get; set; } = new(); } public class UpdateSettings @@ -75,6 +75,15 @@ public class ScreenViewSettings public string CountColor { get; set; } = "#87CEEB"; } + public class StorageSettings + { + [JsonPropertyName("StorageType")] + public string StorageType { get; set; } = "PlayerSettings"; + + [JsonPropertyName("Database")] + public DatabaseConfig Database { get; set; } = new DatabaseConfig(); + } + public class DatabaseConfig { [JsonPropertyName("Host")] diff --git a/Managers/DisplayManager.cs b/Managers/DisplayManager.cs index 89f88fc..a67926a 100644 --- a/Managers/DisplayManager.cs +++ b/Managers/DisplayManager.cs @@ -11,87 +11,65 @@ namespace SpectatorList.Managers public class DisplayManager : IDisposable { private readonly Dictionary _screenDisplays; - private readonly HashSet _disabledPlayers; private readonly SpectatorConfig _config; private readonly BasePlugin _plugin; - private readonly DatabaseService _databaseService; + private readonly IStorageService _storageService; - public DisplayManager(SpectatorConfig config, BasePlugin plugin) + public DisplayManager(SpectatorConfig config, BasePlugin plugin, IStorageService storageService) { _config = config; _plugin = plugin; + _storageService = storageService; _screenDisplays = new Dictionary(); - _disabledPlayers = new HashSet(); - _databaseService = new DatabaseService(config); - if (_databaseService.IsEnabled) - { - _ = InitializeDatabaseAsync(); - } + _ = InitializeStorageAsync(); } - private async Task InitializeDatabaseAsync() + private async Task InitializeStorageAsync() { try { - await _databaseService.InitializeDatabase(); + var success = await _storageService.InitializeAsync(); + var storageType = _storageService.GetStorageType(); + + if (success) + { + Server.PrintToConsole($"[SpectatorList] Storage initialized successfully: {storageType}"); + } + else + { + Server.PrintToConsole($"[SpectatorList] Storage initialization failed, using: {storageType}"); + } } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error initializing database: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error initializing storage: {ex.Message}"); } } public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) { - if (_databaseService.IsEnabled) + try { - try - { - var steamId = player.SteamID.ToString(); - var preferences = await _databaseService.LoadOrCreatePlayerPreferences(steamId); - return preferences.DisplayEnabled; - } - catch (Exception ex) - { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); - }); - return !_disabledPlayers.Contains(player.Slot); - } + return await _storageService.IsPlayerDisplayEnabledAsync(player); } - - return !_disabledPlayers.Contains(player.Slot); - } - - public bool IsPlayerDisplayEnabled(CCSPlayerController player) - { - if (_databaseService.IsEnabled) + catch (Exception ex) { - var cachedPrefs = _databaseService.GetCachedPlayerPreferences(player.SteamID.ToString()); - if (cachedPrefs != null) - { - return cachedPrefs.DisplayEnabled; - } - - _ = LoadPlayerPreferencesAsync(player); + Server.PrintToConsole($"[SpectatorList] Error checking display status for {player.PlayerName}: {ex.Message}"); return true; } - - return !_disabledPlayers.Contains(player.Slot); } - private async Task LoadPlayerPreferencesAsync(CCSPlayerController player) + public bool IsPlayerDisplayEnabled(CCSPlayerController player) { try { - var steamId = player.SteamID.ToString(); - await _databaseService.LoadOrCreatePlayerPreferences(steamId); + return _storageService.IsPlayerDisplayEnabled(player); } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error checking display status for {player.PlayerName}: {ex.Message}"); + return true; } } @@ -105,60 +83,37 @@ public bool CanPlayerViewList(CCSPlayerController player) public async Task TogglePlayerDisplayAsync(CCSPlayerController player) { - if (_databaseService.IsEnabled) + try { - try - { - var steamId = player.SteamID.ToString(); - var preferences = await _databaseService.LoadOrCreatePlayerPreferences(steamId); + await _storageService.TogglePlayerDisplayAsync(player); - preferences.DisplayEnabled = !preferences.DisplayEnabled; - preferences.LastUpdated = DateTime.Now; - - await _databaseService.SavePlayerPreferences(preferences); - - if (!preferences.DisplayEnabled) - { - Server.NextFrame(() => CleanupPlayerDisplay(player)); - } - } - catch (Exception ex) + var isEnabled = await _storageService.IsPlayerDisplayEnabledAsync(player); + if (!isEnabled) { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); - }); - Server.NextFrame(() => TogglePlayerDisplayMemory(player)); + Server.NextFrame(() => CleanupPlayerDisplay(player)); } } - else + catch (Exception ex) { - Server.NextFrame(() => TogglePlayerDisplayMemory(player)); + Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); } } public void TogglePlayerDisplay(CCSPlayerController player) { - if (_databaseService.IsEnabled) - { - _ = TogglePlayerDisplayAsync(player); - } - else + try { - TogglePlayerDisplayMemory(player); - } - } + _storageService.TogglePlayerDisplay(player); - private void TogglePlayerDisplayMemory(CCSPlayerController player) - { - if (_disabledPlayers.Contains(player.Slot)) - { - _disabledPlayers.Remove(player.Slot); + var isEnabled = _storageService.IsPlayerDisplayEnabled(player); + if (!isEnabled) + { + CleanupPlayerDisplay(player); + } } - else + catch (Exception ex) { - _disabledPlayers.Add(player.Slot); - CleanupPlayerDisplay(player); + Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); } } @@ -338,19 +293,13 @@ public void HidePlayerDisplay(CCSPlayerController player) public void OnPlayerDisconnect(CCSPlayerController player) { CleanupPlayerDisplay(player); - _disabledPlayers.Remove(player.Slot); - - if (_databaseService.IsEnabled) - { - _databaseService.RemoveFromCache(player.SteamID.ToString()); - } + _storageService.OnPlayerDisconnect(player); } public void Dispose() { CleanupAllDisplays(); - _disabledPlayers.Clear(); - _databaseService.ClearCache(); + _storageService.ClearCache(); } } } \ No newline at end of file diff --git a/PlayerSettingsApi.dll b/PlayerSettingsApi.dll new file mode 100644 index 0000000000000000000000000000000000000000..0f3d5dd7ae108a1700c5ac469ede2578c8a02712 GIT binary patch literal 4096 zcmeHJU2GIp6h5iG~Ny z?mhRObI(2ZJLmpQ|KrE0hKOoWu3RBHi<+rR^6Rr%bT{2~u8H1hxVZML(syxfYSM63 z+p%(vo>#Map>p)}`=mGdWz>O{;er^evio&r5xY3fN5{+q&>trEGY+Yz@ zpp7UM)CKgKBTNg1jFrYv*jE!u1$7basSssT=e&X#TZHOpP$inHBT`uT4`@$v{~je6 z%~+;7wKlDzt)qQAJF6oT{9`z8eUhjX7~eo?O?r+|$hmAWhxjwLW7sCb?5~ZT# zeRL>tD?JaBC{5F+N|e&ddh~ul%#TZcTJrm)eoVH1CiW1Ahep~O!OjKw-Z>X15yfTY z6^0sUi9V`3p|n;=Zz&sP+zRc7U4+(v*U$wxsn&d=G|)jHZw8Oj?cj}cANX==mwdbA zG0A%+KPdTQlI!3K8Q`mE13f|CkVZ?X1AHaz1iuqHjaq3Od?TF#@1XaW;0&274)4ay^mPhR|u`#Wk$7xa9~+h)gk;(=(=sdGL}cdV*rEEAkn$lrp?ZbxL<~ z!t23RC8jOs$YT2*!xSUJaSf}mxU;t~VL5r-L#J+D(<*KiCX8Isk#=P#Lt-MSPYACx zgs|PRK5pf0!<5ED!qjJEtD--QWY9svvR--MK+!ZgXw1hKOSvN)!JhR(soW7HQ|dh< zW0;0l(Ysf8gQi}>QV7rEvpJ%hMM03RrXCj)deQVor+lNT)zX68zzOw+-eR^v)sC*J z(Z)@aY#l2!zXimEDYBe&ipP__3y#^bOj9^yOU$HYJ;daRkWYZaZW zNISx|+;luCFNbt>Tnj{mnYJ>A(=y6*L3l{UViV0C&rot-Z1a|_6abkBfpD2|}1XQ)^=bE5PKz%78w16;FNArj5d1CuPv)`Bz|U9RBiT%Q;GjNQWe(`$0Kl2@)# zp0q6hyIB#zjpx+xg<#@GbT3v%fk%Pp*b8UHrjm>#N0XR=1AhWF^l@_l literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 9b8131b..d865ccf 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,43 @@ Shows real-time spectators both in chat messages and on-screen display with cust --- -### Installation +## ๐Ÿš€ Installation + +### Basic Installation 1. Install [CounterStrike Sharp](https://github.com/roflmuffin/CounterStrikeSharp) and [Metamod:Source](https://www.sourcemm.net/downloads.php/?branch=master) 2. Download [SpectatorList.zip](https://github.com/wiruwiru/SpectatorList-CS2/releases/latest) from releases 3. Extract and upload to your game server 4. Start server and configure the generated config file -5. (Optional) Configure MySQL database for persistent user preferences + +### Optional Dependencies (for PlayerSettings storage) +If you want to use `PlayerSettings` storage type: +2. Install [PlayerSettingsCS2](https://github.com/NickFox007/PlayerSettingsCS2/releases/latest) (required dependency) +1. Install [AnyBaseLibCS2](https://github.com/NickFox007/AnyBaseLibCS2/releases/latest) (required for PlayerSettings) +3. Set `StorageType` to `"PlayerSettings"` in your config + +### Optional Dependencies (for MySQL storage) +If you want to use `MySQL` storage type: +1. Configure your MySQL database +2. Set `StorageType` to `"MySQL"` in your config +3. Fill in the database connection details --- -### Main Configuration Parameters +## โš™๏ธ Storage Options + +The plugin supports three different storage methods for user preferences: + +| Storage Type | Description | Persistence | Performance | Dependencies | +|--------------|-------------|-------------|-------------|--------------| +| **PlayerSettings** | Uses PlayerSettings plugin | โœ… Persistent | โšก Fast | PlayerSettingsCS2 + AnyBaseLibCS2 | +| **MySQL** | Traditional database storage | โœ… Persistent | ๐Ÿ”„ Database queries | MySQL Server | +| **Memory** | Temporary in-memory storage | โŒ Lost on restart | โšกโšก Fastest | None | + +**Recommendation**: Use `PlayerSettings` for most servers, `MySQL` if you don't care about the number of active pool connections, and `Memory` for testing. + +--- +## ๐Ÿ“‹ Main Configuration Parameters | Parameter | Description | Required | |----------------------|---------------------------------------------------------------------------------------------------|----------| | `Commands` | List of chat commands players can use to toggle spectator list display. (**Default**: `["css_speclist", "css_specs", "css_spectators"]`) | **YES** | @@ -23,10 +49,15 @@ Shows real-time spectators both in chat messages and on-screen display with cust | `CanViewList` | Permission flag required to view spectator lists (both chat and screen). Leave empty for all players. (**Default**: `""`) | **YES** | | `UpdateSettings` | Configuration for automatic updates and periodic displays. | **YES** | | `DisplaySettings` | Configuration for how spectator lists are displayed. | **YES** | -| `Database` | MySQL database configuration for persistent user preferences. | **NO** | +| `StorageSettings` | Configuration for user preference storage method. | **YES** | -### Update Settings Parameters +### Storage Settings Parameters +| Parameter | Description | Required | +|-------------------|-----------------------------------------------------------------------------------------------------|----------| +| `StorageType` | Storage method to use: `"PlayerSettings"`, `"MySQL"`, or `"Memory"`. (**Default**: `"PlayerSettings"`) | **YES** | +| `Database` | MySQL database configuration (only used when `StorageType` is `"MySQL"`). | **NO** | +### Update Settings Parameters | Parameter | Description | Required | |-------------------|-----------------------------------------------------------------------------------------------------|----------| | `CheckInterval` | How often (in seconds) to check for spectator changes. (**Default**: `2.0`) | **YES** | @@ -35,7 +66,6 @@ Shows real-time spectators both in chat messages and on-screen display with cust | `PeriodicInterval` | Interval (in seconds) for periodic displays when `ShowPeriodic` is enabled. (**Default**: `5.0`) | **YES** | ### Display Settings Parameters - | Parameter | Description | Required | |-------------------|-----------------------------------------------------------------------------------------------------|----------| | `ExclusionFlag` | Players with this flag will be hidden from spectator lists. (**Default**: `"@css/generic"`) | **YES** | @@ -45,7 +75,6 @@ Shows real-time spectators both in chat messages and on-screen display with cust | `ScreenViewSettings` | Configuration for on-screen display positioning and appearance. | **YES** | ### Screen View Settings Parameters - | Parameter | Description | Required | |-------------------|-----------------------------------------------------------------------------------------------------|----------| | `PositionX` | Horizontal position offset for on-screen display. (**Default**: `-8.0`) | **YES** | @@ -55,17 +84,17 @@ Shows real-time spectators both in chat messages and on-screen display with cust | `CountColor` | Hex color code for spectator count. (**Default**: `"#87CEEB"`) | **YES** | ### Database Settings Parameters - +*(Only used when `StorageType` is set to `"MySQL"`)* | Parameter | Description | Required | |-------------------|-----------------------------------------------------------------------------------------------------|----------| -| `Host` | MySQL server hostname or IP address. Leave empty to disable database. (**Default**: `""`) | **NO** | +| `Host` | MySQL server hostname or IP address. (**Default**: `"localhost"`) | **NO** | | `Port` | MySQL server port. (**Default**: `3306`) | **NO** | -| `User` | MySQL username for database connection. (**Default**: `""`) | **NO** | +| `User` | MySQL username for database connection. (**Default**: `"root"`) | **NO** | | `Password` | MySQL password for database connection. (**Default**: `""`) | **NO** | -| `DatabaseName` | Name of the MySQL database to use. Leave empty to disable database. (**Default**: `""`) | **NO** | +| `DatabaseName` | Name of the MySQL database to use. (**Default**: `""`) | **NO** | --- -## Support +## ๐Ÿ“Š Support For issues, questions, or feature requests, please visit our [GitHub Issues](https://github.com/wiruwiru/SpectatorList-CS2/issues) page. \ No newline at end of file diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index fada54b..9ce22f7 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -7,30 +7,27 @@ namespace SpectatorList.Services { - public class DatabaseService + public class DatabaseService : IStorageService { private readonly SpectatorConfig _config; private readonly Dictionary _preferencesCache = new(); + private readonly HashSet _fallbackDisabledPlayers = new(); private bool _isInitialized = false; + private bool _databaseAvailable = false; public DatabaseService(SpectatorConfig config) { _config = config; } - public bool IsEnabled => !string.IsNullOrEmpty(_config.Database.Host) && !string.IsNullOrEmpty(_config.Database.DatabaseName); + public bool IsEnabled => !string.IsNullOrEmpty(_config.Storage.Database.Host) && + !string.IsNullOrEmpty(_config.Storage.Database.DatabaseName); - public async Task InitializeDatabase() + public async Task InitializeAsync() { if (!IsEnabled) { - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole("[SpectatorList] Database is disabled in configuration"); - }); - }); + Server.PrintToConsole("[SpectatorList] Database configuration is incomplete"); return false; } @@ -40,26 +37,17 @@ await Task.Run(() => await connection.OpenAsync(); await CreateTable(connection); _isInitialized = true; + _databaseAvailable = true; - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole("[SpectatorList] Database connection established and table created"); - }); - }); + Server.PrintToConsole("[SpectatorList] Database connection established and table created"); return true; } catch (Exception ex) { _isInitialized = false; - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Failed to initialize database: {ex.Message}"); - }); - }); + _databaseAvailable = false; + Server.PrintToConsole($"[SpectatorList] Failed to initialize database: {ex.Message}"); + Server.PrintToConsole("[SpectatorList] Falling back to memory storage for this session"); return false; } } @@ -77,9 +65,95 @@ last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP await cmd.ExecuteNonQueryAsync(); } - public async Task LoadPlayerPreferences(string steamId) + public bool IsPlayerDisplayEnabled(CCSPlayerController player) + { + if (!_databaseAvailable) + { + return !_fallbackDisabledPlayers.Contains(player.Slot); + } + + var cachedPrefs = GetCachedPlayerPreferences(player.SteamID.ToString()); + if (cachedPrefs != null) + { + return cachedPrefs.DisplayEnabled; + } + + _ = LoadPlayerPreferencesAsync(player); + return true; + } + + public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) + { + if (!_databaseAvailable) + { + return !_fallbackDisabledPlayers.Contains(player.Slot); + } + + try + { + var steamId = player.SteamID.ToString(); + var preferences = await LoadOrCreatePlayerPreferences(steamId); + return preferences.DisplayEnabled; + } + catch (Exception ex) + { + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); + }); + return !_fallbackDisabledPlayers.Contains(player.Slot); + } + } + + public void TogglePlayerDisplay(CCSPlayerController player) { - if (!IsEnabled || !_isInitialized) + if (!_databaseAvailable) + { + if (_fallbackDisabledPlayers.Contains(player.Slot)) + { + _fallbackDisabledPlayers.Remove(player.Slot); + } + else + { + _fallbackDisabledPlayers.Add(player.Slot); + } + return; + } + + _ = TogglePlayerDisplayAsync(player); + } + + public async Task TogglePlayerDisplayAsync(CCSPlayerController player) + { + if (!_databaseAvailable) + { + Server.NextFrame(() => TogglePlayerDisplay(player)); + return; + } + + try + { + var steamId = player.SteamID.ToString(); + var preferences = await LoadOrCreatePlayerPreferences(steamId); + + preferences.DisplayEnabled = !preferences.DisplayEnabled; + preferences.LastUpdated = DateTime.Now; + + await SavePlayerPreferences(preferences); + } + catch (Exception ex) + { + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); + TogglePlayerDisplay(player); + }); + } + } + + private async Task LoadPlayerPreferences(string steamId) + { + if (!IsEnabled || !_isInitialized || !_databaseAvailable) return null; try @@ -118,20 +192,15 @@ FROM spectatorlist_preferences } catch (Exception ex) { - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Error loading player preferences for SteamID {steamId}: {ex.Message}"); - }); - }); + Server.PrintToConsole($"[SpectatorList] Error loading player preferences for SteamID {steamId}: {ex.Message}"); + _databaseAvailable = false; return null; } } - public async Task SavePlayerPreferences(PlayerPreferences preferences) + private async Task SavePlayerPreferences(PlayerPreferences preferences) { - if (!IsEnabled || !_isInitialized) + if (!IsEnabled || !_isInitialized || !_databaseAvailable) return false; try @@ -158,18 +227,13 @@ ON DUPLICATE KEY UPDATE } catch (Exception ex) { - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Error saving player preferences for SteamID {preferences.SteamId}: {ex.Message}"); - }); - }); + Server.PrintToConsole($"[SpectatorList] Error saving player preferences for SteamID {preferences.SteamId}: {ex.Message}"); + _databaseAvailable = false; return false; } } - public async Task LoadOrCreatePlayerPreferences(string steamId) + private async Task LoadOrCreatePlayerPreferences(string steamId) { var preferences = await LoadPlayerPreferences(steamId); if (preferences == null) @@ -181,7 +245,7 @@ public async Task LoadOrCreatePlayerPreferences(string steamI LastUpdated = DateTime.Now }; - if (IsEnabled && _isInitialized) + if (IsEnabled && _isInitialized && _databaseAvailable) { await SavePlayerPreferences(preferences); } @@ -190,6 +254,19 @@ public async Task LoadOrCreatePlayerPreferences(string steamI return preferences; } + private async Task LoadPlayerPreferencesAsync(CCSPlayerController player) + { + try + { + var steamId = player.SteamID.ToString(); + await LoadOrCreatePlayerPreferences(steamId); + } + catch (Exception ex) + { + Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); + } + } + public void CachePlayerPreferences(string steamId, PlayerPreferences preferences) { _preferencesCache[steamId] = preferences; @@ -200,30 +277,32 @@ public void CachePlayerPreferences(string steamId, PlayerPreferences preferences return _preferencesCache.TryGetValue(steamId, out var preferences) ? preferences : null; } - public void RemoveFromCache(string steamId) + public void OnPlayerDisconnect(CCSPlayerController player) { - _preferencesCache.Remove(steamId); + _preferencesCache.Remove(player.SteamID.ToString()); + _fallbackDisabledPlayers.Remove(player.Slot); } public void ClearCache() { _preferencesCache.Clear(); + _fallbackDisabledPlayers.Clear(); } private MySqlConnection GetConnection() { - if (_config.Database == null) + if (_config.Storage.Database == null) { throw new InvalidOperationException("Database configuration is null"); } var builder = new MySqlConnectionStringBuilder { - Server = _config.Database.Host, - Port = _config.Database.Port, - UserID = _config.Database.User, - Database = _config.Database.DatabaseName, - Password = _config.Database.Password, + Server = _config.Storage.Database.Host, + Port = _config.Storage.Database.Port, + UserID = _config.Storage.Database.User, + Database = _config.Storage.Database.DatabaseName, + Password = _config.Storage.Database.Password, Pooling = true, SslMode = MySqlSslMode.Preferred }; @@ -244,15 +323,17 @@ public async Task TestConnection() } catch (Exception ex) { - await Task.Run(() => - { - Server.NextFrame(() => - { - Server.PrintToConsole($"[SpectatorList] Database connection test failed: {ex.Message}"); - }); - }); + Server.PrintToConsole($"[SpectatorList] Database connection test failed: {ex.Message}"); return false; } } + + public string GetStorageType() + { + if (_databaseAvailable) + return "MySQL Database"; + else + return "Memory (Database Unavailable)"; + } } } \ No newline at end of file diff --git a/Services/IStorageService.cs b/Services/IStorageService.cs new file mode 100644 index 0000000..cba66fd --- /dev/null +++ b/Services/IStorageService.cs @@ -0,0 +1,16 @@ +using CounterStrikeSharp.API.Core; + +namespace SpectatorList.Services +{ + public interface IStorageService + { + Task InitializeAsync(); + bool IsPlayerDisplayEnabled(CCSPlayerController player); + Task IsPlayerDisplayEnabledAsync(CCSPlayerController player); + void TogglePlayerDisplay(CCSPlayerController player); + Task TogglePlayerDisplayAsync(CCSPlayerController player); + void OnPlayerDisconnect(CCSPlayerController player); + void ClearCache(); + string GetStorageType(); + } +} \ No newline at end of file diff --git a/Services/MemoryStorage.cs b/Services/MemoryStorage.cs new file mode 100644 index 0000000..fa4522b --- /dev/null +++ b/Services/MemoryStorage.cs @@ -0,0 +1,64 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; + +namespace SpectatorList.Services +{ + public class MemoryStorage : IStorageService + { + private readonly HashSet _disabledPlayers; + + public MemoryStorage() + { + _disabledPlayers = new HashSet(); + } + + public async Task InitializeAsync() + { + await Task.CompletedTask; + Server.PrintToConsole("[SpectatorList] Using Memory storage (preferences will not persist between server restarts)"); + return true; + } + + public bool IsPlayerDisplayEnabled(CCSPlayerController player) + { + return !_disabledPlayers.Contains(player.Slot); + } + + public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) + { + return await Task.FromResult(IsPlayerDisplayEnabled(player)); + } + + public void TogglePlayerDisplay(CCSPlayerController player) + { + if (_disabledPlayers.Contains(player.Slot)) + { + _disabledPlayers.Remove(player.Slot); + } + else + { + _disabledPlayers.Add(player.Slot); + } + } + + public async Task TogglePlayerDisplayAsync(CCSPlayerController player) + { + await Task.Run(() => TogglePlayerDisplay(player)); + } + + public void OnPlayerDisconnect(CCSPlayerController player) + { + _disabledPlayers.Remove(player.Slot); + } + + public void ClearCache() + { + _disabledPlayers.Clear(); + } + + public string GetStorageType() + { + return "Memory"; + } + } +} \ No newline at end of file diff --git a/Services/PlayerSettingsStorage.cs b/Services/PlayerSettingsStorage.cs new file mode 100644 index 0000000..fb39def --- /dev/null +++ b/Services/PlayerSettingsStorage.cs @@ -0,0 +1,89 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using PlayerSettings; + +namespace SpectatorList.Services +{ + public class PlayerSettingsStorage : IStorageService + { + private readonly ISettingsApi? _settingsApi; + private readonly HashSet _fallbackDisabledPlayers; + + public PlayerSettingsStorage(ISettingsApi? settingsApi) + { + _settingsApi = settingsApi; + _fallbackDisabledPlayers = new HashSet(); + } + + public async Task InitializeAsync() + { + await Task.CompletedTask; + if (_settingsApi != null) + { + Server.PrintToConsole("[SpectatorList] Using PlayerSettings for storage"); + return true; + } + else + { + Server.PrintToConsole("[SpectatorList] PlayerSettings not available, using memory fallback"); + return false; + } + } + + public bool IsPlayerDisplayEnabled(CCSPlayerController player) + { + if (_settingsApi != null) + { + var displayEnabled = _settingsApi.GetPlayerSettingsValue(player, "spectator_display_enabled", "true"); + return displayEnabled.ToLower() == "true"; + } + return !_fallbackDisabledPlayers.Contains(player.Slot); + } + + public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) + { + return await Task.FromResult(IsPlayerDisplayEnabled(player)); + } + + public void TogglePlayerDisplay(CCSPlayerController player) + { + if (_settingsApi != null) + { + var currentValue = _settingsApi.GetPlayerSettingsValue(player, "spectator_display_enabled", "true"); + var newValue = currentValue.ToLower() == "true" ? "false" : "true"; + _settingsApi.SetPlayerSettingsValue(player, "spectator_display_enabled", newValue); + } + else + { + if (_fallbackDisabledPlayers.Contains(player.Slot)) + { + _fallbackDisabledPlayers.Remove(player.Slot); + } + else + { + _fallbackDisabledPlayers.Add(player.Slot); + } + } + } + + public async Task TogglePlayerDisplayAsync(CCSPlayerController player) + { + await Task.Run(() => TogglePlayerDisplay(player)); + } + + public void OnPlayerDisconnect(CCSPlayerController player) + { + _fallbackDisabledPlayers.Remove(player.Slot); + } + + public void ClearCache() + { + _fallbackDisabledPlayers.Clear(); + } + + public string GetStorageType() + { + return _settingsApi != null ? "PlayerSettings" : "Memory (Fallback)"; + } + } +} \ No newline at end of file diff --git a/Services/StorageFactory.cs b/Services/StorageFactory.cs new file mode 100644 index 0000000..6229f4b --- /dev/null +++ b/Services/StorageFactory.cs @@ -0,0 +1,52 @@ +using CounterStrikeSharp.API; +using PlayerSettings; + +using SpectatorList.Configs; + +namespace SpectatorList.Services +{ + public static class StorageFactory + { + public static IStorageService CreateStorageService(SpectatorConfig config, ISettingsApi? settingsApi) + { + var storageType = config.Storage.StorageType.ToLower(); + + switch (storageType) + { + case "playersettings": + if (settingsApi != null) + { + return new PlayerSettingsStorage(settingsApi); + } + else + { + Server.PrintToConsole("[SpectatorList] PlayerSettings requested but not available, falling back to Memory storage"); + return new MemoryStorage(); + } + + case "mysql": + if (IsValidDatabaseConfig(config.Storage.Database)) + { + return new DatabaseService(config); + } + else + { + Server.PrintToConsole("[SpectatorList] MySQL requested but database configuration is invalid, falling back to Memory storage"); + return new MemoryStorage(); + } + + case "memory": + return new MemoryStorage(); + + default: + Server.PrintToConsole($"[SpectatorList] Unknown storage type '{config.Storage.StorageType}', falling back to Memory storage"); + return new MemoryStorage(); + } + } + + private static bool IsValidDatabaseConfig(DatabaseConfig dbConfig) + { + return !string.IsNullOrEmpty(dbConfig.Host) && !string.IsNullOrEmpty(dbConfig.DatabaseName) && !string.IsNullOrEmpty(dbConfig.User); + } + } +} \ No newline at end of file diff --git a/SpectatorList.cs b/SpectatorList.cs index d00f6a6..f90a0d4 100644 --- a/SpectatorList.cs +++ b/SpectatorList.cs @@ -1,12 +1,15 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Core.Capabilities; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Timers; +using PlayerSettings; using SpectatorList.Configs; using SpectatorList.Managers; +using SpectatorList.Services; namespace SpectatorList; @@ -14,7 +17,7 @@ namespace SpectatorList; public class SpectatorList : BasePlugin, IPluginConfig { public override string ModuleName => "SpectatorList"; - public override string ModuleVersion => "1.0.2"; + public override string ModuleVersion => "1.0.3"; public override string ModuleAuthor => "luca.uy"; public override string ModuleDescription => "Toggle spectator list display via chat messages with ScreenView support and exclusion flags"; @@ -22,13 +25,33 @@ public class SpectatorList : BasePlugin, IPluginConfig private CounterStrikeSharp.API.Modules.Timers.Timer? _updateTimer; private Dictionary> _lastSpectatorLists = new(); private DisplayManager? _displayManager; + private IStorageService? _storageService; + private ISettingsApi? _settingsApi; + private readonly PluginCapability _settingsCapability = new("settings:nfcore"); public void OnConfigParsed(SpectatorConfig config) { Config = config; + ValidateConfig(); + InitializeStorage(); + } + private void ValidateConfig() + { + var validStorageTypes = new[] { "playersettings", "mysql", "memory" }; + if (!validStorageTypes.Contains(Config.Storage.StorageType.ToLower())) + { + Server.PrintToConsole($"[SpectatorList] Invalid StorageType '{Config.Storage.StorageType}'. Valid options: {string.Join(", ", validStorageTypes)}"); + Server.PrintToConsole("[SpectatorList] Falling back to 'memory' storage"); + Config.Storage.StorageType = "memory"; + } + } + + private void InitializeStorage() + { _displayManager?.Dispose(); - _displayManager = new DisplayManager(Config, this); + _storageService = StorageFactory.CreateStorageService(Config, _settingsApi); + _displayManager = new DisplayManager(Config, this, _storageService); } private HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info) @@ -47,7 +70,7 @@ private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) public override void Load(bool hotReload) { - _displayManager = new DisplayManager(Config, this); + InitializeStorage(); foreach (var command in Config.Commands) { @@ -65,6 +88,19 @@ public override void Load(bool hotReload) RegisterEventHandler(OnPlayerTeam); } + public override void OnAllPluginsLoaded(bool hotReload) + { + _settingsApi = _settingsCapability.Get(); + + if (_settingsApi == null && Config.Storage.StorageType.ToLower() == "playersettings") + { + Server.PrintToConsole("[SpectatorList] PlayerSettings core not found, but was requested in config."); + Server.PrintToConsole("[SpectatorList] Reconfiguring to use fallback storage..."); + } + + InitializeStorage(); + } + public override void Unload(bool hotReload) { _updateTimer?.Kill(); @@ -72,6 +108,7 @@ public override void Unload(bool hotReload) _displayManager?.Dispose(); _displayManager = null; + _storageService = null; } private void StartUpdateTimer() @@ -172,7 +209,7 @@ private async Task HandleToggleCommand(CCSPlayerController player, CommandInfo c Server.PrintToConsole($"[SpectatorList] Error in HandleToggleCommand: {ex.Message}"); if (player.IsValid) { - player.PrintToChat($"{Localizer["prefix"]} {Localizer["database_error"]}"); + player.PrintToChat($"{Localizer["prefix"]} An error occurred while processing your request"); } }); } diff --git a/SpectatorList.csproj b/SpectatorList.csproj index f09f0da..7542cb2 100644 --- a/SpectatorList.csproj +++ b/SpectatorList.csproj @@ -11,6 +11,7 @@ + ..\PlayerSettingsApi.dll From 016bd29b2028117287f1d391a5cbf8bfc2a38b2f Mon Sep 17 00:00:00 2001 From: WiruWiru <61034981+wiruwiru@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:40:00 -0300 Subject: [PATCH 2/2] fix: improve error handling by deferring console output to the next frame --- Managers/DisplayManager.cs | 47 ++++++++++++++++++++-------- Services/DatabaseService.cs | 61 +++++++++++++++++++++++++++++-------- SpectatorList.cs | 3 +- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/Managers/DisplayManager.cs b/Managers/DisplayManager.cs index a67926a..1ab4a79 100644 --- a/Managers/DisplayManager.cs +++ b/Managers/DisplayManager.cs @@ -32,18 +32,25 @@ private async Task InitializeStorageAsync() var success = await _storageService.InitializeAsync(); var storageType = _storageService.GetStorageType(); - if (success) + Server.NextFrame(() => { - Server.PrintToConsole($"[SpectatorList] Storage initialized successfully: {storageType}"); - } - else - { - Server.PrintToConsole($"[SpectatorList] Storage initialization failed, using: {storageType}"); - } + if (success) + { + Server.PrintToConsole($"[SpectatorList] Storage initialized successfully: {storageType}"); + } + else + { + Server.PrintToConsole($"[SpectatorList] Storage initialization failed, using: {storageType}"); + } + }); } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error initializing storage: {ex.Message}"); + var errorMessage = ex.Message; + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error initializing storage: {errorMessage}"); + }); } } @@ -55,7 +62,13 @@ public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error checking display status for {player.PlayerName}: {ex.Message}"); + var playerName = player.PlayerName; + var errorMessage = ex.Message; + + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error checking display status for {playerName}: {errorMessage}"); + }); return true; } } @@ -68,7 +81,10 @@ public bool IsPlayerDisplayEnabled(CCSPlayerController player) } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error checking display status for {player.PlayerName}: {ex.Message}"); + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error checking display status for {player.PlayerName}: {ex.Message}"); + }); return true; } } @@ -95,7 +111,13 @@ public async Task TogglePlayerDisplayAsync(CCSPlayerController player) } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); + var playerName = player.PlayerName; + var errorMessage = ex.Message; + + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error toggling display for {playerName}: {errorMessage}"); + }); } } @@ -140,9 +162,10 @@ public List FilterSpectators(List spec filteredList.Add(spectator); } + var errorMessage = ex.Message; Server.NextFrame(() => { - Server.PrintToConsole($"[SpectatorList] Error checking permissions for spectator: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error checking permissions for spectator: {errorMessage}"); }); } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 9ce22f7..bfa5e7e 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -20,14 +20,16 @@ public DatabaseService(SpectatorConfig config) _config = config; } - public bool IsEnabled => !string.IsNullOrEmpty(_config.Storage.Database.Host) && - !string.IsNullOrEmpty(_config.Storage.Database.DatabaseName); + public bool IsEnabled => !string.IsNullOrEmpty(_config.Storage.Database.Host) && !string.IsNullOrEmpty(_config.Storage.Database.DatabaseName); public async Task InitializeAsync() { if (!IsEnabled) { - Server.PrintToConsole("[SpectatorList] Database configuration is incomplete"); + Server.NextFrame(() => + { + Server.PrintToConsole("[SpectatorList] Database configuration is incomplete"); + }); return false; } @@ -39,15 +41,23 @@ public async Task InitializeAsync() _isInitialized = true; _databaseAvailable = true; - Server.PrintToConsole("[SpectatorList] Database connection established and table created"); + Server.NextFrame(() => + { + Server.PrintToConsole("[SpectatorList] Database connection established and table created"); + }); return true; } catch (Exception ex) { _isInitialized = false; _databaseAvailable = false; - Server.PrintToConsole($"[SpectatorList] Failed to initialize database: {ex.Message}"); - Server.PrintToConsole("[SpectatorList] Falling back to memory storage for this session"); + + var errorMessage = ex.Message; + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Failed to initialize database: {errorMessage}"); + Server.PrintToConsole("[SpectatorList] Falling back to memory storage for this session"); + }); return false; } } @@ -97,9 +107,12 @@ public async Task IsPlayerDisplayEnabledAsync(CCSPlayerController player) } catch (Exception ex) { + var playerName = player.PlayerName; + var errorMessage = ex.Message; + Server.NextFrame(() => { - Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error loading preferences for {playerName}: {errorMessage}"); }); return !_fallbackDisabledPlayers.Contains(player.Slot); } @@ -143,9 +156,12 @@ public async Task TogglePlayerDisplayAsync(CCSPlayerController player) } catch (Exception ex) { + var playerName = player.PlayerName; + var errorMessage = ex.Message; + Server.NextFrame(() => { - Server.PrintToConsole($"[SpectatorList] Error toggling display for {player.PlayerName}: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error toggling display for {playerName}: {errorMessage}"); TogglePlayerDisplay(player); }); } @@ -192,7 +208,12 @@ FROM spectatorlist_preferences } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error loading player preferences for SteamID {steamId}: {ex.Message}"); + var errorMessage = ex.Message; + + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error loading player preferences for SteamID {steamId}: {errorMessage}"); + }); _databaseAvailable = false; return null; } @@ -227,7 +248,13 @@ ON DUPLICATE KEY UPDATE } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error saving player preferences for SteamID {preferences.SteamId}: {ex.Message}"); + var steamId = preferences.SteamId; + var errorMessage = ex.Message; + + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error saving player preferences for SteamID {steamId}: {errorMessage}"); + }); _databaseAvailable = false; return false; } @@ -263,7 +290,13 @@ private async Task LoadPlayerPreferencesAsync(CCSPlayerController player) } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Error loading preferences for {player.PlayerName}: {ex.Message}"); + var playerName = player.PlayerName; + var errorMessage = ex.Message; + + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Error loading preferences for {playerName}: {errorMessage}"); + }); } } @@ -323,7 +356,11 @@ public async Task TestConnection() } catch (Exception ex) { - Server.PrintToConsole($"[SpectatorList] Database connection test failed: {ex.Message}"); + var errorMessage = ex.Message; + Server.NextFrame(() => + { + Server.PrintToConsole($"[SpectatorList] Database connection test failed: {errorMessage}"); + }); return false; } } diff --git a/SpectatorList.cs b/SpectatorList.cs index f90a0d4..e94ab5c 100644 --- a/SpectatorList.cs +++ b/SpectatorList.cs @@ -295,9 +295,10 @@ bool ShouldShowPeriodic() } catch (Exception ex) { + var errorMessage = ex.Message; Server.NextFrame(() => { - Server.PrintToConsole($"[SpectatorList] Error in ShowPeriodicSpectatorLists: {ex.Message}"); + Server.PrintToConsole($"[SpectatorList] Error in ShowPeriodicSpectatorLists: {errorMessage}"); }); } }