diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index bdca01b..4cc76cb 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,14 +1 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username custom: ['https://paypal.me/lucauy'] \ No newline at end of file diff --git a/.gitignore b/.gitignore index f677870..b3aff79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin obj -.vs \ No newline at end of file +.vs +*.sln \ No newline at end of file diff --git a/Config.cs b/Config.cs deleted file mode 100644 index e93c0f0..0000000 --- a/Config.cs +++ /dev/null @@ -1,86 +0,0 @@ -using CounterStrikeSharp.API.Core; -using System.Text.Json.Serialization; - -namespace NeedSystem; - -public class BaseConfigs : BasePluginConfig -{ - [JsonPropertyName("WebhookUrl")] - public string WebhookUrl { get; set; } = ""; - - [JsonPropertyName("NotifyAllPlayers")] - public bool NotifyAllPlayers { get; set; } = false; - - [JsonPropertyName("IPandPORT")] - public string IPandPORT { get; set; } = "45.235.99.18:27025"; - - [JsonPropertyName("GetIPandPORTautomatic")] - public bool GetIPandPORTautomatic { get; set; } = true; - - [JsonPropertyName("UseHostname")] - public bool UseHostname { get; set; } = true; - - [JsonPropertyName("CustomDomain")] - public string CustomDomain { get; set; } = "https://crisisgamer.com/connect"; - - [JsonPropertyName("MentionRoleID")] - public string MentionRoleID { get; set; } = ""; - - [JsonPropertyName("MentionMessage")] - public bool MentionMessage { get; set; } = true; - - [JsonPropertyName("MaxServerPlayers")] - public int MaxServerPlayers { get; set; } = 12; - - [JsonPropertyName("GetMaxServerPlayers")] - public bool GetMaxServerPlayers { get; set; } = true; - - [JsonPropertyName("MinPlayers")] - public int MinPlayers { get; set; } = 10; - - [JsonPropertyName("CommandCooldownSeconds")] - public int CommandCooldownSeconds { get; set; } = 120; - - [JsonPropertyName("DontCountAdmins")] - public bool DontCountAdmins { get; set; } = false; - - [JsonPropertyName("AdminBypassFlag")] - public string AdminBypassFlag { get; set; } = "@css/generic"; - - [JsonPropertyName("Command")] - public List Command { get; set; } = new List { "css_need", ".need" }; - - [JsonPropertyName("EmbedImage")] - public bool EmbedImage { get; set; } = true; - - [JsonPropertyName("EmbedColor")] - public string EmbedColor { get; set; } = "#ffb800"; - - [JsonPropertyName("ImagesURL")] - public string ImagesURL { get; set; } = "https://imagenes.lucauy.dev/CS2/{map}.png"; - - [JsonPropertyName("PlayerNameList")] - public bool PlayerNameList { get; set; } = true; - - [JsonPropertyName("EmbedFooter")] - public bool EmbedFooter { get; set; } = true; - - [JsonPropertyName("EmbedFooterImage")] - public string EmbedFooterImage { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; - - [JsonPropertyName("EmbedAuthor")] - public bool EmbedAuthor { get; set; } = true; - - [JsonPropertyName("EmbedAuthorURL")] - public string EmbedAuthorURL { get; set; } = "https://lucauy.dev"; - - [JsonPropertyName("EmbedAuthorImage")] - public string EmbedAuthorImage { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; - - [JsonPropertyName("EmbedThumbnail")] - public bool EmbedThumbnail { get; set; } = true; - - [JsonPropertyName("EmbedThumbnailImage")] - public string EmbedThumbnailImage { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; - -} \ No newline at end of file diff --git a/Configs/BaseConfigs.cs b/Configs/BaseConfigs.cs new file mode 100644 index 0000000..ac72bc6 --- /dev/null +++ b/Configs/BaseConfigs.cs @@ -0,0 +1,22 @@ +using CounterStrikeSharp.API.Core; +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class BaseConfigs : BasePluginConfig +{ + [JsonPropertyName("Commands")] + public CommandSettings Commands { get; set; } = new(); + + [JsonPropertyName("ServerSettings")] + public ServerSettings Server { get; set; } = new(); + + [JsonPropertyName("DiscordSettings")] + public DiscordSettings Discord { get; set; } = new(); + + [JsonPropertyName("PlayerSettings")] + public PlayerSettings Player { get; set; } = new(); + + [JsonPropertyName("Database")] + public DatabaseConfig Database { get; set; } = new(); +} \ No newline at end of file diff --git a/Configs/CommandSettings.cs b/Configs/CommandSettings.cs new file mode 100644 index 0000000..7228cd5 --- /dev/null +++ b/Configs/CommandSettings.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class CommandSettings +{ + [JsonPropertyName("Command")] + public List Command { get; set; } = new List { "css_need", ".need" }; + + [JsonPropertyName("CommandCooldownSeconds")] + public int CooldownSeconds { get; set; } = 120; +} \ No newline at end of file diff --git a/Configs/DatabaseConfig.cs b/Configs/DatabaseConfig.cs new file mode 100644 index 0000000..ec23354 --- /dev/null +++ b/Configs/DatabaseConfig.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class DatabaseConfig +{ + [JsonPropertyName("Enabled")] + public bool Enabled { get; set; } = false; + + [JsonPropertyName("Host")] + public string Host { get; set; } = "localhost"; + + [JsonPropertyName("Port")] + public uint Port { get; set; } = 3306; + + [JsonPropertyName("User")] + public string User { get; set; } = "root"; + + [JsonPropertyName("Password")] + public string Password { get; set; } = ""; + + [JsonPropertyName("DatabaseName")] + public string DatabaseName { get; set; } = "needsystem"; +} \ No newline at end of file diff --git a/Configs/DiscordSettings.cs b/Configs/DiscordSettings.cs new file mode 100644 index 0000000..3cb9eef --- /dev/null +++ b/Configs/DiscordSettings.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class DiscordSettings +{ + [JsonPropertyName("WebhookUrl")] + public string WebhookUrl { get; set; } = ""; + + [JsonPropertyName("MentionRoleID")] + public string MentionRoleID { get; set; } = ""; + + [JsonPropertyName("MentionMessage")] + public bool MentionMessage { get; set; } = true; + + [JsonPropertyName("PlayerNameList")] + public bool ShowPlayerNameList { get; set; } = true; + + [JsonPropertyName("EmbedSettings")] + public EmbedSettings Embed { get; set; } = new(); +} \ No newline at end of file diff --git a/Configs/EmbedSettings.cs b/Configs/EmbedSettings.cs new file mode 100644 index 0000000..fd24cc2 --- /dev/null +++ b/Configs/EmbedSettings.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class EmbedSettings +{ + [JsonPropertyName("EmbedColor")] + public string Color { get; set; } = "#ffb800"; + + [JsonPropertyName("EmbedImage")] + public bool ShowImage { get; set; } = true; + + [JsonPropertyName("ImagesURL")] + public string ImagesURL { get; set; } = "https://cdn.jsdelivr.net/gh/wiruwiru/MapsImagesCDN-CS/png/{map}.png"; + + [JsonPropertyName("FooterSettings")] + public EmbedFooterSettings Footer { get; set; } = new(); + + [JsonPropertyName("AuthorSettings")] + public EmbedAuthorSettings Author { get; set; } = new(); + + [JsonPropertyName("ThumbnailSettings")] + public EmbedThumbnailSettings Thumbnail { get; set; } = new(); +} + +public class EmbedFooterSettings +{ + [JsonPropertyName("EmbedFooter")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("EmbedFooterImage")] + public string ImageUrl { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; +} + +public class EmbedAuthorSettings +{ + [JsonPropertyName("EmbedAuthor")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("EmbedAuthorURL")] + public string Url { get; set; } = "https://lucauy.dev"; + + [JsonPropertyName("EmbedAuthorImage")] + public string ImageUrl { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; +} + +public class EmbedThumbnailSettings +{ + [JsonPropertyName("EmbedThumbnail")] + public bool Enabled { get; set; } = true; + + [JsonPropertyName("EmbedThumbnailImage")] + public string ImageUrl { get; set; } = "https://avatars.githubusercontent.com/u/61034981?v=4"; +} \ No newline at end of file diff --git a/Configs/PlayerSettings.cs b/Configs/PlayerSettings.cs new file mode 100644 index 0000000..10d162b --- /dev/null +++ b/Configs/PlayerSettings.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class PlayerSettings +{ + [JsonPropertyName("NotifyAllPlayers")] + public bool NotifyAllPlayers { get; set; } = false; + + [JsonPropertyName("DontCountSpecAdmins")] + public bool DontCountSpecAdmins { get; set; } = false; + + [JsonPropertyName("AdminBypassFlag")] + public string AdminBypassFlag { get; set; } = "@css/generic"; +} \ No newline at end of file diff --git a/Configs/ServerSettings.cs b/Configs/ServerSettings.cs new file mode 100644 index 0000000..6338ff4 --- /dev/null +++ b/Configs/ServerSettings.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace NeedSystem.Configs; + +public class ServerSettings +{ + [JsonPropertyName("IPandPORT")] + public string IPandPORT { get; set; } = "45.235.99.18:27025"; + + [JsonPropertyName("GetIPandPORTautomatic")] + public bool GetIPandPORTautomatic { get; set; } = true; + + [JsonPropertyName("UseHostname")] + public bool UseHostname { get; set; } = false; + + [JsonPropertyName("CustomDomain")] + public string CustomDomain { get; set; } = "https://crisisgamer.com/connect"; + + [JsonPropertyName("MaxServerPlayers")] + public int MaxServerPlayers { get; set; } = 12; + + [JsonPropertyName("GetMaxServerPlayers")] + public bool GetMaxServerPlayers { get; set; } = true; + + [JsonPropertyName("MinPlayers")] + public int MinPlayers { get; set; } = 10; +} \ No newline at end of file diff --git a/Constants/LocalizationKeys.cs b/Constants/LocalizationKeys.cs new file mode 100644 index 0000000..9310a44 --- /dev/null +++ b/Constants/LocalizationKeys.cs @@ -0,0 +1,30 @@ +namespace NeedSystem.Constants; + +public static class LocalizationKeys +{ + public const string Prefix = "Prefix"; + public const string UnknownPlayer = "UnknownPlayer"; + + public const string CommandCooldownMessage = "CommandCooldownMessage"; + public const string EnoughPlayersMessage = "EnoughPlayersMessage"; + public const string NotifyPlayersMessage = "NotifyPlayersMessage"; + public const string NotifyAllPlayersMessage = "NotifyAllPlayersMessage"; + + public const string EmbedTitle = "EmbedTitle"; + public const string EmbedDescription = "EmbedDescription"; + public const string NeedInServerMessage = "NeedInServerMessage"; + + public const string ServerFieldTitle = "ServerFieldTitle"; + public const string RequestFieldTitle = "RequestFieldTitle"; + public const string MapFieldTitle = "MapFieldTitle"; + public const string PlayersFieldTitle = "PlayersFieldTitle"; + public const string ConnectionFieldTitle = "ConnectionFieldTitle"; + public const string PlayerListTitle = "PlayerListTitle"; + public const string Hour = "Hour"; + + public const string ClickToConnect = "ClickToConnect"; + public const string NoPlayersConnectedMessage = "NoPlayersConnectedMessage"; + + public const string EmbedAuthorName = "EmbedAuthorName"; + public const string EmbedFooterText = "EmbedFooterText"; +} \ No newline at end of file diff --git a/Models/DiscordEmbed.cs b/Models/DiscordEmbed.cs new file mode 100644 index 0000000..0f9e32f --- /dev/null +++ b/Models/DiscordEmbed.cs @@ -0,0 +1,72 @@ +namespace NeedSystem.Models; + +public class DiscordEmbedBuilder +{ + public string? Title { get; set; } + public string? Description { get; set; } + public int Color { get; set; } + public List Fields { get; set; } = new(); + public EmbedImage? Image { get; set; } + public EmbedFooter? Footer { get; set; } + public EmbedAuthor? Author { get; set; } + public EmbedThumbnail? Thumbnail { get; set; } + + public object Build() + { + return new + { + title = Title, + description = Description, + color = Color, + fields = Fields.Select(f => new + { + name = f.Name, + value = f.Value, + inline = f.Inline + }), + image = Image != null ? new { url = Image.Url } : null, + footer = Footer != null ? new + { + text = Footer.Text, + icon_url = Footer.IconUrl + } : null, + author = Author != null ? new + { + name = Author.Name, + url = Author.Url, + icon_url = Author.IconUrl + } : null, + thumbnail = Thumbnail != null ? new { url = Thumbnail.Url } : null + }; + } +} + +public class EmbedField +{ + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public bool Inline { get; set; } +} + +public class EmbedImage +{ + public string Url { get; set; } = string.Empty; +} + +public class EmbedFooter +{ + public string Text { get; set; } = string.Empty; + public string IconUrl { get; set; } = string.Empty; +} + +public class EmbedAuthor +{ + public string Name { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string IconUrl { get; set; } = string.Empty; +} + +public class EmbedThumbnail +{ + public string Url { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Models/NotificationRecord.cs b/Models/NotificationRecord.cs new file mode 100644 index 0000000..c0ad621 --- /dev/null +++ b/Models/NotificationRecord.cs @@ -0,0 +1,12 @@ +namespace NeedSystem.Models; + +public class NotificationRecord +{ + public string Uuid { get; set; } = string.Empty; + public string ServerAddress { get; set; } = string.Empty; + public int ConnectedPlayers { get; set; } + public int MaxPlayers { get; set; } + public string MapName { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } + public string RequestedBy { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MySqlConnector.dll b/MySqlConnector.dll new file mode 100644 index 0000000..4606dde Binary files /dev/null and b/MySqlConnector.dll differ diff --git a/NeedSystem.cs b/NeedSystem.cs index eb97e53..82110ac 100644 --- a/NeedSystem.cs +++ b/NeedSystem.cs @@ -1,95 +1,120 @@ -using System.Text; -using System.Text.Json; -using CounterStrikeSharp.API; +using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using Microsoft.Extensions.Localization; using CounterStrikeSharp.API.Modules.Commands; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Core.Attributes; -using CounterStrikeSharp.API.Modules.Utils; -using CounterStrikeSharp.API.Modules.Admin; + +using NeedSystem.Services; +using NeedSystem.Utils; +using NeedSystem.Models; +using NeedSystem.Constants; +using NeedSystem.Configs; namespace NeedSystem; -[MinimumApiVersion(290)] +[MinimumApiVersion(342)] public class NeedSystemBase : BasePlugin, IPluginConfig { - private string _currentMap = ""; - private DateTime _lastCommandTime = DateTime.MinValue; - private Translator _translator; + private CooldownService? _cooldownService; + private PlayerService? _playerService; + private DiscordService? _discordService; + private DatabaseService? _databaseService; + + private string _currentMap = string.Empty; public override string ModuleName => "NeedSystem"; - public override string ModuleVersion => "1.1.6"; + public override string ModuleVersion => "1.1.7"; public override string ModuleAuthor => "luca.uy"; public override string ModuleDescription => "Allows players to send a message to discord requesting players."; - public NeedSystemBase(IStringLocalizer localizer) - { - _translator = new Translator(localizer); - } - - public CounterStrikeSharp.API.Modules.Timers.Timer? intervalMessages; + public required BaseConfigs Config { get; set; } public override void Load(bool hotReload) { - RegisterListener(mapName => - { - _currentMap = mapName; - }); + InitializeServices(); + RegisterEventListeners(); + RegisterCommands(); - foreach (var command in Config.Command) + if (hotReload) { - AddCommand(command, "", (controller, info) => - { - if (controller == null) return; - - int secondsRemaining; - if (!CheckCommandCooldown(out secondsRemaining)) - { - controller.PrintToChat(_translator["Prefix"] + " " + _translator["CommandCooldownMessage", secondsRemaining]); - return; - } + _currentMap = Server.MapName; + } - int numberOfPlayers = GetNumberOfPlayers(); + _ = InitializeDatabaseAsync(); + } - if (numberOfPlayers >= MinPlayers()) - { - controller.PrintToChat(_translator["Prefix"] + " " + _translator["EnoughPlayersMessage"]); - return; - } + public void OnConfigParsed(BaseConfigs config) + { + Config = config; + InitializeServices(); + _ = InitializeDatabaseAsync(); + } - NeedCommand(controller, controller?.PlayerName ?? _translator["UnknownPlayer"]); + private async Task InitializeDatabaseAsync() + { + try + { + if (_databaseService != null && Config.Database.Enabled) + { + await _databaseService.InitializeDatabase(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[NeedSystem] Database initialized successfully!"); + Console.ResetColor(); + } + else if (_databaseService != null && !Config.Database.Enabled) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("[NeedSystem] Database is disabled in configuration"); + Console.ResetColor(); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[NeedSystem] Failed to initialize database: {ex.Message}"); + Console.ResetColor(); + } + } - _lastCommandTime = DateTime.Now; + private void InitializeServices() + { + _cooldownService = new CooldownService(Config.Commands.CooldownSeconds); + _playerService = new PlayerService(Config.Player.DontCountSpecAdmins, Config.Player.AdminBypassFlag); + _discordService = new DiscordService(Config.Discord.WebhookUrl); + _databaseService = new DatabaseService(Config.Database); + } - if (Config.NotifyAllPlayers) - { - Server.PrintToChatAll(_translator["Prefix"] + " " + _translator["NotifyAllPlayersMessage", controller?.PlayerName ?? _translator["UnknownPlayer"], Config.CommandCooldownSeconds]); - } - else - { - controller?.PrintToChat(_translator["Prefix"] + " " + _translator["NotifyPlayersMessage"]); - } + private void RegisterEventListeners() + { + RegisterListener(mapName => + { + _currentMap = mapName; + }); - }); - } + AddCommandListener("say", OnSayCommand); + AddCommandListener("say_team", OnSayCommand); + } - if (hotReload) + private void RegisterCommands() + { + foreach (var command in Config.Commands.Command) { - _currentMap = Server.MapName; + AddCommand(command, "Request more players on Discord", OnNeedCommand); } + } - AddCommandListener("say", Listener_Say); - AddCommandListener("say_team", Listener_Say); + private void OnNeedCommand(CCSPlayerController? controller, CommandInfo info) + { + if (controller == null) return; + ExecuteNeedCommand(controller); } - private HookResult Listener_Say(CCSPlayerController? caller, CommandInfo command) + private HookResult OnSayCommand(CCSPlayerController? caller, CommandInfo command) { if (caller == null) return HookResult.Continue; string message = command.GetCommandString; - if (Config.Command.Any(cmd => message.Contains(cmd, StringComparison.OrdinalIgnoreCase))) + if (Config.Commands.Command.Any(cmd => message.Contains(cmd, StringComparison.OrdinalIgnoreCase))) { ExecuteNeedCommand(caller); return HookResult.Handled; @@ -98,289 +123,228 @@ private HookResult Listener_Say(CCSPlayerController? caller, CommandInfo command return HookResult.Continue; } - private void ExecuteNeedCommand(CCSPlayerController? caller) + private void ExecuteNeedCommand(CCSPlayerController controller) { - if (caller == null) return; + if (_cooldownService == null || _playerService == null) return; - int secondsRemaining; - if (!CheckCommandCooldown(out secondsRemaining)) + if (!_cooldownService.CanExecute(out int secondsRemaining)) { - caller.PrintToChat(_translator["Prefix"] + " " + _translator["CommandCooldownMessage", secondsRemaining]); + controller.PrintToChat($"{Localizer[LocalizationKeys.Prefix]} {Localizer[LocalizationKeys.CommandCooldownMessage, secondsRemaining]}"); return; } - int numberOfPlayers = GetNumberOfPlayers(); - - if (numberOfPlayers >= MinPlayers()) + int playerCount = _playerService.GetPlayerCount(); + if (playerCount >= Config.Server.MinPlayers) { - caller.PrintToChat(_translator["Prefix"] + " " + _translator["EnoughPlayersMessage"]); + controller.PrintToChat($"{Localizer[LocalizationKeys.Prefix]} {Localizer[LocalizationKeys.EnoughPlayersMessage]}"); return; } - NeedCommand(caller, caller?.PlayerName ?? _translator["UnknownPlayer"]); + string playerName = controller.PlayerName ?? Localizer[LocalizationKeys.UnknownPlayer]; + SendDiscordNotification(playerName); + _cooldownService.UpdateLastExecution(); - _lastCommandTime = DateTime.Now; + NotifyPlayers(controller, playerName); + } - if (Config.NotifyAllPlayers) + private void NotifyPlayers(CCSPlayerController controller, string playerName) + { + if (Config.Player.NotifyAllPlayers) { - Server.PrintToChatAll(_translator["Prefix"] + " " + _translator["NotifyAllPlayersMessage", caller?.PlayerName ?? _translator["UnknownPlayer"], Config.CommandCooldownSeconds]); + Server.PrintToChatAll($"{Localizer[LocalizationKeys.Prefix]} {Localizer[LocalizationKeys.NotifyAllPlayersMessage, playerName, Config.Commands.CooldownSeconds]}"); } else { - caller?.PrintToChat(_translator["Prefix"] + " " + _translator["NotifyPlayersMessage"]); + controller.PrintToChat($"{Localizer[LocalizationKeys.Prefix]} {Localizer[LocalizationKeys.NotifyPlayersMessage]}"); } } - public required BaseConfigs Config { get; set; } - - public void OnConfigParsed(BaseConfigs config) + private void SendDiscordNotification(string playerName) { - Config = config; - } + if (_discordService == null || _playerService == null) return; - private bool CheckCommandCooldown(out int secondsRemaining) - { - var secondsSinceLastCommand = (int)(DateTime.Now - _lastCommandTime).TotalSeconds; - secondsRemaining = Config.CommandCooldownSeconds - secondsSinceLastCommand; - return secondsRemaining <= 0; - } + var embed = BuildDiscordEmbed(playerName); + string? mentionMessage = Config.Discord.MentionMessage ? Convert.ToString(Localizer[LocalizationKeys.NeedInServerMessage]) : null; - public int GetNumberOfPlayers() - { - var players = Utilities.GetPlayers(); - return players.Where(p => !p.IsBot && !p.IsHLTV && ShouldShowPlayerInList(p)).Count(); - } - - private bool ShouldShowPlayerInList(CCSPlayerController player) - { - if (!Config.DontCountAdmins || string.IsNullOrEmpty(Config.AdminBypassFlag)) - return true; - - try + NotificationRecord? notificationRecord = null; + if (_databaseService != null && _databaseService.IsEnabled()) { - if (!AdminManager.PlayerHasPermissions(player, Config.AdminBypassFlag)) - return true; + string cleanPlayerName = TextHelper.CleanPlayerName(playerName); + string serverAddress = ServerHelper.GetServerAddress(Config.Server.GetIPandPORTautomatic, Config.Server.IPandPORT); + int maxPlayers = ServerHelper.GetMaxPlayers(Config.Server.GetMaxServerPlayers, Config.Server.MaxServerPlayers, Server.MaxPlayers); + int playerCount = _playerService.GetPlayerCount(); - return player.Team == CsTeam.Terrorist || player.Team == CsTeam.CounterTerrorist; + notificationRecord = new NotificationRecord + { + Uuid = Guid.NewGuid().ToString(), + ServerAddress = serverAddress, + ConnectedPlayers = playerCount, + MaxPlayers = maxPlayers, + MapName = _currentMap, + Timestamp = DateTime.Now, + RequestedBy = cleanPlayerName + }; } - catch + + Task.Run(async () => { - return true; - } + await _discordService.SendEmbedAsync( + embed.Build(), + Config.Discord.MentionRoleID, + mentionMessage + ); + + if (notificationRecord != null && _databaseService != null) + { + await SaveNotificationToDatabase(notificationRecord); + } + }); } - private int ConvertHexToColor(string hex) + private async Task SaveNotificationToDatabase(NotificationRecord record) { - if (hex.StartsWith("#")) + if (_databaseService == null) return; + + try { - hex = hex[1..]; + bool saved = await _databaseService.SaveNotification(record); + if (saved) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[NeedSystem] Notification record saved to database (UUID: {record.Uuid})"); + Console.ResetColor(); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[NeedSystem] Error saving notification to database: {ex.Message}"); + Console.ResetColor(); } - return int.Parse(hex, System.Globalization.NumberStyles.HexNumber); } - public void NeedCommand(CCSPlayerController? caller, string clientName) + private DiscordEmbedBuilder BuildDiscordEmbed(string playerName) { - if (caller == null) return; + string cleanPlayerName = TextHelper.CleanPlayerName(playerName); + string serverAddress = ServerHelper.GetServerAddress(Config.Server.GetIPandPORTautomatic, Config.Server.IPandPORT); + int maxPlayers = ServerHelper.GetMaxPlayers(Config.Server.GetMaxServerPlayers, Config.Server.MaxServerPlayers, Server.MaxPlayers); + string currentTime = DateTime.Now.ToString("HH:mm"); - clientName = clientName.Replace("[Ready]", "").Replace("[Not Ready]", "").Trim(); + var embedBuilder = new DiscordEmbedBuilder + { + Title = Config.Server.UseHostname + ? ServerHelper.GetServerHostname(ColorHelper.StripColorCodes(Localizer[LocalizationKeys.EmbedTitle])) + : ColorHelper.StripColorCodes(Localizer[LocalizationKeys.EmbedTitle]), + Description = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.EmbedDescription]), + Color = ColorHelper.ConvertHexToColor(Config.Discord.Embed.Color) + }; - string imageUrl = Config.ImagesURL.Replace("{map}", _currentMap); - string hour = DateTime.Now.ToString("HH:mm"); - string playerList = string.Empty; + AddBasicFields(embedBuilder, serverAddress, maxPlayers, currentTime, cleanPlayerName); - if (Config.PlayerNameList) + if (Config.Discord.ShowPlayerNameList && _playerService != null) { - var players = Utilities.GetPlayers() - .Where(p => !p.IsBot && !p.IsHLTV && p.Connected == PlayerConnectedState.PlayerConnected) - .Where(ShouldShowPlayerInList) - .Select(p => new { p.PlayerName, p.SteamID }) - .ToList(); - - if (players.Any()) + string playerList = _playerService.GetFormattedPlayerList(Localizer[LocalizationKeys.NoPlayersConnectedMessage]); + embedBuilder.Fields.Add(new EmbedField { - var playerDetails = players - .Select(p => $"[{p.PlayerName.Replace("[Ready]", "").Replace("[Not Ready]", "").Trim()}](https://steamcommunity.com/profiles/{p.SteamID})") - .ToList(); - playerList = string.Join(", ", playerDetails); - } - else - { - playerList = _translator["NoPlayersConnectedMessage"]; - } + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.PlayerListTitle]), + Value = playerList, + Inline = false + }); } - var fields = new List - { - new + AddConnectionField(embedBuilder, serverAddress); + ConfigureVisualElements(embedBuilder); + + return embedBuilder; + } + + private void AddBasicFields(DiscordEmbedBuilder builder, string serverAddress, int maxPlayers, string time, string playerName) + { + if (_playerService == null) return; + + builder.Fields.AddRange( + [ + new EmbedField { - name = _translator["ServerFieldTitle"], - value = $"```{GetIP()}```", - inline = true + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.ServerFieldTitle]), + Value = $"```{serverAddress}```", + Inline = true }, - new + new EmbedField { - name = _translator["PlayersFieldTitle"], - value = $"```{GetNumberOfPlayers()}/{MaxServerPlayers()}```", - inline = true + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.PlayersFieldTitle]), + Value = $"```{_playerService.GetPlayerCount()}/{maxPlayers}```", + Inline = true }, - new + new EmbedField { - name = _translator["Hour"], - value = $"```{hour}```", - inline = true + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.Hour]), + Value = $"```{time}```", + Inline = true }, - new + new EmbedField { - name = _translator["MapFieldTitle"], - value = $"```{_currentMap}```", - inline = true + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.MapFieldTitle]), + Value = $"```{_currentMap}```", + Inline = true }, - new + new EmbedField { - name = _translator["RequestFieldTitle"], - value = $"```{clientName}```", - inline = true - }, - - }; + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.RequestFieldTitle]), + Value = $"```{playerName}```", + Inline = true + } + ]); + } - if (Config.PlayerNameList) - { - fields.Add(new - { - name = _translator["PlayerListTitle"], - value = playerList, - inline = false - }); - } + private void AddConnectionField(DiscordEmbedBuilder builder, string serverAddress) + { + string clickToConnect = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.ClickToConnect]); + string connectionUrl = $"{Config.Server.CustomDomain}?ip={serverAddress}"; - fields.Add(new + builder.Fields.Add(new EmbedField { - name = _translator["ConnectionFieldTitle"], - value = $"[**`connect {GetIP()}`**]({GetCustomDomain()}?ip={GetIP()}) {_translator["ClickToConnect"]}", - inline = false + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.ConnectionFieldTitle]), + Value = $"[**`connect {serverAddress}`**]({connectionUrl}) {clickToConnect}", + Inline = false }); - - var embed = new - { - title = Config.UseHostname ? (ConVar.Find("hostname")?.StringValue ?? _translator["EmbedTitle"]) : _translator["EmbedTitle"], - description = _translator["EmbedDescription"], - color = ConvertHexToColor(Config.EmbedColor), - fields, - image = Config.EmbedImage ? new - { - url = imageUrl - } : null, - footer = Config.EmbedFooter ? new - { - text = _translator["EmbedFooterText"], - icon_url = Config.EmbedFooterImage - } : null, - author = Config.EmbedAuthor ? new - { - name = _translator["EmbedAuthorName"], - url = Config.EmbedAuthorURL, - icon_url = Config.EmbedAuthorImage - } : null, - thumbnail = Config.EmbedThumbnail ? new - { - url = Config.EmbedThumbnailImage, - } : null - }; - - Task.Run(() => SendEmbedToDiscord(embed)); } - private async Task SendEmbedToDiscord(object embed) + private void ConfigureVisualElements(DiscordEmbedBuilder builder) { - try + if (Config.Discord.Embed.ShowImage) { - var webhookUrl = GetWebhook(); - - if (string.IsNullOrEmpty(webhookUrl)) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Webhook URL is null or empty, skipping Discord notification."); - return; - } - - var httpClient = new HttpClient(); - - string content = string.Empty; - if (Config.MentionMessage) - { - string mention = !string.IsNullOrEmpty(MentionRoleID()) ? $"<@&{MentionRoleID()}>" : string.Empty; - content = $"{mention} {_translator["NeedInServerMessage"]}"; - } - - var payload = new + builder.Image = new EmbedImage { - content, - embeds = new[] { embed } + Url = Config.Discord.Embed.ImagesURL.Replace("{map}", _currentMap) }; - - var json = JsonSerializer.Serialize(payload); - var contentString = new StringContent(json, Encoding.UTF8, "application/json"); - var response = await httpClient.PostAsync(webhookUrl, contentString); - - Console.ForegroundColor = response.IsSuccessStatusCode ? ConsoleColor.Green : ConsoleColor.Red; - Console.WriteLine(response.IsSuccessStatusCode ? "Success" : $"Error: {response.StatusCode}"); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; } - } - private string GetWebhook() - { - return Config.WebhookUrl; - } - private string GetCustomDomain() - { - return Config.CustomDomain; - } - private string GetIP() - { - if (Config.GetIPandPORTautomatic) + if (Config.Discord.Embed.Footer.Enabled) { - string? ip = ConVar.Find("ip")?.StringValue; - string? port = ConVar.Find("hostport")?.GetPrimitiveValue().ToString(); - - if (!string.IsNullOrEmpty(ip) && !string.IsNullOrEmpty(port)) - { - return $"{ip}:{port}"; - } - else + builder.Footer = new EmbedFooter { - return Config.IPandPORT; - } - } - else - { - return Config.IPandPORT; + Text = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.EmbedFooterText]), + IconUrl = Config.Discord.Embed.Footer.ImageUrl + }; } - } - private string MentionRoleID() - { - return Config.MentionRoleID; - } - private string MaxServerPlayers() - { - if (Config.GetMaxServerPlayers) + if (Config.Discord.Embed.Author.Enabled) { - return Server.MaxPlayers.ToString(); + builder.Author = new EmbedAuthor + { + Name = ColorHelper.StripColorCodes(Localizer[LocalizationKeys.EmbedAuthorName]), + Url = Config.Discord.Embed.Author.Url, + IconUrl = Config.Discord.Embed.Author.ImageUrl + }; } - else + + if (Config.Discord.Embed.Thumbnail.Enabled) { - return Config.MaxServerPlayers.ToString(); + builder.Thumbnail = new EmbedThumbnail + { + Url = Config.Discord.Embed.Thumbnail.ImageUrl + }; } } - - private int MinPlayers() - { - return Config.MinPlayers; - } } \ No newline at end of file diff --git a/NeedSystem.csproj b/NeedSystem.csproj index d832d49..9c183a9 100644 --- a/NeedSystem.csproj +++ b/NeedSystem.csproj @@ -4,17 +4,18 @@ net8.0 enable enable + false + false - - ..\..\..\CounterStrikeSharp.API.dll - + + - + - + \ No newline at end of file diff --git a/NeedSystem.sln b/NeedSystem.sln deleted file mode 100644 index e1311da..0000000 --- a/NeedSystem.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34723.18 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NeedSystem", "NeedSystem.csproj", "{232F1448-0C67-4FEF-B04D-26ED121BEBB7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {232F1448-0C67-4FEF-B04D-26ED121BEBB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {232F1448-0C67-4FEF-B04D-26ED121BEBB7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {232F1448-0C67-4FEF-B04D-26ED121BEBB7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {232F1448-0C67-4FEF-B04D-26ED121BEBB7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B5445350-33E4-4456-9D30-7E49A78EFFF2} - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md index 7bcf12d..c7b628b 100644 --- a/README.md +++ b/README.md @@ -15,76 +15,152 @@ https://github.com/user-attachments/assets/fca8fed1-c07c-4546-9972-dc1cd49ab769 5. Complete the configuration file with the parameters of your choice. # Config -| Parameter | Description | Required | + +## Commands Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | +| `Command` | You can change the command to be used by the players or add extra commands. | **YES** | +| `CommandCooldownSeconds` | Command cooldown time in seconds. | **YES** | + +## Server Settings +| Parameter | Description | Required | | :------- | :------- | :------- | -| `WebhookUrl` | You must create it in the channel where you will send the notices. |**YES** | -| `NotifyAllPlayers` | When a user uses !need it notifies the whole server that the command was used and how long it takes to be able to use the command again, if it is set to false only the user who used the command will be notified. |**YES** | -| `IPandPORT` | Replace with the IP address of your server. |**YES** | +| `IPandPORT` | Replace with the IP address of your server. | **YES** | | `GetIPandPORTautomatic` | When you activate this option the plugin will try to get the IP:PORT of your server automatically, in case it is not possible use the IPandPORT configuration. | **YES** | -| `UseHostname` | If you set this configuration to true, the “EmbedTitle” of the translation will be replaced by the hostname you have configured in your server.cfg file. | **YES** | -| `CustomDomain` | You can replace it with your domain if you want, the connect.php file is available in the main branch |**YES** | -| `MentionRoleID` | You must have the discord developer mode activated, right click on the role and copy its ID. |**NO** | -| `MentionMessage` | You can use this option to deactivate the mention message completely, with this deactivated only the embed will be sent. |**YES** | -| `MaxServerPlayers` | Maximum number of slots your server has. |**YES** | -| `GetMaxServerPlayers` | When you activate this option the plugin will try to get the maximum number of players on the server automatically, in case it is not possible use the MaxServerPlayers configuration. |**YES** | +| `UseHostname` | If you set this configuration to true, the "EmbedTitle" of the translation will be replaced by the hostname you have configured in your server.cfg file. | **YES** | +| `CustomDomain` | You can replace it with your domain if you want, the connect.php file is available in the main branch. | **YES** | +| `MaxServerPlayers` | Maximum number of slots your server has. | **YES** | +| `GetMaxServerPlayers` | When you activate this option the plugin will try to get the maximum number of players on the server automatically, in case it is not possible use the MaxServerPlayers configuration. | **YES** | | `MinPlayers` | In this case if there are ten or more players connected the command cannot be used. | **YES** | -| `CommandCooldownSeconds` | Command cooldown time in seconds. | **YES** | -| `Command` | You can change the command to be used by the players or add extra commands. | **YES** | -| `EmbedImage` | Enables or disables the map image to be shown in the Embed. | **YES** | + +## Discord Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | +| `WebhookUrl` | You must create it in the channel where you will send the notices. | **YES** | +| `MentionRoleID` | You must have the discord developer mode activated, right click on the role and copy its ID. | **NO** | +| `MentionMessage` | You can use this option to deactivate the mention message completely, with this deactivated only the embed will be sent. | **YES** | +| `PlayerNameList` | Displays a list of the names and profiles of the users who are logged in at the time the command is sent. | **YES** | + +### Embed Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | | `EmbedColor` | You can change this to your favorite color, in Hex format. | **YES** | +| `EmbedImage` | Enables or disables the map image to be shown in the Embed. | **YES** | | `ImagesURL` | Url from where the map images are taken, recommended to use your own url if you use workshop maps. | **YES** | -| `PlayerNameList` | Displays a list of the names and profiles of the users who are logged in at the time the command is sent. | **YES** | + +### Footer Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | | `EmbedFooter` | You can use this option to disable or enable the embed footer. | **YES** | | `EmbedFooterImage` | It will be the image (logo) that will appear in the embed footer. | **YES** | + +### Author Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | | `EmbedAuthor` | You can use this option to disable or enable the embed author. | **YES** | | `EmbedAuthorURL` | This will be the url that will be redirected to when a user clicks on the embed author. | **YES** | | `EmbedAuthorImage` | It will be the image (logo) that will appear as the author of the embed. | **YES** | + +### Thumbnail Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | | `EmbedThumbnail` | You can use this option to disable or enable the embed thumbnail. | **YES** | -| `EmbedThumbnailImage` | It will be the image (logo) that will appear as the author of the embed. | **YES** | +| `EmbedThumbnailImage` | It will be the image (logo) that will appear as the thumbnail of the embed. | **YES** | + +## Player Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | +| `NotifyAllPlayers` | When a user uses !need it notifies the whole server that the command was used and how long it takes to be able to use the command again, if it is set to false only the user who used the command will be notified. | **YES** | +| `DontCountSpecAdmins` | When enabled, admins in spectator mode won't be counted towards the player count. | **YES** | +| `AdminBypassFlag` | The admin flag required to bypass spectator counting (only works if DontCountSpecAdmins is enabled). | **YES** | + +## Database Settings +| Parameter | Description | Required | +| :------- | :------- | :------- | +| `Enabled` | Enables or disables the database functionality. When disabled, no database operations will be performed, maintaining optimal performance. | **YES** | +| `Host` | MySQL database host address. | **NO*** | +| `Port` | MySQL database port (default: 3306). | **NO*** | +| `User` | MySQL database username. | **NO*** | +| `Password` | MySQL database password. | **NO*** | +| `DatabaseName` | Name of the database to use. | **NO*** | + +**Required only if `Enabled` is set to `true`* + +### Database Features +When the database is enabled, the plugin will automatically store the following information for each notification sent: +- **UUID**: Unique identifier for each notification +- **Server Address**: IP:PORT of the server +- **Connected Players**: Number of players online at the time +- **Max Players**: Server capacity +- **Map Name**: Current map being played +- **Timestamp**: Date and time of the notification +- **Requested By**: Player who triggered the command + +This data is stored in the `need_notifications` table and can be used for statistics, analytics, or historical tracking. ## Configuration example -``` +```json { - "WebhookUrl": "https://discord.com/api/webhooks/xxxxx/xxxxxxxxx, - "NotifyAllPlayers": false, + "Commands": { + "Command": ["css_need", ".need"], + "CommandCooldownSeconds": 120 + }, + "ServerSettings": { "IPandPORT": "45.235.99.18:27025", "GetIPandPORTautomatic": true, "UseHostname": true, - "CustomDomain": "https://crisisgamer.com/redirect/connect.php", - "MentionRoleID": "1111767358881681519", - "MentionMessage": true, + "CustomDomain": "https://crisisgamer.com/connect", "MaxServerPlayers": 12, "GetMaxServerPlayers": true, - "MinPlayers": 10, - "CommandCooldownSeconds": 120, - "Command": [ "css_need", ".need" ], - "EmbedImage": true, - "EmbedColor": "#ffb800", - "ImagesURL": "https://imagenes.lucauy.dev/CS2/{map}.png", + "MinPlayers": 10 + }, + "DiscordSettings": { + "WebhookUrl": "https://discord.com/api/webhooks/xxxxx/xxxxxxxxx", + "MentionRoleID": "1111767358881681519", + "MentionMessage": true, "PlayerNameList": true, - "EmbedFooter": false, - "EmbedFooterImage": "https://avatars.githubusercontent.com/u/61034981?v=4", - "EmbedAuthor": false, - "EmbedAuthorURL": "https://lucauy.dev", - "EmbedAuthorImage": "https://avatars.githubusercontent.com/u/61034981?v=4", - "EmbedThumbnail": true, - "EmbedThumbnailImage": "https://avatars.githubusercontent.com/u/61034981?v=4", + "EmbedSettings": { + "EmbedColor": "#ffb800", + "EmbedImage": true, + "ImagesURL": "https://cdn.jsdelivr.net/gh/wiruwiru/MapsImagesCDN-CS/png/{map}.png", + "FooterSettings": { + "EmbedFooter": true, + "EmbedFooterImage": "https://avatars.githubusercontent.com/u/61034981?v=4" + }, + "AuthorSettings": { + "EmbedAuthor": true, + "EmbedAuthorURL": "https://lucauy.dev", + "EmbedAuthorImage": "https://avatars.githubusercontent.com/u/61034981?v=4" + }, + "ThumbnailSettings": { + "EmbedThumbnail": true, + "EmbedThumbnailImage": "https://avatars.githubusercontent.com/u/61034981?v=4" + } + } + }, + "PlayerSettings": { + "NotifyAllPlayers": false, + "DontCountSpecAdmins": false, + "AdminBypassFlag": "@css/generic" + }, + "Database": { + "Enabled": false, + "Host": "localhost", + "Port": 3306, + "User": "", + "Password": "", + "DatabaseName": "" + } } ``` # Lang configuration - In the 'lang' folder, you'll find various files. For instance, 'es.json' is designated for the Spanish language. You're welcome to modify this file to better suit your style and language preferences. The language utilized depends on your settings in 'core.json' of CounterStrikeSharp. # Custom domain configuration - To configure CustomDomain you must first upload the “connect.php” file to your web hosting, after you have done this step you must place the url of this file in the configuration file. It should look like this `https://domain.com/redirect/connect.php` (EXAMPLE URL). In case you don't have a web hosting you can leave the default url. You can download the **`connect.php`** file directly from here: [Download connect.php](https://raw.githubusercontent.com/wiruwiru/NeedSystem-CS2/main/connect.php). > **Note:** Right-click the link and select "Save link as..." to download the file directly. # Default commands -`!need` `.need` - Send message to Discord - -## TO-DO -- [x] Change configuration file location -- Any improvement you propose to me that you feel would be a good option +`!need` `.need` - Send message to Discord \ No newline at end of file diff --git a/Services/CooldownService.cs b/Services/CooldownService.cs new file mode 100644 index 0000000..34f2ee4 --- /dev/null +++ b/Services/CooldownService.cs @@ -0,0 +1,29 @@ +namespace NeedSystem.Services; + +public class CooldownService +{ + private DateTime _lastCommandTime = DateTime.MinValue; + private readonly int _cooldownSeconds; + + public CooldownService(int cooldownSeconds) + { + _cooldownSeconds = cooldownSeconds; + } + + public bool CanExecute(out int secondsRemaining) + { + var secondsSinceLastCommand = (int)(DateTime.Now - _lastCommandTime).TotalSeconds; + secondsRemaining = _cooldownSeconds - secondsSinceLastCommand; + return secondsRemaining <= 0; + } + + public void UpdateLastExecution() + { + _lastCommandTime = DateTime.Now; + } + + public void Reset() + { + _lastCommandTime = DateTime.MinValue; + } +} \ No newline at end of file diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs new file mode 100644 index 0000000..ec1e8db --- /dev/null +++ b/Services/DatabaseService.cs @@ -0,0 +1,237 @@ +using MySqlConnector; +using NeedSystem.Configs; +using NeedSystem.Models; + +namespace NeedSystem.Services; + +public class DatabaseService +{ + private readonly DatabaseConfig _config; + private readonly bool _enabled; + + public DatabaseService(DatabaseConfig config) + { + _config = config; + _enabled = config.Enabled; + + if (_enabled) + { + LogInfo("DatabaseService initialized and enabled"); + } + else + { + LogInfo("DatabaseService initialized but disabled in configuration"); + } + } + + public async Task InitializeDatabase() + { + if (!_enabled) + { + LogInfo("Database is disabled, skipping initialization"); + return; + } + + try + { + using var connection = GetConnection(); + await connection.OpenAsync(); + await CreateTable(connection); + LogInfo("Database connection established and table created"); + } + catch (Exception ex) + { + LogError($"Failed to initialize database: {ex.Message}"); + throw; + } + } + + private async Task CreateTable(MySqlConnection connection) + { + var createTableQuery = @" + CREATE TABLE IF NOT EXISTS need_notifications ( + uuid VARCHAR(36) PRIMARY KEY UNIQUE NOT NULL, + server_address VARCHAR(64) NOT NULL, + connected_players INT NOT NULL, + max_players INT NOT NULL, + map_name VARCHAR(64) NOT NULL, + timestamp DATETIME NOT NULL, + requested_by VARCHAR(128) NOT NULL, + INDEX idx_timestamp (timestamp), + INDEX idx_server (server_address) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + + using var cmd = new MySqlCommand(createTableQuery, connection); + await cmd.ExecuteNonQueryAsync(); + LogInfo("Notifications table created/verified"); + } + + public async Task SaveNotification(NotificationRecord record) + { + if (!_enabled) + { + return false; + } + + try + { + using var connection = GetConnection(); + await connection.OpenAsync(); + + var insertQuery = @" + INSERT INTO need_notifications + (uuid, server_address, connected_players, max_players, map_name, timestamp, requested_by) + VALUES + (@uuid, @server_address, @connected_players, @max_players, @map_name, @timestamp, @requested_by)"; + + using var cmd = new MySqlCommand(insertQuery, connection); + cmd.Parameters.AddWithValue("@uuid", record.Uuid); + cmd.Parameters.AddWithValue("@server_address", record.ServerAddress); + cmd.Parameters.AddWithValue("@connected_players", record.ConnectedPlayers); + cmd.Parameters.AddWithValue("@max_players", record.MaxPlayers); + cmd.Parameters.AddWithValue("@map_name", record.MapName); + cmd.Parameters.AddWithValue("@timestamp", record.Timestamp); + cmd.Parameters.AddWithValue("@requested_by", record.RequestedBy); + + await cmd.ExecuteNonQueryAsync(); + LogInfo($"Notification saved to database - UUID: {record.Uuid}"); + return true; + } + catch (Exception ex) + { + LogError($"Error saving notification to database: {ex.Message}"); + return false; + } + } + + public async Task> GetRecentNotifications(int limit = 10) + { + if (!_enabled) + { + return new List(); + } + + try + { + using var connection = GetConnection(); + await connection.OpenAsync(); + + var selectQuery = @" + SELECT uuid, server_address, connected_players, max_players, map_name, timestamp, requested_by + FROM need_notifications + ORDER BY timestamp DESC + LIMIT @limit"; + + using var cmd = new MySqlCommand(selectQuery, connection); + cmd.Parameters.AddWithValue("@limit", limit); + + var records = new List(); + using var reader = await cmd.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) + { + records.Add(new NotificationRecord + { + Uuid = reader.GetString("uuid"), + ServerAddress = reader.GetString("server_address"), + ConnectedPlayers = reader.GetInt32("connected_players"), + MaxPlayers = reader.GetInt32("max_players"), + MapName = reader.GetString("map_name"), + Timestamp = reader.GetDateTime("timestamp"), + RequestedBy = reader.GetString("requested_by") + }); + } + + return records; + } + catch (Exception ex) + { + LogError($"Error retrieving notifications from database: {ex.Message}"); + return new List(); + } + } + + public async Task GetNotificationCount() + { + if (!_enabled) + { + return 0; + } + + try + { + using var connection = GetConnection(); + await connection.OpenAsync(); + + var countQuery = "SELECT COUNT(*) FROM need_notifications"; + using var cmd = new MySqlCommand(countQuery, connection); + + var result = await cmd.ExecuteScalarAsync(); + return Convert.ToInt32(result); + } + catch (Exception ex) + { + LogError($"Error getting notification count: {ex.Message}"); + return 0; + } + } + + public async Task TestConnection() + { + if (!_enabled) + { + LogInfo("Database is disabled, skipping connection test"); + return false; + } + + try + { + using var connection = GetConnection(); + await connection.OpenAsync(); + LogInfo("Database connection test successful"); + return true; + } + catch (Exception ex) + { + LogError($"Database connection test failed: {ex.Message}"); + return false; + } + } + + private MySqlConnection GetConnection() + { + if (_config == null) + { + throw new InvalidOperationException("Database configuration is null"); + } + + var builder = new MySqlConnectionStringBuilder + { + Server = _config.Host, + Port = _config.Port, + UserID = _config.User, + Database = _config.DatabaseName, + Password = _config.Password, + Pooling = true, + SslMode = MySqlSslMode.Preferred + }; + + return new MySqlConnection(builder.ConnectionString); + } + + private void LogInfo(string message) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"[NeedSystem Database] {message}"); + Console.ResetColor(); + } + + private void LogError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[NeedSystem Database] {message}"); + Console.ResetColor(); + } + + public bool IsEnabled() => _enabled; +} \ No newline at end of file diff --git a/Services/DiscordService.cs b/Services/DiscordService.cs new file mode 100644 index 0000000..e1f77bf --- /dev/null +++ b/Services/DiscordService.cs @@ -0,0 +1,85 @@ +using System.Text; +using System.Text.Json; + +using NeedSystem.Models; +using NeedSystem.Utils; + +namespace NeedSystem.Services; + +public class DiscordService +{ + private readonly string _webhookUrl; + private readonly HttpClient _httpClient; + + public DiscordService(string webhookUrl) + { + _webhookUrl = webhookUrl; + _httpClient = new HttpClient(); + } + + public async Task SendEmbedAsync(object embed, string? mentionRoleId = null, string? mentionMessage = null) + { + try + { + if (string.IsNullOrEmpty(_webhookUrl)) + { + LogWarning("Webhook URL is null or empty, skipping Discord notification."); + return; + } + + string content = BuildMentionContent(mentionRoleId, mentionMessage); + + var payload = new + { + content, + embeds = new[] { embed } + }; + + var json = JsonSerializer.Serialize(payload); + var contentString = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync(_webhookUrl, contentString); + + LogResponse(response.IsSuccessStatusCode, response.StatusCode.ToString()); + } + catch (Exception e) + { + LogError("Error sending Discord embed", e); + throw; + } + } + + private string BuildMentionContent(string? mentionRoleId, string? mentionMessage) + { + if (string.IsNullOrEmpty(mentionMessage)) + return string.Empty; + + string mention = !string.IsNullOrEmpty(mentionRoleId) + ? $"<@&{mentionRoleId}>" + : string.Empty; + + return $"{mention} {ColorHelper.StripColorCodes(mentionMessage)}".Trim(); + } + + private void LogWarning(string message) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[NeedSystem] {message}"); + Console.ResetColor(); + } + + private void LogResponse(bool success, string statusCode) + { + Console.ForegroundColor = success ? ConsoleColor.Green : ConsoleColor.Red; + Console.WriteLine(success + ? "[NeedSystem] Discord notification sent successfully" + : $"[NeedSystem] Error sending Discord notification: {statusCode}"); + Console.ResetColor(); + } + + private void LogError(string message, Exception e) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[NeedSystem] {message}: {e.Message}"); + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/Services/PlayerService.cs b/Services/PlayerService.cs new file mode 100644 index 0000000..e648767 --- /dev/null +++ b/Services/PlayerService.cs @@ -0,0 +1,69 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Admin; +using CounterStrikeSharp.API.Modules.Utils; + +using NeedSystem.Utils; + +namespace NeedSystem.Services; + +public class PlayerService +{ + private readonly bool _dontCountSpecAdmins; + private readonly string _adminBypassFlag; + + public PlayerService(bool dontCountSpecAdmins, string adminBypassFlag) + { + _dontCountSpecAdmins = dontCountSpecAdmins; + _adminBypassFlag = adminBypassFlag; + } + + public int GetPlayerCount() + { + var players = Utilities.GetPlayers(); + return players.Where(p => !p.IsBot && !p.IsHLTV && ShouldCountPlayer(p)).Count(); + } + + public List<(string Name, ulong SteamId)> GetConnectedPlayers() + { + return Utilities.GetPlayers() + .Where(p => !p.IsBot && !p.IsHLTV && p.Connected == PlayerConnectedState.PlayerConnected) + .Where(ShouldCountPlayer) + .Select(p => (p.PlayerName, p.SteamID)) + .ToList(); + } + + public string GetFormattedPlayerList(string noPlayersMessage) + { + var players = GetConnectedPlayers(); + + if (!players.Any()) + { + return ColorHelper.StripColorCodes(noPlayersMessage); + } + + var playerLinks = players + .Select(p => TextHelper.GenerateSteamLink(p.Name, p.SteamId)) + .ToList(); + + return string.Join(", ", playerLinks); + } + + private bool ShouldCountPlayer(CCSPlayerController player) + { + if (!_dontCountSpecAdmins || string.IsNullOrEmpty(_adminBypassFlag)) + return true; + + try + { + if (!AdminManager.PlayerHasPermissions(player, _adminBypassFlag)) + return true; + + return player.Team == CsTeam.Terrorist || player.Team == CsTeam.CounterTerrorist; + } + catch + { + return true; + } + } +} \ No newline at end of file diff --git a/Translator.cs b/Translator.cs deleted file mode 100644 index e7317e0..0000000 --- a/Translator.cs +++ /dev/null @@ -1,73 +0,0 @@ -using CounterStrikeSharp.API.Modules.Utils; -using Microsoft.Extensions.Localization; - -namespace NeedSystem; - -public class Translator -{ - private IStringLocalizer _stringLocalizerImplementation; - - public Translator(IStringLocalizer localizer) - { - _stringLocalizerImplementation = localizer; - } - - public IEnumerable GetAllStrings(bool includeParentCultures) - { - return _stringLocalizerImplementation.GetAllStrings(includeParentCultures); - } - - public string this[string name] => Translate(name); - - public string this[string name, params object[] arguments] => Translate(name, arguments); - - private const string CenterModifier = "center."; - private const string HtmlModifier = "html."; - - private string Translate(string key, params object[] arguments) - { - var isCenter = key.StartsWith(CenterModifier); - var isHtml = key.StartsWith(HtmlModifier); - - if (isCenter) - { - key = key.Substring(CenterModifier.Length); - } - else if (isHtml) - { - key = key.Substring(HtmlModifier.Length); - } - - var localizedString = _stringLocalizerImplementation[key, arguments]; - - if (localizedString == null || localizedString.ResourceNotFound) - { - return key; - } - - var translation = localizedString.Value; - - return translation - .Replace("[GREEN]", isCenter ? "" : isHtml ? "" : ChatColors.Green.ToString()) - .Replace("[RED]", isCenter ? "" : isHtml ? "" : ChatColors.Red.ToString()) - .Replace("[YELLOW]", isCenter ? "" : isHtml ? "" : ChatColors.Yellow.ToString()) - .Replace("[BLUE]", isCenter ? "" : isHtml ? "" : ChatColors.Blue.ToString()) - .Replace("[PURPLE]", isCenter ? "" : isHtml ? "" : ChatColors.Purple.ToString()) - .Replace("[ORANGE]", isCenter ? "" : isHtml ? "" : ChatColors.Orange.ToString()) - .Replace("[WHITE]", isCenter ? "" : isHtml ? "" : ChatColors.White.ToString()) - .Replace("[NORMAL]", isCenter ? "" : isHtml ? "" : ChatColors.White.ToString()) - .Replace("[GREY]", isCenter ? "" : isHtml ? "" : ChatColors.Grey.ToString()) - .Replace("[LIGHT_RED]", isCenter ? "" : isHtml ? "" : ChatColors.LightRed.ToString()) - .Replace("[LIGHT_BLUE]", isCenter ? "" : isHtml ? "" : ChatColors.LightBlue.ToString()) - .Replace("[LIGHT_PURPLE]", isCenter ? "" : isHtml ? "" : ChatColors.LightPurple.ToString()) - .Replace("[LIGHT_YELLOW]", isCenter ? "" : isHtml ? "" : ChatColors.LightYellow.ToString()) - .Replace("[DARK_RED]", isCenter ? "" : isHtml ? "" : ChatColors.DarkRed.ToString()) - .Replace("[DARK_BLUE]", isCenter ? "" : isHtml ? "" : ChatColors.DarkBlue.ToString()) - .Replace("[BLUE_GREY]", isCenter ? "" : isHtml ? "" : ChatColors.BlueGrey.ToString()) - .Replace("[OLIVE]", isCenter ? "" : isHtml ? "" : ChatColors.Olive.ToString()) - .Replace("[LIME]", isCenter ? "" : isHtml ? "" : ChatColors.Lime.ToString()) - .Replace("[GOLD]", isCenter ? "" : isHtml ? "" : ChatColors.Gold.ToString()) - .Replace("[SILVER]", isCenter ? "" : isHtml ? "" : ChatColors.Silver.ToString()) - .Replace("[MAGENTA]", isCenter ? "" : isHtml ? "" : ChatColors.Magenta.ToString()); - } -} diff --git a/Utils/ColorHelper.cs b/Utils/ColorHelper.cs new file mode 100644 index 0000000..5fbcaf1 --- /dev/null +++ b/Utils/ColorHelper.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; + +namespace NeedSystem.Utils; + +public static class ColorHelper +{ + public static int ConvertHexToColor(string hex) + { + if (hex.StartsWith("#")) + { + hex = hex[1..]; + } + return int.Parse(hex, System.Globalization.NumberStyles.HexNumber); + } + + public static string StripColorCodes(string text) + { + return Regex.Replace(text, @"\{[A-Z_]+\}", ""); + } +} \ No newline at end of file diff --git a/Utils/ServerHelper.cs b/Utils/ServerHelper.cs new file mode 100644 index 0000000..b461a11 --- /dev/null +++ b/Utils/ServerHelper.cs @@ -0,0 +1,34 @@ +using CounterStrikeSharp.API.Modules.Cvars; + +namespace NeedSystem.Utils; + +public static class ServerHelper +{ + public static string GetServerAddress(bool autoDetect, string fallbackIpPort) + { + if (!autoDetect) + { + return fallbackIpPort; + } + + string? ip = ConVar.Find("ip")?.StringValue; + string? port = ConVar.Find("hostport")?.GetPrimitiveValue().ToString(); + + if (!string.IsNullOrEmpty(ip) && !string.IsNullOrEmpty(port)) + { + return $"{ip}:{port}"; + } + + return fallbackIpPort; + } + + public static string GetServerHostname(string fallback) + { + return ConVar.Find("hostname")?.StringValue ?? fallback; + } + + public static int GetMaxPlayers(bool autoDetect, int maxPlayers, int serverMaxPlayers) + { + return autoDetect ? serverMaxPlayers : maxPlayers; + } +} \ No newline at end of file diff --git a/Utils/TextHelper.cs b/Utils/TextHelper.cs new file mode 100644 index 0000000..02c2a26 --- /dev/null +++ b/Utils/TextHelper.cs @@ -0,0 +1,18 @@ +namespace NeedSystem.Utils; + +public static class TextHelper +{ + public static string CleanPlayerName(string playerName) + { + return playerName + .Replace("[Ready]", "") + .Replace("[Not Ready]", "") + .Trim(); + } + + public static string GenerateSteamLink(string playerName, ulong steamId) + { + var cleanName = CleanPlayerName(playerName); + return $"[{cleanName}](https://steamcommunity.com/profiles/{steamId})"; + } +} \ No newline at end of file