diff --git a/.github/workflows/artifact.yml b/.github/workflows/artifact.yml
index 0e8ed5a..7691f1d 100644
--- a/.github/workflows/artifact.yml
+++ b/.github/workflows/artifact.yml
@@ -4,39 +4,30 @@ on:
- push
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_URL: https://exmod-team.github.io/SL-References/Dev.zip
+ SL_REFERENCES: ${{ github.workspace }}/refs
EXILED_REFERENCES: ${{ github.workspace }}/refs
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: Get Exiled References
+ - 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.EXILED_REFERENCES }} -Force
- - name: Get XP System
+ Expand-Archive -Path References.zip -DestinationPath ${{ env.SL_REFERENCES }} -Force
+ - name: Rename Assembly-CSharp (because Exiled removes the normal version)
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
- - name: Restore packages
- run: nuget restore ${{ github.workspace }}/DiscordLab.sln
+ run: Move-Item -Path ${{ env.SL_REFERENCES }}/Assembly-CSharp-Publicized.dll -Destination ${{ env.SL_REFERENCES }}/Assembly-CSharp.dll
- 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/.github/workflows/release.yml b/.github/workflows/release.yml
index a6e8ed2..8516e99 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,7 +5,38 @@ 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: 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 }} --source https://api.nuget.org/v3/index.json
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
notify:
+ if: github.event.release.prerelease == false
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -31,16 +62,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"
}
diff --git a/Directory.Build.props b/Directory.Build.props
index 5d8807b..c3ea473 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,11 +1,38 @@
-
- $(MSBuildThisFileDirectory)bin\
-
-
-
-
-
+
+ false
+ $(MSBuildThisFileDirectory)bin\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..dd4f7d4
--- /dev/null
+++ b/DiscordLab.Administration/Commands/SendCommand.cs
@@ -0,0 +1,60 @@
+using CommandSystem;
+using Discord;
+using Discord.WebSocket;
+using DiscordLab.Bot.API.Extensions;
+using DiscordLab.Bot.API.Features;
+using LabApi.Features.Console;
+using LabApi.Features.Wrappers;
+using RemoteAdmin;
+
+namespace DiscordLab.Administration.Commands;
+
+public class SendCommand : AutocompleteCommand
+{
+ 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,
+ IsAutocomplete = true
+ }
+ ]
+ };
+
+ protected override ulong GuildId { get; } = Config.GuildId;
+
+ public override async Task Run(SocketSlashCommand command)
+ {
+ await command.DeferAsync();
+
+ string response = Server.RunCommand(command.Data.Options.GetOption(Translation.SendCommandOptionName)!);
+
+ TranslationBuilder builder = new TranslationBuilder()
+ .AddCustomReplacer("response", response);
+
+ await Translation.SendCommandResponse.ModifyInteraction(command, 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.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/Config.cs b/DiscordLab.Administration/Config.cs
new file mode 100644
index 0000000..b85d848
--- /dev/null
+++ b/DiscordLab.Administration/Config.cs
@@ -0,0 +1,26 @@
+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("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;
+
+ [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..6f1acd0
--- /dev/null
+++ b/DiscordLab.Administration/DiscordLab.Administration.csproj
@@ -0,0 +1,19 @@
+
+
+ net48
+ enable
+ disable
+ 12
+ x64
+ true
+ 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..8fb9efb
--- /dev/null
+++ b/DiscordLab.Administration/Events.cs
@@ -0,0 +1,114 @@
+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;
+ Shutdown.OnQuit += OnServerQuit;
+ IsSubscribed = true;
+ }
+
+ [CallOnUnload]
+ 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;
+ 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;
+ }
+
+ Translation.ServerStart.SendToChannel(channel, new());
+ }
+
+ 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)
+ return;
+
+ if (!Client.TryGetOrAddChannel(Config.RemoteAdminChannelId, out channel))
+ {
+ Logger.Error(LoggingUtils.GenerateMissingChannelMessage("remote admin logs",
+ Config.RemoteAdminChannelId, Config.GuildId));
+ return;
+ }
+
+ Translation.RemoteAdmin.SendToChannel(channel, 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;
+ }
+
+ 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
new file mode 100644
index 0000000..cc54303
--- /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 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
new file mode 100644
index 0000000..4053f6d
--- /dev/null
+++ b/DiscordLab.Administration/Plugin.cs
@@ -0,0 +1,47 @@
+using DiscordLab.Bot.API.Attributes;
+using DiscordLab.Bot.API.Features;
+using DiscordLab.Dependency;
+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 => GetType().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)
+ SlashCommand.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..4347dd9
--- /dev/null
+++ b/DiscordLab.Administration/Translation.cs
@@ -0,0 +1,27 @@
+using DiscordLab.Bot.API.Features;
+
+namespace DiscordLab.Administration;
+
+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; } =
+ "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}";
+}
\ 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..8cee553
--- /dev/null
+++ b/DiscordLab.Bot/API/Attributes/CallOnLoadAttribute.cs
@@ -0,0 +1,42 @@
+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;
+
+ 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);
+ }
+ }
+ }
+ }
+}
\ 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..dedf485
--- /dev/null
+++ b/DiscordLab.Bot/API/Attributes/CallOnReadyAttribute.cs
@@ -0,0 +1,57 @@
+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;
+
+ Logger.Debug($"Loading ready attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug);
+
+ 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.Clear();
+ }
+}
\ 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..f6cb66c
--- /dev/null
+++ b/DiscordLab.Bot/API/Attributes/CallOnUnloadAttribute.cs
@@ -0,0 +1,41 @@
+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
+ {
+ Logger.Debug($"Calling unload attribute {method.Name} from {type.FullName}", Plugin.Instance.Config.Debug);
+ 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..2fac01d
--- /dev/null
+++ b/DiscordLab.Bot/API/Extensions/BitwiseExtensions.cs
@@ -0,0 +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));
+}
\ 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..b50223e
--- /dev/null
+++ b/DiscordLab.Bot/API/Extensions/DiscordExtensions.cs
@@ -0,0 +1,33 @@
+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));
+
+ ///
+ /// 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)?.Value as T;
+}
\ 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/AutocompleteCommand.cs b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs
new file mode 100644
index 0000000..dbedf5b
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/AutocompleteCommand.cs
@@ -0,0 +1,16 @@
+namespace DiscordLab.Bot.API.Features;
+
+using Discord.WebSocket;
+
+///
+/// Allows you to make autocomplete commands.
+///
+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);
+}
\ 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/Embed/EmbedAuthorBuilder.cs b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs
new file mode 100644
index 0000000..c5eb37e
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/Embed/EmbedAuthorBuilder.cs
@@ -0,0 +1,63 @@
+namespace DiscordLab.Bot.API.Features.Embed;
+
+using YamlDotNet.Serialization;
+
+///
+/// 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;
+ }
+
+ ///
+ /// 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
new file mode 100644
index 0000000..c8f4706
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/Embed/EmbedBuilder.cs
@@ -0,0 +1,152 @@
+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 => Base.Title;
+ set => Base.Title = value;
+ }
+
+ ///
+ /// Gets or sets the embed description.
+ ///
+ public string Description
+ {
+ get => Base.Description;
+ set => Base.Description = value;
+ }
+
+ ///
+ /// Gets or sets the embed fields.
+ ///
+ public IEnumerable Fields
+ {
+ get => Base.Fields.Select(x => new EmbedFieldBuilder(x));
+ set => Base.Fields = value.Select(x => x.Base).ToList();
+ }
+
+ ///
+ /// 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 => Base.Color?.ToString();
+ set
+ {
+ if (value == null)
+ {
+ Base.Color = null;
+ return;
+ }
+
+ Base.Color = Discord.Color.Parse(value);
+ }
+ }
+
+ ///
+ /// Gets or sets the thumbnail URL of the embed.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? ThumbnailUrl
+ {
+ get => Base.ThumbnailUrl;
+ set => Base.ThumbnailUrl = value;
+ }
+
+ ///
+ /// Gets or sets the image URL of the embed.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? ImageUrl
+ {
+ get => Base.ImageUrl;
+ set => Base.ImageUrl = value;
+ }
+
+ ///
+ /// Gets or sets the URL of the embed.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitNull)]
+ public string? Url
+ {
+ 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 Base { get; } = new();
+
+ ///
+ /// 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.Base.Title != null)
+ copy.WithTitle(builder.Base.Title);
+
+ if (builder.Base.Description != null)
+ copy.WithDescription(builder.Base.Description);
+
+ if (builder.Base.Color.HasValue)
+ copy.WithColor(builder.Base.Color.Value);
+
+ if (builder.Base.Url != null)
+ copy.WithUrl(builder.Base.Url);
+
+ if (builder.Base.ImageUrl != null)
+ copy.WithImageUrl(builder.Base.ImageUrl);
+
+ if (builder.Base.ThumbnailUrl != null)
+ copy.WithThumbnailUrl(builder.Base.ThumbnailUrl);
+
+ if (builder.Base.Timestamp.HasValue)
+ copy.WithTimestamp(builder.Base.Timestamp.Value);
+
+ if (builder.Base.Footer != null)
+ copy.WithFooter(builder.Base.Footer);
+
+ if (builder.Base.Author != null)
+ copy.WithAuthor(builder.Base.Author);
+
+ foreach (Discord.EmbedFieldBuilder field in builder.Base.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
new file mode 100644
index 0000000..4511390
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/Embed/EmbedFieldBuilder.cs
@@ -0,0 +1,61 @@
+namespace DiscordLab.Bot.API.Features.Embed;
+
+using YamlDotNet.Serialization;
+
+///
+/// Allows you to create embed fields for a .
+///
+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 => Base.Name;
+ set => Base.Name = value;
+ }
+
+ ///
+ /// Gets or sets the field value.
+ ///
+ public string Value
+ {
+ get => Base.Value.ToString();
+ set => Base.Value = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the field is inline.
+ ///
+ [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
+ public bool IsInline
+ {
+ get => Base.IsInline;
+ set => Base.IsInline = value;
+ }
+
+ ///
+ /// Gets the base builder.
+ ///
+ [YamlIgnore]
+ 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
diff --git a/DiscordLab.Bot/API/Features/MessageContent.cs b/DiscordLab.Bot/API/Features/MessageContent.cs
new file mode 100644
index 0000000..67b9185
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/MessageContent.cs
@@ -0,0 +1,150 @@
+// 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));
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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)
+ 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/Queue.cs b/DiscordLab.Bot/API/Features/Queue.cs
new file mode 100644
index 0000000..02275bf
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/Queue.cs
@@ -0,0 +1,57 @@
+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
index 6db96b7..152882f 100644
--- a/DiscordLab.Bot/API/Features/SlashCommand.cs
+++ b/DiscordLab.Bot/API/Features/SlashCommand.cs
@@ -1,20 +1,106 @@
-using Discord;
+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.Interfaces;
+using DiscordLab.Bot.API.Attributes;
+using LabApi.Features.Console;
-namespace DiscordLab.Bot.API.Features
+///
+/// A wrapper to easily add your own slash commands in your bot.
+///
+public abstract class SlashCommand
{
- public abstract class SlashCommand : IAutocompleteCommand
+ ///
+ /// The list of currently active s.
+ ///
+#pragma warning disable SA1401
+ public static ObservableCollection Commands = [];
+#pragma warning restore SA1401
+
+ ///
+ /// Gets the slash command data.
+ ///
+ public abstract SlashCommandBuilder Data { get; }
+
+ ///
+ /// Gets the guild ID to assign this command to.
+ ///
+ 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)
+ {
+ assembly ??= Assembly.GetCallingAssembly();
+
+ foreach (Type type in assembly.GetTypes())
+ {
+ if (type.IsAbstract || !typeof(SlashCommand).IsAssignableFrom(type))
+ continue;
+
+ if (Activator.CreateInstance(type) is not SlashCommand init)
+ continue;
+ 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()
{
- public abstract SlashCommandBuilder Data { get; }
+ Commands.CollectionChanged += OnCollectionChanged;
+ }
+
+ [CallOnUnload]
+ private static void Unload()
+ {
+ Commands.CollectionChanged -= OnCollectionChanged;
+ Commands.Clear();
+ }
+
+ [CallOnReady]
+ 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;
- public virtual ulong GuildId { get; set; } = 0;
-
- public abstract Task Run(SocketSlashCommand command);
+ Task.Run(() => RegisterGuildCommands((IEnumerable)ev.NewItems));
+ }
- public virtual Task Autocomplete(SocketAutocompleteInteraction autocomplete)
+ private static async Task RegisterGuildCommands(IEnumerable commands)
+ {
+ foreach (IGrouping cmds in commands.GroupBy(cmd => cmd.GuildId))
{
- return Task.CompletedTask;
+ 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/Features/TranslationBuilder.cs b/DiscordLab.Bot/API/Features/TranslationBuilder.cs
new file mode 100644
index 0000000..90b245d
--- /dev/null
+++ b/DiscordLab.Bot/API/Features/TranslationBuilder.cs
@@ -0,0 +1,400 @@
+// 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.Wrappers;
+using LightContainmentZoneDecontamination;
+using Mirror.LiteNetLib4Mirror;
+using PlayerRoles;
+using RoundRestarting;
+using UnityEngine;
+
+///
+/// Allows you to create translations with placeholders being replaced.
+///
+public class TranslationBuilder
+{
+ private static readonly Regex TagRemoveRegex = new("<[^>]+>", RegexOptions.Compiled);
+
+ private static readonly Regex UselessTextRemoveRegex =
+ 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.
+ ///
+ /// 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 the dictionary of replacers that have no argument.
+ ///
+ public static Dictionary> StaticReplacers { get; } = new()
+ {
+ // Map Replacers
+ [CreateRegex("seed")] = () => Map.Seed.ToString(),
+ [CreateRegex("isdecont")] = () => Decontamination.IsDecontaminating.ToString(),
+ [CreateRegex("remainingdeconttime")] = GetRemainingDecontaminationTime,
+ [CreateRegex("isdecontenabled")] = () =>
+ (Decontamination.Status == DecontaminationController.DecontaminationStatus.None).ToString(),
+ [CreateRegex("decontstate")] = () => Decontamination.Status.ToString(),
+
+ // Round Replacers
+ [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
+ [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;
+ },
+ [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()
+ {
+ [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),
+ };
+
+ ///
+ /// 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.RoleBase.RoleName,
+ ["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 or sets the translation.
+ ///
+ 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; }
+
+ ///
+ /// 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; }
+
+ ///
+ /// Gets a Dictionary of cached regexes that are unknown.
+ ///
+ private static Dictionary CachedRegex { get; } = new();
+
+ ///
+ /// .
+ ///
+ /// The instance.
+ ///
+ public static implicit operator string(TranslationBuilder builder) =>
+ builder.Build();
+
+ ///
+ /// .
+ ///
+ /// The instance.
+ ///
+ 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.
+ ///
+ /// 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 regex to replace.
+ /// The string factory to replace with.
+ /// The instance.
+ public TranslationBuilder AddCustomReplacer(Regex toReplace, Func replacer)
+ {
+ CustomReplacers.Add(toReplace, replacer);
+
+ return this;
+ }
+
+ ///
+ ///
+ ///
+ /// 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) =>
+ AddCustomReplacer(toReplace, () => replacer);
+
+ ///
+ /// Builds this instance.
+ ///
+ /// The translation to build from, isn't needed if is defined.
+ /// The translation built.
+ 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 && CustomReplacers.All(replacer => replacer.Key.ToString() != "{players}"))
+ SetupPlayerList();
+
+ string returnTranslation = translation!;
+
+ foreach (KeyValuePair> replacer in CustomReplacers)
+ {
+ returnTranslation = replacer.Key.Replace(returnTranslation, replacer.Value());
+ }
+
+ foreach (KeyValuePair> replacer in StaticReplacers)
+ {
+ returnTranslation = replacer.Key.Replace(returnTranslation, replacer.Value());
+ }
+
+ long unix = new DateTimeOffset(Time).ToUnixTimeSeconds();
+
+ foreach (KeyValuePair> replacer in TimeReplacers)
+ {
+ returnTranslation = replacer.Key.Replace(
+ returnTranslation,
+ replacer.Value(unix));
+ }
+
+ foreach (KeyValuePair player in Players)
+ {
+ if (player.Value is not { IsReady: true })
+ continue;
+
+ Regex baseRegex = CachedRegex.GetOrAdd(player.Key, () => CreateRegex(player.Key));
+
+ returnTranslation = baseRegex.Replace(returnTranslation, player.Value.Nickname);
+
+ 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";
+
+ Regex regex = CachedRegex.GetOrAdd(
+ $"{player.Key}{replacer.Key}",
+ () => CreateRegex($"{player.Key}{replacer.Key}"));
+
+ returnTranslation = regex.Replace(returnTranslation, replacement);
+ }
+ }
+
+ return returnTranslation;
+ }
+
+ private static string ToParameterString(string str) => "{" + str + "}";
+
+#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
+
+ private static TimeSpan TimeSince(long time) =>
+ Round.Duration - (DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds(time));
+
+ 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;
+
+ 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];
+ }
+
+ AddCustomReplacer("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
deleted file mode 100644
index d6e1996..0000000
--- a/DiscordLab.Bot/API/Interfaces/IAutocompleteCommand.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Discord.WebSocket;
-
-namespace DiscordLab.Bot.API.Interfaces
-{
- public interface IAutocompleteCommand : ISlashCommand
- {
- ///
- /// Control what happens when an autocomplete request is sent.
- ///
- /// The slash command instance
- /// The Task completion status
- 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
deleted file mode 100644
index b6d9c8b..0000000
--- a/DiscordLab.Bot/API/Interfaces/ISlashCommand.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Discord;
-using Discord.WebSocket;
-
-namespace DiscordLab.Bot.API.Interfaces
-{
- public interface ISlashCommand
- {
- ///
- /// Here you create your with the data of your command.
- ///
- public SlashCommandBuilder Data { get; }
-
- ///
- /// Set the GuildId where the command should be created here, you should reference Config.GuildId.
- ///
- public ulong GuildId { get; set; }
-
- ///
- /// Here is where your slash command runs.
- ///
- ///
- /// This type contains information about the command that was executed.
- ///
- public Task Run(SocketSlashCommand command);
- }
-}
\ 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..b9fa1b4
--- /dev/null
+++ b/DiscordLab.Bot/API/Updates/GitHubRelease.cs
@@ -0,0 +1,39 @@
+#nullable disable
+
+namespace DiscordLab.Bot.API.Updates;
+
+using Newtonsoft.Json;
+
+///
+/// Contains data for a GitHub release object.
+///
+public class GitHubRelease
+{
+ ///
+ /// Gets or sets the tag name for this release.
+ ///
+ [JsonProperty("tag_name")]
+ public string TagName { get; set; }
+
+ // ReSharper disable CollectionNeverUpdated.Global
+
+ ///
+ /// Gets or sets the assets for this release.
+ ///
+ [JsonProperty("assets")]
+ public List Assets { get; set; }
+
+ // ReSharper restore CollectionNeverUpdated.Global
+
+ ///
+ /// Gets or sets a value indicating whether this is a prerelease release.
+ ///
+ [JsonProperty("prerelease")]
+ public bool Prerelease { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this is a draft release.
+ ///
+ [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
new file mode 100644
index 0000000..da8b935
--- /dev/null
+++ b/DiscordLab.Bot/API/Updates/GitHubReleaseAsset.cs
@@ -0,0 +1,23 @@
+#nullable disable
+
+namespace DiscordLab.Bot.API.Updates;
+
+using Newtonsoft.Json;
+
+///
+/// Gets details for a GitHub release asset.
+///
+public class GitHubReleaseAsset
+{
+ ///
+ /// Gets or sets the name of the asset.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the download name of the asset.
+ ///
+ [JsonProperty("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..1a4800e
--- /dev/null
+++ b/DiscordLab.Bot/API/Updates/Module.cs
@@ -0,0 +1,84 @@
+namespace DiscordLab.Bot.API.Updates;
+
+using LabApi.Loader;
+using LabApi.Loader.Features.Paths;
+
+///
+/// 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..b98c790
--- /dev/null
+++ b/DiscordLab.Bot/API/Updates/Updater.cs
@@ -0,0 +1,132 @@
+namespace DiscordLab.Bot.API.Updates;
+
+using System.Net.Http;
+using DiscordLab.Bot.API.Attributes;
+using LabApi.Features.Console;
+using Newtonsoft.Json;
+
+///
+/// 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 [];
+ }
+
+ string str = await response.Content.ReadAsStringAsync();
+
+ GitHubRelease[] releases = JsonConvert.DeserializeObject(str) ?? [];
+
+ List modules = [];
+
+ foreach (GitHubRelease release in releases)
+ {
+ if (release.Prerelease || release.Draft)
+ continue;
+ 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 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));
+ }
+ }
+
+ 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.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
new file mode 100644
index 0000000..0511e07
--- /dev/null
+++ b/DiscordLab.Bot/API/Utilities/CommandUtils.cs
@@ -0,0 +1,32 @@
+namespace DiscordLab.Bot.API.Utilities;
+
+using LabApi.Features.Wrappers;
+
+///
+/// Utility methods for commands.
+///
+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;
+ }
+
+#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)
+ {
+ 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/API/Utilities/LoggingUtils.cs b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs
new file mode 100644
index 0000000..e1e93f3
--- /dev/null
+++ b/DiscordLab.Bot/API/Utilities/LoggingUtils.cs
@@ -0,0 +1,23 @@
+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..c497678
--- /dev/null
+++ b/DiscordLab.Bot/Client.cs
@@ -0,0 +1,208 @@
+// ReSharper disable MemberCanBePrivate.Global
+
+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
+{
+ ///
+ /// Gets the websocket client for the Discord bot.
+ ///
+ public static DiscordSocketClient SocketClient { get; private set; } = null!;
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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)
+ {
+ 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;
+ }
+
+#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)
+ {
+ channel = GetOrAddChannel(id);
+
+ return channel != null;
+ }
+#nullable restore
+
+ ///
+ /// Starts the bot.
+ ///
+ [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);
+ }
+
+ ///
+ /// Disables the bot.
+ ///
+ [CallOnUnload]
+ internal static void Disable()
+ {
+ SavedTextChannels.Clear();
+
+ 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...");
+ SlashCommand? cmd = SlashCommand.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...");
+ 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/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..28006c8
--- /dev/null
+++ b/DiscordLab.Bot/Commands/DiscordCommand.cs
@@ -0,0 +1,124 @@
+namespace DiscordLab.Bot.Commands;
+
+using Discord;
+using Discord.WebSocket;
+using DiscordLab.Bot.API.Features;
+using DiscordLab.Bot.API.Updates;
+
+///
+public class DiscordCommand : AutocompleteCommand
+{
+ ///
+ 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",
+ },
+
+ 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",
+ }
+ ],
+ };
+
+ ///
+ protected override ulong GuildId { get; } = 0;
+
+ ///
+ public override 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} (v{s.Version})"));
+ 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 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)));
+ }
+}
\ 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..8d059e8
--- /dev/null
+++ b/DiscordLab.Bot/Commands/LocalAdminCommand.cs
@@ -0,0 +1,75 @@
+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, 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} (v{s.Version})"));
+ 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..956a34d 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;
-namespace DiscordLab.Bot
+using System.ComponentModel;
+
+///
+/// The config of this plugin.
+///
+public sealed class Config
{
- public class Config : IConfig
- {
- public bool IsEnabled { get; set; } = true;
- public bool Debug { get; set; } = false;
- [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.")]
- public bool AutoUpdate { get; set; } = true;
- }
+ ///
+ /// Gets or sets the token for the bot.
+ ///
+ [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 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..85c949d 100644
--- a/DiscordLab.Bot/DiscordLab.Bot.csproj
+++ b/DiscordLab.Bot/DiscordLab.Bot.csproj
@@ -2,45 +2,62 @@
net48
enable
- disable
- preview
+ 13
+ enable
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
+
+
-
-
-
-
-
-
+
+ True
+ \
+
+
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)..\bin\dependencies\
+
+
+
-
+
+
+
+
-
+
\ 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/Patches/RestClientCreate.cs b/DiscordLab.Bot/Patches/RestClientCreate.cs
new file mode 100644
index 0000000..df66b69
--- /dev/null
+++ b/DiscordLab.Bot/Patches/RestClientCreate.cs
@@ -0,0 +1,105 @@
+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
+{
+ ///
+ /// Gets the target method to patch.
+ ///
+ /// The method.
+ public static MethodBase? TargetMethod()
+ {
+ foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ try
+ {
+ Type? type = assembly.GetTypes().FirstOrDefault(t => t.Name == "DefaultRestClient");
+ if (type == null)
+ continue;
+ ConstructorInfo? constructor = type.GetConstructors().FirstOrDefault();
+ if (constructor != null)
+ return constructor;
+ }
+ catch
+ {
+ // ignored because sometimes missing dependencies in underlying dependencies... idk why lol, but this works.
+ }
+ }
+
+ 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));
+
+ 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))));
+
+ Logger.Debug("Transpiler end", Plugin.Instance.Config.Debug);
+
+ return matcher.InstructionEnumeration();
+ }
+
+ 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,
+ };
+
+ if (!useProxy)
+ return handler;
+
+ Logger.Debug("Creating HttpClientHandler with proxy", Plugin.Instance.Config.Debug);
+
+ 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 4c444f5..e142585 100644
--- a/DiscordLab.Bot/Plugin.cs
+++ b/DiscordLab.Bot/Plugin.cs
@@ -1,80 +1,79 @@
-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;
-
-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
{
- public class Plugin : Plugin
+ ///
+ /// Gets the current instance of this plugin.
+ ///
+ public static Plugin Instance { get; private set; } = null!;
+
+ ///
+ 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 => GetType().Assembly.GetName().Version;
+
+ ///
+ public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
+
+ ///
+ public override LoadPriority Priority { get; } = LoadPriority.Highest;
+
+ ///
+ /// Gets the current config for the plugin.
+ ///
+ public new Config Config { get; private set; } = null!;
+
+ private Harmony Harmony { get; } = new($"DiscordLab.Bot-{DateTime.Now.Ticks}");
+
+ ///
+ public override void Enable()
{
- 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;
-
- public static Plugin Instance { get; private set; }
-
- private HandlerLoader _handlerLoader;
-
- public override void OnEnabled()
+ Instance = this;
+ Config = base.Config!;
+
+ try
{
- Instance = this;
-
- if(Config.Token is "token" or "")
- {
- Log.Error("Please set the bot token in the config file.");
- return;
- }
-
- try
- {
- TokenUtils.ValidateToken(TokenType.Bot, Config.Token);
- }
- catch (Exception)
- {
- Log.Error("Token is invalid, please put the correct token in the config file.");
- return;
- }
-
- 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");
-
- 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);
-
- Task.Run(UpdateStatus.GetStatus);
-
- base.OnEnabled();
+ TokenUtils.ValidateToken(TokenType.Bot, Config.Token);
}
-
- public override void OnDisabled()
+ catch (Exception)
{
- _handlerLoader.Unload();
- _handlerLoader = null;
-
- SlashCommandLoader.Destroy();
-
- base.OnDisabled();
+ Logger.Error("DiscordLab bot token is invalid");
+ return;
}
+
+ Harmony.PatchAll();
+
+ CallOnLoadAttribute.Load();
+ CallOnReadyAttribute.Load();
+
+ SlashCommand.FindAll();
+ }
+
+ ///
+ public override void Disable()
+ {
+ Harmony.UnpatchAll();
+
+ CallOnUnloadAttribute.Unload();
+
+ Config = null!;
+ Instance = null!;
}
}
\ 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..201083a 100644
--- a/DiscordLab.BotStatus/Config.cs
+++ b/DiscordLab.BotStatus/Config.cs
@@ -1,17 +1,10 @@
-using System.ComponentModel;
-using DiscordLab.Bot.API.Features;
-using Exiled.API.Interfaces;
+using Discord;
-namespace DiscordLab.BotStatus
+namespace DiscordLab.BotStatus;
+
+public class Config
{
- public class Config : IConfig
- {
- [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;
- }
+ 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..088a9d5 100644
--- a/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj
+++ b/DiscordLab.BotStatus/DiscordLab.BotStatus.csproj
@@ -1,45 +1,15 @@
-
+
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..c4f10eb 100644
--- a/DiscordLab.BotStatus/Plugin.cs
+++ b/DiscordLab.BotStatus/Plugin.cs
@@ -1,39 +1,84 @@
-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.Attributes;
+using DiscordLab.Bot.API.Features;
+using DiscordLab.Dependency;
+using LabApi.Events.Arguments.PlayerEvents;
+using LabApi.Events.Handlers;
+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 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()
{
- 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; }
-
- private HandlerLoader _handlerLoader;
-
- public override void OnEnabled()
- {
- Instance = this;
-
- _handlerLoader = new ();
- if (!_handlerLoader.Load(Assembly)) return;
-
- base.OnEnabled();
- }
-
- public override void OnDisabled()
+ 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;
+ }
+
+ public static void OnWaitingForPlayers()
+ {
+ UpdateStatus();
+ }
+
+ public static void OnPlayerJoin(PlayerJoinedEventArgs _)
+ {
+ 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 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)
{
- _handlerLoader.Unload();
- _handlerLoader = null;
-
- base.OnDisabled();
+ 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..d723dca 100644
--- a/DiscordLab.BotStatus/Translation.cs
+++ b/DiscordLab.BotStatus/Translation.cs
@@ -1,14 +1,8 @@
-using System.ComponentModel;
-using Exiled.API.Interfaces;
+namespace DiscordLab.BotStatus;
-namespace DiscordLab.BotStatus
+public class Translation
{
- public class Translation : ITranslation
- {
- [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..7b7ba22 100644
--- a/DiscordLab.ConnectionLogs/Config.cs
+++ b/DiscordLab.ConnectionLogs/Config.cs
@@ -1,31 +1,20 @@
-using System.ComponentModel;
-using DiscordLab.Bot.API.Features;
-using DiscordLab.Bot.API.Interfaces;
-using Exiled.API.Interfaces;
+using System.ComponentModel;
-namespace DiscordLab.ConnectionLogs
+namespace DiscordLab.ConnectionLogs;
+
+public class Config
{
- 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 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 join logs will be sent.")]
- public ulong JoinChannelId { get; set; } = new();
+ [Description("The channel where the round start logs will be sent.")]
+ public ulong RoundStartChannelId { get; set; } = 0;
- [Description("The channel where the leave logs will be sent.")]
- public ulong LeaveChannelId { get; set; } = new();
+ [Description("The channel where the round end logs will be sent. Optional.")]
+ public ulong RoundEndChannelId { get; set; } = 0;
- [Description("The channel where the round start logs will be sent.")]
- public ulong RoundStartChannelId { get; set; } = new();
-
- [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 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..46ef3c0 100644
--- a/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj
+++ b/DiscordLab.ConnectionLogs/DiscordLab.ConnectionLogs.csproj
@@ -3,43 +3,13 @@
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..866c299
--- /dev/null
+++ b/DiscordLab.ConnectionLogs/Events.cs
@@ -0,0 +1,93 @@
+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.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 (!Round.IsRoundInProgress)
+ 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;
+ }
+
+ 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));
+ return;
+ }
+
+ Translation.PlayerLeave.SendToChannel(channel, new("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;
+ }
+
+ Translation.RoundStart.SendToChannel(channel, new()
+ {
+ 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 TranslationBuilder()
+ {
+ PlayerListItem = Translation.RoundPlayers
+ }
+ .AddCustomReplacer("winner", ev.LeadingTeam.ToString());
+
+ Translation.RoundEnd.SendToChannel(channel, 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..7741f3a 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 DiscordLab.Dependency;
+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 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 => GetType().Assembly.GetName().Version;
+ public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
+
+ public Events Events = new();
+
+ public override void Enable()
{
- 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; }
-
- 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();
- }
+ Instance = this;
+
+ CustomHandlersManager.RegisterEventsHandler(Events);
+ }
+
+ public override void Disable()
+ {
+ CustomHandlersManager.UnregisterEventsHandler(Events);
+ Events = null;
+
+ 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..37bf7dc 100644
--- a/DiscordLab.ConnectionLogs/Translation.cs
+++ b/DiscordLab.ConnectionLogs/Translation.cs
@@ -1,23 +1,24 @@
-using System.ComponentModel;
-using Exiled.API.Interfaces;
+using System.ComponentModel;
+using DiscordLab.Bot.API.Features;
-namespace DiscordLab.ConnectionLogs
+namespace DiscordLab.ConnectionLogs;
+
+public class Translation
{
- public class Translation : ITranslation
- {
- [Description("The message that will be sent when a player joins the server.")]
- public string PlayerJoin { get; set; } = "`{player}` (`{id}`) has joined the server.";
+ [Description("The message that will be sent when a player joins 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 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 MessageContent RoundStart { get; set; } = "Round has started with the following people: \n```{players}\n```";
- [Description("The message that will be sent when a player leaves the server.")]
- public string PlayerLeave { get; set; } = "`{player}` (`{id}`) has left the server.";
+ [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 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 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.")]
- 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 7f9b187..298d2e8 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
+namespace DiscordLab.DeathLogs;
+
+public class Config
{
- 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 normal death logs will be sent.")]
- public ulong ChannelId { get; set; } = new();
-
- [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();
-
- [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();
-
- [Description("The channel where logs will be sent when a player dies by a teamkill.")]
- public ulong TeamKillChannelId { get; set; } = new();
-
- [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();
-
- [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.")]
- 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; }
- }
+ [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 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(
+ "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;
+
+ 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..f11b3c7
--- /dev/null
+++ b/DiscordLab.DeathLogs/DamageLogs.cs
@@ -0,0 +1,148 @@
+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 LabApi.Events.Arguments.PlayerEvents;
+using LabApi.Events.Handlers;
+using PlayerStatsSystem;
+using UnityEngine;
+using Logger = LabApi.Features.Console.Logger;
+
+namespace DiscordLab.DeathLogs;
+
+public static class DamageLogs
+{
+ public static List DamageLogEntries { get; set; } = new();
+
+ public static SocketTextChannel Channel;
+
+ 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;
+
+ DamageLogEntries = null;
+ Channel = null;
+ }
+
+ 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.
+ 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;
+
+ 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 (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;
+ }
+
+ Channel.SendMessage(embeds: CreateEmbeds());
+
+ DamageLogEntries.Clear();
+ }
+
+ public static Embed[] CreateEmbeds()
+ {
+ List