From 322fea10aabd6fe53a36e3a200041b2653c67666 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 6 Jul 2025 12:25:13 +0100 Subject: [PATCH 01/68] BREAKING: v2 start --- Directory.Build.props | 16 + DiscordLab.AdminLogs/Config.cs | 30 -- .../DiscordLab.AdminLogs.csproj | 46 --- DiscordLab.AdminLogs/FodyWeavers.xml | 12 - DiscordLab.AdminLogs/FodyWeavers.xsd | 176 ---------- DiscordLab.AdminLogs/Handlers/DiscordBot.cs | 48 --- DiscordLab.AdminLogs/Handlers/Events.cs | 44 --- DiscordLab.AdminLogs/Patches/ErrorLogger.cs | 56 ---- DiscordLab.AdminLogs/Plugin.cs | 60 ---- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.AdminLogs/Translation.cs | 13 - .../Commands/SendCommand.cs | 55 +++ DiscordLab.Administration/Config.cs | 24 ++ .../DiscordLab.Administration.csproj | 20 ++ DiscordLab.Administration/Events.cs | 110 ++++++ DiscordLab.Administration/Patches/ErrorLog.cs | 32 ++ DiscordLab.Administration/Plugin.cs | 48 +++ DiscordLab.Administration/Translation.cs | 33 ++ .../API/Features/ChannelType.cs | 12 - .../API/Features/Log.cs | 13 - .../API/Modules/EventManager.cs | 125 ------- .../API/Modules/GenerateEvent.cs | 47 --- DiscordLab.AdvancedLogging/Commands/AddLog.cs | 38 --- .../Commands/RemoveLog.cs | 50 --- DiscordLab.AdvancedLogging/Config.cs | 17 - .../DiscordLab.AdvancedLogging.csproj | 46 --- DiscordLab.AdvancedLogging/FodyWeavers.xml | 12 - DiscordLab.AdvancedLogging/FodyWeavers.xsd | 176 ---------- .../Handlers/DiscordBot.cs | 157 --------- DiscordLab.AdvancedLogging/Plugin.cs | 54 --- .../Properties/AssemblyInfo.cs | 35 -- .../API/Attributes/CallOnLoadAttribute.cs | 40 +++ .../API/Attributes/CallOnReadyAttribute.cs | 55 +++ .../API/Attributes/CallOnUnloadAttribute.cs | 40 +++ DiscordLab.Bot/API/Enums/ChannelReturn.cs | 33 -- DiscordLab.Bot/API/Enums/GuildReturn.cs | 21 -- .../API/Extensions/BitwiseExtensions.cs | 8 + .../API/Extensions/ChannelExtensions.cs | 40 --- .../API/Extensions/ClientExtensions.cs | 65 ---- .../API/Extensions/ColorExtensions.cs | 12 - .../API/Extensions/DateTimeExtensions.cs | 18 - .../API/Extensions/DiscordExtensions.cs | 23 ++ .../API/Extensions/IMessageExtensions.cs | 10 - .../API/Extensions/TranslationExtensions.cs | 144 -------- .../API/Features/DescriptionConstants.cs | 9 - DiscordLab.Bot/API/Features/Plugin.cs | 33 ++ DiscordLab.Bot/API/Features/Queue.cs | 58 ++++ DiscordLab.Bot/API/Features/SlashCommand.cs | 20 -- .../API/Features/TranslationBuilder.cs | 314 ++++++++++++++++++ DiscordLab.Bot/API/Features/UpdateStatus.cs | 18 - .../API/Interfaces/IAutocompleteCommand.cs | 13 +- DiscordLab.Bot/API/Interfaces/IDLConfig.cs | 10 - .../API/Interfaces/IRegisterable.cs | 16 - .../API/Interfaces/ISlashCommand.cs | 93 +++++- DiscordLab.Bot/API/Modules/HandlerLoader.cs | 55 --- DiscordLab.Bot/API/Modules/QueueSystem.cs | 34 -- .../API/Modules/SlashCommandLoader.cs | 62 ---- DiscordLab.Bot/API/Modules/UpdateStatus.cs | 149 --------- DiscordLab.Bot/API/Modules/WriteableConfig.cs | 46 --- DiscordLab.Bot/API/Updates/GitHubRelease.cs | 34 ++ .../API/Updates/GitHubReleaseAsset.cs | 22 ++ DiscordLab.Bot/API/Updates/Module.cs | 85 +++++ DiscordLab.Bot/API/Updates/Updater.cs | 129 +++++++ DiscordLab.Bot/API/Utilities/CommandUtils.cs | 43 +++ DiscordLab.Bot/API/Utilities/LoggingUtils.cs | 24 ++ DiscordLab.Bot/Client.cs | 180 ++++++++++ DiscordLab.Bot/Commands/Discord.cs | 116 ------- DiscordLab.Bot/Commands/DiscordCommand.cs | 116 +++++++ DiscordLab.Bot/Commands/LocalAdmin.cs | 67 ---- DiscordLab.Bot/Commands/LocalAdminCommand.cs | 69 ++++ DiscordLab.Bot/Config.cs | 43 ++- DiscordLab.Bot/DiscordLab.Bot.csproj | 52 ++- DiscordLab.Bot/FodyWeavers.xml | 14 - DiscordLab.Bot/FodyWeavers.xsd | 176 ---------- DiscordLab.Bot/Handlers/DiscordBot.cs | 137 -------- DiscordLab.Bot/Plugin.cs | 112 +++---- DiscordLab.Bot/Properties/AssemblyInfo.cs | 35 -- DiscordLab.BotStatus/Config.cs | 12 +- .../DiscordLab.BotStatus.csproj | 37 +-- DiscordLab.BotStatus/FodyWeavers.xml | 11 - DiscordLab.BotStatus/FodyWeavers.xsd | 176 ---------- DiscordLab.BotStatus/Handlers/DiscordBot.cs | 50 --- DiscordLab.BotStatus/Handlers/Events.cs | 58 ---- DiscordLab.BotStatus/Plugin.cs | 82 +++-- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.BotStatus/Translation.cs | 11 +- DiscordLab.ConnectionLogs/Config.cs | 26 +- .../DiscordLab.ConnectionLogs.csproj | 35 +- DiscordLab.ConnectionLogs/Events.cs | 85 +++++ DiscordLab.ConnectionLogs/FodyWeavers.xml | 11 - DiscordLab.ConnectionLogs/FodyWeavers.xsd | 176 ---------- .../Handlers/DiscordBot.cs | 73 ---- DiscordLab.ConnectionLogs/Handlers/Events.cs | 101 ------ DiscordLab.ConnectionLogs/Plugin.cs | 41 +-- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.ConnectionLogs/Translation.cs | 11 +- DiscordLab.DeathLogs/Config.cs | 30 +- DiscordLab.DeathLogs/DamageLogs.cs | 131 ++++++++ .../DiscordLab.DeathLogs.csproj | 35 +- DiscordLab.DeathLogs/Events.cs | 226 +++++++++++++ DiscordLab.DeathLogs/FodyWeavers.xml | 11 - DiscordLab.DeathLogs/FodyWeavers.xsd | 176 ---------- .../Handlers/DamageHandler.cs | 142 -------- DiscordLab.DeathLogs/Handlers/DiscordBot.cs | 68 ---- DiscordLab.DeathLogs/Handlers/Events.cs | 192 ----------- DiscordLab.DeathLogs/Plugin.cs | 40 +-- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.DeathLogs/Translation.cs | 22 +- DiscordLab.Moderation/Commands/Ban.cs | 104 +++--- DiscordLab.Moderation/Commands/Mute.cs | 58 ++++ DiscordLab.Moderation/Commands/SendCommand.cs | 43 --- .../Commands/TempMuteRemoteAdmin.cs | 61 ++++ DiscordLab.Moderation/Commands/Unban.cs | 79 +++-- DiscordLab.Moderation/Commands/Unmute.cs | 51 +++ DiscordLab.Moderation/Config.cs | 32 +- .../DiscordLab.Moderation.csproj | 37 +-- DiscordLab.Moderation/Events.cs | 119 +++++++ DiscordLab.Moderation/FodyWeavers.xml | 11 - DiscordLab.Moderation/FodyWeavers.xsd | 176 ---------- .../Handlers/ModerationLogsHandler.cs | 47 --- DiscordLab.Moderation/Plugin.cs | 80 +++-- .../Properties/AssemblyInfo.cs | 4 +- DiscordLab.Moderation/TempMuteConfig.cs | 7 + DiscordLab.Moderation/TempMuteManager.cs | 101 ++++++ DiscordLab.Moderation/Translation.cs | 149 +++++++-- DiscordLab.ModerationLogs/Config.cs | 72 ---- .../DiscordLab.ModerationLogs.csproj | 46 --- DiscordLab.ModerationLogs/FodyWeavers.xml | 11 - DiscordLab.ModerationLogs/FodyWeavers.xsd | 176 ---------- .../Handlers/DiscordBot.cs | 153 --------- DiscordLab.ModerationLogs/Handlers/Events.cs | 160 --------- .../Patches/RemoteAdminLogger.cs | 81 ----- DiscordLab.ModerationLogs/Plugin.cs | 65 ---- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.ModerationLogs/Translation.cs | 41 --- DiscordLab.RoundLogs/Config.cs | 56 ++-- .../DiscordLab.RoundLogs.csproj | 36 +- DiscordLab.RoundLogs/Events.cs | 185 +++++++++++ DiscordLab.RoundLogs/FodyWeavers.xml | 11 - DiscordLab.RoundLogs/FodyWeavers.xsd | 176 ---------- DiscordLab.RoundLogs/Handlers/DiscordBot.cs | 90 ----- DiscordLab.RoundLogs/Handlers/Events.cs | 119 ------- DiscordLab.RoundLogs/Plugin.cs | 42 ++- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.RoundLogs/Translation.cs | 32 +- DiscordLab.SCPSwap/Config.cs | 19 -- DiscordLab.SCPSwap/DiscordLab.SCPSwap.csproj | 45 --- DiscordLab.SCPSwap/FodyWeavers.xml | 11 - DiscordLab.SCPSwap/FodyWeavers.xsd | 176 ---------- DiscordLab.SCPSwap/Handlers/DiscordBot.cs | 30 -- DiscordLab.SCPSwap/Handlers/Events.cs | 43 --- DiscordLab.SCPSwap/Plugin.cs | 39 --- DiscordLab.SCPSwap/Properties/AssemblyInfo.cs | 35 -- DiscordLab.SCPSwap/Translation.cs | 9 - DiscordLab.StatusChannel/Command.cs | 18 + DiscordLab.StatusChannel/Config.cs | 24 +- .../DiscordLab.StatusChannel.csproj | 35 +- DiscordLab.StatusChannel/Events.cs | 114 +++++++ DiscordLab.StatusChannel/FodyWeavers.xml | 11 - DiscordLab.StatusChannel/FodyWeavers.xsd | 176 ---------- .../Handlers/DiscordBot.cs | 108 ------ DiscordLab.StatusChannel/Handlers/Events.cs | 59 ---- DiscordLab.StatusChannel/MessageConfig.cs | 10 + DiscordLab.StatusChannel/Plugin.cs | 60 ++-- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.StatusChannel/Translation.cs | 36 +- DiscordLab.XPSystem/Commands/GetLevel.cs | 58 ---- DiscordLab.XPSystem/Config.cs | 25 -- .../DiscordLab.XPSystem.csproj | 47 --- DiscordLab.XPSystem/FodyWeavers.xml | 12 - DiscordLab.XPSystem/FodyWeavers.xsd | 176 ---------- DiscordLab.XPSystem/Handlers/DiscordBot.cs | 33 -- DiscordLab.XPSystem/Handlers/Events.cs | 37 --- DiscordLab.XPSystem/Plugin.cs | 48 --- .../Properties/AssemblyInfo.cs | 35 -- DiscordLab.XPSystem/Translation.cs | 35 -- DiscordLab.sln | 104 +++--- stylecop.ruleset | 13 + 178 files changed, 3600 insertions(+), 7604 deletions(-) delete mode 100644 DiscordLab.AdminLogs/Config.cs delete mode 100644 DiscordLab.AdminLogs/DiscordLab.AdminLogs.csproj delete mode 100644 DiscordLab.AdminLogs/FodyWeavers.xml delete mode 100644 DiscordLab.AdminLogs/FodyWeavers.xsd delete mode 100644 DiscordLab.AdminLogs/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.AdminLogs/Handlers/Events.cs delete mode 100644 DiscordLab.AdminLogs/Patches/ErrorLogger.cs delete mode 100644 DiscordLab.AdminLogs/Plugin.cs delete mode 100644 DiscordLab.AdminLogs/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.AdminLogs/Translation.cs create mode 100644 DiscordLab.Administration/Commands/SendCommand.cs create mode 100644 DiscordLab.Administration/Config.cs create mode 100644 DiscordLab.Administration/DiscordLab.Administration.csproj create mode 100644 DiscordLab.Administration/Events.cs create mode 100644 DiscordLab.Administration/Patches/ErrorLog.cs create mode 100644 DiscordLab.Administration/Plugin.cs create mode 100644 DiscordLab.Administration/Translation.cs delete mode 100644 DiscordLab.AdvancedLogging/API/Features/ChannelType.cs delete mode 100644 DiscordLab.AdvancedLogging/API/Features/Log.cs delete mode 100644 DiscordLab.AdvancedLogging/API/Modules/EventManager.cs delete mode 100644 DiscordLab.AdvancedLogging/API/Modules/GenerateEvent.cs delete mode 100644 DiscordLab.AdvancedLogging/Commands/AddLog.cs delete mode 100644 DiscordLab.AdvancedLogging/Commands/RemoveLog.cs delete mode 100644 DiscordLab.AdvancedLogging/Config.cs delete mode 100644 DiscordLab.AdvancedLogging/DiscordLab.AdvancedLogging.csproj delete mode 100644 DiscordLab.AdvancedLogging/FodyWeavers.xml delete mode 100644 DiscordLab.AdvancedLogging/FodyWeavers.xsd delete mode 100644 DiscordLab.AdvancedLogging/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.AdvancedLogging/Plugin.cs delete mode 100644 DiscordLab.AdvancedLogging/Properties/AssemblyInfo.cs create mode 100644 DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs create mode 100644 DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs create mode 100644 DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs delete mode 100644 DiscordLab.Bot/API/Enums/ChannelReturn.cs delete mode 100644 DiscordLab.Bot/API/Enums/GuildReturn.cs create mode 100644 DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/ChannelExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/ClientExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/ColorExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/DateTimeExtensions.cs create mode 100644 DiscordLab.Bot/API/Extensions/DiscordExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/IMessageExtensions.cs delete mode 100644 DiscordLab.Bot/API/Extensions/TranslationExtensions.cs delete mode 100644 DiscordLab.Bot/API/Features/DescriptionConstants.cs create mode 100644 DiscordLab.Bot/API/Features/Plugin.cs create mode 100644 DiscordLab.Bot/API/Features/Queue.cs delete mode 100644 DiscordLab.Bot/API/Features/SlashCommand.cs create mode 100644 DiscordLab.Bot/API/Features/TranslationBuilder.cs delete mode 100644 DiscordLab.Bot/API/Features/UpdateStatus.cs delete mode 100644 DiscordLab.Bot/API/Interfaces/IDLConfig.cs delete mode 100644 DiscordLab.Bot/API/Interfaces/IRegisterable.cs delete mode 100644 DiscordLab.Bot/API/Modules/HandlerLoader.cs delete mode 100644 DiscordLab.Bot/API/Modules/QueueSystem.cs delete mode 100644 DiscordLab.Bot/API/Modules/SlashCommandLoader.cs delete mode 100644 DiscordLab.Bot/API/Modules/UpdateStatus.cs delete mode 100644 DiscordLab.Bot/API/Modules/WriteableConfig.cs create mode 100644 DiscordLab.Bot/API/Updates/GitHubRelease.cs create mode 100644 DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs create mode 100644 DiscordLab.Bot/API/Updates/Module.cs create mode 100644 DiscordLab.Bot/API/Updates/Updater.cs create mode 100644 DiscordLab.Bot/API/Utilities/CommandUtils.cs create mode 100644 DiscordLab.Bot/API/Utilities/LoggingUtils.cs create mode 100644 DiscordLab.Bot/Client.cs delete mode 100644 DiscordLab.Bot/Commands/Discord.cs create mode 100644 DiscordLab.Bot/Commands/DiscordCommand.cs delete mode 100644 DiscordLab.Bot/Commands/LocalAdmin.cs create mode 100644 DiscordLab.Bot/Commands/LocalAdminCommand.cs delete mode 100644 DiscordLab.Bot/FodyWeavers.xml delete mode 100644 DiscordLab.Bot/FodyWeavers.xsd delete mode 100644 DiscordLab.Bot/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.Bot/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.BotStatus/FodyWeavers.xml delete mode 100644 DiscordLab.BotStatus/FodyWeavers.xsd delete mode 100644 DiscordLab.BotStatus/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.BotStatus/Handlers/Events.cs delete mode 100644 DiscordLab.BotStatus/Properties/AssemblyInfo.cs create mode 100644 DiscordLab.ConnectionLogs/Events.cs delete mode 100644 DiscordLab.ConnectionLogs/FodyWeavers.xml delete mode 100644 DiscordLab.ConnectionLogs/FodyWeavers.xsd delete mode 100644 DiscordLab.ConnectionLogs/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.ConnectionLogs/Handlers/Events.cs delete mode 100644 DiscordLab.ConnectionLogs/Properties/AssemblyInfo.cs create mode 100644 DiscordLab.DeathLogs/DamageLogs.cs create mode 100644 DiscordLab.DeathLogs/Events.cs delete mode 100644 DiscordLab.DeathLogs/FodyWeavers.xml delete mode 100644 DiscordLab.DeathLogs/FodyWeavers.xsd delete mode 100644 DiscordLab.DeathLogs/Handlers/DamageHandler.cs delete mode 100644 DiscordLab.DeathLogs/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.DeathLogs/Handlers/Events.cs delete mode 100644 DiscordLab.DeathLogs/Properties/AssemblyInfo.cs create mode 100644 DiscordLab.Moderation/Commands/Mute.cs delete mode 100644 DiscordLab.Moderation/Commands/SendCommand.cs create mode 100644 DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs create mode 100644 DiscordLab.Moderation/Commands/Unmute.cs create mode 100644 DiscordLab.Moderation/Events.cs delete mode 100644 DiscordLab.Moderation/FodyWeavers.xml delete mode 100644 DiscordLab.Moderation/FodyWeavers.xsd delete mode 100644 DiscordLab.Moderation/Handlers/ModerationLogsHandler.cs create mode 100644 DiscordLab.Moderation/TempMuteConfig.cs create mode 100644 DiscordLab.Moderation/TempMuteManager.cs delete mode 100644 DiscordLab.ModerationLogs/Config.cs delete mode 100644 DiscordLab.ModerationLogs/DiscordLab.ModerationLogs.csproj delete mode 100644 DiscordLab.ModerationLogs/FodyWeavers.xml delete mode 100644 DiscordLab.ModerationLogs/FodyWeavers.xsd delete mode 100644 DiscordLab.ModerationLogs/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.ModerationLogs/Handlers/Events.cs delete mode 100644 DiscordLab.ModerationLogs/Patches/RemoteAdminLogger.cs delete mode 100644 DiscordLab.ModerationLogs/Plugin.cs delete mode 100644 DiscordLab.ModerationLogs/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.ModerationLogs/Translation.cs create mode 100644 DiscordLab.RoundLogs/Events.cs delete mode 100644 DiscordLab.RoundLogs/FodyWeavers.xml delete mode 100644 DiscordLab.RoundLogs/FodyWeavers.xsd delete mode 100644 DiscordLab.RoundLogs/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.RoundLogs/Handlers/Events.cs delete mode 100644 DiscordLab.RoundLogs/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.SCPSwap/Config.cs delete mode 100644 DiscordLab.SCPSwap/DiscordLab.SCPSwap.csproj delete mode 100644 DiscordLab.SCPSwap/FodyWeavers.xml delete mode 100644 DiscordLab.SCPSwap/FodyWeavers.xsd delete mode 100644 DiscordLab.SCPSwap/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.SCPSwap/Handlers/Events.cs delete mode 100644 DiscordLab.SCPSwap/Plugin.cs delete mode 100644 DiscordLab.SCPSwap/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.SCPSwap/Translation.cs create mode 100644 DiscordLab.StatusChannel/Command.cs create mode 100644 DiscordLab.StatusChannel/Events.cs delete mode 100644 DiscordLab.StatusChannel/FodyWeavers.xml delete mode 100644 DiscordLab.StatusChannel/FodyWeavers.xsd delete mode 100644 DiscordLab.StatusChannel/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.StatusChannel/Handlers/Events.cs create mode 100644 DiscordLab.StatusChannel/MessageConfig.cs delete mode 100644 DiscordLab.StatusChannel/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.XPSystem/Commands/GetLevel.cs delete mode 100644 DiscordLab.XPSystem/Config.cs delete mode 100644 DiscordLab.XPSystem/DiscordLab.XPSystem.csproj delete mode 100644 DiscordLab.XPSystem/FodyWeavers.xml delete mode 100644 DiscordLab.XPSystem/FodyWeavers.xsd delete mode 100644 DiscordLab.XPSystem/Handlers/DiscordBot.cs delete mode 100644 DiscordLab.XPSystem/Handlers/Events.cs delete mode 100644 DiscordLab.XPSystem/Plugin.cs delete mode 100644 DiscordLab.XPSystem/Properties/AssemblyInfo.cs delete mode 100644 DiscordLab.XPSystem/Translation.cs create mode 100644 stylecop.ruleset diff --git a/Directory.Build.props b/Directory.Build.props index 5d8807b..23d22aa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,4 +8,20 @@ DestinationFolder="$(SharedBinPath)" SkipUnchangedFiles="true" /> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Config.cs b/DiscordLab.AdminLogs/Config.cs deleted file mode 100644 index 4590318..0000000 --- a/DiscordLab.AdminLogs/Config.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - -namespace DiscordLab.AdminLogs -{ - public class Config : IConfig, IDLConfig - { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - - [Description("The channel where error logs will be sent.")] - public ulong ErrorLogChannelId { get; set; } = new(); - - [Description("The hex color code of the error logging embed. Do not add the #.")] - public string ErrorLogColor { get; set; } = "3498DB"; - - [Description("The channel where server start logs will be sent.")] - public ulong ServerStartChannelId { get; set; } = new(); - - [Description("The hex color code of the server start embed. Do not add the #.")] - public string ServerStartColor { get; set; } = "3498DB"; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.AdminLogs/DiscordLab.AdminLogs.csproj b/DiscordLab.AdminLogs/DiscordLab.AdminLogs.csproj deleted file mode 100644 index 4375086..0000000 --- a/DiscordLab.AdminLogs/DiscordLab.AdminLogs.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - net48 - enable - disable - preview - x64 - true - false - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.AdminLogs/FodyWeavers.xml b/DiscordLab.AdminLogs/FodyWeavers.xml deleted file mode 100644 index 445194d..0000000 --- a/DiscordLab.AdminLogs/FodyWeavers.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - Discord.Net.Websocket - Discord.Net.Core - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.AdminLogs/FodyWeavers.xsd b/DiscordLab.AdminLogs/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.AdminLogs/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Handlers/DiscordBot.cs b/DiscordLab.AdminLogs/Handlers/DiscordBot.cs deleted file mode 100644 index 5425691..0000000 --- a/DiscordLab.AdminLogs/Handlers/DiscordBot.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.AdminLogs.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel ErrorLogsChannel { get; set; } - - private SocketTextChannel ServerStartChannel { get; set; } - - private SocketGuild Guild { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - ErrorLogsChannel = null; - ServerStartChannel = null; - } - - private SocketGuild GetGuild() - { - return Guild ??= Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - } - - public SocketTextChannel GetErrorLogsChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.ErrorLogChannelId == 0) return null; - return ErrorLogsChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.ErrorLogChannelId); - } - - public SocketTextChannel GetServerStartChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.ServerStartChannelId == 0) return null; - return ServerStartChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.ServerStartChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Handlers/Events.cs b/DiscordLab.AdminLogs/Handlers/Events.cs deleted file mode 100644 index 448ac2e..0000000 --- a/DiscordLab.AdminLogs/Handlers/Events.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; - -namespace DiscordLab.AdminLogs.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Server.WaitingForPlayers += OnWaitingForPlayers; - } - - public void Unregister() - { - } - - private void OnWaitingForPlayers() - { - Exiled.Events.Handlers.Server.WaitingForPlayers -= OnWaitingForPlayers; - if(Plugin.Instance.Config.ServerStartChannelId == 0) return; - SocketTextChannel channel = DiscordBot.Instance.GetServerStartChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the server started message has failed to send."); - return; - } - - EmbedBuilder embed = new() - { - Title = Plugin.Instance.Translation.ServerStart, - }; - - if (Plugin.Instance.Translation.ServerStartDescription != null) - { - embed.Description = Plugin.Instance.Translation.ServerStartDescription.LowercaseParams().StaticReplace(); - } - - channel.SendMessageAsync(embed: embed.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Patches/ErrorLogger.cs b/DiscordLab.AdminLogs/Patches/ErrorLogger.cs deleted file mode 100644 index a9c4768..0000000 --- a/DiscordLab.AdminLogs/Patches/ErrorLogger.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Reflection.Emit; -using Discord; -using Discord.WebSocket; -using DiscordLab.AdminLogs.Handlers; -using Exiled.API.Features; -using HarmonyLib; -using NorthwoodLib.Pools; -using static HarmonyLib.AccessTools; - -namespace DiscordLab.AdminLogs.Patches -{ - [HarmonyPatch(typeof(Log), nameof(Log.Error), typeof(object))] - [HarmonyPatch(typeof(Log), nameof(Log.Error), typeof(string))] - public class ErrorLogger - { - public static IEnumerable Transpiler(IEnumerable instructions) - { - List newInstructions = ListPool.Shared.Rent(instructions); - - int offset = -2; - int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Call) + offset; - - newInstructions.InsertRange(index, new CodeInstruction[] - { - new (OpCodes.Dup), - new (OpCodes.Call, Method(typeof(ErrorLogger), nameof(LogError))), - }); - - for (int z = 0; z < newInstructions.Count; z++) - yield return newInstructions[z]; - - ListPool.Shared.Return(newInstructions); - } - - public static void LogError(string message) - { - try - { - SocketTextChannel channel = DiscordBot.Instance.GetErrorLogsChannel(); - if (channel == null) return; - - EmbedBuilder embed = new() - { - Title = Plugin.Instance.Translation.Error, - Description = message - }; - - channel.SendMessageAsync(embed: embed.Build()); - } - catch - { - // ignored - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Plugin.cs b/DiscordLab.AdminLogs/Plugin.cs deleted file mode 100644 index 67e29bb..0000000 --- a/DiscordLab.AdminLogs/Plugin.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Globalization; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using HarmonyLib; - -namespace DiscordLab.AdminLogs -{ - public class Plugin : Plugin - { - public override string Name => "DiscordLab.AdminLogs"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.AdminLogs"; - public override Version Version => new (1, 0, 2); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - - private Harmony harmony; - - public override void OnEnabled() - { - Instance = this; - - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - try - { - harmony = new($"discordlab.adminlogs.{DateTime.Now.Ticks}"); - harmony.PatchAll(); - } - catch (Exception e) - { - Log.Error($"An error occurred while patching: {e}"); - } - - base.OnEnabled(); - } - - public override void OnDisabled() - { - _handlerLoader.Unload(); - _handlerLoader = null; - - harmony?.UnpatchAll(harmony.Id); - harmony = null; - - base.OnDisabled(); - } - - public static uint GetColor(string color) - { - return uint.Parse(color, NumberStyles.HexNumber); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Properties/AssemblyInfo.cs b/DiscordLab.AdminLogs/Properties/AssemblyInfo.cs deleted file mode 100644 index a671b58..0000000 --- a/DiscordLab.AdminLogs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.AdminLogs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.AdminLogs")] -[assembly: AssemblyCopyright("Copyright © 2025")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6BC3940C-7AD6-491C-BDAD-7B8545E643E2")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.AdminLogs/Translation.cs b/DiscordLab.AdminLogs/Translation.cs deleted file mode 100644 index ce7fdc3..0000000 --- a/DiscordLab.AdminLogs/Translation.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Exiled.API.Interfaces; - -namespace DiscordLab.AdminLogs -{ - public class Translation : ITranslation - { - public string Error { get; set; } = "Error on the server"; - - public string ServerStart { get; set; } = "Server has started"; - - public string ServerStartDescription { get; set; } = "The server has started and players can now connect.\nStarted {timer}"; - } -} \ No newline at end of file diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs new file mode 100644 index 0000000..bf5bd5f --- /dev/null +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -0,0 +1,55 @@ +using CommandSystem; +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using LabApi.Features.Wrappers; +using RemoteAdmin; + +namespace DiscordLab.Administration.Commands +{ + public class SendCommand : IAutocompleteCommand + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public SlashCommandBuilder Data + { + get + { + SlashCommandBuilder builder = Translation.SendCommand; + SlashCommandOptionBuilder option = builder.Options.First(); + option.IsAutocomplete = true; + option.Type = ApplicationCommandOptionType.String; + option.IsRequired = true; + + return builder; + } + } + + public ulong GuildId { get; } = Config.GuildId; + + public async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); + + string response = Server.RunCommand((string)command.Data.Options.First().Value); + + TranslationBuilder builder = new(Translation.SendCommandResponse); + builder.CustomReplacers.Add("response", () => response); + + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + } + + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + IEnumerable commands = + [ + ..CommandProcessor.GetAllCommands().Select(x => "/" + x.Command), + ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command) + ]; + await autocomplete.RespondAsync(commands.Select(x => new AutocompleteResult(x, x))); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Administration/Config.cs b/DiscordLab.Administration/Config.cs new file mode 100644 index 0000000..81435d5 --- /dev/null +++ b/DiscordLab.Administration/Config.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace DiscordLab.Administration +{ + public class Config + { + public ulong GuildId { get; set; } = 0; + + [Description("The channel to send server start logs")] + public ulong ServerStartChannelId { get; set; } = 0; + + [Description("The channel to send error logs")] + public ulong ErrorLogChannelId { get; set; } = 0; + + [Description("The channel to send remote admin logs")] + public ulong RemoteAdminChannelId { get; set; } = 0; + + [Description("The channel to send normal command logs")] + public ulong CommandLogChannelId { get; set; } = 0; + + [Description("Whether to add the commands to the bot. Is false then commands won't be used.")] + public bool AddCommands { get; set; } = true; + } +} \ No newline at end of file diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj new file mode 100644 index 0000000..117357f --- /dev/null +++ b/DiscordLab.Administration/DiscordLab.Administration.csproj @@ -0,0 +1,20 @@ + + + net48 + enable + disable + 12 + x64 + true + false + 2.0.0 + + + + + + + + + + \ No newline at end of file diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs new file mode 100644 index 0000000..d998bcf --- /dev/null +++ b/DiscordLab.Administration/Events.cs @@ -0,0 +1,110 @@ +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events; +using LabApi.Events.Arguments.ServerEvents; +using LabApi.Events.CustomHandlers; +using LabApi.Events.Handlers; +using LabApi.Features.Console; +using LabApi.Features.Enums; +using LabApi.Features.Wrappers; + +namespace DiscordLab.Administration +{ + public class Events : CustomEventsHandler + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + private static bool IsSubscribed { get; set; } + + [CallOnLoad] + public static void Load() + { + ServerEvents.WaitingForPlayers += OnServerStart; + IsSubscribed = true; + } + + [CallOnUnload] + public static void Unload() + { + if (!IsSubscribed) return; + ServerEvents.WaitingForPlayers -= OnServerStart; + IsSubscribed = false; + } + + public static void OnServerStart() + { + ServerEvents.WaitingForPlayers -= OnServerStart; + IsSubscribed = false; + + if (Config.ServerStartChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.ServerStartChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId, Config.GuildId)); + return; + } + + channel.SendMessage(Translation.ServerStart); + } + + public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) + { + if (ev.Sender == null || !Player.TryGet(ev.Sender, out Player player)) + return; + + SocketTextChannel channel; + TranslationBuilder builder; + + Dictionary> customReplacers = new() + { + ["type"] = () => ev.CommandType.ToString(), + ["arguments"] = () => string.Join(" ", ev.Arguments), + ["command"] = () => ev.Command.Command, + ["commanddescription"] = () => ev.Command.Description, + }; + + if (ev.CommandType == CommandType.RemoteAdmin) + { + if (Config.RemoteAdminChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", Config.RemoteAdminChannelId, Config.GuildId)); + return; + } + + builder = new(Translation.RemoteAdmin, "player", player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); + return; + } + + if (Config.CommandLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.CommandLogChannelId, out channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId, Config.GuildId)); + return; + } + + builder = new(Translation.CommandLog, "player", player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Administration/Patches/ErrorLog.cs b/DiscordLab.Administration/Patches/ErrorLog.cs new file mode 100644 index 0000000..0c272da --- /dev/null +++ b/DiscordLab.Administration/Patches/ErrorLog.cs @@ -0,0 +1,32 @@ +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using HarmonyLib; +using LabApi.Features.Console; + +namespace DiscordLab.Administration.Patches +{ + [HarmonyPatch(typeof(Logger), nameof(Logger.Error))] + public static class ErrorLog + { + public static void Postfix(object message) + { + if (Plugin.Instance.Config.ErrorLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.ErrorLogChannelId, out SocketTextChannel channel)) + { + Logger.Raw( + $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}", ConsoleColor.Red); + return; + } + + TranslationBuilder builder = new(Plugin.Instance.Translation.ErrorLog); + builder.CustomReplacers.Add("error", message.ToString); + + channel.SendMessage(builder); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs new file mode 100644 index 0000000..1dcdcb4 --- /dev/null +++ b/DiscordLab.Administration/Plugin.cs @@ -0,0 +1,48 @@ +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using HarmonyLib; +using LabApi.Events.CustomHandlers; +using LabApi.Features; + +namespace DiscordLab.Administration +{ + public class Plugin : Plugin + { + public static Plugin Instance; + + public override string Name { get; } = "DiscordLab.Administration"; + + public override string Description { get; } = + "Allows you to log or do administrative actions from your Discord bot"; + + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + + public Events Events = new(); + + private Harmony harmony = new($"DiscordLab.Administration-{DateTime.Now.Ticks}"); + + public override void Enable() + { + Instance = this; + harmony.PatchAll(); + CallOnLoadAttribute.Load(); + + if(Config.AddCommands) + ISlashCommand.FindAll(); + + CustomHandlersManager.RegisterEventsHandler(Events); + } + + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); + CallOnUnloadAttribute.Unload(); + harmony.UnpatchAll(); + Events = null; + Instance = null; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs new file mode 100644 index 0000000..f518a4f --- /dev/null +++ b/DiscordLab.Administration/Translation.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using Discord; + +namespace DiscordLab.Administration +{ + public class Translation + { + public string ServerStart { get; set; } = "Server has started"; + + public string ErrorLog { get; set; } = "An error has occured:\n{error}"; + + public string RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; + + public string CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; + + public SlashCommandBuilder SendCommand { get; set; } = new() + { + Name = "send", + Description = "Sends a command to the server", + DefaultMemberPermissions = GuildPermission.Administrator, + Options = + [ + new() + { + Name = "command", + Description = "The command to send" + } + ] + }; + + public string SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; + } +} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/API/Features/ChannelType.cs b/DiscordLab.AdvancedLogging/API/Features/ChannelType.cs deleted file mode 100644 index d23d076..0000000 --- a/DiscordLab.AdvancedLogging/API/Features/ChannelType.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Discord.WebSocket; - -namespace DiscordLab.AdvancedLogging.API.Features -{ - public class ChannelType - { - public string Handler { get; set; } - public string Event { get; set; } - public SocketTextChannel Channel { get; set; } - public ulong ChannelId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/API/Features/Log.cs b/DiscordLab.AdvancedLogging/API/Features/Log.cs deleted file mode 100644 index de4e225..0000000 --- a/DiscordLab.AdvancedLogging/API/Features/Log.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JetBrains.Annotations; - -namespace DiscordLab.AdvancedLogging.API.Features -{ - public class Log - { - public string Handler { get; set; } - public string Event { get; set; } - public string Content { get; set; } - [CanBeNull] public string Nullables { get; set; } - public ulong ChannelId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/API/Modules/EventManager.cs b/DiscordLab.AdvancedLogging/API/Modules/EventManager.cs deleted file mode 100644 index 558897d..0000000 --- a/DiscordLab.AdvancedLogging/API/Modules/EventManager.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using DiscordLab.AdvancedLogging.Handlers; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Interfaces; -using Exiled.Events.Features; -using Exiled.Loader; - -namespace DiscordLab.AdvancedLogging.API.Modules -{ - public static class EventManager - { - // ReSharper disable once UseCollectionExpression - // ReSharper disable once MemberCanBePrivate.Global - public static Type[] HandlerTypes = new Type[0]; - // ReSharper disable once UseCollectionExpression - private static readonly List> DynamicHandlers = new (); - - internal static void GetHandlers() - { - HandlerTypes = Loader.Plugins.First(plug => plug.Name == "Exiled.Events") - .Assembly.GetTypes() - .Where(t => t.FullName?.Equals($"Exiled.Events.Handlers.{t.Name}") is true).ToArray(); - } - - internal static void AddEventHandler(string handler, string @event) - { - Type handlerType = HandlerTypes.FirstOrDefault(h => h.Name == handler); - if (handlerType == null) - { - Log.Error($"Handler {handler} not found"); - return; - } - - Delegate @delegate; - PropertyInfo propertyInfo = handlerType.GetProperty(@event); - - if (propertyInfo == null) - { - Log.Error($"Failed to find {@event} under {handler}"); - return; - } - - EventInfo eventInfo = propertyInfo.PropertyType.GetEvent("InnerEvent", (BindingFlags)(-1)); - if (eventInfo == null) - { - Log.Error($"Failed to bind {handler}.{@event}"); - return; - } - MethodInfo subscribe = propertyInfo.PropertyType.GetMethods().First(x => x.Name is "Subscribe"); - - if (propertyInfo.PropertyType == typeof(Event)) - { - @delegate = new CustomEventHandler(EventNoArgs); - } - else if (propertyInfo.PropertyType.IsGenericType && propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Event<>)) - { - @delegate = typeof(EventManager) - .GetMethod(nameof(Event))? - .MakeGenericMethod(eventInfo.EventHandlerType.GenericTypeArguments) - .CreateDelegate(typeof(CustomEventHandler<>) - .MakeGenericType(eventInfo.EventHandlerType.GenericTypeArguments)); - } - else - { - Log.Error($"Failed to bind {handler}.{@event}"); - return; - } - - // ReSharper disable once UseCollectionExpression - subscribe.Invoke(propertyInfo.GetValue(null), new object[] { @delegate }); - DynamicHandlers.Add(new (propertyInfo, @delegate)); - } - - internal static void RemoveEventHandlers() - { - for (int i = 0; i < DynamicHandlers.Count; i++) - { - Tuple tuple = DynamicHandlers[i]; - PropertyInfo propertyInfo = tuple.Item1; - Delegate handler = tuple.Item2; - - MethodInfo unSubscribe = propertyInfo.PropertyType.GetMethods().First(x => x.Name is "Unsubscribe"); - - // ReSharper disable once CoVariantArrayConversion - // ReSharper disable once UseCollectionExpression - unSubscribe.Invoke(propertyInfo.GetValue(null), new[] { handler }); - DynamicHandlers.Remove(tuple); - } - } - - // ReSharper disable once MemberCanBePrivate.Global - public static void EventNoArgs() - { - StackFrame frame = new(3); - MethodBase method = frame.GetMethod(); - if (method == null) return; - - string eventName = method.Name.Replace("On", ""); - string handlerName = method.DeclaringType?.Name; - IEnumerable logs = DiscordBot.Instance.GetLogs(); - Features.Log log = logs.FirstOrDefault(x => x.Handler == handlerName && x.Event == eventName); - if (log == null) return; - - Log.Debug("Event triggered, routing to " + log.ChannelId); - - GenerateEvent.Event(null, DiscordBot.Instance.GetChannel(log.ChannelId), log.Content, Array.Empty()); - } - - public static void Event(T ev) where T : IExiledEvent - { - string typePath = typeof(T).FullName; - string[] parts = typePath!.Split('.'); - string handler = parts[parts.Length - 2]; - string @event = parts[parts.Length - 1].Replace("EventArgs", ""); - IEnumerable logs = DiscordBot.Instance.GetLogs(); - API.Features.Log log = logs.FirstOrDefault(x => x.Handler == handler && x.Event == @event); - if (log == null) return; - - Log.Debug($"{handler}.{@event} triggered, routing to {log.ChannelId}"); - - GenerateEvent.Event(ev, DiscordBot.Instance.GetChannel(log.ChannelId), log.Content, (log.Nullables ?? "").Split(',')); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/API/Modules/GenerateEvent.cs b/DiscordLab.AdvancedLogging/API/Modules/GenerateEvent.cs deleted file mode 100644 index d22149a..0000000 --- a/DiscordLab.AdvancedLogging/API/Modules/GenerateEvent.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Reflection; -using System.Text.RegularExpressions; -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using Exiled.API.Features; - -namespace DiscordLab.AdvancedLogging.API.Modules -{ - public static class GenerateEvent - { - public static void Event(object ev, SocketTextChannel channel, string content, IEnumerable nullables) - { - List nulls = nullables.ToList(); - - Regex regex = new(@"\{([^\}]+)\}"); - MatchCollection matches = regex.Matches(content); - - foreach (Match match in matches) - { - string placeholder = match.Value; - string propertyPath = match.Groups[1].Value; - - string[] properties = propertyPath.Split('.'); - - object currentObject = ev; - foreach (string property in properties) - { - if (currentObject == null) break; - PropertyInfo propertyInfo = currentObject.GetType().GetProperty(property); - if (propertyInfo == null && nulls.Contains(propertyPath)) - { - return; - } - - currentObject = propertyInfo?.GetValue(currentObject); - } - - if (currentObject != null) - { - content = content.Replace(placeholder, currentObject.ToString()); - } - } - - channel.SendMessageAsync(content.LowercaseParams().StaticReplace()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Commands/AddLog.cs b/DiscordLab.AdvancedLogging/Commands/AddLog.cs deleted file mode 100644 index 6d0e21a..0000000 --- a/DiscordLab.AdvancedLogging/Commands/AddLog.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.AdvancedLogging.Commands -{ - public class AddLog : ISlashCommand - { - public SlashCommandBuilder Data { get; } = new() - { - Name = "addlog", - Description = "Add your own custom logger, check the documentation for more info", - DefaultMemberPermissions = GuildPermission.ManageGuild - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; - - public async Task Run(SocketSlashCommand command) - { - ModalBuilder modal = new() - { - Title = "Create a new custom log", - CustomId = "addlogmodal" - }; - modal.AddTextInput("Handler, i.e. 'Player' for Handlers.Player", "handler", TextInputStyle.Short, "Player", - null, null, true); - modal.AddTextInput("Event, i.e. 'Died' for Player.Died", "event", TextInputStyle.Short, "Died", null, null, - true); - modal.AddTextInput("Message, i.e. 'Player {Player.Nickname} died'", "message", TextInputStyle.Paragraph, - "Player {Player.Nickname} died", null, null, true); - modal.AddTextInput("Nulls, do nothing when null, comma separated", "nullables", TextInputStyle.Paragraph, - "Attacker", null, null, false); - modal.AddTextInput("Channel, use channel ID", "channel", TextInputStyle.Short, "", null, null, true); - - await command.RespondWithModalAsync(modal.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Commands/RemoveLog.cs b/DiscordLab.AdvancedLogging/Commands/RemoveLog.cs deleted file mode 100644 index 260343a..0000000 --- a/DiscordLab.AdvancedLogging/Commands/RemoveLog.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.AdvancedLogging.API.Features; -using DiscordLab.AdvancedLogging.Handlers; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Newtonsoft.Json.Linq; - -namespace DiscordLab.AdvancedLogging.Commands -{ - public class RemoveLog : ISlashCommand - { - public SlashCommandBuilder Data { get; } = new() - { - Name = "removelog", - Description = "Removes a log from the list of logs.", - DefaultMemberPermissions = GuildPermission.ManageGuild, - Options = new() - { - new() - { - Name = "log", - Description = "e.g. Player.Died", - Type = ApplicationCommandOptionType.String - } - } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; - - public async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(true); - List logs = DiscordBot.Instance.GetLogs().ToList(); - string log = command.Data.Options.First().Value.ToString(); - Log logToRemove = logs.FirstOrDefault(l => l.Handler == log.Split('.')[0] && l.Event == log.Split('.')[1]); - if (logToRemove == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Log not found."); - return; - } - - logs.Remove(logToRemove); - - WriteableConfig.WriteConfigOption("AdvancedLogging", JArray.FromObject(logs)); - - await command.ModifyOriginalResponseAsync(m => m.Content = "Log removed."); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Config.cs b/DiscordLab.AdvancedLogging/Config.cs deleted file mode 100644 index 235da53..0000000 --- a/DiscordLab.AdvancedLogging/Config.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - -namespace DiscordLab.AdvancedLogging -{ - public class Config : IConfig, IDLConfig - { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/DiscordLab.AdvancedLogging.csproj b/DiscordLab.AdvancedLogging/DiscordLab.AdvancedLogging.csproj deleted file mode 100644 index ca04eeb..0000000 --- a/DiscordLab.AdvancedLogging/DiscordLab.AdvancedLogging.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - net48 - enable - disable - preview - x64 - true - false - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/FodyWeavers.xml b/DiscordLab.AdvancedLogging/FodyWeavers.xml deleted file mode 100644 index 445194d..0000000 --- a/DiscordLab.AdvancedLogging/FodyWeavers.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - Discord.Net.Websocket - Discord.Net.Core - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/FodyWeavers.xsd b/DiscordLab.AdvancedLogging/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.AdvancedLogging/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Handlers/DiscordBot.cs b/DiscordLab.AdvancedLogging/Handlers/DiscordBot.cs deleted file mode 100644 index 5f996a2..0000000 --- a/DiscordLab.AdvancedLogging/Handlers/DiscordBot.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using Discord.WebSocket; -using DiscordLab.AdvancedLogging.API.Features; -using DiscordLab.AdvancedLogging.API.Modules; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Interfaces; -using Exiled.Events.EventArgs.Interfaces; -using Exiled.Events.Features; -using MEC; -using Newtonsoft.Json.Linq; -using Utf8Json.Internal; -using Log = Exiled.API.Features.Log; - -namespace DiscordLab.AdvancedLogging.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private List Channels { get; set; } - - public void Init() - { - Instance = this; - Channels = new(); - Bot.Handlers.DiscordBot.Instance.Client.ModalSubmitted += OnModalSubmitted; - Bot.Handlers.DiscordBot.Instance.Client.Ready += OnReady; - } - - public void Unregister() - { - Channels = null; - } - - internal SocketTextChannel GetChannel(ulong channelId) - { - SocketGuild guild = Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - if (guild == null) return null; - if (Channels.Exists(c => c.ChannelId == channelId)) - return Channels.First(c => c.ChannelId == channelId).Channel; - SocketTextChannel channel = guild.GetTextChannel(channelId); - if (channel != null) return channel; - Log.Error("Either the guild is null or the channel is null."); - return null; - } - - private void GetChannelAndBind(string handler, string @event, ulong channelId) - { - Log.Debug($"Getting channel {channelId} from {handler}.{@event}"); - SocketTextChannel channel = GetChannel(channelId); - Channels.Add(new() - { - Handler = handler, - Event = @event, - Channel = channel, - ChannelId = channelId - }); - } - - private async Task OnReady() - { - IEnumerable logList = GetLogs(); - foreach (API.Features.Log log in logList) - { - GetChannelAndBind(log.Handler, log.Event, log.ChannelId); - } - - await Task.CompletedTask; - } - - public IEnumerable GetLogs() - { - JToken logs = WriteableConfig.GetConfig()["AdvancedLogging"]; - if (logs == null) - { - WriteableConfig.WriteConfigOption("AdvancedLogging", new JArray()); - return new List(); - } - - return logs.ToObject>() ?? new List(); - } - - private bool EventExists(string handler, string @event) - { - IPlugin eventsAssembly = - Exiled.Loader.Loader.Plugins.FirstOrDefault(x => x.Name == "Exiled.Events"); - if (eventsAssembly == null) return false; - Type eventType = eventsAssembly.Assembly.GetTypes() - .FirstOrDefault(x => x.Namespace == "Exiled.Events.Handlers" && x.Name == handler); - if (eventType == null) return false; - PropertyInfo propertyInfo = eventType.GetProperty(@event); - return propertyInfo != null; - } - - private async Task OnModalSubmitted(SocketModal modal) - { - if (modal.Data.CustomId != "addlogmodal") return; - List components = modal.Data.Components.ToList(); - string handler = components.First(x => x.CustomId == "handler").Value; - string @event = components.First(x => x.CustomId == "event").Value; - string message = components.First(x => x.CustomId == "message").Value; - string nullables = components.First(x => x.CustomId == "nullables").Value; - string channelIdString = components.First(x => x.CustomId == "channel").Value; - if (!ulong.TryParse(channelIdString, out ulong channelId)) - { - await modal.RespondAsync("Invalid channel ID", null, false, true); - return; - } - - SocketTextChannel channel = GetChannel(channelId); - if (channel == null) - { - await modal.RespondAsync( - "Either the guild is null or the channel is null. So I couldn't find the channel you linked.", - ephemeral: true); - } - - Channels.Add(new() - { - Handler = handler, - Event = @event, - Channel = channel, - ChannelId = channelId - }); - JToken logs = WriteableConfig.GetConfig()["AdvancedLogging"]; - if (logs == null) - { - WriteableConfig.WriteConfigOption("AdvancedLogging", new JArray()); - logs = WriteableConfig.GetConfig()["AdvancedLogging"]!; - } - - JArray logList = logs.ToObject() ?? new(); - API.Features.Log log = new() - { - Handler = handler, - Event = @event, - Content = message, - Nullables = nullables ?? "", - ChannelId = channelId - }; - bool eventResponse = EventExists(log.Handler, log.Event); - if (eventResponse) - { - logList.Add(JObject.FromObject(log)); - WriteableConfig.WriteConfigOption("AdvancedLogging", logList); - EventManager.AddEventHandler(log.Handler, log.Event); - await modal.RespondAsync("Log added", ephemeral: true); - } - else - { - await modal.RespondAsync("Failed to add log, check server console for more info", ephemeral: true); - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Plugin.cs b/DiscordLab.AdvancedLogging/Plugin.cs deleted file mode 100644 index 30f2d2f..0000000 --- a/DiscordLab.AdvancedLogging/Plugin.cs +++ /dev/null @@ -1,54 +0,0 @@ -using DiscordLab.AdvancedLogging.API.Modules; -using DiscordLab.AdvancedLogging.Handlers; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using UnityEngine; -using Log = DiscordLab.AdvancedLogging.API.Features.Log; - -namespace DiscordLab.AdvancedLogging -{ - public class Plugin : Plugin - { - public override string Name => "DiscordLab.AdvancedLogging"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.AdvancedLogging"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Low; - - public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - - public override void OnEnabled() - { - Instance = this; - - _handlerLoader = new (); - - if(!_handlerLoader.Load(Assembly)) return; - - EventManager.GetHandlers(); - - foreach (Log log in DiscordBot.Instance.GetLogs()) - { - Exiled.API.Features.Log.Debug($"Adding event handler for {log.Handler}.{log.Event}"); - EventManager.AddEventHandler(log.Handler, log.Event); - } - - base.OnEnabled(); - } - - public override void OnDisabled() - { - _handlerLoader.Unload(); - - _handlerLoader = null; - - EventManager.RemoveEventHandlers(); - - base.OnDisabled(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.AdvancedLogging/Properties/AssemblyInfo.cs b/DiscordLab.AdvancedLogging/Properties/AssemblyInfo.cs deleted file mode 100644 index b0d2537..0000000 --- a/DiscordLab.AdvancedLogging/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.AdvancedLogging")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.AdvancedLogging")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("43B91E66-9585-46C0-86AB-0DE55EF8D141")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs new file mode 100644 index 0000000..a9a483e --- /dev/null +++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs @@ -0,0 +1,40 @@ +namespace DiscordLab.Bot.API.Attributes +{ + using System.Reflection; + using LabApi.Features.Console; + + /// + /// An attribute that when used on a method, will trigger whenever your plugin is loaded. Requires you to run . + /// + [AttributeUsage(AttributeTargets.Method)] + public class CallOnLoadAttribute : Attribute + { + /// + /// Find all attributes in your plugin and calls them. + /// + /// The assembly you wish to check, defaults to the current one. + public static void Load(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + foreach (Type type in assembly.GetTypes()) + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + CallOnLoadAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; + + try + { + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + } + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs new file mode 100644 index 0000000..bbacab0 --- /dev/null +++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs @@ -0,0 +1,55 @@ +namespace DiscordLab.Bot.API.Attributes +{ + using System.Reflection; + using LabApi.Features.Console; + + /// + /// An attribute that when used on a method, will trigger whenever the is ready. + /// + [AttributeUsage(AttributeTargets.Method)] + public class CallOnReadyAttribute : Attribute + { + private static List instances = []; + + /// + /// Locates all 's in your plugin and prepares them to be called. + /// + /// The assembly you wish to check, defaults to the current one. + public static void Load(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + foreach (Type type in assembly.GetTypes()) + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + CallOnReadyAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; + + instances.Add(method); + } + } + } + + /// + /// Called whenever the bot is ready. + /// + internal static void Ready() + { + foreach (MethodInfo method in instances) + { + try + { + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + instances = null; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs new file mode 100644 index 0000000..8113549 --- /dev/null +++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs @@ -0,0 +1,40 @@ +namespace DiscordLab.Bot.API.Attributes +{ + using System.Reflection; + using LabApi.Features.Console; + + /// + /// An attribute that when used on a method, will trigger whenever your plugin is unloaded. Requires you to run . + /// + [AttributeUsage(AttributeTargets.Method)] + public class CallOnUnloadAttribute : Attribute + { + /// + /// Find all attributes in your plugin and calls them. + /// + /// The assembly you wish to check, defaults to the current one. + public static void Unload(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + foreach (Type type in assembly.GetTypes()) + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + { + CallOnUnloadAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; + + try + { + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + } + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Enums/ChannelReturn.cs b/DiscordLab.Bot/API/Enums/ChannelReturn.cs deleted file mode 100644 index 54cb000..0000000 --- a/DiscordLab.Bot/API/Enums/ChannelReturn.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DiscordLab.Bot.API.Enums -{ - /// - /// Used to determine if a channel was found or not, and if not, the reason why it wasn't found. - /// - public enum ChannelReturn - { - /// - /// The guild and channel were found. - /// - Found = 0, - /// - /// Couldn't find the guild with the specified ID. - /// - InvalidGuild = 1, - /// - /// The guild ID was 0. - /// - NoGuild = 2, - /// - /// Couldn't find the channel with the specified ID. - /// - InvalidChannel = 3, - /// - /// The channel ID was 0. - /// - NoChannel = 4, - /// - /// The channel type was not the requested type. - /// - InvalidType = 5 - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Enums/GuildReturn.cs b/DiscordLab.Bot/API/Enums/GuildReturn.cs deleted file mode 100644 index d903857..0000000 --- a/DiscordLab.Bot/API/Enums/GuildReturn.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace DiscordLab.Bot.API.Enums -{ - /// - /// Used to determine if a guild was found or not, and if not, the reason why it wasn't found. - /// - public enum GuildReturn - { - /// - /// The guild was found. - /// - Found = 0, - /// - /// The guild ID was invalid. - /// - InvalidGuild = 1, - /// - /// The guild ID was 0. - /// - NoGuild = 2 - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs new file mode 100644 index 0000000..1e8a4a9 --- /dev/null +++ b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs @@ -0,0 +1,8 @@ +namespace DiscordLab.Bot.API.Extensions +{ + public static class BitwiseExtensions + { + public static IEnumerable GetFlags(this T flags) + where T : Enum => Enum.GetValues(typeof(T)).Cast().Where(x => flags.HasFlag(x)); + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/ChannelExtensions.cs b/DiscordLab.Bot/API/Extensions/ChannelExtensions.cs deleted file mode 100644 index e7bd30c..0000000 --- a/DiscordLab.Bot/API/Extensions/ChannelExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Discord; -using Discord.WebSocket; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class ChannelExtensions - { - public static void SendMessageSync(this SocketTextChannel channel, - string text = null, - // ReSharper disable once InconsistentNaming - bool isTTS = false, - Embed embed = null, - RequestOptions options = null, - AllowedMentions allowedMentions = null, - MessageReference messageReference = null, - MessageComponent components = null, - ISticker[] stickers = null, - Embed[] embeds = null, - MessageFlags flags = MessageFlags.None, - PollProperties poll = null - ) - { - Task.Run(async () => - await channel.SendMessageAsync( - text, - isTTS, - embed, - options, - allowedMentions, - messageReference, - components, - stickers, - embeds, - flags, - poll - ) - ); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/ClientExtensions.cs b/DiscordLab.Bot/API/Extensions/ClientExtensions.cs deleted file mode 100644 index aee904a..0000000 --- a/DiscordLab.Bot/API/Extensions/ClientExtensions.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Enums; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class ClientExtensions - { - public static GuildReturn TryGetGuild(this DiscordSocketClient client, ulong guildId, out SocketGuild guild) - { - if (guildId == 0) - { - guild = null; - return GuildReturn.NoGuild; - } - SocketGuild tempGuild = client.GetGuild(guildId); - if (tempGuild == null) - { - guild = null; - return GuildReturn.InvalidGuild; - } - guild = tempGuild; - return GuildReturn.Found; - } - - public static ChannelReturn TryGetTextChannel(this DiscordSocketClient client, ulong channelId, out SocketTextChannel channel) - { - if (channelId == 0) - { - channel = null; - return ChannelReturn.NoChannel; - } - SocketChannel tempChannel = client.GetChannel(channelId); - if (tempChannel == null) - { - channel = null; - return ChannelReturn.InvalidChannel; - } - if(tempChannel is not SocketTextChannel textChannel) - { - channel = null; - return ChannelReturn.InvalidType; - } - channel = textChannel; - return ChannelReturn.Found; - } - - public static ChannelReturn TryGetTextChannel(this SocketGuild guild, ulong channelId, - out SocketTextChannel channel) - { - if(channelId == 0) - { - channel = null; - return ChannelReturn.NoChannel; - } - SocketTextChannel tempChannel = guild.GetTextChannel(channelId); - if (tempChannel == null) - { - channel = null; - return ChannelReturn.InvalidChannel; - } - channel = tempChannel; - return ChannelReturn.Found; - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/ColorExtensions.cs b/DiscordLab.Bot/API/Extensions/ColorExtensions.cs deleted file mode 100644 index bb72dda..0000000 --- a/DiscordLab.Bot/API/Extensions/ColorExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Globalization; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class ColorExtensions - { - public static uint GetColor(this string color) - { - return uint.Parse(color, NumberStyles.HexNumber); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/DateTimeExtensions.cs b/DiscordLab.Bot/API/Extensions/DateTimeExtensions.cs deleted file mode 100644 index 19c2c28..0000000 --- a/DiscordLab.Bot/API/Extensions/DateTimeExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JetBrains.Annotations; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class DateTimeExtensions - { - public static string ToDiscordUnixTimestamp(this DateTime dateTime, string suffix = "") - { - if (suffix != "") return $""; - return $""; - } - - public static long ToUnixTimestamp(this DateTime dateTime) - { - return new DateTimeOffset(dateTime).ToUnixTimeSeconds(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs new file mode 100644 index 0000000..c6b8eeb --- /dev/null +++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs @@ -0,0 +1,23 @@ +namespace DiscordLab.Bot.API.Extensions +{ + using Discord; + using Discord.WebSocket; + + /// + /// Extension methods to help with Discord based tasks. + /// + public static class DiscordExtensions + { + /// + /// Runs a task that sends a message to the specified channel. + /// + /// The channel to send the message to. + /// The text. + /// Whether the message is TTS. + /// The embed. + /// The embeds. + /// Text, embed or embeds is required here. + public static void SendMessage(this SocketTextChannel channel, string text = null, bool isTts = false, Embed embed = null, Embed[] embeds = null) => + Task.Run(async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false)); + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/IMessageExtensions.cs b/DiscordLab.Bot/API/Extensions/IMessageExtensions.cs deleted file mode 100644 index c05c0f2..0000000 --- a/DiscordLab.Bot/API/Extensions/IMessageExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Discord; -using Discord.WebSocket; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class IMessageExtensions - { - public static bool IsUserMessage(this IMessage message) => message is SocketUserMessage; - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/TranslationExtensions.cs b/DiscordLab.Bot/API/Extensions/TranslationExtensions.cs deleted file mode 100644 index e314c04..0000000 --- a/DiscordLab.Bot/API/Extensions/TranslationExtensions.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Globalization; -using System.Text; -using System.Text.RegularExpressions; -using Exiled.API.Features; - -namespace DiscordLab.Bot.API.Extensions -{ - public static class TranslationExtensions - { - // ReSharper disable once MemberCanBePrivate.Global - public static long CurrentUnix => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - - // ReSharper disable once MemberCanBePrivate.Global - // ReSharper disable once FieldCanBeMadeReadOnly.Global - // ReSharper disable once UseCollectionExpression - public static List>> StaticReplacers = new () - { - // Time Replacers - new("time", () => $""), - new("timet", () => $""), - new("timetlong", () => $""), - new("timed", () => $""), - new("timedlong", () => $""), - new("timef", () => $""), - new("timeflong", () => $""), - new("timer", () => $""), - - // Map Replacers - new("seed", () => Map.Seed.ToString()), - new("decontstate", () => Map.DecontaminationState.ToString()), - new("isdecont", () => Map.IsLczDecontaminated.ToString()), - new("isdecontenabled", () => Map.IsDecontaminationEnabled.ToString()), - - // Round Replacers - new("killcount", () => Round.Kills.ToString()), - new("alivesides", () => string.Join(", ", Round.AliveSides.Select(s => s.ToString()))), - new("alivesidescount", () => Round.AliveSides.Count().ToString()), - new("elapsedtime", () => Round.ElapsedTime.ToString()), - new("elapsedtimerelative", () => $""), - new("roundstart", () => $""), - new("roundcount", () => Round.UptimeRounds.ToString()), - new("escapedscientistscount", () => Round.EscapedScientists.ToString()), - new("inprogress", () => Round.InProgress.ToString()), - new("isended", () => Round.IsEnded.ToString()), - new("isstarted", () => Round.IsStarted.ToString()), - new("islocked", () => Round.IsLocked.ToString()), - new("islobby", () => Round.IsLobby.ToString()), - new("changedintozombiescount", () => Round.ChangedIntoZombies.ToString()), - new("escapeddclassescount", () => Round.EscapedDClasses.ToString()), - new("islobbylocked", () => Round.IsLobbyLocked.ToString()), - new("scpkillcount", () => Round.KillsByScp.ToString()), - new("alivescpcount", () => Round.SurvivingSCPs.ToString()), - - // Server Replacers - new("maxplayers", () => Server.MaxPlayerCount.ToString()), - new("name", () => Server.Name), - new("nameparsed", () => - { - const string tagRemoveRegex = @"<[^>]+>"; - const string uselessTextRemove = @"(.*?)<\/color>"; - - string result = Regex.Replace(Server.Name, uselessTextRemove, string.Empty); - result = Regex.Replace(result, tagRemoveRegex, string.Empty); - - return result; - }), - new("port", () => Server.Port.ToString()), - new("ip", () => Server.IpAddress), - new("playercount", () => Server.PlayerCount.ToString()), - new("playercountnonpcs", () => Player.List.Count(p => !p.IsNPC).ToString()), - new("tps", () => Server.Tps.ToString(CultureInfo.CurrentCulture)), - }; - - // ReSharper disable once MemberCanBePrivate.Global - // ReSharper disable once FieldCanBeMadeReadOnly.Global - // ReSharper disable once UseCollectionExpression - public static List>> PlayerReplacers = new () - { - new("nickname", player => player.Nickname.Replace("@", "\\@")), - new("id", player => player.UserId), - new("ip", player => player.IPAddress), - new("userid", player => player.Id.ToString()), - new("role", player => player.Role.Name), - new("roletype", player => player.Role.Type.ToString()), - new("team", player => player.Role.Team.ToString()), - new("side", player => player.Role.Side.ToString()), - new("health", player => player.Health.ToString(CultureInfo.CurrentCulture)), - new("maxhealth", player => player.MaxHealth.ToString(CultureInfo.CurrentCulture)), - new("group", player => player.GroupName), - new("badge", player => player.Group.BadgeText), - new("badgecolor", player => player.Group.BadgeColor) - }; - - /// - /// Makes all parameters lowercase and keeps the rest of the translation in its original state. - /// - /// The translation - /// The translation with lowercase params - public static string LowercaseParams(this string str) - { - const string pattern = @"\{(.*?)\}|(.)"; - - return Regex.Replace( - str, - pattern, - m => - m.Groups[1].Success ? $"{{{m.Groups[1].Value.ToLower()}}}" : m.Groups[2].Value - ); - } - - public static string StaticReplace(this string str) - { - StringBuilder builder = new(str); - foreach ((string placeholder, Func replaceWith) in StaticReplacers) - { - builder.Replace($"{{{placeholder}}}", replaceWith()); - } - - return builder.ToString(); - } - - public static string PlayerReplace(this string str, string prefix, Player player) - { - StringBuilder builder = new(str); - builder.Replace($"{{{prefix}}}", player.Nickname); - foreach ((string placeholder, Func replaceWith) in PlayerReplacers) - { - string replacement; - try - { - replacement = replaceWith(player); - } - catch (NullReferenceException) - { - replacement = "Unknown"; - } - if(string.IsNullOrEmpty(replacement)) replacement = "Unknown"; - builder.Replace($"{{{prefix}{placeholder}}}", replacement); - } - - return builder.ToString(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/DescriptionConstants.cs b/DiscordLab.Bot/API/Features/DescriptionConstants.cs deleted file mode 100644 index 446b95b..0000000 --- a/DiscordLab.Bot/API/Features/DescriptionConstants.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace DiscordLab.Bot.API.Features -{ - public class DescriptionConstants - { - public const string GuildId = "The guild ID where this module will be used. If not set (value = 0), it will use the default guild ID."; - public const string IsEnabled = "Whether the module is enabled or not."; - public const string Debug = "Whether the module is in debug mode or not."; - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Plugin.cs b/DiscordLab.Bot/API/Features/Plugin.cs new file mode 100644 index 0000000..83cf808 --- /dev/null +++ b/DiscordLab.Bot/API/Features/Plugin.cs @@ -0,0 +1,33 @@ +namespace DiscordLab.Bot.API.Features +{ + using LabApi.Loader; + + /// + /// Allows users to easily make plugins with translations also, not just configs. + /// + /// Your config. + /// Your translation. + public abstract class Plugin : LabApi.Loader.Features.Plugins.Plugin + where TConfig : class, new() + where TTranslation : class, new() + { + /// + /// Gets the plugin's config. + /// + public TConfig Config; + + /// + /// Gets the plugin's translation. + /// + public TTranslation Translation; + + /// + public override void LoadConfigs() + { + this.TryLoadConfig("config.yml", out Config); + this.TryLoadConfig("translation.yml", out Translation); + + base.LoadConfigs(); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Queue.cs b/DiscordLab.Bot/API/Features/Queue.cs new file mode 100644 index 0000000..182cdc5 --- /dev/null +++ b/DiscordLab.Bot/API/Features/Queue.cs @@ -0,0 +1,58 @@ +namespace DiscordLab.Bot.API.Features +{ + using MEC; + + /// + /// A utility class that helps with dealing with Discord rate limits by introducing a queue. + /// + public class Queue + { + /// + /// Initializes a new instance of the class. + /// + /// The duration you wish to wait before calling the method. + /// The default action to run. Can be null. + public Queue(float duration, Action defaultAction = null) + { + Duration = duration; + DefaultAction = defaultAction; + } + + /// + /// Gets the duration of time to wait between each call. + /// + public float Duration { get; } + + /// + /// Gets a value indicating whether the queue is ongoing. + /// + public bool IsBusy { get; private set; } + + /// + /// Gets the action to be run during when an action isn't provided. Can be null. + /// + public Action DefaultAction { get; } + + /// + /// Runs the queue process, either using the action parameter or default action. + /// + /// The action to run. Defaults to . + public void Process(Action action = null) + { + if (IsBusy) + return; + + action ??= DefaultAction; + if (action == null) + throw new ArgumentException("DefaultAction and parameter action can not be null at the same time."); + + IsBusy = true; + + Timing.CallDelayed(Duration, () => + { + IsBusy = false; + action(); + }); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/SlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs deleted file mode 100644 index 6db96b7..0000000 --- a/DiscordLab.Bot/API/Features/SlashCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.Bot.API.Features -{ - public abstract class SlashCommand : IAutocompleteCommand - { - public abstract SlashCommandBuilder Data { get; } - - public virtual ulong GuildId { get; set; } = 0; - - public abstract Task Run(SocketSlashCommand command); - - public virtual Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs new file mode 100644 index 0000000..e7f61c9 --- /dev/null +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -0,0 +1,314 @@ +using Discord; + +namespace DiscordLab.Bot.API.Features +{ + using System.Globalization; + using System.Text.RegularExpressions; + using LabApi.Features.Extensions; + using LabApi.Features.Wrappers; + using PlayerRoles; + + /// + /// Allows you to create translations with placeholders being replaced. + /// + public class TranslationBuilder + { + /// + /// The dictionary of replacers that have no argument. + /// + public static Dictionary> StaticReplacers = new() + { + // Map Replacers + ["seed"] = () => Map.Seed.ToString(), + + // Round Replacers + ["killcount"] = () => Round.TotalDeaths.ToString(), + ["elapsedtime"] = () => Round.Duration.ToString(), + ["escapedscientistscount"] = () => Round.EscapedScientists.ToString(), + ["inprogress"] = () => Round.IsRoundInProgress.ToString(), + ["isended"] = () => Round.IsRoundEnded.ToString(), + ["isstarted"] = () => Round.IsRoundStarted.ToString(), + ["islocked"] = () => Round.IsLocked.ToString(), + ["changedintozombiescount"] = () => Round.ChangedIntoZombies.ToString(), + ["escapeddclassescount"] = () => Round.EscapedClassD.ToString(), + ["islobbylocked"] = () => Round.IsLobbyLocked.ToString(), + ["scpkillcount"] = () => Round.KilledBySCPs.ToString(), + ["alivescpcount"] = () => Round.SurvivingSCPs.ToString(), + + // Server Replacers + ["maxplayers"] = () => Server.MaxPlayers.ToString(), + ["name"] = () => Server.ServerListName, + ["nameparsed"] = () => + { + string result = UselessTextRemoveRegex.Replace(Server.ServerListName, string.Empty); + result = TagRemoveRegex.Replace(result, string.Empty); + + return result; + }, + ["port"] = () => Server.Port.ToString(), + ["ip"] = () => Server.IpAddress, + ["playercount"] = () => Server.PlayerCount.ToString(), + ["playercountnonpcs"] = () => Player.List.Count(p => !p.IsNpc).ToString(), + ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), + }; + + /// + /// Time based replacers. The type is the unix timestamp. Can be got with . + /// + public static Dictionary> TimeReplacers = new() + { + ["time"] = time => $"", + ["timet"] = time => $"", + ["timetlong"] = time => $"", + ["timed"] = time => $"", + ["timedlong"] = time => $"", + ["timef"] = time => $"", + ["timeflong"] = time => $"", + ["timer"] = time => $"", + ["elapsedtimerelative"] = time => $"", + ["roundstart"] = time => $"", + }; + + /// + /// Player based replacements. + /// + public static Dictionary> PlayerReplacers = new() + { + ["nickname"] = player => player.Nickname.Replace("@", "\\@"), + ["id"] = player => player.UserId, + ["ip"] = player => player.IpAddress, + ["userid"] = player => player.PlayerId.ToString(), + ["role"] = player => player.Role.GetFullName(), + ["roletype"] = player => player.Role.ToString(), + ["team"] = player => player.Team.ToString(), + ["faction"] = player => player.Team.GetFaction().ToString(), + ["health"] = player => player.Health.ToString(CultureInfo.CurrentCulture), + ["maxhealth"] = player => player.MaxHealth.ToString(CultureInfo.CurrentCulture), + ["group"] = player => player.GroupName, + ["badgecolor"] = player => player.GroupColor.ToString(), + }; + + private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); + + private static readonly Regex UselessTextRemoveRegex = new(@"(.*?)<\/color>", RegexOptions.Compiled); + + /// + /// Initializes a new instance of the class. + /// + /// The translation to modify. + public TranslationBuilder(string translation) + { + Translation = translation; + } + + /// + /// Initializes a new instance of the class with a person added. + /// + /// The translation to modify. + /// The player prefix. + /// The player to use for the prefix. + public TranslationBuilder(string translation, string playerPrefix, Player player) + { + Translation = translation; + AddPlayer(playerPrefix, player); + } + + /// + /// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with. + /// + public Dictionary> CustomReplacers { get; set; } = new(); + + /// + /// Gets or sets the players that need to be translated for, if any. + /// + public Dictionary Players { get; set; } = new(); + + /// + /// Gets or sets the time that this translation will use. + /// + public DateTime Time { get; set; } = DateTime.Now; + + /// + /// Gets the translation. + /// + public string Translation { get; } + + /// + /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null. + /// + /// If you want the {players} placeholder to not work, set this to null. + public string PlayerListItem { get; set; } + + /// + /// Gets or sets the separator between items in . + /// + public string PlayerListSeparator { get; set; } = "\n"; + + /// + /// . + /// + /// The instance. + /// + public static implicit operator string(TranslationBuilder builder) => + builder.Build(); + + /// + /// . + /// + /// The instance. + /// + public static implicit operator Optional(TranslationBuilder builder) => + builder.Build(); + + /// + /// Adds multiple players to the list. + /// + /// The players to add. + /// The instance. + public TranslationBuilder AddPlayers(Dictionary players) + { + foreach (KeyValuePair pair in players) + { + Players.Add(pair.Key, pair.Value); + } + + return this; + } + + /// + /// Adds a player to the list. + /// + /// The prefix for the player. + /// The to add. + /// The instance. + public TranslationBuilder AddPlayer(string prefix, Player player) + { + Players.Add(prefix, player); + + return this; + } + + /// + /// Adds a custom replacer to the dictionary. + /// + /// The text to replace. + /// The string factory to replace with. + /// The instance. + public TranslationBuilder AddCustomReplacer(string toReplace, Func replacer) + { + CustomReplacers.Add(toReplace, replacer); + + return this; + } + + /// + /// Adds a custom replacer to the dictionary. + /// + /// The text to replace. + /// The text to replace with. + /// The instance. + public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) + { + AddCustomReplacer(toReplace, () => replacer); + + return this; + } + + /// + /// Builds this instance. + /// + /// The translation built. + public string Build() + { + if (PlayerListItem != null) + SetupPlayerList(); + + string returnTranslation = Translation; + + foreach (KeyValuePair> replacer in CustomReplacers) + { + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(), + RegexOptions.IgnoreCase); + } + + foreach (KeyValuePair> replacer in StaticReplacers) + { + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(), + RegexOptions.IgnoreCase); + } + + long unix = new DateTimeOffset(Time).ToUnixTimeSeconds(); + foreach (KeyValuePair> replacer in TimeReplacers) + { + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(unix), + RegexOptions.IgnoreCase); + } + + foreach (KeyValuePair player in Players) + { + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(player.Key), + player.Value.Nickname, + RegexOptions.IgnoreCase); + + foreach (KeyValuePair> replacer in PlayerReplacers) + { + string replacement; + + try + { + replacement = replacer.Value(player.Value); + } + catch (NullReferenceException) + { + replacement = "Unknown"; + } + + if (string.IsNullOrEmpty(replacement)) + replacement = "Unknown"; + + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString($"{player.Key}{replacer.Key}"), + replacement, + RegexOptions.IgnoreCase); + } + } + + return returnTranslation; + } + + private static string ToParameterString(string str) => "{" + str + "}"; + + private void SetupPlayerList() + { + Player[] readyPlayers = Player.ReadyList.ToArray(); + + int length = readyPlayers.Length; + + List playerItems = new(length); + Dictionary playerDictionary = new(length); + + for (int i = 0; i < length; i++) + { + string playerKey = $"player{i}"; + playerItems.Add(PlayerListItem.Replace("{player", "{" + $"{playerKey}")); + playerDictionary[playerKey] = readyPlayers[i]; + } + + CustomReplacers.Add("players", () => string.Join(PlayerListSeparator, playerItems)); + + AddPlayers(playerDictionary); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/UpdateStatus.cs b/DiscordLab.Bot/API/Features/UpdateStatus.cs deleted file mode 100644 index abd3081..0000000 --- a/DiscordLab.Bot/API/Features/UpdateStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace DiscordLab.Bot.API.Features -{ - public class UpdateStatus - { - /// - /// The name of the module/plugin. - /// - public string ModuleName { get; set; } - /// - /// The version of the module/plugin. - /// - public Version Version { get; set; } - /// - /// The download url of this version of the module/plugin. - /// - public string Url { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs b/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs index d6e1996..028eec8 100644 --- a/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs +++ b/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs @@ -1,14 +1,17 @@ -using Discord.WebSocket; - namespace DiscordLab.Bot.API.Interfaces { + using Discord.WebSocket; + + /// + /// Allows you to make autocomplete commands. + /// public interface IAutocompleteCommand : ISlashCommand { /// - /// Control what happens when an autocomplete request is sent. + /// The method that is called once an autocomplete request is made. /// - /// The slash command instance - /// The Task completion status + /// The autocomplete instance. + /// The . public Task Autocomplete(SocketAutocompleteInteraction autocomplete); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Interfaces/IDLConfig.cs b/DiscordLab.Bot/API/Interfaces/IDLConfig.cs deleted file mode 100644 index cc2d9b6..0000000 --- a/DiscordLab.Bot/API/Interfaces/IDLConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.ComponentModel; - -namespace DiscordLab.Bot.API.Interfaces -{ - - public interface IDLConfig - { - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Interfaces/IRegisterable.cs b/DiscordLab.Bot/API/Interfaces/IRegisterable.cs deleted file mode 100644 index 539563f..0000000 --- a/DiscordLab.Bot/API/Interfaces/IRegisterable.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace DiscordLab.Bot.API.Interfaces -{ - public interface IRegisterable - { - /// - /// Here is where you put your code that you want to run at the same time as OnEnabled in your plugin root. - /// Useful things would be stuff like binding your static Instance variable. - /// - public void Init(); - - /// - /// Here is what you want to run when the plugin is just above to disable. - /// - public void Unregister(); - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs b/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs index b6d9c8b..88fc90a 100644 --- a/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs +++ b/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs @@ -1,26 +1,97 @@ -using Discord; -using Discord.WebSocket; - namespace DiscordLab.Bot.API.Interfaces { + using System.Collections.ObjectModel; + using System.Collections.Specialized; + using System.Reflection; + using Discord; + using Discord.WebSocket; + using DiscordLab.Bot.API.Attributes; + using LabApi.Features.Console; + + /// + /// A wrapper to easily add your own slash commands in your bot. + /// public interface ISlashCommand { + public static ObservableCollection Commands = []; + /// - /// Here you create your with the data of your command. + /// Gets the slash command data. /// public SlashCommandBuilder Data { get; } - + /// - /// Set the GuildId where the command should be created here, you should reference Config.GuildId. + /// Gets the guild ID to assign this command to. /// - public ulong GuildId { get; set; } + public ulong GuildId { get; } /// - /// Here is where your slash command runs. + /// What should be called when you run this command. /// - /// - /// This type contains information about the command that was executed. - /// + /// The command data. + /// A . public Task Run(SocketSlashCommand command); + + /// + /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself. + /// + /// The assembly you wish to check, defaults to the current one. + public static void FindAll(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + foreach (Type type in assembly.GetTypes()) + { + if (type.IsAbstract || !typeof(ISlashCommand).IsAssignableFrom(type)) + continue; + + ISlashCommand init = Activator.CreateInstance(type) as ISlashCommand; + Commands.Add(init); + } + } + + [CallOnLoad] + private static void Start() + { + Commands.CollectionChanged += OnCollectionChanged; + } + + [CallOnUnload] + private static void Unload() + { + Commands.CollectionChanged -= OnCollectionChanged; + Commands = null; + } + + [CallOnReady] + private static void Ready() + { + Task.Run(() => RegisterGuildCommands(Commands)); + } + + private static void OnCollectionChanged(object _, NotifyCollectionChangedEventArgs ev) + { + if (!Client.IsClientReady) + return; + if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace)) + return; + + Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems)); + } + + private static async Task RegisterGuildCommands(IEnumerable commands) + { + foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) + { + SocketGuild guild = Client.GetGuild(cmds.Key); + if (guild == null) + { + Logger.Warn($"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); + continue; + } + + await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build()).ToArray()); + } + } } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Modules/HandlerLoader.cs b/DiscordLab.Bot/API/Modules/HandlerLoader.cs deleted file mode 100644 index c3ccf97..0000000 --- a/DiscordLab.Bot/API/Modules/HandlerLoader.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Reflection; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using JetBrains.Annotations; - -namespace DiscordLab.Bot.API.Modules -{ - public class HandlerLoader - { - private readonly List _inits = new(); - - /// - /// Once you run this, it will grab all the classes from your plugin's and run their method. - /// It also grabs your classes and registers them. You will need to do no command handling on your side. DiscordLab does it all. - /// - /// - /// Your plugin's . Defaults to . - /// - /// - /// If you use this function, you are required to call when your plugin is about to be disabled. No need to pass in any params though. - /// - public bool Load(Assembly assembly = null) - { - assembly ??= Assembly.GetCallingAssembly(); - if (Plugin.Instance.Config.Token is "token" or "") - { - Log.Error($"Could not load {assembly.GetName().Name} because the bot token is not set in the config file."); - return false; - } - Type registerType = typeof(IRegisterable); - foreach (Type type in assembly.GetTypes()) - { - if (type.IsAbstract || !registerType.IsAssignableFrom(type)) - continue; - - IRegisterable init = Activator.CreateInstance(type) as IRegisterable; - Log.Debug($"Loading {type.Name}..."); - _inits.Add(init); - init!.Init(); - } - - SlashCommandLoader.LoadCommands(assembly); - return true; - } - - /// - /// Unloads all IRegisterable classes that were loaded. - /// - public void Unload() - { - foreach (IRegisterable init in _inits) - init.Unregister(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Modules/QueueSystem.cs b/DiscordLab.Bot/API/Modules/QueueSystem.cs deleted file mode 100644 index a9a13fd..0000000 --- a/DiscordLab.Bot/API/Modules/QueueSystem.cs +++ /dev/null @@ -1,34 +0,0 @@ -using MEC; - -namespace DiscordLab.Bot.API.Modules -{ - public static class QueueSystem - { - private static List _openQueueIds = new(); - - /// - /// Will run code after 5 seconds, but will only run once per id. - /// This is different from because it will only run once - /// 5 seconds after the initial call. - /// - /// - /// A unique identifier for this queue. Like DiscordLab.BotStatus.PlayerVerified - /// - /// - /// The bit of code you want to run after 5 seconds, this shouldn't have any hard coded variables unless they - /// will never change within the 5 seconds. - /// - public static void QueueRun(string id, Action action) - { - if (_openQueueIds.Contains(id)) return; - _openQueueIds.Add(id); - Timing.CallDelayed(5, () => - { - RemoveId(id); - action(); - }); - } - - private static void RemoveId(string id) => _openQueueIds.Remove(id); - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Modules/SlashCommandLoader.cs b/DiscordLab.Bot/API/Modules/SlashCommandLoader.cs deleted file mode 100644 index 7eb2325..0000000 --- a/DiscordLab.Bot/API/Modules/SlashCommandLoader.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.ComponentModel; -using System.Reflection; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.Handlers; -using Exiled.API.Features; - -namespace DiscordLab.Bot.API.Modules -{ - public static class SlashCommandLoader - { - internal static void Create() - { - Commands.AddingNew += OnCommandAdded; - } - - public static BindingList Commands = new(); - - /// - /// Adds all commands in a from the classes to a list. - /// - /// - /// Your plugin's . - /// - public static void LoadCommands(Assembly assembly) - { - Type registerType = typeof(ISlashCommand); - foreach (Type type in assembly.GetTypes()) - { - if (type.IsAbstract || !registerType.IsAssignableFrom(type)) - continue; - - ISlashCommand init = Activator.CreateInstance(type) as ISlashCommand; - Commands.Add(init); - } - } - - /// - /// This clears all commands from the list. - /// - /// - /// This should only be used by the main bot, you should have no reason to run this. - /// - public static void ClearCommands() - { - Commands = new(); - } - - internal static void Destroy() - { - Commands.AddingNew -= OnCommandAdded; - ClearCommands(); - } - - private static void OnCommandAdded(object sender, AddingNewEventArgs ev) - { - ISlashCommand command = (ISlashCommand)ev.NewObject; - Log.Debug($"Added command {command.Data.Name}, processing..."); - if (!DiscordBot.Instance.IsReady) return; - Task.Run(() => DiscordBot.Instance.CreateGuildCommand(command)); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Modules/UpdateStatus.cs b/DiscordLab.Bot/API/Modules/UpdateStatus.cs deleted file mode 100644 index f2d95a5..0000000 --- a/DiscordLab.Bot/API/Modules/UpdateStatus.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Newtonsoft.Json; -using System.Net.Http; -using Exiled.API.Features; -using Exiled.API.Interfaces; -using Exiled.Loader; - -namespace DiscordLab.Bot.API.Modules -{ - public class GitHubRelease - { - [JsonProperty("tag_name")] - public string TagName { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("html_url")] - public string Url { get; set; } - [JsonProperty("published_at")] - public DateTime PublishedAt { get; set; } - [JsonProperty("assets")] - public List Assets { get; set; } - [JsonProperty("prerelease")] - public bool Prerelease { get; set; } - [JsonProperty("draft")] - public bool Draft { get; set; } - } - - public class GitHubReleaseAsset - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } - } - - public static class UpdateStatus - { - private static readonly HttpClient Client = new (); - - private static readonly string Path = Paths.Plugins; - - public static List Statuses { get; private set; } - - /// - /// This will write a plugin to the Plugins folder. - /// - /// The response from a download via - /// The name of the plugin, without .dll - private static void WritePlugin(byte[] bytes, string name) - { - string[] files = Directory.GetFiles(Path); - - string existingPlugin = files.FirstOrDefault(x => x.Contains(name)); - - if (existingPlugin != null) - { - File.Delete(existingPlugin); - } - - string pluginPath = System.IO.Path.Combine(Path, name + ".dll"); - File.WriteAllBytes(pluginPath, bytes); - } - - /// - /// This will download a plugin using - /// - /// The - public static async Task DownloadPlugin(API.Features.UpdateStatus status) - { - byte[] pluginData = await Client.GetByteArrayAsync(status.Url); - WritePlugin(pluginData, status.ModuleName); - } - - /// - /// This will check the GitHub API for the latest version of the modules and plugins. - /// - /// - /// Should only be run once via the main bot, otherwise you'll just be doing unnecessary requests. - /// - public static async Task GetStatus() - { - Statuses = new(); - Client.DefaultRequestHeaders.UserAgent.ParseAdd("request"); - string response = await Client.GetStringAsync("https://api.github.com/repos/DiscordLabSCP/DiscordLab/releases"); - List statuses = new(); - List releases = JsonConvert.DeserializeObject>(response); - foreach (GitHubRelease release in releases) - { - if(release.Prerelease || release.Draft) continue; - foreach (GitHubReleaseAsset asset in release.Assets) - { - Features.UpdateStatus status = new() - { - ModuleName = asset.Name.Replace(".dll", ""), - Version = new (string.Join(".", release.TagName.Split('.').Take(3))), - Url = asset.DownloadUrl - }; - - // do not want to auto update to breaking changes - if(status.Version.Major != Plugin.Instance.Version.Major) continue; - - List moduleStatuses = statuses.Where(s => s.ModuleName == status.ModuleName).ToList(); - if (moduleStatuses.Any(s => s.Version < status.Version)) - { - statuses.RemoveAll(s => s.ModuleName == status.ModuleName); - statuses.Add(status); - } - else if (!moduleStatuses.Any()) - { - statuses.Add(status); - } - } - } - - Statuses = statuses; - - List> plugins = Loader.Plugins.Where(x => x.Name.StartsWith("DiscordLab.")).ToList(); - plugins.Add(Loader.Plugins.First(x => x.Name == Plugin.Instance.Name)); - List pluginsToUpdate = new(); - foreach (IPlugin plugin in plugins) - { - API.Features.UpdateStatus status = statuses.FirstOrDefault(x => x.ModuleName == plugin.Name); - if (status == null) - { - if(plugin.Name == Plugin.Instance.Name) status = statuses.First(x => x.ModuleName == "DiscordLab.Bot"); - else continue; - } - - if (status.Version <= plugin.Version) continue; - if (Plugin.Instance.Config.AutoUpdate) - { - pluginsToUpdate.Add(status.ModuleName); - await DownloadPlugin(status); - } - else - { - Log.Warn($"There is a new version of {status.ModuleName} available, version {status.Version}, you are currently on {plugin.Version}! Download it from {status.Url}"); - } - } - - if (pluginsToUpdate.Any()) - { - ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - Log.Info("Server will restart next round for updates. Updating plugins: " + string.Join(", ", pluginsToUpdate)); - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Modules/WriteableConfig.cs b/DiscordLab.Bot/API/Modules/WriteableConfig.cs deleted file mode 100644 index 6954d9e..0000000 --- a/DiscordLab.Bot/API/Modules/WriteableConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Exiled.API.Features; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DiscordLab.Bot.API.Modules -{ - public static class WriteableConfig - { - private static string Path = - System.IO.Path.Combine(System.IO.Path.Combine(Paths.Configs, "DiscordLab"), "config.json"); - - /// - /// This will get the config.json file from the DiscordLab folder in the Exiled configs. - /// - /// - /// The config.json file as a . - /// - public static JObject GetConfig() - { - if (!File.Exists(Path)) - { - Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)!); - File.WriteAllText(Path, "{}"); - } - - return JObject.Parse(File.ReadAllText(Path)); - } - - /// - /// This will write a new option to the config.json file. - /// It can also overwrite options. - /// - /// - /// The key of the option you want to write. - /// - /// - /// The value of the option you want to write. Should be JSON serializable. - /// - public static void WriteConfigOption(string key, JToken value) - { - JObject config = GetConfig(); - config[key] = value; - File.WriteAllText(Path, JsonConvert.SerializeObject(config)); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/GitHubRelease.cs b/DiscordLab.Bot/API/Updates/GitHubRelease.cs new file mode 100644 index 0000000..f882ba1 --- /dev/null +++ b/DiscordLab.Bot/API/Updates/GitHubRelease.cs @@ -0,0 +1,34 @@ +namespace DiscordLab.Bot.API.Updates +{ + using System.Text.Json.Serialization; + + /// + /// Contains data for a GitHub release object. + /// + public class GitHubRelease + { + /// + /// Gets or sets the tag name for this release. + /// + [JsonPropertyName("tag_name")] + public string TagName { get; set; } + + /// + /// Gets or sets the assets for this release. + /// + [JsonPropertyName("assets")] + public List Assets { get; set; } + + /// + /// Gets or sets a value indicating whether this is a prerelease release. + /// + [JsonPropertyName("prerelease")] + public bool Prerelease { get; set; } + + /// + /// Gets or sets a value indicating whether this is a draft release. + /// + [JsonPropertyName("draft")] + public bool Draft { get; set; } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs new file mode 100644 index 0000000..3cb8ca5 --- /dev/null +++ b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs @@ -0,0 +1,22 @@ +namespace DiscordLab.Bot.API.Updates +{ + using System.Text.Json.Serialization; + + /// + /// Gets details for a GitHub release asset. + /// + public class GitHubReleaseAsset + { + /// + /// Gets or sets the name of the asset. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// Gets or sets the download name of the asset. + /// + [JsonPropertyName("browser_download_url")] + public string DownloadUrl { get; set; } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Bot/API/Updates/Module.cs new file mode 100644 index 0000000..f3f5d12 --- /dev/null +++ b/DiscordLab.Bot/API/Updates/Module.cs @@ -0,0 +1,85 @@ +using HarmonyLib; +using LabApi.Loader; +using LabApi.Loader.Features.Paths; + +namespace DiscordLab.Bot.API.Updates +{ + /// + /// Contains information about a DiscordLab module. + /// + public class Module + { + /// + /// Initializes a new instance of the class. + /// + /// The release of this module. + /// The asset of this module. + public Module(GitHubRelease release, GitHubReleaseAsset asset) + { + Release = release; + Asset = asset; + Name = asset.Name.Replace(".dll", string.Empty); + Version = new(release.TagName.Split('-').First()); + ExistingPlugin = PluginLoader.Plugins.Keys.FirstOrDefault(x => Name == "DiscordLab.Bot" ? x.Name == "DiscordLab" : x.Name == Name); + } + + /// + /// Gets the found modules as of current. + /// + public static IReadOnlyCollection CurrentModules { get; internal set; } = []; + + /// + /// Gets the module name. + /// + public string Name { get; } + + /// + /// Gets the version of this Update. + /// + public Version Version { get; } + + /// + /// Gets the plugin of this module, null if module is not installed. + /// + public LabApi.Loader.Features.Plugins.Plugin ExistingPlugin { get; } + + /// + /// Gets the release this module comes from. + /// + public GitHubRelease Release { get; } + + /// + /// Gets the asset this module comes from. + /// + public GitHubReleaseAsset Asset { get; } + + /// + /// Generates a string used to show current version and latest version for a list of modules. Will throw if no existing plugin. + /// + /// The modules to generate for. + /// The generated string. + public static string GenerateUpdateString(IEnumerable modules) => string.Join( + "\n- ", modules.Select(module => + $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}") + ); + + /// + /// Downloads this module. + /// + /// A . + internal async Task Download() + { + string filePath = Path.Combine(PathManager.Plugins.FullName, "global", Asset.Name); + + if (ExistingPlugin != null) + { + filePath = Path.Combine(Path.GetDirectoryName(ExistingPlugin.FilePath)!, Asset.Name); + File.Delete(ExistingPlugin.FilePath); + } + + byte[] data = await Updater.DownloadClient.GetByteArrayAsync($"/{Release.TagName}/{Asset.Name}"); + + File.WriteAllBytes(filePath, data); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/Updater.cs b/DiscordLab.Bot/API/Updates/Updater.cs new file mode 100644 index 0000000..32079a2 --- /dev/null +++ b/DiscordLab.Bot/API/Updates/Updater.cs @@ -0,0 +1,129 @@ +namespace DiscordLab.Bot.API.Updates +{ + using System.Net.Http; + using DiscordLab.Bot.API.Attributes; + using LabApi.Features.Console; + using LabApi.Loader; + using Utf8Json; + + /// + /// Handle updates within DiscordLab. + /// + public static class Updater + { + /// + /// Gets the HTTP Client to use for checking for releases. + /// + public static HttpClient Client { get; private set; } = new(); + + /// + /// Gets the HTTP Client to use for downloading new modules. + /// + public static HttpClient DownloadClient { get; private set; } = new(); + + /// + /// Checks the GitHub repository for updates, and returns back the latest versions of each module. + /// + /// The latest versions of each module. + public static async Task> CheckForUpdates() + { + using HttpResponseMessage response = await Client.GetAsync(string.Empty); + + try + { + response.EnsureSuccessStatusCode(); + } + catch (Exception) + { + return []; + } + + using Stream stream = await response.Content.ReadAsStreamAsync(); + + GitHubRelease[] releases = JsonSerializer.Deserialize(stream); + + List modules = []; + + foreach (GitHubRelease release in releases) + { + if (release.Prerelease || release.Draft) + continue; + if (release.TagName.Count(c => c == '.') == 3) + continue; + Version version = new(release.TagName.Split('-').First()); + + if (version.Major != Plugin.Instance.Version.Major) + continue; + + foreach (GitHubReleaseAsset asset in from asset in release.Assets let name = asset.Name.Replace(".dll", string.Empty) where !modules.Any(module => module.Name == name && module.Version > version) select asset) + { + modules.Add(new(release, asset)); + } + } + + Module.CurrentModules = modules; + + return Module.CurrentModules; + } + + /// + /// Runs and will either log out the updates available or install the updates, it depends on the config values. + /// + /// The modules that were updated, or need updating. + public static async Task> ManageUpdates() + { + IEnumerable modules = await CheckForUpdates(); + List modulesToUpdate = []; + + foreach (Module module in modules) + { + if (module.ExistingPlugin == null) + continue; + + if (module.Version > module.ExistingPlugin.Version) + modulesToUpdate.Add(module); + } + + if (modulesToUpdate.Count == 0) + return []; + + Logger.Warn($"DiscordLab modules need updating:\n${Module.GenerateUpdateString(modulesToUpdate)}"); + + if (!Plugin.Instance.Config.AutoUpdate) + return modulesToUpdate; + + Logger.Info("Downloading DiscordLab updates..."); + + foreach (Module module in modulesToUpdate) + { + await module.Download(); + } + + Logger.Info("All DiscordLab modules updated..."); + + return modulesToUpdate; + } + + [CallOnLoad] + private static void Setup() + { + Client.BaseAddress = new("https://api.github.com/repos/DiscordLabSCP/DiscordLab/releases"); + Client.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); + + if (Plugin.Instance.Config.AutoUpdate) + { + DownloadClient.BaseAddress = new("https://github.com/DiscordLabSCP/DiscordLab/releases/download"); + DownloadClient.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); + } + + Task.Run(ManageUpdates); + } + + [CallOnUnload] + private static void Disable() + { + Client = null; + DownloadClient = null; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs new file mode 100644 index 0000000..58aeefe --- /dev/null +++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs @@ -0,0 +1,43 @@ +using LabApi.Features.Wrappers; + +namespace DiscordLab.Bot.API.Utilities +{ + public static class CommandUtils + { + /// + /// Gets a player from an unparsed string id, will check if or . + /// + /// The ID to check. + /// The player if found. + public static Player GetPlayerFromUnparsed(string id) + { + return TryGetPlayerFromUnparsed(id, out Player player) ? player : null; + } + + /// + /// Tries to get a player from an unparsed string id, will check if or . + /// + /// The ID to check. + /// The player if found. + /// Whether the player was found. + public static bool TryGetPlayerFromUnparsed(string id, out Player player) + { + if (int.TryParse(id, out int intId)) + { + if (!Player.TryGet(intId, out player)) + { + return false; + } + } + else + { + if (!Player.TryGet(id, out player)) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs new file mode 100644 index 0000000..327938a --- /dev/null +++ b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs @@ -0,0 +1,24 @@ +namespace DiscordLab.Bot.API.Utilities +{ + /// + /// Contains utilities for logging related tasks. + /// + public static class LoggingUtils + { + /// + /// Generates a message that will tell the user that the channel was not found. + /// + /// The submodule that this error comes from. + /// The channel ID that was missing + /// The related guild ID, goes to the default guild ID if 0. + /// The error string. + public static string GenerateMissingChannelMessage(string type, ulong channelId, ulong guildId) + { + if (guildId == 0) + guildId = Plugin.Instance.Config.GuildId; + + return + $"Could not find channel {channelId} under the guild {guildId}, please make sure the bot has access and you put in the right IDs. This was triggered from {type}."; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs new file mode 100644 index 0000000..4d99d0a --- /dev/null +++ b/DiscordLab.Bot/Client.cs @@ -0,0 +1,180 @@ +namespace DiscordLab.Bot +{ + using System.Net; + using System.Net.WebSockets; + using Discord; + using Discord.Net.Rest; + using Discord.Net.WebSockets; + using Discord.WebSocket; + using DiscordLab.Bot.API.Attributes; + using DiscordLab.Bot.API.Interfaces; + using LabApi.Features.Console; + + /// + /// The Discord bot client. + /// + public static class Client + { + /// + /// Gets the websocket client for the Discord bot. + /// + public static DiscordSocketClient SocketClient { get; private set; } + + /// + /// Gets a value indicating whether the client is in the ready state. + /// + public static bool IsClientReady { get; private set; } + + /// + /// Gets a list of saved text channels listed by their ID. + /// + public static Dictionary SavedTextChannels { get; } = new(); + + /// + /// Gets the default guild for the plugin. + /// + public static SocketGuild DefaultGuild { get; private set; } + + private static Config Config => Plugin.Instance.Config; + + /// + /// Gets a cached guild from a ID. + /// + /// The guild ID. + /// If the ID is 0, then the default guild (if it exists), if else then it will return the found guild, or null. + public static SocketGuild GetGuild(ulong id) + { + return id == 0 ? DefaultGuild : SocketClient.GetGuild(id); + } + + /// + /// Gets or adds a channel via its ID. Uses cache. + /// + /// The ID of the channel. + /// The channel, if found. + public static SocketTextChannel GetOrAddChannel(ulong id) => + SavedTextChannels.GetOrAdd(id, () => SocketClient.GetChannel(id) as SocketTextChannel); + + /// + /// Tries to get or add a channel via its ID. Uses cache. + /// + /// The ID of the channel. + /// The channel, if found. + /// Whether the channel was found. + public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) + { + channel = GetOrAddChannel(id); + return channel != null; + } + + /// + /// Starts the bot. + /// + [CallOnLoad] + internal static void Start() + { + DiscordSocketConfig config = new() + { + GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages, + LogLevel = Config.Debug ? LogSeverity.Debug : LogSeverity.Warning, + }; + + if (!string.IsNullOrEmpty(Config.ProxyUrl)) + { + WebProxy proxy = new(Config.ProxyUrl); + config.RestClientProvider = DefaultRestClientProvider.Create(true, proxy); + config.WebSocketProvider = DefaultWebSocketProvider.Create(proxy); + } + + SocketClient = new(config); + SocketClient.Log += OnLog; + SocketClient.Ready += OnReady; + SocketClient.SlashCommandExecuted += SlashCommandHandler; + SocketClient.AutocompleteExecuted += AutocompleteHandler; + Task.Run(StartClient); + } + + /// + /// Disables the bot. + /// + [CallOnUnload] + internal static void Disable() + { + SavedTextChannels = null; + + SocketClient.Log -= OnLog; + SocketClient.Ready -= OnReady; + SocketClient.SlashCommandExecuted -= SlashCommandHandler; + SocketClient.AutocompleteExecuted -= AutocompleteHandler; + Task.Run(async () => + { + await SocketClient.LogoutAsync(); + await SocketClient.StopAsync(); + await SocketClient.DisposeAsync(); + }); + } + + private static async Task StartClient() + { + DebugLog("Starting client..."); + await SocketClient.LoginAsync(TokenType.Bot, Config.Token); + await SocketClient.StartAsync(); + } + + private static Task OnLog(LogMessage msg) + { + if (msg.Exception is WebSocketException or GatewayReconnectException) + return Task.CompletedTask; + + switch (msg.Severity) + { + case LogSeverity.Error or LogSeverity.Critical: + Logger.Error(msg); + break; + case LogSeverity.Warning: + Logger.Warn(msg); + break; + case LogSeverity.Debug: + DebugLog(msg); + break; + default: + Logger.Info(msg); + break; + } + + return Task.CompletedTask; + } + + private static Task OnReady() + { + DebugLog("Bot is ready"); + IsClientReady = true; + DefaultGuild = SocketClient.GetGuild(Config.GuildId); + CallOnReadyAttribute.Ready(); + return Task.CompletedTask; + } + + private static Task SlashCommandHandler(SocketSlashCommand command) + { + DebugLog($"{command.Data.Name} requested a response, finding the command..."); + ISlashCommand cmd = ISlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); + + cmd?.Run(command); + return Task.CompletedTask; + } + + private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) + { + DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); + IAutocompleteCommand command = ISlashCommand.Commands.FirstOrDefault(c => c is IAutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as IAutocompleteCommand; + + command?.Autocomplete(autocomplete); + return Task.CompletedTask; + } + + private static void DebugLog(object message) + { + Logger.Debug(message, Config.Debug); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/Discord.cs b/DiscordLab.Bot/Commands/Discord.cs deleted file mode 100644 index 1b7d865..0000000 --- a/DiscordLab.Bot/Commands/Discord.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; - -namespace DiscordLab.Bot.Commands -{ - public class Discord : IAutocompleteCommand - { - public SlashCommandBuilder Data { get; } = new() - { - Name = "discordlab", - Description = "DiscordLab related commands", - DefaultMemberPermissions = GuildPermission.Administrator, - Options = new() - { - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "list", - Description = "List all available DiscordLab modules" - }, - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "install", - Description = "Install a DiscordLab module", - Options = new () - { - new () - { - Type = ApplicationCommandOptionType.String, - Name = "module", - Description = "The module to install", - IsRequired = true, - IsAutocomplete = true - } - } - }, - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "check", - Description = "Check for DiscordLab updates" - } - } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; - - public async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(true); - string subcommand = command.Data.Options.First().Name; - if (subcommand == "list") - { - if (UpdateStatus.Statuses == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "No modules available as of current, please wait for your server to fully start."); - return; - } - string modules = string.Join("\n", UpdateStatus.Statuses.Where(s => s.ModuleName != "DiscordLab.Bot").Select(s => s.ModuleName)); - await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); - } - else if (subcommand == "install") - { - if (UpdateStatus.Statuses == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "No modules available as of current, please wait for your server to fully start."); - return; - } - string module = command.Data.Options.First().Options.First().Value.ToString(); - if(string.IsNullOrWhiteSpace(module)) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Please provide a module name."); - return; - } - API.Features.UpdateStatus status = UpdateStatus.Statuses.FirstOrDefault(s => string.Equals(s.ModuleName, module, StringComparison.CurrentCultureIgnoreCase)) ?? UpdateStatus.Statuses.FirstOrDefault(s => s.ModuleName.Split('.').Last().Equals(module, StringComparison.CurrentCultureIgnoreCase)); - if (status == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); - return; - } - - await UpdateStatus.DownloadPlugin(status); - ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - await command.ModifyOriginalResponseAsync(m => m.Content = "Downloaded module. Server will restart next round."); - } - else if (subcommand == "check") - { - await UpdateStatus.GetStatus(); - if (UpdateStatus.Statuses == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Could not collect modules successfully, try again later."); - return; - } - await command.ModifyOriginalResponseAsync(m => m.Content = "Checked modules, if there is any updates, your server will restart next round to update to them."); - } - } - - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - if (UpdateStatus.Statuses == null) - { - await autocomplete.RespondAsync(new List()); - return; - } - await autocomplete.RespondAsync(result: UpdateStatus.Statuses - .Where(s => s.ModuleName != "DiscordLab.Bot").Select(s => new AutocompleteResult - { - Name = s.ModuleName, - Value = s.ModuleName - })); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs new file mode 100644 index 0000000..d1ce11e --- /dev/null +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -0,0 +1,116 @@ +namespace DiscordLab.Bot.Commands +{ + using Discord; + using Discord.WebSocket; + using DiscordLab.Bot.API.Interfaces; + using DiscordLab.Bot.API.Updates; + + /// + public class DiscordCommand : IAutocompleteCommand + { + /// + public SlashCommandBuilder Data { get; } = new() + { + Name = "discordlab", + Description = "DiscordLab related commands", + DefaultMemberPermissions = GuildPermission.Administrator, + Options = + [ + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "list", + Description = "List all available DiscordLab modules", + }, + + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "install", + Description = "The module to install", + Options = + [ + new() + { + Type = ApplicationCommandOptionType.String, + Name = "module", + Description = "The module to install", + IsRequired = true, + IsAutocomplete = true, + } + + ], + }, + + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "check", + Description = "Check for DiscordLab updates", + } + + ], + }; + + /// + public ulong GuildId { get; } = 0; + + /// + public async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(true); + string subcommand = command.Data.Options.First().Name; + switch (subcommand) + { + case "list": + { + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); + break; + } + + case "install": + { + string moduleName = command.Data.Options.First().Options.First().Value.ToString(); + if (string.IsNullOrWhiteSpace(moduleName)) + { + await command.ModifyOriginalResponseAsync(m => m.Content = "Please provide a module name."); + return; + } + + Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + if (module == null) + { + await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); + return; + } + + await module.Download(); + ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; + await command.ModifyOriginalResponseAsync(m => m.Content = "Downloaded module. Server will restart next round."); + break; + } + + case "check": + { + IEnumerable modules = await Updater.ManageUpdates(); + if (!modules.Any()) + { + await command.ModifyOriginalResponseAsync(m => m.Content = "No updates found."); + return; + } + + await command.ModifyOriginalResponseAsync(m => + m.Content = $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}"); + break; + } + } + } + + /// + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot").Select(x => new AutocompleteResult(x.Name, x.Name))); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/LocalAdmin.cs b/DiscordLab.Bot/Commands/LocalAdmin.cs deleted file mode 100644 index c0dc855..0000000 --- a/DiscordLab.Bot/Commands/LocalAdmin.cs +++ /dev/null @@ -1,67 +0,0 @@ -using CommandSystem; -using DiscordLab.Bot.API.Modules; -using PluginAPI.Core; - -namespace DiscordLab.Bot.Commands -{ - [CommandHandler(typeof(GameConsoleCommandHandler))] - public class LocalAdmin : ICommand - { - public string Command { get; } = "discordlab"; - - public string[] Aliases { get; } = new [] { "dl", "lab" }; - - public string Description { get; } = "Do things directly with DiscordLab."; - - public bool Execute( - ArraySegment arguments, - ICommandSender sender, - out string response - ) - { - switch (arguments.FirstOrDefault()) - { - case "list": - if (UpdateStatus.Statuses == null) - { - response = "No modules available. Please wait for your server to fully start."; - return false; - } - string modules = string.Join("\n", UpdateStatus.Statuses.Where(s => s.ModuleName != "DiscordLab.Bot").Select(s => s.ModuleName)); - response = - $"Available modules:\n{modules}"; - return true; - case "install": - if (UpdateStatus.Statuses == null) - { - response = "No modules available. Please wait for your server to fully start."; - return false; - } - string module = arguments.ElementAtOrDefault(1); - if(string.IsNullOrWhiteSpace(module)) - { - response = "Please provide a module name."; - return false; - } - API.Features.UpdateStatus status = UpdateStatus.Statuses.FirstOrDefault(s => string.Equals(s.ModuleName, module, StringComparison.CurrentCultureIgnoreCase)) ?? UpdateStatus.Statuses.FirstOrDefault(s => s.ModuleName.Split('.').Last().Equals(module, StringComparison.CurrentCultureIgnoreCase)); - if (status == null) - { - response = "Module not found."; - return false; - } - - Task.Run(async () => await UpdateStatus.DownloadPlugin(status)); - ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - response = "Downloaded module. Server will restart next round."; - return true; - case "check": - Task.Run(UpdateStatus.GetStatus); - response = "Checking for updates... If any require an update, you will soon receive a log."; - return true; - default: - response = "Invalid subcommand. Available subcommands: list, install"; - return false; - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs new file mode 100644 index 0000000..cef85cc --- /dev/null +++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs @@ -0,0 +1,69 @@ +namespace DiscordLab.Bot.Commands +{ + using System.Diagnostics.CodeAnalysis; + using CommandSystem; + using DiscordLab.Bot.API.Updates; + + /// + [CommandHandler(typeof(GameConsoleCommandHandler))] + public class LocalAdminCommand : ICommand + { + /// + public string Command { get; } = "discordlab"; + + /// + public string[] Aliases { get; } = ["dl", "lab"]; + + /// + public string Description { get; } = "Do things directly with DiscordLab."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, [UnscopedRef] out string response) + { + switch (arguments.FirstOrDefault()) + { + case "list": + { + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + response = "List of available DiscordLab modules:\n\n" + modules; + return true; + } + + case "install": + { + string moduleName = arguments.ElementAtOrDefault(1); + if (string.IsNullOrWhiteSpace(moduleName)) + { + response = "Please provide a module name."; + return false; + } + + Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + if (module == null) + { + response = "Module not found."; + return false; + } + + Task.Run(module.Download); + ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; + response = "Downloaded module. Server will restart next round."; + return true; + } + + case "check": + { + Task.Run(Updater.ManageUpdates); + response = "Checking for updates..."; + return true; + } + + default: + { + response = "Invalid subcommand. Available subcommands: list, install, check"; + return false; + } + } + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/Config.cs b/DiscordLab.Bot/Config.cs index 7edd9ce..ad0e518 100644 --- a/DiscordLab.Bot/Config.cs +++ b/DiscordLab.Bot/Config.cs @@ -1,17 +1,40 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; - namespace DiscordLab.Bot { - public class Config : IConfig + using System.ComponentModel; + + /// + /// The config of this plugin. + /// + public sealed class Config { - public bool IsEnabled { get; set; } = true; - public bool Debug { get; set; } = false; - [Description("The token of the bot.")] + /// + /// Gets or sets the token for the bot. + /// + [Description("The token of the bot.")] public string Token { get; set; } = "token"; - [Description("The default guild where the bot will be used. You can set this individually for each module, but if a module doesn't have a guild id set, it will use this one.")] - public ulong GuildId { get; set; } = new(); - [Description("Enable auto updates if any modules are out of date.")] + + /// + /// Gets or sets the default guild ID. + /// + [Description("The default guild ID. Each module that has their guild ID set to 0 has their guild ID set to this.")] + public ulong GuildId { get; set; } = 0; + + /// + /// Gets or sets a value indicating whether the plugin should check for DiscordLab updates. + /// + [Description("Whether the plugin should check for DiscordLab updates.")] public bool AutoUpdate { get; set; } = true; + + /// + /// Gets or sets the proxy URL. Shouldn't be set if proxy is not needed. + /// + [Description("The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")] + public string ProxyUrl { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether debug logging should be enabled. + /// + [Description("Enable debugging mode, useful to enable when needing to debug for developers.")] + public bool Debug { get; set; } = false; } } \ No newline at end of file diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index c5db30e..1f8fab3 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -3,44 +3,36 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 + + + true + true + LumiFae + DiscordLab + A modular Discord bot for SCP:SL servers running LabAPI + git + https://github.com/DiscordLabSCP/DiscordLab + README.md + + + + ../stylecop.ruleset + + - - - - - + - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.Bot/FodyWeavers.xml b/DiscordLab.Bot/FodyWeavers.xml deleted file mode 100644 index 2b3a0a7..0000000 --- a/DiscordLab.Bot/FodyWeavers.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - Discord.Net.Core - Discord.Net.Websocket - Discord.Net.Rest - Microsoft.Bcl.AsyncInterfaces - Newtonsoft.Json - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.Bot/FodyWeavers.xsd b/DiscordLab.Bot/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.Bot/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.Bot/Handlers/DiscordBot.cs b/DiscordLab.Bot/Handlers/DiscordBot.cs deleted file mode 100644 index a773b4f..0000000 --- a/DiscordLab.Bot/Handlers/DiscordBot.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Net.WebSockets; -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Features; -using UpdateStatus = DiscordLab.Bot.API.Modules.UpdateStatus; - -namespace DiscordLab.Bot.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - public DiscordSocketClient Client { get; private set; } - - private SocketGuild _guild; - - public void Init() - { - Instance = this; - DiscordSocketConfig config = new() - { - GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages, - LogLevel = Plugin.Instance.Config.Debug ? LogSeverity.Debug : LogSeverity.Warning - }; - Client = new(config); - Client.Log += DiscLog; - Client.Ready += Ready; - Client.SlashCommandExecuted += SlashCommandHandler; - Client.AutocompleteExecuted += AutoCompleteHandler; - Task.Run(StartClient); - } - - public void Unregister() - { - Task.Run(StopClient); - } - - private Task DiscLog(LogMessage msg) - { - if (msg.Exception is WebSocketException or GatewayReconnectException) - { - return Task.CompletedTask; - } - switch (msg.Severity) - { - case LogSeverity.Error or LogSeverity.Critical: - Log.Error(msg); - break; - case LogSeverity.Warning: - Log.Warn(msg); - break; - default: - Log.Info(msg); - break; - } - - return Task.CompletedTask; - } - - private async Task StartClient() - { - Log.Debug("Starting Discord bot..."); - await Client.LoginAsync(TokenType.Bot, Plugin.Instance.Config.Token); - await Client.StartAsync(); - } - - private async Task StopClient() - { - await Client.LogoutAsync(); - await Client.StopAsync(); - } - - public SocketGuild GetGuild(ulong id = 0) - { - return id == 0 ? _guild : Client.GetGuild(id); - } - - public bool IsReady; - - private async Task Ready() - { - Log.Debug("Bot is ready, getting guild and current commands."); - IsReady = true; - - _guild = Client.GetGuild(Plugin.Instance.Config.GuildId); - - // just in case a command gets added whilst the below loop is happening - List commandsSnapshot = SlashCommandLoader.Commands.ToList(); - - foreach (ISlashCommand command in commandsSnapshot) - { - await CreateGuildCommand(command); - } - } - - public async Task CreateGuildCommand(ISlashCommand command) - { - Log.Debug($"Command creation requested for {command.Data.Name}, checking..."); - try - { - SocketGuild guild = GetGuild(command.GuildId); - if (guild == null) - { - Log.Warn($"Command {command.Data.Name} failed to register, couldn't find guild {command.GuildId} (from module) nor {Plugin.Instance.Config.GuildId} (from the bot). Make sure your guild IDs are correct."); - return; - } - Log.Debug($"Found guild {guild.Id} for command {command.Data.Name}, creating..."); - await guild.CreateApplicationCommandAsync(command.Data.Build()); - } - catch (Exception e) - { - Log.Error($"Failed to create guild command '{command.Data.Name}': {e}"); - } - } - - private async Task SlashCommandHandler(SocketSlashCommand command) - { - Log.Debug($"{command.Data.Name} requested a response, finding the command..."); - ISlashCommand cmd = SlashCommandLoader.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); - if (cmd == null) return; - Log.Debug($"Found command {command.Data.Name}, responding..."); - await cmd.Run(command); - } - - private async Task AutoCompleteHandler(SocketAutocompleteInteraction autocomplete) - { - Log.Debug($"{autocomplete.Data.CommandName} requested an autocomplete response, finding the command..."); - IAutocompleteCommand cmd = (IAutocompleteCommand)SlashCommandLoader.Commands.FirstOrDefault(c => c.Data.Name == autocomplete.Data.CommandName && c is IAutocompleteCommand); - if (cmd == null) return; - Log.Debug($"Found command {autocomplete.Data.CommandName} for autocomplete response, responding..."); - await cmd.Autocomplete(autocomplete); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Bot/Plugin.cs b/DiscordLab.Bot/Plugin.cs index 4c444f5..4cb46d3 100644 --- a/DiscordLab.Bot/Plugin.cs +++ b/DiscordLab.Bot/Plugin.cs @@ -1,80 +1,62 @@ -using Discord; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using GameCore; -using Log = Exiled.API.Features.Log; -using Version = System.Version; +using DiscordLab.Bot.API.Interfaces; namespace DiscordLab.Bot { - public class Plugin : Plugin - { - public override string Name => "DiscordLab"; - public override string Author => "LumiFae"; - public override string Prefix => "DiscordLab"; - public override Version Version => new (1, 6, 1); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Higher; + using DiscordLab.Bot.API.Attributes; + using LabApi.Features; + using LabApi.Loader.Features.Plugins; + using LabApi.Loader.Features.Plugins.Enums; + /// + public sealed class Plugin : Plugin + { + /// + /// Gets the current instance of this plugin. + /// public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - public override void OnEnabled() - { - Instance = this; - - if(Config.Token is "token" or "") - { - Log.Error("Please set the bot token in the config file."); - return; - } + /// + public override string Name { get; } = "DiscordLab"; + + /// + public override string Description { get; } = "A modular Discord bot for SCP:SL servers running LabAPI"; + + /// + public override string Author { get; } = "LumiFae"; + + /// + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - try - { - TokenUtils.ValidateToken(TokenType.Bot, Config.Token); - } - catch (Exception) - { - Log.Error("Token is invalid, please put the correct token in the config file."); - return; - } + /// + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - if (Config.GuildId is 0) - { - Log.Warn("You have no guild ID set in the config file, you might get errors until you set it. " + - "If you plan on having guild IDs separate for every module then you can ignore this. " + - "For more info go to here: https://discordlab.jxtq.moe/getting-started/installation/#22-guild-id"); - } - - string restartAfterRoundsConfig = ConfigFile.ServerConfig.GetString("restart_after_rounds", "0"); + /// + public override LoadPriority Priority { get; } = LoadPriority.Highest; - if (int.TryParse(restartAfterRoundsConfig, out int restartAfterRounds) && - restartAfterRounds is >= 1 and < 10) - { - Log.Warn("You have a restart_after_rounds value set between 1 and 9, which isn't recommended. DiscordLab restarts every time your server restarts, so it's recommended" + - "to set a high number, or 0, for this value to avoid potential Discord rate limits. This is just a warning."); - } - - SlashCommandLoader.Create(); - - _handlerLoader = new (); - _handlerLoader.Load(Assembly); + /// + /// Gets the current config for the plugin. + /// + public new Config Config { get; private set; } - Task.Run(UpdateStatus.GetStatus); - - base.OnEnabled(); + /// + public override void Enable() + { + Instance = this; + Config = base.Config; + + CallOnLoadAttribute.Load(); + CallOnReadyAttribute.Load(); + + ISlashCommand.FindAll(); } - - public override void OnDisabled() + + /// + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; - - SlashCommandLoader.Destroy(); - - base.OnDisabled(); + Config = null; + Instance = null; + + CallOnUnloadAttribute.Unload(); } } } \ No newline at end of file diff --git a/DiscordLab.Bot/Properties/AssemblyInfo.cs b/DiscordLab.Bot/Properties/AssemblyInfo.cs deleted file mode 100644 index d36815b..0000000 --- a/DiscordLab.Bot/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.Bot")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.Bot")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("108A7588-D546-40F7-96A5-A46301CA7D47")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.BotStatus/Config.cs b/DiscordLab.BotStatus/Config.cs index 811e82a..9f3f0b0 100644 --- a/DiscordLab.BotStatus/Config.cs +++ b/DiscordLab.BotStatus/Config.cs @@ -1,17 +1,11 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using Exiled.API.Interfaces; +using Discord; namespace DiscordLab.BotStatus { - public class Config : IConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; + public ActivityType ActivityType { get; set; } = ActivityType.CustomStatus; - [Description("Set the Discord bot's status to orange when the server is empty.")] public bool IdleOnEmpty { get; set; } = false; } } \ No newline at end of file diff --git a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj index 5c394d1..124848c 100644 --- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj +++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj @@ -1,45 +1,16 @@ - + net48 enable disable - preview + 12 x64 true false + 2.0.0 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.BotStatus/FodyWeavers.xml b/DiscordLab.BotStatus/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.BotStatus/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.BotStatus/FodyWeavers.xsd b/DiscordLab.BotStatus/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.BotStatus/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.BotStatus/Handlers/DiscordBot.cs b/DiscordLab.BotStatus/Handlers/DiscordBot.cs deleted file mode 100644 index 0969653..0000000 --- a/DiscordLab.BotStatus/Handlers/DiscordBot.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; - -namespace DiscordLab.BotStatus.Handlers -{ - public class DiscordBot : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public static DiscordBot Instance { get; private set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - // Nothing to unregister here yippie! - } - - public void SetStatus(int? count = null) - { - count ??= Player.List.Count(p => !p.IsNPC); - string status = (count != 0 ? Translation.StatusMessage : Translation.EmptyServer).LowercaseParams() - .Replace("{current}", count.ToString()) - .Replace("{max}", Server.MaxPlayerCount.ToString()).StaticReplace(); - if (Bot.Handlers.DiscordBot.Instance.Client.Activity?.ToString().Trim() == status) return; - try - { - if (count == 0 && Plugin.Instance.Config.IdleOnEmpty) - { - Bot.Handlers.DiscordBot.Instance.Client.SetStatusAsync(UserStatus.Idle); - } - else if (Bot.Handlers.DiscordBot.Instance.Client.Status == UserStatus.Idle && count > 0) - { - Bot.Handlers.DiscordBot.Instance.Client.SetStatusAsync(UserStatus.Online); - } - - Bot.Handlers.DiscordBot.Instance.Client.SetCustomStatusAsync(status); - } - catch (Exception e) - { - Log.Error("Error setting status: " + e); - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.BotStatus/Handlers/Events.cs b/DiscordLab.BotStatus/Handlers/Events.cs deleted file mode 100644 index f4b1f10..0000000 --- a/DiscordLab.BotStatus/Handlers/Events.cs +++ /dev/null @@ -1,58 +0,0 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; - -namespace DiscordLab.BotStatus.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Server.WaitingForPlayers += OnWaitingForPlayers; - Exiled.Events.Handlers.Server.RoundStarted += OnRoundStarted; - Exiled.Events.Handlers.Player.Verified += OnPlayerVerified; - Exiled.Events.Handlers.Player.Left += OnPlayerLeave; - } - - public void Unregister() - { - Exiled.Events.Handlers.Server.WaitingForPlayers -= OnWaitingForPlayers; - Exiled.Events.Handlers.Server.RoundStarted -= OnRoundStarted; - Exiled.Events.Handlers.Player.Verified -= OnPlayerVerified; - Exiled.Events.Handlers.Player.Left -= OnPlayerLeave; - } - - private void OnPlayerVerified(VerifiedEventArgs ev) - { - if (Round.InProgress) DiscordBot.Instance.SetStatus(); - else - QueueSystem.QueueRun("DiscordLab.BotStatus.OnPlayerVerified", () => - DiscordBot.Instance.SetStatus() - ); - } - - private void OnPlayerLeave(LeftEventArgs ev) - { - int players = Player.List.Count(p => p != ev.Player && !p.IsNPC); - if (Round.InProgress || players == 0) - DiscordBot.Instance.SetStatus( - players - ); - else - QueueSystem.QueueRun("DiscordLab.BotStatus.OnPlayerLeave", () => - DiscordBot.Instance.SetStatus() - ); - } - - private void OnRoundStarted() - { - DiscordBot.Instance.SetStatus(); - } - - private void OnWaitingForPlayers() - { - DiscordBot.Instance.SetStatus(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 38e017b..48e0c66 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -1,39 +1,71 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; +using Discord; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Features; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Handlers; +using LabApi.Features; +using LabApi.Features.Wrappers; namespace DiscordLab.BotStatus { public class Plugin : Plugin { - public override string Name => "DiscordLab.BotStatus"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.BotStatus"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; - private HandlerLoader _handlerLoader; - - public override void OnEnabled() + public override string Name { get; } = "DiscordLab.BotStatus"; + public override string Description { get; } = "Allows your bot's status to update with player counts."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + + public override void Enable() { Instance = this; - - _handlerLoader = new (); - if (!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); + + PlayerEvents.Joined += OnPlayerJoin; + PlayerEvents.Left += OnPlayerLeave; } - - public override void OnDisabled() + + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; + PlayerEvents.Joined -= OnPlayerJoin; + PlayerEvents.Left -= OnPlayerLeave; - base.OnDisabled(); + Instance = null; + } + + public static void OnPlayerJoin(PlayerJoinedEventArgs _) + { + if(Round.IsRoundInProgress) + UpdateStatus(); + else + Queue.Process(); + } + + public static void OnPlayerLeave(PlayerLeftEventArgs _) + { + if(Round.IsRoundInProgress) + UpdateStatus(); + else + Queue.Process(); + } + + private static Queue Queue { get; } = new(5, UpdateStatus); + + private static void UpdateStatus() + { + TranslationBuilder builder = new(Server.PlayerCount == 0 ? Instance.Translation.EmptyContent : Instance.Translation.NormalContent); + Task.Run(async () => await Client.SocketClient.SetGameAsync(builder, type:Instance.Config.ActivityType).ConfigureAwait(false)); + switch (Server.PlayerCount) + { + case 0 when Instance.Config.IdleOnEmpty: + Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Idle).ConfigureAwait(false)); + break; + case > 0 when Instance.Config.IdleOnEmpty && + Client.SocketClient.Status == UserStatus.Idle: + Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Online).ConfigureAwait(false)); + break; + } } } } \ No newline at end of file diff --git a/DiscordLab.BotStatus/Properties/AssemblyInfo.cs b/DiscordLab.BotStatus/Properties/AssemblyInfo.cs deleted file mode 100644 index 1b0f1d9..0000000 --- a/DiscordLab.BotStatus/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.BotStatus")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.BotStatus")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("C2E4178B-327D-4D87-BAB3-6F5582F1745F")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.BotStatus/Translation.cs b/DiscordLab.BotStatus/Translation.cs index 9f31945..e273ac3 100644 --- a/DiscordLab.BotStatus/Translation.cs +++ b/DiscordLab.BotStatus/Translation.cs @@ -1,14 +1,9 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; - namespace DiscordLab.BotStatus { - public class Translation : ITranslation + public class Translation { - [Description("The message that will be sent when the match is on-going.")] - public string StatusMessage { get; set; } = "{current}/{max} currently online"; + public string EmptyContent { get; set; } = "0/{maxplayers} players online"; - [Description("The message that will be sent when the server is empty.")] - public string EmptyServer { get; set; } = "0/{max} currently online"; + public string NormalContent { get; set; } = "{playercount}/{maxplayers} players online."; } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Config.cs b/DiscordLab.ConnectionLogs/Config.cs index 072f0e4..f2c82d5 100644 --- a/DiscordLab.ConnectionLogs/Config.cs +++ b/DiscordLab.ConnectionLogs/Config.cs @@ -1,31 +1,21 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.ConnectionLogs { - public class Config : IConfig, IDLConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - [Description("The channel where the join logs will be sent.")] - public ulong JoinChannelId { get; set; } = new(); + public ulong JoinChannelId { get; set; } = 0; [Description("The channel where the leave logs will be sent.")] - public ulong LeaveChannelId { get; set; } = new(); + public ulong LeaveChannelId { get; set; } = 0; [Description("The channel where the round start logs will be sent.")] - public ulong RoundStartChannelId { get; set; } = new(); + public ulong RoundStartChannelId { get; set; } = 0; [Description("The channel where the round end logs will be sent. Optional.")] - public ulong RoundEndChannelId { get; set; } = new(); - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } + public ulong RoundEndChannelId { get; set; } = 0; + + public ulong GuildId { get; set; } = 0; } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj index 5c394d1..b6fe025 100644 --- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj +++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj @@ -3,43 +3,14 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs new file mode 100644 index 0000000..a779e3d --- /dev/null +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -0,0 +1,85 @@ +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Arguments.ServerEvents; +using LabApi.Events.CustomHandlers; +using LabApi.Features.Console; + +namespace DiscordLab.ConnectionLogs +{ + public class Events : CustomEventsHandler + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public override void OnPlayerJoined(PlayerJoinedEventArgs ev) + { + if (Config.JoinChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.JoinChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("join log", Config.JoinChannelId, Config.GuildId)); + return; + } + + channel.SendMessage(new TranslationBuilder(Translation.PlayerJoin, "player", ev.Player)); + } + + public override void OnPlayerLeft(PlayerLeftEventArgs ev) + { + if (Config.LeaveChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.LeaveChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId)); + return; + } + + channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave, "player", ev.Player)); + } + + public override void OnServerRoundStarted() + { + if (Config.RoundStartChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RoundStartChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, Config.GuildId)); + return; + } + + channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave) + { + PlayerListItem = Translation.RoundPlayers + }); + } + + public override void OnServerRoundEnded(RoundEndedEventArgs ev) + { + if (Config.RoundEndChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RoundEndChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.PlayerLeave) + { + PlayerListItem = Translation.RoundPlayers + }; + + builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + + channel.SendMessage(builder); + } + } +} \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/FodyWeavers.xml b/DiscordLab.ConnectionLogs/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.ConnectionLogs/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/FodyWeavers.xsd b/DiscordLab.ConnectionLogs/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.ConnectionLogs/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Handlers/DiscordBot.cs b/DiscordLab.ConnectionLogs/Handlers/DiscordBot.cs deleted file mode 100644 index cd102c9..0000000 --- a/DiscordLab.ConnectionLogs/Handlers/DiscordBot.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Discord; -using Discord.Rest; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Newtonsoft.Json.Linq; - -namespace DiscordLab.ConnectionLogs.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel JoinChannel { get; set; } - private SocketTextChannel LeaveChannel { get; set; } - private SocketTextChannel RoundStartChannel { get; set; } - private SocketTextChannel RoundEndChannel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - JoinChannel = null; - LeaveChannel = null; - RoundStartChannel = null; - RoundEndChannel = null; - } - - public SocketGuild GetGuild() - { - return Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - } - - public SocketTextChannel GetJoinChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.JoinChannelId == 0) return null; - return JoinChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.JoinChannelId); - } - - public SocketTextChannel GetLeaveChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.LeaveChannelId == 0) return null; - return LeaveChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.LeaveChannelId); - } - - public SocketTextChannel GetRoundStartChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.RoundStartChannelId == 0) return null; - return RoundStartChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.RoundStartChannelId); - } - - public SocketTextChannel GetRoundEndChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.RoundEndChannelId == 0) return null; - return RoundEndChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.RoundEndChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Handlers/Events.cs b/DiscordLab.ConnectionLogs/Handlers/Events.cs deleted file mode 100644 index cbc8f66..0000000 --- a/DiscordLab.ConnectionLogs/Handlers/Events.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; -using Exiled.Events.EventArgs.Server; - -namespace DiscordLab.ConnectionLogs.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Player.Verified += OnPlayerVerified; - Exiled.Events.Handlers.Player.Left += OnPlayerLeave; - Exiled.Events.Handlers.Server.RoundStarted += OnRoundStarted; - Exiled.Events.Handlers.Server.RoundEnded += OnRoundEnded; - } - - public void Unregister() - { - Exiled.Events.Handlers.Player.Verified -= OnPlayerVerified; - Exiled.Events.Handlers.Player.Left -= OnPlayerLeave; - Exiled.Events.Handlers.Server.RoundStarted -= OnRoundStarted; - Exiled.Events.Handlers.Server.RoundEnded -= OnRoundEnded; - } - - private void OnPlayerVerified(VerifiedEventArgs ev) - { - if (Round.InProgress && !string.IsNullOrEmpty(ev.Player.Nickname)) - { - string message = Plugin.Instance.Translation.PlayerJoin.LowercaseParams().Replace("{player}", ev.Player.Nickname) - .Replace("{id}", ev.Player.UserId).PlayerReplace("player", ev.Player).StaticReplace(); - SocketTextChannel channel = DiscordBot.Instance.GetJoinChannel(); - if (channel != null) channel.SendMessageAsync(message); - else - Log.Error( - "Either the guild is null or the channel is null. So the join message has failed to send."); - } - } - - private void OnPlayerLeave(LeftEventArgs ev) - { - if (Round.InProgress && !string.IsNullOrEmpty(ev.Player.Nickname)) - { - string message = Plugin.Instance.Translation.PlayerLeave.LowercaseParams().Replace("{player}", ev.Player.Nickname) - .Replace("{id}", ev.Player.UserId).PlayerReplace("player", ev.Player).StaticReplace(); - SocketTextChannel channel = DiscordBot.Instance.GetLeaveChannel(); - if (channel != null) channel.SendMessageAsync(message); - else - Log.Error( - "Either the guild is null or the channel is null. So the leave message has failed to send."); - } - } - - private void OnRoundStarted() - { - string message = Plugin.Instance.Translation.RoundStart.LowercaseParams(); - SocketTextChannel channel = DiscordBot.Instance.GetRoundStartChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the round start message has failed to send."); - return; - } - - List playerList = Player.List.Where(p => !p.IsNPC).ToList(); - string players = string.Join("\n", playerList.Select(player => - Plugin.Instance.Translation.RoundPlayers.LowercaseParams() - .Replace("{playername}", player.Nickname) - .Replace("{playerid}", player.UserId) - .Replace("{ip}", player.IPAddress) - .PlayerReplace("player", player) - .StaticReplace() - )); - channel.SendMessageAsync(message.Replace("{players}", players).StaticReplace()); - } - - private void OnRoundEnded(RoundEndedEventArgs _) - { - string message = Plugin.Instance.Translation.RoundEnd.LowercaseParams(); - if(Plugin.Instance.Config.RoundEndChannelId == 0) return; - SocketTextChannel channel = DiscordBot.Instance.GetRoundEndChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the round end message has failed to send."); - return; - } - - List playerList = Player.List.Where(p => !p.IsNPC).ToList(); - string players = string.Join("\n", playerList.Select(player => - Plugin.Instance.Translation.RoundPlayers.LowercaseParams() - .Replace("{playername}", player.Nickname) - .Replace("{playerid}", player.UserId) - .Replace("{ip}", player.IPAddress) - .PlayerReplace("player", player) - .StaticReplace() - )); - channel.SendMessageAsync(message.Replace("{players}", players).StaticReplace()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index 7494d94..06adb41 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -1,39 +1,34 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; +using DiscordLab.Bot.API.Features; +using LabApi.Events.CustomHandlers; +using LabApi.Features; namespace DiscordLab.ConnectionLogs { public class Plugin : Plugin { - public override string Name => "DiscordLab.ConnectionLogs"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.ConnectionLogs"; - public override Version Version => new (1, 5, 1); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; - private HandlerLoader _handlerLoader; + public override string Name { get; } = "DiscordLab.ConnectionLogs"; + public override string Description { get; } = "Adds logging for connection based information"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public override void OnEnabled() + public Events Events = new(); + + public override void Enable() { Instance = this; - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); + CustomHandlersManager.RegisterEventsHandler(Events); } - - public override void OnDisabled() + + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; + CustomHandlersManager.UnregisterEventsHandler(Events); + Events = null; - base.OnDisabled(); + Instance = null; } } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Properties/AssemblyInfo.cs b/DiscordLab.ConnectionLogs/Properties/AssemblyInfo.cs deleted file mode 100644 index 2057854..0000000 --- a/DiscordLab.ConnectionLogs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.ConnectionLogs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.ConnectionLogs")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3C60D907-52D8-436A-AE61-2433767AB195")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Translation.cs b/DiscordLab.ConnectionLogs/Translation.cs index d591932..9b443d3 100644 --- a/DiscordLab.ConnectionLogs/Translation.cs +++ b/DiscordLab.ConnectionLogs/Translation.cs @@ -1,15 +1,14 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.ConnectionLogs { - public class Translation : ITranslation + public class Translation { [Description("The message that will be sent when a player joins the server.")] - public string PlayerJoin { get; set; } = "`{player}` (`{id}`) has joined the server."; + public string PlayerJoin { get; set; } = "`{player}` (`{playerid}`) has joined the server."; [Description("The message that will be sent when a player leaves the server.")] - public string PlayerLeave { get; set; } = "`{player}` (`{id}`) has left the server."; + public string PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; [Description("The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] public string RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; @@ -17,7 +16,7 @@ public class Translation : ITranslation [Description("The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] public string RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; - [Description("The message that indicates what a player looks like in the round start/end message. Extra placeholder is ip, but only use that if the channel is private or risk being delisted.")] + [Description("The message that indicates what a player looks like in the round start/end message.")] public string RoundPlayers { get; set; } = "{playername} ({playerid})"; } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Config.cs b/DiscordLab.DeathLogs/Config.cs index 7f9b187..12f01fc 100644 --- a/DiscordLab.DeathLogs/Config.cs +++ b/DiscordLab.DeathLogs/Config.cs @@ -1,44 +1,32 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.DeathLogs { - public class Config : IConfig, IDLConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - [Description("The channel where the normal death logs will be sent.")] - public ulong ChannelId { get; set; } = new(); + public ulong ChannelId { get; set; } = 0; [Description( "The channel where the death logs of cuffed players will be sent. Keep as default value to disable. Disabling this will make it so logs are only sent to the normal death logs channel, but without the cuffed identifier.")] - public ulong CuffedChannelId { get; set; } = new(); + public ulong CuffedChannelId { get; set; } = 0; [Description( "The channel where logs will be sent when a player dies by their own actions, or just they died because of something else.")] - public ulong SelfChannelId { get; set; } = new(); + public ulong SelfChannelId { get; set; } = 0; [Description("The channel where logs will be sent when a player dies by a teamkill.")] - public ulong TeamKillChannelId { get; set; } = new(); + public ulong TeamKillChannelId { get; set; } = 0; [Description("If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")] public bool ScpIgnoreCuffed { get; set; } = true; [Description("The channel to send death logs to, if any.")] - public ulong DamageLogChannelId { get; set; } = new(); + public ulong DamageLogChannelId { get; set; } = 0; - [Description("Whether damage logs shouldn't be tracked if the attacker is an SCP. Not recommended because this can hide team killing on the SCP team.")] + [Description("Whether damage logs shouldn't be tracked if the attacker is an SCP.")] public bool IgnoreScpDamage { get; set; } = false; - [Description("The hex color code of the embed for damage logs, do not include the #.")] - public string DamageLogEmbedColor = "3498DB"; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } + public ulong GuildId { get; set; } = 0; } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs new file mode 100644 index 0000000..bee7993 --- /dev/null +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using CustomPlayerEffects; +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using GameCore; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Handlers; +using LabApi.Features.Console; +using PlayerStatsSystem; + +namespace DiscordLab.DeathLogs +{ + public static class DamageLogs + { + public static List DamageLogEntries { get; set; } = new(); + + private static Queue queue = new(5, SendLog); + + [CallOnLoad] + public static void Register() + { + if (Plugin.Instance.Config.DamageLogChannelId == 0) return; + PlayerEvents.Hurt += OnHurt; + } + + [CallOnUnload] + public static void Unregister() + { + if (Plugin.Instance.Config.DamageLogChannelId == 0) return; + PlayerEvents.Hurt -= OnHurt; + } + + public static void OnHurt(PlayerHurtEventArgs ev) + { + if (ev.Attacker == null || ev.Player == ev.Attacker) return; + + if (ev.DamageHandler is not StandardDamageHandler handler) + return; + + if (handler.Damage <= 0) return; + + string type = Events.ConvertToString(ev.DamageHandler); + + // passive damage checkers, don't want these spamming console. + if (type == "Cardiac Arrest") return; + if (ev.Player.HasEffect() && type == "SCP-106") return; + if (ev.Player.HasEffect() && type == "SCP-106") return; + if (type == "Strangled") return; + + if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) return; + + string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) + .AddCustomReplacer("cause", type); + + DamageLogEntries.Add(log); + + queue.Process(); + } + + public static void SendLog() + { + if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.DamageLogChannelId, out SocketTextChannel channel)) + { + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage( + "damage logs", + Plugin.Instance.Config.DamageLogChannelId, + Plugin.Instance.Config.GuildId)); + return; + } + + channel.SendMessage(embeds:CreateEmbeds()); + + DamageLogEntries.Clear(); + } + + public static Embed[] CreateEmbeds() + { + List embeds = new(); + + if (DamageLogEntries.Count == 0) + return embeds.ToArray(); + + int currentIndex = 0; + + while (currentIndex < DamageLogEntries.Count) + { + EmbedBuilder embed = Plugin.Instance.Translation.DamageLogEmbed; + + List currentEmbedLogs = new(); + int currentLength = 0; + + while (currentIndex < DamageLogEntries.Count) + { + string logEntry = DamageLogEntries[currentIndex]; + + int newLength = currentLength + logEntry.Length + (currentEmbedLogs.Count > 0 ? 1 : 0); + + if (newLength > EmbedBuilder.MaxDescriptionLength && currentEmbedLogs.Count > 0) + break; + + if (logEntry.Length > EmbedBuilder.MaxDescriptionLength) + { + logEntry = logEntry.Substring(0, EmbedBuilder.MaxDescriptionLength - 3) + "..."; + currentEmbedLogs.Add(logEntry); + currentIndex++; + break; + } + + currentEmbedLogs.Add(logEntry); + currentLength = newLength; + currentIndex++; + } + + if (currentEmbedLogs.Count <= 0) continue; + embed.Description = new TranslationBuilder(embed.Description).AddCustomReplacer("entries", string.Join("\n", currentEmbedLogs)); + embeds.Add(embed.Build()); + } + + return embeds.ToArray(); + } + } +} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj index 5c394d1..b6fe025 100644 --- a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj +++ b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj @@ -3,43 +3,14 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Events.cs b/DiscordLab.DeathLogs/Events.cs new file mode 100644 index 0000000..f279d4d --- /dev/null +++ b/DiscordLab.DeathLogs/Events.cs @@ -0,0 +1,226 @@ +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Handlers; +using LabApi.Features.Console; +using PlayerRoles; +using PlayerRoles.PlayableScps.Scp1507; +using PlayerRoles.PlayableScps.Scp3114; +using PlayerRoles.PlayableScps.Scp939; +using PlayerStatsSystem; + +namespace DiscordLab.DeathLogs +{ + public static class Events + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + // have to do this here over CustomEventsHandler because easier to maintain different logs in this case. + [CallOnLoad] + public static void Register() + { + PlayerEvents.Death += OnTeamKill; + PlayerEvents.Death += OnCuffKill; + PlayerEvents.Death += OnDeath; + PlayerEvents.Death += OnOwnDeath; + } + + [CallOnUnload] + public static void Unregister() + { + PlayerEvents.Death -= OnTeamKill; + PlayerEvents.Death -= OnCuffKill; + PlayerEvents.Death -= OnDeath; + PlayerEvents.Death -= OnOwnDeath; + } + + public static void OnTeamKill(PlayerDeathEventArgs ev) + { + if (ev.Attacker == null || ev.Attacker.Team.GetFaction() != ev.Player.Team.GetFaction()) + return; + + if (Config.TeamKillChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.TeamKillChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("team kill logs", Config.TeamKillChannelId, Config.GuildId)); + + return; + } + + TranslationBuilder builder = new TranslationBuilder(Translation.TeamKill) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)) + .AddCustomReplacer("role", ev.Player.Team.GetFaction().ToString()); + + channel.SendMessage(builder); + } + + public static void OnCuffKill(PlayerDeathEventArgs ev) + { + if (ev.Attacker == null || !ev.Player.IsDisarmed || (ev.Attacker.IsSCP && Config.ScpIgnoreCuffed)) + return; + + if (Config.CuffedChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuff kill logs", Config.CuffedChannelId, Config.GuildId)); + + return; + } + + TranslationBuilder builder = new TranslationBuilder(Translation.CuffedPlayerDeath) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + + channel.SendMessage(builder); + } + + public static void OnDeath(PlayerDeathEventArgs ev) + { + if (ev.Attacker == null || ev.Player.IsDisarmed || + ev.Attacker.Team.GetFaction() == ev.Player.Team.GetFaction()) + return; + + if (Config.ChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.ChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("kill logs", Config.ChannelId, Config.GuildId)); + + return; + } + + TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeath) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + + channel.SendMessage(builder); + } + + public static void OnOwnDeath(PlayerDeathEventArgs ev) + { + if (ev.Attacker != null) + return; + + if (Config.SelfChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.SelfChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("self kill logs", Config.SelfChannelId, Config.GuildId)); + + return; + } + + TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeathSelf) + .AddPlayer("player", ev.Player) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + + channel.SendMessage(builder); + } + + internal static string ConvertToString(DamageHandlerBase handler) + { + Dictionary translations = new() + { + { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, + { DeathTranslations.Bleeding.Id, "Bleeding" }, + { DeathTranslations.Crushed.Id, "Crushed" }, + { DeathTranslations.Decontamination.Id, "Decontamination" }, + { DeathTranslations.Explosion.Id, "Explosion" }, + { DeathTranslations.Falldown.Id, "Falldown" }, + { DeathTranslations.Poisoned.Id, "Poison" }, + { DeathTranslations.Recontained.Id, "Recontainment" }, + { DeathTranslations.Scp049.Id, "SCP-049" }, + { DeathTranslations.Scp096.Id, "SCP-096" }, + { DeathTranslations.Scp173.Id, "SCP-173" }, + { DeathTranslations.Scp207.Id, "SCP-207" }, + { DeathTranslations.Scp939Lunge.Id, "SCP-939 Lunge" }, + { DeathTranslations.Scp939Other.Id, "SCP-939" }, + { DeathTranslations.Scp3114Slap.Id, "SCP-3114" }, + { DeathTranslations.Tesla.Id, "Tesla" }, + { DeathTranslations.Unknown.Id, "Unknown" }, + { DeathTranslations.Warhead.Id, "Warhead" }, + { DeathTranslations.Zombie.Id, "SCP-049-2" }, + { DeathTranslations.BulletWounds.Id, "Firearm" }, + { DeathTranslations.PocketDecay.Id, "Pocket Decay" }, + { DeathTranslations.SeveredHands.Id, "Severed Hands" }, + { DeathTranslations.FriendlyFireDetector.Id, "Friendly Fire" }, + { DeathTranslations.UsedAs106Bait.Id, "Femur Breaker" }, + { DeathTranslations.MicroHID.Id, "Micro H.I.D." }, + { DeathTranslations.Hypothermia.Id, "Hypothermia" }, + { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, + { DeathTranslations.Scp1344.Id, "Severed Eyes" }, + }; + + switch (handler) + { + case CustomReasonDamageHandler: + return "Unknown, plugin specific death."; + case WarheadDamageHandler: + return "Warhead"; + case ExplosionDamageHandler: + return "Explosion"; + case Scp018DamageHandler: + return "SCP-018"; + case RecontainmentDamageHandler: + return "Recontainment"; + case MicroHidDamageHandler: + return "Micro H.I.D."; + case DisruptorDamageHandler: + return "Particle Disruptor"; + case Scp939DamageHandler: + return "SCP-939"; + case JailbirdDamageHandler: + return "Jailbird"; + case Scp1507DamageHandler: + return "SCP-1507"; + case Scp956DamageHandler: + return "SCP-956"; + case SnowballDamageHandler: + return "Snowball"; + case Scp3114DamageHandler scp3114DamageHandler: + return scp3114DamageHandler.Subtype switch + { + Scp3114DamageHandler.HandlerType.Strangulation => "Strangled", + Scp3114DamageHandler.HandlerType.SkinSteal => "SCP-3114", + Scp3114DamageHandler.HandlerType.Slap => "SCP-3114", + _ => "Unknown", + }; + case Scp049DamageHandler scp049DamageHandler: + return scp049DamageHandler.DamageSubType switch + { + Scp049DamageHandler.AttackType.CardiacArrest => "Cardiac Arrest", + Scp049DamageHandler.AttackType.Instakill => "SCP-049", + Scp049DamageHandler.AttackType.Scp0492 => "SCP-049-2", + _ => "Unknown", + }; + case UniversalDamageHandler universal: + { + DeathTranslation translation = DeathTranslations.TranslationsById[universal.TranslationId]; + + if (translations.TryGetValue(translation.Id, out string s)) + return s; + + break; + } + } + + return "Unknown"; + } + } +} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/FodyWeavers.xml b/DiscordLab.DeathLogs/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.DeathLogs/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.DeathLogs/FodyWeavers.xsd b/DiscordLab.DeathLogs/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.DeathLogs/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Handlers/DamageHandler.cs b/DiscordLab.DeathLogs/Handlers/DamageHandler.cs deleted file mode 100644 index 3df6099..0000000 --- a/DiscordLab.DeathLogs/Handlers/DamageHandler.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Globalization; -using CustomPlayerEffects; -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Enums; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; -using PlayerStatsSystem; - -namespace DiscordLab.DeathLogs.Handlers -{ - public class DamageHandler : IRegisterable - { - public static List DamageLogs { get; set; } = new(); - - public void Init() - { - if (Plugin.Instance.Config.DamageLogChannelId == 0) return; - - Exiled.Events.Handlers.Player.Hurt += OnHurt; - } - - public void Unregister() - { - if (Plugin.Instance.Config.DamageLogChannelId == 0) return; - - Exiled.Events.Handlers.Player.Hurt -= OnHurt; - } - - public static void OnHurt(HurtEventArgs ev) - { - if (ev.Player == null || ev.Attacker == null || ev.Player == ev.Attacker) return; - - if (ev.Amount <= 0) return; - - // passive damage checkers, don't want these spamming console. - if (ev.DamageHandler.Base is Scp049DamageHandler { DamageSubType: Scp049DamageHandler.AttackType.CardiacArrest }) return; - if (ev.Player.IsEffectActive() && ev.DamageHandler.Type == DamageType.Scp106) return; - if (ev.Player.IsEffectActive() && ev.DamageHandler.Type == DamageType.Scp106) return; - if (ev.DamageHandler.Type == DamageType.Strangled) return; - - if (ev.Player.IsScp && ev.Attacker.IsScp && Plugin.Instance.Config.IgnoreScpDamage) return; - - string log = Plugin.Instance.Translation.DamageLogEntry.LowercaseParams() - .Replace("{cause}", Events.ConvertToString(ev.DamageHandler.Type)) - .Replace("{damage}", ev.Amount.ToString(CultureInfo.CurrentCulture)) - .StaticReplace() - .PlayerReplace("attacker", ev.Attacker) - .PlayerReplace("player", ev.Player); - - DamageLogs.Add(log); - - QueueSystem.QueueRun($"DiscordLab.DeathLogs.Handlers.DamageHandler", SendLog); - } - - public static void SendLog() - { - ChannelReturn channelReturn = DiscordBot.Instance.GetGuild().TryGetTextChannel(Plugin.Instance.Config.DamageLogChannelId, out SocketTextChannel channel); - if (channelReturn != ChannelReturn.Found) - { - switch (channelReturn) - { - case ChannelReturn.InvalidChannel or ChannelReturn.NoChannel: - { - Log.Error("Could not find damage logs channel, try putting in the ID again."); - return; - } - case ChannelReturn.InvalidGuild or ChannelReturn.NoGuild: - { - Log.Error("Could not find damage logs channel because the guild you inputted is invalid, try putting in the ID for your guild again."); - return; - } - case ChannelReturn.InvalidType: - { - Log.Error("You input an invalid channel to damage logs, make sure you put in the correct ID."); - return; - } - } - - return; - } - - channel.SendMessageAsync(embeds:CreateEmbeds()); - - DamageLogs.Clear(); - } - - public static Embed[] CreateEmbeds() - { - List embeds = new(); - - if (DamageLogs.Count == 0) - return embeds.ToArray(); - - int currentIndex = 0; - - while (currentIndex < DamageLogs.Count) - { - EmbedBuilder embed = new() - { - Color = Plugin.Instance.Config.DamageLogEmbedColor.GetColor(), - Title = Plugin.Instance.Translation.DamageLogEmbedTitle - }; - - List currentEmbedLogs = new(); - int currentLength = 0; - - while (currentIndex < DamageLogs.Count) - { - string logEntry = DamageLogs[currentIndex]; - - int newLength = currentLength + logEntry.Length + (currentEmbedLogs.Count > 0 ? 1 : 0); - - if (newLength > EmbedBuilder.MaxDescriptionLength && currentEmbedLogs.Count > 0) - break; - - if (logEntry.Length > EmbedBuilder.MaxDescriptionLength) - { - logEntry = logEntry.Substring(0, EmbedBuilder.MaxDescriptionLength - 3) + "..."; - currentEmbedLogs.Add(logEntry); - currentIndex++; - break; - } - - currentEmbedLogs.Add(logEntry); - currentLength = newLength; - currentIndex++; - } - - if (currentEmbedLogs.Count <= 0) continue; - embed.Description = string.Join("\n", currentEmbedLogs); - embeds.Add(embed.Build()); - } - - return embeds.ToArray(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Handlers/DiscordBot.cs b/DiscordLab.DeathLogs/Handlers/DiscordBot.cs deleted file mode 100644 index 9c9e2ff..0000000 --- a/DiscordLab.DeathLogs/Handlers/DiscordBot.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.DeathLogs.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel Channel { get; set; } - private SocketTextChannel CuffedChannel { get; set; } - private SocketTextChannel SelfChannel { get; set; } - private SocketTextChannel TeamKillChannel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - Channel = null; - CuffedChannel = null; - SelfChannel = null; - TeamKillChannel = null; - } - - public SocketGuild GetGuild() - { - return Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - } - - public SocketTextChannel GetChannel() - { - SocketGuild guild = GetGuild(); - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.ChannelId == 0) return null; - return Channel ??= guild.GetTextChannel(Plugin.Instance.Config.ChannelId); - } - - public SocketTextChannel GetCuffedChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.CuffedChannelId == 0) return null; - return CuffedChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.CuffedChannelId); - } - - public SocketTextChannel GetSelfChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.SelfChannelId == 0) return null; - return SelfChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.SelfChannelId); - } - - public SocketTextChannel GetTeamKillChannel() - { - SocketGuild guild = GetGuild(); - if (guild == null) return null; - if (Plugin.Instance.Config.TeamKillChannelId == 0) return null; - return TeamKillChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.TeamKillChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Handlers/Events.cs b/DiscordLab.DeathLogs/Handlers/Events.cs deleted file mode 100644 index 7fb3cf5..0000000 --- a/DiscordLab.DeathLogs/Handlers/Events.cs +++ /dev/null @@ -1,192 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Enums; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; - -namespace DiscordLab.DeathLogs.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Player.Dying += OnTeamKillDeath; - Exiled.Events.Handlers.Player.Dying += OnCuffKillDeath; - Exiled.Events.Handlers.Player.Dying += OnNormalDeath; - Exiled.Events.Handlers.Player.Dying += OnSuicide; - } - - public void Unregister() - { - Exiled.Events.Handlers.Player.Dying -= OnTeamKillDeath; - Exiled.Events.Handlers.Player.Dying -= OnCuffKillDeath; - Exiled.Events.Handlers.Player.Dying -= OnNormalDeath; - Exiled.Events.Handlers.Player.Dying -= OnSuicide; - } - - private void OnTeamKillDeath(DyingEventArgs ev) - { - if (ev.Attacker == null) return; - if (ev.Attacker == ev.Player) return; - if (ev.Attacker.Role.Team != ev.Player.Role.Team) return; - if(Plugin.Instance.Config.TeamKillChannelId == 0) return; - SocketTextChannel channel = DiscordBot.Instance.GetTeamKillChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the death message has failed to send."); - return; - } - - channel.SendMessageAsync( - Plugin.Instance.Translation.TeamKill.LowercaseParams() - .Replace("{player}", ev.Player.Nickname) - .Replace("{attacker}", ev.Attacker.Nickname) - .Replace("{role}",ev.Player.Role.Name) - .Replace("{playerid}", ev.Player.UserId) - .Replace("{attackerid}", ev.Attacker.UserId) - .Replace("{cause}", ConvertToString(ev.DamageHandler.Type)) - .PlayerReplace("player", ev.Player) - .PlayerReplace("attacker", ev.Attacker) - .StaticReplace() - ); - } - - private void OnCuffKillDeath(DyingEventArgs ev) - { - if(ev.Attacker == null) return; - if(ev.Attacker == ev.Player) return; - if(ev.Attacker.IsScp && Plugin.Instance.Config.ScpIgnoreCuffed) return; - if(!ev.Player.IsCuffed) return; - if (Plugin.Instance.Config.CuffedChannelId == 0) - { - ev.Player.Cuffer = null; - OnNormalDeath(ev); - return; - } - SocketTextChannel channel = DiscordBot.Instance.GetCuffedChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the death message has failed to send."); - return; - } - - channel.SendMessageAsync( - Plugin.Instance.Translation.CuffedPlayerDeath.LowercaseParams() - .Replace("{player}", ev.Player.Nickname) - .Replace("{attacker}", ev.Attacker.Nickname) - .Replace("{playerrole}", ev.Player.Role.Name) - .Replace("{attackerrole}", ev.Attacker.Role.Name) - .Replace("{playerid}", ev.Player.UserId) - .Replace("{attackerid}", ev.Attacker.UserId) - .Replace("{cause}", ConvertToString(ev.DamageHandler.Type)) - .PlayerReplace("player", ev.Player) - .PlayerReplace("attacker", ev.Attacker) - .PlayerReplace("cuffer", ev.Player.Cuffer) - .StaticReplace() - ); - - } - - private void OnNormalDeath(DyingEventArgs ev) - { - if(ev.Attacker == null) return; - if(ev.Attacker == ev.Player) return; - if(ev.Attacker.Role.Team == ev.Player.Role.Team) return; - if(ev.Player.IsCuffed) return; - if (Plugin.Instance.Config.ChannelId == 0) return; - SocketTextChannel channel = DiscordBot.Instance.GetChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the death message has failed to send."); - return; - } - channel.SendMessageAsync( - Plugin.Instance.Translation.PlayerDeath.LowercaseParams() - .Replace("{player}", ev.Player.Nickname) - .Replace("{attacker}", ev.Attacker.Nickname) - .Replace("{playerrole}", ev.Player.Role.Name) - .Replace("{attackerrole}", ev.Attacker.Role.Name) - .Replace("{playerid}", ev.Player.UserId) - .Replace("{attackerid}", ev.Attacker.UserId) - .Replace("{cause}", ConvertToString(ev.DamageHandler.Type)) - .PlayerReplace("player", ev.Player) - .PlayerReplace("attacker", ev.Attacker) - .StaticReplace() - ); - } - - private void OnSuicide(DyingEventArgs ev) - { - if (ev.Attacker != null && ev.Attacker != ev.Player) return; - if (Plugin.Instance.Config.SelfChannelId == 0) return; - SocketTextChannel channel = DiscordBot.Instance.GetSelfChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the death message has failed to send."); - return; - } - channel.SendMessageAsync( - Plugin.Instance.Translation.PlayerDeathSelf.LowercaseParams() - .Replace("{player}", ev.Player.Nickname) - .Replace("{playerrole}", ev.Player.Role.Name) - .Replace("{playerid}", ev.Player.UserId) - .Replace("{cause}", ConvertToString(ev.DamageHandler.Type)) - .PlayerReplace("player", ev.Player) - .StaticReplace() - ); - } - - internal static string ConvertToString(DamageType type) => type switch - { - DamageType.A7 => "A7", - DamageType.Unknown => "Unknown", - DamageType.Falldown => "Fell down", - DamageType.Warhead => "Warhead explosion", - DamageType.Decontamination => "Decontamination", - DamageType.Asphyxiation => "Asphyxiation", - DamageType.Poison => "Poison", - DamageType.Bleeding => "Bleeding", - DamageType.Firearm => "Unknown Firearm", - DamageType.MicroHid => "Micro HID", - DamageType.Tesla => "Tesla Gate", - DamageType.Scp => "Unknown SCP", - DamageType.Explosion => "Explosion", - DamageType.Scp018 => "SCP-018", - DamageType.Scp207 => "SCP-207", - DamageType.Recontainment => "Recontainment", - DamageType.Crushed => "Crushed", - DamageType.FemurBreaker => "Femur Breaker", - DamageType.PocketDimension => "Pocket Dimension", - DamageType.FriendlyFireDetector => "Friendly Fire", - DamageType.SeveredHands => "Severed Hands", - DamageType.SeveredEyes => "Severed Eyes", - DamageType.Custom => "Unknown, plugin specific death.", - DamageType.Scp049 => "SCP-049", - DamageType.Scp096 => "SCP-096", - DamageType.Scp173 => "SCP-173", - DamageType.Scp939 => "SCP-939", - DamageType.Scp0492 => "SCP-049-2", - DamageType.Scp106 => "SCP-106", - DamageType.Crossvec => "Cross-vec", - DamageType.Logicer => "Logicer", - DamageType.Revolver => "Revolver", - DamageType.Shotgun => "Shotgun", - DamageType.AK => "AK", - DamageType.Com15 => "COM-15", - DamageType.Com18 => "COM-18", - DamageType.Fsp9 => "FSP-9", - DamageType.E11Sr => "E11-SR", - DamageType.Hypothermia => "Hypothermia", - DamageType.ParticleDisruptor => "Particle Disruptor", - DamageType.CardiacArrest => "Cardiac Arrest", - DamageType.Com45 => "COM-45", - DamageType.Jailbird => "Jailbird", - DamageType.Frmg0 => "FR-MG-0", - DamageType.Scp3114 => "SCP-3114", - DamageType.Strangled => "Strangled", - DamageType.Marshmallow => "Marshmallow", - _ => type.ToString() - }; - } -} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index e5830db..cae56d6 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -1,39 +1,31 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using LabApi.Features; namespace DiscordLab.DeathLogs { public class Plugin : Plugin { - public override string Name => "DiscordLab.DeathLogs"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.DeathLogs"; - public override Version Version => new (1, 6, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; - private HandlerLoader _handlerLoader; - - public override void OnEnabled() + public override string Name { get; } = "DiscordLab.DeathLogs"; + public override string Description { get; } = "Adds death logging capabilities"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + + public override void Enable() { Instance = this; - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); + CallOnLoadAttribute.Load(); } - - public override void OnDisabled() + + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; + CallOnUnloadAttribute.Unload(); - base.OnDisabled(); + Instance = null; } } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Properties/AssemblyInfo.cs b/DiscordLab.DeathLogs/Properties/AssemblyInfo.cs deleted file mode 100644 index c4a46d2..0000000 --- a/DiscordLab.DeathLogs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.DeathLogs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.DeathLogs")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("175B1499-7F87-4ED4-BFEE-F6E0207E4CD8")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index f526135..2fca197 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -1,17 +1,17 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; +using System.ComponentModel; +using Discord; namespace DiscordLab.DeathLogs { - public class Translation : ITranslation + public class Translation { [Description("The message that will be sent when a player dies.")] public string PlayerDeath { get; set; } = - "`{player}` (`{playerrole}`) has been killed by `{attacker}` as `{attackerrole}`. They died from: `{cause}`"; + "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}`. They died from: `{cause}`"; [Description("The message that will be sent when a cuffed player dies, unless the cuffed channel is disabled.")] public string CuffedPlayerDeath { get; set; } = - "`{player}` (`{playerrole}`) has been killed by `{attacker}` as `{attackerrole}` while cuffed. They died from: `{cause}`"; + "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}` while cuffed. They died from: `{cause}`"; [Description( "The message that will be sent when a player dies by their own actions, or just they died because of something else.")] @@ -19,12 +19,16 @@ public class Translation : ITranslation [Description("The message that will be sent when a player dies due to someone on their own team.")] public string TeamKill { get; set; } = - "`{player}` has been team-killed by `{attacker}`, they were both {role}. They died from: `{cause}`"; + "`{target}` has been team-killed by `{player}`, they were both {role}. They died from: `{cause}`"; - [Description("The title of the embed for when sending damage logs")] - public string DamageLogEmbedTitle { get; set; } = "Damage Log"; + [Description("The embed for when sending damage logs. Entries will be replaced with the entries below.")] + public EmbedBuilder DamageLogEmbed { get; set; } = new() + { + Title = "Damage Logs", + Description = "{entries}" + }; [Description("What each instance of damage will look like in the logs.")] - public string DamageLogEntry { get; set; } = "{timet} | `{attacker}` did `{damage}` damage to `{player}` | Cause: `{cause}`"; + public string DamageLogEntry { get; set; } = "{timetlong} | `{player}` did `{damage}` damage to `{target}` | Cause: `{cause}`"; } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index c0bad7d..e83820f 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -1,76 +1,66 @@ -using Discord; +using Discord; using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Moderation.Handlers; -using Exiled.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Features.Wrappers; namespace DiscordLab.Moderation.Commands { - public class Ban : ISlashCommand + public class Ban : IAutocompleteCommand { - private static Translation Translation => Plugin.Instance.Translation; - - public SlashCommandBuilder Data { get; } = new() + public SlashCommandBuilder Data { - Name = Translation.BanCommandName, - Description = Translation.BanCommandDescription, - DefaultMemberPermissions = GuildPermission.BanMembers, - Options = new() + get { - new() - { - Name = Translation.BanCommandUserOptionName, - Description = Translation.BanCommandUserOptionDescription, - IsRequired = true, - Type = ApplicationCommandOptionType.String - }, - new() - { - Name = Translation.BanCommandReasonOptionName, - Description = Translation.BanCommandReasonOptionDescription, - IsRequired = true, - Type = ApplicationCommandOptionType.String - }, - new() - { - Name = Translation.BanCommandDurationOptionName, - Description = Translation.BanCommandDurationOptionDescription, - IsRequired = true, - Type = ApplicationCommandOptionType.String - } + SlashCommandBuilder builder = Plugin.SetupDurationBuilder(Plugin.Instance.Translation.BanCommand, true); + SlashCommandOptionBuilder option = builder.Options[2]; + option.IsRequired = true; + option.IsAutocomplete = false; + option.Type = ApplicationCommandOptionType.String; + + return builder; } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; + } + public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public async Task Run(SocketSlashCommand command) { - await command.DeferAsync(true); - - string user = command.Data.Options.First(option => option.Name == Translation.BanCommandUserOptionName) - .Value.ToString(); - string reason = command.Data.Options.First(option => option.Name == Translation.BanCommandReasonOptionName) - .Value.ToString(); - string duration = command.Data.Options - .First(option => option.Name == Translation.BanCommandDurationOptionName).Value.ToString(); + await command.DeferAsync(); - string response = Server.ExecuteCommand($"/oban {user} {duration} {reason}"); - if (!response.Contains("has been banned")) + string userId = (string)command.Data.Options.ElementAt(0).Value; + long duration = Misc.RelativeTimeToSeconds((string)command.Data.Options.ElementAt(1).Value, 60); + string reason = (string)command.Data.Options.ElementAt(2).Value; + + TranslationBuilder successBuilder = new(Plugin.Instance.Translation.BanSuccess) { - await command.ModifyOriginalResponseAsync(m=> m.Content = Translation.FailedExecuteCommand.LowercaseParams().Replace("{reason}", response)); - } - else + Time = TempMuteManager.GetExpireDate(duration) + }; + TranslationBuilder failBuilder = new(Plugin.Instance.Translation.BanFailure); + + successBuilder.CustomReplacers.Add("userid", () => userId); + failBuilder.CustomReplacers.Add("userid", () => userId); + + if (!CommandUtils.TryGetPlayerFromUnparsed(userId, out Player player)) { - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.BanCommandSuccess.LowercaseParams().Replace("{player}", user)); - if (ModerationLogsHandler.Instance.IsEnabled) - { - ModerationLogsHandler.Instance.SendBanLogMethod.Invoke( - ModerationLogsHandler.Instance.HandlerInstance, - new object[] { null, user, reason, $"<@{command.User.Id}>", null, duration } - ); - } + bool result = userId.Contains("@") ? + Server.BanUserId(userId, reason, duration) : + Server.BanIpAddress(userId, reason, duration); + + await command.ModifyOriginalResponseAsync(m => + m.Content = !result ? failBuilder : successBuilder); + + return; } + + await command.ModifyOriginalResponseAsync(m => + m.Content = Server.BanPlayer(player, reason, duration) ? successBuilder : failBuilder); + } + + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs new file mode 100644 index 0000000..7551999 --- /dev/null +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -0,0 +1,58 @@ +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Bot.API.Utilities; +using LabApi.Features.Wrappers; +using VoiceChat; + +namespace DiscordLab.Moderation.Commands +{ + public class Mute : IAutocompleteCommand + { + public SlashCommandBuilder Data { get; } = Plugin.SetupDurationBuilder(Plugin.Instance.Translation.MuteCommand); + + public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + + public async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); + + if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) + { + await command.ModifyOriginalResponseAsync(m => m.Content = Plugin.Instance.Translation.InvalidUser); + return; + } + + TranslationBuilder builder; + + if (command.Data.Options.Count == 2) + { + string duration = (string)command.Data.Options.ElementAt(1).Value; + DateTime time = TempMuteManager.GetExpireDate(duration); + TempMuteManager.MutePlayer(player, time); + + builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", player) + { + Time = time + }; + + builder.CustomReplacers.Add("duration", () => duration); + + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + return; + } + + VoiceChatMutes.IssueLocalMute(player.UserId); + + builder = new(Plugin.Instance.Translation.PermMuteSuccess, "player", player); + + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + } + + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/SendCommand.cs b/DiscordLab.Moderation/Commands/SendCommand.cs deleted file mode 100644 index e39e23c..0000000 --- a/DiscordLab.Moderation/Commands/SendCommand.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; - -namespace DiscordLab.Moderation.Commands -{ - public class SendCommand : ISlashCommand - { - private static Translation Translation => Plugin.Instance.Translation; - - public SlashCommandBuilder Data { get; } = new() - { - Name = Translation.SendCommandName, - Description = Translation.SendCommandDescription, - DefaultMemberPermissions = GuildPermission.ManageGuild, - Options = new() - { - new() - { - Name = Translation.SendCommandCommandOptionName, - Description = Translation.SendCommandCommandOptionDescription, - IsRequired = true, - Type = ApplicationCommandOptionType.String - } - } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; - - public async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(true); - - string commandToExecute = command.Data.Options.First(option => option.Name == Translation.SendCommandCommandOptionName) - .Value.ToString(); - - string response = Server.ExecuteCommand(commandToExecute); - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.SendCommandResponse.LowercaseParams().Replace("{response}", response)); - } - } -} \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs new file mode 100644 index 0000000..8620fe2 --- /dev/null +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -0,0 +1,61 @@ +using CommandSystem; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Features.Wrappers; + +namespace DiscordLab.Moderation.Commands +{ + [CommandHandler(typeof(RemoteAdminCommandHandler))] + public class TempMuteRemoteAdmin : ICommand, IUsageProvider + { + public string Command { get; } = "tempmute"; + public string[] Aliases { get; } = ["tempm", "mutet"]; + public string Description { get; } = "Temporarily mutes a user."; + + public string[] Usage { get; } = + [ + "{player}", + "{duration}" + ]; + + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission([ + PlayerPermissions.BanningUpToDay, + PlayerPermissions.LongTermBanning, + PlayerPermissions.PlayersManagement + ], out response)) + return false; + + if (arguments.Count < 2) + { + response = "To execute this command provide at least 2 arguments!\nUsage: " + this.DisplayCommandUsage(); + return false; + } + + if (!Player.TryGet(sender, out Player player)) + { + player = Server.Host; + } + + if (!CommandUtils.TryGetPlayerFromUnparsed(arguments.At(0), out Player target)) + { + response = Plugin.Instance.Translation.InvalidUser; + } + + DateTime time = TempMuteManager.GetExpireDate(arguments.At(1)); + + TempMuteManager.MutePlayer(target, time, player); + + TranslationBuilder builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", target) + { + Time = time + }; + + builder.CustomReplacers.Add("duration", () => arguments.At(1)); + + response = builder; + return true; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index 59e1fbf..c0df3ee 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -1,58 +1,53 @@ -using Discord; +using Discord; using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Moderation.Handlers; -using Exiled.API.Features; namespace DiscordLab.Moderation.Commands { - public class Unban : ISlashCommand + public class Unban : IAutocompleteCommand { - private static Translation Translation => Plugin.Instance.Translation; - - public SlashCommandBuilder Data { get; } = new() + public SlashCommandBuilder Data { - Name = Translation.UnbanCommandName, - Description = Translation.UnbanCommandDescription, - DefaultMemberPermissions = GuildPermission.ManageGuild, - Options = new() + get { - new() - { - Name = Translation.UnbanCommandUserOptionName, - Description = Translation.UnbanCommandUserOptionDescription, - IsRequired = true, - Type = ApplicationCommandOptionType.String - } + SlashCommandBuilder builder = Plugin.Instance.Translation.UnbanCommand; + SlashCommandOptionBuilder option = builder.Options[0]; + option.Type = ApplicationCommandOptionType.String; + option.IsRequired = true; + option.IsAutocomplete = true; + + return builder; } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; + } + public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public async Task Run(SocketSlashCommand command) { - await command.DeferAsync(true); - - string user = command.Data.Options.First(option => option.Name == Translation.BanCommandUserOptionName) - .Value.ToString(); + await command.DeferAsync(); - string response = Server.ExecuteCommand($"/unban id {user}"); - if (!response.Contains("Done")) - { - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.FailedExecuteCommand.LowercaseParams().Replace("{reason}", response)); - } - else - { - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.UnbanCommandSuccess.LowercaseParams().Replace("{player}", user)); - if (ModerationLogsHandler.Instance.IsEnabled) - { - ModerationLogsHandler.Instance.SendUnbanLogMethod.Invoke( - ModerationLogsHandler.Instance.HandlerInstance, - new object[] { user } - ); - } - } + string id = (string)command.Data.Options.First().Value; + + BanHandler.RemoveBan(id, id.Contains("@") ? BanHandler.BanType.UserId : BanHandler.BanType.IP); + + TranslationBuilder builder = new(Plugin.Instance.Translation.UnbanSuccess); + + builder.CustomReplacers.Add("userid", () => id); + + await command.ModifyOriginalResponseAsync(m => + m.Content = + builder); + } + + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + IEnumerable response = + [ + ..BanHandler.GetBans(BanHandler.BanType.UserId), + ..BanHandler.GetBans(BanHandler.BanType.IP) + ]; + await autocomplete.RespondAsync(response.Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); } } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs new file mode 100644 index 0000000..d025dd3 --- /dev/null +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -0,0 +1,51 @@ +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Bot.API.Utilities; +using LabApi.Features.Wrappers; + +namespace DiscordLab.Moderation.Commands +{ + public class Unmute : IAutocompleteCommand + { + + public SlashCommandBuilder Data + { + get + { + SlashCommandBuilder builder = Plugin.Instance.Translation.UnmuteCommand; + SlashCommandOptionBuilder option = builder.Options[0]; + option.Type = ApplicationCommandOptionType.String; + option.IsRequired = true; + option.IsAutocomplete = true; + + return builder; + } + } + + public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + + public async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); + + if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) + { + await command.ModifyOriginalResponseAsync(m => m.Content = Plugin.Instance.Translation.InvalidUser); + return; + } + + TempMuteManager.RemoveMute(player); + + await command.ModifyOriginalResponseAsync(m => + m.Content = + new TranslationBuilder(Plugin.Instance.Translation.UnmuteSuccess, "player", player)); + } + + public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/Config.cs b/DiscordLab.Moderation/Config.cs index df55ee6..848a6fb 100644 --- a/DiscordLab.Moderation/Config.cs +++ b/DiscordLab.Moderation/Config.cs @@ -1,25 +1,17 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - namespace DiscordLab.Moderation { - public class Config : IConfig, IDLConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - - [Description("The role ID for the role that is able to run this command")] - public ulong BanCommandRole { get; set; } = 0; - [Description("The role ID for the role that is able to run this command")] - public ulong UnbanCommandRole { get; set; } = 0; - [Description("The role ID for the role that is able to run this command")] - public ulong SendCommandRole { get; set; } = 0; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } + public ulong GuildId { get; set; } = 0; + + public ulong MuteLogChannelId { get; set; } = 0; + + public ulong UnmuteLogChannelId { get; set; } = 0; + + public ulong BanLogChannelId { get; set; } = 0; + + public ulong UnbanLogChannelId { get; set; } = 0; + + public bool AddCommands { get; set; } = true; } } \ No newline at end of file diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index 5c394d1..cb3f9d9 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -3,43 +3,18 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 + - - - - - - + + - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.Moderation/Events.cs b/DiscordLab.Moderation/Events.cs new file mode 100644 index 0000000..9b54b21 --- /dev/null +++ b/DiscordLab.Moderation/Events.cs @@ -0,0 +1,119 @@ +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Arguments.ServerEvents; +using LabApi.Events.CustomHandlers; +using LabApi.Features.Console; +using LabApi.Features.Wrappers; + +namespace DiscordLab.Moderation +{ + public class Events : CustomEventsHandler + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public override void OnPlayerUnmuting(PlayerUnmutingEventArgs ev) + { + // otherwise OnPlayerUnmuted will get triggered twice. + ev.IsAllowed = false; + + TempMuteManager.RemoveMute(ev.Player, ev.Issuer); + } + + public override void OnPlayerUnmuted(PlayerUnmutedEventArgs ev) + { + if (Config.UnmuteLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.UnmuteLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unmute logs", Config.UnmuteLogChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new TranslationBuilder(Translation.UnmuteLog) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Issuer); + + channel.SendMessage(builder); + } + + public override void OnPlayerMuted(PlayerMutedEventArgs ev) + { + if (Config.MuteLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.MuteLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("mute logs", Config.MuteLogChannelId, Config.GuildId)); + return; + } + + string translation = Translation.PermMuteLog; + + if (TempMuteManager.MuteConfig.Mutes.TryGetValue(ev.Player.UserId, out DateTime time)) + { + translation = Translation.TempMuteLog; + } + + TranslationBuilder builder = new TranslationBuilder(translation) + { + Time = time + }.AddPlayer("player", ev.Issuer).AddPlayer("target", ev.Player); + + channel.SendMessage(builder); + } + + public override void OnPlayerBanned(PlayerBannedEventArgs ev) + { + if (Config.BanLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.BanLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("ban logs", Config.BanLogChannelId, Config.GuildId)); + return; + } + + EmbedBuilder builder = Translation.BanLogEmbed; + + foreach (EmbedFieldBuilder field in builder.Fields) + { + TranslationBuilder tBuilder = new((string)field.Value, "player", ev.Issuer); + tBuilder.CustomReplacers.Add("userid", () => ev.PlayerId); + field.Value = tBuilder.Build(); + } + + channel.SendMessage(embed:builder.Build()); + } + + public override void OnServerBanRevoked(BanRevokedEventArgs ev) + { + if (Config.UnbanLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.UnbanLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unban logs", Config.UnbanLogChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.UnbanLog); + + builder.CustomReplacers.Add("userid", () => ev.BanDetails.Id); + builder.CustomReplacers.Add("username", () => ev.BanDetails.OriginalName); + builder.CustomReplacers.Add("playerid", () => ev.BanDetails.Issuer); + + if (Player.TryGet(ev.BanDetails.Issuer, out Player player)) + builder.AddPlayer("player", player); + + channel.SendMessage(builder); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/FodyWeavers.xml b/DiscordLab.Moderation/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.Moderation/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.Moderation/FodyWeavers.xsd b/DiscordLab.Moderation/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.Moderation/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.Moderation/Handlers/ModerationLogsHandler.cs b/DiscordLab.Moderation/Handlers/ModerationLogsHandler.cs deleted file mode 100644 index b56e66a..0000000 --- a/DiscordLab.Moderation/Handlers/ModerationLogsHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Reflection; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Exiled.API.Interfaces; -using Exiled.Loader; - -namespace DiscordLab.Moderation.Handlers -{ - public class ModerationLogsHandler : IRegisterable - { - public static ModerationLogsHandler Instance { get; private set; } - - private Type HandlerType { get; set; } - public bool IsEnabled; - public object HandlerInstance { get; private set; } - public MethodInfo SendBanLogMethod { get; private set; } - public MethodInfo SendUnbanLogMethod { get; private set; } - - public void Init() - { - Instance = this; - IPlugin moderationPlugin = Loader.Plugins.FirstOrDefault(p => p.Name == "DiscordLab.ModerationLogs"); - if (moderationPlugin == null) - { - IsEnabled = false; - return; - } - - IsEnabled = moderationPlugin.Config.IsEnabled; - - Assembly assembly = moderationPlugin.Assembly; - - HandlerType = assembly.GetType("DiscordLab.ModerationLogs.Handlers.DiscordBot"); - HandlerInstance = HandlerType.GetProperty("Instance")!.GetValue(null); - SendBanLogMethod = HandlerType.GetMethod("SendBanMessage")!; - SendUnbanLogMethod = HandlerType.GetMethod("SendUnbanMessage")!; - } - - public void Unregister() - { - HandlerType = null; - HandlerInstance = null; - SendBanLogMethod = null; - SendUnbanLogMethod = null; - } - } -} \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index fb2f768..c3f4c8c 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -1,39 +1,75 @@ -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using Exiled.Loader; +using Discord; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using LabApi.Events.CustomHandlers; +using LabApi.Features; +using LabApi.Features.Wrappers; +using LabApi.Loader; namespace DiscordLab.Moderation { public class Plugin : Plugin { - public override string Name => "DiscordLab.Moderation"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.Moderation"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Low; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; - private HandlerLoader _handlerLoader; + public override string Name { get; } = "DiscordLab.Moderation"; + public override string Description { get; } = "Adds logging and commands for moderation based operations"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public override void OnEnabled() + public TempMuteConfig MuteConfig; + + public Events Events = new(); + + public override void Enable() { Instance = this; - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; + CallOnLoadAttribute.Load(); + + if (Config.AddCommands) + ISlashCommand.FindAll(); - base.OnEnabled(); + CustomHandlersManager.RegisterEventsHandler(Events); } - - public override void OnDisabled() + + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); + + CallOnUnloadAttribute.Unload(); + + Events = null; + + Instance = null; + } + + public override void LoadConfigs() { - _handlerLoader.Unload(); - _handlerLoader = null; + this.TryLoadConfig("mute-config.yml", out MuteConfig); - base.OnDisabled(); + base.LoadConfigs(); + } + + public static IEnumerable PlayersAutocompleteResults => + Player.ReadyList.Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); + + public static SlashCommandBuilder SetupDurationBuilder(SlashCommandBuilder original, bool required = false) + { + SlashCommandOptionBuilder playerOption = original.Options[0]; + SlashCommandOptionBuilder durationOption = original.Options[1]; + + playerOption.Type = ApplicationCommandOptionType.String; + playerOption.IsAutocomplete = true; + playerOption.IsRequired = true; + + durationOption.Type = ApplicationCommandOptionType.String; + durationOption.IsAutocomplete = false; + durationOption.IsRequired = required; + + return original; } } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Properties/AssemblyInfo.cs b/DiscordLab.Moderation/Properties/AssemblyInfo.cs index 9ae59d1..54dfc23 100644 --- a/DiscordLab.Moderation/Properties/AssemblyInfo.cs +++ b/DiscordLab.Moderation/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("DiscordLab.Moderation")] -[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -19,7 +19,7 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("51A2FE08-005B-41BC-9C70-AAE85D3A81CB")] +[assembly: Guid("6806DFF9-8907-49F0-B727-67AF6BD35844")] // Version information for an assembly consists of the following four values: // diff --git a/DiscordLab.Moderation/TempMuteConfig.cs b/DiscordLab.Moderation/TempMuteConfig.cs new file mode 100644 index 0000000..37fb7a0 --- /dev/null +++ b/DiscordLab.Moderation/TempMuteConfig.cs @@ -0,0 +1,7 @@ +namespace DiscordLab.Moderation +{ + public class TempMuteConfig + { + public Dictionary Mutes { get; set; } = new(); + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/TempMuteManager.cs b/DiscordLab.Moderation/TempMuteManager.cs new file mode 100644 index 0000000..3a324e4 --- /dev/null +++ b/DiscordLab.Moderation/TempMuteManager.cs @@ -0,0 +1,101 @@ +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using LabApi.Events.Handlers; +using LabApi.Features.Wrappers; +using LabApi.Loader; +using MEC; +using VoiceChat; + +namespace DiscordLab.Moderation +{ + public static class TempMuteManager + { + public static TempMuteConfig MuteConfig => Plugin.Instance.MuteConfig; + + public static Dictionary Handles { get; private set; } = new(); + + [CallOnLoad] + public static void Start() + { + Dictionary mutes = MuteConfig.Mutes; + foreach (KeyValuePair dict in mutes) + { + TimeSpan time = dict.Value - DateTime.Now; + if (time.TotalSeconds < 0) + { + RemoveMute(dict.Key); + continue; + } + AddHandle(); + } + } + + [CallOnUnload] + public static void Stop() + { + foreach (KeyValuePair mutes in Handles) + { + Timing.KillCoroutines(mutes.Key); + } + + Handles = null; + } + + public static void AddHandle(string userId, DateTime time) => + AddHandle(userId, time - DateTime.Now); + + public static void AddHandle(string userId, TimeSpan time) => + Handles.Add(userId, + Timing.CallDelayed((float)time.TotalSeconds, () => RemoveMute(userId))); + + public static void RemoveHandle(string userId) + { + if (!Handles.TryGetValue(userId, out CoroutineHandle handle)) + return; + + Timing.KillCoroutines(handle); + Handles.Remove(userId); + } + + public static DateTime GetExpireDate(string duration) => + GetExpireDate(Misc.RelativeTimeToSeconds(duration, 60)); + + public static DateTime GetExpireDate(long duration) => DateTime.Now.AddSeconds(duration); + + public static void MutePlayer(Player player, DateTime time, Player sender = null) => + MutePlayer(player.UserId, time, sender?.ReferenceHub); + + public static void MutePlayer(string player, DateTime time, ReferenceHub sender = null) + { + sender ??= Server.Host?.ReferenceHub; + VoiceChatMutes.IssueLocalMute(player); + if(Player.TryGet(player, out Player p) && sender) + PlayerEvents.OnMuted(new(p.ReferenceHub, sender, false)); + + MuteConfig.Mutes.Add(player, time); + AddHandle(player, time); + Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); + } + + public static void RemoveMute(Player player, Player sender = null) => + RemoveMute(player.UserId, sender?.ReferenceHub); + + public static void RemoveMute(string player, ReferenceHub sender = null) + { + if (!VoiceChatMutes.Mutes.Contains(player)) + return; + + sender ??= Server.Host?.ReferenceHub; + + VoiceChatMutes.RevokeLocalMute(player); + + MuteConfig.Mutes.Remove(player); + Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); + if(Player.TryGet(player, out Player p) && sender) + PlayerEvents.OnUnmuted(new(p.ReferenceHub, sender, false)); + + if (Handles.ContainsKey(player)) + RemoveHandle(player); + } + } +} \ No newline at end of file diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index bd1edff..0d8e42a 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -1,30 +1,133 @@ -using Exiled.API.Interfaces; +using System.ComponentModel; +using Discord; namespace DiscordLab.Moderation { - public class Translation : ITranslation + public class Translation { - public string BanCommandName { get; set; } = "ban"; - public string BanCommandDescription { get; set; } = "Bans a player from the server."; - public string BanCommandUserOptionName { get; set; } = "user"; - public string BanCommandUserOptionDescription { get; set; } = "The player to ban."; - public string BanCommandDurationOptionName { get; set; } = "duration"; - public string BanCommandDurationOptionDescription { get; set; } = "The duration of the ban (in minutes)."; - public string BanCommandReasonOptionName { get; set; } = "reason"; - public string BanCommandReasonOptionDescription { get; set; } = "The reason for the ban."; - public string BanCommandSuccess { get; set; } = "Successfully banned `{player}`."; - public string UnbanCommandName { get; set; } = "unban"; - public string UnbanCommandDescription { get; set; } = "Unbans a player from the server."; - public string UnbanCommandUserOptionName { get; set; } = "user"; - public string UnbanCommandUserOptionDescription { get; set; } = "The player to unban."; - public string UnbanCommandSuccess { get; set; } = "Successfully unbanned `{player}`."; - public string FailedExecuteCommand { get; set; } = "Failed to execute the command. Here is the reason the server gave back: \n```{reason}```"; - public string SendCommandName { get; set; } = "sendcommand"; - public string SendCommandDescription { get; set; } = "Sends a command to the server."; - public string SendCommandCommandOptionName { get; set; } = "command"; - public string SendCommandCommandOptionDescription { get; set; } = "The command to send to the server."; - public string SendCommandResponse { get; set; } = "Successfully sent the command to the server. Here is the response the server gave back: \n```{response}```"; + public SlashCommandBuilder MuteCommand { get; set; } = new() + { + Name = "mute", + Description = "Mute a player on the server", + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = "user", + Description = "The user to mute" + }, + new() + { + Name = "duration", + Description = "The duration to mute the user for" + } + ] + }; + + public SlashCommandBuilder UnmuteCommand { get; set; } = new() + { + Name = "unmute", + Description = "Unmute a player on the server", + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = "user", + Description = "The user to unmute" + } + ] + }; + + public SlashCommandBuilder BanCommand { get; set; } = new() + { + Name = "ban", + Description = "Ban a player on the server", + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = "user", + Description = "The user to ban" + }, + new() + { + Name = "duration", + Description = "The duration to ban the user for" + }, + new() + { + Name = "reason", + Description = "The reason to ban the user" + } + ] + }; + + public SlashCommandBuilder UnbanCommand { get; set; } = new() + { + Name = "unban", + Description = "Unban a player on the server", + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = "user", + Description = "The user to unban" + } + ] + }; + + public string InvalidUser { get; set; } = "Please provide a valid user to use this command on."; - public string NoPermissions { get; set; } = "You do not have the required permissions to use this command."; + public string TempMuteSuccess { get; set; } = "Player {player} has been temporarily muted for {duration}. They will get unmuted at {timef}"; + + public string UnmuteSuccess { get; set; } = "Player {player} has been successfully unmuted."; + + public string PermMuteSuccess { get; set; } = "Player {player} has been muted."; + + public string BanFailure { get; set; } = "Failed to ban {userid}. Please make sure the data is valid and try again..."; + + public string BanSuccess { get; set; } = "Successfully banned {userid} for {reason}. They will get unbanned in {timer}"; + + public string UnbanSuccess { get; set; } = "Player {userid} has been unbanned."; + + public string PermMuteLog { get; set; } = "Player {target} has been muted by {player}."; + + public string TempMuteLog { get; set; } = + "Player {target} has been muted by {player} for {timef}, they will be unmuted in {timer}"; + + public string UnmuteLog { get; set; } = "Player {target} has been unmuted by {player}."; + + [Description( + "Every field value accepts placeholders, even if you add more. player in this case is the issuer.")] + public EmbedBuilder BanLogEmbed { get; set; } = new() + { + Title = "Ban Log", + Description = "A user has been banned", + Fields = + [ + new() + { + Name = "Player", + Value = "{userid}" + }, + new() + { + Name = "Issuer", + Value = "{player}" + }, + new() + { + Name = "Duration", + Value = "{timer} ({timef})" + } + ] + }; + + [Description("Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] + public string UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; } } \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Config.cs b/DiscordLab.ModerationLogs/Config.cs deleted file mode 100644 index c2b2985..0000000 --- a/DiscordLab.ModerationLogs/Config.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - -namespace DiscordLab.ModerationLogs -{ - public class Config : IConfig, IDLConfig - { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - - [Description("The channel where the ban logs will be sent.")] - public ulong BanChannelId { get; set; } = new(); - - [Description("The hex color code of the ban embed. Do not add the #.")] - public string BanColor { get; set; } = "3498DB"; - - [Description("The channel where the unban logs will be sent.")] - public ulong UnbanChannelId { get; set; } = new(); - - [Description("The hex color code of the unban embed. Do not add the #.")] - public string UnbanColor { get; set; } = "3498DB"; - - [Description("The channel where the kick logs will be sent.")] - public ulong KickChannelId { get; set; } = new(); - - [Description("The hex color code of the kick embed. Do not add the #.")] - public string KickColor { get; set; } = "3498DB"; - - [Description("The channel where the mute logs will be sent.")] - public ulong MuteChannelId { get; set; } = new(); - - [Description("The hex color code of the mute embed. Do not add the #.")] - public string MuteColor { get; set; } = "3498DB"; - - [Description("The channel where the unmute logs will be sent.")] - public ulong UnmuteChannelId { get; set; } = new(); - - [Description("The hex color code of the unmute embed. Do not add the #.")] - public string UnmuteColor { get; set; } = "3498DB"; - - [Description("The channel where the warn logs will be sent.")] - public ulong AdminChatChannelId { get; set; } = new(); - - [Description("The hex color code of the admin chat embed. Do not add the #.")] - public string AdminChatColor { get; set; } = "3498DB"; - - [Description("The channel where reports will be sent.")] - public ulong ReportChannelId { get; set; } = new(); - - [Description("The hex color code of the report embed. Do not add the #.")] - public string ReportColor { get; set; } = "3498DB"; - - [Description("The channel where remote admin logs will be sent.")] - public ulong RemoteAdminChannelId { get; set; } = new(); - - [Description("The hex color code of the remote admin embed. Do not add the #.")] - public string RemoteAdminColor { get; set; } = "3498DB"; - - [Description("The channel where error logs will be sent.")] - public ulong ErrorLogChannelId { get; set; } = new(); - - [Description("The hex color code of the error logging embed. Do not add the #.")] - public string ErrorLogColor { get; set; } = "3498DB"; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/DiscordLab.ModerationLogs.csproj b/DiscordLab.ModerationLogs/DiscordLab.ModerationLogs.csproj deleted file mode 100644 index 4375086..0000000 --- a/DiscordLab.ModerationLogs/DiscordLab.ModerationLogs.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - net48 - enable - disable - preview - x64 - true - false - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/FodyWeavers.xml b/DiscordLab.ModerationLogs/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.ModerationLogs/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/FodyWeavers.xsd b/DiscordLab.ModerationLogs/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.ModerationLogs/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Handlers/DiscordBot.cs b/DiscordLab.ModerationLogs/Handlers/DiscordBot.cs deleted file mode 100644 index fccf9c1..0000000 --- a/DiscordLab.ModerationLogs/Handlers/DiscordBot.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using JetBrains.Annotations; - -namespace DiscordLab.ModerationLogs.Handlers -{ - public class DiscordBot : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel BanChannel { get; set; } - private SocketTextChannel UnbanChannel { get; set; } - private SocketTextChannel KickChannel { get; set; } - private SocketTextChannel MuteChannel { get; set; } - private SocketTextChannel UnmuteChannel { get; set; } - private SocketTextChannel AdminChatChannel { get; set; } - private SocketTextChannel ReportChannel { get; set; } - private SocketTextChannel RemoteAdminChannel { get; set; } - - private SocketGuild Guild { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - BanChannel = null; - UnbanChannel = null; - KickChannel = null; - MuteChannel = null; - UnmuteChannel = null; - AdminChatChannel = null; - ReportChannel = null; - RemoteAdminChannel = null; - } - - private SocketGuild GetGuild() - { - return Guild ??= Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - } - - public SocketTextChannel GetBanChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.BanChannelId == 0) return null; - return BanChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.BanChannelId); - } - - public SocketTextChannel GetUnbanChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.UnbanChannelId == 0) return null; - return UnbanChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.UnbanChannelId); - } - - public SocketTextChannel GetKickChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.KickChannelId == 0) return null; - return KickChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.KickChannelId); - } - - public SocketTextChannel GetMuteChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.MuteChannelId == 0) return null; - return MuteChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.MuteChannelId); - } - - public SocketTextChannel GetUnmuteChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.UnmuteChannelId == 0) return null; - return UnmuteChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.UnmuteChannelId); - } - - public SocketTextChannel GetAdminChatChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.AdminChatChannelId == 0) return null; - return AdminChatChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.AdminChatChannelId); - } - - public SocketTextChannel GetReportChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.ReportChannelId == 0) return null; - return ReportChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.ReportChannelId); - } - - public SocketTextChannel GetRemoteAdminChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.RemoteAdminChannelId == 0) return null; - return RemoteAdminChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.RemoteAdminChannelId); - } - - // These 2 functions are here, and public because they are used in DiscordLab.Moderation when the ban commands are used. - public void SendBanMessage([CanBeNull] string targetName, string targetId, string reason, string issuerName, - [CanBeNull] string issuerId, string duration) - { - SocketTextChannel channel = GetBanChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.BanChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the ban message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerBanned); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.BanColor)); - if (targetName != null) embed.AddField(Translation.Player, targetName); - embed.AddField(Translation.PlayerId, targetId); - embed.AddField(Translation.Reason, reason); - embed.AddField(Translation.Issuer, issuerName); - if (issuerId != null) embed.AddField(Translation.IssuerId, issuerId); - embed.AddField(Translation.Duration, duration); - channel.SendMessageAsync(Translation.PlayerBannedContent, embed: embed.Build()); - } - - public void SendUnbanMessage(string targetId) - { - SocketTextChannel channel = GetUnbanChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.UnbanChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the unban message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerUnbanned); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.UnbanColor)); - embed.AddField(Translation.PlayerId, targetId); - channel.SendMessageAsync(Translation.PlayerUnbannedContent, embed: embed.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Handlers/Events.cs b/DiscordLab.ModerationLogs/Handlers/Events.cs deleted file mode 100644 index 4db92b0..0000000 --- a/DiscordLab.ModerationLogs/Handlers/Events.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Globalization; -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; -using Exiled.Events.EventArgs.Server; - -namespace DiscordLab.ModerationLogs.Handlers -{ - public class Events : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public void Init() - { - Exiled.Events.Handlers.Player.Banned += OnBanned; - Exiled.Events.Handlers.Server.Unbanned += OnUnbanned; - Exiled.Events.Handlers.Player.Kicking += OnKicking; - Exiled.Events.Handlers.Player.IssuingMute += OnIssuingMute; - Exiled.Events.Handlers.Player.RevokingMute += OnIssuingUnmute; - Exiled.Events.Handlers.Player.SendingAdminChatMessage += OnSendingAdminChatMessage; - Exiled.Events.Handlers.Server.LocalReporting += OnLocalReporting; - } - - public void Unregister() - { - Exiled.Events.Handlers.Player.Banned -= OnBanned; - Exiled.Events.Handlers.Server.Unbanned -= OnUnbanned; - Exiled.Events.Handlers.Player.Kicking -= OnKicking; - Exiled.Events.Handlers.Player.IssuingMute -= OnIssuingMute; - Exiled.Events.Handlers.Player.RevokingMute -= OnIssuingUnmute; - Exiled.Events.Handlers.Player.SendingAdminChatMessage -= OnSendingAdminChatMessage; - Exiled.Events.Handlers.Server.LocalReporting -= OnLocalReporting; - } - - private void OnLocalReporting(LocalReportingEventArgs ev) - { - if (!ev.IsAllowed) return; - SocketTextChannel channel = DiscordBot.Instance.GetReportChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.ReportChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the kick message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerReported); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.ReportColor)); - embed.AddField(Translation.Target, ev.Target.Nickname); - embed.AddField(Translation.TargetId, ev.Target.UserId); - embed.AddField(Translation.Reason, ev.Reason); - embed.AddField(Translation.Reporter, ev.Player.Nickname); - embed.AddField(Translation.ReporterId, ev.Player.UserId); - channel.SendMessageAsync(embed: embed.Build()); - } - - private void OnBanned(BannedEventArgs ev) - { - DiscordBot.Instance.SendBanMessage(ev.Details.OriginalName, - ev.Details.Id, - ev.Details.Reason, - ev.Player.Nickname, - ev.Player.UserId, - $"" - ); - } - - private void OnUnbanned(UnbannedEventArgs ev) - { - DiscordBot.Instance.SendUnbanMessage(ev.TargetId); - } - - private void OnKicking(KickingEventArgs ev) - { - if (!ev.IsAllowed) return; - SocketTextChannel channel = DiscordBot.Instance.GetKickChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.KickChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the kick message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerKicked); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.KickColor)); - embed.AddField(Translation.Player, ev.Target.Nickname); - embed.AddField(Translation.PlayerId, ev.Target.UserId); - embed.AddField(Translation.Reason, ev.Reason); - embed.AddField(Translation.Issuer, ev.Player.Nickname); - embed.AddField(Translation.IssuerId, ev.Player.UserId); - channel.SendMessageAsync(Translation.PlayerKickedContent, embed: embed.Build()); - } - - private void OnIssuingMute(IssuingMuteEventArgs ev) - { - if (!ev.IsAllowed) return; - SocketTextChannel channel = DiscordBot.Instance.GetMuteChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.MuteChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the mute message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerMuted); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.MuteColor)); - embed.AddField(Translation.Player, ev.Player.Nickname); - embed.AddField(Translation.PlayerId, ev.Player.UserId); - embed.AddField(Translation.Issuer, ev.Player.Nickname); - embed.AddField(Translation.IssuerId, ev.Player.UserId); - channel.SendMessageAsync(Translation.PlayerMutedContent, embed: embed.Build()); - } - - private void OnIssuingUnmute(RevokingMuteEventArgs ev) - { - if (!ev.IsAllowed) return; - SocketTextChannel channel = DiscordBot.Instance.GetUnmuteChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.UnmuteChannelId == 0) return; - Log.Error("Either the guild is null or the channel is null. So the unmute message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.PlayerMuteRevoked); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.UnmuteColor)); - embed.AddField(Translation.Player, ev.Player.Nickname); - embed.AddField(Translation.PlayerId, ev.Player.UserId); - embed.AddField(Translation.Issuer, ev.Player.Nickname); - embed.AddField(Translation.IssuerId, ev.Player.UserId); - channel.SendMessageAsync(Translation.PlayerMuteRevokedContent, embed: embed.Build()); - } - - private void OnSendingAdminChatMessage(SendingAdminChatMessageEventsArgs ev) - { - if (!ev.IsAllowed) return; - SocketTextChannel channel = DiscordBot.Instance.GetAdminChatChannel(); - if (channel == null) - { - if (Plugin.Instance.Config.AdminChatChannelId == 0) return; - Log.Error( - "Either the guild is null or the channel is null. So the admin chat message has failed to send."); - return; - } - - EmbedBuilder embed = new(); - embed.WithTitle(Translation.AdminChatMessage); - embed.WithColor(Plugin.GetColor(Plugin.Instance.Config.AdminChatColor)); - embed.AddField(Translation.Player, ev.Player.Nickname); - embed.AddField(Translation.PlayerId, ev.Player.UserId); - embed.AddField(Translation.Message, ev.Message); - channel.SendMessageAsync(embed: embed.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Patches/RemoteAdminLogger.cs b/DiscordLab.ModerationLogs/Patches/RemoteAdminLogger.cs deleted file mode 100644 index f98f0b8..0000000 --- a/DiscordLab.ModerationLogs/Patches/RemoteAdminLogger.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Reflection.Emit; -using Discord; -using Discord.WebSocket; -using DiscordLab.ModerationLogs.Handlers; -using Exiled.API.Features; -using HarmonyLib; -using NorthwoodLib.Pools; -using RemoteAdmin; - -using static HarmonyLib.AccessTools; - -namespace DiscordLab.ModerationLogs.Patches -{ - [HarmonyPatch(typeof(CommandProcessor), nameof(CommandProcessor.ProcessQuery))] - internal class RemoteAdminLogger - { - private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(instructions); - const int index = 0; - - newInstructions.InsertRange(index, new[] - { - new CodeInstruction(OpCodes.Ldarg_0), - new CodeInstruction(OpCodes.Ldarg_1), - new CodeInstruction(OpCodes.Call, Method(typeof(RemoteAdminLogger), nameof(SendCommand))), - }); - - foreach (CodeInstruction t in newInstructions) - yield return t; - - ListPool.Shared.Return(newInstructions); - } - - private static void SendCommand(string query, CommandSender sender) - { - if (Plugin.Instance.Config.RemoteAdminChannelId == 0) return; - - if (query.StartsWith("$")) return; - - Player player = sender is PlayerCommandSender commandSender - ? Player.Get(commandSender) - : Server.Host; - if (player == null || string.IsNullOrEmpty(player.UserId)) return; - - SocketTextChannel channel = DiscordBot.Instance.GetRemoteAdminChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the remote admin message has failed to send."); - return; - } - - EmbedBuilder embed = new() - { - Title = Plugin.Instance.Translation.RemoteAdminCommand, - Color = Plugin.GetColor(Plugin.Instance.Config.RemoteAdminColor), - Fields = new() - { - new() - { - Name = Plugin.Instance.Translation.Command, - Value = query, - IsInline = false - }, - new() - { - Name = Plugin.Instance.Translation.Issuer, - Value = player.Nickname, - }, - new() - { - Name = Plugin.Instance.Translation.IssuerId, - Value = player.UserId, - } - } - }; - - channel.SendMessageAsync(embed: embed.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Plugin.cs b/DiscordLab.ModerationLogs/Plugin.cs deleted file mode 100644 index 14ad774..0000000 --- a/DiscordLab.ModerationLogs/Plugin.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Globalization; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using HarmonyLib; - -namespace DiscordLab.ModerationLogs -{ - public class Plugin : Plugin - { - public override string Name => "DiscordLab.ModerationLogs"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.ModerationLogs"; - public override Version Version => new (1, 5, 2); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - - private Harmony harmony; - - public override void OnEnabled() - { - Instance = this; - - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - try - { - harmony = new($"discordlab.moderationlogs.{DateTime.Now.Ticks}"); - harmony.PatchAll(); - } - catch (Exception e) - { - Log.Error($"An error occurred while patching: {e}"); - } - - base.OnEnabled(); - } - - public override void OnDisabled() - { - _handlerLoader.Unload(); - _handlerLoader = null; - - harmony?.UnpatchAll(harmony.Id); - harmony = null; - - base.OnDisabled(); - } - - public static uint GetColor(string color) - { - return uint.Parse(color, NumberStyles.HexNumber); - } - - public static string ExpiresToString(DateTime dateTime) - { - return new DateTimeOffset(dateTime).ToUnixTimeSeconds().ToString(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Properties/AssemblyInfo.cs b/DiscordLab.ModerationLogs/Properties/AssemblyInfo.cs deleted file mode 100644 index 08b2922..0000000 --- a/DiscordLab.ModerationLogs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.ModerationLogs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.ModerationLogs")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("E5AF10FD-689E-4A9E-87F0-E195112D89D3")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.ModerationLogs/Translation.cs b/DiscordLab.ModerationLogs/Translation.cs deleted file mode 100644 index e83a698..0000000 --- a/DiscordLab.ModerationLogs/Translation.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; - -namespace DiscordLab.ModerationLogs -{ - public class Translation : ITranslation - { - public string Player { get; set; } = "Player"; - public string PlayerId { get; set; } = "Player ID"; - public string Reason { get; set; } = "Reason"; - public string Issuer { get; set; } = "Issuer"; - public string IssuerId { get; set; } = "Issuer ID"; - public string Target { get; set; } = "Target"; - public string TargetId { get; set; } = "Target ID"; - public string Reporter { get; set; } = "Reporter"; - public string ReporterId { get; set; } = "Reporter ID"; - public string Duration { get; set; } = "Duration"; - [Description("What will show as the message content for when a player is banned")] - public string PlayerBannedContent { get; set; } = string.Empty; - public string PlayerBanned { get; set; } = "Player banned"; - [Description("What will show as the message content for when a player is reported")] - public string PlayerReportedContent { get; set; } = string.Empty; - public string PlayerReported { get; set; } = "Player reported"; - [Description("What will show as the message content for when a player is kicked")] - public string PlayerKickedContent { get; set; } = string.Empty; - public string PlayerKicked { get; set; } = "Player kicked"; - [Description("What will show as the message content for when a player is unbanned")] - public string PlayerUnbannedContent { get; set; } = string.Empty; - public string PlayerUnbanned { get; set; } = "Player unbanned"; - [Description("What will show as the message content for when a player is muted")] - public string PlayerMutedContent { get; set; } = string.Empty; - public string PlayerMuted { get; set; } = "Player muted"; - [Description("What will show as the message content for when a player is unmuted")] - public string PlayerMuteRevokedContent { get; set; } = string.Empty; - public string PlayerMuteRevoked { get; set; } = "Player mute revoked"; - public string AdminChatMessage { get; set; } = "Admin chat message"; - public string RemoteAdminCommand { get; set; } = "Remote admin command"; - public string Command { get; set; } = "Command"; - public string Message { get; set; } = "Message"; - } -} \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Config.cs b/DiscordLab.RoundLogs/Config.cs index 18c9dc4..d2b6f53 100644 --- a/DiscordLab.RoundLogs/Config.cs +++ b/DiscordLab.RoundLogs/Config.cs @@ -1,31 +1,37 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.RoundLogs { - public class Config : IConfig, IDLConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - - [Description("The channel ID to send the round start messages to.")] - public ulong RoundStartChannelId { get; set; } = new(); - [Description("The channel ID to send the round end messages to.")] - public ulong RoundEndChannelId { get; set; } = new(); - [Description("The channel ID to send the cuff logs to. (When a player gets cuffed)")] - public ulong CuffedChannelId { get; set; } = new(); - [Description("The channel ID to send the uncuff logs to. (When a player gets uncuffed)")] - public ulong UncuffedChannelId { get; set; } = new(); - [Description("The channel ID to send the Chaos spawn logs to.")] - public ulong ChaosSpawnChannelId { get; set; } = new(); - [Description("The channel ID to send the NTF spawn logs to.")] - public ulong NtfSpawnChannelId { get; set; } = new(); - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } = new(); + [Description("The guild ID, set to 0 for default guild.")] + public ulong GuildId { get; set; } = 0; + + [Description("The channel to log to when someone's role changes.")] + public ulong RoleChangeChannelId { get; set; } = 0; + + [Description("The channel to log to when someone swaps from an SCP to another.")] + public ulong ScpSwapChannelId { get; set; } = 0; + + [Description("The channel to log to when NTF spawns.")] + public ulong NtfSpawnChannelId { get; set; } = 0; + + [Description("The channel to log to when Chaos spawns.")] + public ulong ChaosSpawnChannelId { get; set; } = 0; + + [Description("The channel to log to when someone gets cuffed.")] + public ulong CuffedChannelId { get; set; } = 0; + + [Description("The channel to log to when someone gets uncuffed.")] + public ulong UncuffedChannelId { get; set; } = 0; + + [Description("The channel to log to when the round starts.")] + public ulong RoundStartedChannelId { get; set; } = 0; + + [Description("The channel to log to when the round ends.")] + public ulong RoundEndedChannelId { get; set; } = 0; + + [Description("The channel to log to when decontamination starts.")] + public ulong DecontaminationChannelId { get; set; } = 0; } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj index 4375086..b6fe025 100644 --- a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj +++ b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj @@ -3,44 +3,14 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs new file mode 100644 index 0000000..636d9dc --- /dev/null +++ b/DiscordLab.RoundLogs/Events.cs @@ -0,0 +1,185 @@ +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Extensions; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Arguments.ServerEvents; +using LabApi.Events.CustomHandlers; +using PlayerRoles; +using LabApi.Features.Console; +using LabApi.Features.Wrappers; +using Respawning.Waves; + +namespace DiscordLab.RoundLogs +{ + public class Events : CustomEventsHandler + { + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) + { + if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart + or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin) + return; + + Dictionary> customReplacers = new() + { + ["oldrole"] = () => ev.OldRole.ToString(), + ["newrole"] = () => ev.NewRole.RoleTypeId.ToString(), + ["reason"] = () => ev.ChangeReason.ToString(), + ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) + }; + + SocketTextChannel channel; + + TranslationBuilder builder; + + if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) + { + if (Config.ScpSwapChannelId == 0) + return; + channel = Client.GetOrAddChannel(Config.ScpSwapChannelId); + if (channel == null) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("SCP Swap logs", Config.ScpSwapChannelId, Config.GuildId)); + return; + } + + builder = new(Translation.ScpSwapLog, "player", ev.Player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); + return; + } + + if (Config.RoleChangeChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RoleChangeChannelId, out channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, Config.GuildId)); + } + + builder = new(Translation.RoleChangeLog, "player", ev.Player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); + } + + public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) + { + bool isFoundation = ev.Wave is MtfWave or MiniMtfWave; + + if ((isFoundation && Config.NtfSpawnChannelId == 0) || (!isFoundation && Config.ChaosSpawnChannelId == 0)) + return; + + ulong channelId = isFoundation ? Config.NtfSpawnChannelId : Config.ChaosSpawnChannelId; + + if (!Client.TryGetOrAddChannel(channelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Wave respawn logs", channelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn) + { + PlayerListItem = Translation.PlayerListItem + }; + + channel.SendMessage(builder); + } + + public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) + { + if (Config.CuffedChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuffed logs", Config.CuffedChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.Cuffed); + + builder.AddPlayer("target", ev.Target); + builder.AddPlayer("player", ev.Player); + + channel.SendMessage(builder); + } + + public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) + { + if (Config.UncuffedChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.UncuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("uncuff logs", Config.CuffedChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.Uncuffed); + + builder.AddPlayer("target", ev.Target); + builder.AddPlayer("player", ev.Player); + + channel.SendMessage(builder); + } + + public override void OnServerRoundStarted() + { + if (Config.RoundStartedChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RoundStartedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.RoundStart); + + channel.SendMessage(builder); + } + + public override void OnServerRoundEnded(RoundEndedEventArgs ev) + { + if (Config.RoundEndedChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.RoundEndedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round ended logs", Config.CuffedChannelId, Config.GuildId)); + return; + } + + TranslationBuilder builder = new(Translation.RoundEnd); + + builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + + channel.SendMessage(builder); + } + + public override void OnServerLczDecontaminationStarted() + { + if (Config.DecontaminationChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.DecontaminationChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("decontamination logs", Config.DecontaminationChannelId, Config.GuildId)); + return; + } + + channel.SendMessage(new TranslationBuilder(Translation.Decontamination)); + } + } +} \ No newline at end of file diff --git a/DiscordLab.RoundLogs/FodyWeavers.xml b/DiscordLab.RoundLogs/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.RoundLogs/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.RoundLogs/FodyWeavers.xsd b/DiscordLab.RoundLogs/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.RoundLogs/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Handlers/DiscordBot.cs b/DiscordLab.RoundLogs/Handlers/DiscordBot.cs deleted file mode 100644 index 4c62987..0000000 --- a/DiscordLab.RoundLogs/Handlers/DiscordBot.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.RoundLogs.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private SocketGuild Guild { get; set; } - - private SocketTextChannel RoundStartChannel { get; set; } - private SocketTextChannel RoundEndChannel { get; set; } - - private SocketTextChannel CuffedChannel { get; set; } - private SocketTextChannel UncuffedChannel { get; set; } - - private SocketTextChannel NtfEnterChannel { get; set; } - private SocketTextChannel ChaosEnterChannel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - Guild = null; - RoundStartChannel = null; - RoundEndChannel = null; - CuffedChannel = null; - UncuffedChannel = null; - NtfEnterChannel = null; - ChaosEnterChannel = null; - } - - private SocketGuild GetGuild() - { - return Guild ??= Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - } - - public SocketTextChannel GetRoundStartChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.RoundStartChannelId == 0) return null; - return RoundStartChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.RoundStartChannelId); - } - - public SocketTextChannel GetRoundEndChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.RoundEndChannelId == 0) return null; - return RoundEndChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.RoundEndChannelId); - } - - public SocketTextChannel GetCuffedChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.CuffedChannelId == 0) return null; - return CuffedChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.CuffedChannelId); - } - - public SocketTextChannel GetUncuffedChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.UncuffedChannelId == 0) return null; - return UncuffedChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.UncuffedChannelId); - } - - public SocketTextChannel GetNtfEnterChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.NtfSpawnChannelId == 0) return null; - return NtfEnterChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.NtfSpawnChannelId); - } - - public SocketTextChannel GetChaosEnterChannel() - { - if (GetGuild() == null) return null; - if (Plugin.Instance.Config.ChaosSpawnChannelId == 0) return null; - return ChaosEnterChannel ??= - Guild.GetTextChannel(Plugin.Instance.Config.ChaosSpawnChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Handlers/Events.cs b/DiscordLab.RoundLogs/Handlers/Events.cs deleted file mode 100644 index 676b2bc..0000000 --- a/DiscordLab.RoundLogs/Handlers/Events.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Map; -using Exiled.Events.EventArgs.Player; -using Exiled.Events.EventArgs.Server; - -namespace DiscordLab.RoundLogs.Handlers -{ - public class Events : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public void Init() - { - Exiled.Events.Handlers.Server.RoundStarted += OnRoundStarted; - Exiled.Events.Handlers.Server.RoundEnded += OnRoundEnded; - Exiled.Events.Handlers.Player.Handcuffing += OnCuffing; - Exiled.Events.Handlers.Player.RemovedHandcuffs += OnUncuffed; - Exiled.Events.Handlers.Map.AnnouncingChaosEntrance += OnChaosEnter; - Exiled.Events.Handlers.Map.AnnouncingNtfEntrance += OnNtfEnter; - } - - public void Unregister() - { - Exiled.Events.Handlers.Server.RoundStarted -= OnRoundStarted; - Exiled.Events.Handlers.Server.RoundEnded -= OnRoundEnded; - Exiled.Events.Handlers.Player.Handcuffing -= OnCuffing; - Exiled.Events.Handlers.Player.RemovedHandcuffs -= OnUncuffed; - Exiled.Events.Handlers.Map.AnnouncingChaosEntrance -= OnChaosEnter; - Exiled.Events.Handlers.Map.AnnouncingNtfEntrance -= OnNtfEnter; - } - - private void OnRoundStarted() - { - SocketTextChannel channel = DiscordBot.Instance.GetRoundStartChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.RoundStartChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the round start message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.RoundStartMessage.LowercaseParams().StaticReplace()); - } - - private void OnRoundEnded(RoundEndedEventArgs ev) - { - SocketTextChannel channel = DiscordBot.Instance.GetRoundEndChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.RoundEndChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the round end message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.RoundEndMessage.LowercaseParams().Replace("{team}", ev.LeadingTeam.ToString()).StaticReplace()); - } - - private void OnCuffing(HandcuffingEventArgs ev) - { - SocketTextChannel channel = DiscordBot.Instance.GetCuffedChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.CuffedChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the cuffed message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.PlayerCuffedMessage.LowercaseParams().PlayerReplace("player", ev.Target).PlayerReplace("issuer", ev.Player).StaticReplace()); - } - - private void OnUncuffed(RemovedHandcuffsEventArgs ev) - { - SocketTextChannel channel = DiscordBot.Instance.GetUncuffedChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.UncuffedChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the uncuffed message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.PlayerUncuffedMessage.LowercaseParams().PlayerReplace("player", ev.Target).PlayerReplace("issuer", ev.Player).StaticReplace()); - } - - private void OnNtfEnter(AnnouncingNtfEntranceEventArgs ev) - { - SocketTextChannel channel = DiscordBot.Instance.GetNtfEnterChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.NtfSpawnChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the ntf enter message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.NtfSpawnMessage.LowercaseParams().Replace("{unitname}", ev.UnitName).Replace("{unitnumber}", ev.UnitNumber.ToString()).StaticReplace()); - } - - private void OnChaosEnter(AnnouncingChaosEntranceEventArgs ev) - { - SocketTextChannel channel = DiscordBot.Instance.GetChaosEnterChannel(); - if (channel == null) - { - if(Plugin.Instance.Config.ChaosSpawnChannelId == 0) - return; - Log.Error("Either the guild is null or the channel is null. So the chaos enter message has failed to send."); - return; - } - - channel.SendMessageAsync(Translation.ChaosSpawnMessage.LowercaseParams().StaticReplace()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index 1530160..93cb29c 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -1,38 +1,36 @@ -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using LabApi.Events.CustomHandlers; +using LabApi.Features; namespace DiscordLab.RoundLogs { public class Plugin : Plugin { - public override string Name => "DiscordLab.RoundLogs"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.RoundLogs"; - public override Version Version => new (1, 0, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; - private HandlerLoader _handlerLoader; + public override string Name { get; } = "DiscordLab.RoundLogs"; + public override string Description { get; } = "Allows you to log specific details about the round."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public override void OnEnabled() + public Events Events = new(); + + public override void Enable() { Instance = this; - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); + CustomHandlersManager.RegisterEventsHandler(Events); } - - public override void OnDisabled() + + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; + CustomHandlersManager.UnregisterEventsHandler(Events); + + Events = null; - base.OnDisabled(); + Instance = null; } } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Properties/AssemblyInfo.cs b/DiscordLab.RoundLogs/Properties/AssemblyInfo.cs deleted file mode 100644 index 5fe37c1..0000000 --- a/DiscordLab.RoundLogs/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.RoundLogs")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.RoundLogs")] -[assembly: AssemblyCopyright("Copyright © 2025")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4439C1F3-03C6-4D4B-94B3-7A97744C2DE9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Translation.cs b/DiscordLab.RoundLogs/Translation.cs index ba51597..b692f3f 100644 --- a/DiscordLab.RoundLogs/Translation.cs +++ b/DiscordLab.RoundLogs/Translation.cs @@ -1,16 +1,28 @@ -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.RoundLogs { - public class Translation : ITranslation + public class Translation { - public string RoundStartMessage { get; set; } = "Round has started! Started {timer}"; - public string RoundEndMessage { get; set; } = "Round has ended! Team {team} has won!"; - - public string PlayerCuffedMessage { get; set; } = "{playernickname} has been cuffed by {issuernickname}!"; - public string PlayerUncuffedMessage { get; set; } = "{playernickname} has been uncuffed by {issuernickname}!"; - - public string ChaosSpawnMessage { get; set; } = "Chaos Insurgency has spawned! {timer}"; - public string NtfSpawnMessage { get; set; } = "Nine-Tailed Fox ({unitname}-{unitnumber}) has spawned! {timer}"; + public string ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; + + public string RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; + + public string NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; + + public string ChaosSpawn { get; set; } = "Chaos has respawned with the following player:\n{players}"; + + public string PlayerListItem { get; set; } = "- {player}"; + + public string Cuffed { get; set; } = "Player {target} has been cuffed by {player}"; + + public string Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; + + [Description("This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] + public string RoundStart { get; set; } = "Round has started."; + + public string RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; + + public string Decontamination { get; set; } = "Decontamination has begun."; } } \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Config.cs b/DiscordLab.SCPSwap/Config.cs deleted file mode 100644 index 484f861..0000000 --- a/DiscordLab.SCPSwap/Config.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - -namespace DiscordLab.SCPSwap -{ - public class Config : IConfig, IDLConfig - { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - [Description("The channel where the swap logs will be sent.")] - public ulong ChannelId { get; set; } = new(); - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.SCPSwap/DiscordLab.SCPSwap.csproj b/DiscordLab.SCPSwap/DiscordLab.SCPSwap.csproj deleted file mode 100644 index 5c394d1..0000000 --- a/DiscordLab.SCPSwap/DiscordLab.SCPSwap.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - net48 - enable - disable - preview - x64 - true - false - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.SCPSwap/FodyWeavers.xml b/DiscordLab.SCPSwap/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.SCPSwap/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.SCPSwap/FodyWeavers.xsd b/DiscordLab.SCPSwap/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.SCPSwap/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Handlers/DiscordBot.cs b/DiscordLab.SCPSwap/Handlers/DiscordBot.cs deleted file mode 100644 index 0a7b41b..0000000 --- a/DiscordLab.SCPSwap/Handlers/DiscordBot.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.SCPSwap.Handlers -{ - public class DiscordBot : IRegisterable - { - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel Channel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - Channel = null; - } - - public SocketTextChannel GetChannel() - { - SocketGuild guild = Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - if (guild == null) return null; - if (Plugin.Instance.Config.ChannelId == 0) return null; - return Channel ??= guild.GetTextChannel(Plugin.Instance.Config.ChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Handlers/Events.cs b/DiscordLab.SCPSwap/Handlers/Events.cs deleted file mode 100644 index 9eea51f..0000000 --- a/DiscordLab.SCPSwap/Handlers/Events.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Extensions; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; - -namespace DiscordLab.SCPSwap.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Player.ChangingRole += OnChangingRole; - } - - public void Unregister() - { - Exiled.Events.Handlers.Player.ChangingRole -= OnChangingRole; - } - - private void OnChangingRole(ChangingRoleEventArgs ev) - { - if (!ev.Player.IsScp) return; - if (!ev.NewRole.IsScp()) return; - SocketTextChannel channel = DiscordBot.Instance.GetChannel(); - if (channel == null) - { - Log.Error("Either the guild is null or the channel is null. So the SCP log message has failed to send."); - return; - } - - channel.SendMessageAsync(Plugin.Instance.Translation.Message.LowercaseParams() - .Replace("{player}", ev.Player.Nickname) - .Replace("{playerid}", ev.Player.UserId) - .Replace("{oldrole}", ev.Player.Role.Name) - .Replace("{newrole}", ev.NewRole.GetFullName()) - .PlayerReplace("player", ev.Player) - .StaticReplace() - ); - } - } -} \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Plugin.cs b/DiscordLab.SCPSwap/Plugin.cs deleted file mode 100644 index 0ee50cb..0000000 --- a/DiscordLab.SCPSwap/Plugin.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Globalization; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; - -namespace DiscordLab.SCPSwap -{ - public class Plugin : Plugin - { - public override string Name => "DiscordLab.SCPSwap"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.SCPSwap"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (9, 0, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - - public override void OnEnabled() - { - Instance = this; - - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); - } - - public override void OnDisabled() - { - _handlerLoader.Unload(); - _handlerLoader = null; - - base.OnDisabled(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Properties/AssemblyInfo.cs b/DiscordLab.SCPSwap/Properties/AssemblyInfo.cs deleted file mode 100644 index d9240c7..0000000 --- a/DiscordLab.SCPSwap/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.SCPSwap")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.SCPSwap")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3444CF96-2F51-42EA-A400-BDB24656E70E")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.SCPSwap/Translation.cs b/DiscordLab.SCPSwap/Translation.cs deleted file mode 100644 index a43aad3..0000000 --- a/DiscordLab.SCPSwap/Translation.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Exiled.API.Interfaces; - -namespace DiscordLab.SCPSwap -{ - public class Translation : ITranslation - { - public string Message { get; set; } = "Player `{player}` (`{playerid}`) has swapped from `{oldrole}` to `{newrole}`."; - } -} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs new file mode 100644 index 0000000..743c6f4 --- /dev/null +++ b/DiscordLab.StatusChannel/Command.cs @@ -0,0 +1,18 @@ +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Interfaces; + +namespace DiscordLab.StatusChannel +{ + public class Command : ISlashCommand + { + public SlashCommandBuilder Data { get; } = Plugin.Instance.Translation.PlayerListCommand; + + public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + + public async Task Run(SocketSlashCommand command) + { + await command.RespondAsync(embed: Events.GetEmbed().Build()); + } + } +} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Config.cs b/DiscordLab.StatusChannel/Config.cs index 868b297..ffab52a 100644 --- a/DiscordLab.StatusChannel/Config.cs +++ b/DiscordLab.StatusChannel/Config.cs @@ -1,25 +1,15 @@ -using System.ComponentModel; -using Discord; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; +using System.ComponentModel; namespace DiscordLab.StatusChannel { - public class Config : IConfig, IDLConfig + public class Config { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; + [Description("The channel that you want the message sent to.")] + public ulong ChannelId { get; set; } = 0; - [Description("The channel where the status message will be sent / edited.")] - public ulong ChannelId { get; set; } = new(); + [Description("The guild ID, set to 0 for default guild.")] + public ulong GuildId { get; set; } = 0; - [Description("The hex color code of the embed. Do not add the #.")] - public string Color { get; set; } = "3498DB"; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } + public bool AddCommand { get; set; } = true; } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index 5c394d1..b6fe025 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -3,43 +3,14 @@ net48 enable disable - preview + 12 x64 true false + 2.0.0 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs new file mode 100644 index 0000000..81189c1 --- /dev/null +++ b/DiscordLab.StatusChannel/Events.cs @@ -0,0 +1,114 @@ +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.CustomHandlers; +using LabApi.Features.Console; +using LabApi.Features.Wrappers; +using LabApi.Loader; + +namespace DiscordLab.StatusChannel +{ + public class Events : CustomEventsHandler + { + // events + + public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); + + public override void OnPlayerLeft(PlayerLeftEventArgs _) => Process(); + + public override void OnServerRoundStarted() => EditMessage(); + + // static methods + + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public static SocketTextChannel Channel; + + public static IUserMessage Message; + + public static Queue Queue = new(5, EditMessage); + + [CallOnUnload] + public static void Unregister() + { + Channel = null; + Message = null; + Queue = null; + } + + public static void Process() + { + if(Round.IsRoundInProgress) + EditMessage(); + else + Queue.Process(); + } + + public static EmbedBuilder GetEmbed() + { + EmbedBuilder embed = !Player.ReadyList.Any() ? Translation.EmbedEmpty : Translation.Embed; + TranslationBuilder builder = new(embed.Description); + + if (Player.ReadyList.Any()) + { + builder.PlayerListItem = Translation.PlayerItem; + } + + embed.Description = builder; + + return embed; + } + + public static void EditMessage() + { + if (Channel == null && Message == null) return; + if (Message == null) + { + Task.Run(async () => + { + await GetOrCreateMessage(); + EditMessage(); + }); + return; + } + + Task.Run(async () => await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()).ConfigureAwait(false)); + } + + [CallOnReady] + public static void Ready() + { + if (!Client.TryGetOrAddChannel(Config.ChannelId, out Channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); + Plugin.Instance.Disable(); + return; + } + + Task.Run(GetOrCreateMessage); + } + + public static async Task GetOrCreateMessage() + { + ulong msgId = Plugin.Instance.MessageConfig.MessageId; + Message = Channel.GetCachedMessage(msgId) as IUserMessage ?? + await Channel.GetMessageAsync(msgId) as IUserMessage; + + if (Message == null) + { + EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; + embed.Description = new TranslationBuilder(embed.Description); + Message = await Channel.SendMessageAsync(embed:embed.Build()); + + Plugin.Instance.MessageConfig.MessageId = Message.Id; + Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message_config.yml"); + } + } + } +} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/FodyWeavers.xml b/DiscordLab.StatusChannel/FodyWeavers.xml deleted file mode 100644 index 1a08b63..0000000 --- a/DiscordLab.StatusChannel/FodyWeavers.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Discord.Net.Websocket - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.StatusChannel/FodyWeavers.xsd b/DiscordLab.StatusChannel/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.StatusChannel/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Handlers/DiscordBot.cs b/DiscordLab.StatusChannel/Handlers/DiscordBot.cs deleted file mode 100644 index 1f5f9f6..0000000 --- a/DiscordLab.StatusChannel/Handlers/DiscordBot.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Globalization; -using Discord; -using Discord.Rest; -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using Newtonsoft.Json.Linq; - -namespace DiscordLab.StatusChannel.Handlers -{ - public class DiscordBot : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel StatusChannel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - StatusChannel = null; - } - - private SocketTextChannel GetChannel() - { - SocketGuild guild = Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - if (guild == null) return null; - if (Plugin.Instance.Config.ChannelId == 0) return null; - return StatusChannel ??= - guild.GetTextChannel(Plugin.Instance.Config.ChannelId); - } - - public void SetStatusMessage(IEnumerable players = null) - { - players ??= Player.List.Where(p => !p.IsNPC).ToList(); - IEnumerable playerList = players.ToList(); - string playersString = string.Join("\n", - playerList.Select(p => - Translation.PlayersList.LowercaseParams().Replace("{player}", p.Nickname).Replace("{playerid}", p.UserId).PlayerReplace("player", p).StaticReplace())); - string fullDescription = !playerList.Any() - ? Translation.EmbedNoPlayers.LowercaseParams() - .Replace("{max}", Server.MaxPlayerCount.ToString()).StaticReplace() - : Translation.EmbedDescription.LowercaseParams() - .Replace("{players}", playersString) - .Replace("{current}", playerList.Count().ToString()) - .Replace("{max}", Server.MaxPlayerCount.ToString()) - .StaticReplace(); - EmbedBuilder embed = new EmbedBuilder() - .WithTitle(Translation.EmbedTitle) - .WithColor(uint.Parse(Plugin.Instance.Config.Color, NumberStyles.HexNumber)) - .WithDescription(fullDescription); - JToken jId = Bot.API.Modules.WriteableConfig.GetConfig()["StatusChannelMessageId"]; - if (jId == null) - { - Task.Run(async () => - { - SocketTextChannel channel = GetChannel(); - if (channel == null) - { - Log.Error( - "Either the guild is null or the channel is null. So the status message has failed to send."); - return; - } - - RestUserMessage msg = await channel.SendMessageAsync(null, false, embed.Build()); - Bot.API.Modules.WriteableConfig.WriteConfigOption("StatusChannelMessageId", msg.Id); - }); - } - else - { - SocketTextChannel channel = GetChannel(); - if (channel == null) - { - Log.Error( - "Either the guild is null or the channel is null. So the status message has failed to be edited."); - return; - } - - Task.Run(async () => - { - ulong id = jId.ToObject(); - IMessage oldMessage = channel.GetCachedMessage(id) ?? await channel.GetMessageAsync(id); - if (oldMessage == null || - oldMessage.Author.Id != Bot.Handlers.DiscordBot.Instance.Client.CurrentUser.Id) - { - RestUserMessage msg = await channel.SendMessageAsync(null, false, embed.Build()); - Bot.API.Modules.WriteableConfig.WriteConfigOption("StatusChannelMessageId", msg.Id); - return; - } - - IUserMessage message = (IUserMessage)oldMessage; - - await message.ModifyAsync(msg => - { - msg.Content = null; - msg.Embed = embed.Build(); - }); - }); - } - } - } -} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Handlers/Events.cs b/DiscordLab.StatusChannel/Handlers/Events.cs deleted file mode 100644 index dce3d4d..0000000 --- a/DiscordLab.StatusChannel/Handlers/Events.cs +++ /dev/null @@ -1,59 +0,0 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Features; -using Exiled.Events.EventArgs.Player; - -namespace DiscordLab.StatusChannel.Handlers -{ - public class Events : IRegisterable - { - public void Init() - { - Exiled.Events.Handlers.Server.WaitingForPlayers += OnWaitingForPlayers; - Exiled.Events.Handlers.Server.RoundStarted += OnRoundStarted; - Exiled.Events.Handlers.Player.Verified += OnPlayerVerified; - Exiled.Events.Handlers.Player.Left += OnPlayerLeave; - } - - public void Unregister() - { - Exiled.Events.Handlers.Server.WaitingForPlayers -= OnWaitingForPlayers; - Exiled.Events.Handlers.Server.RoundStarted -= OnRoundStarted; - Exiled.Events.Handlers.Player.Verified -= OnPlayerVerified; - Exiled.Events.Handlers.Player.Left -= OnPlayerLeave; - } - - private void OnWaitingForPlayers() - { - DiscordBot.Instance.SetStatusMessage(); - } - - private void OnRoundStarted() - { - DiscordBot.Instance.SetStatusMessage(); - } - - private void OnPlayerVerified(VerifiedEventArgs ev) - { - if (Round.InProgress) - DiscordBot.Instance.SetStatusMessage(); - else - QueueSystem.QueueRun("DiscordLab.StatusChannel.OnPlayerVerified", () => - DiscordBot.Instance.SetStatusMessage() - ); - } - - private void OnPlayerLeave(LeftEventArgs ev) - { - List players = Player.List.Where(p => p != ev.Player && !p.IsNPC).ToList(); - if (Round.InProgress || !players.Any()) - DiscordBot.Instance.SetStatusMessage( - players - ); - else - QueueSystem.QueueRun("DiscordLab.StatusChannel.OnPlayerLeave", () => - DiscordBot.Instance.SetStatusMessage() - ); - } - } -} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/MessageConfig.cs b/DiscordLab.StatusChannel/MessageConfig.cs new file mode 100644 index 0000000..e900c09 --- /dev/null +++ b/DiscordLab.StatusChannel/MessageConfig.cs @@ -0,0 +1,10 @@ +using System.ComponentModel; + +namespace DiscordLab.StatusChannel +{ + public class MessageConfig + { + [Description("Do not set this, this will be set for you.")] + public ulong MessageId { get; set; } = 0; + } +} \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index ca567f9..e42c7df 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -1,39 +1,55 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Interfaces; +using LabApi.Events.CustomHandlers; +using LabApi.Events.Handlers; +using LabApi.Features; +using LabApi.Loader; namespace DiscordLab.StatusChannel { public class Plugin : Plugin { - public override string Name => "DiscordLab.StatusChannel"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.StatusChannel"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Default; - - public static Plugin Instance { get; private set; } + public static Plugin Instance; + + public override string Name { get; } = "DiscordLab.StatusChannel"; + public override string Description { get; } = "Allows you to update/send a status message in a specific channel and have it update automatically."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - private HandlerLoader _handlerLoader; + public MessageConfig MessageConfig { get; set; } - public override void OnEnabled() + public Events Events = new(); + + public override void Enable() { Instance = this; - _handlerLoader = new (); - if(!_handlerLoader.Load(Assembly)) return; + CallOnReadyAttribute.Load(); - base.OnEnabled(); + CustomHandlersManager.RegisterEventsHandler(Events); + + if(Config.AddCommand) + ISlashCommand.FindAll(); } - - public override void OnDisabled() + + public override void Disable() { - _handlerLoader.Unload(); - _handlerLoader = null; + CallOnUnloadAttribute.Unload(); - base.OnDisabled(); + CustomHandlersManager.UnregisterEventsHandler(Events); + + Events = null; + + Instance = null; + } + + public override void LoadConfigs() + { + this.TryLoadConfig("message_config.yml", out MessageConfig messageConfig); + MessageConfig = messageConfig ?? new(); + base.LoadConfigs(); } } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Properties/AssemblyInfo.cs b/DiscordLab.StatusChannel/Properties/AssemblyInfo.cs deleted file mode 100644 index fe61449..0000000 --- a/DiscordLab.StatusChannel/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.StatusChannel")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.StatusChannel")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("40CCCDFD-2112-4A2A-B952-3EDD16D6057B")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index 3cef44d..d4846c7 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -1,21 +1,33 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; +using System.ComponentModel; +using Discord; namespace DiscordLab.StatusChannel { - public class Translation : ITranslation + public class Translation { - [Description("The text that shows in the embed title.")] - public string EmbedTitle { get; set; } = "Server Status"; + [Description("What will show when the server has players.")] + public EmbedBuilder Embed { get; set; } = new() + { + Title = "Server Status", + Color = Color.Blue, + Description = "{playercount}/{maxplayers} currently online\\n```{players}```" + }; - [Description( - "The text that shows in the embed description when the server has players online. players placeholder is the list of players using the player list translation.")] - public string EmbedDescription { get; set; } = "{current}/{max} currently online\n```{players}```"; + [Description("What will show when the server is empty.")] + public EmbedBuilder EmbedEmpty { get; set; } = new() + { + Title = "Server Status", + Color = Color.Blue, + Description = "0/{maxplayers} currently online" + }; - [Description("The text that shows in the embed description when the server has no players online.")] - public string EmbedNoPlayers { get; set; } = "0/{max} currently online"; + [Description("What will appear for each player when replacing the players variable above.")] + public string PlayerItem { get; set; } = "- {player}"; - [Description("The text that shows for each player in the players list in embed description. Make sure you don't put placeholders here which could break VSR if public.")] - public string PlayersList { get; set; } = "- {player}"; + public SlashCommandBuilder PlayerListCommand = new() + { + Name = "players", + Description = "Get the current list of players on the server" + }; } } \ No newline at end of file diff --git a/DiscordLab.XPSystem/Commands/GetLevel.cs b/DiscordLab.XPSystem/Commands/GetLevel.cs deleted file mode 100644 index b8240a4..0000000 --- a/DiscordLab.XPSystem/Commands/GetLevel.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Globalization; -using Discord; -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using XPSystem.API; -using XPSystem.API.StorageProviders; -using XPSystem.API.StorageProviders.Models; - -namespace DiscordLab.XPSystem.Commands -{ - public class GetLevel : ISlashCommand - { - public SlashCommandBuilder Data { get; } = new() - { - Name = Plugin.Instance.Translation.CommandName, - Description = Plugin.Instance.Translation.CommandDescription, - Options = new() - { - new() - { - Name = Plugin.Instance.Translation.CommandOptionName, - Description = Plugin.Instance.Translation.CommandOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - } - } - }; - - public ulong GuildId { get; set; } = Plugin.Instance.Config.GuildId; - - public async Task Run(SocketSlashCommand command) - { - if (command.Data.Options.Count == 0) return; - string option = command.Data.Options.First().Value.ToString(); - if (XPAPI.TryParseUserId(option, out IPlayerId playerId) == false) - { - await command.RespondAsync(Plugin.Instance.Translation.FailToGetUser); - return; - } - - PlayerInfoWrapper info = XPAPI.GetPlayerInfo(playerId); - EmbedBuilder embed = new() - { - Title = Plugin.Instance.Translation.EmbedTitle, - Description = Plugin.Instance.Translation.EmbedDescription.LowercaseParams().Replace("{level}", info.Level.ToString()) - .Replace("{currentxp}", info.XP.ToString()).Replace("{neededxp}", info.NeededXPCurrent.ToString()).StaticReplace(), - Footer = new() - { - Text = Plugin.Instance.Translation.EmbedFooter.LowercaseParams().Replace("{user}", info.Nickname) - .Replace("{userid}", option).StaticReplace() - }, - Color = new Color(uint.Parse(Plugin.Instance.Config.Color, NumberStyles.HexNumber)) - }; - await command.RespondAsync(embed:embed.Build()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.XPSystem/Config.cs b/DiscordLab.XPSystem/Config.cs deleted file mode 100644 index 556a59d..0000000 --- a/DiscordLab.XPSystem/Config.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using Discord; -using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Interfaces; - -namespace DiscordLab.XPSystem -{ - public class Config : IConfig, IDLConfig - { - [Description(DescriptionConstants.IsEnabled)] - public bool IsEnabled { get; set; } = true; - [Description(DescriptionConstants.Debug)] - public bool Debug { get; set; } = false; - - [Description("The channel ID to send the level up messages to.")] - public ulong ChannelId { get; set; } = new(); - - [Description("The hex color code of the embed. Do not include the #.")] - public string Color { get; set; } = "3498DB"; - - [Description(DescriptionConstants.GuildId)] - public ulong GuildId { get; set; } - } -} \ No newline at end of file diff --git a/DiscordLab.XPSystem/DiscordLab.XPSystem.csproj b/DiscordLab.XPSystem/DiscordLab.XPSystem.csproj deleted file mode 100644 index 2f091b2..0000000 --- a/DiscordLab.XPSystem/DiscordLab.XPSystem.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - net48 - enable - disable - preview - x64 - true - false - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiscordLab.XPSystem/FodyWeavers.xml b/DiscordLab.XPSystem/FodyWeavers.xml deleted file mode 100644 index 445194d..0000000 --- a/DiscordLab.XPSystem/FodyWeavers.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - Discord.Net.Websocket - Discord.Net.Core - Microsoft.Bcl.AsyncInterfaces - System.Collections.Immutable - System.Threading.Tasks.Extensions - System.ValueTuple - - - \ No newline at end of file diff --git a/DiscordLab.XPSystem/FodyWeavers.xsd b/DiscordLab.XPSystem/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.XPSystem/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.XPSystem/Handlers/DiscordBot.cs b/DiscordLab.XPSystem/Handlers/DiscordBot.cs deleted file mode 100644 index 3b4815b..0000000 --- a/DiscordLab.XPSystem/Handlers/DiscordBot.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.XPSystem.Handlers -{ - - public class DiscordBot : IRegisterable - { - private static Translation Translation => Plugin.Instance.Translation; - - public static DiscordBot Instance { get; private set; } - - private SocketTextChannel Channel { get; set; } - - public void Init() - { - Instance = this; - } - - public void Unregister() - { - Channel = null; - } - - public SocketTextChannel GetChannel() - { - SocketGuild guild = Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId); - if (Bot.Handlers.DiscordBot.Instance.GetGuild(Plugin.Instance.Config.GuildId) == null) return null; - if (Plugin.Instance.Config.ChannelId == 0) return null; - return Channel ??= guild.GetTextChannel(Plugin.Instance.Config.ChannelId); - } - } -} \ No newline at end of file diff --git a/DiscordLab.XPSystem/Handlers/Events.cs b/DiscordLab.XPSystem/Handlers/Events.cs deleted file mode 100644 index 96473c3..0000000 --- a/DiscordLab.XPSystem/Handlers/Events.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Discord.WebSocket; -using DiscordLab.Bot.API.Extensions; -using DiscordLab.Bot.API.Interfaces; -using Exiled.API.Features; -using XPSystem.API; -using XPSystem.API.Player; - -namespace DiscordLab.XPSystem.Handlers -{ - - public class Events : IRegisterable - { - public void Init() - { - XPAPI.PlayerLevelUp += OnPlayerLevelUp; - } - - public void Unregister() - { - XPAPI.PlayerLevelUp -= OnPlayerLevelUp; - } - - private void OnPlayerLevelUp(XPPlayer player, int newLevel, int _) - { - SocketTextChannel channel = DiscordBot.Instance.GetChannel(); - if (channel == null) - { - Log.Error( - "Either the channel or guild could not be found. So the XPSystem level up message has failed to send."); - } - - DiscordBot.Instance.GetChannel().SendMessageAsync(Plugin.Instance.Translation.LevelUp.LowercaseParams() - .Replace("{playername}", player.Nickname).Replace("{playerid}", player.UserId) - .Replace("{level}", newLevel.ToString()).StaticReplace()); - } - } -} \ No newline at end of file diff --git a/DiscordLab.XPSystem/Plugin.cs b/DiscordLab.XPSystem/Plugin.cs deleted file mode 100644 index 9431ec7..0000000 --- a/DiscordLab.XPSystem/Plugin.cs +++ /dev/null @@ -1,48 +0,0 @@ -using DiscordLab.Bot.API.Interfaces; -using DiscordLab.Bot.API.Modules; -using Exiled.API.Enums; -using Exiled.API.Features; -using XPSystem; - -namespace DiscordLab.XPSystem -{ - public class Plugin : Plugin - { - public override string Name => "DiscordLab.XPSystem"; - public override string Author => "LumiFae"; - public override string Prefix => "DL.XPSystem"; - public override Version Version => new (1, 5, 0); - public override Version RequiredExiledVersion => new (8, 11, 0); - public override PluginPriority Priority => PluginPriority.Low; - - public static Plugin Instance { get; private set; } - - private HandlerLoader _handlerLoader; - - public override void OnEnabled() - { - if (Main.Instance.Version < new Version(2, 0, 8)) - { - Log.Error("DiscordLab.XPSystem requires XPSystem to be at least version 2.0.8!"); - return; - } - - Instance = this; - - _handlerLoader = new (); - - if(!_handlerLoader.Load(Assembly)) return; - - base.OnEnabled(); - } - - public override void OnDisabled() - { - _handlerLoader.Unload(); - - _handlerLoader = null; - - base.OnDisabled(); - } - } -} \ No newline at end of file diff --git a/DiscordLab.XPSystem/Properties/AssemblyInfo.cs b/DiscordLab.XPSystem/Properties/AssemblyInfo.cs deleted file mode 100644 index 5bbd26f..0000000 --- a/DiscordLab.XPSystem/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.XPSystem")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.XPSystem")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6D392968-BD8E-4449-A8EA-E09576F211E4")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.XPSystem/Translation.cs b/DiscordLab.XPSystem/Translation.cs deleted file mode 100644 index c130155..0000000 --- a/DiscordLab.XPSystem/Translation.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.ComponentModel; -using Exiled.API.Interfaces; - -namespace DiscordLab.XPSystem -{ - public class Translation : ITranslation - { - public string LevelUp { get; set; } = "Player {playername} ({playerid}) has leveled up to level {level}!"; - - [Description("The command name for getting the level of a player. Can not have spaces or special characters.")] - public string CommandName { get; set; } = "getlevel"; - - [Description("The description of the command.")] - public string CommandDescription { get; set; } = "Get the level of a player."; - - [Description("The name of the option for the command.")] - public string CommandOptionName { get; set; } = "player"; - - [Description("The description of the option for the command.")] - public string CommandOptionDescription { get; set; } = "The player to get the level of."; - - [Description("The message to send when the user fails to get a user.")] - public string FailToGetUser { get; set; } = "Failed to get user."; - - [Description("The title of the embed.")] - public string EmbedTitle { get; set; } = "Player Level"; - - [Description("The start of the description of the embed.")] - public string EmbedDescription { get; set; } = - "The level of the player is {level}.\nThey have {currentxp} out of {neededxp} XP."; - - [Description("The footer of the embed.")] - public string EmbedFooter { get; set; } = "Requested user: {user} ({userid})"; - } -} \ No newline at end of file diff --git a/DiscordLab.sln b/DiscordLab.sln index d6b517e..36846c2 100644 --- a/DiscordLab.sln +++ b/DiscordLab.sln @@ -1,28 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Bot", "DiscordLab.Bot\DiscordLab.Bot.csproj", "{108A7588-D546-40F7-96A5-A46301CA7D47}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Bot", "DiscordLab.Bot\DiscordLab.Bot.csproj", "{092A4D07-4DB9-4FB2-86F1-27ED3E252DAE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.StatusChannel", "DiscordLab.StatusChannel\DiscordLab.StatusChannel.csproj", "{40CCCDFD-2112-4A2A-B952-3EDD16D6057B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.BotStatus", "DiscordLab.BotStatus\DiscordLab.BotStatus.csproj", "{AFB30C66-D090-45A2-AEFD-2569001B3BCF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.BotStatus", "DiscordLab.BotStatus\DiscordLab.BotStatus.csproj", "{C2E4178B-327D-4D87-BAB3-6F5582F1745F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.StatusChannel", "DiscordLab.StatusChannel\DiscordLab.StatusChannel.csproj", "{ABCBB56D-E03B-4BBF-AA36-9D0365BF4977}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.ConnectionLogs", "DiscordLab.ConnectionLogs\DiscordLab.ConnectionLogs.csproj", "{3C60D907-52D8-436A-AE61-2433767AB195}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.RoundLogs", "DiscordLab.RoundLogs\DiscordLab.RoundLogs.csproj", "{BD3D7907-FBF0-4D65-9E7E-2181FE34C378}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.DeathLogs", "DiscordLab.DeathLogs\DiscordLab.DeathLogs.csproj", "{175B1499-7F87-4ED4-BFEE-F6E0207E4CD8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Administration", "DiscordLab.Administration\DiscordLab.Administration.csproj", "{EBEFD775-9E12-4F8E-9C9E-948611703EDA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.XPSystem", "DiscordLab.XPSystem\DiscordLab.XPSystem.csproj", "{6D392968-BD8E-4449-A8EA-E09576F211E4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Moderation", "DiscordLab.Moderation\DiscordLab.Moderation.csproj", "{6806DFF9-8907-49F0-B727-67AF6BD35844}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.AdvancedLogging", "DiscordLab.AdvancedLogging\DiscordLab.AdvancedLogging.csproj", "{43B91E66-9585-46C0-86AB-0DE55EF8D141}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.ConnectionLogs", "DiscordLab.ConnectionLogs\DiscordLab.ConnectionLogs.csproj", "{61AA3C8B-1907-4632-92F2-47B6F2CBF49B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.ModerationLogs", "DiscordLab.ModerationLogs\DiscordLab.ModerationLogs.csproj", "{E5AF10FD-689E-4A9E-87F0-E195112D89D3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Moderation", "DiscordLab.Moderation\DiscordLab.Moderation.csproj", "{51A2FE08-005B-41BC-9C70-AAE85D3A81CB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.SCPSwap", "DiscordLab.SCPSwap\DiscordLab.SCPSwap.csproj", "{3444CF96-2F51-42EA-A400-BDB24656E70E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.AdminLogs", "DiscordLab.AdminLogs\DiscordLab.AdminLogs.csproj", "{6BC3940C-7AD6-491C-BDAD-7B8545E643E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.RoundLogs", "DiscordLab.RoundLogs\DiscordLab.RoundLogs.csproj", "{4439C1F3-03C6-4D4B-94B3-7A97744C2DE9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.DeathLogs", "DiscordLab.DeathLogs\DiscordLab.DeathLogs.csproj", "{35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -30,53 +22,37 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {108A7588-D546-40F7-96A5-A46301CA7D47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {108A7588-D546-40F7-96A5-A46301CA7D47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {108A7588-D546-40F7-96A5-A46301CA7D47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {108A7588-D546-40F7-96A5-A46301CA7D47}.Release|Any CPU.Build.0 = Release|Any CPU - {40CCCDFD-2112-4A2A-B952-3EDD16D6057B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40CCCDFD-2112-4A2A-B952-3EDD16D6057B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40CCCDFD-2112-4A2A-B952-3EDD16D6057B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40CCCDFD-2112-4A2A-B952-3EDD16D6057B}.Release|Any CPU.Build.0 = Release|Any CPU - {C2E4178B-327D-4D87-BAB3-6F5582F1745F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2E4178B-327D-4D87-BAB3-6F5582F1745F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2E4178B-327D-4D87-BAB3-6F5582F1745F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2E4178B-327D-4D87-BAB3-6F5582F1745F}.Release|Any CPU.Build.0 = Release|Any CPU - {3C60D907-52D8-436A-AE61-2433767AB195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3C60D907-52D8-436A-AE61-2433767AB195}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C60D907-52D8-436A-AE61-2433767AB195}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C60D907-52D8-436A-AE61-2433767AB195}.Release|Any CPU.Build.0 = Release|Any CPU - {175B1499-7F87-4ED4-BFEE-F6E0207E4CD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {175B1499-7F87-4ED4-BFEE-F6E0207E4CD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {175B1499-7F87-4ED4-BFEE-F6E0207E4CD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {175B1499-7F87-4ED4-BFEE-F6E0207E4CD8}.Release|Any CPU.Build.0 = Release|Any CPU - {6D392968-BD8E-4449-A8EA-E09576F211E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D392968-BD8E-4449-A8EA-E09576F211E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D392968-BD8E-4449-A8EA-E09576F211E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D392968-BD8E-4449-A8EA-E09576F211E4}.Release|Any CPU.Build.0 = Release|Any CPU - {43B91E66-9585-46C0-86AB-0DE55EF8D141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {43B91E66-9585-46C0-86AB-0DE55EF8D141}.Debug|Any CPU.Build.0 = Debug|Any CPU - {43B91E66-9585-46C0-86AB-0DE55EF8D141}.Release|Any CPU.ActiveCfg = Release|Any CPU - {43B91E66-9585-46C0-86AB-0DE55EF8D141}.Release|Any CPU.Build.0 = Release|Any CPU - {E5AF10FD-689E-4A9E-87F0-E195112D89D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5AF10FD-689E-4A9E-87F0-E195112D89D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5AF10FD-689E-4A9E-87F0-E195112D89D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5AF10FD-689E-4A9E-87F0-E195112D89D3}.Release|Any CPU.Build.0 = Release|Any CPU - {51A2FE08-005B-41BC-9C70-AAE85D3A81CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51A2FE08-005B-41BC-9C70-AAE85D3A81CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51A2FE08-005B-41BC-9C70-AAE85D3A81CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51A2FE08-005B-41BC-9C70-AAE85D3A81CB}.Release|Any CPU.Build.0 = Release|Any CPU - {3444CF96-2F51-42EA-A400-BDB24656E70E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3444CF96-2F51-42EA-A400-BDB24656E70E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3444CF96-2F51-42EA-A400-BDB24656E70E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3444CF96-2F51-42EA-A400-BDB24656E70E}.Release|Any CPU.Build.0 = Release|Any CPU - {6BC3940C-7AD6-491C-BDAD-7B8545E643E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6BC3940C-7AD6-491C-BDAD-7B8545E643E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BC3940C-7AD6-491C-BDAD-7B8545E643E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6BC3940C-7AD6-491C-BDAD-7B8545E643E2}.Release|Any CPU.Build.0 = Release|Any CPU - {4439C1F3-03C6-4D4B-94B3-7A97744C2DE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4439C1F3-03C6-4D4B-94B3-7A97744C2DE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4439C1F3-03C6-4D4B-94B3-7A97744C2DE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4439C1F3-03C6-4D4B-94B3-7A97744C2DE9}.Release|Any CPU.Build.0 = Release|Any CPU + {092A4D07-4DB9-4FB2-86F1-27ED3E252DAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {092A4D07-4DB9-4FB2-86F1-27ED3E252DAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {092A4D07-4DB9-4FB2-86F1-27ED3E252DAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {092A4D07-4DB9-4FB2-86F1-27ED3E252DAE}.Release|Any CPU.Build.0 = Release|Any CPU + {AFB30C66-D090-45A2-AEFD-2569001B3BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFB30C66-D090-45A2-AEFD-2569001B3BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFB30C66-D090-45A2-AEFD-2569001B3BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFB30C66-D090-45A2-AEFD-2569001B3BCF}.Release|Any CPU.Build.0 = Release|Any CPU + {ABCBB56D-E03B-4BBF-AA36-9D0365BF4977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABCBB56D-E03B-4BBF-AA36-9D0365BF4977}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABCBB56D-E03B-4BBF-AA36-9D0365BF4977}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABCBB56D-E03B-4BBF-AA36-9D0365BF4977}.Release|Any CPU.Build.0 = Release|Any CPU + {BD3D7907-FBF0-4D65-9E7E-2181FE34C378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD3D7907-FBF0-4D65-9E7E-2181FE34C378}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD3D7907-FBF0-4D65-9E7E-2181FE34C378}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD3D7907-FBF0-4D65-9E7E-2181FE34C378}.Release|Any CPU.Build.0 = Release|Any CPU + {EBEFD775-9E12-4F8E-9C9E-948611703EDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBEFD775-9E12-4F8E-9C9E-948611703EDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBEFD775-9E12-4F8E-9C9E-948611703EDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBEFD775-9E12-4F8E-9C9E-948611703EDA}.Release|Any CPU.Build.0 = Release|Any CPU + {6806DFF9-8907-49F0-B727-67AF6BD35844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6806DFF9-8907-49F0-B727-67AF6BD35844}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6806DFF9-8907-49F0-B727-67AF6BD35844}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6806DFF9-8907-49F0-B727-67AF6BD35844}.Release|Any CPU.Build.0 = Release|Any CPU + {61AA3C8B-1907-4632-92F2-47B6F2CBF49B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61AA3C8B-1907-4632-92F2-47B6F2CBF49B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61AA3C8B-1907-4632-92F2-47B6F2CBF49B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61AA3C8B-1907-4632-92F2-47B6F2CBF49B}.Release|Any CPU.Build.0 = Release|Any CPU + {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/stylecop.ruleset b/stylecop.ruleset new file mode 100644 index 0000000..6caba2e --- /dev/null +++ b/stylecop.ruleset @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From 049070e708dc5e9bfbd406169c0cc1384c859d2f Mon Sep 17 00:00:00 2001 From: Lumi Date: Mon, 7 Jul 2025 15:58:28 +0100 Subject: [PATCH 02/68] fix: bugs that were found --- Directory.Build.props | 20 ++-- .../Commands/SendCommand.cs | 38 +++---- DiscordLab.Administration/Plugin.cs | 4 +- DiscordLab.Administration/Translation.cs | 21 ++-- .../API/Extensions/BitwiseExtensions.cs | 9 ++ .../AutocompleteCommand.cs} | 6 +- .../SlashCommand.cs} | 41 ++++--- .../API/Features/TranslationBuilder.cs | 74 +++++++------ DiscordLab.Bot/API/Updates/Module.cs | 10 +- DiscordLab.Bot/API/Utilities/CommandUtils.cs | 7 +- DiscordLab.Bot/API/Utilities/LoggingUtils.cs | 2 +- DiscordLab.Bot/Client.cs | 20 +++- DiscordLab.Bot/Commands/DiscordCommand.cs | 12 +-- DiscordLab.Bot/DiscordLab.Bot.csproj | 16 ++- DiscordLab.Bot/Patches/RestClientCreate.cs | 90 ++++++++++++++++ DiscordLab.Bot/Plugin.cs | 18 ++-- DiscordLab.BotStatus/Plugin.cs | 1 + DiscordLab.ConnectionLogs/Events.cs | 4 + DiscordLab.ConnectionLogs/Plugin.cs | 1 + DiscordLab.DeathLogs/DamageLogs.cs | 4 +- DiscordLab.DeathLogs/Events.cs | 102 ++++++++++-------- DiscordLab.DeathLogs/Plugin.cs | 2 +- .../DiscordLab.Dependency.csproj | 12 +++ .../Plugin.cs | 8 +- DiscordLab.Moderation/Commands/Ban.cs | 56 ++++++---- DiscordLab.Moderation/Commands/Mute.cs | 41 +++++-- .../Commands/TempMuteRemoteAdmin.cs | 4 +- DiscordLab.Moderation/Commands/Unban.cs | 40 +++---- DiscordLab.Moderation/Commands/Unmute.cs | 41 +++---- .../DiscordLab.Moderation.csproj | 2 +- DiscordLab.Moderation/Plugin.cs | 20 +--- .../Properties/AssemblyInfo.cs | 35 ------ DiscordLab.Moderation/TempMuteManager.cs | 2 +- DiscordLab.Moderation/Translation.cs | 96 +++++------------ DiscordLab.RoundLogs/Events.cs | 10 +- DiscordLab.RoundLogs/Plugin.cs | 1 + DiscordLab.StatusChannel/Command.cs | 14 ++- DiscordLab.StatusChannel/Plugin.cs | 4 +- DiscordLab.StatusChannel/Translation.cs | 8 +- DiscordLab.sln | 6 ++ 40 files changed, 514 insertions(+), 388 deletions(-) rename DiscordLab.Bot/API/{Interfaces/IAutocompleteCommand.cs => Features/AutocompleteCommand.cs} (67%) rename DiscordLab.Bot/API/{Interfaces/ISlashCommand.cs => Features/SlashCommand.cs} (74%) create mode 100644 DiscordLab.Bot/Patches/RestClientCreate.cs create mode 100644 DiscordLab.Dependency/DiscordLab.Dependency.csproj rename {DiscordLab.Bot/API/Features => DiscordLab.Dependency}/Plugin.cs (85%) delete mode 100644 DiscordLab.Moderation/Properties/AssemblyInfo.cs diff --git a/Directory.Build.props b/Directory.Build.props index 23d22aa..d9e51b2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,14 +10,18 @@ - - - - - - - - + + + + + + + + + + + + diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index bf5bd5f..6654379 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -2,35 +2,37 @@ using Discord; using Discord.WebSocket; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; using LabApi.Features.Wrappers; using RemoteAdmin; namespace DiscordLab.Administration.Commands { - public class SendCommand : IAutocompleteCommand + public class SendCommand : AutocompleteCommand { public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - public SlashCommandBuilder Data + public override SlashCommandBuilder Data { get; } = new() { - get - { - SlashCommandBuilder builder = Translation.SendCommand; - SlashCommandOptionBuilder option = builder.Options.First(); - option.IsAutocomplete = true; - option.Type = ApplicationCommandOptionType.String; - option.IsRequired = true; - - return builder; - } - } - - public ulong GuildId { get; } = Config.GuildId; + Name = Translation.SendCommandName, + Description = Translation.SendCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.SendCommandOptionName, + Description = Translation.SendCommandOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + } + ] + }; + + public override ulong GuildId { get; } = Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -42,7 +44,7 @@ public async Task Run(SocketSlashCommand command) await command.ModifyOriginalResponseAsync(m => m.Content = builder); } - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { IEnumerable commands = [ diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs index 1dcdcb4..f7ceadb 100644 --- a/DiscordLab.Administration/Plugin.cs +++ b/DiscordLab.Administration/Plugin.cs @@ -1,6 +1,6 @@ using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Dependency; using HarmonyLib; using LabApi.Events.CustomHandlers; using LabApi.Features; @@ -31,7 +31,7 @@ public override void Enable() CallOnLoadAttribute.Load(); if(Config.AddCommands) - ISlashCommand.FindAll(); + SlashCommand.FindAll(); CustomHandlersManager.RegisterEventsHandler(Events); } diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs index f518a4f..07a3f8f 100644 --- a/DiscordLab.Administration/Translation.cs +++ b/DiscordLab.Administration/Translation.cs @@ -13,20 +13,13 @@ public class Translation public string CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; - public SlashCommandBuilder SendCommand { get; set; } = new() - { - Name = "send", - Description = "Sends a command to the server", - DefaultMemberPermissions = GuildPermission.Administrator, - Options = - [ - new() - { - Name = "command", - Description = "The command to send" - } - ] - }; + public string SendCommandName { get; set; } = "send"; + + public string SendCommandDescription { get; set; } = "Sends a command to the server"; + + public string SendCommandOptionName { get; set; } = "command"; + + public string SendCommandOptionDescription { get; set; } = "The command to send"; public string SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; } diff --git a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs index 1e8a4a9..4bfa94e 100644 --- a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs @@ -1,7 +1,16 @@ namespace DiscordLab.Bot.API.Extensions { + /// + /// Contains extension methods for bitwise operations. + /// public static class BitwiseExtensions { + /// + /// Get flags from a . + /// + /// The flags. + /// The . + /// The flags that are active. public static IEnumerable GetFlags(this T flags) where T : Enum => Enum.GetValues(typeof(T)).Cast().Where(x => flags.HasFlag(x)); } diff --git a/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs similarity index 67% rename from DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs rename to DiscordLab.Bot/API/Features/AutocompleteCommand.cs index 028eec8..90b757f 100644 --- a/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs +++ b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs @@ -1,17 +1,17 @@ -namespace DiscordLab.Bot.API.Interfaces +namespace DiscordLab.Bot.API.Features { using Discord.WebSocket; /// /// Allows you to make autocomplete commands. /// - public interface IAutocompleteCommand : ISlashCommand + public abstract class AutocompleteCommand : SlashCommand { /// /// The method that is called once an autocomplete request is made. /// /// The autocomplete instance. /// The . - public Task Autocomplete(SocketAutocompleteInteraction autocomplete); + public abstract Task Autocomplete(SocketAutocompleteInteraction autocomplete); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs similarity index 74% rename from DiscordLab.Bot/API/Interfaces/ISlashCommand.cs rename to DiscordLab.Bot/API/Features/SlashCommand.cs index 88fc90a..0097a5b 100644 --- a/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs +++ b/DiscordLab.Bot/API/Features/SlashCommand.cs @@ -1,4 +1,4 @@ -namespace DiscordLab.Bot.API.Interfaces +namespace DiscordLab.Bot.API.Features { using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -11,26 +11,24 @@ namespace DiscordLab.Bot.API.Interfaces /// /// A wrapper to easily add your own slash commands in your bot. /// - public interface ISlashCommand + public abstract class SlashCommand { - public static ObservableCollection Commands = []; - /// - /// Gets the slash command data. + /// The list of currently active s. /// - public SlashCommandBuilder Data { get; } +#pragma warning disable SA1401 + public static ObservableCollection Commands = []; +#pragma warning restore SA1401 /// - /// Gets the guild ID to assign this command to. + /// Gets the slash command data. /// - public ulong GuildId { get; } + public abstract SlashCommandBuilder Data { get; } /// - /// What should be called when you run this command. + /// Gets the guild ID to assign this command to. /// - /// The command data. - /// A . - public Task Run(SocketSlashCommand command); + public abstract ulong GuildId { get; } /// /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself. @@ -42,14 +40,21 @@ public static void FindAll(Assembly assembly = null) foreach (Type type in assembly.GetTypes()) { - if (type.IsAbstract || !typeof(ISlashCommand).IsAssignableFrom(type)) + if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type)) continue; - ISlashCommand init = Activator.CreateInstance(type) as ISlashCommand; + SlashCommand init = Activator.CreateInstance(type) as SlashCommand; Commands.Add(init); } } + /// + /// What should be called when you run this command. + /// + /// The command data. + /// A . + public abstract Task Run(SocketSlashCommand command); + [CallOnLoad] private static void Start() { @@ -69,19 +74,21 @@ private static void Ready() Task.Run(() => RegisterGuildCommands(Commands)); } +#pragma warning disable SA1313 private static void OnCollectionChanged(object _, NotifyCollectionChangedEventArgs ev) +#pragma warning restore SA1313 { if (!Client.IsClientReady) return; if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace)) return; - Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems)); + Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems)); } - private static async Task RegisterGuildCommands(IEnumerable commands) + private static async Task RegisterGuildCommands(IEnumerable commands) { - foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) + foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) { SocketGuild guild = Client.GetGuild(cmds.Key); if (guild == null) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index e7f61c9..3293b16 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -1,9 +1,8 @@ -using Discord; - namespace DiscordLab.Bot.API.Features { using System.Globalization; using System.Text.RegularExpressions; + using Discord; using LabApi.Features.Extensions; using LabApi.Features.Wrappers; using PlayerRoles; @@ -13,10 +12,36 @@ namespace DiscordLab.Bot.API.Features /// public class TranslationBuilder { + private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); + + private static readonly Regex UselessTextRemoveRegex = new(@"(.*?)<\/color>", RegexOptions.Compiled); + /// - /// The dictionary of replacers that have no argument. + /// Initializes a new instance of the class. /// - public static Dictionary> StaticReplacers = new() + /// The translation to modify. + public TranslationBuilder(string translation) + { + Translation = translation; + } + + /// + /// Initializes a new instance of the class with a person added. + /// + /// The translation to modify. + /// The player prefix. + /// The player to use for the prefix. + public TranslationBuilder(string translation, string playerPrefix, Player player) + { + Translation = translation; + AddPlayer(playerPrefix, player); + } + +#pragma warning disable SA1401 // FieldsMustBePrivate + /// + /// Gets the dictionary of replacers that have no argument. + /// + public static Dictionary> StaticReplacers { get; } = new() { // Map Replacers ["seed"] = () => Map.Seed.ToString(), @@ -53,9 +78,9 @@ public class TranslationBuilder }; /// - /// Time based replacers. The type is the unix timestamp. Can be got with . + /// Gets time based replacers. The type is the unix timestamp. Can be got with . /// - public static Dictionary> TimeReplacers = new() + public static Dictionary> TimeReplacers { get; } = new() { ["time"] = time => $"", ["timet"] = time => $"", @@ -70,9 +95,9 @@ public class TranslationBuilder }; /// - /// Player based replacements. + /// Gets player based replacements. /// - public static Dictionary> PlayerReplacers = new() + public static Dictionary> PlayerReplacers { get; } = new() { ["nickname"] = player => player.Nickname.Replace("@", "\\@"), ["id"] = player => player.UserId, @@ -87,31 +112,7 @@ public class TranslationBuilder ["group"] = player => player.GroupName, ["badgecolor"] = player => player.GroupColor.ToString(), }; - - private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); - - private static readonly Regex UselessTextRemoveRegex = new(@"(.*?)<\/color>", RegexOptions.Compiled); - - /// - /// Initializes a new instance of the class. - /// - /// The translation to modify. - public TranslationBuilder(string translation) - { - Translation = translation; - } - - /// - /// Initializes a new instance of the class with a person added. - /// - /// The translation to modify. - /// The player prefix. - /// The player to use for the prefix. - public TranslationBuilder(string translation, string playerPrefix, Player player) - { - Translation = translation; - AddPlayer(playerPrefix, player); - } +#pragma warning restore SA1401 // FieldsMustBePrivate /// /// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with. @@ -144,6 +145,11 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// public string PlayerListSeparator { get; set; } = "\n"; + /// + /// Gets or sets the player list that will be used for . + /// + public IEnumerable PlayerList { get; set; } + /// /// . /// @@ -292,7 +298,7 @@ public string Build() private void SetupPlayerList() { - Player[] readyPlayers = Player.ReadyList.ToArray(); + Player[] readyPlayers = (PlayerList ?? Player.ReadyList).ToArray(); int length = readyPlayers.Length; diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Bot/API/Updates/Module.cs index f3f5d12..ff12ef5 100644 --- a/DiscordLab.Bot/API/Updates/Module.cs +++ b/DiscordLab.Bot/API/Updates/Module.cs @@ -1,9 +1,8 @@ -using HarmonyLib; -using LabApi.Loader; -using LabApi.Loader.Features.Paths; - namespace DiscordLab.Bot.API.Updates { + using LabApi.Loader; + using LabApi.Loader.Features.Paths; + /// /// Contains information about a DiscordLab module. /// @@ -60,8 +59,7 @@ public Module(GitHubRelease release, GitHubReleaseAsset asset) /// The generated string. public static string GenerateUpdateString(IEnumerable modules) => string.Join( "\n- ", modules.Select(module => - $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}") - ); + $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}")); /// /// Downloads this module. diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs index 58aeefe..4d564aa 100644 --- a/DiscordLab.Bot/API/Utilities/CommandUtils.cs +++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs @@ -1,7 +1,10 @@ -using LabApi.Features.Wrappers; - namespace DiscordLab.Bot.API.Utilities { + using LabApi.Features.Wrappers; + + /// + /// Utility methods for commands. + /// public static class CommandUtils { /// diff --git a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs index 327938a..3523ffa 100644 --- a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs +++ b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs @@ -9,7 +9,7 @@ public static class LoggingUtils /// Generates a message that will tell the user that the channel was not found. /// /// The submodule that this error comes from. - /// The channel ID that was missing + /// The channel ID that was missing. /// The related guild ID, goes to the default guild ID if 0. /// The error string. public static string GenerateMissingChannelMessage(string type, ulong channelId, ulong guildId) diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs index 4d99d0a..cd80598 100644 --- a/DiscordLab.Bot/Client.cs +++ b/DiscordLab.Bot/Client.cs @@ -7,7 +7,7 @@ namespace DiscordLab.Bot using Discord.Net.WebSockets; using Discord.WebSocket; using DiscordLab.Bot.API.Attributes; - using DiscordLab.Bot.API.Interfaces; + using DiscordLab.Bot.API.Features; using LabApi.Features.Console; /// @@ -28,7 +28,7 @@ public static class Client /// /// Gets a list of saved text channels listed by their ID. /// - public static Dictionary SavedTextChannels { get; } = new(); + public static Dictionary SavedTextChannels { get; private set; } = new(); /// /// Gets the default guild for the plugin. @@ -73,24 +73,36 @@ public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) [CallOnLoad] internal static void Start() { + DebugLog("Starting the Client"); DiscordSocketConfig config = new() { GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages, LogLevel = Config.Debug ? LogSeverity.Debug : LogSeverity.Warning, + RestClientProvider = DefaultRestClientProvider.Create(), + WebSocketProvider = DefaultWebSocketProvider.Create(), }; if (!string.IsNullOrEmpty(Config.ProxyUrl)) { + DebugLog("Proxy is configured."); WebProxy proxy = new(Config.ProxyUrl); config.RestClientProvider = DefaultRestClientProvider.Create(true, proxy); config.WebSocketProvider = DefaultWebSocketProvider.Create(proxy); } + DebugLog("Done the initial setup..."); + SocketClient = new(config); + + DebugLog("Client has been created..."); + SocketClient.Log += OnLog; SocketClient.Ready += OnReady; SocketClient.SlashCommandExecuted += SlashCommandHandler; SocketClient.AutocompleteExecuted += AutocompleteHandler; + + DebugLog("Client events subscribed..."); + Task.Run(StartClient); } @@ -157,7 +169,7 @@ private static Task OnReady() private static Task SlashCommandHandler(SocketSlashCommand command) { DebugLog($"{command.Data.Name} requested a response, finding the command..."); - ISlashCommand cmd = ISlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); + SlashCommand cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); cmd?.Run(command); return Task.CompletedTask; @@ -166,7 +178,7 @@ private static Task SlashCommandHandler(SocketSlashCommand command) private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) { DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); - IAutocompleteCommand command = ISlashCommand.Commands.FirstOrDefault(c => c is IAutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as IAutocompleteCommand; + AutocompleteCommand command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; command?.Autocomplete(autocomplete); return Task.CompletedTask; diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index d1ce11e..18fa7ba 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -2,14 +2,14 @@ namespace DiscordLab.Bot.Commands { using Discord; using Discord.WebSocket; - using DiscordLab.Bot.API.Interfaces; + using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Updates; /// - public class DiscordCommand : IAutocompleteCommand + public class DiscordCommand : AutocompleteCommand { /// - public SlashCommandBuilder Data { get; } = new() + public override SlashCommandBuilder Data { get; } = new() { Name = "discordlab", Description = "DiscordLab related commands", @@ -53,10 +53,10 @@ public class DiscordCommand : IAutocompleteCommand }; /// - public ulong GuildId { get; } = 0; + public override ulong GuildId { get; } = 0; /// - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(true); string subcommand = command.Data.Options.First().Name; @@ -108,7 +108,7 @@ await command.ModifyOriginalResponseAsync(m => } /// - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot").Select(x => new AutocompleteResult(x.Name, x.Name))); } diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 1f8fab3..4fad09f 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -3,10 +3,9 @@ net48 enable disable - 12 + 13 x64 true - false 2.0.0 @@ -24,15 +23,24 @@ ../stylecop.ruleset + + + + True + \ + + - + + + - + \ No newline at end of file diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs new file mode 100644 index 0000000..0c0e457 --- /dev/null +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -0,0 +1,90 @@ +namespace DiscordLab.Bot.Patches +{ + using System.Net; + using System.Net.Http; + using System.Reflection; + using System.Reflection.Emit; + using Discord.Net.Rest; + using HarmonyLib; + + /// + /// Patches . + /// + [HarmonyPatch] + public static class RestClientCreate + { + /// + /// Gets the target method to patch. + /// + /// The method. + public static MethodBase TargetMethod() + { + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); + if (type == null) + continue; + ConstructorInfo constructor = type.GetConstructors().FirstOrDefault(); + if (constructor != null) + return constructor; + } + + return null; + } + + /// + /// The patch. + /// + /// The instructions. + /// The patched code. + public static IEnumerable Transpiler(IEnumerable instructions) + { + CodeMatcher matcher = new CodeMatcher(instructions) + .MatchEndForward( + new CodeMatch(OpCodes.Ldarg_0), + new CodeMatch(OpCodes.Newobj, AccessTools.Constructor(typeof(HttpClientHandler))), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldc_I4_3), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldc_I4_0), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldarg_2), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldarg_3), + new CodeMatch(OpCodes.Callvirt)); + + matcher.Advance(-13); + + matcher.RemoveInstructions(14) + .Insert( + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldc_I4_3), // DecompressionMethods.GZip | DecompressionMethods.Deflate + new CodeInstruction(OpCodes.Ldc_I4_0), // UseCookies = false + new CodeInstruction(OpCodes.Ldarg_2), // useProxy parameter + new CodeInstruction(OpCodes.Ldarg_3), // webProxy parameter + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(RestClientCreate), nameof(CreateHttpClientHandler)))); + + return matcher.InstructionEnumeration(); + } + + private static HttpClientHandler CreateHttpClientHandler(DecompressionMethods decompressionMethods, bool useCookies, bool useProxy, IWebProxy webProxy) + { + HttpClientHandler handler = new() + { + AutomaticDecompression = decompressionMethods, + UseCookies = useCookies, + }; + + if (!useProxy) + return handler; + + handler.UseProxy = true; + handler.Proxy = webProxy; + + return handler; + } + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/Plugin.cs b/DiscordLab.Bot/Plugin.cs index 4cb46d3..7e15047 100644 --- a/DiscordLab.Bot/Plugin.cs +++ b/DiscordLab.Bot/Plugin.cs @@ -1,8 +1,8 @@ -using DiscordLab.Bot.API.Interfaces; - -namespace DiscordLab.Bot +namespace DiscordLab.Bot { using DiscordLab.Bot.API.Attributes; + using DiscordLab.Bot.API.Features; + using HarmonyLib; using LabApi.Features; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; @@ -38,25 +38,31 @@ public sealed class Plugin : Plugin /// public new Config Config { get; private set; } + private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}"); + /// public override void Enable() { + Harmony.PatchAll(); + Instance = this; Config = base.Config; CallOnLoadAttribute.Load(); CallOnReadyAttribute.Load(); - ISlashCommand.FindAll(); + SlashCommand.FindAll(); } /// public override void Disable() { - Config = null; - Instance = null; + Harmony.UnpatchAll(); CallOnUnloadAttribute.Unload(); + + Config = null; + Instance = null; } } } \ No newline at end of file diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 48e0c66..5ffe7b3 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -1,6 +1,7 @@ using Discord; using DiscordLab.Bot; using DiscordLab.Bot.API.Features; +using DiscordLab.Dependency; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features; diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index a779e3d..fbc140d 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -32,6 +32,10 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev) public override void OnPlayerLeft(PlayerLeftEventArgs ev) { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (ev.Player == null) // for some reason, leave logs are sent with no player... + return; + if (Config.LeaveChannelId == 0) return; diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index 06adb41..ea0f0bb 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -1,4 +1,5 @@ using DiscordLab.Bot.API.Features; +using DiscordLab.Dependency; using LabApi.Events.CustomHandlers; using LabApi.Features; diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index bee7993..5041f0c 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -42,7 +42,7 @@ public static void OnHurt(PlayerHurtEventArgs ev) if (ev.DamageHandler is not StandardDamageHandler handler) return; - if (handler.Damage <= 0) return; + if (handler.TotalDamageDealt <= 0) return; string type = Events.ConvertToString(ev.DamageHandler); @@ -57,7 +57,7 @@ public static void OnHurt(PlayerHurtEventArgs ev) string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) + .AddCustomReplacer("damage", handler.TotalDamageDealt.ToString(CultureInfo.InvariantCulture)) .AddCustomReplacer("cause", type); DamageLogEntries.Add(log); diff --git a/DiscordLab.DeathLogs/Events.cs b/DiscordLab.DeathLogs/Events.cs index f279d4d..28d3588 100644 --- a/DiscordLab.DeathLogs/Events.cs +++ b/DiscordLab.DeathLogs/Events.cs @@ -25,22 +25,22 @@ public static class Events [CallOnLoad] public static void Register() { - PlayerEvents.Death += OnTeamKill; - PlayerEvents.Death += OnCuffKill; - PlayerEvents.Death += OnDeath; - PlayerEvents.Death += OnOwnDeath; + PlayerEvents.Dying += OnTeamKill; + PlayerEvents.Dying += OnCuffKill; + PlayerEvents.Dying += OnDeath; + PlayerEvents.Dying += OnOwnDeath; } [CallOnUnload] public static void Unregister() { - PlayerEvents.Death -= OnTeamKill; - PlayerEvents.Death -= OnCuffKill; - PlayerEvents.Death -= OnDeath; - PlayerEvents.Death -= OnOwnDeath; + PlayerEvents.Dying -= OnTeamKill; + PlayerEvents.Dying -= OnCuffKill; + PlayerEvents.Dying -= OnDeath; + PlayerEvents.Dying -= OnOwnDeath; } - public static void OnTeamKill(PlayerDeathEventArgs ev) + public static void OnTeamKill(PlayerDyingEventArgs ev) { if (ev.Attacker == null || ev.Attacker.Team.GetFaction() != ev.Player.Team.GetFaction()) return; @@ -64,7 +64,7 @@ public static void OnTeamKill(PlayerDeathEventArgs ev) channel.SendMessage(builder); } - public static void OnCuffKill(PlayerDeathEventArgs ev) + public static void OnCuffKill(PlayerDyingEventArgs ev) { if (ev.Attacker == null || !ev.Player.IsDisarmed || (ev.Attacker.IsSCP && Config.ScpIgnoreCuffed)) return; @@ -87,7 +87,7 @@ public static void OnCuffKill(PlayerDeathEventArgs ev) channel.SendMessage(builder); } - public static void OnDeath(PlayerDeathEventArgs ev) + public static void OnDeath(PlayerDyingEventArgs ev) { if (ev.Attacker == null || ev.Player.IsDisarmed || ev.Attacker.Team.GetFaction() == ev.Player.Team.GetFaction()) @@ -111,7 +111,7 @@ public static void OnDeath(PlayerDeathEventArgs ev) channel.SendMessage(builder); } - public static void OnOwnDeath(PlayerDeathEventArgs ev) + public static void OnOwnDeath(PlayerDyingEventArgs ev) { if (ev.Attacker != null) return; @@ -125,48 +125,54 @@ public static void OnOwnDeath(PlayerDeathEventArgs ev) return; } + + string converted = ConvertToString(ev.DamageHandler); + + // usually because of disconnect, only way to really track rn + if (converted == "Unknown") + return; TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeathSelf) .AddPlayer("player", ev.Player) - .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + .AddCustomReplacer("cause", converted); channel.SendMessage(builder); } - + + private static Dictionary _translations = new() + { + { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, + { DeathTranslations.Bleeding.Id, "Bleeding" }, + { DeathTranslations.Crushed.Id, "Crushed" }, + { DeathTranslations.Decontamination.Id, "Decontamination" }, + { DeathTranslations.Explosion.Id, "Explosion" }, + { DeathTranslations.Falldown.Id, "Falldown" }, + { DeathTranslations.Poisoned.Id, "Poison" }, + { DeathTranslations.Recontained.Id, "Recontainment" }, + { DeathTranslations.Scp049.Id, "SCP-049" }, + { DeathTranslations.Scp096.Id, "SCP-096" }, + { DeathTranslations.Scp173.Id, "SCP-173" }, + { DeathTranslations.Scp207.Id, "SCP-207" }, + { DeathTranslations.Scp939Lunge.Id, "SCP-939 Lunge" }, + { DeathTranslations.Scp939Other.Id, "SCP-939" }, + { DeathTranslations.Scp3114Slap.Id, "SCP-3114" }, + { DeathTranslations.Tesla.Id, "Tesla" }, + { DeathTranslations.Unknown.Id, "Unknown" }, + { DeathTranslations.Warhead.Id, "Warhead" }, + { DeathTranslations.Zombie.Id, "SCP-049-2" }, + { DeathTranslations.BulletWounds.Id, "Firearm" }, + { DeathTranslations.PocketDecay.Id, "Pocket Decay" }, + { DeathTranslations.SeveredHands.Id, "Severed Hands" }, + { DeathTranslations.FriendlyFireDetector.Id, "Friendly Fire" }, + { DeathTranslations.UsedAs106Bait.Id, "Femur Breaker" }, + { DeathTranslations.MicroHID.Id, "Micro H.I.D." }, + { DeathTranslations.Hypothermia.Id, "Hypothermia" }, + { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, + { DeathTranslations.Scp1344.Id, "Severed Eyes" }, + }; + internal static string ConvertToString(DamageHandlerBase handler) { - Dictionary translations = new() - { - { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, - { DeathTranslations.Bleeding.Id, "Bleeding" }, - { DeathTranslations.Crushed.Id, "Crushed" }, - { DeathTranslations.Decontamination.Id, "Decontamination" }, - { DeathTranslations.Explosion.Id, "Explosion" }, - { DeathTranslations.Falldown.Id, "Falldown" }, - { DeathTranslations.Poisoned.Id, "Poison" }, - { DeathTranslations.Recontained.Id, "Recontainment" }, - { DeathTranslations.Scp049.Id, "SCP-049" }, - { DeathTranslations.Scp096.Id, "SCP-096" }, - { DeathTranslations.Scp173.Id, "SCP-173" }, - { DeathTranslations.Scp207.Id, "SCP-207" }, - { DeathTranslations.Scp939Lunge.Id, "SCP-939 Lunge" }, - { DeathTranslations.Scp939Other.Id, "SCP-939" }, - { DeathTranslations.Scp3114Slap.Id, "SCP-3114" }, - { DeathTranslations.Tesla.Id, "Tesla" }, - { DeathTranslations.Unknown.Id, "Unknown" }, - { DeathTranslations.Warhead.Id, "Warhead" }, - { DeathTranslations.Zombie.Id, "SCP-049-2" }, - { DeathTranslations.BulletWounds.Id, "Firearm" }, - { DeathTranslations.PocketDecay.Id, "Pocket Decay" }, - { DeathTranslations.SeveredHands.Id, "Severed Hands" }, - { DeathTranslations.FriendlyFireDetector.Id, "Friendly Fire" }, - { DeathTranslations.UsedAs106Bait.Id, "Femur Breaker" }, - { DeathTranslations.MicroHID.Id, "Micro H.I.D." }, - { DeathTranslations.Hypothermia.Id, "Hypothermia" }, - { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, - { DeathTranslations.Scp1344.Id, "Severed Eyes" }, - }; - switch (handler) { case CustomReasonDamageHandler: @@ -213,11 +219,13 @@ internal static string ConvertToString(DamageHandlerBase handler) { DeathTranslation translation = DeathTranslations.TranslationsById[universal.TranslationId]; - if (translations.TryGetValue(translation.Id, out string s)) + if (_translations.TryGetValue(translation.Id, out string s)) return s; break; } + case FirearmDamageHandler firearm: + return firearm.Firearm.Name; } return "Unknown"; diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index cae56d6..173c6de 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -1,6 +1,6 @@ using DiscordLab.Bot.API.Attributes; -using DiscordLab.Bot.API.Features; using LabApi.Features; +using DiscordLab.Dependency; namespace DiscordLab.DeathLogs { diff --git a/DiscordLab.Dependency/DiscordLab.Dependency.csproj b/DiscordLab.Dependency/DiscordLab.Dependency.csproj new file mode 100644 index 0000000..3412dfa --- /dev/null +++ b/DiscordLab.Dependency/DiscordLab.Dependency.csproj @@ -0,0 +1,12 @@ + + + net48 + enable + disable + 12 + x64 + true + false + 2.0.0 + + \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Plugin.cs b/DiscordLab.Dependency/Plugin.cs similarity index 85% rename from DiscordLab.Bot/API/Features/Plugin.cs rename to DiscordLab.Dependency/Plugin.cs index 83cf808..1329bce 100644 --- a/DiscordLab.Bot/API/Features/Plugin.cs +++ b/DiscordLab.Dependency/Plugin.cs @@ -1,7 +1,7 @@ -namespace DiscordLab.Bot.API.Features -{ - using LabApi.Loader; +using LabApi.Loader; +namespace DiscordLab.Dependency +{ /// /// Allows users to easily make plugins with translations also, not just configs. /// @@ -11,6 +11,7 @@ public abstract class Plugin : LabApi.Loader.Features.Plu where TConfig : class, new() where TTranslation : class, new() { +#pragma warning disable SA1401 // FieldsMustBePrivate /// /// Gets the plugin's config. /// @@ -20,6 +21,7 @@ public abstract class Plugin : LabApi.Loader.Features.Plu /// Gets the plugin's translation. /// public TTranslation Translation; +#pragma warning restore SA1401 // FieldsMustBePrivate /// public override void LoadConfigs() diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index e83820f..b522717 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -1,31 +1,49 @@ using Discord; using Discord.WebSocket; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; namespace DiscordLab.Moderation.Commands { - public class Ban : IAutocompleteCommand + public class Ban : AutocompleteCommand { - public SlashCommandBuilder Data - { - get - { - SlashCommandBuilder builder = Plugin.SetupDurationBuilder(Plugin.Instance.Translation.BanCommand, true); - SlashCommandOptionBuilder option = builder.Options[2]; - option.IsRequired = true; - option.IsAutocomplete = false; - option.Type = ApplicationCommandOptionType.String; + public static Translation Translation => Plugin.Instance.Translation; - return builder; - } - } + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.BanCommandName, + Description = Translation.BanCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.BanUserOptionName, + Description = Translation.BanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.BanDurationOptionName, + Description = Translation.BanDurationOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.BanReasonOptionName, + Description = Translation.BanReasonOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + } + ] + }; - public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -33,11 +51,11 @@ public async Task Run(SocketSlashCommand command) long duration = Misc.RelativeTimeToSeconds((string)command.Data.Options.ElementAt(1).Value, 60); string reason = (string)command.Data.Options.ElementAt(2).Value; - TranslationBuilder successBuilder = new(Plugin.Instance.Translation.BanSuccess) + TranslationBuilder successBuilder = new(Translation.BanSuccess) { Time = TempMuteManager.GetExpireDate(duration) }; - TranslationBuilder failBuilder = new(Plugin.Instance.Translation.BanFailure); + TranslationBuilder failBuilder = new(Translation.BanFailure); successBuilder.CustomReplacers.Add("userid", () => userId); failBuilder.CustomReplacers.Add("userid", () => userId); @@ -58,7 +76,7 @@ await command.ModifyOriginalResponseAsync(m => m.Content = Server.BanPlayer(player, reason, duration) ? successBuilder : failBuilder); } - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index 7551999..6f6c762 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -1,26 +1,49 @@ using Discord; using Discord.WebSocket; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; using VoiceChat; namespace DiscordLab.Moderation.Commands { - public class Mute : IAutocompleteCommand + public class Mute : AutocompleteCommand { - public SlashCommandBuilder Data { get; } = Plugin.SetupDurationBuilder(Plugin.Instance.Translation.MuteCommand); + public static Translation Translation => Plugin.Instance.Translation; - public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.MuteCommandName, + Description = Translation.MuteCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.MuteUserOptionName, + Description = Translation.MuteUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.MuteDurationOptionName, + Description = Translation.MuteDurationOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = false + } + ] + }; + + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) { - await command.ModifyOriginalResponseAsync(m => m.Content = Plugin.Instance.Translation.InvalidUser); + await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); return; } @@ -32,7 +55,7 @@ public async Task Run(SocketSlashCommand command) DateTime time = TempMuteManager.GetExpireDate(duration); TempMuteManager.MutePlayer(player, time); - builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", player) + builder = new(Translation.TempMuteSuccess, "player", player) { Time = time }; @@ -45,12 +68,12 @@ public async Task Run(SocketSlashCommand command) VoiceChatMutes.IssueLocalMute(player.UserId); - builder = new(Plugin.Instance.Translation.PermMuteSuccess, "player", player); + builder = new(Translation.PermMuteSuccess, "player", player); await command.ModifyOriginalResponseAsync(m => m.Content = builder); } - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs index 8620fe2..5191ffd 100644 --- a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -14,8 +14,8 @@ public class TempMuteRemoteAdmin : ICommand, IUsageProvider public string[] Usage { get; } = [ - "{player}", - "{duration}" + "player", + "duration" ]; public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index c0df3ee..3807f91 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -1,29 +1,33 @@ using Discord; using Discord.WebSocket; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; namespace DiscordLab.Moderation.Commands { - public class Unban : IAutocompleteCommand + public class Unban : AutocompleteCommand { - public SlashCommandBuilder Data - { - get - { - SlashCommandBuilder builder = Plugin.Instance.Translation.UnbanCommand; - SlashCommandOptionBuilder option = builder.Options[0]; - option.Type = ApplicationCommandOptionType.String; - option.IsRequired = true; - option.IsAutocomplete = true; + public static Translation Translation => Plugin.Instance.Translation; - return builder; - } - } + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.UnbanCommandName, + Description = Translation.UnbanCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.UnbanUserOptionName, + Description = Translation.UnbanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + ] + }; - public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -31,7 +35,7 @@ public async Task Run(SocketSlashCommand command) BanHandler.RemoveBan(id, id.Contains("@") ? BanHandler.BanType.UserId : BanHandler.BanType.IP); - TranslationBuilder builder = new(Plugin.Instance.Translation.UnbanSuccess); + TranslationBuilder builder = new(Translation.UnbanSuccess); builder.CustomReplacers.Add("userid", () => id); @@ -40,7 +44,7 @@ await command.ModifyOriginalResponseAsync(m => builder); } - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { IEnumerable response = [ diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs index d025dd3..a532456 100644 --- a/DiscordLab.Moderation/Commands/Unmute.cs +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -1,38 +1,41 @@ using Discord; using Discord.WebSocket; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; namespace DiscordLab.Moderation.Commands { - public class Unmute : IAutocompleteCommand + public class Unmute : AutocompleteCommand { + public static Translation Translation => Plugin.Instance.Translation; - public SlashCommandBuilder Data + public override SlashCommandBuilder Data { get; } = new() { - get - { - SlashCommandBuilder builder = Plugin.Instance.Translation.UnmuteCommand; - SlashCommandOptionBuilder option = builder.Options[0]; - option.Type = ApplicationCommandOptionType.String; - option.IsRequired = true; - option.IsAutocomplete = true; - - return builder; - } - } + Name = Translation.UnmuteCommandName, + Description = Translation.UnmuteCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.UnbanUserOptionName, + Description = Translation.UnbanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + ] + }; - public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) { - await command.ModifyOriginalResponseAsync(m => m.Content = Plugin.Instance.Translation.InvalidUser); + await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); return; } @@ -40,10 +43,10 @@ public async Task Run(SocketSlashCommand command) await command.ModifyOriginalResponseAsync(m => m.Content = - new TranslationBuilder(Plugin.Instance.Translation.UnmuteSuccess, "player", player)); + new TranslationBuilder(Translation.UnmuteSuccess, "player", player)); } - public async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index cb3f9d9..7d33c6a 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -15,6 +15,6 @@ - + \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index c3f4c8c..3dda9e8 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -1,7 +1,7 @@ using Discord; using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Dependency; using LabApi.Events.CustomHandlers; using LabApi.Features; using LabApi.Features.Wrappers; @@ -30,7 +30,7 @@ public override void Enable() CallOnLoadAttribute.Load(); if (Config.AddCommands) - ISlashCommand.FindAll(); + SlashCommand.FindAll(); CustomHandlersManager.RegisterEventsHandler(Events); } @@ -55,21 +55,5 @@ public override void LoadConfigs() public static IEnumerable PlayersAutocompleteResults => Player.ReadyList.Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); - - public static SlashCommandBuilder SetupDurationBuilder(SlashCommandBuilder original, bool required = false) - { - SlashCommandOptionBuilder playerOption = original.Options[0]; - SlashCommandOptionBuilder durationOption = original.Options[1]; - - playerOption.Type = ApplicationCommandOptionType.String; - playerOption.IsAutocomplete = true; - playerOption.IsRequired = true; - - durationOption.Type = ApplicationCommandOptionType.String; - durationOption.IsAutocomplete = false; - durationOption.IsRequired = required; - - return original; - } } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Properties/AssemblyInfo.cs b/DiscordLab.Moderation/Properties/AssemblyInfo.cs deleted file mode 100644 index 54dfc23..0000000 --- a/DiscordLab.Moderation/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiscordLab.Moderation")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DiscordLab.Moderation")] -[assembly: AssemblyCopyright("Copyright © 2025")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6806DFF9-8907-49F0-B727-67AF6BD35844")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/DiscordLab.Moderation/TempMuteManager.cs b/DiscordLab.Moderation/TempMuteManager.cs index 3a324e4..40fcd20 100644 --- a/DiscordLab.Moderation/TempMuteManager.cs +++ b/DiscordLab.Moderation/TempMuteManager.cs @@ -26,7 +26,7 @@ public static void Start() RemoveMute(dict.Key); continue; } - AddHandle(); + AddHandle(dict.Key, time); } } diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index 0d8e42a..b00d4e5 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -5,80 +5,32 @@ namespace DiscordLab.Moderation { public class Translation { - public SlashCommandBuilder MuteCommand { get; set; } = new() - { - Name = "mute", - Description = "Mute a player on the server", - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = "user", - Description = "The user to mute" - }, - new() - { - Name = "duration", - Description = "The duration to mute the user for" - } - ] - }; + // String properties for command and option names + public string MuteCommandName { get; set; } = "mute"; + public string MuteCommandDescription { get; set; } = "Mute a player on the server"; + public string MuteUserOptionName { get; set; } = "user"; + public string MuteUserOptionDescription { get; set; } = "The user to mute"; + public string MuteDurationOptionName { get; set; } = "duration"; + public string MuteDurationOptionDescription { get; set; } = "The duration to mute the user for"; - public SlashCommandBuilder UnmuteCommand { get; set; } = new() - { - Name = "unmute", - Description = "Unmute a player on the server", - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = "user", - Description = "The user to unmute" - } - ] - }; + public string UnmuteCommandName { get; set; } = "unmute"; + public string UnmuteCommandDescription { get; set; } = "Unmute a player on the server"; + public string UnmuteUserOptionName { get; set; } = "user"; + public string UnmuteUserOptionDescription { get; set; } = "The user to unmute"; - public SlashCommandBuilder BanCommand { get; set; } = new() - { - Name = "ban", - Description = "Ban a player on the server", - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = "user", - Description = "The user to ban" - }, - new() - { - Name = "duration", - Description = "The duration to ban the user for" - }, - new() - { - Name = "reason", - Description = "The reason to ban the user" - } - ] - }; - - public SlashCommandBuilder UnbanCommand { get; set; } = new() - { - Name = "unban", - Description = "Unban a player on the server", - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = "user", - Description = "The user to unban" - } - ] - }; + public string BanCommandName { get; set; } = "ban"; + public string BanCommandDescription { get; set; } = "Ban a player on the server"; + public string BanUserOptionName { get; set; } = "user"; + public string BanUserOptionDescription { get; set; } = "The user to ban"; + public string BanDurationOptionName { get; set; } = "duration"; + public string BanDurationOptionDescription { get; set; } = "The duration to ban the user for"; + public string BanReasonOptionName { get; set; } = "reason"; + public string BanReasonOptionDescription { get; set; } = "The reason to ban the user"; + + public string UnbanCommandName { get; set; } = "unban"; + public string UnbanCommandDescription { get; set; } = "Unban a player on the server"; + public string UnbanUserOptionName { get; set; } = "user"; + public string UnbanUserOptionDescription { get; set; } = "The user to unban"; public string InvalidUser { get; set; } = "Please provide a valid user to use this command on."; diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs index 636d9dc..8d24c9e 100644 --- a/DiscordLab.RoundLogs/Events.cs +++ b/DiscordLab.RoundLogs/Events.cs @@ -8,6 +8,7 @@ using LabApi.Events.CustomHandlers; using PlayerRoles; using LabApi.Features.Console; +using LabApi.Features.Extensions; using LabApi.Features.Wrappers; using Respawning.Waves; @@ -22,13 +23,13 @@ public class Events : CustomEventsHandler public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) { if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart - or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin) + or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died or RoleChangeReason.Destroyed) return; Dictionary> customReplacers = new() { - ["oldrole"] = () => ev.OldRole.ToString(), - ["newrole"] = () => ev.NewRole.RoleTypeId.ToString(), + ["oldrole"] = () => ev.OldRole.GetFullName(), + ["newrole"] = () => ev.NewRole.RoleName, ["reason"] = () => ev.ChangeReason.ToString(), ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) }; @@ -90,7 +91,8 @@ public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) TranslationBuilder builder = new(isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn) { - PlayerListItem = Translation.PlayerListItem + PlayerListItem = Translation.PlayerListItem, + PlayerList = ev.Players }; channel.SendMessage(builder); diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index 93cb29c..f1c8e94 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -1,5 +1,6 @@ using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; +using DiscordLab.Dependency; using LabApi.Events.CustomHandlers; using LabApi.Features; diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs index 743c6f4..bacef58 100644 --- a/DiscordLab.StatusChannel/Command.cs +++ b/DiscordLab.StatusChannel/Command.cs @@ -1,16 +1,20 @@ using Discord; using Discord.WebSocket; -using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Bot.API.Features; namespace DiscordLab.StatusChannel { - public class Command : ISlashCommand + public class Command : SlashCommand { - public SlashCommandBuilder Data { get; } = Plugin.Instance.Translation.PlayerListCommand; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Plugin.Instance.Translation.PlayerListCommandName, + Description = Plugin.Instance.Translation.PlayerListCommandDescription, + }; - public ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public async Task Run(SocketSlashCommand command) + public override async Task Run(SocketSlashCommand command) { await command.RespondAsync(embed: Events.GetEmbed().Build()); } diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index e42c7df..46b61e4 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -1,6 +1,6 @@ using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; -using DiscordLab.Bot.API.Interfaces; +using DiscordLab.Dependency; using LabApi.Events.CustomHandlers; using LabApi.Events.Handlers; using LabApi.Features; @@ -31,7 +31,7 @@ public override void Enable() CustomHandlersManager.RegisterEventsHandler(Events); if(Config.AddCommand) - ISlashCommand.FindAll(); + SlashCommand.FindAll(); } public override void Disable() diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index d4846c7..d530009 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -24,10 +24,8 @@ public class Translation [Description("What will appear for each player when replacing the players variable above.")] public string PlayerItem { get; set; } = "- {player}"; - public SlashCommandBuilder PlayerListCommand = new() - { - Name = "players", - Description = "Get the current list of players on the server" - }; + public string PlayerListCommandName { get; set; } = "players"; + + public string PlayerListCommandDescription { get; set; } = "Get the current list of players on the server"; } } \ No newline at end of file diff --git a/DiscordLab.sln b/DiscordLab.sln index 36846c2..3aa62f1 100644 --- a/DiscordLab.sln +++ b/DiscordLab.sln @@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.ConnectionLogs", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.DeathLogs", "DiscordLab.DeathLogs\DiscordLab.DeathLogs.csproj", "{35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordLab.Dependency", "DiscordLab.Dependency\DiscordLab.Dependency.csproj", "{77A6BBBD-8A7D-4EBF-969F-F9FCDEE52F9F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,5 +56,9 @@ Global {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Debug|Any CPU.Build.0 = Debug|Any CPU {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Release|Any CPU.ActiveCfg = Release|Any CPU {35D1EB24-04BB-47F9-ACAC-2E6BFA695EC3}.Release|Any CPU.Build.0 = Release|Any CPU + {77A6BBBD-8A7D-4EBF-969F-F9FCDEE52F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77A6BBBD-8A7D-4EBF-969F-F9FCDEE52F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77A6BBBD-8A7D-4EBF-969F-F9FCDEE52F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77A6BBBD-8A7D-4EBF-969F-F9FCDEE52F9F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 5aeccd042011e957b677d97c1420a532c4cf137d Mon Sep 17 00:00:00 2001 From: Lumi Date: Mon, 7 Jul 2025 16:16:06 +0100 Subject: [PATCH 03/68] feat: costura fody --- Directory.Build.props | 2 +- DiscordLab.Bot/DiscordLab.Bot.csproj | 8 ++ DiscordLab.Bot/FodyWeavers.xml | 11 ++ DiscordLab.Bot/FodyWeavers.xsd | 176 +++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 DiscordLab.Bot/FodyWeavers.xml create mode 100644 DiscordLab.Bot/FodyWeavers.xsd diff --git a/Directory.Build.props b/Directory.Build.props index d9e51b2..e45b656 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,7 +21,7 @@ - + diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 4fad09f..fef4b3e 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -36,8 +36,16 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive; compile + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DiscordLab.Bot/FodyWeavers.xml b/DiscordLab.Bot/FodyWeavers.xml new file mode 100644 index 0000000..fd36d8d --- /dev/null +++ b/DiscordLab.Bot/FodyWeavers.xml @@ -0,0 +1,11 @@ + + + + Discord.Net.Core + Discord.Net.Rest + Discord.Net.WebSocket + DiscordLab.Dependency + Newtonsoft.Json + + + \ No newline at end of file diff --git a/DiscordLab.Bot/FodyWeavers.xsd b/DiscordLab.Bot/FodyWeavers.xsd new file mode 100644 index 0000000..f2dbece --- /dev/null +++ b/DiscordLab.Bot/FodyWeavers.xsd @@ -0,0 +1,176 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead. + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file From 64f15082a3808fb925132e9694b8bf154833899f Mon Sep 17 00:00:00 2001 From: Lumi Date: Mon, 7 Jul 2025 16:19:01 +0100 Subject: [PATCH 04/68] fix: loading issues --- DiscordLab.Bot/FodyWeavers.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/DiscordLab.Bot/FodyWeavers.xml b/DiscordLab.Bot/FodyWeavers.xml index fd36d8d..4f99b35 100644 --- a/DiscordLab.Bot/FodyWeavers.xml +++ b/DiscordLab.Bot/FodyWeavers.xml @@ -4,7 +4,6 @@ Discord.Net.Core Discord.Net.Rest Discord.Net.WebSocket - DiscordLab.Dependency Newtonsoft.Json From f07f714e844a73cdfa2864b71c17b1ca8d49bb58 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 8 Jul 2025 18:04:10 +0100 Subject: [PATCH 05/68] test: add some logs --- .../API/Attributes/CallOnLoadAttribute.cs | 2 + .../API/Attributes/CallOnReadyAttribute.cs | 2 + .../API/Attributes/CallOnUnloadAttribute.cs | 1 + DiscordLab.Bot/DiscordLab.Bot.csproj | 8 - DiscordLab.Bot/FodyWeavers.xml | 10 - DiscordLab.Bot/FodyWeavers.xsd | 176 ------------------ DiscordLab.Bot/Patches/RestClientCreate.cs | 9 + DiscordLab.Bot/Plugin.cs | 18 +- 8 files changed, 29 insertions(+), 197 deletions(-) delete mode 100644 DiscordLab.Bot/FodyWeavers.xml delete mode 100644 DiscordLab.Bot/FodyWeavers.xsd diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs index a9a483e..5cb4722 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs @@ -25,6 +25,8 @@ public static void Load(Assembly assembly = null) if (attribute == null) continue; + Logger.Debug($"Loading load attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); + try { method.Invoke(null, null); diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs index bbacab0..093f0f7 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs @@ -27,6 +27,8 @@ public static void Load(Assembly assembly = null) if (attribute == null) continue; + Logger.Debug($"Loading ready attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); + instances.Add(method); } } diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs index 8113549..6b58280 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs @@ -27,6 +27,7 @@ public static void Unload(Assembly assembly = null) try { + Logger.Debug($"Calling unload attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); method.Invoke(null, null); } catch (Exception ex) diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index fef4b3e..4fad09f 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -36,16 +36,8 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive; compile - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/DiscordLab.Bot/FodyWeavers.xml b/DiscordLab.Bot/FodyWeavers.xml deleted file mode 100644 index 4f99b35..0000000 --- a/DiscordLab.Bot/FodyWeavers.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Discord.Net.Core - Discord.Net.Rest - Discord.Net.WebSocket - Newtonsoft.Json - - - \ No newline at end of file diff --git a/DiscordLab.Bot/FodyWeavers.xsd b/DiscordLab.Bot/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/DiscordLab.Bot/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead. - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. - - - - - The order of preloaded assemblies, delimited with line breaks. - - - - - - This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. - - - - - Controls if .pdbs for reference assemblies are also embedded. - - - - - Controls if runtime assemblies are also embedded. - - - - - Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. - - - - - Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. - - - - - As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. - - - - - The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. - - - - - Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. - - - - - Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. - - - - - A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | - - - - - A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. - - - - - Obsolete, use UnmanagedWinX86Assemblies instead - - - - - A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. - - - - - Obsolete, use UnmanagedWinX64Assemblies instead - - - - - A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. - - - - - A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. - - - - - The order of preloaded assemblies, delimited with |. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index 0c0e457..6629b6c 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -6,6 +6,7 @@ using System.Reflection.Emit; using Discord.Net.Rest; using HarmonyLib; + using LabApi.Features.Console; /// /// Patches . @@ -39,6 +40,8 @@ public static MethodBase TargetMethod() /// The patched code. public static IEnumerable Transpiler(IEnumerable instructions) { + Logger.Debug("Transpiler start", Plugin.Instance.Config.Debug); + CodeMatcher matcher = new CodeMatcher(instructions) .MatchEndForward( new CodeMatch(OpCodes.Ldarg_0), @@ -67,11 +70,15 @@ public static IEnumerable Transpiler(IEnumerable /// public override void Enable() { - Harmony.PatchAll(); - Instance = this; - Config = base.Config; + Config = base.Config!; + + try + { + TokenUtils.ValidateToken(TokenType.Bot, Config.Token); + } + catch (Exception) + { + Logger.Error("DiscordLab bot token is invalid"); + return; + } + + Harmony.PatchAll(); CallOnLoadAttribute.Load(); CallOnReadyAttribute.Load(); From 391e671c8fce6545736455611958ea75a6b81fb7 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 8 Jul 2025 18:05:37 +0100 Subject: [PATCH 06/68] ci: fix build --- .github/workflows/artifact.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index 0e8ed5a..c7fe6b3 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -6,7 +6,8 @@ on: env: EXILED_REFERENCES_URL: https://exslmod-team.github.io/SL-References/Dev.zip XP_SYSTEM_URL: https://api.github.com/repos/RowpannSCP/XP/releases - EXILED_REFERENCES: ${{ github.workspace }}/refs + SL_REFERENCES: ${{ github.workspace }}/refs + EXILED_REFERENCES: ${{ env.SL_REFERENCES }} jobs: build: @@ -25,14 +26,14 @@ jobs: shell: pwsh run: | Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip - Expand-Archive -Path References.zip -DestinationPath ${{ env.EXILED_REFERENCES }} -Force + Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force - name: Get XP System shell: pwsh run: | $response = Invoke-RestMethod -Uri ${{ env.XP_SYSTEM_URL }} $asset = $response.assets | Where-Object { $_.name -eq 'XPSystem-EXILED.dll' } | Select-Object -First 1 $url = $asset.browser_download_url - Invoke-WebRequest -Uri $url -OutFile ${{ env.EXILED_REFERENCES }}/XPSystem.dll + Invoke-WebRequest -Uri $url -OutFile ${{ env.SL_REFERENCES }}/XPSystem.dll - name: Restore packages run: nuget restore ${{ github.workspace }}/DiscordLab.sln - name: Build From 94bdd3e73ae3093f561802220b594fff18849f83 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 8 Jul 2025 18:08:11 +0100 Subject: [PATCH 07/68] ci: fix again --- .github/workflows/artifact.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index c7fe6b3..e643d82 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -7,7 +7,7 @@ env: EXILED_REFERENCES_URL: https://exslmod-team.github.io/SL-References/Dev.zip XP_SYSTEM_URL: https://api.github.com/repos/RowpannSCP/XP/releases SL_REFERENCES: ${{ github.workspace }}/refs - EXILED_REFERENCES: ${{ env.SL_REFERENCES }} + EXILED_REFERENCES: ${{ github.workspace }}/refs jobs: build: From ec62237b99ce6a2468bb7e255f2675b8077a8145 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 8 Jul 2025 23:02:36 +0100 Subject: [PATCH 08/68] fix: small issues --- .../API/Features/TranslationBuilder.cs | 2 +- DiscordLab.BotStatus/Plugin.cs | 12 +++++++++- .../DiscordLab.StatusChannel.csproj | 4 ++++ DiscordLab.StatusChannel/Events.cs | 22 ++++++++++++++----- DiscordLab.StatusChannel/Translation.cs | 4 ++-- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 3293b16..edc0fbb 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -73,7 +73,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["port"] = () => Server.Port.ToString(), ["ip"] = () => Server.IpAddress, ["playercount"] = () => Server.PlayerCount.ToString(), - ["playercountnonpcs"] = () => Player.List.Count(p => !p.IsNpc).ToString(), + ["playercountnonpcs"] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), }; diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 5ffe7b3..5e987ba 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -1,5 +1,6 @@ using Discord; using DiscordLab.Bot; +using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; using DiscordLab.Dependency; using LabApi.Events.Arguments.PlayerEvents; @@ -25,16 +26,25 @@ public override void Enable() PlayerEvents.Joined += OnPlayerJoin; PlayerEvents.Left += OnPlayerLeave; + + ServerEvents.WaitingForPlayers += OnWaitingForPlayers; } public override void Disable() { + ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; + PlayerEvents.Joined -= OnPlayerJoin; PlayerEvents.Left -= OnPlayerLeave; Instance = null; } + public static void OnWaitingForPlayers() + { + UpdateStatus(); + } + public static void OnPlayerJoin(PlayerJoinedEventArgs _) { if(Round.IsRoundInProgress) @@ -52,7 +62,7 @@ public static void OnPlayerLeave(PlayerLeftEventArgs _) } private static Queue Queue { get; } = new(5, UpdateStatus); - + private static void UpdateStatus() { TranslationBuilder builder = new(Server.PlayerCount == 0 ? Instance.Translation.EmptyContent : Instance.Translation.NormalContent); diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index b6fe025..7e639b9 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -9,6 +9,10 @@ false 2.0.0 + + + + diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 81189c1..d9720a6 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -9,6 +9,7 @@ using LabApi.Features.Console; using LabApi.Features.Wrappers; using LabApi.Loader; +using MEC; namespace DiscordLab.StatusChannel { @@ -91,20 +92,29 @@ public static void Ready() return; } - Task.Run(GetOrCreateMessage); + Timing.CallDelayed(0.1f, () => Task.Run(GetOrCreateMessage)); } public static async Task GetOrCreateMessage() { ulong msgId = Plugin.Instance.MessageConfig.MessageId; - Message = Channel.GetCachedMessage(msgId) as IUserMessage ?? - await Channel.GetMessageAsync(msgId) as IUserMessage; - + Message = msgId != 0 ? Channel.GetCachedMessage(msgId) as IUserMessage ?? + await Channel.GetMessageAsync(msgId) as IUserMessage : null; + if (Message == null) { EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; - embed.Description = new TranslationBuilder(embed.Description); - Message = await Channel.SendMessageAsync(embed:embed.Build()); + try + { + embed.Description = new TranslationBuilder(embed.Description).Build(); + } + catch (Exception e) + { + Logger.Error(e); + return; + } + + Message = await Channel.SendMessageAsync(embed: embed.Build()); Plugin.Instance.MessageConfig.MessageId = Message.Id; Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message_config.yml"); diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index d530009..781a0c7 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -9,7 +9,7 @@ public class Translation public EmbedBuilder Embed { get; set; } = new() { Title = "Server Status", - Color = Color.Blue, + Color = Color.Blue.RawValue, Description = "{playercount}/{maxplayers} currently online\\n```{players}```" }; @@ -17,7 +17,7 @@ public class Translation public EmbedBuilder EmbedEmpty { get; set; } = new() { Title = "Server Status", - Color = Color.Blue, + Color = Color.Blue.RawValue, Description = "0/{maxplayers} currently online" }; From 6189dd4afb03254f7f20b8e0acc507ca5c4d9a87 Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 9 Jul 2025 17:07:50 +0100 Subject: [PATCH 09/68] fix: embed builder being weird in translation --- .../API/Features/Embed/EmbedBuilder.cs | 57 +++++++++++++++++++ .../API/Features/Embed/EmbedFieldBuilder.cs | 43 ++++++++++++++ DiscordLab.DeathLogs/Translation.cs | 2 +- DiscordLab.Moderation/Translation.cs | 2 +- DiscordLab.StatusChannel/Translation.cs | 5 +- 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs create mode 100644 DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs new file mode 100644 index 0000000..d642111 --- /dev/null +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -0,0 +1,57 @@ +namespace DiscordLab.Bot.API.Features.Embed +{ + using YamlDotNet.Serialization; + + /// + /// Allows you to make an embed. Should be used in translations only. + /// + public class EmbedBuilder + { + /// + /// Gets or sets the embed title. + /// + public string Title + { + get => Builder.Title; + set => Builder.Title = value; + } + + /// + /// Gets or sets the embed description. + /// + public string Description + { + get => Builder.Description; + set => Builder.Description = value; + } + + /// + /// Gets or sets the embed fields. + /// + public IEnumerable Fields + { + get => Builder.Fields.Select(x => new EmbedFieldBuilder { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); + set => Builder.Fields = value.Select(x => x.Builder).ToList(); + } + + /// + /// Gets or sets the color of the embed. In string so #, 0x or the raw hex value will work. + /// + public string Color + { + get => Builder.Color?.ToString(); + set => Builder.Color = Discord.Color.Parse(value); + } + + [YamlIgnore] + private Discord.EmbedBuilder Builder { get; } = new(); + + /// + /// Changes a into a instance. + /// + /// The instance. + /// The instance. + public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) => + builder.Builder; + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs new file mode 100644 index 0000000..fd23aa7 --- /dev/null +++ b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs @@ -0,0 +1,43 @@ +namespace DiscordLab.Bot.API.Features.Embed +{ + using YamlDotNet.Serialization; + + /// + /// Allows you to create embed fields for a . + /// + public class EmbedFieldBuilder + { + /// + /// Gets or sets the field name. + /// + public string Name + { + get => Builder.Name; + set => Builder.Name = value; + } + + /// + /// Gets or sets the field value. + /// + public string Value + { + get => Builder.Value.ToString(); + set => Builder.Value = value; + } + + /// + /// Gets or sets a value indicating whether the field is inline. + /// + public bool IsInline + { + get => Builder.IsInline; + set => Builder.IsInline = value; + } + + /// + /// Gets the base builder. + /// + [YamlIgnore] + internal Discord.EmbedFieldBuilder Builder { get; } = new(); + } +} \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index 2fca197..23136cf 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -1,5 +1,5 @@ using System.ComponentModel; -using Discord; +using DiscordLab.Bot.API.Features.Embed; namespace DiscordLab.DeathLogs { diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index b00d4e5..4bff721 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -1,5 +1,5 @@ using System.ComponentModel; -using Discord; +using DiscordLab.Bot.API.Features.Embed; namespace DiscordLab.Moderation { diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index 781a0c7..f533dd5 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Discord; +using EmbedBuilder = DiscordLab.Bot.API.Features.Embed.EmbedBuilder; namespace DiscordLab.StatusChannel { @@ -9,7 +10,7 @@ public class Translation public EmbedBuilder Embed { get; set; } = new() { Title = "Server Status", - Color = Color.Blue.RawValue, + Color = Color.Blue.ToString(), Description = "{playercount}/{maxplayers} currently online\\n```{players}```" }; @@ -17,7 +18,7 @@ public class Translation public EmbedBuilder EmbedEmpty { get; set; } = new() { Title = "Server Status", - Color = Color.Blue.RawValue, + Color = Color.Blue.ToString(), Description = "0/{maxplayers} currently online" }; From 1e1c012df9c12b51723273954531ce03119835be Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 10 Jul 2025 08:47:48 +0100 Subject: [PATCH 10/68] fix: status channel not getting/creating message --- DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj | 4 ---- DiscordLab.StatusChannel/Events.cs | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index 7e639b9..b6fe025 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -9,10 +9,6 @@ false 2.0.0 - - - - diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index d9720a6..7b561c9 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -9,13 +9,14 @@ using LabApi.Features.Console; using LabApi.Features.Wrappers; using LabApi.Loader; -using MEC; namespace DiscordLab.StatusChannel { public class Events : CustomEventsHandler { // events + + public override void OnServerWaitingForPlayers() => Task.Run(GetOrCreateMessage); public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); @@ -91,8 +92,6 @@ public static void Ready() Plugin.Instance.Disable(); return; } - - Timing.CallDelayed(0.1f, () => Task.Run(GetOrCreateMessage)); } public static async Task GetOrCreateMessage() From f91cf28eaf51cb1b0cf15e417f8fd5761249e51c Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 10 Jul 2025 16:10:29 +0100 Subject: [PATCH 11/68] fix: embed builder colors --- DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index d642111..2cb6550 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -40,7 +40,16 @@ public IEnumerable Fields public string Color { get => Builder.Color?.ToString(); - set => Builder.Color = Discord.Color.Parse(value); + set + { + if (value == null) + { + Builder.Color = null; + return; + } + + Builder.Color = Discord.Color.Parse(value); + } } [YamlIgnore] From 4f32d59bcd28cddbba87813d75abe3a34f9157d2 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 10 Jul 2025 16:43:58 +0100 Subject: [PATCH 12/68] fix: use correct damage --- DiscordLab.DeathLogs/DamageLogs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index 5041f0c..bee7993 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -42,7 +42,7 @@ public static void OnHurt(PlayerHurtEventArgs ev) if (ev.DamageHandler is not StandardDamageHandler handler) return; - if (handler.TotalDamageDealt <= 0) return; + if (handler.Damage <= 0) return; string type = Events.ConvertToString(ev.DamageHandler); @@ -57,7 +57,7 @@ public static void OnHurt(PlayerHurtEventArgs ev) string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("damage", handler.TotalDamageDealt.ToString(CultureInfo.InvariantCulture)) + .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) .AddCustomReplacer("cause", type); DamageLogEntries.Add(log); From e96f61ec23005bc6c595cb325d4f9a9d8f5ea182 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 10 Jul 2025 16:57:46 +0100 Subject: [PATCH 13/68] feat: cache damage log channel --- DiscordLab.DeathLogs/DamageLogs.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index bee7993..7ca123b 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -7,7 +7,6 @@ using DiscordLab.Bot.API.Extensions; using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Utilities; -using GameCore; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features.Console; @@ -19,6 +18,8 @@ public static class DamageLogs { public static List DamageLogEntries { get; set; } = new(); + public static SocketTextChannel Channel; + private static Queue queue = new(5, SendLog); [CallOnLoad] @@ -33,6 +34,9 @@ public static void Unregister() { if (Plugin.Instance.Config.DamageLogChannelId == 0) return; PlayerEvents.Hurt -= OnHurt; + + DamageLogEntries = null; + Channel = null; } public static void OnHurt(PlayerHurtEventArgs ev) @@ -67,7 +71,7 @@ public static void OnHurt(PlayerHurtEventArgs ev) public static void SendLog() { - if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.DamageLogChannelId, out SocketTextChannel channel)) + if (Channel == null && !Client.TryGetOrAddChannel(Plugin.Instance.Config.DamageLogChannelId, out Channel)) { Logger.Error( LoggingUtils.GenerateMissingChannelMessage( @@ -77,7 +81,7 @@ public static void SendLog() return; } - channel.SendMessage(embeds:CreateEmbeds()); + Channel.SendMessage(embeds:CreateEmbeds()); DamageLogEntries.Clear(); } From af125614d241fca8decf2b5480b76aa9d69a05a2 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 10 Jul 2025 20:46:47 +0100 Subject: [PATCH 14/68] fix: translation embed builders --- .../API/Features/Embed/EmbedBuilder.cs | 42 +++++++++++++++++-- .../API/Features/TranslationBuilder.cs | 2 +- DiscordLab.StatusChannel/Events.cs | 25 +++++------ DiscordLab.StatusChannel/Translation.cs | 2 +- 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index 2cb6550..62c705b 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -59,8 +59,44 @@ public string Color /// Changes a into a instance. /// /// The instance. - /// The instance. - public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) => - builder.Builder; + /// A copy of the instance. + public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) + { + Discord.EmbedBuilder copy = new(); + + if (builder.Builder.Title != null) + copy.WithTitle(builder.Builder.Title); + + if (builder.Builder.Description != null) + copy.WithDescription(builder.Builder.Description); + + if (builder.Builder.Color.HasValue) + copy.WithColor(builder.Builder.Color.Value); + + if (builder.Builder.Url != null) + copy.WithUrl(builder.Builder.Url); + + if (builder.Builder.ImageUrl != null) + copy.WithImageUrl(builder.Builder.ImageUrl); + + if (builder.Builder.ThumbnailUrl != null) + copy.WithThumbnailUrl(builder.Builder.ThumbnailUrl); + + if (builder.Builder.Timestamp.HasValue) + copy.WithTimestamp(builder.Builder.Timestamp.Value); + + if (builder.Builder.Footer != null) + copy.WithFooter(builder.Builder.Footer); + + if (builder.Builder.Author != null) + copy.WithAuthor(builder.Builder.Author); + + foreach (Discord.EmbedFieldBuilder field in builder.Builder.Fields) + { + copy.AddField(field.Name, field.Value, field.IsInline); + } + + return copy; + } } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index edc0fbb..0df0513 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -312,7 +312,7 @@ private void SetupPlayerList() playerDictionary[playerKey] = readyPlayers[i]; } - CustomReplacers.Add("players", () => string.Join(PlayerListSeparator, playerItems)); + AddCustomReplacer("players", string.Join(PlayerListSeparator, playerItems)); AddPlayers(playerDictionary); } diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 7b561c9..1b68b48 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -55,6 +55,7 @@ public static void Process() public static EmbedBuilder GetEmbed() { EmbedBuilder embed = !Player.ReadyList.Any() ? Translation.EmbedEmpty : Translation.Embed; + TranslationBuilder builder = new(embed.Description); if (Player.ReadyList.Any()) @@ -69,7 +70,6 @@ public static EmbedBuilder GetEmbed() public static void EditMessage() { - if (Channel == null && Message == null) return; if (Message == null) { Task.Run(async () => @@ -80,7 +80,17 @@ public static void EditMessage() return; } - Task.Run(async () => await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()).ConfigureAwait(false)); + Task.Run(async () => + { + try + { + await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()); + } + catch (Exception e) + { + Logger.Error(e); + } + }); } [CallOnReady] @@ -90,7 +100,6 @@ public static void Ready() { Logger.Error(LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); Plugin.Instance.Disable(); - return; } } @@ -103,15 +112,7 @@ public static async Task GetOrCreateMessage() if (Message == null) { EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; - try - { - embed.Description = new TranslationBuilder(embed.Description).Build(); - } - catch (Exception e) - { - Logger.Error(e); - return; - } + embed.Description = new TranslationBuilder(embed.Description); Message = await Channel.SendMessageAsync(embed: embed.Build()); diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index f533dd5..8d0a97e 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -11,7 +11,7 @@ public class Translation { Title = "Server Status", Color = Color.Blue.ToString(), - Description = "{playercount}/{maxplayers} currently online\\n```{players}```" + Description = "{playercount}/{maxplayers} currently online\n```{players}```" }; [Description("What will show when the server is empty.")] From a0564b88a9ef6a3edd6616552028acd9ab068e65 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 10 Jul 2025 21:09:44 +0100 Subject: [PATCH 15/68] feat: more translations --- .../API/Features/TranslationBuilder.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 0df0513..a7b130c 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -5,7 +5,11 @@ namespace DiscordLab.Bot.API.Features using Discord; using LabApi.Features.Extensions; using LabApi.Features.Wrappers; + using LightContainmentZoneDecontamination; + using Mirror.LiteNetLib4Mirror; using PlayerRoles; + using RoundRestarting; + using UnityEngine; /// /// Allows you to create translations with placeholders being replaced. @@ -45,6 +49,14 @@ public TranslationBuilder(string translation, string playerPrefix, Player player { // Map Replacers ["seed"] = () => Map.Seed.ToString(), + ["isdecont"] = () => DecontaminationController.Singleton.IsDecontaminating.ToString(), + ["remainingdeconttime"] = () => + Mathf + .Min( + 0, + (float)(DecontaminationController.Singleton.DecontaminationPhases[DecontaminationController.Singleton.DecontaminationPhases.Length - 1].TimeTrigger - DecontaminationController.GetServerTime)) + .ToString(CultureInfo.InvariantCulture), + ["isdecontenabled"] = () => (DecontaminationController.Singleton.NetworkDecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), // Round Replacers ["killcount"] = () => Round.TotalDeaths.ToString(), @@ -59,6 +71,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["islobbylocked"] = () => Round.IsLobbyLocked.ToString(), ["scpkillcount"] = () => Round.KilledBySCPs.ToString(), ["alivescpcount"] = () => Round.SurvivingSCPs.ToString(), + ["roundcount"] = () => RoundRestart.UptimeRounds.ToString(), // Server Replacers ["maxplayers"] = () => Server.MaxPlayers.ToString(), @@ -75,6 +88,9 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["playercount"] = () => Server.PlayerCount.ToString(), ["playercountnonpcs"] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), + ["version"] = () => GameCore.Version.VersionString, + ["isbeta"] = () => (GameCore.Version.PublicBeta || GameCore.Version.PublicBeta).ToString(), + ["isfriendlyfire"] = () => Server.FriendlyFire.ToString(), }; /// @@ -111,6 +127,14 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["maxhealth"] = player => player.MaxHealth.ToString(CultureInfo.CurrentCulture), ["group"] = player => player.GroupName, ["badgecolor"] = player => player.GroupColor.ToString(), + ["hasdnt"] = player => player.DoNotTrack.ToString(), + ["hasra"] = player => player.RemoteAdminAccess.ToString(), + ["isnorthwood"] = player => player.IsNorthwoodStaff.ToString(), + ["room"] = player => player.Room?.ToString() ?? "None", + ["zone"] = player => player.Zone.ToString(), + ["position"] = player => player.Position.ToString(), + ["ping"] = player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(), + ["isglobalmod"] = player => player.IsGlobalModerator.ToString(), }; #pragma warning restore SA1401 // FieldsMustBePrivate From dddd981c47f8bb43c2a547a13e01d795e6d1c59d Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 11 Jul 2025 14:03:33 +0100 Subject: [PATCH 16/68] fix(SC): no reset on waiting for players --- DiscordLab.StatusChannel/Events.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 1b68b48..0151954 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -16,7 +16,7 @@ public class Events : CustomEventsHandler { // events - public override void OnServerWaitingForPlayers() => Task.Run(GetOrCreateMessage); + public override void OnServerWaitingForPlayers() => EditMessage(); public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); From ac66ce6d29ea38f3f0a0717a3ce4542dd0e008f0 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 11 Jul 2025 14:05:00 +0100 Subject: [PATCH 17/68] style(DL): default blue embed --- DiscordLab.DeathLogs/Translation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index 23136cf..c7e21b5 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -25,7 +25,8 @@ public class Translation public EmbedBuilder DamageLogEmbed { get; set; } = new() { Title = "Damage Logs", - Description = "{entries}" + Description = "{entries}", + Color = Discord.Color.Blue.ToString() }; [Description("What each instance of damage will look like in the logs.")] From 8274cdbd43f34d29d6b80c0eefc9e575e1286b8e Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 11 Jul 2025 14:17:57 +0100 Subject: [PATCH 18/68] ci: automatically add dependencies --- DiscordLab.Bot/DiscordLab.Bot.csproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 4fad09f..3e78174 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -43,4 +43,21 @@ + + + $(MSBuildThisFileDirectory)bin\dependencies\ + + + + + + + + + + + + \ No newline at end of file From e1975db6306765de7a17cb5dee184132ff0785a4 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 11 Jul 2025 20:53:33 +0100 Subject: [PATCH 19/68] fix: spam on round restart --- DiscordLab.ConnectionLogs/Events.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index fbc140d..c658948 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -7,6 +7,7 @@ using LabApi.Events.Arguments.ServerEvents; using LabApi.Events.CustomHandlers; using LabApi.Features.Console; +using LabApi.Features.Wrappers; namespace DiscordLab.ConnectionLogs { @@ -18,6 +19,9 @@ public class Events : CustomEventsHandler public override void OnPlayerJoined(PlayerJoinedEventArgs ev) { + if (!Round.IsRoundInProgress) + return; + if (Config.JoinChannelId == 0) return; @@ -32,8 +36,7 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev) public override void OnPlayerLeft(PlayerLeftEventArgs ev) { - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (ev.Player == null) // for some reason, leave logs are sent with no player... + if (!Round.IsRoundInProgress) return; if (Config.LeaveChannelId == 0) From 01e2346ad15998f96fad5f245c6c33ceea428241 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 11 Jul 2025 22:13:56 +0100 Subject: [PATCH 20/68] fix: wrong translations --- DiscordLab.ConnectionLogs/Events.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index c658948..ea37808 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -62,7 +62,7 @@ public override void OnServerRoundStarted() return; } - channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave) + channel.SendMessage(new TranslationBuilder(Translation.RoundStart) { PlayerListItem = Translation.RoundPlayers }); @@ -79,7 +79,7 @@ public override void OnServerRoundEnded(RoundEndedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.PlayerLeave) + TranslationBuilder builder = new(Translation.RoundEnd) { PlayerListItem = Translation.RoundPlayers }; From 9a37447dea18febd99accb340d1a002ca37c0615 Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 12 Jul 2025 00:34:12 +0100 Subject: [PATCH 21/68] feat: more player placeholders --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index a7b130c..b1648ca 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -115,7 +115,9 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// public static Dictionary> PlayerReplacers { get; } = new() { - ["nickname"] = player => player.Nickname.Replace("@", "\\@"), + ["name"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere"), + ["nickname"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere"), + ["displayname"] = player => player.DisplayName, ["id"] = player => player.UserId, ["ip"] = player => player.IpAddress, ["userid"] = player => player.PlayerId.ToString(), @@ -135,6 +137,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["position"] = player => player.Position.ToString(), ["ping"] = player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(), ["isglobalmod"] = player => player.IsGlobalModerator.ToString(), + ["permissiongroup"] = player => player.PermissionsGroupName ?? "None", }; #pragma warning restore SA1401 // FieldsMustBePrivate From 314f352bbb0480f0343406f664a9c96fb1fa6e1f Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 12 Jul 2025 09:18:22 +0100 Subject: [PATCH 22/68] ci: remove xp system --- .github/workflows/artifact.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index e643d82..2fe2340 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -22,18 +22,11 @@ jobs: uses: NuGet/setup-nuget@v2 - name: Checkout uses: actions/checkout@v4 - - name: Get Exiled References + - name: Get References shell: pwsh run: | Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force - - name: Get XP System - shell: pwsh - run: | - $response = Invoke-RestMethod -Uri ${{ env.XP_SYSTEM_URL }} - $asset = $response.assets | Where-Object { $_.name -eq 'XPSystem-EXILED.dll' } | Select-Object -First 1 - $url = $asset.browser_download_url - Invoke-WebRequest -Uri $url -OutFile ${{ env.SL_REFERENCES }}/XPSystem.dll - name: Restore packages run: nuget restore ${{ github.workspace }}/DiscordLab.sln - name: Build From 05633fb9d339e5264d9a5116ef4fc02a56f00925 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 12 Jul 2025 16:17:25 +0100 Subject: [PATCH 23/68] fix: disconnect data mismatch --- DiscordLab.BotStatus/Plugin.cs | 6 +++--- DiscordLab.StatusChannel/Events.cs | 12 ++++++++++-- DiscordLab.StatusChannel/Plugin.cs | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 5e987ba..9538acb 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -25,7 +25,7 @@ public override void Enable() Instance = this; PlayerEvents.Joined += OnPlayerJoin; - PlayerEvents.Left += OnPlayerLeave; + ReferenceHub.OnPlayerRemoved += OnPlayerLeave; ServerEvents.WaitingForPlayers += OnWaitingForPlayers; } @@ -35,7 +35,7 @@ public override void Disable() ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; PlayerEvents.Joined -= OnPlayerJoin; - PlayerEvents.Left -= OnPlayerLeave; + ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; Instance = null; } @@ -53,7 +53,7 @@ public static void OnPlayerJoin(PlayerJoinedEventArgs _) Queue.Process(); } - public static void OnPlayerLeave(PlayerLeftEventArgs _) + public static void OnPlayerLeave(ReferenceHub _) { if(Round.IsRoundInProgress) UpdateStatus(); diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 0151954..7696360 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -19,8 +19,8 @@ public class Events : CustomEventsHandler public override void OnServerWaitingForPlayers() => EditMessage(); public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); - - public override void OnPlayerLeft(PlayerLeftEventArgs _) => Process(); + + public static void OnPlayerLeave(ReferenceHub _) => Process(); public override void OnServerRoundStarted() => EditMessage(); @@ -36,12 +36,20 @@ public class Events : CustomEventsHandler public static Queue Queue = new(5, EditMessage); + [CallOnLoad] + public static void Register() + { + ReferenceHub.OnPlayerRemoved += OnPlayerLeave; + } + [CallOnUnload] public static void Unregister() { Channel = null; Message = null; Queue = null; + + ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; } public static void Process() diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index 46b61e4..b36ea70 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -26,6 +26,8 @@ public override void Enable() { Instance = this; + CallOnLoadAttribute.Load(); + CallOnReadyAttribute.Load(); CustomHandlersManager.RegisterEventsHandler(Events); From 316920cea09d889b6b856953d922f5e6d0995087 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 12 Jul 2025 16:18:05 +0100 Subject: [PATCH 24/68] feat(translationbuilder): trim names --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index b1648ca..3cc264e 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -115,8 +115,8 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// public static Dictionary> PlayerReplacers { get; } = new() { - ["name"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere"), - ["nickname"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere"), + ["name"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), + ["nickname"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), ["displayname"] = player => player.DisplayName, ["id"] = player => player.UserId, ["ip"] = player => player.IpAddress, From 52c27c4c2ea2c247574179f8d89a3835e59acf8e Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 12 Jul 2025 16:21:43 +0100 Subject: [PATCH 25/68] fix: passive dmg tracker --- DiscordLab.DeathLogs/DamageLogs.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index 7ca123b..013e42c 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -9,8 +9,9 @@ using DiscordLab.Bot.API.Utilities; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; -using LabApi.Features.Console; using PlayerStatsSystem; +using UnityEngine; +using Logger = LabApi.Features.Console.Logger; namespace DiscordLab.DeathLogs { @@ -49,14 +50,24 @@ public static void OnHurt(PlayerHurtEventArgs ev) if (handler.Damage <= 0) return; string type = Events.ConvertToString(ev.DamageHandler); + // passive damage checkers, don't want these spamming console. - if (type == "Cardiac Arrest") return; - if (ev.Player.HasEffect() && type == "SCP-106") return; - if (ev.Player.HasEffect() && type == "SCP-106") return; - if (type == "Strangled") return; + switch (type) + { + case "Cardiac Arrest": + case "Unknown" when Mathf.Approximately(handler.Damage, 2.1f): + return; + } + if (ev.Player.HasEffect() && type == "SCP-106") + return; + if (ev.Player.HasEffect() && type == "SCP-106") + return; + if (type == "Strangled") + return; - if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) return; + if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) + return; string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) .AddPlayer("target", ev.Player) From 528282e92c85ecc7c0644fa8632b0c50723aebc4 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 12 Jul 2025 18:20:14 +0100 Subject: [PATCH 26/68] feat: add temp mute command config --- DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs | 3 +-- DiscordLab.Moderation/Config.cs | 6 ++++++ DiscordLab.Moderation/Plugin.cs | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs index 5191ffd..b33950c 100644 --- a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -5,11 +5,10 @@ namespace DiscordLab.Moderation.Commands { - [CommandHandler(typeof(RemoteAdminCommandHandler))] public class TempMuteRemoteAdmin : ICommand, IUsageProvider { public string Command { get; } = "tempmute"; - public string[] Aliases { get; } = ["tempm", "mutet"]; + public string[] Aliases { get; } = ["tempm", "mutet", "temporarymute", "mutetemp", "mutetemporary"]; public string Description { get; } = "Temporarily mutes a user."; public string[] Usage { get; } = diff --git a/DiscordLab.Moderation/Config.cs b/DiscordLab.Moderation/Config.cs index 848a6fb..197d98e 100644 --- a/DiscordLab.Moderation/Config.cs +++ b/DiscordLab.Moderation/Config.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; + namespace DiscordLab.Moderation { public class Config @@ -12,6 +14,10 @@ public class Config public ulong UnbanLogChannelId { get; set; } = 0; + [Description("Whether to add the Discord slash commands.")] public bool AddCommands { get; set; } = true; + + [Description("Whether to enable the temp mute remote admin command.")] + public bool AddTempMuteCommand { get; set; } = true; } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index 3dda9e8..2e4ad8a 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -2,10 +2,12 @@ using DiscordLab.Bot.API.Attributes; using DiscordLab.Bot.API.Features; using DiscordLab.Dependency; +using DiscordLab.Moderation.Commands; using LabApi.Events.CustomHandlers; using LabApi.Features; using LabApi.Features.Wrappers; using LabApi.Loader; +using RemoteAdmin; namespace DiscordLab.Moderation { @@ -32,6 +34,9 @@ public override void Enable() if (Config.AddCommands) SlashCommand.FindAll(); + if (Config.AddTempMuteCommand) + CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(new TempMuteRemoteAdmin()); + CustomHandlersManager.RegisterEventsHandler(Events); } From f630943d38086cbb46cf8c0a0fb6bdb94309e50b Mon Sep 17 00:00:00 2001 From: Lumi Date: Mon, 14 Jul 2025 20:13:45 +0100 Subject: [PATCH 27/68] ci: add pre-release check --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6e8ed2..23e017c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,7 @@ on: jobs: notify: + if: github.event.release.prerelease == false runs-on: ubuntu-latest steps: - name: Checkout From 01eaa65dd721dec46028ec6c5739592cd91e6fdb Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 1 Aug 2025 17:29:31 +0100 Subject: [PATCH 28/68] fix: index out of range exception --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 3cc264e..01ff7a3 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -288,6 +288,9 @@ public string Build() foreach (KeyValuePair player in Players) { + if (player.Value is not { IsReady: true }) + continue; + returnTranslation = Regex.Replace( returnTranslation, ToParameterString(player.Key), @@ -306,6 +309,10 @@ public string Build() { replacement = "Unknown"; } + catch (IndexOutOfRangeException) + { + replacement = "Unknown"; + } if (string.IsNullOrEmpty(replacement)) replacement = "Unknown"; From 23effb7aa5095fea246609cfe229e79410b99742 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sun, 3 Aug 2025 22:06:10 +0100 Subject: [PATCH 29/68] fix: v14.1.3 changes --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 01ff7a3..58a97ae 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -56,7 +56,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player 0, (float)(DecontaminationController.Singleton.DecontaminationPhases[DecontaminationController.Singleton.DecontaminationPhases.Length - 1].TimeTrigger - DecontaminationController.GetServerTime)) .ToString(CultureInfo.InvariantCulture), - ["isdecontenabled"] = () => (DecontaminationController.Singleton.NetworkDecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), + ["isdecontenabled"] = () => (DecontaminationController.Singleton.DecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), // Round Replacers ["killcount"] = () => Round.TotalDeaths.ToString(), From e41834097dc8c4312bd1519ba63f7ac5bc69d035 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 15 Aug 2025 21:34:59 +0100 Subject: [PATCH 30/68] feat: file scoped namespaces --- .github/workflows/artifact.yml | 15 +- .../Commands/SendCommand.cs | 89 ++- DiscordLab.Administration/Config.cs | 29 +- DiscordLab.Administration/Events.cs | 139 ++-- DiscordLab.Administration/Patches/ErrorLog.cs | 33 +- DiscordLab.Administration/Plugin.cs | 57 +- DiscordLab.Administration/Translation.cs | 25 +- .../API/Attributes/CallOnLoadAttribute.cs | 57 +- .../API/Attributes/CallOnReadyAttribute.cs | 75 ++- .../API/Attributes/CallOnUnloadAttribute.cs | 57 +- .../API/Extensions/BitwiseExtensions.cs | 25 +- .../API/Extensions/DiscordExtensions.cs | 37 +- .../API/Features/AutocompleteCommand.cs | 25 +- .../API/Features/Embed/EmbedBuilder.cs | 151 +++-- .../API/Features/Embed/EmbedFieldBuilder.cs | 69 +- DiscordLab.Bot/API/Features/Queue.cs | 95 ++- DiscordLab.Bot/API/Features/SlashCommand.cs | 155 +++-- .../API/Features/TranslationBuilder.cs | 627 +++++++++--------- DiscordLab.Bot/API/Updates/GitHubRelease.cs | 53 +- .../API/Updates/GitHubReleaseAsset.cs | 33 +- DiscordLab.Bot/API/Updates/Module.cs | 131 ++-- DiscordLab.Bot/API/Updates/Updater.cs | 191 +++--- DiscordLab.Bot/API/Utilities/CommandUtils.cs | 65 +- DiscordLab.Bot/API/Utilities/LoggingUtils.cs | 33 +- DiscordLab.Bot/Client.cs | 321 +++++---- DiscordLab.Bot/Commands/DiscordCommand.cs | 183 +++-- DiscordLab.Bot/Commands/LocalAdminCommand.cs | 101 ++- DiscordLab.Bot/Config.cs | 63 +- DiscordLab.Bot/Patches/RestClientCreate.cs | 157 +++-- DiscordLab.Bot/Plugin.cs | 117 ++-- DiscordLab.BotStatus/Config.cs | 11 +- DiscordLab.BotStatus/Plugin.cs | 109 ++- DiscordLab.BotStatus/Translation.cs | 11 +- DiscordLab.ConnectionLogs/Config.cs | 25 +- DiscordLab.ConnectionLogs/Events.cs | 121 ++-- DiscordLab.ConnectionLogs/Plugin.cs | 41 +- DiscordLab.ConnectionLogs/Translation.cs | 27 +- DiscordLab.DeathLogs/Config.cs | 41 +- DiscordLab.DeathLogs/DamageLogs.cs | 187 +++--- DiscordLab.DeathLogs/Events.cs | 359 +++++----- DiscordLab.DeathLogs/Plugin.cs | 37 +- DiscordLab.DeathLogs/Translation.cs | 49 +- DiscordLab.Dependency/Plugin.cs | 47 +- DiscordLab.Moderation/Commands/Ban.cs | 127 ++-- DiscordLab.Moderation/Commands/Mute.cs | 115 ++-- .../Commands/TempMuteRemoteAdmin.cs | 83 ++- DiscordLab.Moderation/Commands/Unban.cs | 81 ++- DiscordLab.Moderation/Commands/Unmute.cs | 75 ++- DiscordLab.Moderation/Config.cs | 25 +- DiscordLab.Moderation/Events.cs | 159 +++-- DiscordLab.Moderation/Plugin.cs | 71 +- DiscordLab.Moderation/TempMuteConfig.cs | 9 +- DiscordLab.Moderation/TempMuteManager.cs | 131 ++-- DiscordLab.Moderation/Translation.cs | 129 ++-- DiscordLab.RoundLogs/Config.cs | 47 +- DiscordLab.RoundLogs/Events.cs | 249 ++++--- DiscordLab.RoundLogs/Plugin.cs | 41 +- DiscordLab.RoundLogs/Translation.cs | 29 +- DiscordLab.StatusChannel/Command.cs | 23 +- DiscordLab.StatusChannel/Config.cs | 17 +- DiscordLab.StatusChannel/Events.cs | 165 +++-- DiscordLab.StatusChannel/MessageConfig.cs | 11 +- DiscordLab.StatusChannel/Plugin.cs | 65 +- DiscordLab.StatusChannel/Translation.cs | 41 +- DiscordLab.sln.DotSettings | 3 - 65 files changed, 2954 insertions(+), 3015 deletions(-) delete mode 100644 DiscordLab.sln.DotSettings diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index 2fe2340..b0f8dbd 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -13,24 +13,19 @@ jobs: build: runs-on: windows-2022 steps: - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.1.3 - with: - vs-prerelease: false - msbuild-architecture: x64 - - name: Setup NuGet - uses: NuGet/setup-nuget@v2 - name: Checkout uses: actions/checkout@v4 + - name: Setup Dotnet + uses: actions/setup-dotnet@v4.0.1 + with: + dotnet-version: 9.0.x - name: Get References shell: pwsh run: | Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force - - name: Restore packages - run: nuget restore ${{ github.workspace }}/DiscordLab.sln - name: Build - run: msbuild.exe ${{ github.workspace }}/DiscordLab.sln /p:Configuration=Debug /p:Platform="Any CPU" + run: dotnet build -c:Release - name: Publish Artifact uses: actions/upload-artifact@v4 with: diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index 6654379..ae402ad 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -5,53 +5,52 @@ using LabApi.Features.Wrappers; using RemoteAdmin; -namespace DiscordLab.Administration.Commands +namespace DiscordLab.Administration.Commands; + +public class SendCommand : AutocompleteCommand { - public class SendCommand : AutocompleteCommand + public static Config Config => Plugin.Instance.Config; + + public static Translation Translation => Plugin.Instance.Translation; + + public override SlashCommandBuilder Data { get; } = new() { - public static Config Config => Plugin.Instance.Config; - - public static Translation Translation => Plugin.Instance.Translation; - - public override SlashCommandBuilder Data { get; } = new() - { - Name = Translation.SendCommandName, - Description = Translation.SendCommandDescription, - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = Translation.SendCommandOptionName, - Description = Translation.SendCommandOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - } - ] - }; - - public override ulong GuildId { get; } = Config.GuildId; + Name = Translation.SendCommandName, + Description = Translation.SendCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.SendCommandOptionName, + Description = Translation.SendCommandOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + } + ] + }; + + public override ulong GuildId { get; } = Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(); - - string response = Server.RunCommand((string)command.Data.Options.First().Value); - - TranslationBuilder builder = new(Translation.SendCommandResponse); - builder.CustomReplacers.Add("response", () => response); - - await command.ModifyOriginalResponseAsync(m => m.Content = builder); - } - - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - IEnumerable commands = - [ - ..CommandProcessor.GetAllCommands().Select(x => "/" + x.Command), - ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command) - ]; - await autocomplete.RespondAsync(commands.Select(x => new AutocompleteResult(x, x))); - } + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); + + string response = Server.RunCommand((string)command.Data.Options.First().Value); + + TranslationBuilder builder = new(Translation.SendCommandResponse); + builder.CustomReplacers.Add("response", () => response); + + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + } + + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + IEnumerable commands = + [ + ..CommandProcessor.GetAllCommands().Select(x => "/" + x.Command), + ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command) + ]; + await autocomplete.RespondAsync(commands.Select(x => new AutocompleteResult(x, x))); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Config.cs b/DiscordLab.Administration/Config.cs index 81435d5..04007a9 100644 --- a/DiscordLab.Administration/Config.cs +++ b/DiscordLab.Administration/Config.cs @@ -1,24 +1,23 @@ using System.ComponentModel; -namespace DiscordLab.Administration +namespace DiscordLab.Administration; + +public class Config { - public class Config - { - public ulong GuildId { get; set; } = 0; + public ulong GuildId { get; set; } = 0; - [Description("The channel to send server start logs")] - public ulong ServerStartChannelId { get; set; } = 0; + [Description("The channel to send server start logs")] + public ulong ServerStartChannelId { get; set; } = 0; - [Description("The channel to send error logs")] - public ulong ErrorLogChannelId { get; set; } = 0; + [Description("The channel to send error logs")] + public ulong ErrorLogChannelId { get; set; } = 0; - [Description("The channel to send remote admin logs")] - public ulong RemoteAdminChannelId { get; set; } = 0; + [Description("The channel to send remote admin logs")] + public ulong RemoteAdminChannelId { get; set; } = 0; - [Description("The channel to send normal command logs")] - public ulong CommandLogChannelId { get; set; } = 0; + [Description("The channel to send normal command logs")] + public ulong CommandLogChannelId { get; set; } = 0; - [Description("Whether to add the commands to the bot. Is false then commands won't be used.")] - public bool AddCommands { get; set; } = true; - } + [Description("Whether to add the commands to the bot. Is false then commands won't be used.")] + public bool AddCommands { get; set; } = true; } \ No newline at end of file diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs index d998bcf..4e0a2e6 100644 --- a/DiscordLab.Administration/Events.cs +++ b/DiscordLab.Administration/Events.cs @@ -12,99 +12,98 @@ using LabApi.Features.Enums; using LabApi.Features.Wrappers; -namespace DiscordLab.Administration +namespace DiscordLab.Administration; + +public class Events : CustomEventsHandler { - public class Events : CustomEventsHandler - { - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - private static bool IsSubscribed { get; set; } + private static bool IsSubscribed { get; set; } - [CallOnLoad] - public static void Load() - { - ServerEvents.WaitingForPlayers += OnServerStart; - IsSubscribed = true; - } + [CallOnLoad] + public static void Load() + { + ServerEvents.WaitingForPlayers += OnServerStart; + IsSubscribed = true; + } - [CallOnUnload] - public static void Unload() - { - if (!IsSubscribed) return; - ServerEvents.WaitingForPlayers -= OnServerStart; - IsSubscribed = false; - } + [CallOnUnload] + public static void Unload() + { + if (!IsSubscribed) return; + ServerEvents.WaitingForPlayers -= OnServerStart; + IsSubscribed = false; + } - public static void OnServerStart() - { - ServerEvents.WaitingForPlayers -= OnServerStart; - IsSubscribed = false; + public static void OnServerStart() + { + ServerEvents.WaitingForPlayers -= OnServerStart; + IsSubscribed = false; - if (Config.ServerStartChannelId == 0) - return; + if (Config.ServerStartChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.ServerStartChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId, Config.GuildId)); - return; - } - - channel.SendMessage(Translation.ServerStart); - } - - public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) + if (!Client.TryGetOrAddChannel(Config.ServerStartChannelId, out SocketTextChannel channel)) { - if (ev.Sender == null || !Player.TryGet(ev.Sender, out Player player)) - return; + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId, Config.GuildId)); + return; + } - SocketTextChannel channel; - TranslationBuilder builder; + channel.SendMessage(Translation.ServerStart); + } - Dictionary> customReplacers = new() - { - ["type"] = () => ev.CommandType.ToString(), - ["arguments"] = () => string.Join(" ", ev.Arguments), - ["command"] = () => ev.Command.Command, - ["commanddescription"] = () => ev.Command.Description, - }; + public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) + { + if (ev.Sender == null || !Player.TryGet(ev.Sender, out Player player)) + return; - if (ev.CommandType == CommandType.RemoteAdmin) - { - if (Config.RemoteAdminChannelId == 0) - return; - - if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", Config.RemoteAdminChannelId, Config.GuildId)); - return; - } + SocketTextChannel channel; + TranslationBuilder builder; - builder = new(Translation.RemoteAdmin, "player", player) - { - CustomReplacers = customReplacers - }; - - channel.SendMessage(builder); - return; - } - - if (Config.CommandLogChannelId == 0) + Dictionary> customReplacers = new() + { + ["type"] = () => ev.CommandType.ToString(), + ["arguments"] = () => string.Join(" ", ev.Arguments), + ["command"] = () => ev.Command.Command, + ["commanddescription"] = () => ev.Command.Description, + }; + + if (ev.CommandType == CommandType.RemoteAdmin) + { + if (Config.RemoteAdminChannelId == 0) return; - if (!Client.TryGetOrAddChannel(Config.CommandLogChannelId, out channel)) + if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", Config.RemoteAdminChannelId, Config.GuildId)); return; } - builder = new(Translation.CommandLog, "player", player) + builder = new(Translation.RemoteAdmin, "player", player) { CustomReplacers = customReplacers }; - + channel.SendMessage(builder); + return; + } + + if (Config.CommandLogChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.CommandLogChannelId, out channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId, Config.GuildId)); + return; } + + builder = new(Translation.CommandLog, "player", player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Patches/ErrorLog.cs b/DiscordLab.Administration/Patches/ErrorLog.cs index 0c272da..a1012a2 100644 --- a/DiscordLab.Administration/Patches/ErrorLog.cs +++ b/DiscordLab.Administration/Patches/ErrorLog.cs @@ -6,27 +6,26 @@ using HarmonyLib; using LabApi.Features.Console; -namespace DiscordLab.Administration.Patches +namespace DiscordLab.Administration.Patches; + +[HarmonyPatch(typeof(Logger), nameof(Logger.Error))] +public static class ErrorLog { - [HarmonyPatch(typeof(Logger), nameof(Logger.Error))] - public static class ErrorLog + public static void Postfix(object message) { - public static void Postfix(object message) - { - if (Plugin.Instance.Config.ErrorLogChannelId == 0) - return; + if (Plugin.Instance.Config.ErrorLogChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.ErrorLogChannelId, out SocketTextChannel channel)) - { - Logger.Raw( - $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}", ConsoleColor.Red); - return; - } + if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.ErrorLogChannelId, out SocketTextChannel channel)) + { + Logger.Raw( + $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}", ConsoleColor.Red); + return; + } - TranslationBuilder builder = new(Plugin.Instance.Translation.ErrorLog); - builder.CustomReplacers.Add("error", message.ToString); + TranslationBuilder builder = new(Plugin.Instance.Translation.ErrorLog); + builder.CustomReplacers.Add("error", message.ToString); - channel.SendMessage(builder); - } + channel.SendMessage(builder); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs index f7ceadb..5bcb4cd 100644 --- a/DiscordLab.Administration/Plugin.cs +++ b/DiscordLab.Administration/Plugin.cs @@ -5,44 +5,43 @@ using LabApi.Events.CustomHandlers; using LabApi.Features; -namespace DiscordLab.Administration +namespace DiscordLab.Administration; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.Administration"; + public override string Name { get; } = "DiscordLab.Administration"; - public override string Description { get; } = - "Allows you to log or do administrative actions from your Discord bot"; + public override string Description { get; } = + "Allows you to log or do administrative actions from your Discord bot"; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public Events Events = new(); + public Events Events = new(); - private Harmony harmony = new($"DiscordLab.Administration-{DateTime.Now.Ticks}"); + private Harmony harmony = new($"DiscordLab.Administration-{DateTime.Now.Ticks}"); - public override void Enable() - { - Instance = this; - harmony.PatchAll(); - CallOnLoadAttribute.Load(); + public override void Enable() + { + Instance = this; + harmony.PatchAll(); + CallOnLoadAttribute.Load(); - if(Config.AddCommands) - SlashCommand.FindAll(); + if(Config.AddCommands) + SlashCommand.FindAll(); - CustomHandlersManager.RegisterEventsHandler(Events); - } + CustomHandlersManager.RegisterEventsHandler(Events); + } - public override void Disable() - { - CustomHandlersManager.UnregisterEventsHandler(Events); - CallOnUnloadAttribute.Unload(); - harmony.UnpatchAll(); - Events = null; - Instance = null; - } + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); + CallOnUnloadAttribute.Unload(); + harmony.UnpatchAll(); + Events = null; + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs index 07a3f8f..4fdf710 100644 --- a/DiscordLab.Administration/Translation.cs +++ b/DiscordLab.Administration/Translation.cs @@ -1,26 +1,25 @@ using System.ComponentModel; using Discord; -namespace DiscordLab.Administration +namespace DiscordLab.Administration; + +public class Translation { - public class Translation - { - public string ServerStart { get; set; } = "Server has started"; + public string ServerStart { get; set; } = "Server has started"; - public string ErrorLog { get; set; } = "An error has occured:\n{error}"; + public string ErrorLog { get; set; } = "An error has occured:\n{error}"; - public string RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; + public string RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; - public string CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; + public string CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; - public string SendCommandName { get; set; } = "send"; + public string SendCommandName { get; set; } = "send"; - public string SendCommandDescription { get; set; } = "Sends a command to the server"; + public string SendCommandDescription { get; set; } = "Sends a command to the server"; - public string SendCommandOptionName { get; set; } = "command"; + public string SendCommandOptionName { get; set; } = "command"; - public string SendCommandOptionDescription { get; set; } = "The command to send"; + public string SendCommandOptionDescription { get; set; } = "The command to send"; - public string SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; - } + public string SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs index 5cb4722..32279c3 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs @@ -1,40 +1,39 @@ -namespace DiscordLab.Bot.API.Attributes -{ - using System.Reflection; - using LabApi.Features.Console; +namespace DiscordLab.Bot.API.Attributes; + +using System.Reflection; +using LabApi.Features.Console; +/// +/// An attribute that when used on a method, will trigger whenever your plugin is loaded. Requires you to run . +/// +[AttributeUsage(AttributeTargets.Method)] +public class CallOnLoadAttribute : Attribute +{ /// - /// An attribute that when used on a method, will trigger whenever your plugin is loaded. Requires you to run . + /// Find all attributes in your plugin and calls them. /// - [AttributeUsage(AttributeTargets.Method)] - public class CallOnLoadAttribute : Attribute + /// The assembly you wish to check, defaults to the current one. + public static void Load(Assembly assembly = null) { - /// - /// Find all attributes in your plugin and calls them. - /// - /// The assembly you wish to check, defaults to the current one. - public static void Load(Assembly assembly = null) - { - assembly ??= Assembly.GetCallingAssembly(); + assembly ??= Assembly.GetCallingAssembly(); - foreach (Type type in assembly.GetTypes()) + foreach (Type type in assembly.GetTypes()) + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - { - CallOnLoadAttribute attribute = method.GetCustomAttribute(); - if (attribute == null) - continue; + CallOnLoadAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; - Logger.Debug($"Loading load attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); + Logger.Debug($"Loading load attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); - try - { - method.Invoke(null, null); - } - catch (Exception ex) - { - Logger.Error(ex); - } + try + { + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); } } } diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs index 093f0f7..9334f71 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs @@ -1,57 +1,56 @@ -namespace DiscordLab.Bot.API.Attributes +namespace DiscordLab.Bot.API.Attributes; + +using System.Reflection; +using LabApi.Features.Console; + +/// +/// An attribute that when used on a method, will trigger whenever the is ready. +/// +[AttributeUsage(AttributeTargets.Method)] +public class CallOnReadyAttribute : Attribute { - using System.Reflection; - using LabApi.Features.Console; + private static List instances = []; /// - /// An attribute that when used on a method, will trigger whenever the is ready. + /// Locates all 's in your plugin and prepares them to be called. /// - [AttributeUsage(AttributeTargets.Method)] - public class CallOnReadyAttribute : Attribute + /// The assembly you wish to check, defaults to the current one. + public static void Load(Assembly assembly = null) { - private static List instances = []; + assembly ??= Assembly.GetCallingAssembly(); - /// - /// Locates all 's in your plugin and prepares them to be called. - /// - /// The assembly you wish to check, defaults to the current one. - public static void Load(Assembly assembly = null) + foreach (Type type in assembly.GetTypes()) { - assembly ??= Assembly.GetCallingAssembly(); - - foreach (Type type in assembly.GetTypes()) + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - { - CallOnReadyAttribute attribute = method.GetCustomAttribute(); - if (attribute == null) - continue; + CallOnReadyAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; - Logger.Debug($"Loading ready attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); + Logger.Debug($"Loading ready attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); - instances.Add(method); - } + instances.Add(method); } } + } - /// - /// Called whenever the bot is ready. - /// - internal static void Ready() + /// + /// Called whenever the bot is ready. + /// + internal static void Ready() + { + foreach (MethodInfo method in instances) { - foreach (MethodInfo method in instances) + try { - try - { - method.Invoke(null, null); - } - catch (Exception ex) - { - Logger.Error(ex); - } + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); } - - instances = null; } + + instances = null; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs index 6b58280..36e9f25 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs @@ -1,39 +1,38 @@ -namespace DiscordLab.Bot.API.Attributes -{ - using System.Reflection; - using LabApi.Features.Console; +namespace DiscordLab.Bot.API.Attributes; + +using System.Reflection; +using LabApi.Features.Console; +/// +/// An attribute that when used on a method, will trigger whenever your plugin is unloaded. Requires you to run . +/// +[AttributeUsage(AttributeTargets.Method)] +public class CallOnUnloadAttribute : Attribute +{ /// - /// An attribute that when used on a method, will trigger whenever your plugin is unloaded. Requires you to run . + /// Find all attributes in your plugin and calls them. /// - [AttributeUsage(AttributeTargets.Method)] - public class CallOnUnloadAttribute : Attribute + /// The assembly you wish to check, defaults to the current one. + public static void Unload(Assembly assembly = null) { - /// - /// Find all attributes in your plugin and calls them. - /// - /// The assembly you wish to check, defaults to the current one. - public static void Unload(Assembly assembly = null) - { - assembly ??= Assembly.GetCallingAssembly(); + assembly ??= Assembly.GetCallingAssembly(); - foreach (Type type in assembly.GetTypes()) + foreach (Type type in assembly.GetTypes()) + { + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - { - CallOnUnloadAttribute attribute = method.GetCustomAttribute(); - if (attribute == null) - continue; + CallOnUnloadAttribute attribute = method.GetCustomAttribute(); + if (attribute == null) + continue; - try - { - Logger.Debug($"Calling unload attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); - method.Invoke(null, null); - } - catch (Exception ex) - { - Logger.Error(ex); - } + try + { + Logger.Debug($"Calling unload attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug); + method.Invoke(null, null); + } + catch (Exception ex) + { + Logger.Error(ex); } } } diff --git a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs index 4bfa94e..2fac01d 100644 --- a/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs @@ -1,17 +1,16 @@ -namespace DiscordLab.Bot.API.Extensions +namespace DiscordLab.Bot.API.Extensions; + +/// +/// Contains extension methods for bitwise operations. +/// +public static class BitwiseExtensions { /// - /// Contains extension methods for bitwise operations. + /// Get flags from a . /// - public static class BitwiseExtensions - { - /// - /// Get flags from a . - /// - /// The flags. - /// The . - /// The flags that are active. - public static IEnumerable GetFlags(this T flags) - where T : Enum => Enum.GetValues(typeof(T)).Cast().Where(x => flags.HasFlag(x)); - } + /// The flags. + /// The . + /// The flags that are active. + public static IEnumerable GetFlags(this T flags) + where T : Enum => Enum.GetValues(typeof(T)).Cast().Where(x => flags.HasFlag(x)); } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs index c6b8eeb..59fca3e 100644 --- a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs @@ -1,23 +1,22 @@ -namespace DiscordLab.Bot.API.Extensions -{ - using Discord; - using Discord.WebSocket; +namespace DiscordLab.Bot.API.Extensions; + +using Discord; +using Discord.WebSocket; +/// +/// Extension methods to help with Discord based tasks. +/// +public static class DiscordExtensions +{ /// - /// Extension methods to help with Discord based tasks. + /// Runs a task that sends a message to the specified channel. /// - public static class DiscordExtensions - { - /// - /// Runs a task that sends a message to the specified channel. - /// - /// The channel to send the message to. - /// The text. - /// Whether the message is TTS. - /// The embed. - /// The embeds. - /// Text, embed or embeds is required here. - public static void SendMessage(this SocketTextChannel channel, string text = null, bool isTts = false, Embed embed = null, Embed[] embeds = null) => - Task.Run(async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false)); - } + /// The channel to send the message to. + /// The text. + /// Whether the message is TTS. + /// The embed. + /// The embeds. + /// Text, embed or embeds is required here. + public static void SendMessage(this SocketTextChannel channel, string text = null, bool isTts = false, Embed embed = null, Embed[] embeds = null) => + Task.Run(async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false)); } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/AutocompleteCommand.cs b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs index 90b757f..dbedf5b 100644 --- a/DiscordLab.Bot/API/Features/AutocompleteCommand.cs +++ b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs @@ -1,17 +1,16 @@ -namespace DiscordLab.Bot.API.Features -{ - using Discord.WebSocket; +namespace DiscordLab.Bot.API.Features; + +using Discord.WebSocket; +/// +/// Allows you to make autocomplete commands. +/// +public abstract class AutocompleteCommand : SlashCommand +{ /// - /// Allows you to make autocomplete commands. + /// The method that is called once an autocomplete request is made. /// - public abstract class AutocompleteCommand : SlashCommand - { - /// - /// The method that is called once an autocomplete request is made. - /// - /// The autocomplete instance. - /// The . - public abstract Task Autocomplete(SocketAutocompleteInteraction autocomplete); - } + /// The autocomplete instance. + /// The . + public abstract Task Autocomplete(SocketAutocompleteInteraction autocomplete); } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index 62c705b..151e15e 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -1,102 +1,101 @@ -namespace DiscordLab.Bot.API.Features.Embed -{ - using YamlDotNet.Serialization; +namespace DiscordLab.Bot.API.Features.Embed; + +using YamlDotNet.Serialization; +/// +/// Allows you to make an embed. Should be used in translations only. +/// +public class EmbedBuilder +{ /// - /// Allows you to make an embed. Should be used in translations only. + /// Gets or sets the embed title. /// - public class EmbedBuilder + public string Title { - /// - /// Gets or sets the embed title. - /// - public string Title - { - get => Builder.Title; - set => Builder.Title = value; - } + get => Builder.Title; + set => Builder.Title = value; + } - /// - /// Gets or sets the embed description. - /// - public string Description - { - get => Builder.Description; - set => Builder.Description = value; - } + /// + /// Gets or sets the embed description. + /// + public string Description + { + get => Builder.Description; + set => Builder.Description = value; + } - /// - /// Gets or sets the embed fields. - /// - public IEnumerable Fields - { - get => Builder.Fields.Select(x => new EmbedFieldBuilder { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); - set => Builder.Fields = value.Select(x => x.Builder).ToList(); - } + /// + /// Gets or sets the embed fields. + /// + public IEnumerable Fields + { + get => Builder.Fields.Select(x => new EmbedFieldBuilder { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); + set => Builder.Fields = value.Select(x => x.Builder).ToList(); + } - /// - /// Gets or sets the color of the embed. In string so #, 0x or the raw hex value will work. - /// - public string Color + /// + /// Gets or sets the color of the embed. In string so #, 0x or the raw hex value will work. + /// + public string Color + { + get => Builder.Color?.ToString(); + set { - get => Builder.Color?.ToString(); - set + if (value == null) { - if (value == null) - { - Builder.Color = null; - return; - } - - Builder.Color = Discord.Color.Parse(value); + Builder.Color = null; + return; } - } - [YamlIgnore] - private Discord.EmbedBuilder Builder { get; } = new(); + Builder.Color = Discord.Color.Parse(value); + } + } - /// - /// Changes a into a instance. - /// - /// The instance. - /// A copy of the instance. - public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) - { - Discord.EmbedBuilder copy = new(); + [YamlIgnore] + private Discord.EmbedBuilder Builder { get; } = new(); - if (builder.Builder.Title != null) - copy.WithTitle(builder.Builder.Title); + /// + /// Changes a into a instance. + /// + /// The instance. + /// A copy of the instance. + public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) + { + Discord.EmbedBuilder copy = new(); - if (builder.Builder.Description != null) - copy.WithDescription(builder.Builder.Description); + if (builder.Builder.Title != null) + copy.WithTitle(builder.Builder.Title); - if (builder.Builder.Color.HasValue) - copy.WithColor(builder.Builder.Color.Value); + if (builder.Builder.Description != null) + copy.WithDescription(builder.Builder.Description); - if (builder.Builder.Url != null) - copy.WithUrl(builder.Builder.Url); + if (builder.Builder.Color.HasValue) + copy.WithColor(builder.Builder.Color.Value); - if (builder.Builder.ImageUrl != null) - copy.WithImageUrl(builder.Builder.ImageUrl); + if (builder.Builder.Url != null) + copy.WithUrl(builder.Builder.Url); - if (builder.Builder.ThumbnailUrl != null) - copy.WithThumbnailUrl(builder.Builder.ThumbnailUrl); + if (builder.Builder.ImageUrl != null) + copy.WithImageUrl(builder.Builder.ImageUrl); - if (builder.Builder.Timestamp.HasValue) - copy.WithTimestamp(builder.Builder.Timestamp.Value); + if (builder.Builder.ThumbnailUrl != null) + copy.WithThumbnailUrl(builder.Builder.ThumbnailUrl); - if (builder.Builder.Footer != null) - copy.WithFooter(builder.Builder.Footer); + if (builder.Builder.Timestamp.HasValue) + copy.WithTimestamp(builder.Builder.Timestamp.Value); - if (builder.Builder.Author != null) - copy.WithAuthor(builder.Builder.Author); + if (builder.Builder.Footer != null) + copy.WithFooter(builder.Builder.Footer); - foreach (Discord.EmbedFieldBuilder field in builder.Builder.Fields) - { - copy.AddField(field.Name, field.Value, field.IsInline); - } + if (builder.Builder.Author != null) + copy.WithAuthor(builder.Builder.Author); - return copy; + foreach (Discord.EmbedFieldBuilder field in builder.Builder.Fields) + { + copy.AddField(field.Name, field.Value, field.IsInline); } + + return copy; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs index fd23aa7..0d9c27b 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs @@ -1,43 +1,42 @@ -namespace DiscordLab.Bot.API.Features.Embed -{ - using YamlDotNet.Serialization; +namespace DiscordLab.Bot.API.Features.Embed; + +using YamlDotNet.Serialization; +/// +/// Allows you to create embed fields for a . +/// +public class EmbedFieldBuilder +{ /// - /// Allows you to create embed fields for a . + /// Gets or sets the field name. /// - public class EmbedFieldBuilder + public string Name { - /// - /// Gets or sets the field name. - /// - public string Name - { - get => Builder.Name; - set => Builder.Name = value; - } - - /// - /// Gets or sets the field value. - /// - public string Value - { - get => Builder.Value.ToString(); - set => Builder.Value = value; - } + get => Builder.Name; + set => Builder.Name = value; + } - /// - /// Gets or sets a value indicating whether the field is inline. - /// - public bool IsInline - { - get => Builder.IsInline; - set => Builder.IsInline = value; - } + /// + /// Gets or sets the field value. + /// + public string Value + { + get => Builder.Value.ToString(); + set => Builder.Value = value; + } - /// - /// Gets the base builder. - /// - [YamlIgnore] - internal Discord.EmbedFieldBuilder Builder { get; } = new(); + /// + /// Gets or sets a value indicating whether the field is inline. + /// + public bool IsInline + { + get => Builder.IsInline; + set => Builder.IsInline = value; } + + /// + /// Gets the base builder. + /// + [YamlIgnore] + internal Discord.EmbedFieldBuilder Builder { get; } = new(); } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Queue.cs b/DiscordLab.Bot/API/Features/Queue.cs index 182cdc5..f118fed 100644 --- a/DiscordLab.Bot/API/Features/Queue.cs +++ b/DiscordLab.Bot/API/Features/Queue.cs @@ -1,58 +1,57 @@ -namespace DiscordLab.Bot.API.Features +namespace DiscordLab.Bot.API.Features; + +using MEC; + +/// +/// A utility class that helps with dealing with Discord rate limits by introducing a queue. +/// +public class Queue { - using MEC; + /// + /// Initializes a new instance of the class. + /// + /// The duration you wish to wait before calling the method. + /// The default action to run. Can be null. + public Queue(float duration, Action defaultAction = null) + { + Duration = duration; + DefaultAction = defaultAction; + } + + /// + /// Gets the duration of time to wait between each call. + /// + public float Duration { get; } /// - /// A utility class that helps with dealing with Discord rate limits by introducing a queue. + /// Gets a value indicating whether the queue is ongoing. /// - public class Queue + public bool IsBusy { get; private set; } + + /// + /// Gets the action to be run during when an action isn't provided. Can be null. + /// + public Action DefaultAction { get; } + + /// + /// Runs the queue process, either using the action parameter or default action. + /// + /// The action to run. Defaults to . + public void Process(Action action = null) { - /// - /// Initializes a new instance of the class. - /// - /// The duration you wish to wait before calling the method. - /// The default action to run. Can be null. - public Queue(float duration, Action defaultAction = null) - { - Duration = duration; - DefaultAction = defaultAction; - } - - /// - /// Gets the duration of time to wait between each call. - /// - public float Duration { get; } - - /// - /// Gets a value indicating whether the queue is ongoing. - /// - public bool IsBusy { get; private set; } - - /// - /// Gets the action to be run during when an action isn't provided. Can be null. - /// - public Action DefaultAction { get; } - - /// - /// Runs the queue process, either using the action parameter or default action. - /// - /// The action to run. Defaults to . - public void Process(Action action = null) - { - if (IsBusy) - return; + if (IsBusy) + return; - action ??= DefaultAction; - if (action == null) - throw new ArgumentException("DefaultAction and parameter action can not be null at the same time."); + action ??= DefaultAction; + if (action == null) + throw new ArgumentException("DefaultAction and parameter action can not be null at the same time."); - IsBusy = true; + IsBusy = true; - Timing.CallDelayed(Duration, () => - { - IsBusy = false; - action(); - }); - } + Timing.CallDelayed(Duration, () => + { + IsBusy = false; + action(); + }); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/SlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs index 0097a5b..52eabad 100644 --- a/DiscordLab.Bot/API/Features/SlashCommand.cs +++ b/DiscordLab.Bot/API/Features/SlashCommand.cs @@ -1,104 +1,103 @@ -namespace DiscordLab.Bot.API.Features -{ - using System.Collections.ObjectModel; - using System.Collections.Specialized; - using System.Reflection; - using Discord; - using Discord.WebSocket; - using DiscordLab.Bot.API.Attributes; - using LabApi.Features.Console; +namespace DiscordLab.Bot.API.Features; + +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Reflection; +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Attributes; +using LabApi.Features.Console; +/// +/// A wrapper to easily add your own slash commands in your bot. +/// +public abstract class SlashCommand +{ /// - /// A wrapper to easily add your own slash commands in your bot. + /// The list of currently active s. /// - public abstract class SlashCommand - { - /// - /// The list of currently active s. - /// #pragma warning disable SA1401 - public static ObservableCollection Commands = []; + public static ObservableCollection Commands = []; #pragma warning restore SA1401 - /// - /// Gets the slash command data. - /// - public abstract SlashCommandBuilder Data { get; } + /// + /// Gets the slash command data. + /// + public abstract SlashCommandBuilder Data { get; } - /// - /// Gets the guild ID to assign this command to. - /// - public abstract ulong GuildId { get; } + /// + /// Gets the guild ID to assign this command to. + /// + public abstract ulong GuildId { get; } - /// - /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself. - /// - /// The assembly you wish to check, defaults to the current one. - public static void FindAll(Assembly assembly = null) - { - assembly ??= Assembly.GetCallingAssembly(); + /// + /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself. + /// + /// The assembly you wish to check, defaults to the current one. + public static void FindAll(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); - foreach (Type type in assembly.GetTypes()) - { - if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type)) - continue; + foreach (Type type in assembly.GetTypes()) + { + if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type)) + continue; - SlashCommand init = Activator.CreateInstance(type) as SlashCommand; - Commands.Add(init); - } + SlashCommand init = Activator.CreateInstance(type) as SlashCommand; + Commands.Add(init); } + } - /// - /// What should be called when you run this command. - /// - /// The command data. - /// A . - public abstract Task Run(SocketSlashCommand command); + /// + /// What should be called when you run this command. + /// + /// The command data. + /// A . + public abstract Task Run(SocketSlashCommand command); - [CallOnLoad] - private static void Start() - { - Commands.CollectionChanged += OnCollectionChanged; - } + [CallOnLoad] + private static void Start() + { + Commands.CollectionChanged += OnCollectionChanged; + } - [CallOnUnload] - private static void Unload() - { - Commands.CollectionChanged -= OnCollectionChanged; - Commands = null; - } + [CallOnUnload] + private static void Unload() + { + Commands.CollectionChanged -= OnCollectionChanged; + Commands = null; + } - [CallOnReady] - private static void Ready() - { - Task.Run(() => RegisterGuildCommands(Commands)); - } + [CallOnReady] + private static void Ready() + { + Task.Run(() => RegisterGuildCommands(Commands)); + } #pragma warning disable SA1313 - private static void OnCollectionChanged(object _, NotifyCollectionChangedEventArgs ev) + private static void OnCollectionChanged(object _, NotifyCollectionChangedEventArgs ev) #pragma warning restore SA1313 - { - if (!Client.IsClientReady) - return; - if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace)) - return; + { + if (!Client.IsClientReady) + return; + if (ev.Action is not (NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace)) + return; - Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems)); - } + Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems)); + } - private static async Task RegisterGuildCommands(IEnumerable commands) + private static async Task RegisterGuildCommands(IEnumerable commands) + { + foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) { - foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) + SocketGuild guild = Client.GetGuild(cmds.Key); + if (guild == null) { - SocketGuild guild = Client.GetGuild(cmds.Key); - if (guild == null) - { - Logger.Warn($"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); - continue; - } - - await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build()).ToArray()); + Logger.Warn($"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); + continue; } + + await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build()).ToArray()); } } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 58a97ae..f278968 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -1,354 +1,363 @@ -namespace DiscordLab.Bot.API.Features +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable PropertyCanBeMadeInitOnly.Global +namespace DiscordLab.Bot.API.Features; + +using System.Globalization; +using System.Text.RegularExpressions; +using Discord; +using LabApi.Features.Extensions; +using LabApi.Features.Wrappers; +using LightContainmentZoneDecontamination; +using Mirror.LiteNetLib4Mirror; +using PlayerRoles; +using RoundRestarting; +using UnityEngine; + +/// +/// Allows you to create translations with placeholders being replaced. +/// +public class TranslationBuilder { - using System.Globalization; - using System.Text.RegularExpressions; - using Discord; - using LabApi.Features.Extensions; - using LabApi.Features.Wrappers; - using LightContainmentZoneDecontamination; - using Mirror.LiteNetLib4Mirror; - using PlayerRoles; - using RoundRestarting; - using UnityEngine; + private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); + + private static readonly Regex UselessTextRemoveRegex = + new(@"(.*?)<\/color>", RegexOptions.Compiled); /// - /// Allows you to create translations with placeholders being replaced. + /// Initializes a new instance of the class. /// - public class TranslationBuilder + /// The translation to modify. + public TranslationBuilder(string translation) { - private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); + Translation = translation; + } - private static readonly Regex UselessTextRemoveRegex = new(@"(.*?)<\/color>", RegexOptions.Compiled); + /// + /// Initializes a new instance of the class with a person added. + /// + /// The translation to modify. + /// The player prefix. + /// The player to use for the prefix. + public TranslationBuilder(string translation, string playerPrefix, Player player) + { + Translation = translation; + AddPlayer(playerPrefix, player); + } - /// - /// Initializes a new instance of the class. - /// - /// The translation to modify. - public TranslationBuilder(string translation) + /// + /// Gets the dictionary of replacers that have no argument. + /// + public static Dictionary> StaticReplacers { get; } = new() + { + // Map Replacers + ["seed"] = () => Map.Seed.ToString(), + ["isdecont"] = () => DecontaminationController.Singleton.IsDecontaminating.ToString(), + ["remainingdeconttime"] = GetRemainingDecontaminationTime, + ["isdecontenabled"] = () => + (DecontaminationController.Singleton.DecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), + + // Round Replacers + ["killcount"] = () => Round.TotalDeaths.ToString(), + ["elapsedtime"] = () => Round.Duration.ToString(), + ["escapedscientistscount"] = () => Round.EscapedScientists.ToString(), + ["inprogress"] = () => Round.IsRoundInProgress.ToString(), + ["isended"] = () => Round.IsRoundEnded.ToString(), + ["isstarted"] = () => Round.IsRoundStarted.ToString(), + ["islocked"] = () => Round.IsLocked.ToString(), + ["changedintozombiescount"] = () => Round.ChangedIntoZombies.ToString(), + ["escapeddclassescount"] = () => Round.EscapedClassD.ToString(), + ["islobbylocked"] = () => Round.IsLobbyLocked.ToString(), + ["scpkillcount"] = () => Round.KilledBySCPs.ToString(), + ["alivescpcount"] = () => Round.SurvivingSCPs.ToString(), + ["roundcount"] = () => RoundRestart.UptimeRounds.ToString(), + + // Server Replacers + ["maxplayers"] = () => Server.MaxPlayers.ToString(), + ["name"] = () => Server.ServerListName, + ["nameparsed"] = () => { - Translation = translation; - } + string result = UselessTextRemoveRegex.Replace(Server.ServerListName, string.Empty); + result = TagRemoveRegex.Replace(result, string.Empty); + + return result; + }, + ["port"] = () => Server.Port.ToString(), + ["ip"] = () => Server.IpAddress, + ["playercount"] = () => Server.PlayerCount.ToString(), + ["playercountnonpcs"] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), + ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), + ["version"] = () => GameCore.Version.VersionString, + ["isbeta"] = () => (GameCore.Version.PublicBeta || GameCore.Version.PublicBeta).ToString(), + ["isfriendlyfire"] = () => Server.FriendlyFire.ToString(), + }; + + /// + /// Gets time based replacers. The type is the unix timestamp. Can be got with . + /// + public static Dictionary> TimeReplacers { get; } = new() + { + ["time"] = time => $"", + ["timet"] = time => $"", + ["timetlong"] = time => $"", + ["timed"] = time => $"", + ["timedlong"] = time => $"", + ["timef"] = time => $"", + ["timeflong"] = time => $"", + ["timer"] = time => $"", + ["elapsedtimerelative"] = time => $"", + ["roundstart"] = time => $"", + ["secondssince"] = time => new DateTimeOffset(time, Round.Duration).Second.ToString(), + ["minutessince"] = time => new DateTimeOffset(time, Round.Duration).Minute.ToString(), + }; + + /// + /// Gets player based replacements. + /// + public static Dictionary> PlayerReplacers { get; } = new() + { + ["name"] = player => + player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), + ["nickname"] = player => + player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), + ["displayname"] = player => player.DisplayName, + ["id"] = player => player.UserId, + ["ip"] = player => player.IpAddress, + ["userid"] = player => player.PlayerId.ToString(), + ["role"] = player => player.Role.GetFullName(), + ["roletype"] = player => player.Role.ToString(), + ["team"] = player => player.Team.ToString(), + ["faction"] = player => player.Team.GetFaction().ToString(), + ["health"] = player => player.Health.ToString(CultureInfo.CurrentCulture), + ["maxhealth"] = player => player.MaxHealth.ToString(CultureInfo.CurrentCulture), + ["group"] = player => player.GroupName, + ["badgecolor"] = player => player.GroupColor.ToString(), + ["hasdnt"] = player => player.DoNotTrack.ToString(), + ["hasra"] = player => player.RemoteAdminAccess.ToString(), + ["isnorthwood"] = player => player.IsNorthwoodStaff.ToString(), + ["room"] = player => player.Room?.ToString() ?? "None", + ["zone"] = player => player.Zone.ToString(), + ["position"] = player => player.Position.ToString(), + ["ping"] = player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(), + ["isglobalmod"] = player => player.IsGlobalModerator.ToString(), + ["permissiongroup"] = player => player.PermissionsGroupName ?? "None", + }; + + /// + /// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with. + /// + public Dictionary> CustomReplacers { get; set; } = new(); + + /// + /// Gets or sets the players that need to be translated for, if any. + /// + public Dictionary Players { get; set; } = new(); + + /// + /// Gets or sets the time that this translation will use. + /// + public DateTime Time { get; set; } = DateTime.Now; + + /// + /// Gets the translation. + /// + public string Translation { get; } + + /// + /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null. + /// + /// If you want the {players} placeholder to not work, set this to null. + public string PlayerListItem { get; set; } - /// - /// Initializes a new instance of the class with a person added. - /// - /// The translation to modify. - /// The player prefix. - /// The player to use for the prefix. - public TranslationBuilder(string translation, string playerPrefix, Player player) + /// + /// Gets or sets the separator between items in . + /// + public string PlayerListSeparator { get; set; } = "\n"; + + /// + /// Gets or sets the player list that will be used for . + /// + public IEnumerable PlayerList { get; set; } + + /// + /// . + /// + /// The instance. + /// + public static implicit operator string(TranslationBuilder builder) => + builder.Build(); + + /// + /// . + /// + /// The instance. + /// + public static implicit operator Optional(TranslationBuilder builder) => + builder.Build(); + + /// + /// Adds multiple players to the list. + /// + /// The players to add. + /// The instance. + public TranslationBuilder AddPlayers(Dictionary players) + { + foreach (KeyValuePair pair in players) { - Translation = translation; - AddPlayer(playerPrefix, player); + Players.Add(pair.Key, pair.Value); } -#pragma warning disable SA1401 // FieldsMustBePrivate - /// - /// Gets the dictionary of replacers that have no argument. - /// - public static Dictionary> StaticReplacers { get; } = new() - { - // Map Replacers - ["seed"] = () => Map.Seed.ToString(), - ["isdecont"] = () => DecontaminationController.Singleton.IsDecontaminating.ToString(), - ["remainingdeconttime"] = () => - Mathf - .Min( - 0, - (float)(DecontaminationController.Singleton.DecontaminationPhases[DecontaminationController.Singleton.DecontaminationPhases.Length - 1].TimeTrigger - DecontaminationController.GetServerTime)) - .ToString(CultureInfo.InvariantCulture), - ["isdecontenabled"] = () => (DecontaminationController.Singleton.DecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), - - // Round Replacers - ["killcount"] = () => Round.TotalDeaths.ToString(), - ["elapsedtime"] = () => Round.Duration.ToString(), - ["escapedscientistscount"] = () => Round.EscapedScientists.ToString(), - ["inprogress"] = () => Round.IsRoundInProgress.ToString(), - ["isended"] = () => Round.IsRoundEnded.ToString(), - ["isstarted"] = () => Round.IsRoundStarted.ToString(), - ["islocked"] = () => Round.IsLocked.ToString(), - ["changedintozombiescount"] = () => Round.ChangedIntoZombies.ToString(), - ["escapeddclassescount"] = () => Round.EscapedClassD.ToString(), - ["islobbylocked"] = () => Round.IsLobbyLocked.ToString(), - ["scpkillcount"] = () => Round.KilledBySCPs.ToString(), - ["alivescpcount"] = () => Round.SurvivingSCPs.ToString(), - ["roundcount"] = () => RoundRestart.UptimeRounds.ToString(), - - // Server Replacers - ["maxplayers"] = () => Server.MaxPlayers.ToString(), - ["name"] = () => Server.ServerListName, - ["nameparsed"] = () => - { - string result = UselessTextRemoveRegex.Replace(Server.ServerListName, string.Empty); - result = TagRemoveRegex.Replace(result, string.Empty); - - return result; - }, - ["port"] = () => Server.Port.ToString(), - ["ip"] = () => Server.IpAddress, - ["playercount"] = () => Server.PlayerCount.ToString(), - ["playercountnonpcs"] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), - ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), - ["version"] = () => GameCore.Version.VersionString, - ["isbeta"] = () => (GameCore.Version.PublicBeta || GameCore.Version.PublicBeta).ToString(), - ["isfriendlyfire"] = () => Server.FriendlyFire.ToString(), - }; - - /// - /// Gets time based replacers. The type is the unix timestamp. Can be got with . - /// - public static Dictionary> TimeReplacers { get; } = new() - { - ["time"] = time => $"", - ["timet"] = time => $"", - ["timetlong"] = time => $"", - ["timed"] = time => $"", - ["timedlong"] = time => $"", - ["timef"] = time => $"", - ["timeflong"] = time => $"", - ["timer"] = time => $"", - ["elapsedtimerelative"] = time => $"", - ["roundstart"] = time => $"", - }; - - /// - /// Gets player based replacements. - /// - public static Dictionary> PlayerReplacers { get; } = new() - { - ["name"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), - ["nickname"] = player => player.Nickname.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere").Trim(), - ["displayname"] = player => player.DisplayName, - ["id"] = player => player.UserId, - ["ip"] = player => player.IpAddress, - ["userid"] = player => player.PlayerId.ToString(), - ["role"] = player => player.Role.GetFullName(), - ["roletype"] = player => player.Role.ToString(), - ["team"] = player => player.Team.ToString(), - ["faction"] = player => player.Team.GetFaction().ToString(), - ["health"] = player => player.Health.ToString(CultureInfo.CurrentCulture), - ["maxhealth"] = player => player.MaxHealth.ToString(CultureInfo.CurrentCulture), - ["group"] = player => player.GroupName, - ["badgecolor"] = player => player.GroupColor.ToString(), - ["hasdnt"] = player => player.DoNotTrack.ToString(), - ["hasra"] = player => player.RemoteAdminAccess.ToString(), - ["isnorthwood"] = player => player.IsNorthwoodStaff.ToString(), - ["room"] = player => player.Room?.ToString() ?? "None", - ["zone"] = player => player.Zone.ToString(), - ["position"] = player => player.Position.ToString(), - ["ping"] = player => LiteNetLib4MirrorServer.GetPing(player.Connection.connectionId).ToString(), - ["isglobalmod"] = player => player.IsGlobalModerator.ToString(), - ["permissiongroup"] = player => player.PermissionsGroupName ?? "None", - }; -#pragma warning restore SA1401 // FieldsMustBePrivate - - /// - /// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with. - /// - public Dictionary> CustomReplacers { get; set; } = new(); - - /// - /// Gets or sets the players that need to be translated for, if any. - /// - public Dictionary Players { get; set; } = new(); - - /// - /// Gets or sets the time that this translation will use. - /// - public DateTime Time { get; set; } = DateTime.Now; - - /// - /// Gets the translation. - /// - public string Translation { get; } - - /// - /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null. - /// - /// If you want the {players} placeholder to not work, set this to null. - public string PlayerListItem { get; set; } - - /// - /// Gets or sets the separator between items in . - /// - public string PlayerListSeparator { get; set; } = "\n"; - - /// - /// Gets or sets the player list that will be used for . - /// - public IEnumerable PlayerList { get; set; } - - /// - /// . - /// - /// The instance. - /// - public static implicit operator string(TranslationBuilder builder) => - builder.Build(); - - /// - /// . - /// - /// The instance. - /// - public static implicit operator Optional(TranslationBuilder builder) => - builder.Build(); - - /// - /// Adds multiple players to the list. - /// - /// The players to add. - /// The instance. - public TranslationBuilder AddPlayers(Dictionary players) - { - foreach (KeyValuePair pair in players) - { - Players.Add(pair.Key, pair.Value); - } + return this; + } - return this; - } + /// + /// Adds a player to the list. + /// + /// The prefix for the player. + /// The to add. + /// The instance. + public TranslationBuilder AddPlayer(string prefix, Player player) + { + Players.Add(prefix, player); - /// - /// Adds a player to the list. - /// - /// The prefix for the player. - /// The to add. - /// The instance. - public TranslationBuilder AddPlayer(string prefix, Player player) - { - Players.Add(prefix, player); + return this; + } - return this; - } + /// + /// Adds a custom replacer to the dictionary. + /// + /// The text to replace. + /// The string factory to replace with. + /// The instance. + public TranslationBuilder AddCustomReplacer(string toReplace, Func replacer) + { + CustomReplacers.Add(toReplace, replacer); - /// - /// Adds a custom replacer to the dictionary. - /// - /// The text to replace. - /// The string factory to replace with. - /// The instance. - public TranslationBuilder AddCustomReplacer(string toReplace, Func replacer) - { - CustomReplacers.Add(toReplace, replacer); + return this; + } - return this; - } + /// + /// Adds a custom replacer to the dictionary. + /// + /// The text to replace. + /// The text to replace with. + /// The instance. + public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) + { + AddCustomReplacer(toReplace, () => replacer); - /// - /// Adds a custom replacer to the dictionary. - /// - /// The text to replace. - /// The text to replace with. - /// The instance. - public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) + return this; + } + + /// + /// Builds this instance. + /// + /// The translation built. + public string Build() + { + if (PlayerListItem != null) + SetupPlayerList(); + + string returnTranslation = Translation; + + foreach (KeyValuePair> replacer in CustomReplacers) { - AddCustomReplacer(toReplace, () => replacer); + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(), + RegexOptions.IgnoreCase); + } - return this; + foreach (KeyValuePair> replacer in StaticReplacers) + { + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(), + RegexOptions.IgnoreCase); } - /// - /// Builds this instance. - /// - /// The translation built. - public string Build() + long unix = new DateTimeOffset(Time).ToUnixTimeSeconds(); + foreach (KeyValuePair> replacer in TimeReplacers) { - if (PlayerListItem != null) - SetupPlayerList(); + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(replacer.Key), + replacer.Value(unix), + RegexOptions.IgnoreCase); + } - string returnTranslation = Translation; + foreach (KeyValuePair player in Players) + { + if (player.Value is not { IsReady: true }) + continue; - foreach (KeyValuePair> replacer in CustomReplacers) - { - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(), - RegexOptions.IgnoreCase); - } + returnTranslation = Regex.Replace( + returnTranslation, + ToParameterString(player.Key), + player.Value.Nickname, + RegexOptions.IgnoreCase); - foreach (KeyValuePair> replacer in StaticReplacers) + foreach (KeyValuePair> replacer in PlayerReplacers) { - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(), - RegexOptions.IgnoreCase); - } + string replacement; - long unix = new DateTimeOffset(Time).ToUnixTimeSeconds(); - foreach (KeyValuePair> replacer in TimeReplacers) - { - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(unix), - RegexOptions.IgnoreCase); - } + try + { + replacement = replacer.Value(player.Value); + } + catch (NullReferenceException) + { + replacement = "Unknown"; + } + catch (IndexOutOfRangeException) + { + replacement = "Unknown"; + } - foreach (KeyValuePair player in Players) - { - if (player.Value is not { IsReady: true }) - continue; + if (string.IsNullOrEmpty(replacement)) + replacement = "Unknown"; returnTranslation = Regex.Replace( returnTranslation, - ToParameterString(player.Key), - player.Value.Nickname, + ToParameterString($"{player.Key}{replacer.Key}"), + replacement, RegexOptions.IgnoreCase); - - foreach (KeyValuePair> replacer in PlayerReplacers) - { - string replacement; - - try - { - replacement = replacer.Value(player.Value); - } - catch (NullReferenceException) - { - replacement = "Unknown"; - } - catch (IndexOutOfRangeException) - { - replacement = "Unknown"; - } - - if (string.IsNullOrEmpty(replacement)) - replacement = "Unknown"; - - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString($"{player.Key}{replacer.Key}"), - replacement, - RegexOptions.IgnoreCase); - } } - - return returnTranslation; } - private static string ToParameterString(string str) => "{" + str + "}"; + return returnTranslation; + } - private void SetupPlayerList() - { - Player[] readyPlayers = (PlayerList ?? Player.ReadyList).ToArray(); + private static string ToParameterString(string str) => "{" + str + "}"; - int length = readyPlayers.Length; +#pragma warning disable SA1118 + private static string GetRemainingDecontaminationTime() => Mathf.Min( + 0, + (float)(DecontaminationController.Singleton + .DecontaminationPhases[DecontaminationController.Singleton.DecontaminationPhases.Length - 1] + .TimeTrigger - DecontaminationController.GetServerTime)) + .ToString(CultureInfo.InvariantCulture); +#pragma warning restore SA1118 - List playerItems = new(length); - Dictionary playerDictionary = new(length); + private void SetupPlayerList() + { + Player[] readyPlayers = (PlayerList ?? Player.ReadyList).ToArray(); - for (int i = 0; i < length; i++) - { - string playerKey = $"player{i}"; - playerItems.Add(PlayerListItem.Replace("{player", "{" + $"{playerKey}")); - playerDictionary[playerKey] = readyPlayers[i]; - } + int length = readyPlayers.Length; - AddCustomReplacer("players", string.Join(PlayerListSeparator, playerItems)); + List playerItems = new(length); + Dictionary playerDictionary = new(length); - AddPlayers(playerDictionary); + for (int i = 0; i < length; i++) + { + string playerKey = $"player{i}"; + playerItems.Add(PlayerListItem.Replace("{player", "{" + $"{playerKey}")); + playerDictionary[playerKey] = readyPlayers[i]; } + + AddCustomReplacer("players", string.Join(PlayerListSeparator, playerItems)); + + AddPlayers(playerDictionary); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/GitHubRelease.cs b/DiscordLab.Bot/API/Updates/GitHubRelease.cs index f882ba1..496d061 100644 --- a/DiscordLab.Bot/API/Updates/GitHubRelease.cs +++ b/DiscordLab.Bot/API/Updates/GitHubRelease.cs @@ -1,34 +1,33 @@ -namespace DiscordLab.Bot.API.Updates -{ - using System.Text.Json.Serialization; +namespace DiscordLab.Bot.API.Updates; + +using System.Text.Json.Serialization; +/// +/// Contains data for a GitHub release object. +/// +public class GitHubRelease +{ /// - /// Contains data for a GitHub release object. + /// Gets or sets the tag name for this release. /// - public class GitHubRelease - { - /// - /// Gets or sets the tag name for this release. - /// - [JsonPropertyName("tag_name")] - public string TagName { get; set; } + [JsonPropertyName("tag_name")] + public string TagName { get; set; } - /// - /// Gets or sets the assets for this release. - /// - [JsonPropertyName("assets")] - public List Assets { get; set; } + /// + /// Gets or sets the assets for this release. + /// + [JsonPropertyName("assets")] + public List Assets { get; set; } - /// - /// Gets or sets a value indicating whether this is a prerelease release. - /// - [JsonPropertyName("prerelease")] - public bool Prerelease { get; set; } + /// + /// Gets or sets a value indicating whether this is a prerelease release. + /// + [JsonPropertyName("prerelease")] + public bool Prerelease { get; set; } - /// - /// Gets or sets a value indicating whether this is a draft release. - /// - [JsonPropertyName("draft")] - public bool Draft { get; set; } - } + /// + /// Gets or sets a value indicating whether this is a draft release. + /// + [JsonPropertyName("draft")] + public bool Draft { get; set; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs index 3cb8ca5..2d57a98 100644 --- a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs +++ b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs @@ -1,22 +1,21 @@ -namespace DiscordLab.Bot.API.Updates -{ - using System.Text.Json.Serialization; +namespace DiscordLab.Bot.API.Updates; + +using System.Text.Json.Serialization; +/// +/// Gets details for a GitHub release asset. +/// +public class GitHubReleaseAsset +{ /// - /// Gets details for a GitHub release asset. + /// Gets or sets the name of the asset. /// - public class GitHubReleaseAsset - { - /// - /// Gets or sets the name of the asset. - /// - [JsonPropertyName("name")] - public string Name { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } - /// - /// Gets or sets the download name of the asset. - /// - [JsonPropertyName("browser_download_url")] - public string DownloadUrl { get; set; } - } + /// + /// Gets or sets the download name of the asset. + /// + [JsonPropertyName("browser_download_url")] + public string DownloadUrl { get; set; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Bot/API/Updates/Module.cs index ff12ef5..1afa2db 100644 --- a/DiscordLab.Bot/API/Updates/Module.cs +++ b/DiscordLab.Bot/API/Updates/Module.cs @@ -1,83 +1,82 @@ -namespace DiscordLab.Bot.API.Updates -{ - using LabApi.Loader; - using LabApi.Loader.Features.Paths; +namespace DiscordLab.Bot.API.Updates; + +using LabApi.Loader; +using LabApi.Loader.Features.Paths; +/// +/// Contains information about a DiscordLab module. +/// +public class Module +{ /// - /// Contains information about a DiscordLab module. + /// Initializes a new instance of the class. /// - public class Module + /// The release of this module. + /// The asset of this module. + public Module(GitHubRelease release, GitHubReleaseAsset asset) { - /// - /// Initializes a new instance of the class. - /// - /// The release of this module. - /// The asset of this module. - public Module(GitHubRelease release, GitHubReleaseAsset asset) - { - Release = release; - Asset = asset; - Name = asset.Name.Replace(".dll", string.Empty); - Version = new(release.TagName.Split('-').First()); - ExistingPlugin = PluginLoader.Plugins.Keys.FirstOrDefault(x => Name == "DiscordLab.Bot" ? x.Name == "DiscordLab" : x.Name == Name); - } + Release = release; + Asset = asset; + Name = asset.Name.Replace(".dll", string.Empty); + Version = new(release.TagName.Split('-').First()); + ExistingPlugin = PluginLoader.Plugins.Keys.FirstOrDefault(x => Name == "DiscordLab.Bot" ? x.Name == "DiscordLab" : x.Name == Name); + } - /// - /// Gets the found modules as of current. - /// - public static IReadOnlyCollection CurrentModules { get; internal set; } = []; + /// + /// Gets the found modules as of current. + /// + public static IReadOnlyCollection CurrentModules { get; internal set; } = []; - /// - /// Gets the module name. - /// - public string Name { get; } + /// + /// Gets the module name. + /// + public string Name { get; } - /// - /// Gets the version of this Update. - /// - public Version Version { get; } + /// + /// Gets the version of this Update. + /// + public Version Version { get; } - /// - /// Gets the plugin of this module, null if module is not installed. - /// - public LabApi.Loader.Features.Plugins.Plugin ExistingPlugin { get; } + /// + /// Gets the plugin of this module, null if module is not installed. + /// + public LabApi.Loader.Features.Plugins.Plugin ExistingPlugin { get; } - /// - /// Gets the release this module comes from. - /// - public GitHubRelease Release { get; } + /// + /// Gets the release this module comes from. + /// + public GitHubRelease Release { get; } - /// - /// Gets the asset this module comes from. - /// - public GitHubReleaseAsset Asset { get; } + /// + /// Gets the asset this module comes from. + /// + public GitHubReleaseAsset Asset { get; } - /// - /// Generates a string used to show current version and latest version for a list of modules. Will throw if no existing plugin. - /// - /// The modules to generate for. - /// The generated string. - public static string GenerateUpdateString(IEnumerable modules) => string.Join( - "\n- ", modules.Select(module => - $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}")); + /// + /// Generates a string used to show current version and latest version for a list of modules. Will throw if no existing plugin. + /// + /// The modules to generate for. + /// The generated string. + public static string GenerateUpdateString(IEnumerable modules) => string.Join( + "\n- ", modules.Select(module => + $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}")); - /// - /// Downloads this module. - /// - /// A . - internal async Task Download() - { - string filePath = Path.Combine(PathManager.Plugins.FullName, "global", Asset.Name); + /// + /// Downloads this module. + /// + /// A . + internal async Task Download() + { + string filePath = Path.Combine(PathManager.Plugins.FullName, "global", Asset.Name); - if (ExistingPlugin != null) - { - filePath = Path.Combine(Path.GetDirectoryName(ExistingPlugin.FilePath)!, Asset.Name); - File.Delete(ExistingPlugin.FilePath); - } + if (ExistingPlugin != null) + { + filePath = Path.Combine(Path.GetDirectoryName(ExistingPlugin.FilePath)!, Asset.Name); + File.Delete(ExistingPlugin.FilePath); + } - byte[] data = await Updater.DownloadClient.GetByteArrayAsync($"/{Release.TagName}/{Asset.Name}"); + byte[] data = await Updater.DownloadClient.GetByteArrayAsync($"/{Release.TagName}/{Asset.Name}"); - File.WriteAllBytes(filePath, data); - } + File.WriteAllBytes(filePath, data); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/Updater.cs b/DiscordLab.Bot/API/Updates/Updater.cs index 32079a2..2506268 100644 --- a/DiscordLab.Bot/API/Updates/Updater.cs +++ b/DiscordLab.Bot/API/Updates/Updater.cs @@ -1,129 +1,128 @@ -namespace DiscordLab.Bot.API.Updates +namespace DiscordLab.Bot.API.Updates; + +using System.Net.Http; +using DiscordLab.Bot.API.Attributes; +using LabApi.Features.Console; +using LabApi.Loader; +using Utf8Json; + +/// +/// Handle updates within DiscordLab. +/// +public static class Updater { - using System.Net.Http; - using DiscordLab.Bot.API.Attributes; - using LabApi.Features.Console; - using LabApi.Loader; - using Utf8Json; + /// + /// Gets the HTTP Client to use for checking for releases. + /// + public static HttpClient Client { get; private set; } = new(); + + /// + /// Gets the HTTP Client to use for downloading new modules. + /// + public static HttpClient DownloadClient { get; private set; } = new(); /// - /// Handle updates within DiscordLab. + /// Checks the GitHub repository for updates, and returns back the latest versions of each module. /// - public static class Updater + /// The latest versions of each module. + public static async Task> CheckForUpdates() { - /// - /// Gets the HTTP Client to use for checking for releases. - /// - public static HttpClient Client { get; private set; } = new(); - - /// - /// Gets the HTTP Client to use for downloading new modules. - /// - public static HttpClient DownloadClient { get; private set; } = new(); - - /// - /// Checks the GitHub repository for updates, and returns back the latest versions of each module. - /// - /// The latest versions of each module. - public static async Task> CheckForUpdates() + using HttpResponseMessage response = await Client.GetAsync(string.Empty); + + try { - using HttpResponseMessage response = await Client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + } + catch (Exception) + { + return []; + } - try - { - response.EnsureSuccessStatusCode(); - } - catch (Exception) - { - return []; - } + using Stream stream = await response.Content.ReadAsStreamAsync(); - using Stream stream = await response.Content.ReadAsStreamAsync(); + GitHubRelease[] releases = JsonSerializer.Deserialize(stream); - GitHubRelease[] releases = JsonSerializer.Deserialize(stream); + List modules = []; - List modules = []; + foreach (GitHubRelease release in releases) + { + if (release.Prerelease || release.Draft) + continue; + if (release.TagName.Count(c => c == '.') == 3) + continue; + Version version = new(release.TagName.Split('-').First()); - foreach (GitHubRelease release in releases) + if (version.Major != Plugin.Instance.Version.Major) + continue; + + foreach (GitHubReleaseAsset asset in from asset in release.Assets let name = asset.Name.Replace(".dll", string.Empty) where !modules.Any(module => module.Name == name && module.Version > version) select asset) { - if (release.Prerelease || release.Draft) - continue; - if (release.TagName.Count(c => c == '.') == 3) - continue; - Version version = new(release.TagName.Split('-').First()); - - if (version.Major != Plugin.Instance.Version.Major) - continue; - - foreach (GitHubReleaseAsset asset in from asset in release.Assets let name = asset.Name.Replace(".dll", string.Empty) where !modules.Any(module => module.Name == name && module.Version > version) select asset) - { - modules.Add(new(release, asset)); - } + modules.Add(new(release, asset)); } - - Module.CurrentModules = modules; - - return Module.CurrentModules; } - /// - /// Runs and will either log out the updates available or install the updates, it depends on the config values. - /// - /// The modules that were updated, or need updating. - public static async Task> ManageUpdates() - { - IEnumerable modules = await CheckForUpdates(); - List modulesToUpdate = []; + Module.CurrentModules = modules; - foreach (Module module in modules) - { - if (module.ExistingPlugin == null) - continue; + return Module.CurrentModules; + } - if (module.Version > module.ExistingPlugin.Version) - modulesToUpdate.Add(module); - } + /// + /// Runs and will either log out the updates available or install the updates, it depends on the config values. + /// + /// The modules that were updated, or need updating. + public static async Task> ManageUpdates() + { + IEnumerable modules = await CheckForUpdates(); + List modulesToUpdate = []; - if (modulesToUpdate.Count == 0) - return []; + foreach (Module module in modules) + { + if (module.ExistingPlugin == null) + continue; - Logger.Warn($"DiscordLab modules need updating:\n${Module.GenerateUpdateString(modulesToUpdate)}"); + if (module.Version > module.ExistingPlugin.Version) + modulesToUpdate.Add(module); + } - if (!Plugin.Instance.Config.AutoUpdate) - return modulesToUpdate; + if (modulesToUpdate.Count == 0) + return []; - Logger.Info("Downloading DiscordLab updates..."); + Logger.Warn($"DiscordLab modules need updating:\n${Module.GenerateUpdateString(modulesToUpdate)}"); - foreach (Module module in modulesToUpdate) - { - await module.Download(); - } + if (!Plugin.Instance.Config.AutoUpdate) + return modulesToUpdate; - Logger.Info("All DiscordLab modules updated..."); + Logger.Info("Downloading DiscordLab updates..."); - return modulesToUpdate; + foreach (Module module in modulesToUpdate) + { + await module.Download(); } - [CallOnLoad] - private static void Setup() - { - Client.BaseAddress = new("https://api.github.com/repos/DiscordLabSCP/DiscordLab/releases"); - Client.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); + Logger.Info("All DiscordLab modules updated..."); - if (Plugin.Instance.Config.AutoUpdate) - { - DownloadClient.BaseAddress = new("https://github.com/DiscordLabSCP/DiscordLab/releases/download"); - DownloadClient.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); - } + return modulesToUpdate; + } - Task.Run(ManageUpdates); - } + [CallOnLoad] + private static void Setup() + { + Client.BaseAddress = new("https://api.github.com/repos/DiscordLabSCP/DiscordLab/releases"); + Client.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); - [CallOnUnload] - private static void Disable() + if (Plugin.Instance.Config.AutoUpdate) { - Client = null; - DownloadClient = null; + DownloadClient.BaseAddress = new("https://github.com/DiscordLabSCP/DiscordLab/releases/download"); + DownloadClient.DefaultRequestHeaders.Add("User-Agent", $"DiscordLab/{Plugin.Instance.Version}"); } + + Task.Run(ManageUpdates); + } + + [CallOnUnload] + private static void Disable() + { + Client = null; + DownloadClient = null; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs index 4d564aa..27ec6f0 100644 --- a/DiscordLab.Bot/API/Utilities/CommandUtils.cs +++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs @@ -1,46 +1,45 @@ -namespace DiscordLab.Bot.API.Utilities -{ - using LabApi.Features.Wrappers; +namespace DiscordLab.Bot.API.Utilities; + +using LabApi.Features.Wrappers; +/// +/// Utility methods for commands. +/// +public static class CommandUtils +{ /// - /// Utility methods for commands. + /// Gets a player from an unparsed string id, will check if or . /// - public static class CommandUtils + /// The ID to check. + /// The player if found. + public static Player GetPlayerFromUnparsed(string id) { - /// - /// Gets a player from an unparsed string id, will check if or . - /// - /// The ID to check. - /// The player if found. - public static Player GetPlayerFromUnparsed(string id) - { - return TryGetPlayerFromUnparsed(id, out Player player) ? player : null; - } + return TryGetPlayerFromUnparsed(id, out Player player) ? player : null; + } - /// - /// Tries to get a player from an unparsed string id, will check if or . - /// - /// The ID to check. - /// The player if found. - /// Whether the player was found. - public static bool TryGetPlayerFromUnparsed(string id, out Player player) + /// + /// Tries to get a player from an unparsed string id, will check if or . + /// + /// The ID to check. + /// The player if found. + /// Whether the player was found. + public static bool TryGetPlayerFromUnparsed(string id, out Player player) + { + if (int.TryParse(id, out int intId)) { - if (int.TryParse(id, out int intId)) + if (!Player.TryGet(intId, out player)) { - if (!Player.TryGet(intId, out player)) - { - return false; - } + return false; } - else + } + else + { + if (!Player.TryGet(id, out player)) { - if (!Player.TryGet(id, out player)) - { - return false; - } + return false; } - - return true; } + + return true; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs index 3523ffa..e1e93f3 100644 --- a/DiscordLab.Bot/API/Utilities/LoggingUtils.cs +++ b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs @@ -1,24 +1,23 @@ -namespace DiscordLab.Bot.API.Utilities +namespace DiscordLab.Bot.API.Utilities; + +/// +/// Contains utilities for logging related tasks. +/// +public static class LoggingUtils { /// - /// Contains utilities for logging related tasks. + /// Generates a message that will tell the user that the channel was not found. /// - public static class LoggingUtils + /// The submodule that this error comes from. + /// The channel ID that was missing. + /// The related guild ID, goes to the default guild ID if 0. + /// The error string. + public static string GenerateMissingChannelMessage(string type, ulong channelId, ulong guildId) { - /// - /// Generates a message that will tell the user that the channel was not found. - /// - /// The submodule that this error comes from. - /// The channel ID that was missing. - /// The related guild ID, goes to the default guild ID if 0. - /// The error string. - public static string GenerateMissingChannelMessage(string type, ulong channelId, ulong guildId) - { - if (guildId == 0) - guildId = Plugin.Instance.Config.GuildId; + if (guildId == 0) + guildId = Plugin.Instance.Config.GuildId; - return - $"Could not find channel {channelId} under the guild {guildId}, please make sure the bot has access and you put in the right IDs. This was triggered from {type}."; - } + return + $"Could not find channel {channelId} under the guild {guildId}, please make sure the bot has access and you put in the right IDs. This was triggered from {type}."; } } \ No newline at end of file diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs index cd80598..17fd937 100644 --- a/DiscordLab.Bot/Client.cs +++ b/DiscordLab.Bot/Client.cs @@ -1,192 +1,191 @@ -namespace DiscordLab.Bot +namespace DiscordLab.Bot; + +using System.Net; +using System.Net.WebSockets; +using Discord; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Discord.WebSocket; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using LabApi.Features.Console; + +/// +/// The Discord bot client. +/// +public static class Client { - using System.Net; - using System.Net.WebSockets; - using Discord; - using Discord.Net.Rest; - using Discord.Net.WebSockets; - using Discord.WebSocket; - using DiscordLab.Bot.API.Attributes; - using DiscordLab.Bot.API.Features; - using LabApi.Features.Console; + /// + /// Gets the websocket client for the Discord bot. + /// + public static DiscordSocketClient SocketClient { get; private set; } + + /// + /// Gets a value indicating whether the client is in the ready state. + /// + public static bool IsClientReady { get; private set; } + + /// + /// Gets a list of saved text channels listed by their ID. + /// + public static Dictionary SavedTextChannels { get; private set; } = new(); /// - /// The Discord bot client. + /// Gets the default guild for the plugin. /// - public static class Client + public static SocketGuild DefaultGuild { get; private set; } + + private static Config Config => Plugin.Instance.Config; + + /// + /// Gets a cached guild from a ID. + /// + /// The guild ID. + /// If the ID is 0, then the default guild (if it exists), if else then it will return the found guild, or null. + public static SocketGuild GetGuild(ulong id) { - /// - /// Gets the websocket client for the Discord bot. - /// - public static DiscordSocketClient SocketClient { get; private set; } - - /// - /// Gets a value indicating whether the client is in the ready state. - /// - public static bool IsClientReady { get; private set; } - - /// - /// Gets a list of saved text channels listed by their ID. - /// - public static Dictionary SavedTextChannels { get; private set; } = new(); - - /// - /// Gets the default guild for the plugin. - /// - public static SocketGuild DefaultGuild { get; private set; } - - private static Config Config => Plugin.Instance.Config; - - /// - /// Gets a cached guild from a ID. - /// - /// The guild ID. - /// If the ID is 0, then the default guild (if it exists), if else then it will return the found guild, or null. - public static SocketGuild GetGuild(ulong id) - { - return id == 0 ? DefaultGuild : SocketClient.GetGuild(id); - } + return id == 0 ? DefaultGuild : SocketClient.GetGuild(id); + } - /// - /// Gets or adds a channel via its ID. Uses cache. - /// - /// The ID of the channel. - /// The channel, if found. - public static SocketTextChannel GetOrAddChannel(ulong id) => - SavedTextChannels.GetOrAdd(id, () => SocketClient.GetChannel(id) as SocketTextChannel); - - /// - /// Tries to get or add a channel via its ID. Uses cache. - /// - /// The ID of the channel. - /// The channel, if found. - /// Whether the channel was found. - public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) - { - channel = GetOrAddChannel(id); - return channel != null; - } + /// + /// Gets or adds a channel via its ID. Uses cache. + /// + /// The ID of the channel. + /// The channel, if found. + public static SocketTextChannel GetOrAddChannel(ulong id) => + SavedTextChannels.GetOrAdd(id, () => SocketClient.GetChannel(id) as SocketTextChannel); + + /// + /// Tries to get or add a channel via its ID. Uses cache. + /// + /// The ID of the channel. + /// The channel, if found. + /// Whether the channel was found. + public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) + { + channel = GetOrAddChannel(id); + return channel != null; + } - /// - /// Starts the bot. - /// - [CallOnLoad] - internal static void Start() + /// + /// Starts the bot. + /// + [CallOnLoad] + internal static void Start() + { + DebugLog("Starting the Client"); + DiscordSocketConfig config = new() { - DebugLog("Starting the Client"); - DiscordSocketConfig config = new() - { - GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages, - LogLevel = Config.Debug ? LogSeverity.Debug : LogSeverity.Warning, - RestClientProvider = DefaultRestClientProvider.Create(), - WebSocketProvider = DefaultWebSocketProvider.Create(), - }; + GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages, + LogLevel = Config.Debug ? LogSeverity.Debug : LogSeverity.Warning, + RestClientProvider = DefaultRestClientProvider.Create(), + WebSocketProvider = DefaultWebSocketProvider.Create(), + }; - if (!string.IsNullOrEmpty(Config.ProxyUrl)) - { - DebugLog("Proxy is configured."); - WebProxy proxy = new(Config.ProxyUrl); - config.RestClientProvider = DefaultRestClientProvider.Create(true, proxy); - config.WebSocketProvider = DefaultWebSocketProvider.Create(proxy); - } + if (!string.IsNullOrEmpty(Config.ProxyUrl)) + { + DebugLog("Proxy is configured."); + WebProxy proxy = new(Config.ProxyUrl); + config.RestClientProvider = DefaultRestClientProvider.Create(true, proxy); + config.WebSocketProvider = DefaultWebSocketProvider.Create(proxy); + } - DebugLog("Done the initial setup..."); + DebugLog("Done the initial setup..."); - SocketClient = new(config); + SocketClient = new(config); - DebugLog("Client has been created..."); + DebugLog("Client has been created..."); - SocketClient.Log += OnLog; - SocketClient.Ready += OnReady; - SocketClient.SlashCommandExecuted += SlashCommandHandler; - SocketClient.AutocompleteExecuted += AutocompleteHandler; + SocketClient.Log += OnLog; + SocketClient.Ready += OnReady; + SocketClient.SlashCommandExecuted += SlashCommandHandler; + SocketClient.AutocompleteExecuted += AutocompleteHandler; - DebugLog("Client events subscribed..."); + DebugLog("Client events subscribed..."); - Task.Run(StartClient); - } + Task.Run(StartClient); + } - /// - /// Disables the bot. - /// - [CallOnUnload] - internal static void Disable() - { - SavedTextChannels = null; - - SocketClient.Log -= OnLog; - SocketClient.Ready -= OnReady; - SocketClient.SlashCommandExecuted -= SlashCommandHandler; - SocketClient.AutocompleteExecuted -= AutocompleteHandler; - Task.Run(async () => - { - await SocketClient.LogoutAsync(); - await SocketClient.StopAsync(); - await SocketClient.DisposeAsync(); - }); - } + /// + /// Disables the bot. + /// + [CallOnUnload] + internal static void Disable() + { + SavedTextChannels = null; - private static async Task StartClient() + SocketClient.Log -= OnLog; + SocketClient.Ready -= OnReady; + SocketClient.SlashCommandExecuted -= SlashCommandHandler; + SocketClient.AutocompleteExecuted -= AutocompleteHandler; + Task.Run(async () => { - DebugLog("Starting client..."); - await SocketClient.LoginAsync(TokenType.Bot, Config.Token); - await SocketClient.StartAsync(); - } + await SocketClient.LogoutAsync(); + await SocketClient.StopAsync(); + await SocketClient.DisposeAsync(); + }); + } - private static Task OnLog(LogMessage msg) - { - if (msg.Exception is WebSocketException or GatewayReconnectException) - return Task.CompletedTask; - - switch (msg.Severity) - { - case LogSeverity.Error or LogSeverity.Critical: - Logger.Error(msg); - break; - case LogSeverity.Warning: - Logger.Warn(msg); - break; - case LogSeverity.Debug: - DebugLog(msg); - break; - default: - Logger.Info(msg); - break; - } + private static async Task StartClient() + { + DebugLog("Starting client..."); + await SocketClient.LoginAsync(TokenType.Bot, Config.Token); + await SocketClient.StartAsync(); + } + private static Task OnLog(LogMessage msg) + { + if (msg.Exception is WebSocketException or GatewayReconnectException) return Task.CompletedTask; - } - private static Task OnReady() + switch (msg.Severity) { - DebugLog("Bot is ready"); - IsClientReady = true; - DefaultGuild = SocketClient.GetGuild(Config.GuildId); - CallOnReadyAttribute.Ready(); - return Task.CompletedTask; + case LogSeverity.Error or LogSeverity.Critical: + Logger.Error(msg); + break; + case LogSeverity.Warning: + Logger.Warn(msg); + break; + case LogSeverity.Debug: + DebugLog(msg); + break; + default: + Logger.Info(msg); + break; } - private static Task SlashCommandHandler(SocketSlashCommand command) - { - DebugLog($"{command.Data.Name} requested a response, finding the command..."); - SlashCommand cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); + return Task.CompletedTask; + } - cmd?.Run(command); - return Task.CompletedTask; - } + private static Task OnReady() + { + DebugLog("Bot is ready"); + IsClientReady = true; + DefaultGuild = SocketClient.GetGuild(Config.GuildId); + CallOnReadyAttribute.Ready(); + return Task.CompletedTask; + } - private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) - { - DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); - AutocompleteCommand command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; + private static Task SlashCommandHandler(SocketSlashCommand command) + { + DebugLog($"{command.Data.Name} requested a response, finding the command..."); + SlashCommand cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); - command?.Autocomplete(autocomplete); - return Task.CompletedTask; - } + cmd?.Run(command); + return Task.CompletedTask; + } - private static void DebugLog(object message) - { - Logger.Debug(message, Config.Debug); - } + private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) + { + DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); + AutocompleteCommand command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; + + command?.Autocomplete(autocomplete); + return Task.CompletedTask; + } + + private static void DebugLog(object message) + { + Logger.Debug(message, Config.Debug); } } \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index 18fa7ba..6889130 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -1,116 +1,115 @@ -namespace DiscordLab.Bot.Commands -{ - using Discord; - using Discord.WebSocket; - using DiscordLab.Bot.API.Features; - using DiscordLab.Bot.API.Updates; +namespace DiscordLab.Bot.Commands; + +using Discord; +using Discord.WebSocket; +using DiscordLab.Bot.API.Features; +using DiscordLab.Bot.API.Updates; +/// +public class DiscordCommand : AutocompleteCommand +{ /// - public class DiscordCommand : AutocompleteCommand + public override SlashCommandBuilder Data { get; } = new() { - /// - public override SlashCommandBuilder Data { get; } = new() - { - Name = "discordlab", - Description = "DiscordLab related commands", - DefaultMemberPermissions = GuildPermission.Administrator, - Options = - [ - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "list", - Description = "List all available DiscordLab modules", - }, + Name = "discordlab", + Description = "DiscordLab related commands", + DefaultMemberPermissions = GuildPermission.Administrator, + Options = + [ + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "list", + Description = "List all available DiscordLab modules", + }, - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "install", - Description = "The module to install", - Options = - [ - new() - { - Type = ApplicationCommandOptionType.String, - Name = "module", - Description = "The module to install", - IsRequired = true, - IsAutocomplete = true, - } + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "install", + Description = "The module to install", + Options = + [ + new() + { + Type = ApplicationCommandOptionType.String, + Name = "module", + Description = "The module to install", + IsRequired = true, + IsAutocomplete = true, + } - ], - }, + ], + }, - new() - { - Type = ApplicationCommandOptionType.SubCommand, - Name = "check", - Description = "Check for DiscordLab updates", - } + new() + { + Type = ApplicationCommandOptionType.SubCommand, + Name = "check", + Description = "Check for DiscordLab updates", + } - ], - }; + ], + }; - /// - public override ulong GuildId { get; } = 0; + /// + public override ulong GuildId { get; } = 0; - /// - public override async Task Run(SocketSlashCommand command) + /// + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(true); + string subcommand = command.Data.Options.First().Name; + switch (subcommand) { - await command.DeferAsync(true); - string subcommand = command.Data.Options.First().Name; - switch (subcommand) + case "list": + { + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); + break; + } + + case "install": { - case "list": + string moduleName = command.Data.Options.First().Options.First().Value.ToString(); + if (string.IsNullOrWhiteSpace(moduleName)) { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); - await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); - break; + await command.ModifyOriginalResponseAsync(m => m.Content = "Please provide a module name."); + return; } - case "install": + Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + if (module == null) { - string moduleName = command.Data.Options.First().Options.First().Value.ToString(); - if (string.IsNullOrWhiteSpace(moduleName)) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Please provide a module name."); - return; - } - - Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); - if (module == null) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); - return; - } - - await module.Download(); - ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - await command.ModifyOriginalResponseAsync(m => m.Content = "Downloaded module. Server will restart next round."); - break; + await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); + return; } - case "check": - { - IEnumerable modules = await Updater.ManageUpdates(); - if (!modules.Any()) - { - await command.ModifyOriginalResponseAsync(m => m.Content = "No updates found."); - return; - } + await module.Download(); + ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; + await command.ModifyOriginalResponseAsync(m => m.Content = "Downloaded module. Server will restart next round."); + break; + } - await command.ModifyOriginalResponseAsync(m => - m.Content = $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}"); - break; + case "check": + { + IEnumerable modules = await Updater.ManageUpdates(); + if (!modules.Any()) + { + await command.ModifyOriginalResponseAsync(m => m.Content = "No updates found."); + return; } + + await command.ModifyOriginalResponseAsync(m => + m.Content = $"Updates found, modules that need updating:\n{Module.GenerateUpdateString(modules)}"); + break; } } + } - /// - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot").Select(x => new AutocompleteResult(x.Name, x.Name))); - } + /// + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot").Select(x => new AutocompleteResult(x.Name, x.Name))); } } \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs index cef85cc..04b5eda 100644 --- a/DiscordLab.Bot/Commands/LocalAdminCommand.cs +++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs @@ -1,68 +1,67 @@ -namespace DiscordLab.Bot.Commands -{ - using System.Diagnostics.CodeAnalysis; - using CommandSystem; - using DiscordLab.Bot.API.Updates; +namespace DiscordLab.Bot.Commands; + +using System.Diagnostics.CodeAnalysis; +using CommandSystem; +using DiscordLab.Bot.API.Updates; +/// +[CommandHandler(typeof(GameConsoleCommandHandler))] +public class LocalAdminCommand : ICommand +{ /// - [CommandHandler(typeof(GameConsoleCommandHandler))] - public class LocalAdminCommand : ICommand - { - /// - public string Command { get; } = "discordlab"; + public string Command { get; } = "discordlab"; - /// - public string[] Aliases { get; } = ["dl", "lab"]; + /// + public string[] Aliases { get; } = ["dl", "lab"]; - /// - public string Description { get; } = "Do things directly with DiscordLab."; + /// + public string Description { get; } = "Do things directly with DiscordLab."; - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, [UnscopedRef] out string response) + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, [UnscopedRef] out string response) + { + switch (arguments.FirstOrDefault()) { - switch (arguments.FirstOrDefault()) + case "list": { - case "list": - { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); - response = "List of available DiscordLab modules:\n\n" + modules; - return true; - } - - case "install": - { - string moduleName = arguments.ElementAtOrDefault(1); - if (string.IsNullOrWhiteSpace(moduleName)) - { - response = "Please provide a module name."; - return false; - } - - Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); - if (module == null) - { - response = "Module not found."; - return false; - } - - Task.Run(module.Download); - ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - response = "Downloaded module. Server will restart next round."; - return true; - } + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + response = "List of available DiscordLab modules:\n\n" + modules; + return true; + } - case "check": + case "install": + { + string moduleName = arguments.ElementAtOrDefault(1); + if (string.IsNullOrWhiteSpace(moduleName)) { - Task.Run(Updater.ManageUpdates); - response = "Checking for updates..."; - return true; + response = "Please provide a module name."; + return false; } - default: + Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + if (module == null) { - response = "Invalid subcommand. Available subcommands: list, install, check"; + response = "Module not found."; return false; } + + Task.Run(module.Download); + ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; + response = "Downloaded module. Server will restart next round."; + return true; + } + + case "check": + { + Task.Run(Updater.ManageUpdates); + response = "Checking for updates..."; + return true; + } + + default: + { + response = "Invalid subcommand. Available subcommands: list, install, check"; + return false; } } } diff --git a/DiscordLab.Bot/Config.cs b/DiscordLab.Bot/Config.cs index ad0e518..d7559ac 100644 --- a/DiscordLab.Bot/Config.cs +++ b/DiscordLab.Bot/Config.cs @@ -1,40 +1,39 @@ -namespace DiscordLab.Bot -{ - using System.ComponentModel; +namespace DiscordLab.Bot; + +using System.ComponentModel; +/// +/// The config of this plugin. +/// +public sealed class Config +{ /// - /// The config of this plugin. + /// Gets or sets the token for the bot. /// - public sealed class Config - { - /// - /// Gets or sets the token for the bot. - /// - [Description("The token of the bot.")] - public string Token { get; set; } = "token"; + [Description("The token of the bot.")] + public string Token { get; set; } = "token"; - /// - /// Gets or sets the default guild ID. - /// - [Description("The default guild ID. Each module that has their guild ID set to 0 has their guild ID set to this.")] - public ulong GuildId { get; set; } = 0; + /// + /// Gets or sets the default guild ID. + /// + [Description("The default guild ID. Each module that has their guild ID set to 0 has their guild ID set to this.")] + public ulong GuildId { get; set; } = 0; - /// - /// Gets or sets a value indicating whether the plugin should check for DiscordLab updates. - /// - [Description("Whether the plugin should check for DiscordLab updates.")] - public bool AutoUpdate { get; set; } = true; + /// + /// Gets or sets a value indicating whether the plugin should check for DiscordLab updates. + /// + [Description("Whether the plugin should check for DiscordLab updates.")] + public bool AutoUpdate { get; set; } = true; - /// - /// Gets or sets the proxy URL. Shouldn't be set if proxy is not needed. - /// - [Description("The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")] - public string ProxyUrl { get; set; } = string.Empty; + /// + /// Gets or sets the proxy URL. Shouldn't be set if proxy is not needed. + /// + [Description("The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")] + public string ProxyUrl { get; set; } = string.Empty; - /// - /// Gets or sets a value indicating whether debug logging should be enabled. - /// - [Description("Enable debugging mode, useful to enable when needing to debug for developers.")] - public bool Debug { get; set; } = false; - } + /// + /// Gets or sets a value indicating whether debug logging should be enabled. + /// + [Description("Enable debugging mode, useful to enable when needing to debug for developers.")] + public bool Debug { get; set; } = false; } \ No newline at end of file diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index 6629b6c..bc5da3e 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -1,99 +1,98 @@ -namespace DiscordLab.Bot.Patches -{ - using System.Net; - using System.Net.Http; - using System.Reflection; - using System.Reflection.Emit; - using Discord.Net.Rest; - using HarmonyLib; - using LabApi.Features.Console; +namespace DiscordLab.Bot.Patches; + +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Reflection.Emit; +using Discord.Net.Rest; +using HarmonyLib; +using LabApi.Features.Console; +/// +/// Patches . +/// +[HarmonyPatch] +public static class RestClientCreate +{ /// - /// Patches . + /// Gets the target method to patch. /// - [HarmonyPatch] - public static class RestClientCreate + /// The method. + public static MethodBase TargetMethod() { - /// - /// Gets the target method to patch. - /// - /// The method. - public static MethodBase TargetMethod() + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - Type type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); - if (type == null) - continue; - ConstructorInfo constructor = type.GetConstructors().FirstOrDefault(); - if (constructor != null) - return constructor; - } - - return null; + Type type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); + if (type == null) + continue; + ConstructorInfo constructor = type.GetConstructors().FirstOrDefault(); + if (constructor != null) + return constructor; } - /// - /// The patch. - /// - /// The instructions. - /// The patched code. - public static IEnumerable Transpiler(IEnumerable instructions) - { - Logger.Debug("Transpiler start", Plugin.Instance.Config.Debug); + return null; + } + + /// + /// The patch. + /// + /// The instructions. + /// The patched code. + public static IEnumerable Transpiler(IEnumerable instructions) + { + Logger.Debug("Transpiler start", Plugin.Instance.Config.Debug); - CodeMatcher matcher = new CodeMatcher(instructions) - .MatchEndForward( - new CodeMatch(OpCodes.Ldarg_0), - new CodeMatch(OpCodes.Newobj, AccessTools.Constructor(typeof(HttpClientHandler))), - new CodeMatch(OpCodes.Dup), - new CodeMatch(OpCodes.Ldc_I4_3), - new CodeMatch(OpCodes.Callvirt), - new CodeMatch(OpCodes.Dup), - new CodeMatch(OpCodes.Ldc_I4_0), - new CodeMatch(OpCodes.Callvirt), - new CodeMatch(OpCodes.Dup), - new CodeMatch(OpCodes.Ldarg_2), - new CodeMatch(OpCodes.Callvirt), - new CodeMatch(OpCodes.Dup), - new CodeMatch(OpCodes.Ldarg_3), - new CodeMatch(OpCodes.Callvirt)); + CodeMatcher matcher = new CodeMatcher(instructions) + .MatchEndForward( + new CodeMatch(OpCodes.Ldarg_0), + new CodeMatch(OpCodes.Newobj, AccessTools.Constructor(typeof(HttpClientHandler))), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldc_I4_3), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldc_I4_0), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldarg_2), + new CodeMatch(OpCodes.Callvirt), + new CodeMatch(OpCodes.Dup), + new CodeMatch(OpCodes.Ldarg_3), + new CodeMatch(OpCodes.Callvirt)); - matcher.Advance(-13); + matcher.Advance(-13); - matcher.RemoveInstructions(14) - .Insert( - new CodeInstruction(OpCodes.Ldarg_0), - new CodeInstruction(OpCodes.Ldc_I4_3), // DecompressionMethods.GZip | DecompressionMethods.Deflate - new CodeInstruction(OpCodes.Ldc_I4_0), // UseCookies = false - new CodeInstruction(OpCodes.Ldarg_2), // useProxy parameter - new CodeInstruction(OpCodes.Ldarg_3), // webProxy parameter - new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(RestClientCreate), nameof(CreateHttpClientHandler)))); + matcher.RemoveInstructions(14) + .Insert( + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldc_I4_3), // DecompressionMethods.GZip | DecompressionMethods.Deflate + new CodeInstruction(OpCodes.Ldc_I4_0), // UseCookies = false + new CodeInstruction(OpCodes.Ldarg_2), // useProxy parameter + new CodeInstruction(OpCodes.Ldarg_3), // webProxy parameter + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(RestClientCreate), nameof(CreateHttpClientHandler)))); - Logger.Debug("Transpiler end", Plugin.Instance.Config.Debug); + Logger.Debug("Transpiler end", Plugin.Instance.Config.Debug); - return matcher.InstructionEnumeration(); - } + return matcher.InstructionEnumeration(); + } - private static HttpClientHandler CreateHttpClientHandler(DecompressionMethods decompressionMethods, bool useCookies, bool useProxy, IWebProxy webProxy) - { - Logger.Debug("Creating HttpClientHandler", Plugin.Instance.Config.Debug); + private static HttpClientHandler CreateHttpClientHandler(DecompressionMethods decompressionMethods, bool useCookies, bool useProxy, IWebProxy webProxy) + { + Logger.Debug("Creating HttpClientHandler", Plugin.Instance.Config.Debug); - HttpClientHandler handler = new() - { - AutomaticDecompression = decompressionMethods, - UseCookies = useCookies, - }; + HttpClientHandler handler = new() + { + AutomaticDecompression = decompressionMethods, + UseCookies = useCookies, + }; - if (!useProxy) - return handler; + if (!useProxy) + return handler; - Logger.Debug("Creating HttpClientHandler with proxy", Plugin.Instance.Config.Debug); + Logger.Debug("Creating HttpClientHandler with proxy", Plugin.Instance.Config.Debug); - handler.UseProxy = true; - handler.Proxy = webProxy; + handler.UseProxy = true; + handler.Proxy = webProxy; - return handler; - } + return handler; } } \ No newline at end of file diff --git a/DiscordLab.Bot/Plugin.cs b/DiscordLab.Bot/Plugin.cs index 3070472..d22153d 100644 --- a/DiscordLab.Bot/Plugin.cs +++ b/DiscordLab.Bot/Plugin.cs @@ -1,80 +1,79 @@ -namespace DiscordLab.Bot +namespace DiscordLab.Bot; + +using Discord; +using DiscordLab.Bot.API.Attributes; +using DiscordLab.Bot.API.Features; +using HarmonyLib; +using LabApi.Features; +using LabApi.Features.Console; +using LabApi.Loader.Features.Plugins; +using LabApi.Loader.Features.Plugins.Enums; + +/// +public sealed class Plugin : Plugin { - using Discord; - using DiscordLab.Bot.API.Attributes; - using DiscordLab.Bot.API.Features; - using HarmonyLib; - using LabApi.Features; - using LabApi.Features.Console; - using LabApi.Loader.Features.Plugins; - using LabApi.Loader.Features.Plugins.Enums; + /// + /// Gets the current instance of this plugin. + /// + public static Plugin Instance { get; private set; } /// - public sealed class Plugin : Plugin - { - /// - /// Gets the current instance of this plugin. - /// - public static Plugin Instance { get; private set; } + public override string Name { get; } = "DiscordLab"; - /// - public override string Name { get; } = "DiscordLab"; + /// + public override string Description { get; } = "A modular Discord bot for SCP:SL servers running LabAPI"; - /// - public override string Description { get; } = "A modular Discord bot for SCP:SL servers running LabAPI"; + /// + public override string Author { get; } = "LumiFae"; - /// - public override string Author { get; } = "LumiFae"; + /// + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - /// - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + /// + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - /// - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + /// + public override LoadPriority Priority { get; } = LoadPriority.Highest; - /// - public override LoadPriority Priority { get; } = LoadPriority.Highest; + /// + /// Gets the current config for the plugin. + /// + public new Config Config { get; private set; } - /// - /// Gets the current config for the plugin. - /// - public new Config Config { get; private set; } + private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}"); - private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}"); + /// + public override void Enable() + { + Instance = this; + Config = base.Config!; - /// - public override void Enable() + try { - Instance = this; - Config = base.Config!; - - try - { - TokenUtils.ValidateToken(TokenType.Bot, Config.Token); - } - catch (Exception) - { - Logger.Error("DiscordLab bot token is invalid"); - return; - } + TokenUtils.ValidateToken(TokenType.Bot, Config.Token); + } + catch (Exception) + { + Logger.Error("DiscordLab bot token is invalid"); + return; + } - Harmony.PatchAll(); + Harmony.PatchAll(); - CallOnLoadAttribute.Load(); - CallOnReadyAttribute.Load(); + CallOnLoadAttribute.Load(); + CallOnReadyAttribute.Load(); - SlashCommand.FindAll(); - } + SlashCommand.FindAll(); + } - /// - public override void Disable() - { - Harmony.UnpatchAll(); + /// + public override void Disable() + { + Harmony.UnpatchAll(); - CallOnUnloadAttribute.Unload(); + CallOnUnloadAttribute.Unload(); - Config = null; - Instance = null; - } + Config = null; + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.BotStatus/Config.cs b/DiscordLab.BotStatus/Config.cs index 9f3f0b0..201083a 100644 --- a/DiscordLab.BotStatus/Config.cs +++ b/DiscordLab.BotStatus/Config.cs @@ -1,11 +1,10 @@ using Discord; -namespace DiscordLab.BotStatus +namespace DiscordLab.BotStatus; + +public class Config { - public class Config - { - public ActivityType ActivityType { get; set; } = ActivityType.CustomStatus; + public ActivityType ActivityType { get; set; } = ActivityType.CustomStatus; - public bool IdleOnEmpty { get; set; } = false; - } + public bool IdleOnEmpty { get; set; } = false; } \ No newline at end of file diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 9538acb..a38f770 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -8,75 +8,74 @@ using LabApi.Features; using LabApi.Features.Wrappers; -namespace DiscordLab.BotStatus +namespace DiscordLab.BotStatus; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.BotStatus"; - public override string Description { get; } = "Allows your bot's status to update with player counts."; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.BotStatus"; + public override string Description { get; } = "Allows your bot's status to update with player counts."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - PlayerEvents.Joined += OnPlayerJoin; - ReferenceHub.OnPlayerRemoved += OnPlayerLeave; + PlayerEvents.Joined += OnPlayerJoin; + ReferenceHub.OnPlayerRemoved += OnPlayerLeave; - ServerEvents.WaitingForPlayers += OnWaitingForPlayers; - } + ServerEvents.WaitingForPlayers += OnWaitingForPlayers; + } - public override void Disable() - { - ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; + public override void Disable() + { + ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; - PlayerEvents.Joined -= OnPlayerJoin; - ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; + PlayerEvents.Joined -= OnPlayerJoin; + ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; - Instance = null; - } + Instance = null; + } - public static void OnWaitingForPlayers() - { - UpdateStatus(); - } + public static void OnWaitingForPlayers() + { + UpdateStatus(); + } - public static void OnPlayerJoin(PlayerJoinedEventArgs _) - { - if(Round.IsRoundInProgress) - UpdateStatus(); - else - Queue.Process(); - } + public static void OnPlayerJoin(PlayerJoinedEventArgs _) + { + if(Round.IsRoundInProgress) + UpdateStatus(); + else + Queue.Process(); + } - public static void OnPlayerLeave(ReferenceHub _) - { - if(Round.IsRoundInProgress) - UpdateStatus(); - else - Queue.Process(); - } + public static void OnPlayerLeave(ReferenceHub _) + { + if(Round.IsRoundInProgress) + UpdateStatus(); + else + Queue.Process(); + } - private static Queue Queue { get; } = new(5, UpdateStatus); + private static Queue Queue { get; } = new(5, UpdateStatus); - private static void UpdateStatus() + private static void UpdateStatus() + { + TranslationBuilder builder = new(Server.PlayerCount == 0 ? Instance.Translation.EmptyContent : Instance.Translation.NormalContent); + Task.Run(async () => await Client.SocketClient.SetGameAsync(builder, type:Instance.Config.ActivityType).ConfigureAwait(false)); + switch (Server.PlayerCount) { - TranslationBuilder builder = new(Server.PlayerCount == 0 ? Instance.Translation.EmptyContent : Instance.Translation.NormalContent); - Task.Run(async () => await Client.SocketClient.SetGameAsync(builder, type:Instance.Config.ActivityType).ConfigureAwait(false)); - switch (Server.PlayerCount) - { - case 0 when Instance.Config.IdleOnEmpty: - Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Idle).ConfigureAwait(false)); - break; - case > 0 when Instance.Config.IdleOnEmpty && - Client.SocketClient.Status == UserStatus.Idle: - Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Online).ConfigureAwait(false)); - break; - } + case 0 when Instance.Config.IdleOnEmpty: + Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Idle).ConfigureAwait(false)); + break; + case > 0 when Instance.Config.IdleOnEmpty && + Client.SocketClient.Status == UserStatus.Idle: + Task.Run(async () => await Client.SocketClient.SetStatusAsync(UserStatus.Online).ConfigureAwait(false)); + break; } } } \ No newline at end of file diff --git a/DiscordLab.BotStatus/Translation.cs b/DiscordLab.BotStatus/Translation.cs index e273ac3..d723dca 100644 --- a/DiscordLab.BotStatus/Translation.cs +++ b/DiscordLab.BotStatus/Translation.cs @@ -1,9 +1,8 @@ -namespace DiscordLab.BotStatus +namespace DiscordLab.BotStatus; + +public class Translation { - public class Translation - { - public string EmptyContent { get; set; } = "0/{maxplayers} players online"; + public string EmptyContent { get; set; } = "0/{maxplayers} players online"; - public string NormalContent { get; set; } = "{playercount}/{maxplayers} players online."; - } + public string NormalContent { get; set; } = "{playercount}/{maxplayers} players online."; } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Config.cs b/DiscordLab.ConnectionLogs/Config.cs index f2c82d5..4f147ff 100644 --- a/DiscordLab.ConnectionLogs/Config.cs +++ b/DiscordLab.ConnectionLogs/Config.cs @@ -1,21 +1,20 @@ using System.ComponentModel; -namespace DiscordLab.ConnectionLogs +namespace DiscordLab.ConnectionLogs; + +public class Config { - public class Config - { - [Description("The channel where the join logs will be sent.")] - public ulong JoinChannelId { get; set; } = 0; + [Description("The channel where the join logs will be sent.")] + public ulong JoinChannelId { get; set; } = 0; - [Description("The channel where the leave logs will be sent.")] - public ulong LeaveChannelId { get; set; } = 0; + [Description("The channel where the leave logs will be sent.")] + public ulong LeaveChannelId { get; set; } = 0; - [Description("The channel where the round start logs will be sent.")] - public ulong RoundStartChannelId { get; set; } = 0; + [Description("The channel where the round start logs will be sent.")] + public ulong RoundStartChannelId { get; set; } = 0; - [Description("The channel where the round end logs will be sent. Optional.")] - public ulong RoundEndChannelId { get; set; } = 0; + [Description("The channel where the round end logs will be sent. Optional.")] + public ulong RoundEndChannelId { get; set; } = 0; - public ulong GuildId { get; set; } = 0; - } + public ulong GuildId { get; set; } = 0; } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index ea37808..785d8b5 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -9,84 +9,83 @@ using LabApi.Features.Console; using LabApi.Features.Wrappers; -namespace DiscordLab.ConnectionLogs +namespace DiscordLab.ConnectionLogs; + +public class Events : CustomEventsHandler { - public class Events : CustomEventsHandler - { - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override void OnPlayerJoined(PlayerJoinedEventArgs ev) - { - if (!Round.IsRoundInProgress) - return; + public override void OnPlayerJoined(PlayerJoinedEventArgs ev) + { + if (!Round.IsRoundInProgress) + return; - if (Config.JoinChannelId == 0) - return; + if (Config.JoinChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.JoinChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("join log", Config.JoinChannelId, Config.GuildId)); - return; - } - - channel.SendMessage(new TranslationBuilder(Translation.PlayerJoin, "player", ev.Player)); + if (!Client.TryGetOrAddChannel(Config.JoinChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("join log", Config.JoinChannelId, Config.GuildId)); + return; } + + channel.SendMessage(new TranslationBuilder(Translation.PlayerJoin, "player", ev.Player)); + } - public override void OnPlayerLeft(PlayerLeftEventArgs ev) - { - if (!Round.IsRoundInProgress) - return; + public override void OnPlayerLeft(PlayerLeftEventArgs ev) + { + if (!Round.IsRoundInProgress) + return; - if (Config.LeaveChannelId == 0) - return; + if (Config.LeaveChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.LeaveChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId)); - return; - } - - channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave, "player", ev.Player)); + if (!Client.TryGetOrAddChannel(Config.LeaveChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId)); + return; } + + channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave, "player", ev.Player)); + } - public override void OnServerRoundStarted() - { - if (Config.RoundStartChannelId == 0) - return; + public override void OnServerRoundStarted() + { + if (Config.RoundStartChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.RoundStartChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, Config.GuildId)); - return; - } - - channel.SendMessage(new TranslationBuilder(Translation.RoundStart) - { - PlayerListItem = Translation.RoundPlayers - }); + if (!Client.TryGetOrAddChannel(Config.RoundStartChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, Config.GuildId)); + return; } - - public override void OnServerRoundEnded(RoundEndedEventArgs ev) + + channel.SendMessage(new TranslationBuilder(Translation.RoundStart) { - if (Config.RoundEndChannelId == 0) - return; + PlayerListItem = Translation.RoundPlayers + }); + } + + public override void OnServerRoundEnded(RoundEndedEventArgs ev) + { + if (Config.RoundEndChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.RoundEndChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.RoundEndChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new(Translation.RoundEnd) - { - PlayerListItem = Translation.RoundPlayers - }; + TranslationBuilder builder = new(Translation.RoundEnd) + { + PlayerListItem = Translation.RoundPlayers + }; - builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); - channel.SendMessage(builder); - } + channel.SendMessage(builder); } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index ea0f0bb..baecf4a 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -3,33 +3,32 @@ using LabApi.Events.CustomHandlers; using LabApi.Features; -namespace DiscordLab.ConnectionLogs +namespace DiscordLab.ConnectionLogs; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.ConnectionLogs"; - public override string Description { get; } = "Adds logging for connection based information"; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.ConnectionLogs"; + public override string Description { get; } = "Adds logging for connection based information"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public Events Events = new(); + public Events Events = new(); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - CustomHandlersManager.RegisterEventsHandler(Events); - } + CustomHandlersManager.RegisterEventsHandler(Events); + } - public override void Disable() - { - CustomHandlersManager.UnregisterEventsHandler(Events); - Events = null; + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); + Events = null; - Instance = null; - } + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Translation.cs b/DiscordLab.ConnectionLogs/Translation.cs index 9b443d3..b87ce3d 100644 --- a/DiscordLab.ConnectionLogs/Translation.cs +++ b/DiscordLab.ConnectionLogs/Translation.cs @@ -1,22 +1,21 @@ using System.ComponentModel; -namespace DiscordLab.ConnectionLogs +namespace DiscordLab.ConnectionLogs; + +public class Translation { - public class Translation - { - [Description("The message that will be sent when a player joins the server.")] - public string PlayerJoin { get; set; } = "`{player}` (`{playerid}`) has joined the server."; + [Description("The message that will be sent when a player joins the server.")] + public string PlayerJoin { get; set; } = "`{player}` (`{playerid}`) has joined the server."; - [Description("The message that will be sent when a player leaves the server.")] - public string PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; + [Description("The message that will be sent when a player leaves the server.")] + public string PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; - [Description("The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] - public string RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; + [Description("The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] + public string RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; - [Description("The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] - public string RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; + [Description("The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] + public string RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; - [Description("The message that indicates what a player looks like in the round start/end message.")] - public string RoundPlayers { get; set; } = "{playername} ({playerid})"; - } + [Description("The message that indicates what a player looks like in the round start/end message.")] + public string RoundPlayers { get; set; } = "{playername} ({playerid})"; } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Config.cs b/DiscordLab.DeathLogs/Config.cs index 12f01fc..bb45176 100644 --- a/DiscordLab.DeathLogs/Config.cs +++ b/DiscordLab.DeathLogs/Config.cs @@ -1,32 +1,31 @@ using System.ComponentModel; -namespace DiscordLab.DeathLogs +namespace DiscordLab.DeathLogs; + +public class Config { - public class Config - { - [Description("The channel where the normal death logs will be sent.")] - public ulong ChannelId { get; set; } = 0; + [Description("The channel where the normal death logs will be sent.")] + public ulong ChannelId { get; set; } = 0; - [Description( - "The channel where the death logs of cuffed players will be sent. Keep as default value to disable. Disabling this will make it so logs are only sent to the normal death logs channel, but without the cuffed identifier.")] - public ulong CuffedChannelId { get; set; } = 0; + [Description( + "The channel where the death logs of cuffed players will be sent. Keep as default value to disable. Disabling this will make it so logs are only sent to the normal death logs channel, but without the cuffed identifier.")] + public ulong CuffedChannelId { get; set; } = 0; - [Description( - "The channel where logs will be sent when a player dies by their own actions, or just they died because of something else.")] - public ulong SelfChannelId { get; set; } = 0; + [Description( + "The channel where logs will be sent when a player dies by their own actions, or just they died because of something else.")] + public ulong SelfChannelId { get; set; } = 0; - [Description("The channel where logs will be sent when a player dies by a teamkill.")] - public ulong TeamKillChannelId { get; set; } = 0; + [Description("The channel where logs will be sent when a player dies by a teamkill.")] + public ulong TeamKillChannelId { get; set; } = 0; - [Description("If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")] - public bool ScpIgnoreCuffed { get; set; } = true; + [Description("If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")] + public bool ScpIgnoreCuffed { get; set; } = true; - [Description("The channel to send death logs to, if any.")] - public ulong DamageLogChannelId { get; set; } = 0; + [Description("The channel to send death logs to, if any.")] + public ulong DamageLogChannelId { get; set; } = 0; - [Description("Whether damage logs shouldn't be tracked if the attacker is an SCP.")] - public bool IgnoreScpDamage { get; set; } = false; + [Description("Whether damage logs shouldn't be tracked if the attacker is an SCP.")] + public bool IgnoreScpDamage { get; set; } = false; - public ulong GuildId { get; set; } = 0; - } + public ulong GuildId { get; set; } = 0; } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index 013e42c..379d95f 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -13,134 +13,133 @@ using UnityEngine; using Logger = LabApi.Features.Console.Logger; -namespace DiscordLab.DeathLogs +namespace DiscordLab.DeathLogs; + +public static class DamageLogs { - public static class DamageLogs - { - public static List DamageLogEntries { get; set; } = new(); + public static List DamageLogEntries { get; set; } = new(); - public static SocketTextChannel Channel; + public static SocketTextChannel Channel; - private static Queue queue = new(5, SendLog); + private static Queue queue = new(5, SendLog); - [CallOnLoad] - public static void Register() - { - if (Plugin.Instance.Config.DamageLogChannelId == 0) return; - PlayerEvents.Hurt += OnHurt; - } + [CallOnLoad] + public static void Register() + { + if (Plugin.Instance.Config.DamageLogChannelId == 0) return; + PlayerEvents.Hurt += OnHurt; + } - [CallOnUnload] - public static void Unregister() - { - if (Plugin.Instance.Config.DamageLogChannelId == 0) return; - PlayerEvents.Hurt -= OnHurt; + [CallOnUnload] + public static void Unregister() + { + if (Plugin.Instance.Config.DamageLogChannelId == 0) return; + PlayerEvents.Hurt -= OnHurt; - DamageLogEntries = null; - Channel = null; - } + DamageLogEntries = null; + Channel = null; + } - public static void OnHurt(PlayerHurtEventArgs ev) - { - if (ev.Attacker == null || ev.Player == ev.Attacker) return; + public static void OnHurt(PlayerHurtEventArgs ev) + { + if (ev.Attacker == null || ev.Player == ev.Attacker) return; - if (ev.DamageHandler is not StandardDamageHandler handler) - return; + if (ev.DamageHandler is not StandardDamageHandler handler) + return; - if (handler.Damage <= 0) return; + if (handler.Damage <= 0) return; - string type = Events.ConvertToString(ev.DamageHandler); + string type = Events.ConvertToString(ev.DamageHandler); - // passive damage checkers, don't want these spamming console. - switch (type) - { - case "Cardiac Arrest": - case "Unknown" when Mathf.Approximately(handler.Damage, 2.1f): - return; - } - if (ev.Player.HasEffect() && type == "SCP-106") - return; - if (ev.Player.HasEffect() && type == "SCP-106") - return; - if (type == "Strangled") + // passive damage checkers, don't want these spamming console. + switch (type) + { + case "Cardiac Arrest": + case "Unknown" when Mathf.Approximately(handler.Damage, 2.1f): return; + } + if (ev.Player.HasEffect() && type == "SCP-106") + return; + if (ev.Player.HasEffect() && type == "SCP-106") + return; + if (type == "Strangled") + return; - if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) - return; + if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) + return; - string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) - .AddPlayer("target", ev.Player) - .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) - .AddCustomReplacer("cause", type); + string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) + .AddCustomReplacer("cause", type); - DamageLogEntries.Add(log); + DamageLogEntries.Add(log); - queue.Process(); - } + queue.Process(); + } - public static void SendLog() + public static void SendLog() + { + if (Channel == null && !Client.TryGetOrAddChannel(Plugin.Instance.Config.DamageLogChannelId, out Channel)) { - if (Channel == null && !Client.TryGetOrAddChannel(Plugin.Instance.Config.DamageLogChannelId, out Channel)) - { - Logger.Error( - LoggingUtils.GenerateMissingChannelMessage( - "damage logs", - Plugin.Instance.Config.DamageLogChannelId, - Plugin.Instance.Config.GuildId)); - return; - } + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage( + "damage logs", + Plugin.Instance.Config.DamageLogChannelId, + Plugin.Instance.Config.GuildId)); + return; + } - Channel.SendMessage(embeds:CreateEmbeds()); + Channel.SendMessage(embeds:CreateEmbeds()); - DamageLogEntries.Clear(); - } + DamageLogEntries.Clear(); + } - public static Embed[] CreateEmbeds() - { - List embeds = new(); + public static Embed[] CreateEmbeds() + { + List embeds = new(); - if (DamageLogEntries.Count == 0) - return embeds.ToArray(); + if (DamageLogEntries.Count == 0) + return embeds.ToArray(); - int currentIndex = 0; + int currentIndex = 0; - while (currentIndex < DamageLogEntries.Count) - { - EmbedBuilder embed = Plugin.Instance.Translation.DamageLogEmbed; + while (currentIndex < DamageLogEntries.Count) + { + EmbedBuilder embed = Plugin.Instance.Translation.DamageLogEmbed; - List currentEmbedLogs = new(); - int currentLength = 0; + List currentEmbedLogs = new(); + int currentLength = 0; - while (currentIndex < DamageLogEntries.Count) - { - string logEntry = DamageLogEntries[currentIndex]; - - int newLength = currentLength + logEntry.Length + (currentEmbedLogs.Count > 0 ? 1 : 0); + while (currentIndex < DamageLogEntries.Count) + { + string logEntry = DamageLogEntries[currentIndex]; - if (newLength > EmbedBuilder.MaxDescriptionLength && currentEmbedLogs.Count > 0) - break; + int newLength = currentLength + logEntry.Length + (currentEmbedLogs.Count > 0 ? 1 : 0); - if (logEntry.Length > EmbedBuilder.MaxDescriptionLength) - { - logEntry = logEntry.Substring(0, EmbedBuilder.MaxDescriptionLength - 3) + "..."; - currentEmbedLogs.Add(logEntry); - currentIndex++; - break; - } + if (newLength > EmbedBuilder.MaxDescriptionLength && currentEmbedLogs.Count > 0) + break; + if (logEntry.Length > EmbedBuilder.MaxDescriptionLength) + { + logEntry = logEntry.Substring(0, EmbedBuilder.MaxDescriptionLength - 3) + "..."; currentEmbedLogs.Add(logEntry); - currentLength = newLength; currentIndex++; + break; } - - if (currentEmbedLogs.Count <= 0) continue; - embed.Description = new TranslationBuilder(embed.Description).AddCustomReplacer("entries", string.Join("\n", currentEmbedLogs)); - embeds.Add(embed.Build()); + + currentEmbedLogs.Add(logEntry); + currentLength = newLength; + currentIndex++; } - - return embeds.ToArray(); + + if (currentEmbedLogs.Count <= 0) continue; + embed.Description = new TranslationBuilder(embed.Description).AddCustomReplacer("entries", string.Join("\n", currentEmbedLogs)); + embeds.Add(embed.Build()); } + + return embeds.ToArray(); } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Events.cs b/DiscordLab.DeathLogs/Events.cs index 28d3588..f8b5ae5 100644 --- a/DiscordLab.DeathLogs/Events.cs +++ b/DiscordLab.DeathLogs/Events.cs @@ -13,222 +13,221 @@ using PlayerRoles.PlayableScps.Scp939; using PlayerStatsSystem; -namespace DiscordLab.DeathLogs +namespace DiscordLab.DeathLogs; + +public static class Events { - public static class Events - { - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - // have to do this here over CustomEventsHandler because easier to maintain different logs in this case. - [CallOnLoad] - public static void Register() - { - PlayerEvents.Dying += OnTeamKill; - PlayerEvents.Dying += OnCuffKill; - PlayerEvents.Dying += OnDeath; - PlayerEvents.Dying += OnOwnDeath; - } + // have to do this here over CustomEventsHandler because easier to maintain different logs in this case. + [CallOnLoad] + public static void Register() + { + PlayerEvents.Dying += OnTeamKill; + PlayerEvents.Dying += OnCuffKill; + PlayerEvents.Dying += OnDeath; + PlayerEvents.Dying += OnOwnDeath; + } - [CallOnUnload] - public static void Unregister() - { - PlayerEvents.Dying -= OnTeamKill; - PlayerEvents.Dying -= OnCuffKill; - PlayerEvents.Dying -= OnDeath; - PlayerEvents.Dying -= OnOwnDeath; - } + [CallOnUnload] + public static void Unregister() + { + PlayerEvents.Dying -= OnTeamKill; + PlayerEvents.Dying -= OnCuffKill; + PlayerEvents.Dying -= OnDeath; + PlayerEvents.Dying -= OnOwnDeath; + } - public static void OnTeamKill(PlayerDyingEventArgs ev) - { - if (ev.Attacker == null || ev.Attacker.Team.GetFaction() != ev.Player.Team.GetFaction()) - return; + public static void OnTeamKill(PlayerDyingEventArgs ev) + { + if (ev.Attacker == null || ev.Attacker.Team.GetFaction() != ev.Player.Team.GetFaction()) + return; - if (Config.TeamKillChannelId == 0) - return; + if (Config.TeamKillChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.TeamKillChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("team kill logs", Config.TeamKillChannelId, Config.GuildId)); + if (!Client.TryGetOrAddChannel(Config.TeamKillChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("team kill logs", Config.TeamKillChannelId, Config.GuildId)); - return; - } + return; + } - TranslationBuilder builder = new TranslationBuilder(Translation.TeamKill) - .AddPlayer("target", ev.Player) - .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)) - .AddCustomReplacer("role", ev.Player.Team.GetFaction().ToString()); + TranslationBuilder builder = new TranslationBuilder(Translation.TeamKill) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)) + .AddCustomReplacer("role", ev.Player.Team.GetFaction().ToString()); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public static void OnCuffKill(PlayerDyingEventArgs ev) - { - if (ev.Attacker == null || !ev.Player.IsDisarmed || (ev.Attacker.IsSCP && Config.ScpIgnoreCuffed)) - return; + public static void OnCuffKill(PlayerDyingEventArgs ev) + { + if (ev.Attacker == null || !ev.Player.IsDisarmed || (ev.Attacker.IsSCP && Config.ScpIgnoreCuffed)) + return; - if (Config.CuffedChannelId == 0) - return; + if (Config.CuffedChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuff kill logs", Config.CuffedChannelId, Config.GuildId)); + if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuff kill logs", Config.CuffedChannelId, Config.GuildId)); - return; - } + return; + } - TranslationBuilder builder = new TranslationBuilder(Translation.CuffedPlayerDeath) - .AddPlayer("target", ev.Player) - .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + TranslationBuilder builder = new TranslationBuilder(Translation.CuffedPlayerDeath) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public static void OnDeath(PlayerDyingEventArgs ev) - { - if (ev.Attacker == null || ev.Player.IsDisarmed || - ev.Attacker.Team.GetFaction() == ev.Player.Team.GetFaction()) - return; + public static void OnDeath(PlayerDyingEventArgs ev) + { + if (ev.Attacker == null || ev.Player.IsDisarmed || + ev.Attacker.Team.GetFaction() == ev.Player.Team.GetFaction()) + return; - if (Config.ChannelId == 0) - return; + if (Config.ChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.ChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("kill logs", Config.ChannelId, Config.GuildId)); + if (!Client.TryGetOrAddChannel(Config.ChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("kill logs", Config.ChannelId, Config.GuildId)); - return; - } + return; + } - TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeath) - .AddPlayer("target", ev.Player) - .AddPlayer("player", ev.Attacker) - .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); + TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeath) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Attacker) + .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public static void OnOwnDeath(PlayerDyingEventArgs ev) - { - if (ev.Attacker != null) - return; + public static void OnOwnDeath(PlayerDyingEventArgs ev) + { + if (ev.Attacker != null) + return; - if (Config.SelfChannelId == 0) - return; + if (Config.SelfChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.SelfChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("self kill logs", Config.SelfChannelId, Config.GuildId)); + if (!Client.TryGetOrAddChannel(Config.SelfChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("self kill logs", Config.SelfChannelId, Config.GuildId)); - return; - } + return; + } - string converted = ConvertToString(ev.DamageHandler); + string converted = ConvertToString(ev.DamageHandler); - // usually because of disconnect, only way to really track rn - if (converted == "Unknown") - return; + // usually because of disconnect, only way to really track rn + if (converted == "Unknown") + return; - TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeathSelf) - .AddPlayer("player", ev.Player) - .AddCustomReplacer("cause", converted); + TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeathSelf) + .AddPlayer("player", ev.Player) + .AddCustomReplacer("cause", converted); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - private static Dictionary _translations = new() - { - { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, - { DeathTranslations.Bleeding.Id, "Bleeding" }, - { DeathTranslations.Crushed.Id, "Crushed" }, - { DeathTranslations.Decontamination.Id, "Decontamination" }, - { DeathTranslations.Explosion.Id, "Explosion" }, - { DeathTranslations.Falldown.Id, "Falldown" }, - { DeathTranslations.Poisoned.Id, "Poison" }, - { DeathTranslations.Recontained.Id, "Recontainment" }, - { DeathTranslations.Scp049.Id, "SCP-049" }, - { DeathTranslations.Scp096.Id, "SCP-096" }, - { DeathTranslations.Scp173.Id, "SCP-173" }, - { DeathTranslations.Scp207.Id, "SCP-207" }, - { DeathTranslations.Scp939Lunge.Id, "SCP-939 Lunge" }, - { DeathTranslations.Scp939Other.Id, "SCP-939" }, - { DeathTranslations.Scp3114Slap.Id, "SCP-3114" }, - { DeathTranslations.Tesla.Id, "Tesla" }, - { DeathTranslations.Unknown.Id, "Unknown" }, - { DeathTranslations.Warhead.Id, "Warhead" }, - { DeathTranslations.Zombie.Id, "SCP-049-2" }, - { DeathTranslations.BulletWounds.Id, "Firearm" }, - { DeathTranslations.PocketDecay.Id, "Pocket Decay" }, - { DeathTranslations.SeveredHands.Id, "Severed Hands" }, - { DeathTranslations.FriendlyFireDetector.Id, "Friendly Fire" }, - { DeathTranslations.UsedAs106Bait.Id, "Femur Breaker" }, - { DeathTranslations.MicroHID.Id, "Micro H.I.D." }, - { DeathTranslations.Hypothermia.Id, "Hypothermia" }, - { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, - { DeathTranslations.Scp1344.Id, "Severed Eyes" }, - }; + private static Dictionary _translations = new() + { + { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, + { DeathTranslations.Bleeding.Id, "Bleeding" }, + { DeathTranslations.Crushed.Id, "Crushed" }, + { DeathTranslations.Decontamination.Id, "Decontamination" }, + { DeathTranslations.Explosion.Id, "Explosion" }, + { DeathTranslations.Falldown.Id, "Falldown" }, + { DeathTranslations.Poisoned.Id, "Poison" }, + { DeathTranslations.Recontained.Id, "Recontainment" }, + { DeathTranslations.Scp049.Id, "SCP-049" }, + { DeathTranslations.Scp096.Id, "SCP-096" }, + { DeathTranslations.Scp173.Id, "SCP-173" }, + { DeathTranslations.Scp207.Id, "SCP-207" }, + { DeathTranslations.Scp939Lunge.Id, "SCP-939 Lunge" }, + { DeathTranslations.Scp939Other.Id, "SCP-939" }, + { DeathTranslations.Scp3114Slap.Id, "SCP-3114" }, + { DeathTranslations.Tesla.Id, "Tesla" }, + { DeathTranslations.Unknown.Id, "Unknown" }, + { DeathTranslations.Warhead.Id, "Warhead" }, + { DeathTranslations.Zombie.Id, "SCP-049-2" }, + { DeathTranslations.BulletWounds.Id, "Firearm" }, + { DeathTranslations.PocketDecay.Id, "Pocket Decay" }, + { DeathTranslations.SeveredHands.Id, "Severed Hands" }, + { DeathTranslations.FriendlyFireDetector.Id, "Friendly Fire" }, + { DeathTranslations.UsedAs106Bait.Id, "Femur Breaker" }, + { DeathTranslations.MicroHID.Id, "Micro H.I.D." }, + { DeathTranslations.Hypothermia.Id, "Hypothermia" }, + { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, + { DeathTranslations.Scp1344.Id, "Severed Eyes" }, + }; - internal static string ConvertToString(DamageHandlerBase handler) + internal static string ConvertToString(DamageHandlerBase handler) + { + switch (handler) { - switch (handler) + case CustomReasonDamageHandler: + return "Unknown, plugin specific death."; + case WarheadDamageHandler: + return "Warhead"; + case ExplosionDamageHandler: + return "Explosion"; + case Scp018DamageHandler: + return "SCP-018"; + case RecontainmentDamageHandler: + return "Recontainment"; + case MicroHidDamageHandler: + return "Micro H.I.D."; + case DisruptorDamageHandler: + return "Particle Disruptor"; + case Scp939DamageHandler: + return "SCP-939"; + case JailbirdDamageHandler: + return "Jailbird"; + case Scp1507DamageHandler: + return "SCP-1507"; + case Scp956DamageHandler: + return "SCP-956"; + case SnowballDamageHandler: + return "Snowball"; + case Scp3114DamageHandler scp3114DamageHandler: + return scp3114DamageHandler.Subtype switch + { + Scp3114DamageHandler.HandlerType.Strangulation => "Strangled", + Scp3114DamageHandler.HandlerType.SkinSteal => "SCP-3114", + Scp3114DamageHandler.HandlerType.Slap => "SCP-3114", + _ => "Unknown", + }; + case Scp049DamageHandler scp049DamageHandler: + return scp049DamageHandler.DamageSubType switch + { + Scp049DamageHandler.AttackType.CardiacArrest => "Cardiac Arrest", + Scp049DamageHandler.AttackType.Instakill => "SCP-049", + Scp049DamageHandler.AttackType.Scp0492 => "SCP-049-2", + _ => "Unknown", + }; + case UniversalDamageHandler universal: { - case CustomReasonDamageHandler: - return "Unknown, plugin specific death."; - case WarheadDamageHandler: - return "Warhead"; - case ExplosionDamageHandler: - return "Explosion"; - case Scp018DamageHandler: - return "SCP-018"; - case RecontainmentDamageHandler: - return "Recontainment"; - case MicroHidDamageHandler: - return "Micro H.I.D."; - case DisruptorDamageHandler: - return "Particle Disruptor"; - case Scp939DamageHandler: - return "SCP-939"; - case JailbirdDamageHandler: - return "Jailbird"; - case Scp1507DamageHandler: - return "SCP-1507"; - case Scp956DamageHandler: - return "SCP-956"; - case SnowballDamageHandler: - return "Snowball"; - case Scp3114DamageHandler scp3114DamageHandler: - return scp3114DamageHandler.Subtype switch - { - Scp3114DamageHandler.HandlerType.Strangulation => "Strangled", - Scp3114DamageHandler.HandlerType.SkinSteal => "SCP-3114", - Scp3114DamageHandler.HandlerType.Slap => "SCP-3114", - _ => "Unknown", - }; - case Scp049DamageHandler scp049DamageHandler: - return scp049DamageHandler.DamageSubType switch - { - Scp049DamageHandler.AttackType.CardiacArrest => "Cardiac Arrest", - Scp049DamageHandler.AttackType.Instakill => "SCP-049", - Scp049DamageHandler.AttackType.Scp0492 => "SCP-049-2", - _ => "Unknown", - }; - case UniversalDamageHandler universal: - { - DeathTranslation translation = DeathTranslations.TranslationsById[universal.TranslationId]; - - if (_translations.TryGetValue(translation.Id, out string s)) - return s; + DeathTranslation translation = DeathTranslations.TranslationsById[universal.TranslationId]; + + if (_translations.TryGetValue(translation.Id, out string s)) + return s; - break; - } - case FirearmDamageHandler firearm: - return firearm.Firearm.Name; + break; } - - return "Unknown"; + case FirearmDamageHandler firearm: + return firearm.Firearm.Name; } + + return "Unknown"; } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index 173c6de..526150a 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -2,30 +2,29 @@ using LabApi.Features; using DiscordLab.Dependency; -namespace DiscordLab.DeathLogs +namespace DiscordLab.DeathLogs; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.DeathLogs"; - public override string Description { get; } = "Adds death logging capabilities"; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.DeathLogs"; + public override string Description { get; } = "Adds death logging capabilities"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - CallOnLoadAttribute.Load(); - } + CallOnLoadAttribute.Load(); + } - public override void Disable() - { - CallOnUnloadAttribute.Unload(); + public override void Disable() + { + CallOnUnloadAttribute.Unload(); - Instance = null; - } + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index c7e21b5..66b6cd1 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -1,35 +1,34 @@ using System.ComponentModel; using DiscordLab.Bot.API.Features.Embed; -namespace DiscordLab.DeathLogs +namespace DiscordLab.DeathLogs; + +public class Translation { - public class Translation - { - [Description("The message that will be sent when a player dies.")] - public string PlayerDeath { get; set; } = - "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}`. They died from: `{cause}`"; + [Description("The message that will be sent when a player dies.")] + public string PlayerDeath { get; set; } = + "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}`. They died from: `{cause}`"; - [Description("The message that will be sent when a cuffed player dies, unless the cuffed channel is disabled.")] - public string CuffedPlayerDeath { get; set; } = - "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}` while cuffed. They died from: `{cause}`"; + [Description("The message that will be sent when a cuffed player dies, unless the cuffed channel is disabled.")] + public string CuffedPlayerDeath { get; set; } = + "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}` while cuffed. They died from: `{cause}`"; - [Description( - "The message that will be sent when a player dies by their own actions, or just they died because of something else.")] - public string PlayerDeathSelf { get; set; } = "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; + [Description( + "The message that will be sent when a player dies by their own actions, or just they died because of something else.")] + public string PlayerDeathSelf { get; set; } = "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; - [Description("The message that will be sent when a player dies due to someone on their own team.")] - public string TeamKill { get; set; } = - "`{target}` has been team-killed by `{player}`, they were both {role}. They died from: `{cause}`"; + [Description("The message that will be sent when a player dies due to someone on their own team.")] + public string TeamKill { get; set; } = + "`{target}` has been team-killed by `{player}`, they were both {role}. They died from: `{cause}`"; - [Description("The embed for when sending damage logs. Entries will be replaced with the entries below.")] - public EmbedBuilder DamageLogEmbed { get; set; } = new() - { - Title = "Damage Logs", - Description = "{entries}", - Color = Discord.Color.Blue.ToString() - }; + [Description("The embed for when sending damage logs. Entries will be replaced with the entries below.")] + public EmbedBuilder DamageLogEmbed { get; set; } = new() + { + Title = "Damage Logs", + Description = "{entries}", + Color = Discord.Color.Blue.ToString() + }; - [Description("What each instance of damage will look like in the logs.")] - public string DamageLogEntry { get; set; } = "{timetlong} | `{player}` did `{damage}` damage to `{target}` | Cause: `{cause}`"; - } + [Description("What each instance of damage will look like in the logs.")] + public string DamageLogEntry { get; set; } = "{timetlong} | `{player}` did `{damage}` damage to `{target}` | Cause: `{cause}`"; } \ No newline at end of file diff --git a/DiscordLab.Dependency/Plugin.cs b/DiscordLab.Dependency/Plugin.cs index 1329bce..6151fd9 100644 --- a/DiscordLab.Dependency/Plugin.cs +++ b/DiscordLab.Dependency/Plugin.cs @@ -1,35 +1,34 @@ using LabApi.Loader; -namespace DiscordLab.Dependency +namespace DiscordLab.Dependency; + +/// +/// Allows users to easily make plugins with translations also, not just configs. +/// +/// Your config. +/// Your translation. +public abstract class Plugin : LabApi.Loader.Features.Plugins.Plugin + where TConfig : class, new() + where TTranslation : class, new() { +#pragma warning disable SA1401 // FieldsMustBePrivate /// - /// Allows users to easily make plugins with translations also, not just configs. + /// Gets the plugin's config. /// - /// Your config. - /// Your translation. - public abstract class Plugin : LabApi.Loader.Features.Plugins.Plugin - where TConfig : class, new() - where TTranslation : class, new() - { -#pragma warning disable SA1401 // FieldsMustBePrivate - /// - /// Gets the plugin's config. - /// - public TConfig Config; + public TConfig Config; - /// - /// Gets the plugin's translation. - /// - public TTranslation Translation; + /// + /// Gets the plugin's translation. + /// + public TTranslation Translation; #pragma warning restore SA1401 // FieldsMustBePrivate - /// - public override void LoadConfigs() - { - this.TryLoadConfig("config.yml", out Config); - this.TryLoadConfig("translation.yml", out Translation); + /// + public override void LoadConfigs() + { + this.TryLoadConfig("config.yml", out Config); + this.TryLoadConfig("translation.yml", out Translation); - base.LoadConfigs(); - } + base.LoadConfigs(); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index b522717..e203ad0 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -4,81 +4,80 @@ using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; -namespace DiscordLab.Moderation.Commands +namespace DiscordLab.Moderation.Commands; + +public class Ban : AutocompleteCommand { - public class Ban : AutocompleteCommand - { - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override SlashCommandBuilder Data { get; } = new() - { - Name = Translation.BanCommandName, - Description = Translation.BanCommandDescription, - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = Translation.BanUserOptionName, - Description = Translation.BanUserOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - }, - new() - { - Name = Translation.BanDurationOptionName, - Description = Translation.BanDurationOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - }, - new() - { - Name = Translation.BanReasonOptionName, - Description = Translation.BanReasonOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - } - ] - }; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.BanCommandName, + Description = Translation.BanCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.BanUserOptionName, + Description = Translation.BanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.BanDurationOptionName, + Description = Translation.BanDurationOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.BanReasonOptionName, + Description = Translation.BanReasonOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + } + ] + }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(); + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); - string userId = (string)command.Data.Options.ElementAt(0).Value; - long duration = Misc.RelativeTimeToSeconds((string)command.Data.Options.ElementAt(1).Value, 60); - string reason = (string)command.Data.Options.ElementAt(2).Value; + string userId = (string)command.Data.Options.ElementAt(0).Value; + long duration = Misc.RelativeTimeToSeconds((string)command.Data.Options.ElementAt(1).Value, 60); + string reason = (string)command.Data.Options.ElementAt(2).Value; - TranslationBuilder successBuilder = new(Translation.BanSuccess) - { - Time = TempMuteManager.GetExpireDate(duration) - }; - TranslationBuilder failBuilder = new(Translation.BanFailure); + TranslationBuilder successBuilder = new(Translation.BanSuccess) + { + Time = TempMuteManager.GetExpireDate(duration) + }; + TranslationBuilder failBuilder = new(Translation.BanFailure); - successBuilder.CustomReplacers.Add("userid", () => userId); - failBuilder.CustomReplacers.Add("userid", () => userId); + successBuilder.CustomReplacers.Add("userid", () => userId); + failBuilder.CustomReplacers.Add("userid", () => userId); - if (!CommandUtils.TryGetPlayerFromUnparsed(userId, out Player player)) - { - bool result = userId.Contains("@") ? - Server.BanUserId(userId, reason, duration) : - Server.BanIpAddress(userId, reason, duration); - - await command.ModifyOriginalResponseAsync(m => - m.Content = !result ? failBuilder : successBuilder); - - return; - } + if (!CommandUtils.TryGetPlayerFromUnparsed(userId, out Player player)) + { + bool result = userId.Contains("@") ? + Server.BanUserId(userId, reason, duration) : + Server.BanIpAddress(userId, reason, duration); await command.ModifyOriginalResponseAsync(m => - m.Content = Server.BanPlayer(player, reason, duration) ? successBuilder : failBuilder); + m.Content = !result ? failBuilder : successBuilder); + + return; } - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); - } + await command.ModifyOriginalResponseAsync(m => + m.Content = Server.BanPlayer(player, reason, duration) ? successBuilder : failBuilder); + } + + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index 6f6c762..e559f10 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -5,77 +5,76 @@ using LabApi.Features.Wrappers; using VoiceChat; -namespace DiscordLab.Moderation.Commands +namespace DiscordLab.Moderation.Commands; + +public class Mute : AutocompleteCommand { - public class Mute : AutocompleteCommand - { - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override SlashCommandBuilder Data { get; } = new() - { - Name = Translation.MuteCommandName, - Description = Translation.MuteCommandDescription, - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = Translation.MuteUserOptionName, - Description = Translation.MuteUserOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - }, - new() - { - Name = Translation.MuteDurationOptionName, - Description = Translation.MuteDurationOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = false - } - ] - }; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.MuteCommandName, + Description = Translation.MuteCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.MuteUserOptionName, + Description = Translation.MuteUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + new() + { + Name = Translation.MuteDurationOptionName, + Description = Translation.MuteDurationOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = false + } + ] + }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(); + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); - if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) - { - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); - return; - } + if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) + { + await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); + return; + } - TranslationBuilder builder; + TranslationBuilder builder; - if (command.Data.Options.Count == 2) - { - string duration = (string)command.Data.Options.ElementAt(1).Value; - DateTime time = TempMuteManager.GetExpireDate(duration); - TempMuteManager.MutePlayer(player, time); + if (command.Data.Options.Count == 2) + { + string duration = (string)command.Data.Options.ElementAt(1).Value; + DateTime time = TempMuteManager.GetExpireDate(duration); + TempMuteManager.MutePlayer(player, time); - builder = new(Translation.TempMuteSuccess, "player", player) - { - Time = time - }; + builder = new(Translation.TempMuteSuccess, "player", player) + { + Time = time + }; - builder.CustomReplacers.Add("duration", () => duration); + builder.CustomReplacers.Add("duration", () => duration); - await command.ModifyOriginalResponseAsync(m => m.Content = builder); - return; - } + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + return; + } - VoiceChatMutes.IssueLocalMute(player.UserId); + VoiceChatMutes.IssueLocalMute(player.UserId); - builder = new(Translation.PermMuteSuccess, "player", player); + builder = new(Translation.PermMuteSuccess, "player", player); - await command.ModifyOriginalResponseAsync(m => m.Content = builder); - } + await command.ModifyOriginalResponseAsync(m => m.Content = builder); + } - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); - } + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs index b33950c..0d0ed50 100644 --- a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -3,58 +3,57 @@ using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; -namespace DiscordLab.Moderation.Commands +namespace DiscordLab.Moderation.Commands; + +public class TempMuteRemoteAdmin : ICommand, IUsageProvider { - public class TempMuteRemoteAdmin : ICommand, IUsageProvider - { - public string Command { get; } = "tempmute"; - public string[] Aliases { get; } = ["tempm", "mutet", "temporarymute", "mutetemp", "mutetemporary"]; - public string Description { get; } = "Temporarily mutes a user."; + public string Command { get; } = "tempmute"; + public string[] Aliases { get; } = ["tempm", "mutet", "temporarymute", "mutetemp", "mutetemporary"]; + public string Description { get; } = "Temporarily mutes a user."; - public string[] Usage { get; } = - [ - "player", - "duration" - ]; + public string[] Usage { get; } = + [ + "player", + "duration" + ]; - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (!sender.CheckPermission([ - PlayerPermissions.BanningUpToDay, - PlayerPermissions.LongTermBanning, - PlayerPermissions.PlayersManagement - ], out response)) - return false; + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission([ + PlayerPermissions.BanningUpToDay, + PlayerPermissions.LongTermBanning, + PlayerPermissions.PlayersManagement + ], out response)) + return false; - if (arguments.Count < 2) - { - response = "To execute this command provide at least 2 arguments!\nUsage: " + this.DisplayCommandUsage(); - return false; - } + if (arguments.Count < 2) + { + response = "To execute this command provide at least 2 arguments!\nUsage: " + this.DisplayCommandUsage(); + return false; + } - if (!Player.TryGet(sender, out Player player)) - { - player = Server.Host; - } + if (!Player.TryGet(sender, out Player player)) + { + player = Server.Host; + } - if (!CommandUtils.TryGetPlayerFromUnparsed(arguments.At(0), out Player target)) - { - response = Plugin.Instance.Translation.InvalidUser; - } + if (!CommandUtils.TryGetPlayerFromUnparsed(arguments.At(0), out Player target)) + { + response = Plugin.Instance.Translation.InvalidUser; + } - DateTime time = TempMuteManager.GetExpireDate(arguments.At(1)); + DateTime time = TempMuteManager.GetExpireDate(arguments.At(1)); - TempMuteManager.MutePlayer(target, time, player); + TempMuteManager.MutePlayer(target, time, player); - TranslationBuilder builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", target) - { - Time = time - }; + TranslationBuilder builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", target) + { + Time = time + }; - builder.CustomReplacers.Add("duration", () => arguments.At(1)); + builder.CustomReplacers.Add("duration", () => arguments.At(1)); - response = builder; - return true; - } + response = builder; + return true; } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index 3807f91..25b9b6c 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -2,56 +2,55 @@ using Discord.WebSocket; using DiscordLab.Bot.API.Features; -namespace DiscordLab.Moderation.Commands +namespace DiscordLab.Moderation.Commands; + +public class Unban : AutocompleteCommand { - public class Unban : AutocompleteCommand - { - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override SlashCommandBuilder Data { get; } = new() - { - Name = Translation.UnbanCommandName, - Description = Translation.UnbanCommandDescription, - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = Translation.UnbanUserOptionName, - Description = Translation.UnbanUserOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - }, - ] - }; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.UnbanCommandName, + Description = Translation.UnbanCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.UnbanUserOptionName, + Description = Translation.UnbanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + ] + }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(); + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); - string id = (string)command.Data.Options.First().Value; + string id = (string)command.Data.Options.First().Value; - BanHandler.RemoveBan(id, id.Contains("@") ? BanHandler.BanType.UserId : BanHandler.BanType.IP); + BanHandler.RemoveBan(id, id.Contains("@") ? BanHandler.BanType.UserId : BanHandler.BanType.IP); - TranslationBuilder builder = new(Translation.UnbanSuccess); + TranslationBuilder builder = new(Translation.UnbanSuccess); - builder.CustomReplacers.Add("userid", () => id); + builder.CustomReplacers.Add("userid", () => id); - await command.ModifyOriginalResponseAsync(m => - m.Content = - builder); - } + await command.ModifyOriginalResponseAsync(m => + m.Content = + builder); + } - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - IEnumerable response = - [ - ..BanHandler.GetBans(BanHandler.BanType.UserId), - ..BanHandler.GetBans(BanHandler.BanType.IP) - ]; - await autocomplete.RespondAsync(response.Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); - } + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + IEnumerable response = + [ + ..BanHandler.GetBans(BanHandler.BanType.UserId), + ..BanHandler.GetBans(BanHandler.BanType.IP) + ]; + await autocomplete.RespondAsync(response.Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs index a532456..d3df885 100644 --- a/DiscordLab.Moderation/Commands/Unmute.cs +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -4,51 +4,50 @@ using DiscordLab.Bot.API.Utilities; using LabApi.Features.Wrappers; -namespace DiscordLab.Moderation.Commands +namespace DiscordLab.Moderation.Commands; + +public class Unmute : AutocompleteCommand { - public class Unmute : AutocompleteCommand - { - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override SlashCommandBuilder Data { get; } = new() - { - Name = Translation.UnmuteCommandName, - Description = Translation.UnmuteCommandDescription, - DefaultMemberPermissions = GuildPermission.ModerateMembers, - Options = - [ - new() - { - Name = Translation.UnbanUserOptionName, - Description = Translation.UnbanUserOptionDescription, - Type = ApplicationCommandOptionType.String, - IsRequired = true - }, - ] - }; + public override SlashCommandBuilder Data { get; } = new() + { + Name = Translation.UnmuteCommandName, + Description = Translation.UnmuteCommandDescription, + DefaultMemberPermissions = GuildPermission.ModerateMembers, + Options = + [ + new() + { + Name = Translation.UnbanUserOptionName, + Description = Translation.UnbanUserOptionDescription, + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + ] + }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.DeferAsync(); + public override async Task Run(SocketSlashCommand command) + { + await command.DeferAsync(); - if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) - { - await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); - return; - } + if (!CommandUtils.TryGetPlayerFromUnparsed((string)command.Data.Options.First().Value, out Player player)) + { + await command.ModifyOriginalResponseAsync(m => m.Content = Translation.InvalidUser); + return; + } - TempMuteManager.RemoveMute(player); + TempMuteManager.RemoveMute(player); - await command.ModifyOriginalResponseAsync(m => - m.Content = - new TranslationBuilder(Translation.UnmuteSuccess, "player", player)); - } + await command.ModifyOriginalResponseAsync(m => + m.Content = + new TranslationBuilder(Translation.UnmuteSuccess, "player", player)); + } - public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) - { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); - } + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) + { + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Config.cs b/DiscordLab.Moderation/Config.cs index 197d98e..fb7059e 100644 --- a/DiscordLab.Moderation/Config.cs +++ b/DiscordLab.Moderation/Config.cs @@ -1,23 +1,22 @@ using System.ComponentModel; -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public class Config { - public class Config - { - public ulong GuildId { get; set; } = 0; + public ulong GuildId { get; set; } = 0; - public ulong MuteLogChannelId { get; set; } = 0; + public ulong MuteLogChannelId { get; set; } = 0; - public ulong UnmuteLogChannelId { get; set; } = 0; + public ulong UnmuteLogChannelId { get; set; } = 0; - public ulong BanLogChannelId { get; set; } = 0; + public ulong BanLogChannelId { get; set; } = 0; - public ulong UnbanLogChannelId { get; set; } = 0; + public ulong UnbanLogChannelId { get; set; } = 0; - [Description("Whether to add the Discord slash commands.")] - public bool AddCommands { get; set; } = true; + [Description("Whether to add the Discord slash commands.")] + public bool AddCommands { get; set; } = true; - [Description("Whether to enable the temp mute remote admin command.")] - public bool AddTempMuteCommand { get; set; } = true; - } + [Description("Whether to enable the temp mute remote admin command.")] + public bool AddTempMuteCommand { get; set; } = true; } \ No newline at end of file diff --git a/DiscordLab.Moderation/Events.cs b/DiscordLab.Moderation/Events.cs index 9b54b21..6ad355f 100644 --- a/DiscordLab.Moderation/Events.cs +++ b/DiscordLab.Moderation/Events.cs @@ -10,110 +10,109 @@ using LabApi.Features.Console; using LabApi.Features.Wrappers; -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public class Events : CustomEventsHandler { - public class Events : CustomEventsHandler - { - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override void OnPlayerUnmuting(PlayerUnmutingEventArgs ev) - { - // otherwise OnPlayerUnmuted will get triggered twice. - ev.IsAllowed = false; + public override void OnPlayerUnmuting(PlayerUnmutingEventArgs ev) + { + // otherwise OnPlayerUnmuted will get triggered twice. + ev.IsAllowed = false; - TempMuteManager.RemoveMute(ev.Player, ev.Issuer); - } + TempMuteManager.RemoveMute(ev.Player, ev.Issuer); + } - public override void OnPlayerUnmuted(PlayerUnmutedEventArgs ev) - { - if (Config.UnmuteLogChannelId == 0) - return; + public override void OnPlayerUnmuted(PlayerUnmutedEventArgs ev) + { + if (Config.UnmuteLogChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.UnmuteLogChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unmute logs", Config.UnmuteLogChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.UnmuteLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unmute logs", Config.UnmuteLogChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new TranslationBuilder(Translation.UnmuteLog) - .AddPlayer("target", ev.Player) - .AddPlayer("player", ev.Issuer); + TranslationBuilder builder = new TranslationBuilder(Translation.UnmuteLog) + .AddPlayer("target", ev.Player) + .AddPlayer("player", ev.Issuer); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnPlayerMuted(PlayerMutedEventArgs ev) - { - if (Config.MuteLogChannelId == 0) - return; + public override void OnPlayerMuted(PlayerMutedEventArgs ev) + { + if (Config.MuteLogChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.MuteLogChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("mute logs", Config.MuteLogChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.MuteLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("mute logs", Config.MuteLogChannelId, Config.GuildId)); + return; + } - string translation = Translation.PermMuteLog; + string translation = Translation.PermMuteLog; - if (TempMuteManager.MuteConfig.Mutes.TryGetValue(ev.Player.UserId, out DateTime time)) - { - translation = Translation.TempMuteLog; - } + if (TempMuteManager.MuteConfig.Mutes.TryGetValue(ev.Player.UserId, out DateTime time)) + { + translation = Translation.TempMuteLog; + } - TranslationBuilder builder = new TranslationBuilder(translation) - { - Time = time - }.AddPlayer("player", ev.Issuer).AddPlayer("target", ev.Player); + TranslationBuilder builder = new TranslationBuilder(translation) + { + Time = time + }.AddPlayer("player", ev.Issuer).AddPlayer("target", ev.Player); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnPlayerBanned(PlayerBannedEventArgs ev) - { - if (Config.BanLogChannelId == 0) - return; + public override void OnPlayerBanned(PlayerBannedEventArgs ev) + { + if (Config.BanLogChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.BanLogChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("ban logs", Config.BanLogChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.BanLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("ban logs", Config.BanLogChannelId, Config.GuildId)); + return; + } - EmbedBuilder builder = Translation.BanLogEmbed; - - foreach (EmbedFieldBuilder field in builder.Fields) - { - TranslationBuilder tBuilder = new((string)field.Value, "player", ev.Issuer); - tBuilder.CustomReplacers.Add("userid", () => ev.PlayerId); - field.Value = tBuilder.Build(); - } + EmbedBuilder builder = Translation.BanLogEmbed; - channel.SendMessage(embed:builder.Build()); + foreach (EmbedFieldBuilder field in builder.Fields) + { + TranslationBuilder tBuilder = new((string)field.Value, "player", ev.Issuer); + tBuilder.CustomReplacers.Add("userid", () => ev.PlayerId); + field.Value = tBuilder.Build(); } + + channel.SendMessage(embed:builder.Build()); + } - public override void OnServerBanRevoked(BanRevokedEventArgs ev) - { - if (Config.UnbanLogChannelId == 0) - return; + public override void OnServerBanRevoked(BanRevokedEventArgs ev) + { + if (Config.UnbanLogChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.UnbanLogChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unban logs", Config.UnbanLogChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.UnbanLogChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unban logs", Config.UnbanLogChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new(Translation.UnbanLog); + TranslationBuilder builder = new(Translation.UnbanLog); - builder.CustomReplacers.Add("userid", () => ev.BanDetails.Id); - builder.CustomReplacers.Add("username", () => ev.BanDetails.OriginalName); - builder.CustomReplacers.Add("playerid", () => ev.BanDetails.Issuer); + builder.CustomReplacers.Add("userid", () => ev.BanDetails.Id); + builder.CustomReplacers.Add("username", () => ev.BanDetails.OriginalName); + builder.CustomReplacers.Add("playerid", () => ev.BanDetails.Issuer); - if (Player.TryGet(ev.BanDetails.Issuer, out Player player)) - builder.AddPlayer("player", player); + if (Player.TryGet(ev.BanDetails.Issuer, out Player player)) + builder.AddPlayer("player", player); - channel.SendMessage(builder); - } + channel.SendMessage(builder); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index 2e4ad8a..4e30c6d 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -9,56 +9,55 @@ using LabApi.Loader; using RemoteAdmin; -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.Moderation"; - public override string Description { get; } = "Adds logging and commands for moderation based operations"; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.Moderation"; + public override string Description { get; } = "Adds logging and commands for moderation based operations"; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public TempMuteConfig MuteConfig; + public TempMuteConfig MuteConfig; - public Events Events = new(); + public Events Events = new(); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - CallOnLoadAttribute.Load(); + CallOnLoadAttribute.Load(); - if (Config.AddCommands) - SlashCommand.FindAll(); + if (Config.AddCommands) + SlashCommand.FindAll(); - if (Config.AddTempMuteCommand) - CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(new TempMuteRemoteAdmin()); + if (Config.AddTempMuteCommand) + CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(new TempMuteRemoteAdmin()); - CustomHandlersManager.RegisterEventsHandler(Events); - } + CustomHandlersManager.RegisterEventsHandler(Events); + } - public override void Disable() - { - CustomHandlersManager.UnregisterEventsHandler(Events); + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); - CallOnUnloadAttribute.Unload(); + CallOnUnloadAttribute.Unload(); - Events = null; + Events = null; - Instance = null; - } + Instance = null; + } - public override void LoadConfigs() - { - this.TryLoadConfig("mute-config.yml", out MuteConfig); + public override void LoadConfigs() + { + this.TryLoadConfig("mute-config.yml", out MuteConfig); - base.LoadConfigs(); - } - - public static IEnumerable PlayersAutocompleteResults => - Player.ReadyList.Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); + base.LoadConfigs(); } + + public static IEnumerable PlayersAutocompleteResults => + Player.ReadyList.Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); } \ No newline at end of file diff --git a/DiscordLab.Moderation/TempMuteConfig.cs b/DiscordLab.Moderation/TempMuteConfig.cs index 37fb7a0..0fec083 100644 --- a/DiscordLab.Moderation/TempMuteConfig.cs +++ b/DiscordLab.Moderation/TempMuteConfig.cs @@ -1,7 +1,6 @@ -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public class TempMuteConfig { - public class TempMuteConfig - { - public Dictionary Mutes { get; set; } = new(); - } + public Dictionary Mutes { get; set; } = new(); } \ No newline at end of file diff --git a/DiscordLab.Moderation/TempMuteManager.cs b/DiscordLab.Moderation/TempMuteManager.cs index 40fcd20..80106b6 100644 --- a/DiscordLab.Moderation/TempMuteManager.cs +++ b/DiscordLab.Moderation/TempMuteManager.cs @@ -6,96 +6,95 @@ using MEC; using VoiceChat; -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public static class TempMuteManager { - public static class TempMuteManager - { - public static TempMuteConfig MuteConfig => Plugin.Instance.MuteConfig; + public static TempMuteConfig MuteConfig => Plugin.Instance.MuteConfig; - public static Dictionary Handles { get; private set; } = new(); + public static Dictionary Handles { get; private set; } = new(); - [CallOnLoad] - public static void Start() + [CallOnLoad] + public static void Start() + { + Dictionary mutes = MuteConfig.Mutes; + foreach (KeyValuePair dict in mutes) { - Dictionary mutes = MuteConfig.Mutes; - foreach (KeyValuePair dict in mutes) + TimeSpan time = dict.Value - DateTime.Now; + if (time.TotalSeconds < 0) { - TimeSpan time = dict.Value - DateTime.Now; - if (time.TotalSeconds < 0) - { - RemoveMute(dict.Key); - continue; - } - AddHandle(dict.Key, time); + RemoveMute(dict.Key); + continue; } + AddHandle(dict.Key, time); } + } - [CallOnUnload] - public static void Stop() + [CallOnUnload] + public static void Stop() + { + foreach (KeyValuePair mutes in Handles) { - foreach (KeyValuePair mutes in Handles) - { - Timing.KillCoroutines(mutes.Key); - } - - Handles = null; + Timing.KillCoroutines(mutes.Key); } - public static void AddHandle(string userId, DateTime time) => - AddHandle(userId, time - DateTime.Now); + Handles = null; + } + + public static void AddHandle(string userId, DateTime time) => + AddHandle(userId, time - DateTime.Now); - public static void AddHandle(string userId, TimeSpan time) => - Handles.Add(userId, - Timing.CallDelayed((float)time.TotalSeconds, () => RemoveMute(userId))); + public static void AddHandle(string userId, TimeSpan time) => + Handles.Add(userId, + Timing.CallDelayed((float)time.TotalSeconds, () => RemoveMute(userId))); - public static void RemoveHandle(string userId) - { - if (!Handles.TryGetValue(userId, out CoroutineHandle handle)) - return; + public static void RemoveHandle(string userId) + { + if (!Handles.TryGetValue(userId, out CoroutineHandle handle)) + return; - Timing.KillCoroutines(handle); - Handles.Remove(userId); - } + Timing.KillCoroutines(handle); + Handles.Remove(userId); + } - public static DateTime GetExpireDate(string duration) => - GetExpireDate(Misc.RelativeTimeToSeconds(duration, 60)); + public static DateTime GetExpireDate(string duration) => + GetExpireDate(Misc.RelativeTimeToSeconds(duration, 60)); - public static DateTime GetExpireDate(long duration) => DateTime.Now.AddSeconds(duration); + public static DateTime GetExpireDate(long duration) => DateTime.Now.AddSeconds(duration); - public static void MutePlayer(Player player, DateTime time, Player sender = null) => - MutePlayer(player.UserId, time, sender?.ReferenceHub); + public static void MutePlayer(Player player, DateTime time, Player sender = null) => + MutePlayer(player.UserId, time, sender?.ReferenceHub); - public static void MutePlayer(string player, DateTime time, ReferenceHub sender = null) - { - sender ??= Server.Host?.ReferenceHub; - VoiceChatMutes.IssueLocalMute(player); - if(Player.TryGet(player, out Player p) && sender) - PlayerEvents.OnMuted(new(p.ReferenceHub, sender, false)); + public static void MutePlayer(string player, DateTime time, ReferenceHub sender = null) + { + sender ??= Server.Host?.ReferenceHub; + VoiceChatMutes.IssueLocalMute(player); + if(Player.TryGet(player, out Player p) && sender) + PlayerEvents.OnMuted(new(p.ReferenceHub, sender, false)); - MuteConfig.Mutes.Add(player, time); - AddHandle(player, time); - Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); - } + MuteConfig.Mutes.Add(player, time); + AddHandle(player, time); + Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); + } - public static void RemoveMute(Player player, Player sender = null) => - RemoveMute(player.UserId, sender?.ReferenceHub); + public static void RemoveMute(Player player, Player sender = null) => + RemoveMute(player.UserId, sender?.ReferenceHub); - public static void RemoveMute(string player, ReferenceHub sender = null) - { - if (!VoiceChatMutes.Mutes.Contains(player)) - return; + public static void RemoveMute(string player, ReferenceHub sender = null) + { + if (!VoiceChatMutes.Mutes.Contains(player)) + return; - sender ??= Server.Host?.ReferenceHub; + sender ??= Server.Host?.ReferenceHub; - VoiceChatMutes.RevokeLocalMute(player); + VoiceChatMutes.RevokeLocalMute(player); - MuteConfig.Mutes.Remove(player); - Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); - if(Player.TryGet(player, out Player p) && sender) - PlayerEvents.OnUnmuted(new(p.ReferenceHub, sender, false)); + MuteConfig.Mutes.Remove(player); + Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); + if(Player.TryGet(player, out Player p) && sender) + PlayerEvents.OnUnmuted(new(p.ReferenceHub, sender, false)); - if (Handles.ContainsKey(player)) - RemoveHandle(player); - } + if (Handles.ContainsKey(player)) + RemoveHandle(player); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index 4bff721..b0398ac 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -1,85 +1,84 @@ using System.ComponentModel; using DiscordLab.Bot.API.Features.Embed; -namespace DiscordLab.Moderation +namespace DiscordLab.Moderation; + +public class Translation { - public class Translation - { - // String properties for command and option names - public string MuteCommandName { get; set; } = "mute"; - public string MuteCommandDescription { get; set; } = "Mute a player on the server"; - public string MuteUserOptionName { get; set; } = "user"; - public string MuteUserOptionDescription { get; set; } = "The user to mute"; - public string MuteDurationOptionName { get; set; } = "duration"; - public string MuteDurationOptionDescription { get; set; } = "The duration to mute the user for"; + // String properties for command and option names + public string MuteCommandName { get; set; } = "mute"; + public string MuteCommandDescription { get; set; } = "Mute a player on the server"; + public string MuteUserOptionName { get; set; } = "user"; + public string MuteUserOptionDescription { get; set; } = "The user to mute"; + public string MuteDurationOptionName { get; set; } = "duration"; + public string MuteDurationOptionDescription { get; set; } = "The duration to mute the user for"; - public string UnmuteCommandName { get; set; } = "unmute"; - public string UnmuteCommandDescription { get; set; } = "Unmute a player on the server"; - public string UnmuteUserOptionName { get; set; } = "user"; - public string UnmuteUserOptionDescription { get; set; } = "The user to unmute"; + public string UnmuteCommandName { get; set; } = "unmute"; + public string UnmuteCommandDescription { get; set; } = "Unmute a player on the server"; + public string UnmuteUserOptionName { get; set; } = "user"; + public string UnmuteUserOptionDescription { get; set; } = "The user to unmute"; - public string BanCommandName { get; set; } = "ban"; - public string BanCommandDescription { get; set; } = "Ban a player on the server"; - public string BanUserOptionName { get; set; } = "user"; - public string BanUserOptionDescription { get; set; } = "The user to ban"; - public string BanDurationOptionName { get; set; } = "duration"; - public string BanDurationOptionDescription { get; set; } = "The duration to ban the user for"; - public string BanReasonOptionName { get; set; } = "reason"; - public string BanReasonOptionDescription { get; set; } = "The reason to ban the user"; + public string BanCommandName { get; set; } = "ban"; + public string BanCommandDescription { get; set; } = "Ban a player on the server"; + public string BanUserOptionName { get; set; } = "user"; + public string BanUserOptionDescription { get; set; } = "The user to ban"; + public string BanDurationOptionName { get; set; } = "duration"; + public string BanDurationOptionDescription { get; set; } = "The duration to ban the user for"; + public string BanReasonOptionName { get; set; } = "reason"; + public string BanReasonOptionDescription { get; set; } = "The reason to ban the user"; - public string UnbanCommandName { get; set; } = "unban"; - public string UnbanCommandDescription { get; set; } = "Unban a player on the server"; - public string UnbanUserOptionName { get; set; } = "user"; - public string UnbanUserOptionDescription { get; set; } = "The user to unban"; + public string UnbanCommandName { get; set; } = "unban"; + public string UnbanCommandDescription { get; set; } = "Unban a player on the server"; + public string UnbanUserOptionName { get; set; } = "user"; + public string UnbanUserOptionDescription { get; set; } = "The user to unban"; - public string InvalidUser { get; set; } = "Please provide a valid user to use this command on."; + public string InvalidUser { get; set; } = "Please provide a valid user to use this command on."; - public string TempMuteSuccess { get; set; } = "Player {player} has been temporarily muted for {duration}. They will get unmuted at {timef}"; + public string TempMuteSuccess { get; set; } = "Player {player} has been temporarily muted for {duration}. They will get unmuted at {timef}"; - public string UnmuteSuccess { get; set; } = "Player {player} has been successfully unmuted."; + public string UnmuteSuccess { get; set; } = "Player {player} has been successfully unmuted."; - public string PermMuteSuccess { get; set; } = "Player {player} has been muted."; + public string PermMuteSuccess { get; set; } = "Player {player} has been muted."; - public string BanFailure { get; set; } = "Failed to ban {userid}. Please make sure the data is valid and try again..."; + public string BanFailure { get; set; } = "Failed to ban {userid}. Please make sure the data is valid and try again..."; - public string BanSuccess { get; set; } = "Successfully banned {userid} for {reason}. They will get unbanned in {timer}"; + public string BanSuccess { get; set; } = "Successfully banned {userid} for {reason}. They will get unbanned in {timer}"; - public string UnbanSuccess { get; set; } = "Player {userid} has been unbanned."; + public string UnbanSuccess { get; set; } = "Player {userid} has been unbanned."; - public string PermMuteLog { get; set; } = "Player {target} has been muted by {player}."; + public string PermMuteLog { get; set; } = "Player {target} has been muted by {player}."; - public string TempMuteLog { get; set; } = - "Player {target} has been muted by {player} for {timef}, they will be unmuted in {timer}"; + public string TempMuteLog { get; set; } = + "Player {target} has been muted by {player} for {timef}, they will be unmuted in {timer}"; - public string UnmuteLog { get; set; } = "Player {target} has been unmuted by {player}."; + public string UnmuteLog { get; set; } = "Player {target} has been unmuted by {player}."; - [Description( - "Every field value accepts placeholders, even if you add more. player in this case is the issuer.")] - public EmbedBuilder BanLogEmbed { get; set; } = new() - { - Title = "Ban Log", - Description = "A user has been banned", - Fields = - [ - new() - { - Name = "Player", - Value = "{userid}" - }, - new() - { - Name = "Issuer", - Value = "{player}" - }, - new() - { - Name = "Duration", - Value = "{timer} ({timef})" - } - ] - }; + [Description( + "Every field value accepts placeholders, even if you add more. player in this case is the issuer.")] + public EmbedBuilder BanLogEmbed { get; set; } = new() + { + Title = "Ban Log", + Description = "A user has been banned", + Fields = + [ + new() + { + Name = "Player", + Value = "{userid}" + }, + new() + { + Name = "Issuer", + Value = "{player}" + }, + new() + { + Name = "Duration", + Value = "{timer} ({timef})" + } + ] + }; - [Description("Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] - public string UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; - } + [Description("Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] + public string UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Config.cs b/DiscordLab.RoundLogs/Config.cs index d2b6f53..1fc5ccd 100644 --- a/DiscordLab.RoundLogs/Config.cs +++ b/DiscordLab.RoundLogs/Config.cs @@ -1,37 +1,36 @@ using System.ComponentModel; -namespace DiscordLab.RoundLogs +namespace DiscordLab.RoundLogs; + +public class Config { - public class Config - { - [Description("The guild ID, set to 0 for default guild.")] - public ulong GuildId { get; set; } = 0; + [Description("The guild ID, set to 0 for default guild.")] + public ulong GuildId { get; set; } = 0; - [Description("The channel to log to when someone's role changes.")] - public ulong RoleChangeChannelId { get; set; } = 0; + [Description("The channel to log to when someone's role changes.")] + public ulong RoleChangeChannelId { get; set; } = 0; - [Description("The channel to log to when someone swaps from an SCP to another.")] - public ulong ScpSwapChannelId { get; set; } = 0; + [Description("The channel to log to when someone swaps from an SCP to another.")] + public ulong ScpSwapChannelId { get; set; } = 0; - [Description("The channel to log to when NTF spawns.")] - public ulong NtfSpawnChannelId { get; set; } = 0; + [Description("The channel to log to when NTF spawns.")] + public ulong NtfSpawnChannelId { get; set; } = 0; - [Description("The channel to log to when Chaos spawns.")] - public ulong ChaosSpawnChannelId { get; set; } = 0; + [Description("The channel to log to when Chaos spawns.")] + public ulong ChaosSpawnChannelId { get; set; } = 0; - [Description("The channel to log to when someone gets cuffed.")] - public ulong CuffedChannelId { get; set; } = 0; + [Description("The channel to log to when someone gets cuffed.")] + public ulong CuffedChannelId { get; set; } = 0; - [Description("The channel to log to when someone gets uncuffed.")] - public ulong UncuffedChannelId { get; set; } = 0; + [Description("The channel to log to when someone gets uncuffed.")] + public ulong UncuffedChannelId { get; set; } = 0; - [Description("The channel to log to when the round starts.")] - public ulong RoundStartedChannelId { get; set; } = 0; + [Description("The channel to log to when the round starts.")] + public ulong RoundStartedChannelId { get; set; } = 0; - [Description("The channel to log to when the round ends.")] - public ulong RoundEndedChannelId { get; set; } = 0; + [Description("The channel to log to when the round ends.")] + public ulong RoundEndedChannelId { get; set; } = 0; - [Description("The channel to log to when decontamination starts.")] - public ulong DecontaminationChannelId { get; set; } = 0; - } + [Description("The channel to log to when decontamination starts.")] + public ulong DecontaminationChannelId { get; set; } = 0; } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs index 8d24c9e..84af393 100644 --- a/DiscordLab.RoundLogs/Events.cs +++ b/DiscordLab.RoundLogs/Events.cs @@ -12,176 +12,175 @@ using LabApi.Features.Wrappers; using Respawning.Waves; -namespace DiscordLab.RoundLogs +namespace DiscordLab.RoundLogs; + +public class Events : CustomEventsHandler { - public class Events : CustomEventsHandler - { - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) - { - if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart - or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died or RoleChangeReason.Destroyed) - return; + public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) + { + if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart + or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died or RoleChangeReason.Destroyed) + return; - Dictionary> customReplacers = new() - { - ["oldrole"] = () => ev.OldRole.GetFullName(), - ["newrole"] = () => ev.NewRole.RoleName, - ["reason"] = () => ev.ChangeReason.ToString(), - ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) - }; + Dictionary> customReplacers = new() + { + ["oldrole"] = () => ev.OldRole.GetFullName(), + ["newrole"] = () => ev.NewRole.RoleName, + ["reason"] = () => ev.ChangeReason.ToString(), + ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) + }; - SocketTextChannel channel; + SocketTextChannel channel; - TranslationBuilder builder; + TranslationBuilder builder; - if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) - { - if (Config.ScpSwapChannelId == 0) - return; - channel = Client.GetOrAddChannel(Config.ScpSwapChannelId); - if (channel == null) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("SCP Swap logs", Config.ScpSwapChannelId, Config.GuildId)); - return; - } - - builder = new(Translation.ScpSwapLog, "player", ev.Player) - { - CustomReplacers = customReplacers - }; - - channel.SendMessage(builder); - return; - } - - if (Config.RoleChangeChannelId == 0) + if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) + { + if (Config.ScpSwapChannelId == 0) return; - - if (!Client.TryGetOrAddChannel(Config.RoleChangeChannelId, out channel)) + channel = Client.GetOrAddChannel(Config.ScpSwapChannelId); + if (channel == null) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("SCP Swap logs", Config.ScpSwapChannelId, Config.GuildId)); + return; } - builder = new(Translation.RoleChangeLog, "player", ev.Player) + builder = new(Translation.ScpSwapLog, "player", ev.Player) { CustomReplacers = customReplacers }; - + channel.SendMessage(builder); + return; } + + if (Config.RoleChangeChannelId == 0) + return; - public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) + if (!Client.TryGetOrAddChannel(Config.RoleChangeChannelId, out channel)) { - bool isFoundation = ev.Wave is MtfWave or MiniMtfWave; + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, Config.GuildId)); + } - if ((isFoundation && Config.NtfSpawnChannelId == 0) || (!isFoundation && Config.ChaosSpawnChannelId == 0)) - return; + builder = new(Translation.RoleChangeLog, "player", ev.Player) + { + CustomReplacers = customReplacers + }; + + channel.SendMessage(builder); + } - ulong channelId = isFoundation ? Config.NtfSpawnChannelId : Config.ChaosSpawnChannelId; + public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) + { + bool isFoundation = ev.Wave is MtfWave or MiniMtfWave; - if (!Client.TryGetOrAddChannel(channelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Wave respawn logs", channelId, Config.GuildId)); - return; - } + if ((isFoundation && Config.NtfSpawnChannelId == 0) || (!isFoundation && Config.ChaosSpawnChannelId == 0)) + return; - TranslationBuilder builder = new(isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn) - { - PlayerListItem = Translation.PlayerListItem, - PlayerList = ev.Players - }; - - channel.SendMessage(builder); + ulong channelId = isFoundation ? Config.NtfSpawnChannelId : Config.ChaosSpawnChannelId; + + if (!Client.TryGetOrAddChannel(channelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Wave respawn logs", channelId, Config.GuildId)); + return; } - public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) + TranslationBuilder builder = new(isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn) { - if (Config.CuffedChannelId == 0) - return; + PlayerListItem = Translation.PlayerListItem, + PlayerList = ev.Players + }; + + channel.SendMessage(builder); + } - if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuffed logs", Config.CuffedChannelId, Config.GuildId)); - return; - } + public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) + { + if (Config.CuffedChannelId == 0) + return; - TranslationBuilder builder = new(Translation.Cuffed); + if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuffed logs", Config.CuffedChannelId, Config.GuildId)); + return; + } - builder.AddPlayer("target", ev.Target); - builder.AddPlayer("player", ev.Player); + TranslationBuilder builder = new(Translation.Cuffed); + + builder.AddPlayer("target", ev.Target); + builder.AddPlayer("player", ev.Player); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) - { - if (Config.UncuffedChannelId == 0) - return; + public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) + { + if (Config.UncuffedChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.UncuffedChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("uncuff logs", Config.CuffedChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.UncuffedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("uncuff logs", Config.CuffedChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new(Translation.Uncuffed); + TranslationBuilder builder = new(Translation.Uncuffed); - builder.AddPlayer("target", ev.Target); - builder.AddPlayer("player", ev.Player); + builder.AddPlayer("target", ev.Target); + builder.AddPlayer("player", ev.Player); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnServerRoundStarted() - { - if (Config.RoundStartedChannelId == 0) - return; + public override void OnServerRoundStarted() + { + if (Config.RoundStartedChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.RoundStartedChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.RoundStartedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new(Translation.RoundStart); + TranslationBuilder builder = new(Translation.RoundStart); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnServerRoundEnded(RoundEndedEventArgs ev) - { - if (Config.RoundEndedChannelId == 0) - return; + public override void OnServerRoundEnded(RoundEndedEventArgs ev) + { + if (Config.RoundEndedChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.RoundEndedChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round ended logs", Config.CuffedChannelId, Config.GuildId)); - return; - } + if (!Client.TryGetOrAddChannel(Config.RoundEndedChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round ended logs", Config.CuffedChannelId, Config.GuildId)); + return; + } - TranslationBuilder builder = new(Translation.RoundEnd); + TranslationBuilder builder = new(Translation.RoundEnd); - builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); - channel.SendMessage(builder); - } + channel.SendMessage(builder); + } - public override void OnServerLczDecontaminationStarted() - { - if (Config.DecontaminationChannelId == 0) - return; + public override void OnServerLczDecontaminationStarted() + { + if (Config.DecontaminationChannelId == 0) + return; - if (!Client.TryGetOrAddChannel(Config.DecontaminationChannelId, out SocketTextChannel channel)) - { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("decontamination logs", Config.DecontaminationChannelId, Config.GuildId)); - return; - } - - channel.SendMessage(new TranslationBuilder(Translation.Decontamination)); + if (!Client.TryGetOrAddChannel(Config.DecontaminationChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("decontamination logs", Config.DecontaminationChannelId, Config.GuildId)); + return; } + + channel.SendMessage(new TranslationBuilder(Translation.Decontamination)); } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index f1c8e94..11639f8 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -4,34 +4,33 @@ using LabApi.Events.CustomHandlers; using LabApi.Features; -namespace DiscordLab.RoundLogs +namespace DiscordLab.RoundLogs; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.RoundLogs"; - public override string Description { get; } = "Allows you to log specific details about the round."; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.RoundLogs"; + public override string Description { get; } = "Allows you to log specific details about the round."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public Events Events = new(); + public Events Events = new(); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - CustomHandlersManager.RegisterEventsHandler(Events); - } + CustomHandlersManager.RegisterEventsHandler(Events); + } - public override void Disable() - { - CustomHandlersManager.UnregisterEventsHandler(Events); + public override void Disable() + { + CustomHandlersManager.UnregisterEventsHandler(Events); - Events = null; + Events = null; - Instance = null; - } + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Translation.cs b/DiscordLab.RoundLogs/Translation.cs index b692f3f..0f11f43 100644 --- a/DiscordLab.RoundLogs/Translation.cs +++ b/DiscordLab.RoundLogs/Translation.cs @@ -1,28 +1,27 @@ using System.ComponentModel; -namespace DiscordLab.RoundLogs +namespace DiscordLab.RoundLogs; + +public class Translation { - public class Translation - { - public string ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; + public string ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; - public string RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; + public string RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; - public string NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; + public string NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; - public string ChaosSpawn { get; set; } = "Chaos has respawned with the following player:\n{players}"; + public string ChaosSpawn { get; set; } = "Chaos has respawned with the following player:\n{players}"; - public string PlayerListItem { get; set; } = "- {player}"; + public string PlayerListItem { get; set; } = "- {player}"; - public string Cuffed { get; set; } = "Player {target} has been cuffed by {player}"; + public string Cuffed { get; set; } = "Player {target} has been cuffed by {player}"; - public string Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; + public string Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; - [Description("This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] - public string RoundStart { get; set; } = "Round has started."; + [Description("This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] + public string RoundStart { get; set; } = "Round has started."; - public string RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; + public string RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; - public string Decontamination { get; set; } = "Decontamination has begun."; - } + public string Decontamination { get; set; } = "Decontamination has begun."; } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs index bacef58..1f8dee7 100644 --- a/DiscordLab.StatusChannel/Command.cs +++ b/DiscordLab.StatusChannel/Command.cs @@ -2,21 +2,20 @@ using Discord.WebSocket; using DiscordLab.Bot.API.Features; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class Command : SlashCommand { - public class Command : SlashCommand + public override SlashCommandBuilder Data { get; } = new() { - public override SlashCommandBuilder Data { get; } = new() - { - Name = Plugin.Instance.Translation.PlayerListCommandName, - Description = Plugin.Instance.Translation.PlayerListCommandDescription, - }; + Name = Plugin.Instance.Translation.PlayerListCommandName, + Description = Plugin.Instance.Translation.PlayerListCommandDescription, + }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - public override async Task Run(SocketSlashCommand command) - { - await command.RespondAsync(embed: Events.GetEmbed().Build()); - } + public override async Task Run(SocketSlashCommand command) + { + await command.RespondAsync(embed: Events.GetEmbed().Build()); } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Config.cs b/DiscordLab.StatusChannel/Config.cs index ffab52a..ded2371 100644 --- a/DiscordLab.StatusChannel/Config.cs +++ b/DiscordLab.StatusChannel/Config.cs @@ -1,15 +1,14 @@ using System.ComponentModel; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class Config { - public class Config - { - [Description("The channel that you want the message sent to.")] - public ulong ChannelId { get; set; } = 0; + [Description("The channel that you want the message sent to.")] + public ulong ChannelId { get; set; } = 0; - [Description("The guild ID, set to 0 for default guild.")] - public ulong GuildId { get; set; } = 0; + [Description("The guild ID, set to 0 for default guild.")] + public ulong GuildId { get; set; } = 0; - public bool AddCommand { get; set; } = true; - } + public bool AddCommand { get; set; } = true; } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 7696360..f09f30c 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -10,123 +10,122 @@ using LabApi.Features.Wrappers; using LabApi.Loader; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class Events : CustomEventsHandler { - public class Events : CustomEventsHandler - { - // events + // events - public override void OnServerWaitingForPlayers() => EditMessage(); + public override void OnServerWaitingForPlayers() => EditMessage(); - public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); + public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); - public static void OnPlayerLeave(ReferenceHub _) => Process(); + public static void OnPlayerLeave(ReferenceHub _) => Process(); - public override void OnServerRoundStarted() => EditMessage(); + public override void OnServerRoundStarted() => EditMessage(); - // static methods + // static methods - public static Config Config => Plugin.Instance.Config; + public static Config Config => Plugin.Instance.Config; - public static Translation Translation => Plugin.Instance.Translation; + public static Translation Translation => Plugin.Instance.Translation; - public static SocketTextChannel Channel; + public static SocketTextChannel Channel; - public static IUserMessage Message; + public static IUserMessage Message; - public static Queue Queue = new(5, EditMessage); + public static Queue Queue = new(5, EditMessage); - [CallOnLoad] - public static void Register() - { - ReferenceHub.OnPlayerRemoved += OnPlayerLeave; - } + [CallOnLoad] + public static void Register() + { + ReferenceHub.OnPlayerRemoved += OnPlayerLeave; + } - [CallOnUnload] - public static void Unregister() - { - Channel = null; - Message = null; - Queue = null; + [CallOnUnload] + public static void Unregister() + { + Channel = null; + Message = null; + Queue = null; - ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; - } + ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; + } - public static void Process() - { - if(Round.IsRoundInProgress) - EditMessage(); - else - Queue.Process(); - } + public static void Process() + { + if(Round.IsRoundInProgress) + EditMessage(); + else + Queue.Process(); + } - public static EmbedBuilder GetEmbed() - { - EmbedBuilder embed = !Player.ReadyList.Any() ? Translation.EmbedEmpty : Translation.Embed; + public static EmbedBuilder GetEmbed() + { + EmbedBuilder embed = !Player.ReadyList.Any() ? Translation.EmbedEmpty : Translation.Embed; - TranslationBuilder builder = new(embed.Description); + TranslationBuilder builder = new(embed.Description); - if (Player.ReadyList.Any()) - { - builder.PlayerListItem = Translation.PlayerItem; - } + if (Player.ReadyList.Any()) + { + builder.PlayerListItem = Translation.PlayerItem; + } - embed.Description = builder; + embed.Description = builder; - return embed; - } + return embed; + } - public static void EditMessage() + public static void EditMessage() + { + if (Message == null) { - if (Message == null) - { - Task.Run(async () => - { - await GetOrCreateMessage(); - EditMessage(); - }); - return; - } - Task.Run(async () => { - try - { - await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()); - } - catch (Exception e) - { - Logger.Error(e); - } + await GetOrCreateMessage(); + EditMessage(); }); + return; } - - [CallOnReady] - public static void Ready() + + Task.Run(async () => { - if (!Client.TryGetOrAddChannel(Config.ChannelId, out Channel)) + try { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); - Plugin.Instance.Disable(); + await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()); } - } + catch (Exception e) + { + Logger.Error(e); + } + }); + } - public static async Task GetOrCreateMessage() + [CallOnReady] + public static void Ready() + { + if (!Client.TryGetOrAddChannel(Config.ChannelId, out Channel)) { - ulong msgId = Plugin.Instance.MessageConfig.MessageId; - Message = msgId != 0 ? Channel.GetCachedMessage(msgId) as IUserMessage ?? - await Channel.GetMessageAsync(msgId) as IUserMessage : null; + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); + Plugin.Instance.Disable(); + } + } + + public static async Task GetOrCreateMessage() + { + ulong msgId = Plugin.Instance.MessageConfig.MessageId; + Message = msgId != 0 ? Channel.GetCachedMessage(msgId) as IUserMessage ?? + await Channel.GetMessageAsync(msgId) as IUserMessage : null; - if (Message == null) - { - EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; - embed.Description = new TranslationBuilder(embed.Description); + if (Message == null) + { + EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; + embed.Description = new TranslationBuilder(embed.Description); - Message = await Channel.SendMessageAsync(embed: embed.Build()); + Message = await Channel.SendMessageAsync(embed: embed.Build()); - Plugin.Instance.MessageConfig.MessageId = Message.Id; - Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message_config.yml"); - } + Plugin.Instance.MessageConfig.MessageId = Message.Id; + Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message_config.yml"); } } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/MessageConfig.cs b/DiscordLab.StatusChannel/MessageConfig.cs index e900c09..a1fe4fb 100644 --- a/DiscordLab.StatusChannel/MessageConfig.cs +++ b/DiscordLab.StatusChannel/MessageConfig.cs @@ -1,10 +1,9 @@ using System.ComponentModel; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class MessageConfig { - public class MessageConfig - { - [Description("Do not set this, this will be set for you.")] - public ulong MessageId { get; set; } = 0; - } + [Description("Do not set this, this will be set for you.")] + public ulong MessageId { get; set; } = 0; } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index b36ea70..3c66fb0 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -6,52 +6,51 @@ using LabApi.Features; using LabApi.Loader; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class Plugin : Plugin { - public class Plugin : Plugin - { - public static Plugin Instance; + public static Plugin Instance; - public override string Name { get; } = "DiscordLab.StatusChannel"; - public override string Description { get; } = "Allows you to update/send a status message in a specific channel and have it update automatically."; - public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; - public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override string Name { get; } = "DiscordLab.StatusChannel"; + public override string Description { get; } = "Allows you to update/send a status message in a specific channel and have it update automatically."; + public override string Author { get; } = "LumiFae"; + public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - public MessageConfig MessageConfig { get; set; } + public MessageConfig MessageConfig { get; set; } - public Events Events = new(); + public Events Events = new(); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - CallOnLoadAttribute.Load(); + CallOnLoadAttribute.Load(); - CallOnReadyAttribute.Load(); + CallOnReadyAttribute.Load(); - CustomHandlersManager.RegisterEventsHandler(Events); + CustomHandlersManager.RegisterEventsHandler(Events); - if(Config.AddCommand) - SlashCommand.FindAll(); - } + if(Config.AddCommand) + SlashCommand.FindAll(); + } - public override void Disable() - { - CallOnUnloadAttribute.Unload(); + public override void Disable() + { + CallOnUnloadAttribute.Unload(); - CustomHandlersManager.UnregisterEventsHandler(Events); + CustomHandlersManager.UnregisterEventsHandler(Events); - Events = null; + Events = null; - Instance = null; - } + Instance = null; + } - public override void LoadConfigs() - { - this.TryLoadConfig("message_config.yml", out MessageConfig messageConfig); - MessageConfig = messageConfig ?? new(); - base.LoadConfigs(); - } + public override void LoadConfigs() + { + this.TryLoadConfig("message_config.yml", out MessageConfig messageConfig); + MessageConfig = messageConfig ?? new(); + base.LoadConfigs(); } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index 8d0a97e..ca4dc98 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -2,31 +2,30 @@ using Discord; using EmbedBuilder = DiscordLab.Bot.API.Features.Embed.EmbedBuilder; -namespace DiscordLab.StatusChannel +namespace DiscordLab.StatusChannel; + +public class Translation { - public class Translation + [Description("What will show when the server has players.")] + public EmbedBuilder Embed { get; set; } = new() { - [Description("What will show when the server has players.")] - public EmbedBuilder Embed { get; set; } = new() - { - Title = "Server Status", - Color = Color.Blue.ToString(), - Description = "{playercount}/{maxplayers} currently online\n```{players}```" - }; + Title = "Server Status", + Color = Color.Blue.ToString(), + Description = "{playercount}/{maxplayers} currently online\n```{players}```" + }; - [Description("What will show when the server is empty.")] - public EmbedBuilder EmbedEmpty { get; set; } = new() - { - Title = "Server Status", - Color = Color.Blue.ToString(), - Description = "0/{maxplayers} currently online" - }; + [Description("What will show when the server is empty.")] + public EmbedBuilder EmbedEmpty { get; set; } = new() + { + Title = "Server Status", + Color = Color.Blue.ToString(), + Description = "0/{maxplayers} currently online" + }; - [Description("What will appear for each player when replacing the players variable above.")] - public string PlayerItem { get; set; } = "- {player}"; + [Description("What will appear for each player when replacing the players variable above.")] + public string PlayerItem { get; set; } = "- {player}"; - public string PlayerListCommandName { get; set; } = "players"; + public string PlayerListCommandName { get; set; } = "players"; - public string PlayerListCommandDescription { get; set; } = "Get the current list of players on the server"; - } + public string PlayerListCommandDescription { get; set; } = "Get the current list of players on the server"; } \ No newline at end of file diff --git a/DiscordLab.sln.DotSettings b/DiscordLab.sln.DotSettings deleted file mode 100644 index ee7219b..0000000 --- a/DiscordLab.sln.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - ERROR - BlockScoped \ No newline at end of file From 6fca519af24a5f3e607acb902e34e5176f230128 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 15 Aug 2025 21:40:24 +0100 Subject: [PATCH 31/68] ci: fix exiled ref --- .github/workflows/artifact.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index b0f8dbd..b40e2a1 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -4,7 +4,7 @@ on: - push env: - EXILED_REFERENCES_URL: https://exslmod-team.github.io/SL-References/Dev.zip + EXILED_REFERENCES_URL: https://exmod-team.github.io/SL-References/Dev.zip XP_SYSTEM_URL: https://api.github.com/repos/RowpannSCP/XP/releases SL_REFERENCES: ${{ github.workspace }}/refs EXILED_REFERENCES: ${{ github.workspace }}/refs From c53d751f6e5bf7684946c939d94fd3ed436c524c Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 16 Aug 2025 12:39:03 +0100 Subject: [PATCH 32/68] feat: remove useless method --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index f278968..cf490ce 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -5,7 +5,6 @@ namespace DiscordLab.Bot.API.Features; using System.Globalization; using System.Text.RegularExpressions; using Discord; -using LabApi.Features.Extensions; using LabApi.Features.Wrappers; using LightContainmentZoneDecontamination; using Mirror.LiteNetLib4Mirror; @@ -123,7 +122,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["id"] = player => player.UserId, ["ip"] = player => player.IpAddress, ["userid"] = player => player.PlayerId.ToString(), - ["role"] = player => player.Role.GetFullName(), + ["role"] = player => player.RoleBase.RoleName, ["roletype"] = player => player.Role.ToString(), ["team"] = player => player.Team.ToString(), ["faction"] = player => player.Team.GetFaction().ToString(), From 72a76f06bc63fbe1173a03d34448c6c7b7230690 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 16 Aug 2025 12:39:31 +0100 Subject: [PATCH 33/68] fix(ban logs): no reason and invalid time --- DiscordLab.Moderation/Events.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Moderation/Events.cs b/DiscordLab.Moderation/Events.cs index 6ad355f..a43b9ae 100644 --- a/DiscordLab.Moderation/Events.cs +++ b/DiscordLab.Moderation/Events.cs @@ -82,11 +82,19 @@ public override void OnPlayerBanned(PlayerBannedEventArgs ev) } EmbedBuilder builder = Translation.BanLogEmbed; + + TimeSpan timeSpan = TimeSpan.FromSeconds(ev.Duration); + DateTime expiration = DateTime.Now.Add(timeSpan); foreach (EmbedFieldBuilder field in builder.Fields) { - TranslationBuilder tBuilder = new((string)field.Value, "player", ev.Issuer); - tBuilder.CustomReplacers.Add("userid", () => ev.PlayerId); + TranslationBuilder tBuilder = new TranslationBuilder((string)field.Value, "player", ev.Issuer) + { + Time = expiration + } + .AddCustomReplacer("userid", ev.PlayerId) + .AddCustomReplacer("reason", ev.Reason); + field.Value = tBuilder.Build(); } From 6507dafd5f4f21e9661af1616b5252aaac860126 Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 16 Aug 2025 12:44:42 +0100 Subject: [PATCH 34/68] ci: fix exiled dumb renaming thing --- .github/workflows/artifact.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index b40e2a1..acc842c 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -5,7 +5,6 @@ on: env: EXILED_REFERENCES_URL: https://exmod-team.github.io/SL-References/Dev.zip - XP_SYSTEM_URL: https://api.github.com/repos/RowpannSCP/XP/releases SL_REFERENCES: ${{ github.workspace }}/refs EXILED_REFERENCES: ${{ github.workspace }}/refs @@ -24,6 +23,9 @@ jobs: run: | Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force + - name: Rename Assembly-CSharp (because Exiled removes the normal version) + shell: bash + run: mv ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll - name: Build run: dotnet build -c:Release - name: Publish Artifact From e0890f32df1617a278b968c43191ec19b49637dd Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 16 Aug 2025 12:47:17 +0100 Subject: [PATCH 35/68] ci: fix again bruh --- .github/workflows/artifact.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml index acc842c..7691f1d 100644 --- a/.github/workflows/artifact.yml +++ b/.github/workflows/artifact.yml @@ -24,8 +24,8 @@ jobs: Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force - name: Rename Assembly-CSharp (because Exiled removes the normal version) - shell: bash - run: mv ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll + shell: pwsh + run: Move-Item -Path ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll -Destination ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll - name: Build run: dotnet build -c:Release - name: Publish Artifact From da127f5e9d9eedcc16d3a4c45f2194fbab58d2dd Mon Sep 17 00:00:00 2001 From: Lumi Date: Sat, 16 Aug 2025 12:53:12 +0100 Subject: [PATCH 36/68] fix: no dependencies --- DiscordLab.Bot/DiscordLab.Bot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 3e78174..04c79a6 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -45,7 +45,7 @@ - $(MSBuildThisFileDirectory)bin\dependencies\ + $(MSBuildThisFileDirectory)..\bin\dependencies\ From e7aae3d4219ff77e74f518c46f45b98aa8ec58b8 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 11:12:50 +0100 Subject: [PATCH 37/68] feat: start on message content --- Directory.Build.props | 54 ++++----- DiscordLab.Bot/API/Features/MessageContent.cs | 110 ++++++++++++++++++ .../API/Features/TranslationBuilder.cs | 31 ++++- DiscordLab.StatusChannel/Command.cs | 2 +- .../DiscordLab.StatusChannel.csproj | 2 +- DiscordLab.StatusChannel/Events.cs | 68 +++++------ DiscordLab.StatusChannel/Plugin.cs | 25 ++-- DiscordLab.StatusChannel/Translation.cs | 5 +- 8 files changed, 209 insertions(+), 88 deletions(-) create mode 100644 DiscordLab.Bot/API/Features/MessageContent.cs diff --git a/Directory.Build.props b/Directory.Build.props index e45b656..df9990f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,31 +1,31 @@  - - $(MSBuildThisFileDirectory)bin\ - + + $(MSBuildThisFileDirectory)bin\ + - - - - - - - - - - - - - - - - - - + + + - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs new file mode 100644 index 0000000..2272dd6 --- /dev/null +++ b/DiscordLab.Bot/API/Features/MessageContent.cs @@ -0,0 +1,110 @@ +// ReSharper disable MemberCanBePrivate.Global + +namespace DiscordLab.Bot.API.Features; + +using Discord.Rest; +using Discord.WebSocket; +using DiscordLab.Bot.API.Extensions; +using YamlDotNet.Serialization; + +/// +/// Message config object for either string messages or embeds. +/// +public class MessageContent +{ + /// + /// Gets or sets the embed to send, if any. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public Embed.EmbedBuilder Embed { get; set; } + + /// + /// Gets or sets the string to send, if any. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string Message { get; set; } + + /// + /// Converts an embed into a instance. + /// + /// The embed. + /// The instance. + public static implicit operator MessageContent(Embed.EmbedBuilder embed) => new() { Embed = embed }; + + /// + /// Converts a string into a instance. + /// + /// The content. + /// The instance. + public static implicit operator MessageContent(string content) => new() { Message = content }; + + /// + /// Sends this message to a channel. + /// + /// The channel to send the message to. + /// The instance to utilise. + public void SendToChannel(SocketTextChannel channel, TranslationBuilder builder) + { + if (Embed == null && Message == null) + throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined."); + + (Discord.Embed embed, string content) = Build(builder); + + channel.SendMessage(content, embed: embed); + } + + /// + /// Sends this message to a channel, asynchronously. + /// + /// The channel to send the message to. + /// The instance to utilise. + /// The new message object. + public async Task SendToChannelAsync(SocketTextChannel channel, TranslationBuilder builder) + { + if (Embed == null && Message == null) + throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined."); + + (Discord.Embed embed, string content) = Build(builder); + + return await channel.SendMessageAsync(content, embed: embed); + } + + /// + /// Modifies the instance with this message and builder. + /// + /// The instance. + /// The instance to utilise. + public void ModifyMessage(Discord.IUserMessage message, TranslationBuilder builder) + { + if (Embed == null && Message == null) + throw new ArgumentNullException($"Message {message.Id} (in #{message.Channel.Name} ({message.Channel.Id})) failed to be edited because both embed and message contents were undefined."); + + (Discord.Embed embed, string content) = Build(builder); + + Task.Run(async () => await message.ModifyAsync(msg => + { + if (!string.IsNullOrEmpty(content)) + msg.Content = content; + + if (embed != null) + msg.Embed = embed; + }).ConfigureAwait(false)); + } + + private (Discord.Embed Embed, string Content) Build(TranslationBuilder builder) + { + if (Embed == null) + return (null, Message != null ? builder.Build(Message) : null); + + Discord.EmbedBuilder embed = Embed; + if (!string.IsNullOrEmpty(embed.Description)) + embed.Description = builder.Build(embed.Description); + + foreach (Discord.EmbedFieldBuilder field in embed.Fields.Where(field => field.Value is string value && !string.IsNullOrEmpty(value))) + { + field.Value = builder.Build((string)field.Value); + } + + return (embed?.Build(), Message != null ? builder.Build(Message) : null); + } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index cf490ce..2a7a85f 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -20,7 +20,24 @@ public class TranslationBuilder private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled); private static readonly Regex UselessTextRemoveRegex = - new(@"(.*?)<\/color>", RegexOptions.Compiled); + new(@"(?:.*?)<\/color>", RegexOptions.Compiled); + + /// + /// Initializes a new instance of the class. + /// + public TranslationBuilder() + { + } + + /// + /// Initializes a new instance of the class with a person added. + /// + /// The player prefix. + /// The player to use for the prefix. + public TranslationBuilder(string playerPrefix, Player player) + { + AddPlayer(playerPrefix, player); + } /// /// Initializes a new instance of the class. @@ -157,9 +174,9 @@ public TranslationBuilder(string translation, string playerPrefix, Player player public DateTime Time { get; set; } = DateTime.Now; /// - /// Gets the translation. + /// Gets or sets the translation. /// - public string Translation { get; } + public string Translation { get; set; } /// /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null. @@ -250,9 +267,15 @@ public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) /// /// Builds this instance. /// + /// The translation to build from, isn't needed if is defined. /// The translation built. - public string Build() + public string Build(string translation = null) { + translation ??= Translation; + + if (string.IsNullOrEmpty(translation)) + throw new ArgumentNullException($"{nameof(TranslationBuilder)} failed to build because of no valid translation."); + if (PlayerListItem != null) SetupPlayerList(); diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs index 1f8dee7..d55ec1a 100644 --- a/DiscordLab.StatusChannel/Command.cs +++ b/DiscordLab.StatusChannel/Command.cs @@ -13,7 +13,7 @@ public class Command : SlashCommand }; public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - + public override async Task Run(SocketSlashCommand command) { await command.RespondAsync(embed: Events.GetEmbed().Build()); diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index b6fe025..b174597 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index f09f30c..f20ea7e 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -17,19 +17,19 @@ public class Events : CustomEventsHandler // events public override void OnServerWaitingForPlayers() => EditMessage(); - + public override void OnPlayerJoined(PlayerJoinedEventArgs _) => Process(); - + public static void OnPlayerLeave(ReferenceHub _) => Process(); public override void OnServerRoundStarted() => EditMessage(); - + // static methods - + public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - + public static SocketTextChannel Channel; public static IUserMessage Message; @@ -41,7 +41,7 @@ public static void Register() { ReferenceHub.OnPlayerRemoved += OnPlayerLeave; } - + [CallOnUnload] public static void Unregister() { @@ -51,31 +51,15 @@ public static void Unregister() ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; } - + public static void Process() { - if(Round.IsRoundInProgress) + if (Round.IsRoundInProgress) EditMessage(); else Queue.Process(); } - public static EmbedBuilder GetEmbed() - { - EmbedBuilder embed = !Player.ReadyList.Any() ? Translation.EmbedEmpty : Translation.Embed; - - TranslationBuilder builder = new(embed.Description); - - if (Player.ReadyList.Any()) - { - builder.PlayerListItem = Translation.PlayerItem; - } - - embed.Description = builder; - - return embed; - } - public static void EditMessage() { if (Message == null) @@ -87,18 +71,18 @@ public static void EditMessage() }); return; } - - Task.Run(async () => + + try { - try - { - await Message.ModifyAsync(x => x.Embed = GetEmbed().Build()); - } - catch (Exception e) + (Player.ReadyList.Any() ? Translation.Content : Translation.EmptyContent).ModifyMessage(Message, new() { - Logger.Error(e); - } - }); + PlayerListItem = Translation.PlayerItem + }); + } + catch (Exception e) + { + Logger.Error(e); + } } [CallOnReady] @@ -106,7 +90,8 @@ public static void Ready() { if (!Client.TryGetOrAddChannel(Config.ChannelId, out Channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("status channel", Config.ChannelId, Config.GuildId)); Plugin.Instance.Disable(); } } @@ -114,15 +99,14 @@ public static void Ready() public static async Task GetOrCreateMessage() { ulong msgId = Plugin.Instance.MessageConfig.MessageId; - Message = msgId != 0 ? Channel.GetCachedMessage(msgId) as IUserMessage ?? - await Channel.GetMessageAsync(msgId) as IUserMessage : null; - + Message = msgId != 0 + ? Channel.GetCachedMessage(msgId) as IUserMessage ?? + await Channel.GetMessageAsync(msgId) as IUserMessage + : null; + if (Message == null) { - EmbedBuilder embed = Plugin.Instance.Translation.EmbedEmpty; - embed.Description = new TranslationBuilder(embed.Description); - - Message = await Channel.SendMessageAsync(embed: embed.Build()); + Message = await Translation.EmptyContent.SendToChannelAsync(Channel, new()); Plugin.Instance.MessageConfig.MessageId = Message.Id; Plugin.Instance.SaveConfig(Plugin.Instance.MessageConfig, "message_config.yml"); diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index 3c66fb0..85398ad 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -11,39 +11,42 @@ namespace DiscordLab.StatusChannel; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.StatusChannel"; - public override string Description { get; } = "Allows you to update/send a status message in a specific channel and have it update automatically."; + + public override string Description { get; } = + "Allows you to update/send a status message in a specific channel and have it update automatically."; + public override string Author { get; } = "LumiFae"; public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - + public MessageConfig MessageConfig { get; set; } public Events Events = new(); - + public override void Enable() { Instance = this; - + CallOnLoadAttribute.Load(); - + CallOnReadyAttribute.Load(); - + CustomHandlersManager.RegisterEventsHandler(Events); - - if(Config.AddCommand) + + if (Config.AddCommand) SlashCommand.FindAll(); } public override void Disable() { CallOnUnloadAttribute.Unload(); - + CustomHandlersManager.UnregisterEventsHandler(Events); Events = null; - + Instance = null; } diff --git a/DiscordLab.StatusChannel/Translation.cs b/DiscordLab.StatusChannel/Translation.cs index ca4dc98..68ba4b8 100644 --- a/DiscordLab.StatusChannel/Translation.cs +++ b/DiscordLab.StatusChannel/Translation.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Discord; +using DiscordLab.Bot.API.Features; using EmbedBuilder = DiscordLab.Bot.API.Features.Embed.EmbedBuilder; namespace DiscordLab.StatusChannel; @@ -7,7 +8,7 @@ namespace DiscordLab.StatusChannel; public class Translation { [Description("What will show when the server has players.")] - public EmbedBuilder Embed { get; set; } = new() + public MessageContent Content { get; set; } = new EmbedBuilder { Title = "Server Status", Color = Color.Blue.ToString(), @@ -15,7 +16,7 @@ public class Translation }; [Description("What will show when the server is empty.")] - public EmbedBuilder EmbedEmpty { get; set; } = new() + public MessageContent EmptyContent { get; set; } = new EmbedBuilder { Title = "Server Status", Color = Color.Blue.ToString(), From 3e3481747314cb2d111207c90b65d1eb9f8987cb Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 11:20:46 +0100 Subject: [PATCH 38/68] feat: interaction response --- DiscordLab.Bot/API/Features/MessageContent.cs | 16 ++++++++++++++++ DiscordLab.StatusChannel/Command.cs | 5 ++++- DiscordLab.StatusChannel/Events.cs | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs index 2272dd6..6f24562 100644 --- a/DiscordLab.Bot/API/Features/MessageContent.cs +++ b/DiscordLab.Bot/API/Features/MessageContent.cs @@ -91,6 +91,22 @@ public void ModifyMessage(Discord.IUserMessage message, TranslationBuilder build }).ConfigureAwait(false)); } + /// + /// Responds to a with this message and builder. + /// + /// The instance. + /// The instance to utilise. + /// This task. + public async Task InteractionRespond(SocketCommandBase command, TranslationBuilder builder) + { + if (Embed == null && Message == null) + throw new ArgumentNullException($"Failed to respond to command {command.CommandName} because both embed and message contents were undefined."); + + (Discord.Embed embed, string content) = Build(builder); + + await command.RespondAsync(content, embed: embed); + } + private (Discord.Embed Embed, string Content) Build(TranslationBuilder builder) { if (Embed == null) diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs index d55ec1a..930392c 100644 --- a/DiscordLab.StatusChannel/Command.cs +++ b/DiscordLab.StatusChannel/Command.cs @@ -16,6 +16,9 @@ public class Command : SlashCommand public override async Task Run(SocketSlashCommand command) { - await command.RespondAsync(embed: Events.GetEmbed().Build()); + await Events.UsableContent.InteractionRespond(command, new() + { + PlayerListItem = Plugin.Instance.Translation.PlayerItem + }); } } \ No newline at end of file diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index f20ea7e..6051faa 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -60,6 +60,8 @@ public static void Process() Queue.Process(); } + public static MessageContent UsableContent => Player.ReadyList.Any() ? Translation.Content : Translation.EmptyContent; + public static void EditMessage() { if (Message == null) @@ -74,7 +76,7 @@ public static void EditMessage() try { - (Player.ReadyList.Any() ? Translation.Content : Translation.EmptyContent).ModifyMessage(Message, new() + UsableContent.ModifyMessage(Message, new() { PlayerListItem = Translation.PlayerItem }); From 7b190c8ebb37a88ec489f5f5db3666dd987e315e Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 13:38:30 +0100 Subject: [PATCH 39/68] feat: log revamp --- .../Commands/SendCommand.cs | 6 +- DiscordLab.Administration/Events.cs | 31 +++----- DiscordLab.Administration/Patches/ErrorLog.cs | 6 +- DiscordLab.Administration/Translation.cs | 11 +-- DiscordLab.Bot/API/Features/MessageContent.cs | 23 ++++++ DiscordLab.ConnectionLogs/Events.cs | 17 +++-- DiscordLab.ConnectionLogs/Translation.cs | 9 +-- DiscordLab.DeathLogs/Events.cs | 18 ++--- DiscordLab.DeathLogs/Translation.cs | 9 +-- DiscordLab.Moderation/Events.cs | 56 +++++++-------- DiscordLab.Moderation/Translation.cs | 16 +++-- DiscordLab.RoundLogs/Events.cs | 72 ++++++++----------- DiscordLab.RoundLogs/Translation.cs | 19 ++--- 13 files changed, 148 insertions(+), 145 deletions(-) diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index ae402ad..be59aa5 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -38,10 +38,10 @@ public override async Task Run(SocketSlashCommand command) string response = Server.RunCommand((string)command.Data.Options.First().Value); - TranslationBuilder builder = new(Translation.SendCommandResponse); - builder.CustomReplacers.Add("response", () => response); + TranslationBuilder builder = new TranslationBuilder() + .AddCustomReplacer("response", response); - await command.ModifyOriginalResponseAsync(m => m.Content = builder); + await Translation.SendCommandResponse.ModifyInteraction(command, builder); } public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs index 4e0a2e6..c53ffc5 100644 --- a/DiscordLab.Administration/Events.cs +++ b/DiscordLab.Administration/Events.cs @@ -51,7 +51,7 @@ public static void OnServerStart() return; } - channel.SendMessage(Translation.ServerStart); + Translation.ServerStart.SendToChannel(channel, new()); } public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) @@ -60,14 +60,15 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) return; SocketTextChannel channel; - TranslationBuilder builder; - - Dictionary> customReplacers = new() + TranslationBuilder builder = new("player", player) { - ["type"] = () => ev.CommandType.ToString(), - ["arguments"] = () => string.Join(" ", ev.Arguments), - ["command"] = () => ev.Command.Command, - ["commanddescription"] = () => ev.Command.Description, + CustomReplacers = new() + { + ["type"] = () => ev.CommandType.ToString(), + ["arguments"] = () => string.Join(" ", ev.Arguments), + ["command"] = () => ev.Command.Command, + ["commanddescription"] = () => ev.Command.Description, + } }; if (ev.CommandType == CommandType.RemoteAdmin) @@ -80,13 +81,8 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", Config.RemoteAdminChannelId, Config.GuildId)); return; } - - builder = new(Translation.RemoteAdmin, "player", player) - { - CustomReplacers = customReplacers - }; - channel.SendMessage(builder); + Translation.RemoteAdmin.SendToChannel(channel, builder); return; } @@ -99,11 +95,6 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) return; } - builder = new(Translation.CommandLog, "player", player) - { - CustomReplacers = customReplacers - }; - - channel.SendMessage(builder); + Translation.CommandLog.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Patches/ErrorLog.cs b/DiscordLab.Administration/Patches/ErrorLog.cs index a1012a2..f3e4a61 100644 --- a/DiscordLab.Administration/Patches/ErrorLog.cs +++ b/DiscordLab.Administration/Patches/ErrorLog.cs @@ -23,9 +23,9 @@ public static void Postfix(object message) return; } - TranslationBuilder builder = new(Plugin.Instance.Translation.ErrorLog); - builder.CustomReplacers.Add("error", message.ToString); + TranslationBuilder builder = new TranslationBuilder() + .AddCustomReplacer("error", message.ToString()); - channel.SendMessage(builder); + Plugin.Instance.Translation.ErrorLog.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs index 4fdf710..46272ae 100644 --- a/DiscordLab.Administration/Translation.cs +++ b/DiscordLab.Administration/Translation.cs @@ -1,17 +1,18 @@ using System.ComponentModel; using Discord; +using DiscordLab.Bot.API.Features; namespace DiscordLab.Administration; public class Translation { - public string ServerStart { get; set; } = "Server has started"; + public MessageContent ServerStart { get; set; } = "Server has started"; - public string ErrorLog { get; set; } = "An error has occured:\n{error}"; + public MessageContent ErrorLog { get; set; } = "An error has occured:\n{error}"; - public string RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; + public MessageContent RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; - public string CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; + public MessageContent CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; public string SendCommandName { get; set; } = "send"; @@ -21,5 +22,5 @@ public class Translation public string SendCommandOptionDescription { get; set; } = "The command to send"; - public string SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; + public MessageContent SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs index 6f24562..f1f1571 100644 --- a/DiscordLab.Bot/API/Features/MessageContent.cs +++ b/DiscordLab.Bot/API/Features/MessageContent.cs @@ -107,6 +107,29 @@ public async Task InteractionRespond(SocketCommandBase command, TranslationBuild await command.RespondAsync(content, embed: embed); } + /// + /// Modifies a 's response with the new message and builder. + /// + /// The instance. + /// The instance to utilise. + /// This task. + public async Task ModifyInteraction(SocketCommandBase command, TranslationBuilder builder) + { + if (Embed == null && Message == null) + throw new ArgumentNullException($"Failed to modify command {command.CommandName}'s response because both embed and message contents were undefined."); + + (Discord.Embed embed, string content) = Build(builder); + + await command.ModifyOriginalResponseAsync(msg => + { + if (!string.IsNullOrEmpty(content)) + msg.Content = content; + + if (embed != null) + msg.Embed = embed; + }); + } + private (Discord.Embed Embed, string Content) Build(TranslationBuilder builder) { if (Embed == null) diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index 785d8b5..41e4e69 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -31,7 +31,7 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev) return; } - channel.SendMessage(new TranslationBuilder(Translation.PlayerJoin, "player", ev.Player)); + Translation.PlayerJoin.SendToChannel(channel, new( "player", ev.Player)); } public override void OnPlayerLeft(PlayerLeftEventArgs ev) @@ -48,7 +48,7 @@ public override void OnPlayerLeft(PlayerLeftEventArgs ev) return; } - channel.SendMessage(new TranslationBuilder(Translation.PlayerLeave, "player", ev.Player)); + Translation.PlayerLeave.SendToChannel(channel, new("player", ev.Player)); } public override void OnServerRoundStarted() @@ -61,8 +61,8 @@ public override void OnServerRoundStarted() Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, Config.GuildId)); return; } - - channel.SendMessage(new TranslationBuilder(Translation.RoundStart) + + Translation.RoundStart.SendToChannel(channel, new() { PlayerListItem = Translation.RoundPlayers }); @@ -79,13 +79,12 @@ public override void OnServerRoundEnded(RoundEndedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.RoundEnd) + TranslationBuilder builder = new TranslationBuilder() { PlayerListItem = Translation.RoundPlayers - }; - - builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + } + .AddCustomReplacer("winner", ev.LeadingTeam.ToString()); - channel.SendMessage(builder); + Translation.RoundEnd.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Translation.cs b/DiscordLab.ConnectionLogs/Translation.cs index b87ce3d..3913b0c 100644 --- a/DiscordLab.ConnectionLogs/Translation.cs +++ b/DiscordLab.ConnectionLogs/Translation.cs @@ -1,20 +1,21 @@ using System.ComponentModel; +using DiscordLab.Bot.API.Features; namespace DiscordLab.ConnectionLogs; public class Translation { [Description("The message that will be sent when a player joins the server.")] - public string PlayerJoin { get; set; } = "`{player}` (`{playerid}`) has joined the server."; + public MessageContent PlayerJoin { get; set; } = "`{player}` (`{playerid}`) has joined the server."; [Description("The message that will be sent when a player leaves the server.")] - public string PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; + public MessageContent PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; [Description("The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] - public string RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; + public MessageContent RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; [Description("The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] - public string RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; + public MessageContent RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; [Description("The message that indicates what a player looks like in the round start/end message.")] public string RoundPlayers { get; set; } = "{playername} ({playerid})"; diff --git a/DiscordLab.DeathLogs/Events.cs b/DiscordLab.DeathLogs/Events.cs index f8b5ae5..064d152 100644 --- a/DiscordLab.DeathLogs/Events.cs +++ b/DiscordLab.DeathLogs/Events.cs @@ -55,13 +55,13 @@ public static void OnTeamKill(PlayerDyingEventArgs ev) return; } - TranslationBuilder builder = new TranslationBuilder(Translation.TeamKill) + TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)) .AddCustomReplacer("role", ev.Player.Team.GetFaction().ToString()); - channel.SendMessage(builder); + Translation.TeamKill.SendToChannel(channel, builder); } public static void OnCuffKill(PlayerDyingEventArgs ev) @@ -79,12 +79,12 @@ public static void OnCuffKill(PlayerDyingEventArgs ev) return; } - TranslationBuilder builder = new TranslationBuilder(Translation.CuffedPlayerDeath) + TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - - channel.SendMessage(builder); + + Translation.CuffedPlayerDeath.SendToChannel(channel, builder); } public static void OnDeath(PlayerDyingEventArgs ev) @@ -103,12 +103,12 @@ public static void OnDeath(PlayerDyingEventArgs ev) return; } - TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeath) + TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - channel.SendMessage(builder); + Translation.PlayerDeath.SendToChannel(channel, builder); } public static void OnOwnDeath(PlayerDyingEventArgs ev) @@ -132,11 +132,11 @@ public static void OnOwnDeath(PlayerDyingEventArgs ev) if (converted == "Unknown") return; - TranslationBuilder builder = new TranslationBuilder(Translation.PlayerDeathSelf) + TranslationBuilder builder = new TranslationBuilder() .AddPlayer("player", ev.Player) .AddCustomReplacer("cause", converted); - channel.SendMessage(builder); + Translation.PlayerDeathSelf.SendToChannel(channel, builder); } private static Dictionary _translations = new() diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index 66b6cd1..4b4b028 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Features.Embed; namespace DiscordLab.DeathLogs; @@ -6,19 +7,19 @@ namespace DiscordLab.DeathLogs; public class Translation { [Description("The message that will be sent when a player dies.")] - public string PlayerDeath { get; set; } = + public MessageContent PlayerDeath { get; set; } = "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}`. They died from: `{cause}`"; [Description("The message that will be sent when a cuffed player dies, unless the cuffed channel is disabled.")] - public string CuffedPlayerDeath { get; set; } = + public MessageContent CuffedPlayerDeath { get; set; } = "`{target}` (`{targetrole}`) has been killed by `{player}` as `{playerrole}` while cuffed. They died from: `{cause}`"; [Description( "The message that will be sent when a player dies by their own actions, or just they died because of something else.")] - public string PlayerDeathSelf { get; set; } = "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; + public MessageContent PlayerDeathSelf { get; set; } = "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; [Description("The message that will be sent when a player dies due to someone on their own team.")] - public string TeamKill { get; set; } = + public MessageContent TeamKill { get; set; } = "`{target}` has been team-killed by `{player}`, they were both {role}. They died from: `{cause}`"; [Description("The embed for when sending damage logs. Entries will be replaced with the entries below.")] diff --git a/DiscordLab.Moderation/Events.cs b/DiscordLab.Moderation/Events.cs index a43b9ae..9fb97b2 100644 --- a/DiscordLab.Moderation/Events.cs +++ b/DiscordLab.Moderation/Events.cs @@ -37,11 +37,11 @@ public override void OnPlayerUnmuted(PlayerUnmutedEventArgs ev) return; } - TranslationBuilder builder = new TranslationBuilder(Translation.UnmuteLog) + TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Issuer); - - channel.SendMessage(builder); + + Translation.UnmuteLog.SendToChannel(channel, builder); } public override void OnPlayerMuted(PlayerMutedEventArgs ev) @@ -55,19 +55,21 @@ public override void OnPlayerMuted(PlayerMutedEventArgs ev) return; } - string translation = Translation.PermMuteLog; + MessageContent translation = Translation.PermMuteLog; if (TempMuteManager.MuteConfig.Mutes.TryGetValue(ev.Player.UserId, out DateTime time)) { translation = Translation.TempMuteLog; } - TranslationBuilder builder = new TranslationBuilder(translation) + TranslationBuilder builder = new TranslationBuilder { Time = time - }.AddPlayer("player", ev.Issuer).AddPlayer("target", ev.Player); + } + .AddPlayer("player", ev.Issuer) + .AddPlayer("target", ev.Player); - channel.SendMessage(builder); + translation.SendToChannel(channel, builder); } public override void OnPlayerBanned(PlayerBannedEventArgs ev) @@ -77,28 +79,22 @@ public override void OnPlayerBanned(PlayerBannedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.BanLogChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("ban logs", Config.BanLogChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("ban logs", Config.BanLogChannelId, Config.GuildId)); return; } - EmbedBuilder builder = Translation.BanLogEmbed; - TimeSpan timeSpan = TimeSpan.FromSeconds(ev.Duration); DateTime expiration = DateTime.Now.Add(timeSpan); + + TranslationBuilder builder = new TranslationBuilder("player", ev.Issuer) + { + Time = expiration + } + .AddCustomReplacer("userid", ev.PlayerId) + .AddCustomReplacer("reason", ev.Reason); - foreach (EmbedFieldBuilder field in builder.Fields) - { - TranslationBuilder tBuilder = new TranslationBuilder((string)field.Value, "player", ev.Issuer) - { - Time = expiration - } - .AddCustomReplacer("userid", ev.PlayerId) - .AddCustomReplacer("reason", ev.Reason); - - field.Value = tBuilder.Build(); - } - - channel.SendMessage(embed:builder.Build()); + Translation.BanLogEmbed.SendToChannel(channel, builder); } public override void OnServerBanRevoked(BanRevokedEventArgs ev) @@ -112,15 +108,11 @@ public override void OnServerBanRevoked(BanRevokedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.UnbanLog); - - builder.CustomReplacers.Add("userid", () => ev.BanDetails.Id); - builder.CustomReplacers.Add("username", () => ev.BanDetails.OriginalName); - builder.CustomReplacers.Add("playerid", () => ev.BanDetails.Issuer); - - if (Player.TryGet(ev.BanDetails.Issuer, out Player player)) - builder.AddPlayer("player", player); + TranslationBuilder builder = new TranslationBuilder() + .AddCustomReplacer("userid", ev.BanDetails.Id) + .AddCustomReplacer("username", ev.BanDetails.OriginalName) + .AddCustomReplacer("playerid", ev.BanDetails.Issuer); - channel.SendMessage(builder); + Translation.UnbanLog.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index b0398ac..4b5de5d 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using DiscordLab.Bot.API.Features; using DiscordLab.Bot.API.Features.Embed; namespace DiscordLab.Moderation; @@ -46,16 +47,16 @@ public class Translation public string UnbanSuccess { get; set; } = "Player {userid} has been unbanned."; - public string PermMuteLog { get; set; } = "Player {target} has been muted by {player}."; + public MessageContent PermMuteLog { get; set; } = "Player {target} has been muted by {player}."; - public string TempMuteLog { get; set; } = + public MessageContent TempMuteLog { get; set; } = "Player {target} has been muted by {player} for {timef}, they will be unmuted in {timer}"; - public string UnmuteLog { get; set; } = "Player {target} has been unmuted by {player}."; + public MessageContent UnmuteLog { get; set; } = "Player {target} has been unmuted by {player}."; [Description( "Every field value accepts placeholders, even if you add more. player in this case is the issuer.")] - public EmbedBuilder BanLogEmbed { get; set; } = new() + public MessageContent BanLogEmbed { get; set; } = new EmbedBuilder { Title = "Ban Log", Description = "A user has been banned", @@ -75,10 +76,15 @@ public class Translation { Name = "Duration", Value = "{timer} ({timef})" + }, + new() + { + Name = "Reason", + Value = "{reason}" } ] }; [Description("Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] - public string UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; + public MessageContent UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs index 84af393..f477f5a 100644 --- a/DiscordLab.RoundLogs/Events.cs +++ b/DiscordLab.RoundLogs/Events.cs @@ -25,18 +25,19 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died or RoleChangeReason.Destroyed) return; - - Dictionary> customReplacers = new() - { - ["oldrole"] = () => ev.OldRole.GetFullName(), - ["newrole"] = () => ev.NewRole.RoleName, - ["reason"] = () => ev.ChangeReason.ToString(), - ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) - }; SocketTextChannel channel; - TranslationBuilder builder; + TranslationBuilder builder = new("player", ev.Player) + { + CustomReplacers = new() + { + ["oldrole"] = () => ev.OldRole.GetFullName(), + ["newrole"] = () => ev.NewRole.RoleName, + ["reason"] = () => ev.ChangeReason.ToString(), + ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) + } + }; if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) { @@ -49,12 +50,7 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) return; } - builder = new(Translation.ScpSwapLog, "player", ev.Player) - { - CustomReplacers = customReplacers - }; - - channel.SendMessage(builder); + Translation.ScpSwapLog.SendToChannel(channel, builder); return; } @@ -65,13 +61,8 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) { Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, Config.GuildId)); } - - builder = new(Translation.RoleChangeLog, "player", ev.Player) - { - CustomReplacers = customReplacers - }; - channel.SendMessage(builder); + Translation.RoleChangeLog.SendToChannel(channel, builder); } public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) @@ -89,13 +80,15 @@ public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) return; } - TranslationBuilder builder = new(isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn) + MessageContent content = isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn; + + TranslationBuilder builder = new() { PlayerListItem = Translation.PlayerListItem, PlayerList = ev.Players }; - channel.SendMessage(builder); + content.SendToChannel(channel, builder); } public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) @@ -109,12 +102,11 @@ public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.Cuffed); - - builder.AddPlayer("target", ev.Target); - builder.AddPlayer("player", ev.Player); - - channel.SendMessage(builder); + TranslationBuilder builder = new TranslationBuilder() + .AddPlayer("target", ev.Target) + .AddPlayer("player", ev.Player); + + Translation.Cuffed.SendToChannel(channel, builder); } public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) @@ -128,12 +120,11 @@ public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.Uncuffed); - - builder.AddPlayer("target", ev.Target); - builder.AddPlayer("player", ev.Player); + TranslationBuilder builder = new TranslationBuilder() + .AddPlayer("target", ev.Target) + .AddPlayer("player", ev.Player); - channel.SendMessage(builder); + Translation.Uncuffed.SendToChannel(channel, builder); } public override void OnServerRoundStarted() @@ -146,10 +137,8 @@ public override void OnServerRoundStarted() Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, Config.GuildId)); return; } - - TranslationBuilder builder = new(Translation.RoundStart); - channel.SendMessage(builder); + Translation.RoundStart.SendToChannel(channel, new()); } public override void OnServerRoundEnded(RoundEndedEventArgs ev) @@ -163,11 +152,10 @@ public override void OnServerRoundEnded(RoundEndedEventArgs ev) return; } - TranslationBuilder builder = new(Translation.RoundEnd); - - builder.CustomReplacers.Add("winner", () => ev.LeadingTeam.ToString()); + TranslationBuilder builder = new TranslationBuilder() + .AddCustomReplacer("winner", ev.LeadingTeam.ToString()); - channel.SendMessage(builder); + Translation.RoundEnd.SendToChannel(channel, builder); } public override void OnServerLczDecontaminationStarted() @@ -181,6 +169,6 @@ public override void OnServerLczDecontaminationStarted() return; } - channel.SendMessage(new TranslationBuilder(Translation.Decontamination)); + Translation.Decontamination.SendToChannel(channel, new()); } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Translation.cs b/DiscordLab.RoundLogs/Translation.cs index 0f11f43..c03a252 100644 --- a/DiscordLab.RoundLogs/Translation.cs +++ b/DiscordLab.RoundLogs/Translation.cs @@ -1,27 +1,28 @@ using System.ComponentModel; +using DiscordLab.Bot.API.Features; namespace DiscordLab.RoundLogs; public class Translation { - public string ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; + public MessageContent ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; - public string RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; + public MessageContent RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; - public string NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; + public MessageContent NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; - public string ChaosSpawn { get; set; } = "Chaos has respawned with the following player:\n{players}"; + public MessageContent ChaosSpawn { get; set; } = "Chaos has respawned with the following player:\n{players}"; public string PlayerListItem { get; set; } = "- {player}"; - public string Cuffed { get; set; } = "Player {target} has been cuffed by {player}"; + public MessageContent Cuffed { get; set; } = "Player {target} has been cuffed by {player}"; - public string Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; + public MessageContent Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; [Description("This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] - public string RoundStart { get; set; } = "Round has started."; + public MessageContent RoundStart { get; set; } = "Round has started."; - public string RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; + public MessageContent RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; - public string Decontamination { get; set; } = "Decontamination has begun."; + public MessageContent Decontamination { get; set; } = "Decontamination has begun."; } \ No newline at end of file From 3775bbd483a5300754eea99a03a7ab8eccf6e051 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 13:55:31 +0100 Subject: [PATCH 40/68] feat: get option extension --- DiscordLab.Administration/Commands/SendCommand.cs | 3 ++- DiscordLab.Bot/API/Extensions/DiscordExtensions.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index be59aa5..8a6eec2 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -1,6 +1,7 @@ using CommandSystem; using Discord; using Discord.WebSocket; +using DiscordLab.Bot.API.Extensions; using DiscordLab.Bot.API.Features; using LabApi.Features.Wrappers; using RemoteAdmin; @@ -36,7 +37,7 @@ public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); - string response = Server.RunCommand((string)command.Data.Options.First().Value); + string response = Server.RunCommand(command.Data.Options.GetOption(Translation.SendCommandOptionName)); TranslationBuilder builder = new TranslationBuilder() .AddCustomReplacer("response", response); diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs index 59fca3e..6b5f103 100644 --- a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs @@ -19,4 +19,15 @@ public static class DiscordExtensions /// Text, embed or embeds is required here. public static void SendMessage(this SocketTextChannel channel, string text = null, bool isTts = false, Embed embed = null, Embed[] embeds = null) => Task.Run(async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false)); + + /// + /// Gets an option from a list of slash command options. + /// + /// The options to check from. + /// The option name to get. + /// The type that this option should return. + /// The found item, if any. + public static T GetOption(this IReadOnlyCollection options, string name) + where T : class => + options.FirstOrDefault(option => option.Name == name) as T; } \ No newline at end of file From 26ab4dfd146f952b57d97db69edffcec48297b0e Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 14:04:49 +0100 Subject: [PATCH 41/68] fix: not getting option value --- DiscordLab.Bot/API/Extensions/DiscordExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs index 6b5f103..101e5ce 100644 --- a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs @@ -27,7 +27,7 @@ public static void SendMessage(this SocketTextChannel channel, string text = nul /// The option name to get. /// The type that this option should return. /// The found item, if any. - public static T GetOption(this IReadOnlyCollection options, string name) + public static T GetOption(this IReadOnlyCollection options, string name) where T : class => - options.FirstOrDefault(option => option.Name == name) as T; + options.FirstOrDefault(option => option.Name == name)?.Value as T; } \ No newline at end of file From 293a8b6f582a379b29ad215a33fb09ca4f6854f5 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 16:15:02 +0100 Subject: [PATCH 42/68] feat!: nullable --- .../Commands/SendCommand.cs | 2 +- .../API/Attributes/CallOnLoadAttribute.cs | 2 +- .../API/Attributes/CallOnReadyAttribute.cs | 4 +-- .../API/Attributes/CallOnUnloadAttribute.cs | 2 +- .../API/Extensions/DiscordExtensions.cs | 4 +-- .../API/Features/Embed/EmbedBuilder.cs | 2 +- DiscordLab.Bot/API/Features/MessageContent.cs | 19 ++++++------ DiscordLab.Bot/API/Features/Queue.cs | 6 ++-- DiscordLab.Bot/API/Features/SlashCommand.cs | 11 +++---- .../API/Features/TranslationBuilder.cs | 15 ++++++---- DiscordLab.Bot/API/Updates/GitHubRelease.cs | 6 ++++ .../API/Updates/GitHubReleaseAsset.cs | 2 ++ DiscordLab.Bot/API/Updates/Module.cs | 4 +-- DiscordLab.Bot/API/Updates/Updater.cs | 4 +-- DiscordLab.Bot/API/Utilities/CommandUtils.cs | 6 ++-- DiscordLab.Bot/Client.cs | 29 +++++++++++++------ DiscordLab.Bot/Commands/DiscordCommand.cs | 4 +-- DiscordLab.Bot/Commands/LocalAdminCommand.cs | 4 +-- DiscordLab.Bot/Patches/RestClientCreate.cs | 6 ++-- DiscordLab.Bot/Plugin.cs | 8 ++--- DiscordLab.Moderation/Commands/Ban.cs | 2 +- DiscordLab.Moderation/Commands/Mute.cs | 2 +- DiscordLab.Moderation/Commands/Unban.cs | 2 +- DiscordLab.Moderation/Commands/Unmute.cs | 2 +- DiscordLab.StatusChannel/Command.cs | 2 +- 25 files changed, 86 insertions(+), 64 deletions(-) diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index 8a6eec2..64f900b 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -31,7 +31,7 @@ public class SendCommand : AutocompleteCommand ] }; - public override ulong GuildId { get; } = Config.GuildId; + protected override ulong GuildId { get; } = Config.GuildId; public override async Task Run(SocketSlashCommand command) { diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs index 32279c3..d3f94fe 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs @@ -13,7 +13,7 @@ public class CallOnLoadAttribute : Attribute /// Find all attributes in your plugin and calls them. /// /// The assembly you wish to check, defaults to the current one. - public static void Load(Assembly assembly = null) + public static void Load(Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs index 9334f71..f5164c1 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs @@ -15,7 +15,7 @@ public class CallOnReadyAttribute : Attribute /// Locates all 's in your plugin and prepares them to be called. /// /// The assembly you wish to check, defaults to the current one. - public static void Load(Assembly assembly = null) + public static void Load(Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); @@ -51,6 +51,6 @@ internal static void Ready() } } - instances = null; + instances.Clear(); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs index 36e9f25..d0ac478 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs @@ -13,7 +13,7 @@ public class CallOnUnloadAttribute : Attribute /// Find all attributes in your plugin and calls them. /// /// The assembly you wish to check, defaults to the current one. - public static void Unload(Assembly assembly = null) + public static void Unload(Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); diff --git a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs index 101e5ce..b50223e 100644 --- a/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs +++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs @@ -17,7 +17,7 @@ public static class DiscordExtensions /// The embed. /// The embeds. /// Text, embed or embeds is required here. - public static void SendMessage(this SocketTextChannel channel, string text = null, bool isTts = false, Embed embed = null, Embed[] embeds = null) => + public static void SendMessage(this SocketTextChannel channel, string? text = null, bool isTts = false, Embed? embed = null, Embed[]? embeds = null) => Task.Run(async () => await channel.SendMessageAsync(text, isTts, embed, embeds: embeds).ConfigureAwait(false)); /// @@ -27,7 +27,7 @@ public static void SendMessage(this SocketTextChannel channel, string text = nul /// The option name to get. /// The type that this option should return. /// The found item, if any. - public static T GetOption(this IReadOnlyCollection options, string name) + public static T? GetOption(this IReadOnlyCollection options, string name) where T : class => options.FirstOrDefault(option => option.Name == name)?.Value as T; } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index 151e15e..c867fdb 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -37,7 +37,7 @@ public IEnumerable Fields /// /// Gets or sets the color of the embed. In string so #, 0x or the raw hex value will work. /// - public string Color + public string? Color { get => Builder.Color?.ToString(); set diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs index f1f1571..f288af0 100644 --- a/DiscordLab.Bot/API/Features/MessageContent.cs +++ b/DiscordLab.Bot/API/Features/MessageContent.cs @@ -1,5 +1,4 @@ // ReSharper disable MemberCanBePrivate.Global - namespace DiscordLab.Bot.API.Features; using Discord.Rest; @@ -16,13 +15,13 @@ public class MessageContent /// Gets or sets the embed to send, if any. /// [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] - public Embed.EmbedBuilder Embed { get; set; } + public Embed.EmbedBuilder? Embed { get; set; } /// /// Gets or sets the string to send, if any. /// [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] - public string Message { get; set; } + public string? Message { get; set; } /// /// Converts an embed into a instance. @@ -48,7 +47,7 @@ public void SendToChannel(SocketTextChannel channel, TranslationBuilder builder) if (Embed == null && Message == null) throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined."); - (Discord.Embed embed, string content) = Build(builder); + (Discord.Embed? embed, string? content) = Build(builder); channel.SendMessage(content, embed: embed); } @@ -64,7 +63,7 @@ public async Task SendToChannelAsync(SocketTextChannel channel, if (Embed == null && Message == null) throw new ArgumentNullException($"A message failed to send to {channel.Name} ({channel.Id}) because both embed and message contents were undefined."); - (Discord.Embed embed, string content) = Build(builder); + (Discord.Embed? embed, string? content) = Build(builder); return await channel.SendMessageAsync(content, embed: embed); } @@ -79,7 +78,7 @@ public void ModifyMessage(Discord.IUserMessage message, TranslationBuilder build if (Embed == null && Message == null) throw new ArgumentNullException($"Message {message.Id} (in #{message.Channel.Name} ({message.Channel.Id})) failed to be edited because both embed and message contents were undefined."); - (Discord.Embed embed, string content) = Build(builder); + (Discord.Embed? embed, string? content) = Build(builder); Task.Run(async () => await message.ModifyAsync(msg => { @@ -102,7 +101,7 @@ public async Task InteractionRespond(SocketCommandBase command, TranslationBuild if (Embed == null && Message == null) throw new ArgumentNullException($"Failed to respond to command {command.CommandName} because both embed and message contents were undefined."); - (Discord.Embed embed, string content) = Build(builder); + (Discord.Embed? embed, string? content) = Build(builder); await command.RespondAsync(content, embed: embed); } @@ -118,7 +117,7 @@ public async Task ModifyInteraction(SocketCommandBase command, TranslationBuilde if (Embed == null && Message == null) throw new ArgumentNullException($"Failed to modify command {command.CommandName}'s response because both embed and message contents were undefined."); - (Discord.Embed embed, string content) = Build(builder); + (Discord.Embed? embed, string? content) = Build(builder); await command.ModifyOriginalResponseAsync(msg => { @@ -130,7 +129,7 @@ await command.ModifyOriginalResponseAsync(msg => }); } - private (Discord.Embed Embed, string Content) Build(TranslationBuilder builder) + private (Discord.Embed? Embed, string? Content) Build(TranslationBuilder builder) { if (Embed == null) return (null, Message != null ? builder.Build(Message) : null); @@ -144,6 +143,6 @@ await command.ModifyOriginalResponseAsync(msg => field.Value = builder.Build((string)field.Value); } - return (embed?.Build(), Message != null ? builder.Build(Message) : null); + return (embed.Build(), Message != null ? builder.Build(Message) : null); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Queue.cs b/DiscordLab.Bot/API/Features/Queue.cs index f118fed..02275bf 100644 --- a/DiscordLab.Bot/API/Features/Queue.cs +++ b/DiscordLab.Bot/API/Features/Queue.cs @@ -12,7 +12,7 @@ public class Queue /// /// The duration you wish to wait before calling the method. /// The default action to run. Can be null. - public Queue(float duration, Action defaultAction = null) + public Queue(float duration, Action? defaultAction = null) { Duration = duration; DefaultAction = defaultAction; @@ -31,13 +31,13 @@ public Queue(float duration, Action defaultAction = null) /// /// Gets the action to be run during when an action isn't provided. Can be null. /// - public Action DefaultAction { get; } + public Action? DefaultAction { get; } /// /// Runs the queue process, either using the action parameter or default action. /// /// The action to run. Defaults to . - public void Process(Action action = null) + public void Process(Action? action = null) { if (IsBusy) return; diff --git a/DiscordLab.Bot/API/Features/SlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs index 52eabad..7519072 100644 --- a/DiscordLab.Bot/API/Features/SlashCommand.cs +++ b/DiscordLab.Bot/API/Features/SlashCommand.cs @@ -28,13 +28,13 @@ public abstract class SlashCommand /// /// Gets the guild ID to assign this command to. /// - public abstract ulong GuildId { get; } + protected abstract ulong GuildId { get; } /// /// Finds and creates all slash commands in your plugin. There is no method to delete all your commands, as that is handled by the bot itself. /// /// The assembly you wish to check, defaults to the current one. - public static void FindAll(Assembly assembly = null) + public static void FindAll(Assembly? assembly = null) { assembly ??= Assembly.GetCallingAssembly(); @@ -43,7 +43,8 @@ public static void FindAll(Assembly assembly = null) if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type)) continue; - SlashCommand init = Activator.CreateInstance(type) as SlashCommand; + if (Activator.CreateInstance(type) is not SlashCommand init) + continue; Commands.Add(init); } } @@ -65,7 +66,7 @@ private static void Start() private static void Unload() { Commands.CollectionChanged -= OnCollectionChanged; - Commands = null; + Commands.Clear(); } [CallOnReady] @@ -90,7 +91,7 @@ private static async Task RegisterGuildCommands(IEnumerable comman { foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId)) { - SocketGuild guild = Client.GetGuild(cmds.Key); + SocketGuild? guild = Client.GetGuild(cmds.Key); if (guild == null) { Logger.Warn($"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 2a7a85f..8fcc1b6 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -176,13 +176,13 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// /// Gets or sets the translation. /// - public string Translation { get; set; } + public string? Translation { get; set; } /// /// Gets or sets the item that will show for each player when the {players} placeholder is used. Defaults to null. /// /// If you want the {players} placeholder to not work, set this to null. - public string PlayerListItem { get; set; } + public string? PlayerListItem { get; set; } /// /// Gets or sets the separator between items in . @@ -192,7 +192,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// /// Gets or sets the player list that will be used for . /// - public IEnumerable PlayerList { get; set; } + public IEnumerable? PlayerList { get; set; } /// /// . @@ -269,7 +269,7 @@ public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) /// /// The translation to build from, isn't needed if is defined. /// The translation built. - public string Build(string translation = null) + public string Build(string? translation = null) { translation ??= Translation; @@ -279,7 +279,7 @@ public string Build(string translation = null) if (PlayerListItem != null) SetupPlayerList(); - string returnTranslation = Translation; + string returnTranslation = translation!; foreach (KeyValuePair> replacer in CustomReplacers) { @@ -364,6 +364,9 @@ private static string GetRemainingDecontaminationTime() => Mathf.Min( private void SetupPlayerList() { + if (string.IsNullOrEmpty(PlayerListItem)) + throw new ArgumentException($"Invalid {nameof(PlayerListItem)} provided, it was either null or empty."); + Player[] readyPlayers = (PlayerList ?? Player.ReadyList).ToArray(); int length = readyPlayers.Length; @@ -374,7 +377,7 @@ private void SetupPlayerList() for (int i = 0; i < length; i++) { string playerKey = $"player{i}"; - playerItems.Add(PlayerListItem.Replace("{player", "{" + $"{playerKey}")); + playerItems.Add(PlayerListItem!.Replace("{player", "{" + $"{playerKey}")); playerDictionary[playerKey] = readyPlayers[i]; } diff --git a/DiscordLab.Bot/API/Updates/GitHubRelease.cs b/DiscordLab.Bot/API/Updates/GitHubRelease.cs index 496d061..ef11b3d 100644 --- a/DiscordLab.Bot/API/Updates/GitHubRelease.cs +++ b/DiscordLab.Bot/API/Updates/GitHubRelease.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace DiscordLab.Bot.API.Updates; using System.Text.Json.Serialization; @@ -13,12 +15,16 @@ public class GitHubRelease [JsonPropertyName("tag_name")] public string TagName { get; set; } + // ReSharper disable CollectionNeverUpdated.Global + /// /// Gets or sets the assets for this release. /// [JsonPropertyName("assets")] public List Assets { get; set; } + // ReSharper restore CollectionNeverUpdated.Global + /// /// Gets or sets a value indicating whether this is a prerelease release. /// diff --git a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs index 2d57a98..4352479 100644 --- a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs +++ b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs @@ -1,3 +1,5 @@ +#nullable disable + namespace DiscordLab.Bot.API.Updates; using System.Text.Json.Serialization; diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Bot/API/Updates/Module.cs index 1afa2db..4416bdf 100644 --- a/DiscordLab.Bot/API/Updates/Module.cs +++ b/DiscordLab.Bot/API/Updates/Module.cs @@ -40,7 +40,7 @@ public Module(GitHubRelease release, GitHubReleaseAsset asset) /// /// Gets the plugin of this module, null if module is not installed. /// - public LabApi.Loader.Features.Plugins.Plugin ExistingPlugin { get; } + public LabApi.Loader.Features.Plugins.Plugin? ExistingPlugin { get; } /// /// Gets the release this module comes from. @@ -59,7 +59,7 @@ public Module(GitHubRelease release, GitHubReleaseAsset asset) /// The generated string. public static string GenerateUpdateString(IEnumerable modules) => string.Join( "\n- ", modules.Select(module => - $"{module.Name} | Current Version: {module.ExistingPlugin.Version} | Latest Version: {module.Version}")); + $"{module.Name} | Current Version: {module.ExistingPlugin!.Version} | Latest Version: {module.Version}")); /// /// Downloads this module. diff --git a/DiscordLab.Bot/API/Updates/Updater.cs b/DiscordLab.Bot/API/Updates/Updater.cs index 2506268..4670051 100644 --- a/DiscordLab.Bot/API/Updates/Updater.cs +++ b/DiscordLab.Bot/API/Updates/Updater.cs @@ -122,7 +122,7 @@ private static void Setup() [CallOnUnload] private static void Disable() { - Client = null; - DownloadClient = null; + Client.Dispose(); + DownloadClient.Dispose(); } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs index 27ec6f0..a47d9e8 100644 --- a/DiscordLab.Bot/API/Utilities/CommandUtils.cs +++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs @@ -12,9 +12,9 @@ public static class CommandUtils /// /// The ID to check. /// The player if found. - public static Player GetPlayerFromUnparsed(string id) + public static Player? GetPlayerFromUnparsed(string id) { - return TryGetPlayerFromUnparsed(id, out Player player) ? player : null; + return TryGetPlayerFromUnparsed(id, out Player? player) ? player : null; } /// @@ -23,7 +23,7 @@ public static Player GetPlayerFromUnparsed(string id) /// The ID to check. /// The player if found. /// Whether the player was found. - public static bool TryGetPlayerFromUnparsed(string id, out Player player) + public static bool TryGetPlayerFromUnparsed(string id, out Player? player) { if (int.TryParse(id, out int intId)) { diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs index 17fd937..f46d3b2 100644 --- a/DiscordLab.Bot/Client.cs +++ b/DiscordLab.Bot/Client.cs @@ -1,3 +1,4 @@ +// ReSharper disable MemberCanBePrivate.Global namespace DiscordLab.Bot; using System.Net; @@ -18,7 +19,7 @@ public static class Client /// /// Gets the websocket client for the Discord bot. /// - public static DiscordSocketClient SocketClient { get; private set; } + public static DiscordSocketClient SocketClient { get; private set; } = null!; /// /// Gets a value indicating whether the client is in the ready state. @@ -33,7 +34,7 @@ public static class Client /// /// Gets the default guild for the plugin. /// - public static SocketGuild DefaultGuild { get; private set; } + public static SocketGuild? DefaultGuild { get; private set; } private static Config Config => Plugin.Instance.Config; @@ -42,7 +43,7 @@ public static class Client /// /// The guild ID. /// If the ID is 0, then the default guild (if it exists), if else then it will return the found guild, or null. - public static SocketGuild GetGuild(ulong id) + public static SocketGuild? GetGuild(ulong id) { return id == 0 ? DefaultGuild : SocketClient.GetGuild(id); } @@ -52,8 +53,18 @@ public static SocketGuild GetGuild(ulong id) /// /// The ID of the channel. /// The channel, if found. - public static SocketTextChannel GetOrAddChannel(ulong id) => - SavedTextChannels.GetOrAdd(id, () => SocketClient.GetChannel(id) as SocketTextChannel); + public static SocketTextChannel? GetOrAddChannel(ulong id) + { + if (SavedTextChannels.TryGetValue(id, out SocketTextChannel ret)) + return ret; + + SocketChannel channel = SocketClient.GetChannel(id); + if (channel is not SocketTextChannel text) + return null; + + SavedTextChannels.Add(id, text); + return text; + } /// /// Tries to get or add a channel via its ID. Uses cache. @@ -61,7 +72,7 @@ public static SocketTextChannel GetOrAddChannel(ulong id) => /// The ID of the channel. /// The channel, if found. /// Whether the channel was found. - public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) + public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel? channel) { channel = GetOrAddChannel(id); return channel != null; @@ -112,7 +123,7 @@ internal static void Start() [CallOnUnload] internal static void Disable() { - SavedTextChannels = null; + SavedTextChannels.Clear(); SocketClient.Log -= OnLog; SocketClient.Ready -= OnReady; @@ -169,7 +180,7 @@ private static Task OnReady() private static Task SlashCommandHandler(SocketSlashCommand command) { DebugLog($"{command.Data.Name} requested a response, finding the command..."); - SlashCommand cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); + SlashCommand? cmd = SlashCommand.Commands.FirstOrDefault(c => c.Data.Name == command.Data.Name); cmd?.Run(command); return Task.CompletedTask; @@ -178,7 +189,7 @@ private static Task SlashCommandHandler(SocketSlashCommand command) private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) { DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); - AutocompleteCommand command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; + AutocompleteCommand? command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; command?.Autocomplete(autocomplete); return Task.CompletedTask; diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index 6889130..2e81704 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -53,7 +53,7 @@ public class DiscordCommand : AutocompleteCommand }; /// - public override ulong GuildId { get; } = 0; + protected override ulong GuildId { get; } = 0; /// public override async Task Run(SocketSlashCommand command) @@ -78,7 +78,7 @@ public override async Task Run(SocketSlashCommand command) return; } - Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + Module? module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); if (module == null) { await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs index 04b5eda..bddd9c3 100644 --- a/DiscordLab.Bot/Commands/LocalAdminCommand.cs +++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs @@ -31,14 +31,14 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, [Unsc case "install": { - string moduleName = arguments.ElementAtOrDefault(1); + string? moduleName = arguments.ElementAtOrDefault(1); if (string.IsNullOrWhiteSpace(moduleName)) { response = "Please provide a module name."; return false; } - Module module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + Module? module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); if (module == null) { response = "Module not found."; diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index bc5da3e..65de9c9 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -18,14 +18,14 @@ public static class RestClientCreate /// Gets the target method to patch. /// /// The method. - public static MethodBase TargetMethod() + public static MethodBase? TargetMethod() { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { - Type type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); + Type? type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); if (type == null) continue; - ConstructorInfo constructor = type.GetConstructors().FirstOrDefault(); + ConstructorInfo? constructor = type.GetConstructors().FirstOrDefault(); if (constructor != null) return constructor; } diff --git a/DiscordLab.Bot/Plugin.cs b/DiscordLab.Bot/Plugin.cs index d22153d..e21813f 100644 --- a/DiscordLab.Bot/Plugin.cs +++ b/DiscordLab.Bot/Plugin.cs @@ -15,7 +15,7 @@ public sealed class Plugin : Plugin /// /// Gets the current instance of this plugin. /// - public static Plugin Instance { get; private set; } + public static Plugin Instance { get; private set; } = null!; /// public override string Name { get; } = "DiscordLab"; @@ -38,7 +38,7 @@ public sealed class Plugin : Plugin /// /// Gets the current config for the plugin. /// - public new Config Config { get; private set; } + public new Config Config { get; private set; } = null!; private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}"); @@ -73,7 +73,7 @@ public override void Disable() CallOnUnloadAttribute.Unload(); - Config = null; - Instance = null; + Config = null!; + Instance = null!; } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index e203ad0..1f2e6c5 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -41,7 +41,7 @@ public class Ban : AutocompleteCommand ] }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; public override async Task Run(SocketSlashCommand command) { diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index e559f10..758cb2a 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -35,7 +35,7 @@ public class Mute : AutocompleteCommand ] }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; public override async Task Run(SocketSlashCommand command) { diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index 25b9b6c..d862615 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -25,7 +25,7 @@ public class Unban : AutocompleteCommand ] }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; public override async Task Run(SocketSlashCommand command) { diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs index d3df885..1e05449 100644 --- a/DiscordLab.Moderation/Commands/Unmute.cs +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -27,7 +27,7 @@ public class Unmute : AutocompleteCommand ] }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; public override async Task Run(SocketSlashCommand command) { diff --git a/DiscordLab.StatusChannel/Command.cs b/DiscordLab.StatusChannel/Command.cs index 930392c..8e6ed95 100644 --- a/DiscordLab.StatusChannel/Command.cs +++ b/DiscordLab.StatusChannel/Command.cs @@ -12,7 +12,7 @@ public class Command : SlashCommand Description = Plugin.Instance.Translation.PlayerListCommandDescription, }; - public override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; + protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; public override async Task Run(SocketSlashCommand command) { From 1dd238175cd9772edb4142aeab8af7753e63e217 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 16:15:48 +0100 Subject: [PATCH 43/68] feat: enable nullable --- DiscordLab.Bot/DiscordLab.Bot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 04c79a6..c17f5e1 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -2,8 +2,8 @@ net48 enable - disable 13 + enable x64 true 2.0.0 From df02f7cd636d271792f3be306958455a32be8ee5 Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 19:59:18 +0100 Subject: [PATCH 44/68] fix: nullables --- DiscordLab.Bot/API/Utilities/CommandUtils.cs | 21 ++++---------------- DiscordLab.Bot/Client.cs | 6 +++++- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/DiscordLab.Bot/API/Utilities/CommandUtils.cs b/DiscordLab.Bot/API/Utilities/CommandUtils.cs index a47d9e8..0511e07 100644 --- a/DiscordLab.Bot/API/Utilities/CommandUtils.cs +++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs @@ -17,29 +17,16 @@ public static class CommandUtils return TryGetPlayerFromUnparsed(id, out Player? player) ? player : null; } +#nullable disable /// /// Tries to get a player from an unparsed string id, will check if or . /// /// The ID to check. /// The player if found. /// Whether the player was found. - public static bool TryGetPlayerFromUnparsed(string id, out Player? player) + public static bool TryGetPlayerFromUnparsed(string id, out Player player) { - if (int.TryParse(id, out int intId)) - { - if (!Player.TryGet(intId, out player)) - { - return false; - } - } - else - { - if (!Player.TryGet(id, out player)) - { - return false; - } - } - - return true; + player = int.TryParse(id, out int intId) ? Player.Get(intId) : Player.Get(id); + return player != null; } } \ No newline at end of file diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs index f46d3b2..c1498ad 100644 --- a/DiscordLab.Bot/Client.cs +++ b/DiscordLab.Bot/Client.cs @@ -1,4 +1,5 @@ // ReSharper disable MemberCanBePrivate.Global + namespace DiscordLab.Bot; using System.Net; @@ -66,17 +67,20 @@ public static class Client return text; } +#nullable disable /// /// Tries to get or add a channel via its ID. Uses cache. /// /// The ID of the channel. /// The channel, if found. /// Whether the channel was found. - public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel? channel) + public static bool TryGetOrAddChannel(ulong id, out SocketTextChannel channel) { channel = GetOrAddChannel(id); + return channel != null; } +#nullable restore /// /// Starts the bot. From 213785d0fbe155a695c5198aaaf71f59f5ee1f3d Mon Sep 17 00:00:00 2001 From: Lumi Date: Tue, 19 Aug 2025 19:59:31 +0100 Subject: [PATCH 45/68] fix: dates crashing servers --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 8fcc1b6..e517aee 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -122,8 +122,8 @@ public TranslationBuilder(string translation, string playerPrefix, Player player ["timer"] = time => $"", ["elapsedtimerelative"] = time => $"", ["roundstart"] = time => $"", - ["secondssince"] = time => new DateTimeOffset(time, Round.Duration).Second.ToString(), - ["minutessince"] = time => new DateTimeOffset(time, Round.Duration).Minute.ToString(), + ["secondssince"] = time => TimeSince(time).Seconds.ToString(CultureInfo.InvariantCulture), + ["minutessince"] = time => TimeSince(time).Minutes.ToString(CultureInfo.InvariantCulture), }; /// @@ -300,6 +300,7 @@ public string Build(string? translation = null) } long unix = new DateTimeOffset(Time).ToUnixTimeSeconds(); + foreach (KeyValuePair> replacer in TimeReplacers) { returnTranslation = Regex.Replace( @@ -362,6 +363,9 @@ private static string GetRemainingDecontaminationTime() => Mathf.Min( .ToString(CultureInfo.InvariantCulture); #pragma warning restore SA1118 + private static TimeSpan TimeSince(long time) => + Round.Duration - (DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds(time)); + private void SetupPlayerList() { if (string.IsNullOrEmpty(PlayerListItem)) From 9439665395806c240652f8d736fd52b743afd838 Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 08:07:31 +0100 Subject: [PATCH 46/68] fix: versioning --- DiscordLab.Administration/Plugin.cs | 2 +- DiscordLab.Bot/Plugin.cs | 2 +- DiscordLab.BotStatus/Plugin.cs | 2 +- DiscordLab.ConnectionLogs/Plugin.cs | 2 +- DiscordLab.DeathLogs/Plugin.cs | 2 +- DiscordLab.Moderation/Plugin.cs | 2 +- DiscordLab.RoundLogs/Plugin.cs | 2 +- DiscordLab.StatusChannel/Plugin.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs index 5bcb4cd..c232562 100644 --- a/DiscordLab.Administration/Plugin.cs +++ b/DiscordLab.Administration/Plugin.cs @@ -17,7 +17,7 @@ public class Plugin : Plugin "Allows you to log or do administrative actions from your Discord bot"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.Bot/Plugin.cs b/DiscordLab.Bot/Plugin.cs index e21813f..e142585 100644 --- a/DiscordLab.Bot/Plugin.cs +++ b/DiscordLab.Bot/Plugin.cs @@ -27,7 +27,7 @@ public sealed class Plugin : Plugin public override string Author { get; } = "LumiFae"; /// - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version => GetType().Assembly.GetName().Version; /// public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index a38f770..59a2cbd 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -17,7 +17,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.BotStatus"; public override string Description { get; } = "Allows your bot's status to update with player counts."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public override void Enable() diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index baecf4a..5e3eaf1 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -12,7 +12,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.ConnectionLogs"; public override string Description { get; } = "Adds logging for connection based information"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index 526150a..10ef9fd 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -11,7 +11,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.DeathLogs"; public override string Description { get; } = "Adds death logging capabilities"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public override void Enable() diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index 4e30c6d..f87c1de 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -18,7 +18,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.Moderation"; public override string Description { get; } = "Adds logging and commands for moderation based operations"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public TempMuteConfig MuteConfig; diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index 11639f8..4b08e19 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -13,7 +13,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.RoundLogs"; public override string Description { get; } = "Allows you to log specific details about the round."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index 85398ad..16be7c5 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -18,7 +18,7 @@ public class Plugin : Plugin "Allows you to update/send a status message in a specific channel and have it update automatically."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = typeof(Plugin).Assembly.GetName().Version; + public override Version Version { get; } = new(2, 0, 0); public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public MessageConfig MessageConfig { get; set; } From 330561fa5768a8fe05a9d35a32b5cf3a44b63c6a Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 09:19:27 +0100 Subject: [PATCH 47/68] ci: nuget package push --- .github/workflows/release.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23e017c..50f6673 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,34 @@ on: types: [published] jobs: + nuget: + if: github.event.release.prerelease == false + runs-on: windows-2022 + env: + EXILED_REFERENCES_URL: https://exmod-team.github.io/SL-References/Dev.zip + SL_REFERENCES: ${{ github.workspace }}/refs + EXILED_REFERENCES: ${{ github.workspace }}/refs + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Dotnet + uses: actions/setup-dotnet@v4.0.1 + with: + dotnet-version: 9.0.x + - name: Get References + shell: pwsh + run: | + Invoke-WebRequest -Uri ${{ env.EXILED_REFERENCES_URL }} -OutFile ${{ github.workspace }}/References.zip + Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force + - name: Rename Assembly-CSharp (because Exiled removes the normal version) + shell: pwsh + run: Move-Item -Path ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll -Destination ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll + - name: Build + run: dotnet build -c:Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj + - name: Publish + run: dotnet nuget push ${{ github.workspace }}/DiscordLab.Bot/bin/Release/DiscordLab.Bot.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} notify: if: github.event.release.prerelease == false runs-on: ubuntu-latest From 2c4c5d64d696ee4c81845c85424569ebbd27324e Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 09:46:34 +0100 Subject: [PATCH 48/68] fix: versioning (final) --- Directory.Build.props | 1 + DiscordLab.Administration/DiscordLab.Administration.csproj | 1 - DiscordLab.Administration/Plugin.cs | 2 +- DiscordLab.BotStatus/DiscordLab.BotStatus.csproj | 1 - DiscordLab.BotStatus/Plugin.cs | 2 +- DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj | 1 - DiscordLab.ConnectionLogs/Plugin.cs | 2 +- DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj | 1 - DiscordLab.DeathLogs/Plugin.cs | 2 +- DiscordLab.Dependency/DiscordLab.Dependency.csproj | 1 - DiscordLab.Moderation/DiscordLab.Moderation.csproj | 1 - DiscordLab.Moderation/Plugin.cs | 2 +- DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj | 1 - DiscordLab.RoundLogs/Plugin.cs | 2 +- DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj | 1 - DiscordLab.StatusChannel/Plugin.cs | 2 +- 16 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index df9990f..f4783cd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@  + false $(MSBuildThisFileDirectory)bin\ diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj index 117357f..206d41c 100644 --- a/DiscordLab.Administration/DiscordLab.Administration.csproj +++ b/DiscordLab.Administration/DiscordLab.Administration.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs index c232562..9257a01 100644 --- a/DiscordLab.Administration/Plugin.cs +++ b/DiscordLab.Administration/Plugin.cs @@ -17,7 +17,7 @@ public class Plugin : Plugin "Allows you to log or do administrative actions from your Discord bot"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj index 124848c..57493f0 100644 --- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj +++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 59a2cbd..4e2ff79 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -17,7 +17,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.BotStatus"; public override string Description { get; } = "Allows your bot's status to update with player counts."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public override void Enable() diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj index b6fe025..c800f07 100644 --- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj +++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index 5e3eaf1..a5b329b 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -12,7 +12,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.ConnectionLogs"; public override string Description { get; } = "Adds logging for connection based information"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj index b6fe025..c800f07 100644 --- a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj +++ b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index 10ef9fd..4a898db 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -11,7 +11,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.DeathLogs"; public override string Description { get; } = "Adds death logging capabilities"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public override void Enable() diff --git a/DiscordLab.Dependency/DiscordLab.Dependency.csproj b/DiscordLab.Dependency/DiscordLab.Dependency.csproj index 3412dfa..236ea0b 100644 --- a/DiscordLab.Dependency/DiscordLab.Dependency.csproj +++ b/DiscordLab.Dependency/DiscordLab.Dependency.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 \ No newline at end of file diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index 7d33c6a..b09a6a8 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index f87c1de..6d7d5f1 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -18,7 +18,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.Moderation"; public override string Description { get; } = "Adds logging and commands for moderation based operations"; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public TempMuteConfig MuteConfig; diff --git a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj index b6fe025..c800f07 100644 --- a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj +++ b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index 4b08e19..b917052 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -13,7 +13,7 @@ public class Plugin : Plugin public override string Name { get; } = "DiscordLab.RoundLogs"; public override string Description { get; } = "Allows you to log specific details about the round."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index b174597..46ef3c0 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -6,7 +6,6 @@ 12 x64 true - false 2.0.0 diff --git a/DiscordLab.StatusChannel/Plugin.cs b/DiscordLab.StatusChannel/Plugin.cs index 16be7c5..a24d0cb 100644 --- a/DiscordLab.StatusChannel/Plugin.cs +++ b/DiscordLab.StatusChannel/Plugin.cs @@ -18,7 +18,7 @@ public class Plugin : Plugin "Allows you to update/send a status message in a specific channel and have it update automatically."; public override string Author { get; } = "LumiFae"; - public override Version Version { get; } = new(2, 0, 0); + public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public MessageConfig MessageConfig { get; set; } From ffa31034963dee8866119aeabf84ba7d0a915614 Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 14:24:54 +0100 Subject: [PATCH 49/68] fix: autocomplete commands --- .../Commands/SendCommand.cs | 8 ++-- DiscordLab.Bot/Commands/DiscordCommand.cs | 2 +- DiscordLab.Moderation/Commands/Ban.cs | 5 ++- DiscordLab.Moderation/Commands/Mute.cs | 5 ++- DiscordLab.Moderation/Commands/Unban.cs | 5 ++- DiscordLab.Moderation/Commands/Unmute.cs | 5 ++- DiscordLab.Moderation/Plugin.cs | 4 +- README.md | 37 +++++++++++++++++++ 8 files changed, 57 insertions(+), 14 deletions(-) diff --git a/DiscordLab.Administration/Commands/SendCommand.cs b/DiscordLab.Administration/Commands/SendCommand.cs index 64f900b..dbedc51 100644 --- a/DiscordLab.Administration/Commands/SendCommand.cs +++ b/DiscordLab.Administration/Commands/SendCommand.cs @@ -3,6 +3,7 @@ using Discord.WebSocket; using DiscordLab.Bot.API.Extensions; using DiscordLab.Bot.API.Features; +using LabApi.Features.Console; using LabApi.Features.Wrappers; using RemoteAdmin; @@ -26,7 +27,8 @@ public class SendCommand : AutocompleteCommand Name = Translation.SendCommandOptionName, Description = Translation.SendCommandOptionDescription, Type = ApplicationCommandOptionType.String, - IsRequired = true + IsRequired = true, + IsAutocomplete = true } ] }; @@ -37,7 +39,7 @@ public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); - string response = Server.RunCommand(command.Data.Options.GetOption(Translation.SendCommandOptionName)); + string response = Server.RunCommand(command.Data.Options.GetOption(Translation.SendCommandOptionName)!); TranslationBuilder builder = new TranslationBuilder() .AddCustomReplacer("response", response); @@ -52,6 +54,6 @@ public override async Task Autocomplete(SocketAutocompleteInteraction autocomple ..CommandProcessor.GetAllCommands().Select(x => "/" + x.Command), ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command) ]; - await autocomplete.RespondAsync(commands.Select(x => new AutocompleteResult(x, x))); + await autocomplete.RespondAsync(commands.Where(x => x.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult(x, x))); } } \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index 2e81704..1ee99a4 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -110,6 +110,6 @@ await command.ModifyOriginalResponseAsync(m => /// public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot").Select(x => new AutocompleteResult(x.Name, x.Name))); + await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult(x.Name, x.Name))); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index 1f2e6c5..40c7e1d 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -22,7 +22,8 @@ public class Ban : AutocompleteCommand Name = Translation.BanUserOptionName, Description = Translation.BanUserOptionDescription, Type = ApplicationCommandOptionType.String, - IsRequired = true + IsRequired = true, + IsAutocomplete = true }, new() { @@ -78,6 +79,6 @@ await command.ModifyOriginalResponseAsync(m => public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults(autocomplete.Data.Current.Value)); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index 758cb2a..8000295 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -23,7 +23,8 @@ public class Mute : AutocompleteCommand Name = Translation.MuteUserOptionName, Description = Translation.MuteUserOptionDescription, Type = ApplicationCommandOptionType.String, - IsRequired = true + IsRequired = true, + IsAutocomplete = true }, new() { @@ -75,6 +76,6 @@ public override async Task Run(SocketSlashCommand command) public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults(autocomplete.Data.Current.Value)); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index d862615..a7bc877 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -20,7 +20,8 @@ public class Unban : AutocompleteCommand Name = Translation.UnbanUserOptionName, Description = Translation.UnbanUserOptionDescription, Type = ApplicationCommandOptionType.String, - IsRequired = true + IsRequired = true, + IsAutocomplete = true }, ] }; @@ -51,6 +52,6 @@ public override async Task Autocomplete(SocketAutocompleteInteraction autocomple ..BanHandler.GetBans(BanHandler.BanType.UserId), ..BanHandler.GetBans(BanHandler.BanType.IP) ]; - await autocomplete.RespondAsync(response.Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); + await autocomplete.RespondAsync(response.Where(x => x.Id.Contains((string)autocomplete.Data.Current.Value) || x.OriginalName.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs index 1e05449..c88eeb4 100644 --- a/DiscordLab.Moderation/Commands/Unmute.cs +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -22,7 +22,8 @@ public class Unmute : AutocompleteCommand Name = Translation.UnbanUserOptionName, Description = Translation.UnbanUserOptionDescription, Type = ApplicationCommandOptionType.String, - IsRequired = true + IsRequired = true, + IsAutocomplete = true }, ] }; @@ -48,6 +49,6 @@ await command.ModifyOriginalResponseAsync(m => public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults); + await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults(autocomplete.Data.Current.Value)); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index 6d7d5f1..7420d85 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -58,6 +58,6 @@ public override void LoadConfigs() base.LoadConfigs(); } - public static IEnumerable PlayersAutocompleteResults => - Player.ReadyList.Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); + public static IEnumerable PlayersAutocompleteResults(object current) => + Player.ReadyList.Where(p => p.Nickname.Contains((string)current) || p.UserId.Contains((string)current) || (int.TryParse((string)current, out int id) && p.PlayerId == id)).Take(25).Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); } \ No newline at end of file diff --git a/README.md b/README.md index 45b2d18..d4539ab 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,43 @@ features you want on your Discord server/bot. This allows you to have a Discord To get started, check out our installation guide: https://discordlab.jxtq.moe/getting-started/installation/ +## Features + +### Modular + +DiscordLab easily allows users to only have a subset of features and doesn't require the server to be tracking loads of events that will never even be logged. +Your server (or other plugin) developers can easily integrate with the bot as well because of the [NuGet package](https://www.nuget.org/packages/DiscordLab). +Go to the [API section](#api) for more information. + +### Logging for events + +DiscordLab's modules track loads of different kinds of events and sends them directly to your Discord server. + +### Customisable messages + +DiscordLab has a feature that makes it so messages can easily be edited from their default to make it so you can have an embed, raw message content or both! + +There is also large variety of placeholders that can be used, which will then be replaced by DiscordLab before sending out the message. + +### Support for multiple servers/channels + +DiscordLab allows for you to put channel and guild IDs directly into configs that allows you to seperate logs however you want, +with each trackable event being assigned a separate channel config option. + +If you want damage logs and death logs to be in separate channels, they can be routed to different channels. + +### Commands + +Some DiscordLab modules come with slash commands (can also be disabled) that can be used within Discord, i.e. in `DiscordLab.Administration` there is the `/send` command that +allows admins to send commands to your server. + +*Slash commands on DiscordLab do not have their own permission system, commands that should be hidden are hidden behind some default permissions, if you wish to edit the +permissions, you can read up on how to on this [Discord blog post](https://discord.com/blog/slash-commands-permissions-discord-apps-bots)* + +### Moderation Utilities + +DiscordLab.Moderation comes with commands and utilities to better help with mutes, including adding the functionality of temporary mutes. Can be done via RA commands or Discord. + ## API You can find all information here: https://discordlab.jxtq.moe/api/ From 644c6eb8f58a700349d5032bfcda0567a2b25797 Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 14:25:10 +0100 Subject: [PATCH 50/68] fix: translation builder player list --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index e517aee..d41946b 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -276,7 +276,7 @@ public string Build(string? translation = null) if (string.IsNullOrEmpty(translation)) throw new ArgumentNullException($"{nameof(TranslationBuilder)} failed to build because of no valid translation."); - if (PlayerListItem != null) + if (PlayerListItem != null && !CustomReplacers.ContainsKey("players")) SetupPlayerList(); string returnTranslation = translation!; From f114bcfd07373a7b773b3829ea9c9c3496b869da Mon Sep 17 00:00:00 2001 From: Lumi Date: Wed, 20 Aug 2025 17:31:00 +0100 Subject: [PATCH 51/68] fix: module updater --- DiscordLab.Bot/API/Updates/GitHubRelease.cs | 10 +++++----- DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs | 6 +++--- DiscordLab.Bot/API/Updates/Updater.cs | 16 ++++++++++------ DiscordLab.Bot/Commands/DiscordCommand.cs | 4 ++-- DiscordLab.Bot/Commands/LocalAdminCommand.cs | 4 ++-- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/DiscordLab.Bot/API/Updates/GitHubRelease.cs b/DiscordLab.Bot/API/Updates/GitHubRelease.cs index ef11b3d..b9fa1b4 100644 --- a/DiscordLab.Bot/API/Updates/GitHubRelease.cs +++ b/DiscordLab.Bot/API/Updates/GitHubRelease.cs @@ -2,7 +2,7 @@ namespace DiscordLab.Bot.API.Updates; -using System.Text.Json.Serialization; +using Newtonsoft.Json; /// /// Contains data for a GitHub release object. @@ -12,7 +12,7 @@ public class GitHubRelease /// /// Gets or sets the tag name for this release. /// - [JsonPropertyName("tag_name")] + [JsonProperty("tag_name")] public string TagName { get; set; } // ReSharper disable CollectionNeverUpdated.Global @@ -20,7 +20,7 @@ public class GitHubRelease /// /// Gets or sets the assets for this release. /// - [JsonPropertyName("assets")] + [JsonProperty("assets")] public List Assets { get; set; } // ReSharper restore CollectionNeverUpdated.Global @@ -28,12 +28,12 @@ public class GitHubRelease /// /// Gets or sets a value indicating whether this is a prerelease release. /// - [JsonPropertyName("prerelease")] + [JsonProperty("prerelease")] public bool Prerelease { get; set; } /// /// Gets or sets a value indicating whether this is a draft release. /// - [JsonPropertyName("draft")] + [JsonProperty("draft")] public bool Draft { get; set; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs index 4352479..da8b935 100644 --- a/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs +++ b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs @@ -2,7 +2,7 @@ namespace DiscordLab.Bot.API.Updates; -using System.Text.Json.Serialization; +using Newtonsoft.Json; /// /// Gets details for a GitHub release asset. @@ -12,12 +12,12 @@ public class GitHubReleaseAsset /// /// Gets or sets the name of the asset. /// - [JsonPropertyName("name")] + [JsonProperty("name")] public string Name { get; set; } /// /// Gets or sets the download name of the asset. /// - [JsonPropertyName("browser_download_url")] + [JsonProperty("browser_download_url")] public string DownloadUrl { get; set; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Updates/Updater.cs b/DiscordLab.Bot/API/Updates/Updater.cs index 4670051..b98c790 100644 --- a/DiscordLab.Bot/API/Updates/Updater.cs +++ b/DiscordLab.Bot/API/Updates/Updater.cs @@ -3,8 +3,7 @@ namespace DiscordLab.Bot.API.Updates; using System.Net.Http; using DiscordLab.Bot.API.Attributes; using LabApi.Features.Console; -using LabApi.Loader; -using Utf8Json; +using Newtonsoft.Json; /// /// Handle updates within DiscordLab. @@ -38,9 +37,9 @@ public static async Task> CheckForUpdates() return []; } - using Stream stream = await response.Content.ReadAsStreamAsync(); + string str = await response.Content.ReadAsStringAsync(); - GitHubRelease[] releases = JsonSerializer.Deserialize(stream); + GitHubRelease[] releases = JsonConvert.DeserializeObject(str) ?? []; List modules = []; @@ -48,15 +47,20 @@ public static async Task> CheckForUpdates() { if (release.Prerelease || release.Draft) continue; - if (release.TagName.Count(c => c == '.') == 3) + if (release.TagName.Count(c => c == '.') > 2) continue; Version version = new(release.TagName.Split('-').First()); if (version.Major != Plugin.Instance.Version.Major) continue; - foreach (GitHubReleaseAsset asset in from asset in release.Assets let name = asset.Name.Replace(".dll", string.Empty) where !modules.Any(module => module.Name == name && module.Version > version) select asset) + foreach (GitHubReleaseAsset asset in release.Assets) { + if (asset.Name is "dependencies.zip" or "DiscordLab.Bot.dll") + continue; + string projectName = asset.Name.Replace(".dll", string.Empty); + if (modules.Any(module => module.Name == projectName && module.Version >= version)) + continue; modules.Add(new(release, asset)); } } diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index 1ee99a4..ff4fca2 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -64,7 +64,7 @@ public override async Task Run(SocketSlashCommand command) { case "list": { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => $"{s.Name} (v{s.Version})")); await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); break; } @@ -110,6 +110,6 @@ await command.ModifyOriginalResponseAsync(m => /// public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult(x.Name, x.Name))); + await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult($"{x.Name} (v{x.Version})", x.Name))); } } \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs index bddd9c3..126be92 100644 --- a/DiscordLab.Bot/Commands/LocalAdminCommand.cs +++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs @@ -18,13 +18,13 @@ public class LocalAdminCommand : ICommand public string Description { get; } = "Do things directly with DiscordLab."; /// - public bool Execute(ArraySegment arguments, ICommandSender sender, [UnscopedRef] out string response) + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { switch (arguments.FirstOrDefault()) { case "list": { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => s.Name)); + string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => $"{s.Name} (v{s.Version})")); response = "List of available DiscordLab modules:\n\n" + modules; return true; } From 8c03c47f624626e5c4da0ab301d1a424b3fa9405 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 09:14:32 +0100 Subject: [PATCH 52/68] feat!: translation builder optimisation --- DiscordLab.Administration/Events.cs | 15 +- .../API/Features/TranslationBuilder.cs | 164 +++++++++--------- DiscordLab.Moderation/Commands/Ban.cs | 11 +- DiscordLab.Moderation/Commands/Mute.cs | 7 +- .../Commands/TempMuteRemoteAdmin.cs | 7 +- DiscordLab.Moderation/Commands/Unban.cs | 5 +- DiscordLab.RoundLogs/Events.cs | 15 +- 7 files changed, 107 insertions(+), 117 deletions(-) diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs index c53ffc5..37e21ce 100644 --- a/DiscordLab.Administration/Events.cs +++ b/DiscordLab.Administration/Events.cs @@ -60,16 +60,11 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) return; SocketTextChannel channel; - TranslationBuilder builder = new("player", player) - { - CustomReplacers = new() - { - ["type"] = () => ev.CommandType.ToString(), - ["arguments"] = () => string.Join(" ", ev.Arguments), - ["command"] = () => ev.Command.Command, - ["commanddescription"] = () => ev.Command.Description, - } - }; + TranslationBuilder builder = new TranslationBuilder("player", player) + .AddCustomReplacer("type", ev.CommandType.ToString()) + .AddCustomReplacer("arguments", () => string.Join(" ", ev.Arguments)) + .AddCustomReplacer("command", ev.Command.Command) + .AddCustomReplacer("commanddescription", ev.Command.Description); if (ev.CommandType == CommandType.RemoteAdmin) { diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index d41946b..6149bc1 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -63,67 +63,66 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// /// Gets the dictionary of replacers that have no argument. /// - public static Dictionary> StaticReplacers { get; } = new() + public static Dictionary> StaticReplacers { get; } = new() { // Map Replacers - ["seed"] = () => Map.Seed.ToString(), - ["isdecont"] = () => DecontaminationController.Singleton.IsDecontaminating.ToString(), - ["remainingdeconttime"] = GetRemainingDecontaminationTime, - ["isdecontenabled"] = () => - (DecontaminationController.Singleton.DecontaminationOverride == DecontaminationController.DecontaminationStatus.None).ToString(), + [CreateRegex("seed")] = () => Map.Seed.ToString(), + [CreateRegex("isdecont")] = () => Decontamination.IsDecontaminating.ToString(), + [CreateRegex("remainingdeconttime")] = GetRemainingDecontaminationTime, + [CreateRegex("isdecontenabled")] = () => (Decontamination.Status == DecontaminationController.DecontaminationStatus.None).ToString(), // Round Replacers - ["killcount"] = () => Round.TotalDeaths.ToString(), - ["elapsedtime"] = () => Round.Duration.ToString(), - ["escapedscientistscount"] = () => Round.EscapedScientists.ToString(), - ["inprogress"] = () => Round.IsRoundInProgress.ToString(), - ["isended"] = () => Round.IsRoundEnded.ToString(), - ["isstarted"] = () => Round.IsRoundStarted.ToString(), - ["islocked"] = () => Round.IsLocked.ToString(), - ["changedintozombiescount"] = () => Round.ChangedIntoZombies.ToString(), - ["escapeddclassescount"] = () => Round.EscapedClassD.ToString(), - ["islobbylocked"] = () => Round.IsLobbyLocked.ToString(), - ["scpkillcount"] = () => Round.KilledBySCPs.ToString(), - ["alivescpcount"] = () => Round.SurvivingSCPs.ToString(), - ["roundcount"] = () => RoundRestart.UptimeRounds.ToString(), + [CreateRegex("killcount")] = () => Round.TotalDeaths.ToString(), + [CreateRegex("elapsedtime")] = () => Round.Duration.ToString(), + [CreateRegex("escapedscientistscount")] = () => Round.EscapedScientists.ToString(), + [CreateRegex("inprogress")] = () => Round.IsRoundInProgress.ToString(), + [CreateRegex("isended")] = () => Round.IsRoundEnded.ToString(), + [CreateRegex("isstarted")] = () => Round.IsRoundStarted.ToString(), + [CreateRegex("islocked")] = () => Round.IsLocked.ToString(), + [CreateRegex("changedintozombiescount")] = () => Round.ChangedIntoZombies.ToString(), + [CreateRegex("escapeddclassescount")] = () => Round.EscapedClassD.ToString(), + [CreateRegex("islobbylocked")] = () => Round.IsLobbyLocked.ToString(), + [CreateRegex("scpkillcount")] = () => Round.KilledBySCPs.ToString(), + [CreateRegex("alivescpcount")] = () => Round.SurvivingSCPs.ToString(), + [CreateRegex("roundcount")] = () => RoundRestart.UptimeRounds.ToString(), // Server Replacers - ["maxplayers"] = () => Server.MaxPlayers.ToString(), - ["name"] = () => Server.ServerListName, - ["nameparsed"] = () => + [CreateRegex("maxplayers")] = () => Server.MaxPlayers.ToString(), + [CreateRegex("name")] = () => Server.ServerListName, + [CreateRegex("nameparsed")] = () => { string result = UselessTextRemoveRegex.Replace(Server.ServerListName, string.Empty); result = TagRemoveRegex.Replace(result, string.Empty); return result; }, - ["port"] = () => Server.Port.ToString(), - ["ip"] = () => Server.IpAddress, - ["playercount"] = () => Server.PlayerCount.ToString(), - ["playercountnonpcs"] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), - ["tps"] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), - ["version"] = () => GameCore.Version.VersionString, - ["isbeta"] = () => (GameCore.Version.PublicBeta || GameCore.Version.PublicBeta).ToString(), - ["isfriendlyfire"] = () => Server.FriendlyFire.ToString(), + [CreateRegex("port")] = () => Server.Port.ToString(), + [CreateRegex("ip")] = () => Server.IpAddress, + [CreateRegex("playercount")] = () => Server.PlayerCount.ToString(), + [CreateRegex("playercountnonpcs")] = () => Player.ReadyList.Count(p => !p.IsNpc).ToString(), + [CreateRegex("tps")] = () => Server.Tps.ToString(CultureInfo.CurrentCulture), + [CreateRegex("version")] = () => GameCore.Version.VersionString, + [CreateRegex("isbeta")] = () => (GameCore.Version.PublicBeta || GameCore.Version.PublicBeta).ToString(), + [CreateRegex("isfriendlyfire")] = () => Server.FriendlyFire.ToString(), }; /// /// Gets time based replacers. The type is the unix timestamp. Can be got with . /// - public static Dictionary> TimeReplacers { get; } = new() + public static Dictionary> TimeReplacers { get; } = new() { - ["time"] = time => $"", - ["timet"] = time => $"", - ["timetlong"] = time => $"", - ["timed"] = time => $"", - ["timedlong"] = time => $"", - ["timef"] = time => $"", - ["timeflong"] = time => $"", - ["timer"] = time => $"", - ["elapsedtimerelative"] = time => $"", - ["roundstart"] = time => $"", - ["secondssince"] = time => TimeSince(time).Seconds.ToString(CultureInfo.InvariantCulture), - ["minutessince"] = time => TimeSince(time).Minutes.ToString(CultureInfo.InvariantCulture), + [CreateRegex("time")] = time => $"", + [CreateRegex("timet")] = time => $"", + [CreateRegex("timetlong")] = time => $"", + [CreateRegex("timed")] = time => $"", + [CreateRegex("timedlong")] = time => $"", + [CreateRegex("timef")] = time => $"", + [CreateRegex("timeflong")] = time => $"", + [CreateRegex("timer")] = time => $"", + [CreateRegex("elapsedtimerelative")] = time => $"", + [CreateRegex("roundstart")] = time => $"", + [CreateRegex("secondssince")] = time => TimeSince(time).Seconds.ToString(CultureInfo.InvariantCulture), + [CreateRegex("minutessince")] = time => TimeSince(time).Minutes.ToString(CultureInfo.InvariantCulture), }; /// @@ -161,7 +160,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// /// Gets or sets a Dictionary of custom replacers. Key is the text to replace and value is the factory to replace with. /// - public Dictionary> CustomReplacers { get; set; } = new(); + public Dictionary> CustomReplacers { get; set; } = new(); /// /// Gets or sets the players that need to be translated for, if any. @@ -194,6 +193,11 @@ public TranslationBuilder(string translation, string playerPrefix, Player player /// public IEnumerable? PlayerList { get; set; } + /// + /// Gets a Dictionary of cached regexes that are unknown. + /// + private static Dictionary CachedRegex { get; } = new(); + /// /// . /// @@ -210,6 +214,13 @@ public static implicit operator string(TranslationBuilder builder) => public static implicit operator Optional(TranslationBuilder builder) => builder.Build(); + /// + /// Creates a compatible placeholder regex. + /// + /// The placeholder. + /// The new regex. + public static Regex CreateRegex(string placeholder) => new(ToParameterString(placeholder), RegexOptions.IgnoreCase | RegexOptions.Compiled); + /// /// Adds multiple players to the list. /// @@ -241,10 +252,10 @@ public TranslationBuilder AddPlayer(string prefix, Player player) /// /// Adds a custom replacer to the dictionary. /// - /// The text to replace. + /// The regex to replace. /// The string factory to replace with. /// The instance. - public TranslationBuilder AddCustomReplacer(string toReplace, Func replacer) + public TranslationBuilder AddCustomReplacer(Regex toReplace, Func replacer) { CustomReplacers.Add(toReplace, replacer); @@ -252,18 +263,23 @@ public TranslationBuilder AddCustomReplacer(string toReplace, Func repla } /// - /// Adds a custom replacer to the dictionary. + /// + /// + /// The text to replace. + /// The string factory to replace with. + /// The instance. + public TranslationBuilder AddCustomReplacer(string toReplace, Func replacer) => + AddCustomReplacer(CreateRegex(toReplace), replacer); + + /// + /// /// /// The text to replace. /// The text to replace with. /// The instance. - public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) - { + public TranslationBuilder AddCustomReplacer(string toReplace, string replacer) => AddCustomReplacer(toReplace, () => replacer); - return this; - } - /// /// Builds this instance. /// @@ -276,38 +292,28 @@ public string Build(string? translation = null) if (string.IsNullOrEmpty(translation)) throw new ArgumentNullException($"{nameof(TranslationBuilder)} failed to build because of no valid translation."); - if (PlayerListItem != null && !CustomReplacers.ContainsKey("players")) + if (PlayerListItem != null && CustomReplacers.All(replacer => replacer.Key.ToString() != "{players}")) SetupPlayerList(); string returnTranslation = translation!; - foreach (KeyValuePair> replacer in CustomReplacers) + foreach (KeyValuePair> replacer in CustomReplacers) { - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(), - RegexOptions.IgnoreCase); + returnTranslation = replacer.Key.Replace(returnTranslation, replacer.Value()); } - foreach (KeyValuePair> replacer in StaticReplacers) + foreach (KeyValuePair> replacer in StaticReplacers) { - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(), - RegexOptions.IgnoreCase); + returnTranslation = replacer.Key.Replace(returnTranslation, replacer.Value()); } long unix = new DateTimeOffset(Time).ToUnixTimeSeconds(); - foreach (KeyValuePair> replacer in TimeReplacers) + foreach (KeyValuePair> replacer in TimeReplacers) { - returnTranslation = Regex.Replace( + returnTranslation = replacer.Key.Replace( returnTranslation, - ToParameterString(replacer.Key), - replacer.Value(unix), - RegexOptions.IgnoreCase); + replacer.Value(unix)); } foreach (KeyValuePair player in Players) @@ -315,11 +321,9 @@ public string Build(string? translation = null) if (player.Value is not { IsReady: true }) continue; - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString(player.Key), - player.Value.Nickname, - RegexOptions.IgnoreCase); + Regex baseRegex = CachedRegex.GetOrAdd(player.Key, () => CreateRegex(player.Key)); + + returnTranslation = baseRegex.Replace(returnTranslation, player.Value.Nickname); foreach (KeyValuePair> replacer in PlayerReplacers) { @@ -341,11 +345,11 @@ public string Build(string? translation = null) if (string.IsNullOrEmpty(replacement)) replacement = "Unknown"; - returnTranslation = Regex.Replace( - returnTranslation, - ToParameterString($"{player.Key}{replacer.Key}"), - replacement, - RegexOptions.IgnoreCase); + Regex regex = CachedRegex.GetOrAdd( + $"{player.Key}{replacer.Key}", + () => CreateRegex($"{player.Key}{replacer.Key}")); + + returnTranslation = regex.Replace(returnTranslation, replacement); } } diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index 40c7e1d..d1ff0a4 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -52,14 +52,13 @@ public override async Task Run(SocketSlashCommand command) long duration = Misc.RelativeTimeToSeconds((string)command.Data.Options.ElementAt(1).Value, 60); string reason = (string)command.Data.Options.ElementAt(2).Value; - TranslationBuilder successBuilder = new(Translation.BanSuccess) + TranslationBuilder successBuilder = new TranslationBuilder(Translation.BanSuccess) { Time = TempMuteManager.GetExpireDate(duration) - }; - TranslationBuilder failBuilder = new(Translation.BanFailure); - - successBuilder.CustomReplacers.Add("userid", () => userId); - failBuilder.CustomReplacers.Add("userid", () => userId); + } + .AddCustomReplacer("userid", userId); + TranslationBuilder failBuilder = new TranslationBuilder(Translation.BanFailure) + .AddCustomReplacer("userid", userId); if (!CommandUtils.TryGetPlayerFromUnparsed(userId, out Player player)) { diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index 8000295..9dcfbbe 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -56,12 +56,11 @@ public override async Task Run(SocketSlashCommand command) DateTime time = TempMuteManager.GetExpireDate(duration); TempMuteManager.MutePlayer(player, time); - builder = new(Translation.TempMuteSuccess, "player", player) + builder = new TranslationBuilder(Translation.TempMuteSuccess, "player", player) { Time = time - }; - - builder.CustomReplacers.Add("duration", () => duration); + } + .AddCustomReplacer("duration", duration); await command.ModifyOriginalResponseAsync(m => m.Content = builder); return; diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs index 0d0ed50..d718b5d 100644 --- a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -46,12 +46,11 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s TempMuteManager.MutePlayer(target, time, player); - TranslationBuilder builder = new(Plugin.Instance.Translation.TempMuteSuccess, "player", target) + TranslationBuilder builder = new TranslationBuilder(Plugin.Instance.Translation.TempMuteSuccess, "player", target) { Time = time - }; - - builder.CustomReplacers.Add("duration", () => arguments.At(1)); + } + .AddCustomReplacer("duration", () => arguments.At(1)); response = builder; return true; diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index a7bc877..f371ced 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -36,9 +36,8 @@ public override async Task Run(SocketSlashCommand command) BanHandler.RemoveBan(id, id.Contains("@") ? BanHandler.BanType.UserId : BanHandler.BanType.IP); - TranslationBuilder builder = new(Translation.UnbanSuccess); - - builder.CustomReplacers.Add("userid", () => id); + TranslationBuilder builder = new TranslationBuilder(Translation.UnbanSuccess) + .AddCustomReplacer("userid", id); await command.ModifyOriginalResponseAsync(m => m.Content = diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs index f477f5a..6abfb66 100644 --- a/DiscordLab.RoundLogs/Events.cs +++ b/DiscordLab.RoundLogs/Events.cs @@ -28,16 +28,11 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) SocketTextChannel channel; - TranslationBuilder builder = new("player", ev.Player) - { - CustomReplacers = new() - { - ["oldrole"] = () => ev.OldRole.GetFullName(), - ["newrole"] = () => ev.NewRole.RoleName, - ["reason"] = () => ev.ChangeReason.ToString(), - ["spawnflags"] = () => string.Join(", ", ev.SpawnFlags.GetFlags()) - } - }; + TranslationBuilder builder = new TranslationBuilder("player", ev.Player) + .AddCustomReplacer("oldrole", () => ev.OldRole.GetFullName()) + .AddCustomReplacer("newrole", ev.NewRole.RoleName) + .AddCustomReplacer("reason", ev.ChangeReason.ToString()) + .AddCustomReplacer("spawnflags", string.Join(", ", ev.SpawnFlags.GetFlags())); if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) { From 1925fa0bafe0852c66e0614534456c52a1f680b5 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 09:31:27 +0100 Subject: [PATCH 53/68] feat: embed omitting --- DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs | 2 ++ DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index c867fdb..ec5d9f8 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -28,6 +28,7 @@ public string Description /// /// Gets or sets the embed fields. /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public IEnumerable Fields { get => Builder.Fields.Select(x => new EmbedFieldBuilder { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); @@ -37,6 +38,7 @@ public IEnumerable Fields /// /// Gets or sets the color of the embed. In string so #, 0x or the raw hex value will work. /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string? Color { get => Builder.Color?.ToString(); diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs index 0d9c27b..2162623 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs @@ -28,6 +28,7 @@ public string Value /// /// Gets or sets a value indicating whether the field is inline. /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public bool IsInline { get => Builder.IsInline; From ac402861c82ef4c7aec8ec5a52ec3a2f63fdb232 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 09:34:43 +0100 Subject: [PATCH 54/68] chore: unpublicize discord.net.rest --- DiscordLab.Bot/DiscordLab.Bot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index c17f5e1..4dc0e00 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -37,7 +37,7 @@ - + From d8d599275768a9c788bf6d047efce07e8c1aea26 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 10:24:52 +0100 Subject: [PATCH 55/68] chore(deps): bump labapi ver --- DiscordLab.Administration/DiscordLab.Administration.csproj | 1 + DiscordLab.BotStatus/DiscordLab.BotStatus.csproj | 4 ++++ DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj | 4 ++++ DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj | 4 ++++ DiscordLab.Dependency/DiscordLab.Dependency.csproj | 3 +++ DiscordLab.Moderation/DiscordLab.Moderation.csproj | 4 ++++ DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj | 4 ++++ DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj | 4 ++++ 8 files changed, 28 insertions(+) diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj index 206d41c..f067d04 100644 --- a/DiscordLab.Administration/DiscordLab.Administration.csproj +++ b/DiscordLab.Administration/DiscordLab.Administration.csproj @@ -15,5 +15,6 @@ + \ No newline at end of file diff --git a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj index 57493f0..3cf773a 100644 --- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj +++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj index c800f07..6951372 100644 --- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj +++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj index c800f07..6951372 100644 --- a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj +++ b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/DiscordLab.Dependency/DiscordLab.Dependency.csproj b/DiscordLab.Dependency/DiscordLab.Dependency.csproj index 236ea0b..7f5a119 100644 --- a/DiscordLab.Dependency/DiscordLab.Dependency.csproj +++ b/DiscordLab.Dependency/DiscordLab.Dependency.csproj @@ -8,4 +8,7 @@ true 2.0.0 + + + \ No newline at end of file diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index b09a6a8..3488d41 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -16,4 +16,8 @@ + + + + \ No newline at end of file diff --git a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj index c800f07..6951372 100644 --- a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj +++ b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index 46ef3c0..6fb9bea 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -12,4 +12,8 @@ + + + + \ No newline at end of file From ebdf86c0301b18ef07f37d5d44d6d08fec3ef811 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 10:25:03 +0100 Subject: [PATCH 56/68] fix: patch crash --- DiscordLab.Bot/Patches/RestClientCreate.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index 65de9c9..ac304e7 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -22,12 +22,20 @@ public static class RestClientCreate { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { - Type? type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); - if (type == null) - continue; - ConstructorInfo? constructor = type.GetConstructors().FirstOrDefault(); - if (constructor != null) - return constructor; + try + { + Type? type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient"); + if (type == null) + continue; + ConstructorInfo? constructor = type.GetConstructors().FirstOrDefault(); + Logger.Info(constructor != null); + if (constructor != null) + return constructor; + } + catch + { + // ignored because sometimes missing dependencies in underlying dependencies... idk why lol, but this works. + } } return null; From 968936da10c349e76160717f46b47cf58184cf9f Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 10:35:53 +0100 Subject: [PATCH 57/68] ci: fix packing nuget --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50f6673..b328a11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,8 +27,8 @@ jobs: - name: Rename Assembly-CSharp (because Exiled removes the normal version) shell: pwsh run: Move-Item -Path ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll -Destination ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll - - name: Build - run: dotnet build -c:Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj + - name: Build and Pack + run: dotnet pack -c:Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj - name: Publish run: dotnet nuget push ${{ github.workspace }}/DiscordLab.Bot/bin/Release/DiscordLab.Bot.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} env: From a2baba9acd4d3939096447fd33a946c6fbfe69b4 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 11:20:10 +0100 Subject: [PATCH 58/68] feat: move dependency to dependencies --- Directory.Build.props | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index f4783cd..0e3e1ad 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,11 +4,17 @@ $(MSBuildThisFileDirectory)bin\ - + + + + + @@ -26,7 +32,7 @@ - + \ No newline at end of file From 50eb3881008f0e457d249fbd2a662aacae6410f2 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 11:20:32 +0100 Subject: [PATCH 59/68] fix: dep issue --- DiscordLab.Administration/DiscordLab.Administration.csproj | 1 - DiscordLab.BotStatus/DiscordLab.BotStatus.csproj | 4 ---- DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj | 4 ---- DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj | 4 ---- DiscordLab.Dependency/DiscordLab.Dependency.csproj | 3 --- DiscordLab.Moderation/DiscordLab.Moderation.csproj | 4 ---- DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj | 4 ---- DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj | 4 ---- 8 files changed, 28 deletions(-) diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj index f067d04..206d41c 100644 --- a/DiscordLab.Administration/DiscordLab.Administration.csproj +++ b/DiscordLab.Administration/DiscordLab.Administration.csproj @@ -15,6 +15,5 @@ - \ No newline at end of file diff --git a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj index 3cf773a..57493f0 100644 --- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj +++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj index 6951372..c800f07 100644 --- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj +++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj index 6951372..c800f07 100644 --- a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj +++ b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file diff --git a/DiscordLab.Dependency/DiscordLab.Dependency.csproj b/DiscordLab.Dependency/DiscordLab.Dependency.csproj index 7f5a119..236ea0b 100644 --- a/DiscordLab.Dependency/DiscordLab.Dependency.csproj +++ b/DiscordLab.Dependency/DiscordLab.Dependency.csproj @@ -8,7 +8,4 @@ true 2.0.0 - - - \ No newline at end of file diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index 3488d41..b09a6a8 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -16,8 +16,4 @@ - - - - \ No newline at end of file diff --git a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj index 6951372..c800f07 100644 --- a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj +++ b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file diff --git a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj index 6fb9bea..46ef3c0 100644 --- a/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj +++ b/DiscordLab.StatusChannel/DiscordLab.StatusChannel.csproj @@ -12,8 +12,4 @@ - - - - \ No newline at end of file From 5eef9f49dca34d1d84f7ba3b061c55f391d30fe0 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 11:24:13 +0100 Subject: [PATCH 60/68] chore: code cleanup --- Directory.Build.props | 2 +- .../Commands/SendCommand.cs | 5 +- .../DiscordLab.Administration.csproj | 6 +- DiscordLab.Administration/Events.cs | 23 ++++---- DiscordLab.Administration/Patches/ErrorLog.cs | 5 +- DiscordLab.Administration/Plugin.cs | 10 ++-- DiscordLab.Administration/Translation.cs | 13 +++-- .../API/Attributes/CallOnLoadAttribute.cs | 3 +- .../API/Attributes/CallOnReadyAttribute.cs | 3 +- .../API/Attributes/CallOnUnloadAttribute.cs | 3 +- .../API/Features/Embed/EmbedBuilder.cs | 3 +- DiscordLab.Bot/API/Features/MessageContent.cs | 4 +- DiscordLab.Bot/API/Features/SlashCommand.cs | 6 +- .../API/Features/TranslationBuilder.cs | 7 ++- DiscordLab.Bot/API/Updates/Module.cs | 4 +- DiscordLab.Bot/Client.cs | 4 +- DiscordLab.Bot/Commands/DiscordCommand.cs | 23 +++++--- DiscordLab.Bot/Commands/LocalAdminCommand.cs | 11 +++- DiscordLab.Bot/Config.cs | 3 +- DiscordLab.Bot/DiscordLab.Bot.csproj | 34 ++++++------ DiscordLab.Bot/Patches/RestClientCreate.cs | 4 +- .../DiscordLab.BotStatus.csproj | 4 +- DiscordLab.BotStatus/Plugin.cs | 23 ++++---- DiscordLab.ConnectionLogs/Config.cs | 2 +- .../DiscordLab.ConnectionLogs.csproj | 2 +- DiscordLab.ConnectionLogs/Events.cs | 35 ++++++------ DiscordLab.ConnectionLogs/Plugin.cs | 8 +-- DiscordLab.ConnectionLogs/Translation.cs | 10 ++-- DiscordLab.DeathLogs/Config.cs | 7 ++- DiscordLab.DeathLogs/DamageLogs.cs | 55 ++++++++++--------- .../DiscordLab.DeathLogs.csproj | 2 +- DiscordLab.DeathLogs/Events.cs | 31 ++++++----- DiscordLab.DeathLogs/Plugin.cs | 8 +-- DiscordLab.DeathLogs/Translation.cs | 6 +- .../DiscordLab.Dependency.csproj | 18 +++--- DiscordLab.Moderation/Commands/Ban.cs | 20 +++---- DiscordLab.Moderation/Commands/Mute.cs | 12 ++-- .../Commands/TempMuteRemoteAdmin.cs | 19 ++++--- DiscordLab.Moderation/Commands/Unban.cs | 15 +++-- DiscordLab.Moderation/Commands/Unmute.cs | 10 ++-- .../DiscordLab.Moderation.csproj | 6 +- DiscordLab.Moderation/Events.cs | 33 ++++++----- DiscordLab.Moderation/Plugin.cs | 25 +++++---- DiscordLab.Moderation/TempMuteManager.cs | 19 ++++--- DiscordLab.Moderation/Translation.cs | 16 ++++-- .../DiscordLab.RoundLogs.csproj | 2 +- DiscordLab.RoundLogs/Events.cs | 46 +++++++++------- DiscordLab.RoundLogs/Plugin.cs | 8 +-- DiscordLab.RoundLogs/Translation.cs | 6 +- DiscordLab.StatusChannel/Events.cs | 3 +- README.md | 30 ++++++---- 51 files changed, 374 insertions(+), 283 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 0e3e1ad..c3ea473 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ DestinationFolder="$(SharedBinPath)" SkipUnchangedFiles="true"/> - + "/" + x.Command), ..QueryProcessor.DotCommandHandler.AllCommands.Select(x => "." + x.Command) ]; - await autocomplete.RespondAsync(commands.Where(x => x.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult(x, x))); + await autocomplete.RespondAsync(commands.Where(x => x.Contains((string)autocomplete.Data.Current.Value)) + .Take(25).Select(x => new AutocompleteResult(x, x))); } } \ No newline at end of file diff --git a/DiscordLab.Administration/DiscordLab.Administration.csproj b/DiscordLab.Administration/DiscordLab.Administration.csproj index 206d41c..6f1acd0 100644 --- a/DiscordLab.Administration/DiscordLab.Administration.csproj +++ b/DiscordLab.Administration/DiscordLab.Administration.csproj @@ -10,10 +10,10 @@ - + - + - + \ No newline at end of file diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs index 37e21ce..ae5c71c 100644 --- a/DiscordLab.Administration/Events.cs +++ b/DiscordLab.Administration/Events.cs @@ -19,9 +19,9 @@ public class Events : CustomEventsHandler public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - + private static bool IsSubscribed { get; set; } - + [CallOnLoad] public static void Load() { @@ -36,7 +36,7 @@ public static void Unload() ServerEvents.WaitingForPlayers -= OnServerStart; IsSubscribed = false; } - + public static void OnServerStart() { ServerEvents.WaitingForPlayers -= OnServerStart; @@ -47,10 +47,11 @@ public static void OnServerStart() if (!Client.TryGetOrAddChannel(Config.ServerStartChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server start logs", Config.ServerStartChannelId, + Config.GuildId)); return; } - + Translation.ServerStart.SendToChannel(channel, new()); } @@ -58,14 +59,14 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) { if (ev.Sender == null || !Player.TryGet(ev.Sender, out Player player)) return; - + SocketTextChannel channel; TranslationBuilder builder = new TranslationBuilder("player", player) .AddCustomReplacer("type", ev.CommandType.ToString()) .AddCustomReplacer("arguments", () => string.Join(" ", ev.Arguments)) .AddCustomReplacer("command", ev.Command.Command) .AddCustomReplacer("commanddescription", ev.Command.Description); - + if (ev.CommandType == CommandType.RemoteAdmin) { if (Config.RemoteAdminChannelId == 0) @@ -73,10 +74,11 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", Config.RemoteAdminChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs", + Config.RemoteAdminChannelId, Config.GuildId)); return; } - + Translation.RemoteAdmin.SendToChannel(channel, builder); return; } @@ -86,7 +88,8 @@ public override void OnServerCommandExecuted(CommandExecutedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.CommandLogChannelId, out channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("command logs", Config.CommandLogChannelId, + Config.GuildId)); return; } diff --git a/DiscordLab.Administration/Patches/ErrorLog.cs b/DiscordLab.Administration/Patches/ErrorLog.cs index f3e4a61..cc54303 100644 --- a/DiscordLab.Administration/Patches/ErrorLog.cs +++ b/DiscordLab.Administration/Patches/ErrorLog.cs @@ -19,13 +19,14 @@ public static void Postfix(object message) if (!Client.TryGetOrAddChannel(Plugin.Instance.Config.ErrorLogChannelId, out SocketTextChannel channel)) { Logger.Raw( - $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}", ConsoleColor.Red); + $"[ERROR] [{Plugin.Instance.Name}] {LoggingUtils.GenerateMissingChannelMessage("error logs", Plugin.Instance.Config.ErrorLogChannelId, Plugin.Instance.Config.GuildId)}", + ConsoleColor.Red); return; } TranslationBuilder builder = new TranslationBuilder() .AddCustomReplacer("error", message.ToString()); - + Plugin.Instance.Translation.ErrorLog.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.Administration/Plugin.cs b/DiscordLab.Administration/Plugin.cs index 9257a01..4053f6d 100644 --- a/DiscordLab.Administration/Plugin.cs +++ b/DiscordLab.Administration/Plugin.cs @@ -10,7 +10,7 @@ namespace DiscordLab.Administration; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.Administration"; public override string Description { get; } = @@ -23,16 +23,16 @@ public class Plugin : Plugin public Events Events = new(); private Harmony harmony = new($"DiscordLab.Administration-{DateTime.Now.Ticks}"); - + public override void Enable() { Instance = this; harmony.PatchAll(); CallOnLoadAttribute.Load(); - - if(Config.AddCommands) + + if (Config.AddCommands) SlashCommand.FindAll(); - + CustomHandlersManager.RegisterEventsHandler(Events); } diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs index 46272ae..881deb3 100644 --- a/DiscordLab.Administration/Translation.cs +++ b/DiscordLab.Administration/Translation.cs @@ -10,16 +10,17 @@ public class Translation public MessageContent ErrorLog { get; set; } = "An error has occured:\n{error}"; - public MessageContent RemoteAdmin { get; set; } = "Player {player} has executed the remote admin command: `{command}`"; - + public MessageContent RemoteAdmin { get; set; } = + "Player {player} has executed the remote admin command: `{command}`"; + public MessageContent CommandLog { get; set; } = "Player {player} has executed the command: `{command}`"; - + public string SendCommandName { get; set; } = "send"; - + public string SendCommandDescription { get; set; } = "Sends a command to the server"; - + public string SendCommandOptionName { get; set; } = "command"; - + public string SendCommandOptionDescription { get; set; } = "The command to send"; public MessageContent SendCommandResponse { get; set; } = "The command has been sent, it returned: {response}"; diff --git a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs index d3f94fe..8cee553 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs @@ -19,7 +19,8 @@ public static void Load(Assembly? assembly = null) foreach (Type type in assembly.GetTypes()) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic)) { CallOnLoadAttribute attribute = method.GetCustomAttribute(); if (attribute == null) diff --git a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs index f5164c1..dedf485 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs @@ -21,7 +21,8 @@ public static void Load(Assembly? assembly = null) foreach (Type type in assembly.GetTypes()) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic)) { CallOnReadyAttribute attribute = method.GetCustomAttribute(); if (attribute == null) diff --git a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs index d0ac478..f6cb66c 100644 --- a/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs +++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs @@ -19,7 +19,8 @@ public static void Unload(Assembly? assembly = null) foreach (Type type in assembly.GetTypes()) { - foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) + foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic)) { CallOnUnloadAttribute attribute = method.GetCustomAttribute(); if (attribute == null) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index ec5d9f8..50de2db 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -31,7 +31,8 @@ public string Description [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public IEnumerable Fields { - get => Builder.Fields.Select(x => new EmbedFieldBuilder { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); + get => Builder.Fields.Select(x => new EmbedFieldBuilder + { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); set => Builder.Fields = value.Select(x => x.Builder).ToList(); } diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs index f288af0..67b9185 100644 --- a/DiscordLab.Bot/API/Features/MessageContent.cs +++ b/DiscordLab.Bot/API/Features/MessageContent.cs @@ -1,4 +1,5 @@ // ReSharper disable MemberCanBePrivate.Global + namespace DiscordLab.Bot.API.Features; using Discord.Rest; @@ -138,7 +139,8 @@ await command.ModifyOriginalResponseAsync(msg => if (!string.IsNullOrEmpty(embed.Description)) embed.Description = builder.Build(embed.Description); - foreach (Discord.EmbedFieldBuilder field in embed.Fields.Where(field => field.Value is string value && !string.IsNullOrEmpty(value))) + foreach (Discord.EmbedFieldBuilder field in embed.Fields.Where(field => + field.Value is string value && !string.IsNullOrEmpty(value))) { field.Value = builder.Build((string)field.Value); } diff --git a/DiscordLab.Bot/API/Features/SlashCommand.cs b/DiscordLab.Bot/API/Features/SlashCommand.cs index 7519072..152882f 100644 --- a/DiscordLab.Bot/API/Features/SlashCommand.cs +++ b/DiscordLab.Bot/API/Features/SlashCommand.cs @@ -94,11 +94,13 @@ private static async Task RegisterGuildCommands(IEnumerable comman SocketGuild? guild = Client.GetGuild(cmds.Key); if (guild == null) { - Logger.Warn($"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); + Logger.Warn( + $"Could not find guild {cmds.Key}, so could not register the commands {string.Join(",", cmds.Select(cmd => cmd.Data.Name))}"); continue; } - await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build()).ToArray()); + await guild.BulkOverwriteApplicationCommandAsync(cmds.Select(cmd => cmd.Data.Build()) + .ToArray()); } } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index 6149bc1..ef472f7 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -1,5 +1,6 @@ // ReSharper disable MemberCanBePrivate.Global // ReSharper disable PropertyCanBeMadeInitOnly.Global + namespace DiscordLab.Bot.API.Features; using System.Globalization; @@ -69,7 +70,8 @@ public TranslationBuilder(string translation, string playerPrefix, Player player [CreateRegex("seed")] = () => Map.Seed.ToString(), [CreateRegex("isdecont")] = () => Decontamination.IsDecontaminating.ToString(), [CreateRegex("remainingdeconttime")] = GetRemainingDecontaminationTime, - [CreateRegex("isdecontenabled")] = () => (Decontamination.Status == DecontaminationController.DecontaminationStatus.None).ToString(), + [CreateRegex("isdecontenabled")] = () => + (Decontamination.Status == DecontaminationController.DecontaminationStatus.None).ToString(), // Round Replacers [CreateRegex("killcount")] = () => Round.TotalDeaths.ToString(), @@ -219,7 +221,8 @@ public static implicit operator Optional(TranslationBuilder builder) => /// /// The placeholder. /// The new regex. - public static Regex CreateRegex(string placeholder) => new(ToParameterString(placeholder), RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static Regex CreateRegex(string placeholder) => + new(ToParameterString(placeholder), RegexOptions.IgnoreCase | RegexOptions.Compiled); /// /// Adds multiple players to the list. diff --git a/DiscordLab.Bot/API/Updates/Module.cs b/DiscordLab.Bot/API/Updates/Module.cs index 4416bdf..1a4800e 100644 --- a/DiscordLab.Bot/API/Updates/Module.cs +++ b/DiscordLab.Bot/API/Updates/Module.cs @@ -19,7 +19,9 @@ public Module(GitHubRelease release, GitHubReleaseAsset asset) Asset = asset; Name = asset.Name.Replace(".dll", string.Empty); Version = new(release.TagName.Split('-').First()); - ExistingPlugin = PluginLoader.Plugins.Keys.FirstOrDefault(x => Name == "DiscordLab.Bot" ? x.Name == "DiscordLab" : x.Name == Name); + ExistingPlugin = + PluginLoader.Plugins.Keys.FirstOrDefault(x => + Name == "DiscordLab.Bot" ? x.Name == "DiscordLab" : x.Name == Name); } /// diff --git a/DiscordLab.Bot/Client.cs b/DiscordLab.Bot/Client.cs index c1498ad..c497678 100644 --- a/DiscordLab.Bot/Client.cs +++ b/DiscordLab.Bot/Client.cs @@ -193,7 +193,9 @@ private static Task SlashCommandHandler(SocketSlashCommand command) private static Task AutocompleteHandler(SocketAutocompleteInteraction autocomplete) { DebugLog($"{autocomplete.Data.CommandName} requested a response, finding the command..."); - AutocompleteCommand? command = SlashCommand.Commands.FirstOrDefault(c => c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; + AutocompleteCommand? command = + SlashCommand.Commands.FirstOrDefault(c => + c is AutocompleteCommand cmd && cmd.Data.Name == autocomplete.Data.CommandName) as AutocompleteCommand; command?.Autocomplete(autocomplete); return Task.CompletedTask; diff --git a/DiscordLab.Bot/Commands/DiscordCommand.cs b/DiscordLab.Bot/Commands/DiscordCommand.cs index ff4fca2..28006c8 100644 --- a/DiscordLab.Bot/Commands/DiscordCommand.cs +++ b/DiscordLab.Bot/Commands/DiscordCommand.cs @@ -38,7 +38,6 @@ public class DiscordCommand : AutocompleteCommand IsRequired = true, IsAutocomplete = true, } - ], }, @@ -48,7 +47,6 @@ public class DiscordCommand : AutocompleteCommand Name = "check", Description = "Check for DiscordLab updates", } - ], }; @@ -64,8 +62,12 @@ public override async Task Run(SocketSlashCommand command) { case "list": { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => $"{s.Name} (v{s.Version})")); - await command.ModifyOriginalResponseAsync(m => m.Content = "List of available DiscordLab modules:\n\n" + modules); + string modules = string.Join( + "\n", + Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot") + .Select(s => $"{s.Name} (v{s.Version})")); + await command.ModifyOriginalResponseAsync(m => + m.Content = "List of available DiscordLab modules:\n\n" + modules); break; } @@ -78,7 +80,11 @@ public override async Task Run(SocketSlashCommand command) return; } - Module? module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + Module? module = + Module.CurrentModules.FirstOrDefault(s => + string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? + Module.CurrentModules.FirstOrDefault(s => + s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); if (module == null) { await command.ModifyOriginalResponseAsync(m => m.Content = "Module not found."); @@ -87,7 +93,8 @@ public override async Task Run(SocketSlashCommand command) await module.Download(); ServerStatic.StopNextRound = ServerStatic.NextRoundAction.Restart; - await command.ModifyOriginalResponseAsync(m => m.Content = "Downloaded module. Server will restart next round."); + await command.ModifyOriginalResponseAsync(m => + m.Content = "Downloaded module. Server will restart next round."); break; } @@ -110,6 +117,8 @@ await command.ModifyOriginalResponseAsync(m => /// public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { - await autocomplete.RespondAsync(Module.CurrentModules.Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult($"{x.Name} (v{x.Version})", x.Name))); + await autocomplete.RespondAsync(Module.CurrentModules + .Where(x => x.Name != "DiscordLab.Bot" && x.Name.Contains((string)autocomplete.Data.Current.Value)).Take(25) + .Select(x => new AutocompleteResult($"{x.Name} (v{x.Version})", x.Name))); } } \ No newline at end of file diff --git a/DiscordLab.Bot/Commands/LocalAdminCommand.cs b/DiscordLab.Bot/Commands/LocalAdminCommand.cs index 126be92..8d059e8 100644 --- a/DiscordLab.Bot/Commands/LocalAdminCommand.cs +++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs @@ -24,7 +24,10 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { case "list": { - string modules = string.Join("\n", Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot").Select(s => $"{s.Name} (v{s.Version})")); + string modules = string.Join( + "\n", + Module.CurrentModules.Where(s => s.Name != "DiscordLab.Bot") + .Select(s => $"{s.Name} (v{s.Version})")); response = "List of available DiscordLab modules:\n\n" + modules; return true; } @@ -38,7 +41,11 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s return false; } - Module? module = Module.CurrentModules.FirstOrDefault(s => string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? Module.CurrentModules.FirstOrDefault(s => s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); + Module? module = + Module.CurrentModules.FirstOrDefault(s => + string.Equals(s.Name, moduleName, StringComparison.CurrentCultureIgnoreCase)) ?? + Module.CurrentModules.FirstOrDefault(s => + s.Name.Split('.').Last().Equals(moduleName, StringComparison.CurrentCultureIgnoreCase)); if (module == null) { response = "Module not found."; diff --git a/DiscordLab.Bot/Config.cs b/DiscordLab.Bot/Config.cs index d7559ac..956a34d 100644 --- a/DiscordLab.Bot/Config.cs +++ b/DiscordLab.Bot/Config.cs @@ -28,7 +28,8 @@ public sealed class Config /// /// Gets or sets the proxy URL. Shouldn't be set if proxy is not needed. /// - [Description("The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")] + [Description( + "The proxy URL to use. Should only be used in very specific cases like Discord being banned in your country. Please set to empty to not use.")] public string ProxyUrl { get; set; } = string.Empty; /// diff --git a/DiscordLab.Bot/DiscordLab.Bot.csproj b/DiscordLab.Bot/DiscordLab.Bot.csproj index 4dc0e00..85c949d 100644 --- a/DiscordLab.Bot/DiscordLab.Bot.csproj +++ b/DiscordLab.Bot/DiscordLab.Bot.csproj @@ -8,7 +8,7 @@ true 2.0.0 - + true true @@ -19,7 +19,7 @@ https://github.com/DiscordLabSCP/DiscordLab README.md - + ../stylecop.ruleset @@ -30,32 +30,32 @@ \ - + - + - + - - - - - - + + + + + + - + $(MSBuildThisFileDirectory)..\bin\dependencies\ - - - - + + + + - + diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index ac304e7..72d601b 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -74,8 +74,8 @@ public static IEnumerable Transpiler(IEnumerabletrue 2.0.0 - + - + \ No newline at end of file diff --git a/DiscordLab.BotStatus/Plugin.cs b/DiscordLab.BotStatus/Plugin.cs index 4e2ff79..c4f10eb 100644 --- a/DiscordLab.BotStatus/Plugin.cs +++ b/DiscordLab.BotStatus/Plugin.cs @@ -13,30 +13,30 @@ namespace DiscordLab.BotStatus; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.BotStatus"; public override string Description { get; } = "Allows your bot's status to update with player counts."; public override string Author { get; } = "LumiFae"; public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - + public override void Enable() { Instance = this; PlayerEvents.Joined += OnPlayerJoin; ReferenceHub.OnPlayerRemoved += OnPlayerLeave; - + ServerEvents.WaitingForPlayers += OnWaitingForPlayers; } public override void Disable() { ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; - + PlayerEvents.Joined -= OnPlayerJoin; ReferenceHub.OnPlayerRemoved -= OnPlayerLeave; - + Instance = null; } @@ -47,7 +47,7 @@ public static void OnWaitingForPlayers() public static void OnPlayerJoin(PlayerJoinedEventArgs _) { - if(Round.IsRoundInProgress) + if (Round.IsRoundInProgress) UpdateStatus(); else Queue.Process(); @@ -55,18 +55,21 @@ public static void OnPlayerJoin(PlayerJoinedEventArgs _) public static void OnPlayerLeave(ReferenceHub _) { - if(Round.IsRoundInProgress) + if (Round.IsRoundInProgress) UpdateStatus(); else Queue.Process(); } private static Queue Queue { get; } = new(5, UpdateStatus); - + private static void UpdateStatus() { - TranslationBuilder builder = new(Server.PlayerCount == 0 ? Instance.Translation.EmptyContent : Instance.Translation.NormalContent); - Task.Run(async () => await Client.SocketClient.SetGameAsync(builder, type:Instance.Config.ActivityType).ConfigureAwait(false)); + TranslationBuilder builder = new(Server.PlayerCount == 0 + ? Instance.Translation.EmptyContent + : Instance.Translation.NormalContent); + Task.Run(async () => await Client.SocketClient.SetGameAsync(builder, type: Instance.Config.ActivityType) + .ConfigureAwait(false)); switch (Server.PlayerCount) { case 0 when Instance.Config.IdleOnEmpty: diff --git a/DiscordLab.ConnectionLogs/Config.cs b/DiscordLab.ConnectionLogs/Config.cs index 4f147ff..7b7ba22 100644 --- a/DiscordLab.ConnectionLogs/Config.cs +++ b/DiscordLab.ConnectionLogs/Config.cs @@ -12,7 +12,7 @@ public class Config [Description("The channel where the round start logs will be sent.")] public ulong RoundStartChannelId { get; set; } = 0; - + [Description("The channel where the round end logs will be sent. Optional.")] public ulong RoundEndChannelId { get; set; } = 0; diff --git a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj index c800f07..46ef3c0 100644 --- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj +++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Events.cs b/DiscordLab.ConnectionLogs/Events.cs index 41e4e69..866c299 100644 --- a/DiscordLab.ConnectionLogs/Events.cs +++ b/DiscordLab.ConnectionLogs/Events.cs @@ -21,7 +21,7 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev) { if (!Round.IsRoundInProgress) return; - + if (Config.JoinChannelId == 0) return; @@ -30,24 +30,25 @@ public override void OnPlayerJoined(PlayerJoinedEventArgs ev) Logger.Error(LoggingUtils.GenerateMissingChannelMessage("join log", Config.JoinChannelId, Config.GuildId)); return; } - - Translation.PlayerJoin.SendToChannel(channel, new( "player", ev.Player)); + + Translation.PlayerJoin.SendToChannel(channel, new("player", ev.Player)); } - + public override void OnPlayerLeft(PlayerLeftEventArgs ev) { if (!Round.IsRoundInProgress) return; - + if (Config.LeaveChannelId == 0) return; if (!Client.TryGetOrAddChannel(Config.LeaveChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("leave log", Config.LeaveChannelId, Config.GuildId)); return; } - + Translation.PlayerLeave.SendToChannel(channel, new("player", ev.Player)); } @@ -58,16 +59,17 @@ public override void OnServerRoundStarted() if (!Client.TryGetOrAddChannel(Config.RoundStartChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundStartChannelId, + Config.GuildId)); return; } - + Translation.RoundStart.SendToChannel(channel, new() { PlayerListItem = Translation.RoundPlayers }); } - + public override void OnServerRoundEnded(RoundEndedEventArgs ev) { if (Config.RoundEndChannelId == 0) @@ -75,16 +77,17 @@ public override void OnServerRoundEnded(RoundEndedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.RoundEndChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start log", Config.RoundEndChannelId, + Config.GuildId)); return; } TranslationBuilder builder = new TranslationBuilder() - { - PlayerListItem = Translation.RoundPlayers - } - .AddCustomReplacer("winner", ev.LeadingTeam.ToString()); - + { + PlayerListItem = Translation.RoundPlayers + } + .AddCustomReplacer("winner", ev.LeadingTeam.ToString()); + Translation.RoundEnd.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Plugin.cs b/DiscordLab.ConnectionLogs/Plugin.cs index a5b329b..7741f3a 100644 --- a/DiscordLab.ConnectionLogs/Plugin.cs +++ b/DiscordLab.ConnectionLogs/Plugin.cs @@ -8,7 +8,7 @@ namespace DiscordLab.ConnectionLogs; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.ConnectionLogs"; public override string Description { get; } = "Adds logging for connection based information"; public override string Author { get; } = "LumiFae"; @@ -16,11 +16,11 @@ public class Plugin : Plugin public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); - + public override void Enable() { Instance = this; - + CustomHandlersManager.RegisterEventsHandler(Events); } @@ -28,7 +28,7 @@ public override void Disable() { CustomHandlersManager.UnregisterEventsHandler(Events); Events = null; - + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.ConnectionLogs/Translation.cs b/DiscordLab.ConnectionLogs/Translation.cs index 3913b0c..37bf7dc 100644 --- a/DiscordLab.ConnectionLogs/Translation.cs +++ b/DiscordLab.ConnectionLogs/Translation.cs @@ -11,12 +11,14 @@ public class Translation [Description("The message that will be sent when a player leaves the server.")] public MessageContent PlayerLeave { get; set; } = "`{player}` (`{playerid}`) has left the server."; - [Description("The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] + [Description( + "The message that will be sent when the round starts, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] public MessageContent RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```"; - - [Description("The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] + + [Description( + "The message that will be sent when the round ends, just before the player list. The players placeholder will be replaced with the list of players using the round start players translation.")] public MessageContent RoundEnd { get; set; } = "Round has ended with the following people: \n```{players}\n```"; - + [Description("The message that indicates what a player looks like in the round start/end message.")] public string RoundPlayers { get; set; } = "{playername} ({playerid})"; } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Config.cs b/DiscordLab.DeathLogs/Config.cs index bb45176..298d2e8 100644 --- a/DiscordLab.DeathLogs/Config.cs +++ b/DiscordLab.DeathLogs/Config.cs @@ -17,13 +17,14 @@ public class Config [Description("The channel where logs will be sent when a player dies by a teamkill.")] public ulong TeamKillChannelId { get; set; } = 0; - - [Description("If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")] + + [Description( + "If this is true, then the plugin will ignore the cuff state of the player and send the death logs to the normal death logs channel.")] public bool ScpIgnoreCuffed { get; set; } = true; [Description("The channel to send death logs to, if any.")] public ulong DamageLogChannelId { get; set; } = 0; - + [Description("Whether damage logs shouldn't be tracked if the attacker is an SCP.")] public bool IgnoreScpDamage { get; set; } = false; diff --git a/DiscordLab.DeathLogs/DamageLogs.cs b/DiscordLab.DeathLogs/DamageLogs.cs index 379d95f..f11b3c7 100644 --- a/DiscordLab.DeathLogs/DamageLogs.cs +++ b/DiscordLab.DeathLogs/DamageLogs.cs @@ -22,7 +22,7 @@ public static class DamageLogs public static SocketTextChannel Channel; private static Queue queue = new(5, SendLog); - + [CallOnLoad] public static void Register() { @@ -46,12 +46,12 @@ public static void OnHurt(PlayerHurtEventArgs ev) if (ev.DamageHandler is not StandardDamageHandler handler) return; - + if (handler.Damage <= 0) return; string type = Events.ConvertToString(ev.DamageHandler); - + // passive damage checkers, don't want these spamming console. switch (type) { @@ -59,14 +59,15 @@ public static void OnHurt(PlayerHurtEventArgs ev) case "Unknown" when Mathf.Approximately(handler.Damage, 2.1f): return; } - if (ev.Player.HasEffect() && type == "SCP-106") + + if (ev.Player.HasEffect() && type == "SCP-106") return; - if (ev.Player.HasEffect() && type == "SCP-106") + if (ev.Player.HasEffect() && type == "SCP-106") return; - if (type == "Strangled") + if (type == "Strangled") return; - - if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) + + if (ev.Player.IsSCP && ev.Attacker.IsSCP && Plugin.Instance.Config.IgnoreScpDamage) return; string log = new TranslationBuilder(Plugin.Instance.Translation.DamageLogEntry) @@ -74,9 +75,9 @@ public static void OnHurt(PlayerHurtEventArgs ev) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("damage", handler.Damage.ToString(CultureInfo.InvariantCulture)) .AddCustomReplacer("cause", type); - + DamageLogEntries.Add(log); - + queue.Process(); } @@ -86,42 +87,42 @@ public static void SendLog() { Logger.Error( LoggingUtils.GenerateMissingChannelMessage( - "damage logs", - Plugin.Instance.Config.DamageLogChannelId, + "damage logs", + Plugin.Instance.Config.DamageLogChannelId, Plugin.Instance.Config.GuildId)); return; } - - Channel.SendMessage(embeds:CreateEmbeds()); - + + Channel.SendMessage(embeds: CreateEmbeds()); + DamageLogEntries.Clear(); } public static Embed[] CreateEmbeds() { List embeds = new(); - + if (DamageLogEntries.Count == 0) return embeds.ToArray(); - + int currentIndex = 0; - + while (currentIndex < DamageLogEntries.Count) { EmbedBuilder embed = Plugin.Instance.Translation.DamageLogEmbed; - + List currentEmbedLogs = new(); int currentLength = 0; - + while (currentIndex < DamageLogEntries.Count) { string logEntry = DamageLogEntries[currentIndex]; - + int newLength = currentLength + logEntry.Length + (currentEmbedLogs.Count > 0 ? 1 : 0); - + if (newLength > EmbedBuilder.MaxDescriptionLength && currentEmbedLogs.Count > 0) break; - + if (logEntry.Length > EmbedBuilder.MaxDescriptionLength) { logEntry = logEntry.Substring(0, EmbedBuilder.MaxDescriptionLength - 3) + "..."; @@ -129,17 +130,19 @@ public static Embed[] CreateEmbeds() currentIndex++; break; } - + currentEmbedLogs.Add(logEntry); currentLength = newLength; currentIndex++; } if (currentEmbedLogs.Count <= 0) continue; - embed.Description = new TranslationBuilder(embed.Description).AddCustomReplacer("entries", string.Join("\n", currentEmbedLogs)); + embed.Description = + new TranslationBuilder(embed.Description).AddCustomReplacer("entries", + string.Join("\n", currentEmbedLogs)); embeds.Add(embed.Build()); } - + return embeds.ToArray(); } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj index c800f07..46ef3c0 100644 --- a/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj +++ b/DiscordLab.DeathLogs/DiscordLab.DeathLogs.csproj @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Events.cs b/DiscordLab.DeathLogs/Events.cs index 064d152..419d8c3 100644 --- a/DiscordLab.DeathLogs/Events.cs +++ b/DiscordLab.DeathLogs/Events.cs @@ -20,7 +20,7 @@ public static class Events public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - + // have to do this here over CustomEventsHandler because easier to maintain different logs in this case. [CallOnLoad] public static void Register() @@ -50,7 +50,8 @@ public static void OnTeamKill(PlayerDyingEventArgs ev) if (!Client.TryGetOrAddChannel(Config.TeamKillChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("team kill logs", Config.TeamKillChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("team kill logs", Config.TeamKillChannelId, + Config.GuildId)); return; } @@ -60,7 +61,7 @@ public static void OnTeamKill(PlayerDyingEventArgs ev) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)) .AddCustomReplacer("role", ev.Player.Team.GetFaction().ToString()); - + Translation.TeamKill.SendToChannel(channel, builder); } @@ -74,7 +75,8 @@ public static void OnCuffKill(PlayerDyingEventArgs ev) if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuff kill logs", Config.CuffedChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuff kill logs", Config.CuffedChannelId, + Config.GuildId)); return; } @@ -83,7 +85,7 @@ public static void OnCuffKill(PlayerDyingEventArgs ev) .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - + Translation.CuffedPlayerDeath.SendToChannel(channel, builder); } @@ -92,7 +94,7 @@ public static void OnDeath(PlayerDyingEventArgs ev) if (ev.Attacker == null || ev.Player.IsDisarmed || ev.Attacker.Team.GetFaction() == ev.Player.Team.GetFaction()) return; - + if (Config.ChannelId == 0) return; @@ -107,7 +109,7 @@ public static void OnDeath(PlayerDyingEventArgs ev) .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Attacker) .AddCustomReplacer("cause", ConvertToString(ev.DamageHandler)); - + Translation.PlayerDeath.SendToChannel(channel, builder); } @@ -115,17 +117,18 @@ public static void OnOwnDeath(PlayerDyingEventArgs ev) { if (ev.Attacker != null) return; - + if (Config.SelfChannelId == 0) return; if (!Client.TryGetOrAddChannel(Config.SelfChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("self kill logs", Config.SelfChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("self kill logs", Config.SelfChannelId, Config.GuildId)); return; } - + string converted = ConvertToString(ev.DamageHandler); // usually because of disconnect, only way to really track rn @@ -135,10 +138,10 @@ public static void OnOwnDeath(PlayerDyingEventArgs ev) TranslationBuilder builder = new TranslationBuilder() .AddPlayer("player", ev.Player) .AddCustomReplacer("cause", converted); - + Translation.PlayerDeathSelf.SendToChannel(channel, builder); } - + private static Dictionary _translations = new() { { DeathTranslations.Asphyxiated.Id, "Asphyxiation" }, @@ -170,7 +173,7 @@ public static void OnOwnDeath(PlayerDyingEventArgs ev) { DeathTranslations.MarshmallowMan.Id, "Marshmellow" }, { DeathTranslations.Scp1344.Id, "Severed Eyes" }, }; - + internal static string ConvertToString(DamageHandlerBase handler) { switch (handler) @@ -221,7 +224,7 @@ internal static string ConvertToString(DamageHandlerBase handler) if (_translations.TryGetValue(translation.Id, out string s)) return s; - + break; } case FirearmDamageHandler firearm: diff --git a/DiscordLab.DeathLogs/Plugin.cs b/DiscordLab.DeathLogs/Plugin.cs index 4a898db..cf730f8 100644 --- a/DiscordLab.DeathLogs/Plugin.cs +++ b/DiscordLab.DeathLogs/Plugin.cs @@ -7,24 +7,24 @@ namespace DiscordLab.DeathLogs; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.DeathLogs"; public override string Description { get; } = "Adds death logging capabilities"; public override string Author { get; } = "LumiFae"; public override Version Version => GetType().Assembly.GetName().Version; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); - + public override void Enable() { Instance = this; - + CallOnLoadAttribute.Load(); } public override void Disable() { CallOnUnloadAttribute.Unload(); - + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.DeathLogs/Translation.cs b/DiscordLab.DeathLogs/Translation.cs index 4b4b028..24d6ee6 100644 --- a/DiscordLab.DeathLogs/Translation.cs +++ b/DiscordLab.DeathLogs/Translation.cs @@ -16,7 +16,8 @@ public class Translation [Description( "The message that will be sent when a player dies by their own actions, or just they died because of something else.")] - public MessageContent PlayerDeathSelf { get; set; } = "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; + public MessageContent PlayerDeathSelf { get; set; } = + "`{player}` (`{playerrole}`) has died. They died from: `{cause}`"; [Description("The message that will be sent when a player dies due to someone on their own team.")] public MessageContent TeamKill { get; set; } = @@ -31,5 +32,6 @@ public class Translation }; [Description("What each instance of damage will look like in the logs.")] - public string DamageLogEntry { get; set; } = "{timetlong} | `{player}` did `{damage}` damage to `{target}` | Cause: `{cause}`"; + public string DamageLogEntry { get; set; } = + "{timetlong} | `{player}` did `{damage}` damage to `{target}` | Cause: `{cause}`"; } \ No newline at end of file diff --git a/DiscordLab.Dependency/DiscordLab.Dependency.csproj b/DiscordLab.Dependency/DiscordLab.Dependency.csproj index 236ea0b..c7ba73a 100644 --- a/DiscordLab.Dependency/DiscordLab.Dependency.csproj +++ b/DiscordLab.Dependency/DiscordLab.Dependency.csproj @@ -1,11 +1,11 @@ - - net48 - enable - disable - 12 - x64 - true - 2.0.0 - + + net48 + enable + disable + 12 + x64 + true + 2.0.0 + \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Ban.cs b/DiscordLab.Moderation/Commands/Ban.cs index d1ff0a4..b4fb1ea 100644 --- a/DiscordLab.Moderation/Commands/Ban.cs +++ b/DiscordLab.Moderation/Commands/Ban.cs @@ -43,7 +43,7 @@ public class Ban : AutocompleteCommand }; protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -53,22 +53,22 @@ public override async Task Run(SocketSlashCommand command) string reason = (string)command.Data.Options.ElementAt(2).Value; TranslationBuilder successBuilder = new TranslationBuilder(Translation.BanSuccess) - { - Time = TempMuteManager.GetExpireDate(duration) - } - .AddCustomReplacer("userid", userId); + { + Time = TempMuteManager.GetExpireDate(duration) + } + .AddCustomReplacer("userid", userId); TranslationBuilder failBuilder = new TranslationBuilder(Translation.BanFailure) .AddCustomReplacer("userid", userId); - + if (!CommandUtils.TryGetPlayerFromUnparsed(userId, out Player player)) { - bool result = userId.Contains("@") ? - Server.BanUserId(userId, reason, duration) : - Server.BanIpAddress(userId, reason, duration); + bool result = userId.Contains("@") + ? Server.BanUserId(userId, reason, duration) + : Server.BanIpAddress(userId, reason, duration); await command.ModifyOriginalResponseAsync(m => m.Content = !result ? failBuilder : successBuilder); - + return; } diff --git a/DiscordLab.Moderation/Commands/Mute.cs b/DiscordLab.Moderation/Commands/Mute.cs index 9dcfbbe..75b8ae9 100644 --- a/DiscordLab.Moderation/Commands/Mute.cs +++ b/DiscordLab.Moderation/Commands/Mute.cs @@ -37,7 +37,7 @@ public class Mute : AutocompleteCommand }; protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -55,12 +55,12 @@ public override async Task Run(SocketSlashCommand command) string duration = (string)command.Data.Options.ElementAt(1).Value; DateTime time = TempMuteManager.GetExpireDate(duration); TempMuteManager.MutePlayer(player, time); - + builder = new TranslationBuilder(Translation.TempMuteSuccess, "player", player) - { - Time = time - } - .AddCustomReplacer("duration", duration); + { + Time = time + } + .AddCustomReplacer("duration", duration); await command.ModifyOriginalResponseAsync(m => m.Content = builder); return; diff --git a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs index d718b5d..96a67fb 100644 --- a/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs +++ b/DiscordLab.Moderation/Commands/TempMuteRemoteAdmin.cs @@ -10,13 +10,13 @@ public class TempMuteRemoteAdmin : ICommand, IUsageProvider public string Command { get; } = "tempmute"; public string[] Aliases { get; } = ["tempm", "mutet", "temporarymute", "mutetemp", "mutetemporary"]; public string Description { get; } = "Temporarily mutes a user."; - + public string[] Usage { get; } = [ "player", "duration" ]; - + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission([ @@ -25,7 +25,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s PlayerPermissions.PlayersManagement ], out response)) return false; - + if (arguments.Count < 2) { response = "To execute this command provide at least 2 arguments!\nUsage: " + this.DisplayCommandUsage(); @@ -43,14 +43,15 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s } DateTime time = TempMuteManager.GetExpireDate(arguments.At(1)); - + TempMuteManager.MutePlayer(target, time, player); - TranslationBuilder builder = new TranslationBuilder(Plugin.Instance.Translation.TempMuteSuccess, "player", target) - { - Time = time - } - .AddCustomReplacer("duration", () => arguments.At(1)); + TranslationBuilder builder = + new TranslationBuilder(Plugin.Instance.Translation.TempMuteSuccess, "player", target) + { + Time = time + } + .AddCustomReplacer("duration", () => arguments.At(1)); response = builder; return true; diff --git a/DiscordLab.Moderation/Commands/Unban.cs b/DiscordLab.Moderation/Commands/Unban.cs index f371ced..f3630ab 100644 --- a/DiscordLab.Moderation/Commands/Unban.cs +++ b/DiscordLab.Moderation/Commands/Unban.cs @@ -27,7 +27,7 @@ public class Unban : AutocompleteCommand }; protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -38,12 +38,12 @@ public override async Task Run(SocketSlashCommand command) TranslationBuilder builder = new TranslationBuilder(Translation.UnbanSuccess) .AddCustomReplacer("userid", id); - - await command.ModifyOriginalResponseAsync(m => - m.Content = + + await command.ModifyOriginalResponseAsync(m => + m.Content = builder); } - + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { IEnumerable response = @@ -51,6 +51,9 @@ public override async Task Autocomplete(SocketAutocompleteInteraction autocomple ..BanHandler.GetBans(BanHandler.BanType.UserId), ..BanHandler.GetBans(BanHandler.BanType.IP) ]; - await autocomplete.RespondAsync(response.Where(x => x.Id.Contains((string)autocomplete.Data.Current.Value) || x.OriginalName.Contains((string)autocomplete.Data.Current.Value)).Take(25).Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); + await autocomplete.RespondAsync(response + .Where(x => x.Id.Contains((string)autocomplete.Data.Current.Value) || + x.OriginalName.Contains((string)autocomplete.Data.Current.Value)).Take(25) + .Select(x => new AutocompleteResult($"{x.OriginalName} ({x.Id})", x.Id))); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Commands/Unmute.cs b/DiscordLab.Moderation/Commands/Unmute.cs index c88eeb4..6b1d968 100644 --- a/DiscordLab.Moderation/Commands/Unmute.cs +++ b/DiscordLab.Moderation/Commands/Unmute.cs @@ -29,7 +29,7 @@ public class Unmute : AutocompleteCommand }; protected override ulong GuildId { get; } = Plugin.Instance.Config.GuildId; - + public override async Task Run(SocketSlashCommand command) { await command.DeferAsync(); @@ -41,12 +41,12 @@ public override async Task Run(SocketSlashCommand command) } TempMuteManager.RemoveMute(player); - - await command.ModifyOriginalResponseAsync(m => - m.Content = + + await command.ModifyOriginalResponseAsync(m => + m.Content = new TranslationBuilder(Translation.UnmuteSuccess, "player", player)); } - + public override async Task Autocomplete(SocketAutocompleteInteraction autocomplete) { await autocomplete.RespondAsync(Plugin.PlayersAutocompleteResults(autocomplete.Data.Current.Value)); diff --git a/DiscordLab.Moderation/DiscordLab.Moderation.csproj b/DiscordLab.Moderation/DiscordLab.Moderation.csproj index b09a6a8..998722f 100644 --- a/DiscordLab.Moderation/DiscordLab.Moderation.csproj +++ b/DiscordLab.Moderation/DiscordLab.Moderation.csproj @@ -10,10 +10,10 @@ - + - + - + \ No newline at end of file diff --git a/DiscordLab.Moderation/Events.cs b/DiscordLab.Moderation/Events.cs index 9fb97b2..1744d73 100644 --- a/DiscordLab.Moderation/Events.cs +++ b/DiscordLab.Moderation/Events.cs @@ -17,12 +17,12 @@ public class Events : CustomEventsHandler public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - + public override void OnPlayerUnmuting(PlayerUnmutingEventArgs ev) { // otherwise OnPlayerUnmuted will get triggered twice. ev.IsAllowed = false; - + TempMuteManager.RemoveMute(ev.Player, ev.Issuer); } @@ -33,14 +33,15 @@ public override void OnPlayerUnmuted(PlayerUnmutedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.UnmuteLogChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unmute logs", Config.UnmuteLogChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unmute logs", Config.UnmuteLogChannelId, + Config.GuildId)); return; } TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Player) .AddPlayer("player", ev.Issuer); - + Translation.UnmuteLog.SendToChannel(channel, builder); } @@ -51,24 +52,25 @@ public override void OnPlayerMuted(PlayerMutedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.MuteLogChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("mute logs", Config.MuteLogChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("mute logs", Config.MuteLogChannelId, Config.GuildId)); return; } - + MessageContent translation = Translation.PermMuteLog; if (TempMuteManager.MuteConfig.Mutes.TryGetValue(ev.Player.UserId, out DateTime time)) { translation = Translation.TempMuteLog; } - + TranslationBuilder builder = new TranslationBuilder - { - Time = time - } + { + Time = time + } .AddPlayer("player", ev.Issuer) .AddPlayer("target", ev.Player); - + translation.SendToChannel(channel, builder); } @@ -93,10 +95,10 @@ public override void OnPlayerBanned(PlayerBannedEventArgs ev) } .AddCustomReplacer("userid", ev.PlayerId) .AddCustomReplacer("reason", ev.Reason); - + Translation.BanLogEmbed.SendToChannel(channel, builder); } - + public override void OnServerBanRevoked(BanRevokedEventArgs ev) { if (Config.UnbanLogChannelId == 0) @@ -104,7 +106,8 @@ public override void OnServerBanRevoked(BanRevokedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.UnbanLogChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("unban logs", Config.UnbanLogChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("unban logs", Config.UnbanLogChannelId, Config.GuildId)); return; } @@ -112,7 +115,7 @@ public override void OnServerBanRevoked(BanRevokedEventArgs ev) .AddCustomReplacer("userid", ev.BanDetails.Id) .AddCustomReplacer("username", ev.BanDetails.OriginalName) .AddCustomReplacer("playerid", ev.BanDetails.Issuer); - + Translation.UnbanLog.SendToChannel(channel, builder); } } \ No newline at end of file diff --git a/DiscordLab.Moderation/Plugin.cs b/DiscordLab.Moderation/Plugin.cs index 7420d85..55974b8 100644 --- a/DiscordLab.Moderation/Plugin.cs +++ b/DiscordLab.Moderation/Plugin.cs @@ -14,7 +14,7 @@ namespace DiscordLab.Moderation; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.Moderation"; public override string Description { get; } = "Adds logging and commands for moderation based operations"; public override string Author { get; } = "LumiFae"; @@ -24,40 +24,43 @@ public class Plugin : Plugin public TempMuteConfig MuteConfig; public Events Events = new(); - + public override void Enable() { Instance = this; - + CallOnLoadAttribute.Load(); - + if (Config.AddCommands) SlashCommand.FindAll(); - + if (Config.AddTempMuteCommand) CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(new TempMuteRemoteAdmin()); - + CustomHandlersManager.RegisterEventsHandler(Events); } public override void Disable() { CustomHandlersManager.UnregisterEventsHandler(Events); - + CallOnUnloadAttribute.Unload(); - + Events = null; - + Instance = null; } public override void LoadConfigs() { this.TryLoadConfig("mute-config.yml", out MuteConfig); - + base.LoadConfigs(); } public static IEnumerable PlayersAutocompleteResults(object current) => - Player.ReadyList.Where(p => p.Nickname.Contains((string)current) || p.UserId.Contains((string)current) || (int.TryParse((string)current, out int id) && p.PlayerId == id)).Take(25).Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); + Player.ReadyList + .Where(p => p.Nickname.Contains((string)current) || p.UserId.Contains((string)current) || + (int.TryParse((string)current, out int id) && p.PlayerId == id)).Take(25) + .Select(p => new AutocompleteResult(p.Nickname, p.PlayerId)); } \ No newline at end of file diff --git a/DiscordLab.Moderation/TempMuteManager.cs b/DiscordLab.Moderation/TempMuteManager.cs index 80106b6..e2b0ab2 100644 --- a/DiscordLab.Moderation/TempMuteManager.cs +++ b/DiscordLab.Moderation/TempMuteManager.cs @@ -11,9 +11,9 @@ namespace DiscordLab.Moderation; public static class TempMuteManager { public static TempMuteConfig MuteConfig => Plugin.Instance.MuteConfig; - + public static Dictionary Handles { get; private set; } = new(); - + [CallOnLoad] public static void Start() { @@ -26,6 +26,7 @@ public static void Start() RemoveMute(dict.Key); continue; } + AddHandle(dict.Key, time); } } @@ -43,7 +44,7 @@ public static void Stop() public static void AddHandle(string userId, DateTime time) => AddHandle(userId, time - DateTime.Now); - + public static void AddHandle(string userId, TimeSpan time) => Handles.Add(userId, Timing.CallDelayed((float)time.TotalSeconds, () => RemoveMute(userId))); @@ -61,17 +62,17 @@ public static DateTime GetExpireDate(string duration) => GetExpireDate(Misc.RelativeTimeToSeconds(duration, 60)); public static DateTime GetExpireDate(long duration) => DateTime.Now.AddSeconds(duration); - - public static void MutePlayer(Player player, DateTime time, Player sender = null) => + + public static void MutePlayer(Player player, DateTime time, Player sender = null) => MutePlayer(player.UserId, time, sender?.ReferenceHub); public static void MutePlayer(string player, DateTime time, ReferenceHub sender = null) { sender ??= Server.Host?.ReferenceHub; VoiceChatMutes.IssueLocalMute(player); - if(Player.TryGet(player, out Player p) && sender) + if (Player.TryGet(player, out Player p) && sender) PlayerEvents.OnMuted(new(p.ReferenceHub, sender, false)); - + MuteConfig.Mutes.Add(player, time); AddHandle(player, time); Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); @@ -88,10 +89,10 @@ public static void RemoveMute(string player, ReferenceHub sender = null) sender ??= Server.Host?.ReferenceHub; VoiceChatMutes.RevokeLocalMute(player); - + MuteConfig.Mutes.Remove(player); Plugin.Instance.SaveConfig(MuteConfig, "mute-config.yml"); - if(Player.TryGet(player, out Player p) && sender) + if (Player.TryGet(player, out Player p) && sender) PlayerEvents.OnUnmuted(new(p.ReferenceHub, sender, false)); if (Handles.ContainsKey(player)) diff --git a/DiscordLab.Moderation/Translation.cs b/DiscordLab.Moderation/Translation.cs index 4b5de5d..51229c4 100644 --- a/DiscordLab.Moderation/Translation.cs +++ b/DiscordLab.Moderation/Translation.cs @@ -34,16 +34,19 @@ public class Translation public string UnbanUserOptionDescription { get; set; } = "The user to unban"; public string InvalidUser { get; set; } = "Please provide a valid user to use this command on."; - - public string TempMuteSuccess { get; set; } = "Player {player} has been temporarily muted for {duration}. They will get unmuted at {timef}"; + + public string TempMuteSuccess { get; set; } = + "Player {player} has been temporarily muted for {duration}. They will get unmuted at {timef}"; public string UnmuteSuccess { get; set; } = "Player {player} has been successfully unmuted."; - + public string PermMuteSuccess { get; set; } = "Player {player} has been muted."; - public string BanFailure { get; set; } = "Failed to ban {userid}. Please make sure the data is valid and try again..."; + public string BanFailure { get; set; } = + "Failed to ban {userid}. Please make sure the data is valid and try again..."; - public string BanSuccess { get; set; } = "Successfully banned {userid} for {reason}. They will get unbanned in {timer}"; + public string BanSuccess { get; set; } = + "Successfully banned {userid} for {reason}. They will get unbanned in {timer}"; public string UnbanSuccess { get; set; } = "Player {userid} has been unbanned."; @@ -85,6 +88,7 @@ public class Translation ] }; - [Description("Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] + [Description( + "Normal player things may not work here, but playerid always will, unless somehow banned by something without an ID.")] public MessageContent UnbanLog { get; set; } = "Player {username} ({userid}) has been unbanned by {playerid}"; } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj index c800f07..46ef3c0 100644 --- a/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj +++ b/DiscordLab.RoundLogs/DiscordLab.RoundLogs.csproj @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Events.cs b/DiscordLab.RoundLogs/Events.cs index 6abfb66..bf0d919 100644 --- a/DiscordLab.RoundLogs/Events.cs +++ b/DiscordLab.RoundLogs/Events.cs @@ -19,11 +19,12 @@ public class Events : CustomEventsHandler public static Config Config => Plugin.Instance.Config; public static Translation Translation => Plugin.Instance.Translation; - + public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) { if (ev.ChangeReason is RoleChangeReason.Respawn or RoleChangeReason.RoundStart - or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died or RoleChangeReason.Destroyed) + or RoleChangeReason.RespawnMiniwave or RoleChangeReason.LateJoin or RoleChangeReason.Died + or RoleChangeReason.Destroyed) return; SocketTextChannel channel; @@ -33,7 +34,7 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) .AddCustomReplacer("newrole", ev.NewRole.RoleName) .AddCustomReplacer("reason", ev.ChangeReason.ToString()) .AddCustomReplacer("spawnflags", string.Join(", ", ev.SpawnFlags.GetFlags())); - + if (ev.NewRole.Team == ev.OldRole.GetTeam() && ev.NewRole.Team == Team.SCPs) { if (Config.ScpSwapChannelId == 0) @@ -41,22 +42,24 @@ public override void OnPlayerChangedRole(PlayerChangedRoleEventArgs ev) channel = Client.GetOrAddChannel(Config.ScpSwapChannelId); if (channel == null) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("SCP Swap logs", Config.ScpSwapChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("SCP Swap logs", Config.ScpSwapChannelId, + Config.GuildId)); return; } Translation.ScpSwapLog.SendToChannel(channel, builder); return; } - + if (Config.RoleChangeChannelId == 0) return; if (!Client.TryGetOrAddChannel(Config.RoleChangeChannelId, out channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("Role change logs", Config.RoleChangeChannelId, + Config.GuildId)); } - + Translation.RoleChangeLog.SendToChannel(channel, builder); } @@ -76,13 +79,13 @@ public override void OnServerWaveRespawned(WaveRespawnedEventArgs ev) } MessageContent content = isFoundation ? Translation.NtfSpawn : Translation.ChaosSpawn; - + TranslationBuilder builder = new() { PlayerListItem = Translation.PlayerListItem, PlayerList = ev.Players }; - + content.SendToChannel(channel, builder); } @@ -93,14 +96,15 @@ public override void OnPlayerCuffed(PlayerCuffedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.CuffedChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("cuffed logs", Config.CuffedChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("cuffed logs", Config.CuffedChannelId, Config.GuildId)); return; } TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Target) .AddPlayer("player", ev.Player); - + Translation.Cuffed.SendToChannel(channel, builder); } @@ -111,14 +115,15 @@ public override void OnPlayerUncuffed(PlayerUncuffedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.UncuffedChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("uncuff logs", Config.CuffedChannelId, Config.GuildId)); + Logger.Error( + LoggingUtils.GenerateMissingChannelMessage("uncuff logs", Config.CuffedChannelId, Config.GuildId)); return; } TranslationBuilder builder = new TranslationBuilder() .AddPlayer("target", ev.Target) .AddPlayer("player", ev.Player); - + Translation.Uncuffed.SendToChannel(channel, builder); } @@ -129,10 +134,11 @@ public override void OnServerRoundStarted() if (!Client.TryGetOrAddChannel(Config.RoundStartedChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round start logs", Config.CuffedChannelId, + Config.GuildId)); return; } - + Translation.RoundStart.SendToChannel(channel, new()); } @@ -143,13 +149,14 @@ public override void OnServerRoundEnded(RoundEndedEventArgs ev) if (!Client.TryGetOrAddChannel(Config.RoundEndedChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round ended logs", Config.CuffedChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("round ended logs", Config.CuffedChannelId, + Config.GuildId)); return; } TranslationBuilder builder = new TranslationBuilder() .AddCustomReplacer("winner", ev.LeadingTeam.ToString()); - + Translation.RoundEnd.SendToChannel(channel, builder); } @@ -160,10 +167,11 @@ public override void OnServerLczDecontaminationStarted() if (!Client.TryGetOrAddChannel(Config.DecontaminationChannelId, out SocketTextChannel channel)) { - Logger.Error(LoggingUtils.GenerateMissingChannelMessage("decontamination logs", Config.DecontaminationChannelId, Config.GuildId)); + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("decontamination logs", + Config.DecontaminationChannelId, Config.GuildId)); return; } - + Translation.Decontamination.SendToChannel(channel, new()); } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Plugin.cs b/DiscordLab.RoundLogs/Plugin.cs index b917052..f7dad7a 100644 --- a/DiscordLab.RoundLogs/Plugin.cs +++ b/DiscordLab.RoundLogs/Plugin.cs @@ -9,7 +9,7 @@ namespace DiscordLab.RoundLogs; public class Plugin : Plugin { public static Plugin Instance; - + public override string Name { get; } = "DiscordLab.RoundLogs"; public override string Description { get; } = "Allows you to log specific details about the round."; public override string Author { get; } = "LumiFae"; @@ -17,11 +17,11 @@ public class Plugin : Plugin public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); public Events Events = new(); - + public override void Enable() { Instance = this; - + CustomHandlersManager.RegisterEventsHandler(Events); } @@ -30,7 +30,7 @@ public override void Disable() CustomHandlersManager.UnregisterEventsHandler(Events); Events = null; - + Instance = null; } } \ No newline at end of file diff --git a/DiscordLab.RoundLogs/Translation.cs b/DiscordLab.RoundLogs/Translation.cs index c03a252..a277f69 100644 --- a/DiscordLab.RoundLogs/Translation.cs +++ b/DiscordLab.RoundLogs/Translation.cs @@ -7,7 +7,8 @@ public class Translation { public MessageContent ScpSwapLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}."; - public MessageContent RoleChangeLog { get; set; } = "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; + public MessageContent RoleChangeLog { get; set; } = + "Player {player} has swapped from {oldrole} to {newrole}. They were swapped because of {reason}"; public MessageContent NtfSpawn { get; set; } = "NTF has respawned with the following players:\n{players}"; @@ -19,7 +20,8 @@ public class Translation public MessageContent Uncuffed { get; set; } = "Player {target} has been uncuffed by {player}"; - [Description("This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] + [Description( + "This doesn't come with players as that is available in DiscordLab.ConnectionLogs. Same applies to RoundEnd")] public MessageContent RoundStart { get; set; } = "Round has started."; public MessageContent RoundEnd { get; set; } = "Round has ended, {winner} has won the round."; diff --git a/DiscordLab.StatusChannel/Events.cs b/DiscordLab.StatusChannel/Events.cs index 6051faa..b208fd6 100644 --- a/DiscordLab.StatusChannel/Events.cs +++ b/DiscordLab.StatusChannel/Events.cs @@ -60,7 +60,8 @@ public static void Process() Queue.Process(); } - public static MessageContent UsableContent => Player.ReadyList.Any() ? Translation.Content : Translation.EmptyContent; + public static MessageContent UsableContent => + Player.ReadyList.Any() ? Translation.Content : Translation.EmptyContent; public static void EditMessage() { diff --git a/README.md b/README.md index d4539ab..ad68192 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # DiscordLab ![Downloads](https://img.shields.io/github/downloads/JayXTQ/DiscordLab/total) The modular Discord bot plugin for SCP: Secret Laboratory servers. With DiscordLab you can pick and choose what -features you want on your Discord server/bot. This allows you to have a Discord bot that is tailored to your server's needs. +features you want on your Discord server/bot. This allows you to have a Discord bot that is tailored to your server's +needs. ## Get started @@ -11,8 +12,10 @@ To get started, check out our installation guide: https://discordlab.jxtq.moe/ge ### Modular -DiscordLab easily allows users to only have a subset of features and doesn't require the server to be tracking loads of events that will never even be logged. -Your server (or other plugin) developers can easily integrate with the bot as well because of the [NuGet package](https://www.nuget.org/packages/DiscordLab). +DiscordLab easily allows users to only have a subset of features and doesn't require the server to be tracking loads of +events that will never even be logged. +Your server (or other plugin) developers can easily integrate with the bot as well because of +the [NuGet package](https://www.nuget.org/packages/DiscordLab). Go to the [API section](#api) for more information. ### Logging for events @@ -21,28 +24,35 @@ DiscordLab's modules track loads of different kinds of events and sends them dir ### Customisable messages -DiscordLab has a feature that makes it so messages can easily be edited from their default to make it so you can have an embed, raw message content or both! +DiscordLab has a feature that makes it so messages can easily be edited from their default to make it so you can have an +embed, raw message content or both! -There is also large variety of placeholders that can be used, which will then be replaced by DiscordLab before sending out the message. +There is also large variety of placeholders that can be used, which will then be replaced by DiscordLab before sending +out the message. ### Support for multiple servers/channels -DiscordLab allows for you to put channel and guild IDs directly into configs that allows you to seperate logs however you want, +DiscordLab allows for you to put channel and guild IDs directly into configs that allows you to seperate logs however +you want, with each trackable event being assigned a separate channel config option. If you want damage logs and death logs to be in separate channels, they can be routed to different channels. ### Commands -Some DiscordLab modules come with slash commands (can also be disabled) that can be used within Discord, i.e. in `DiscordLab.Administration` there is the `/send` command that +Some DiscordLab modules come with slash commands (can also be disabled) that can be used within Discord, i.e. in +`DiscordLab.Administration` there is the `/send` command that allows admins to send commands to your server. -*Slash commands on DiscordLab do not have their own permission system, commands that should be hidden are hidden behind some default permissions, if you wish to edit the -permissions, you can read up on how to on this [Discord blog post](https://discord.com/blog/slash-commands-permissions-discord-apps-bots)* +*Slash commands on DiscordLab do not have their own permission system, commands that should be hidden are hidden behind +some default permissions, if you wish to edit the +permissions, you can read up on how to on +this [Discord blog post](https://discord.com/blog/slash-commands-permissions-discord-apps-bots)* ### Moderation Utilities -DiscordLab.Moderation comes with commands and utilities to better help with mutes, including adding the functionality of temporary mutes. Can be done via RA commands or Discord. +DiscordLab.Moderation comes with commands and utilities to better help with mutes, including adding the functionality of +temporary mutes. Can be done via RA commands or Discord. ## API From 8963300c1d5d4f9c758988e76be906fae88af128 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 17:33:35 +0100 Subject: [PATCH 61/68] feat: server shutdown logs --- DiscordLab.Administration/Config.cs | 3 +++ DiscordLab.Administration/Events.cs | 16 ++++++++++++++++ DiscordLab.Administration/Translation.cs | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/DiscordLab.Administration/Config.cs b/DiscordLab.Administration/Config.cs index 04007a9..b85d848 100644 --- a/DiscordLab.Administration/Config.cs +++ b/DiscordLab.Administration/Config.cs @@ -9,6 +9,9 @@ public class Config [Description("The channel to send server start logs")] public ulong ServerStartChannelId { get; set; } = 0; + [Description("Where server shutdown logs should be sent")] + public ulong ServerShutdownChannelId { get; set; } = 0; + [Description("The channel to send error logs")] public ulong ErrorLogChannelId { get; set; } = 0; diff --git a/DiscordLab.Administration/Events.cs b/DiscordLab.Administration/Events.cs index ae5c71c..8fb9efb 100644 --- a/DiscordLab.Administration/Events.cs +++ b/DiscordLab.Administration/Events.cs @@ -26,6 +26,7 @@ public class Events : CustomEventsHandler public static void Load() { ServerEvents.WaitingForPlayers += OnServerStart; + Shutdown.OnQuit += OnServerQuit; IsSubscribed = true; } @@ -34,9 +35,24 @@ public static void Unload() { if (!IsSubscribed) return; ServerEvents.WaitingForPlayers -= OnServerStart; + Shutdown.OnQuit -= OnServerQuit; IsSubscribed = false; } + public static void OnServerQuit() + { + if (Config.ServerShutdownChannelId == 0) + return; + + if (!Client.TryGetOrAddChannel(Config.ServerShutdownChannelId, out SocketTextChannel channel)) + { + Logger.Error(LoggingUtils.GenerateMissingChannelMessage("server quit logs", Config.ServerShutdownChannelId, Config.GuildId)); + return; + } + + Translation.ServerShutdown.SendToChannel(channel, new()); + } + public static void OnServerStart() { ServerEvents.WaitingForPlayers -= OnServerStart; diff --git a/DiscordLab.Administration/Translation.cs b/DiscordLab.Administration/Translation.cs index 881deb3..4347dd9 100644 --- a/DiscordLab.Administration/Translation.cs +++ b/DiscordLab.Administration/Translation.cs @@ -1,5 +1,3 @@ -using System.ComponentModel; -using Discord; using DiscordLab.Bot.API.Features; namespace DiscordLab.Administration; @@ -8,6 +6,8 @@ public class Translation { public MessageContent ServerStart { get; set; } = "Server has started"; + public MessageContent ServerShutdown { get; set; } = "Server has shutdown"; + public MessageContent ErrorLog { get; set; } = "An error has occured:\n{error}"; public MessageContent RemoteAdmin { get; set; } = From c7f88889a7d93305aec025ab4957a00d26780be1 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 18:03:13 +0100 Subject: [PATCH 62/68] feat: more embed options --- .../API/Features/Embed/EmbedBuilder.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index 50de2db..b9036ab 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -55,6 +55,36 @@ public string? Color } } + /// + /// Gets or sets the thumbnail URL of the embed. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? ThumbnailUrl + { + get => Builder.ThumbnailUrl; + set => Builder.ThumbnailUrl = value; + } + + /// + /// Gets or sets the image URL of the embed. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? ImageUrl + { + get => Builder.ImageUrl; + set => Builder.ImageUrl = value; + } + + /// + /// Gets or sets the URL of the embed. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? Url + { + get => Builder.Url; + set => Builder.Url = value; + } + [YamlIgnore] private Discord.EmbedBuilder Builder { get; } = new(); From dafd35338505ca17d9870d8450ad2f65b841e7e1 Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 23:36:17 +0100 Subject: [PATCH 63/68] feat: more embed options --- .../API/Features/Embed/EmbedAuthorBuilder.cs | 60 ++++++++++++ .../API/Features/Embed/EmbedBuilder.cs | 91 +++++++++++-------- .../API/Features/Embed/EmbedFieldBuilder.cs | 32 +++++-- .../API/Features/Embed/EmbedFooterBuilder.cs | 53 +++++++++++ 4 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs create mode 100644 DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs new file mode 100644 index 0000000..5a1d8bf --- /dev/null +++ b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs @@ -0,0 +1,60 @@ +using YamlDotNet.Serialization; + +namespace DiscordLab.Bot.API.Features.Embed; + +/// +/// Contains information about an author field in an embed. +/// +public class EmbedAuthorBuilder +{ + /// + /// Initializes a new instance of the class. + /// + public EmbedAuthorBuilder() + { + Base = new(); + } + + /// + /// Initializes a new instance of the class. + /// Replaces the base with the instance. + /// + /// The instance. + public EmbedAuthorBuilder(Discord.EmbedAuthorBuilder builder) + { + Base = builder; + } + + /// + /// Gets or sets the author name. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? Name + { + get => Base.Name; + set => Base.Name = value; + } + + /// + /// Gets or sets the icon URL. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? IconUrl + { + get => Base.IconUrl; + set => Base.IconUrl = value; + } + + /// + /// Gets or sets the URL. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? Url + { + get => Base.Url; + set => Base.Url = value; + } + + [YamlIgnore] + public Discord.EmbedAuthorBuilder Base { get; init; } +} \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index b9036ab..b70078b 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -12,8 +12,8 @@ public class EmbedBuilder /// public string Title { - get => Builder.Title; - set => Builder.Title = value; + get => Base.Title; + set => Base.Title = value; } /// @@ -21,8 +21,8 @@ public string Title /// public string Description { - get => Builder.Description; - set => Builder.Description = value; + get => Base.Description; + set => Base.Description = value; } /// @@ -31,9 +31,8 @@ public string Description [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public IEnumerable Fields { - get => Builder.Fields.Select(x => new EmbedFieldBuilder - { Name = x.Name, Value = x.Value.ToString(), IsInline = x.IsInline }); - set => Builder.Fields = value.Select(x => x.Builder).ToList(); + get => Base.Fields.Select(x => new EmbedFieldBuilder(x)); + set => Base.Fields = value.Select(x => x.Base).ToList(); } /// @@ -42,16 +41,16 @@ public IEnumerable Fields [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string? Color { - get => Builder.Color?.ToString(); + get => Base.Color?.ToString(); set { if (value == null) { - Builder.Color = null; + Base.Color = null; return; } - Builder.Color = Discord.Color.Parse(value); + Base.Color = Discord.Color.Parse(value); } } @@ -61,8 +60,8 @@ public string? Color [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string? ThumbnailUrl { - get => Builder.ThumbnailUrl; - set => Builder.ThumbnailUrl = value; + get => Base.ThumbnailUrl; + set => Base.ThumbnailUrl = value; } /// @@ -71,8 +70,8 @@ public string? ThumbnailUrl [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string? ImageUrl { - get => Builder.ImageUrl; - set => Builder.ImageUrl = value; + get => Base.ImageUrl; + set => Base.ImageUrl = value; } /// @@ -81,12 +80,32 @@ public string? ImageUrl [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] public string? Url { - get => Builder.Url; - set => Builder.Url = value; + get => Base.Url; + set => Base.Url = value; + } + + /// + /// Gets or sets the footer of the embed. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public EmbedFooterBuilder? Footer + { + get => Base.Footer != null ? new(Base.Footer) : null; + set => Base.Footer = value?.Base; + } + + /// + /// Gets or sets the author of the embed. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public EmbedAuthorBuilder? Author + { + get => Base.Author != null ? new(Base.Author) : null; + set => Base.Author = value?.Base; } [YamlIgnore] - private Discord.EmbedBuilder Builder { get; } = new(); + private Discord.EmbedBuilder Base { get; } = new(); /// /// Changes a into a instance. @@ -97,34 +116,34 @@ public static implicit operator Discord.EmbedBuilder(EmbedBuilder builder) { Discord.EmbedBuilder copy = new(); - if (builder.Builder.Title != null) - copy.WithTitle(builder.Builder.Title); + if (builder.Base.Title != null) + copy.WithTitle(builder.Base.Title); - if (builder.Builder.Description != null) - copy.WithDescription(builder.Builder.Description); + if (builder.Base.Description != null) + copy.WithDescription(builder.Base.Description); - if (builder.Builder.Color.HasValue) - copy.WithColor(builder.Builder.Color.Value); + if (builder.Base.Color.HasValue) + copy.WithColor(builder.Base.Color.Value); - if (builder.Builder.Url != null) - copy.WithUrl(builder.Builder.Url); + if (builder.Base.Url != null) + copy.WithUrl(builder.Base.Url); - if (builder.Builder.ImageUrl != null) - copy.WithImageUrl(builder.Builder.ImageUrl); + if (builder.Base.ImageUrl != null) + copy.WithImageUrl(builder.Base.ImageUrl); - if (builder.Builder.ThumbnailUrl != null) - copy.WithThumbnailUrl(builder.Builder.ThumbnailUrl); + if (builder.Base.ThumbnailUrl != null) + copy.WithThumbnailUrl(builder.Base.ThumbnailUrl); - if (builder.Builder.Timestamp.HasValue) - copy.WithTimestamp(builder.Builder.Timestamp.Value); + if (builder.Base.Timestamp.HasValue) + copy.WithTimestamp(builder.Base.Timestamp.Value); - if (builder.Builder.Footer != null) - copy.WithFooter(builder.Builder.Footer); + if (builder.Base.Footer != null) + copy.WithFooter(builder.Base.Footer); - if (builder.Builder.Author != null) - copy.WithAuthor(builder.Builder.Author); + if (builder.Base.Author != null) + copy.WithAuthor(builder.Base.Author); - foreach (Discord.EmbedFieldBuilder field in builder.Builder.Fields) + foreach (Discord.EmbedFieldBuilder field in builder.Base.Fields) { copy.AddField(field.Name, field.Value, field.IsInline); } diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs index 2162623..4511390 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs @@ -7,13 +7,31 @@ /// public class EmbedFieldBuilder { + /// + /// Initializes a new instance of the class. + /// + public EmbedFieldBuilder() + { + Base = new(); + } + + /// + /// Initializes a new instance of the class. + /// Replaces the base with the instance. + /// + /// The instance. + public EmbedFieldBuilder(Discord.EmbedFieldBuilder builder) + { + Base = builder; + } + /// /// Gets or sets the field name. /// public string Name { - get => Builder.Name; - set => Builder.Name = value; + get => Base.Name; + set => Base.Name = value; } /// @@ -21,8 +39,8 @@ public string Name /// public string Value { - get => Builder.Value.ToString(); - set => Builder.Value = value; + get => Base.Value.ToString(); + set => Base.Value = value; } /// @@ -31,13 +49,13 @@ public string Value [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public bool IsInline { - get => Builder.IsInline; - set => Builder.IsInline = value; + get => Base.IsInline; + set => Base.IsInline = value; } /// /// Gets the base builder. /// [YamlIgnore] - internal Discord.EmbedFieldBuilder Builder { get; } = new(); + public Discord.EmbedFieldBuilder Base { get; init; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs new file mode 100644 index 0000000..50e8f1f --- /dev/null +++ b/DiscordLab.Bot/API/Features/Embed/EmbedFooterBuilder.cs @@ -0,0 +1,53 @@ +namespace DiscordLab.Bot.API.Features.Embed; + +using YamlDotNet.Serialization; + +/// +/// Holds information for an Embed footer. +/// +public class EmbedFooterBuilder +{ + /// + /// Initializes a new instance of the class. + /// + public EmbedFooterBuilder() + { + Base = new(); + } + + /// + /// Initializes a new instance of the class. + /// Replaces the base with the instance. + /// + /// The instance. + public EmbedFooterBuilder(Discord.EmbedFooterBuilder builder) + { + Base = builder; + } + + /// + /// Gets or sets the text for this footer. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? Text + { + get => Base.Text; + set => Base.Text = value; + } + + /// + /// Gets or sets the icon URl for this footer. + /// + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)] + public string? IconUrl + { + get => Base.IconUrl; + set => Base.IconUrl = value; + } + + /// + /// Gets the base builder object. + /// + [YamlIgnore] + public Discord.EmbedFooterBuilder Base { get; init; } +} \ No newline at end of file From 5f3b098899fc4af286059d41071e2b1c6cacf07c Mon Sep 17 00:00:00 2001 From: Lumi Date: Thu, 21 Aug 2025 23:45:31 +0100 Subject: [PATCH 64/68] chore: stylecop requirements --- DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs | 7 +++++-- DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs index 5a1d8bf..c5eb37e 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs @@ -1,7 +1,7 @@ -using YamlDotNet.Serialization; - namespace DiscordLab.Bot.API.Features.Embed; +using YamlDotNet.Serialization; + /// /// Contains information about an author field in an embed. /// @@ -55,6 +55,9 @@ public string? Url set => Base.Url = value; } + /// + /// Gets the base of this builder. + /// [YamlIgnore] public Discord.EmbedAuthorBuilder Base { get; init; } } \ No newline at end of file diff --git a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs index b70078b..c8f4706 100644 --- a/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs +++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs @@ -28,7 +28,6 @@ public string Description /// /// Gets or sets the embed fields. /// - [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] public IEnumerable Fields { get => Base.Fields.Select(x => new EmbedFieldBuilder(x)); From d0348195c61e6131e579867e9ac82063358117f6 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 22 Aug 2025 11:42:00 +0100 Subject: [PATCH 65/68] feat: remove log --- DiscordLab.Bot/Patches/RestClientCreate.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DiscordLab.Bot/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs index 72d601b..df66b69 100644 --- a/DiscordLab.Bot/Patches/RestClientCreate.cs +++ b/DiscordLab.Bot/Patches/RestClientCreate.cs @@ -28,7 +28,6 @@ public static class RestClientCreate if (type == null) continue; ConstructorInfo? constructor = type.GetConstructors().FirstOrDefault(); - Logger.Info(constructor != null); if (constructor != null) return constructor; } From 1444b521d31e551ac9def28f894849b01504e62b Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 22 Aug 2025 15:28:49 +0100 Subject: [PATCH 66/68] feat: decont state placeholder --- DiscordLab.Bot/API/Features/TranslationBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DiscordLab.Bot/API/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs index ef472f7..90b245d 100644 --- a/DiscordLab.Bot/API/Features/TranslationBuilder.cs +++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs @@ -72,6 +72,7 @@ public TranslationBuilder(string translation, string playerPrefix, Player player [CreateRegex("remainingdeconttime")] = GetRemainingDecontaminationTime, [CreateRegex("isdecontenabled")] = () => (Decontamination.Status == DecontaminationController.DecontaminationStatus.None).ToString(), + [CreateRegex("decontstate")] = () => Decontamination.Status.ToString(), // Round Replacers [CreateRegex("killcount")] = () => Round.TotalDeaths.ToString(), From e2c13d3eaaf00285c451532f5389ab230948f4e4 Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 22 Aug 2025 15:43:31 +0100 Subject: [PATCH 67/68] ci: setup role map --- .github/workflows/release.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b328a11..0559c01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,16 +60,12 @@ jobs: $roleMap = @{ "DiscordLab.Bot" = "1326565513392033844" - "DiscordLab.AdvancedLogging" = "1326565584334225469" "DiscordLab.BotStatus" = "1326565625249660990" "DiscordLab.ConnectionLogs" = "1326565678601212016" "DiscordLab.DeathLogs" = "1326565793856622628" - "DiscordLab.Moderation" = "1326565888962596966" - "DiscordLab.ModerationLogs" = "1326565923242639492" - "DiscordLab.SCPSwap" = "1326565946915295365" + "DiscordLab.Moderation" = "1326565923242639492" "DiscordLab.StatusChannel" = "1326565978125107211" - "DiscordLab.XPSystem" = "1326566015861260298" - "DiscordLab.AdminLogs" = "1327362549368361000" + "DiscordLab.Administration" = "1327362549368361000" "DiscordLab.RoundLogs" = "1327631517769531453" } From 8e3f1dcad600b31b9c3bc072f5da7a1c6f92101c Mon Sep 17 00:00:00 2001 From: Lumi Date: Fri, 22 Aug 2025 15:48:40 +0100 Subject: [PATCH 68/68] ci: finalise nuget pack --- .github/workflows/release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0559c01..8516e99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,12 @@ jobs: - name: Rename Assembly-CSharp (because Exiled removes the normal version) shell: pwsh run: Move-Item -Path ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll -Destination ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll - - name: Build and Pack - run: dotnet pack -c:Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj + - name: Build + run: dotnet build -c:Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj + - name: Pack + run: dotnet pack -c Release ${{ github.workspace }}/DiscordLab.Bot/DiscordLab.Bot.csproj - name: Publish - run: dotnet nuget push ${{ github.workspace }}/DiscordLab.Bot/bin/Release/DiscordLab.Bot.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push ${{ github.workspace }}/DiscordLab.Bot/bin/Release/DiscordLab.Bot.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} notify: