From 80f5449ae83aa8476de2e7a1cf0df6f2e295e998 Mon Sep 17 00:00:00 2001
From: zznty <94796179+zznty@users.noreply.github.com>
Date: Mon, 20 Nov 2023 13:56:59 +0700
Subject: [PATCH] meh
---
Kits/Kits.csproj | 2 +-
LightPerms.Discord/LightPerms.Discord.csproj | 2 +-
.../LightPerms.TorchCommands.csproj | 2 +-
LightPerms/LightPerms.csproj | 2 +-
.../LuckPerms.Torch.Api.nuspec | 1 +
.../Extensions/DelegateExtensions.cs | 78 ++++
.../Extensions/EnumerableExtensions.cs | 6 +-
.../Extensions/IteratorExtensions.cs | 26 +-
.../Extensions/StreamExtensions.cs | 92 ++++
.../Extensions/UUIDExtensions.cs | 2 +-
.../LuckPerms.Torch.Utils.csproj | 27 ++
.../Extensions/DelegateExtensions.cs | 19 -
.../MultiplayerManagerExtensions.cs | 1 +
.../Extensions/StreamExtensions.cs | 19 -
.../Impl/Listeners/LpConnectionListener.cs | 1 +
LuckPerms.Torch/Impl/LpContextManager.cs | 1 +
LuckPerms.Torch/Impl/LpSenderFactory.cs | 1 +
LuckPerms.Torch/Impl/LpTorchBootstrap.cs | 16 +-
LuckPerms.Torch/Impl/LpTorchPlugin.cs | 1 +
LuckPerms.Torch/LuckPerms.Torch.csproj | 4 +
LuckPerms.Torch/ModApi/ModApiManager.cs | 1 +
LuckPerms.Torch/manifest.xml | 2 +-
LuckPerms.Torch/packages.lock.json | 8 +-
Maintenance/Commands.cs | 144 ++++++
Maintenance/ConfigKeys.cs | 20 +
.../Extensions/CompletableFutureExtensions.cs | 31 ++
Maintenance/LangKeys.cs | 18 +
Maintenance/Maintenance.csproj | 53 +++
Maintenance/Managers/ConfigManager.cs | 38 ++
Maintenance/Managers/LanguageManager.cs | 62 +++
Maintenance/Managers/MaintenanceManager.cs | 179 +++++++
.../Managers/MaintenanceScheduleManager.cs | 181 ++++++++
Maintenance/Patches/SteamQueryPatch.cs | 361 +++++++++++++++
Maintenance/Plugin.cs | 21 +
Maintenance/Resources/config.yml | 32 ++
Maintenance/Resources/translations/en.yml | 13 +
Maintenance/manifest.xml | 6 +
Maintenance/packages.lock.json | 438 ++++++++++++++++++
TorchPlugins.sln | 14 +
heh/heh.csproj | 2 +-
40 files changed, 1863 insertions(+), 64 deletions(-)
create mode 100644 LuckPerms.Torch.Utils/Extensions/DelegateExtensions.cs
rename {LuckPerms.Torch => LuckPerms.Torch.Utils}/Extensions/EnumerableExtensions.cs (81%)
rename {LuckPerms.Torch => LuckPerms.Torch.Utils}/Extensions/IteratorExtensions.cs (55%)
create mode 100644 LuckPerms.Torch.Utils/Extensions/StreamExtensions.cs
rename {LuckPerms.Torch => LuckPerms.Torch.Utils}/Extensions/UUIDExtensions.cs (84%)
create mode 100644 LuckPerms.Torch.Utils/LuckPerms.Torch.Utils.csproj
delete mode 100644 LuckPerms.Torch/Extensions/DelegateExtensions.cs
delete mode 100644 LuckPerms.Torch/Extensions/StreamExtensions.cs
create mode 100644 Maintenance/Commands.cs
create mode 100644 Maintenance/ConfigKeys.cs
create mode 100644 Maintenance/Extensions/CompletableFutureExtensions.cs
create mode 100644 Maintenance/LangKeys.cs
create mode 100644 Maintenance/Maintenance.csproj
create mode 100644 Maintenance/Managers/ConfigManager.cs
create mode 100644 Maintenance/Managers/LanguageManager.cs
create mode 100644 Maintenance/Managers/MaintenanceManager.cs
create mode 100644 Maintenance/Managers/MaintenanceScheduleManager.cs
create mode 100644 Maintenance/Patches/SteamQueryPatch.cs
create mode 100644 Maintenance/Plugin.cs
create mode 100644 Maintenance/Resources/config.yml
create mode 100644 Maintenance/Resources/translations/en.yml
create mode 100644 Maintenance/manifest.xml
create mode 100644 Maintenance/packages.lock.json
diff --git a/Kits/Kits.csproj b/Kits/Kits.csproj
index 0b04b09..04471e8 100644
--- a/Kits/Kits.csproj
+++ b/Kits/Kits.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/LightPerms.Discord/LightPerms.Discord.csproj b/LightPerms.Discord/LightPerms.Discord.csproj
index f115e55..8fc9992 100644
--- a/LightPerms.Discord/LightPerms.Discord.csproj
+++ b/LightPerms.Discord/LightPerms.Discord.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj b/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj
index a7f9278..e565feb 100644
--- a/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj
+++ b/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/LightPerms/LightPerms.csproj b/LightPerms/LightPerms.csproj
index 29a5170..9c1aadc 100644
--- a/LightPerms/LightPerms.csproj
+++ b/LightPerms/LightPerms.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/LuckPerms.Torch.Api/LuckPerms.Torch.Api.nuspec b/LuckPerms.Torch.Api/LuckPerms.Torch.Api.nuspec
index dad4120..cd9533c 100644
--- a/LuckPerms.Torch.Api/LuckPerms.Torch.Api.nuspec
+++ b/LuckPerms.Torch.Api/LuckPerms.Torch.Api.nuspec
@@ -8,6 +8,7 @@
+
diff --git a/LuckPerms.Torch.Utils/Extensions/DelegateExtensions.cs b/LuckPerms.Torch.Utils/Extensions/DelegateExtensions.cs
new file mode 100644
index 0000000..9470c51
--- /dev/null
+++ b/LuckPerms.Torch.Utils/Extensions/DelegateExtensions.cs
@@ -0,0 +1,78 @@
+using System.Reflection;
+using java.lang;
+using java.util.concurrent;
+using java.util.function;
+using net.luckperms.api.@event;
+
+namespace LuckPerms.Torch.Utils.Extensions;
+
+public static class DelegateExtensions
+{
+ private static readonly Func AndThenDefault =
+ (Func)typeof(Consumer).GetMethod("andThen",
+ BindingFlags.Static | BindingFlags.NonPublic)!.CreateDelegate(typeof(Func));
+
+ private static readonly Func AndDefault =
+ (Func)typeof(Predicate).GetMethod("and",
+ BindingFlags.Static | BindingFlags.NonPublic)!.CreateDelegate(typeof(Func));
+
+ private static readonly Func NegateDefault =
+ (Func)typeof(Predicate).GetMethod("negate",
+ BindingFlags.Static | BindingFlags.NonPublic)!.CreateDelegate(typeof(Func));
+
+ private static readonly Func OrDefault =
+ (Func)typeof(Predicate).GetMethod("or",
+ BindingFlags.Static | BindingFlags.NonPublic)!.CreateDelegate(typeof(Func));
+
+ public static Runnable ToRunnable(this Action action) => new DelegateRunnable(action);
+
+ public static Consumer ToConsumer(this Action action) => new DelegateConsumer(action);
+
+ public static Supplier ToSupplier(this Func func) where T : notnull => new DelegateSupplier(func);
+
+ public static Predicate ToPredicate(this Predicate predicate) => new DelegatePredicate(predicate);
+
+ public static Callable ToCallable(this Func func) => new DelegateSupplier(func);
+
+ private sealed class DelegatePredicate(Predicate predicate) : Predicate
+ {
+ public bool test(object t) => predicate((T)t);
+
+ public Predicate and(Predicate other) => AndDefault(this, other);
+
+ public Predicate negate() => NegateDefault(this);
+
+ public Predicate or(Predicate other) => OrDefault(this, other);
+ }
+
+ private sealed class DelegateSupplier(Func func) : Supplier, Callable where T : notnull
+ {
+ public object get() => func();
+ public object call() => func();
+ }
+
+ private sealed class DelegateConsumer(Action action) : Consumer
+ {
+ public void accept(object t)
+ {
+ action((T)t);
+ }
+
+ public Consumer andThen(Consumer after) => AndThenDefault(this, after);
+ }
+
+ // ReSharper disable once InconsistentNaming
+ // lets make it an overload for convenience
+ public static void execute(this Executor executor, Action action) => executor.execute(action.ToRunnable());
+
+ public static EventSubscription subscribe(this EventBus bus, object plugin, Action action)
+ where TEvent : class, LuckPermsEvent => bus.subscribe(plugin, typeof(TEvent), action.ToConsumer());
+
+ public static EventSubscription subscribe(this EventBus bus, Action action)
+ where TEvent : class, LuckPermsEvent => bus.subscribe(typeof(TEvent), action.ToConsumer());
+
+ private sealed class DelegateRunnable(Action action) : Runnable
+ {
+ public void run() => action();
+ }
+}
\ No newline at end of file
diff --git a/LuckPerms.Torch/Extensions/EnumerableExtensions.cs b/LuckPerms.Torch.Utils/Extensions/EnumerableExtensions.cs
similarity index 81%
rename from LuckPerms.Torch/Extensions/EnumerableExtensions.cs
rename to LuckPerms.Torch.Utils/Extensions/EnumerableExtensions.cs
index 40857b4..eb4496e 100644
--- a/LuckPerms.Torch/Extensions/EnumerableExtensions.cs
+++ b/LuckPerms.Torch.Utils/Extensions/EnumerableExtensions.cs
@@ -1,8 +1,6 @@
-using System.Collections.Generic;
-using System.Linq;
-using java.util;
+using java.util;
-namespace LuckPerms.Torch.Extensions;
+namespace LuckPerms.Torch.Utils.Extensions;
public static class EnumerableExtensions
{
diff --git a/LuckPerms.Torch/Extensions/IteratorExtensions.cs b/LuckPerms.Torch.Utils/Extensions/IteratorExtensions.cs
similarity index 55%
rename from LuckPerms.Torch/Extensions/IteratorExtensions.cs
rename to LuckPerms.Torch.Utils/Extensions/IteratorExtensions.cs
index 115a488..3ada80a 100644
--- a/LuckPerms.Torch/Extensions/IteratorExtensions.cs
+++ b/LuckPerms.Torch.Utils/Extensions/IteratorExtensions.cs
@@ -1,12 +1,23 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections;
using java.util;
+using java.util.stream;
-namespace LuckPerms.Torch.Extensions;
+namespace LuckPerms.Torch.Utils.Extensions;
public static class IteratorExtensions
{
+ public static StreamEnumerable AsEnumerable(this BaseStream stream) => new(stream);
+
+ public static IteratorEnumerator
+
+
+
+
\ No newline at end of file
diff --git a/LuckPerms.Torch/ModApi/ModApiManager.cs b/LuckPerms.Torch/ModApi/ModApiManager.cs
index 213fa89..de7b074 100644
--- a/LuckPerms.Torch/ModApi/ModApiManager.cs
+++ b/LuckPerms.Torch/ModApi/ModApiManager.cs
@@ -5,6 +5,7 @@ using ikvm.extensions;
using ikvm.runtime;
using java.lang;
using LuckPerms.Torch.Extensions;
+using LuckPerms.Torch.Utils.Extensions;
using Torch.API;
using Torch.API.Managers;
using VRage.Scripting;
diff --git a/LuckPerms.Torch/manifest.xml b/LuckPerms.Torch/manifest.xml
index 2a54795..6c24c11 100644
--- a/LuckPerms.Torch/manifest.xml
+++ b/LuckPerms.Torch/manifest.xml
@@ -2,5 +2,5 @@
LuckPerms.Torch
7E4B3CC8-64FA-416E-8910-AACDF2DA5E2C
- v5.4.106.4
+ v5.4.106.5
\ No newline at end of file
diff --git a/LuckPerms.Torch/packages.lock.json b/LuckPerms.Torch/packages.lock.json
index 43d9fbf..ccfa903 100644
--- a/LuckPerms.Torch/packages.lock.json
+++ b/LuckPerms.Torch/packages.lock.json
@@ -49,7 +49,7 @@
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
- "contentHash": "GAf9Mv1t1/qTGHSgDqkiKAc7Xbh36+U8Ce1PuSoJZNKxHVmzbKHc3nSVz0dIBHhLE7Op8k60NfmclDRAQAppbQ=="
+ "contentHash": "HkuAujKa9IqPPqoA1205teUPBxuNOC9z0xZJkrMlFT0htH02X0ieZ5qh4onwyV10qVKiRBCSLkc5tA8lp1l5ig=="
},
"Torch.Server.ReferenceAssemblies": {
"type": "Direct",
@@ -357,6 +357,12 @@
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
+ },
+ "luckperms.torch.utils": {
+ "type": "Project",
+ "dependencies": {
+ "IKVM": "[8.7.1, )"
+ }
}
},
".NETFramework,Version=v4.8/win-x64": {
diff --git a/Maintenance/Commands.cs b/Maintenance/Commands.cs
new file mode 100644
index 0000000..c922461
--- /dev/null
+++ b/Maintenance/Commands.cs
@@ -0,0 +1,144 @@
+using Maintenance.Managers;
+using Torch.API.Managers;
+using Torch.Commands;
+
+namespace Maintenance;
+
+[Category("maintenance")]
+public class Commands : CommandModule
+{
+ private static readonly TimeSpan MaxTime = TimeSpan.FromDays(28);
+
+ private MaintenanceManager MaintenanceManager =>
+ Context.Torch.CurrentSession.Managers.GetManager();
+
+ private MaintenanceScheduleManager MaintenanceScheduler =>
+ Context.Torch.CurrentSession.Managers.GetManager();
+
+ private LanguageManager LanguageManager =>
+ Context.Torch.Managers.GetManager();
+
+ [Command("on", "Set the status of the maintenance mode to enabled.")]
+ public void On()
+ {
+ if (MaintenanceManager.MaintenanceEnabled)
+ {
+ Context.Respond(LanguageManager[LangKeys.AlreadyEnabled]);
+ return;
+ }
+
+ MaintenanceManager.MaintenanceEnabled = true;
+
+ Context.Respond(LanguageManager[LangKeys.MaintenanceActivated]);
+ }
+
+ [Command("off", "Set the status of the maintenance mode to disabled.")]
+ public void Off()
+ {
+ if (!MaintenanceManager.MaintenanceEnabled)
+ {
+ Context.Respond(LanguageManager[LangKeys.AlreadyDisabled]);
+ return;
+ }
+
+ MaintenanceManager.MaintenanceEnabled = false;
+ MaintenanceScheduler.CancelTimer(null);
+
+ Context.Respond(LanguageManager[LangKeys.MaintenanceDeactivated]);
+ }
+
+ [Command("starttimer", "Will enable maintenance mode after the time is up.")]
+ public void StartTimer(string time)
+ {
+ var startTime = double.TryParse(time, out var seconds) ? TimeSpan.FromSeconds(seconds) : TimeSpan.Parse(time);
+
+ if (startTime > MaxTime)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerTooLong]);
+ return;
+ }
+
+ if (MaintenanceManager.MaintenanceEnabled)
+ {
+ Context.Respond(LanguageManager[LangKeys.AlreadyEnabled]);
+ return;
+ }
+
+ if (MaintenanceScheduler.CurrentSchedule.StartTime.HasValue)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerAlreadyRunning]);
+ return;
+ }
+
+ MaintenanceScheduler.ScheduleTimer(TimerType.Start, startTime);
+
+ Context.Respond(LanguageManager.Format(LangKeys.StartTimerStarted, MaintenanceScheduler.CurrentSchedule));
+ }
+
+ [Command("endtimer",
+ "Will enable maintenance mode for the given time in minutes. After the time is up, it'll be disabled again.")]
+ public void EndTimer(string duration)
+ {
+ var endTime = double.TryParse(duration, out var seconds) ? TimeSpan.FromSeconds(seconds) : TimeSpan.Parse(duration);
+
+ if (endTime > MaxTime)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerTooLong]);
+ return;
+ }
+
+ if (MaintenanceScheduler.CurrentSchedule.EndTime.HasValue)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerAlreadyRunning]);
+ return;
+ }
+
+ MaintenanceManager.MaintenanceEnabled = true;
+ MaintenanceScheduler.ScheduleTimer(TimerType.End, endTime);
+
+ Context.Respond(LanguageManager.Format(LangKeys.EndTimerStarted, MaintenanceScheduler.CurrentSchedule));
+ }
+
+ [Command("schedule", "Will enable maintenance mode after the given time, then disable it according to the second parameter.")]
+ public void Schedule(string time, string duration)
+ {
+ var startTime = double.TryParse(time, out var startSeconds) ? TimeSpan.FromSeconds(startSeconds) : TimeSpan.Parse(time);
+ var endTime = double.TryParse(duration, out var endSeconds) ? TimeSpan.FromSeconds(endSeconds) : TimeSpan.Parse(duration);
+
+ if (startTime > MaxTime || endTime > MaxTime)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerTooLong]);
+ return;
+ }
+
+ if (MaintenanceScheduler.CurrentSchedule != MaintenanceSchedule.Default)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerAlreadyRunning]);
+ return;
+ }
+
+ if (MaintenanceManager.MaintenanceEnabled)
+ {
+ Context.Respond(LanguageManager[LangKeys.AlreadyEnabled]);
+ return;
+ }
+
+ MaintenanceScheduler.ScheduleMaintenance(startTime, endTime);
+
+ Context.Respond(LanguageManager.Format(LangKeys.ScheduleTimerBroadcast, MaintenanceScheduler.CurrentSchedule));
+ }
+
+ [Command("aborttimer", "Cancels a running start-/endtimer")]
+ public void AbortTimer()
+ {
+ if (MaintenanceScheduler.CurrentSchedule == MaintenanceSchedule.Default)
+ {
+ Context.Respond(LanguageManager[LangKeys.TimerNotRunning]);
+ return;
+ }
+
+ MaintenanceScheduler.CancelTimer(null);
+
+ Context.Respond(LanguageManager[LangKeys.TimerCancelled]);
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/ConfigKeys.cs b/Maintenance/ConfigKeys.cs
new file mode 100644
index 0000000..5082944
--- /dev/null
+++ b/Maintenance/ConfigKeys.cs
@@ -0,0 +1,20 @@
+namespace Maintenance;
+
+public static class ConfigKeys
+{
+ public const string MaintenanceEnabled = "maintenance-enabled";
+
+ public const string CommandsOnMaintenanceEnable = "commands-on-maintenance-enable";
+ public const string CommandsOnMaintenanceDisable = "commands-on-maintenance-disable";
+
+ public const string EnableWorldMessage = "enable-world-message";
+ public const string WorldMessage = "world-message";
+
+ public const string KickOnlinePlayers = "kick-online-players";
+
+ public const string Language = "language";
+
+ public const string ContinueEndTimerAfterRestart = "continue-endtimer-after-restart";
+
+ public const string TimerBroadcastForSeconds = "timer-broadcast-for-seconds";
+}
\ No newline at end of file
diff --git a/Maintenance/Extensions/CompletableFutureExtensions.cs b/Maintenance/Extensions/CompletableFutureExtensions.cs
new file mode 100644
index 0000000..c7a90fb
--- /dev/null
+++ b/Maintenance/Extensions/CompletableFutureExtensions.cs
@@ -0,0 +1,31 @@
+using java.util.concurrent;
+using java.util.function;
+using Torch.Utils;
+
+namespace Maintenance.Extensions;
+
+public static class CompletableFutureExtensions
+{
+ [ReflectedStaticMethod(Type = typeof(Consumer), Name = "andThen")]
+ private static readonly Func AndThenDefault = null!;
+
+ public static Task ToTask(this CompletableFuture completableFuture)
+ {
+ var taskCompletionSource = new TaskCompletionSource();
+
+ completableFuture.thenAccept(new TaskConsumer(taskCompletionSource));
+
+ return taskCompletionSource.Task;
+ }
+
+ private sealed class TaskConsumer(TaskCompletionSource completionSource) : Consumer
+ {
+
+ public void accept(object t)
+ {
+ completionSource.SetResult((T)t);
+ }
+
+ public Consumer andThen(Consumer after) => AndThenDefault(this, after);
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/LangKeys.cs b/Maintenance/LangKeys.cs
new file mode 100644
index 0000000..8eca20a
--- /dev/null
+++ b/Maintenance/LangKeys.cs
@@ -0,0 +1,18 @@
+namespace Maintenance;
+
+public static class LangKeys
+{
+ public const string MaintenanceActivated = "maintenanceActivated";
+ public const string MaintenanceDeactivated = "maintenanceDeactivated";
+ public const string AlreadyEnabled = "alreadyEnabled";
+ public const string AlreadyDisabled = "alreadyDisabled";
+ public const string EndTimerBroadcast = "endtimerBroadcast";
+ public const string EndTimerStarted = "endtimerStarted";
+ public const string StartTimerBroadcast = "starttimerBroadcast";
+ public const string StartTimerStarted = "starttimerStarted";
+ public const string ScheduleTimerBroadcast = "scheduletimerBroadcast";
+ public const string TimerAlreadyRunning = "timerAlreadyRunning";
+ public const string TimerNotRunning = "timerNotRunning";
+ public const string TimerCancelled = "timerCancelled";
+ public const string TimerTooLong = "timerTooLong";
+}
\ No newline at end of file
diff --git a/Maintenance/Maintenance.csproj b/Maintenance/Maintenance.csproj
new file mode 100644
index 0000000..20356f9
--- /dev/null
+++ b/Maintenance/Maintenance.csproj
@@ -0,0 +1,53 @@
+
+
+
+ net48
+ enable
+ enable
+ x64
+ 12
+ true
+ true
+ true
+
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+ libs\Steamworks.NET.dll
+ False
+
+
+
+
diff --git a/Maintenance/Managers/ConfigManager.cs b/Maintenance/Managers/ConfigManager.cs
new file mode 100644
index 0000000..bb1eb2d
--- /dev/null
+++ b/Maintenance/Managers/ConfigManager.cs
@@ -0,0 +1,38 @@
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Torch.API.Managers;
+
+namespace Maintenance.Managers;
+
+public class ConfigManager(string storagePath) : IManager
+{
+ private IConfigurationRoot? _configurationRoot;
+
+ public IConfiguration Configuration => _configurationRoot ?? throw new InvalidOperationException("Manager is not attached");
+
+ public void Attach()
+ {
+ var configPath = Path.Combine(storagePath, "config.yml");
+
+ if (!File.Exists(configPath))
+ ExtractDefault(configPath);
+
+ _configurationRoot = new ConfigurationBuilder()
+ .AddYamlFile(configPath, false, true)
+ .Build();
+ }
+
+ private void ExtractDefault(string path)
+ {
+ var name = path[storagePath.Length..].TrimStart(Path.GetInvalidFileNameChars()).Replace('\\', '.');
+
+ using var stream = typeof(ConfigManager).Assembly.GetManifestResourceStream(name)!;
+ using var file = File.Create(path);
+
+ stream.CopyTo(file);
+ }
+
+ public void Detach()
+ {
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/Managers/LanguageManager.cs b/Maintenance/Managers/LanguageManager.cs
new file mode 100644
index 0000000..6abe047
--- /dev/null
+++ b/Maintenance/Managers/LanguageManager.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using SmartFormat;
+using SmartFormat.Core.Settings;
+using Torch.API.Managers;
+using Torch.Managers;
+
+namespace Maintenance.Managers;
+
+public class LanguageManager(string storagePath) : IManager
+{
+ private readonly string[] _langs = { "en" };
+
+ public IConfiguration Language => _configurationRoot ?? throw new InvalidOperationException("Manager is not attached");
+
+ private IConfigurationRoot? _configurationRoot;
+
+ [Manager.Dependency]
+ private readonly ConfigManager _configManager = null!;
+
+ public SmartFormatter Formatter { get; } = Smart.CreateDefaultSmartFormat(new()
+ {
+ CaseSensitivity = CaseSensitivityType.CaseInsensitive
+ });
+
+ public string this[string key] => Language[key] ?? throw new KeyNotFoundException(key);
+
+ public void Attach()
+ {
+ var langDirectory = Path.Combine(storagePath, "translations");
+
+ if (!Directory.Exists(langDirectory))
+ Directory.CreateDirectory(langDirectory);
+
+ foreach (var lang in _langs)
+ {
+ var path = Path.Combine(langDirectory, $"{lang}.yml");
+ if (!File.Exists(path))
+ ExtractTranslation(path);
+ }
+
+ _configurationRoot = new ConfigurationBuilder()
+ .AddYamlFile(Path.Combine(langDirectory, $"{_configManager.Configuration.GetValue(ConfigKeys.Language)!}.yml"), false, true)
+ .Build();
+ }
+
+ public string Format(string key, object obj) => Formatter.Format(this[key], obj);
+
+ private void ExtractTranslation(string path)
+ {
+ var lang = path[storagePath.Length..].TrimStart(Path.GetInvalidFileNameChars()).Replace('\\', '.');
+
+ using var stream = typeof(LanguageManager).Assembly.GetManifestResourceStream(lang)!;
+ using var file = File.Create(path);
+
+ stream.CopyTo(file);
+ }
+
+ public void Detach()
+ {
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/Managers/MaintenanceManager.cs b/Maintenance/Managers/MaintenanceManager.cs
new file mode 100644
index 0000000..c4921d2
--- /dev/null
+++ b/Maintenance/Managers/MaintenanceManager.cs
@@ -0,0 +1,179 @@
+using java.lang;
+using LuckPerms.Torch.Utils.Extensions;
+using Maintenance.Extensions;
+using Microsoft.Extensions.Configuration;
+using net.luckperms.api;
+using net.luckperms.api.model.user;
+using NLog;
+using Sandbox;
+using Sandbox.Engine.Multiplayer;
+using Torch.API;
+using Torch.API.Event;
+using Torch.Commands;
+using Torch.Managers;
+using Torch.Server.Managers;
+using Torch.Utils;
+using VRage.Game.ModAPI;
+using VRage.Network;
+
+namespace Maintenance.Managers;
+
+public class MaintenanceManager(ITorchBase torch) : Manager(torch), IEventHandler
+{
+ public const string BypassPermission = "maintenance.bypass";
+
+ [ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase))]
+ private static readonly Func ConvertSteamIDFrom64 = null!;
+
+ private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
+
+ [Dependency]
+ private readonly IEventManager _eventManager = null!;
+
+ [Dependency]
+ private readonly ConfigManager _configManager = null!;
+
+ [Dependency]
+ private readonly MultiplayerManagerDedicated _multiplayerManager = null!;
+
+ [Dependency]
+ private readonly CommandManager _commandManager = null!;
+
+ private bool _maintenanceEnabled;
+ private IDisposable? _disposable;
+
+ public bool MaintenanceEnabled
+ {
+ get => _maintenanceEnabled;
+ set
+ {
+ if (value == _maintenanceEnabled) return;
+ _maintenanceEnabled = value;
+
+ Log.Info(_maintenanceEnabled ? "Maintenance Enabled" : "Maintenance Disabled");
+
+ ExecuteCommandsOnModeChange();
+
+ if (!_maintenanceEnabled ||
+ !_configManager.Configuration.GetValue(ConfigKeys.KickOnlinePlayers)) return;
+
+ Torch.Invoke(() =>
+ {
+ Log.Info("Kicking online players");
+ foreach (var steamId in _multiplayerManager.Players.Keys)
+ {
+ if (!GetIsAllowedToJoin(steamId))
+ MyMultiplayer.Static.DisconnectClient(steamId);
+ }
+ });
+ }
+ }
+
+ private void ExecuteCommandsOnModeChange()
+ {
+ var commandsOnEnable =
+ _configManager.Configuration.GetSection(ConfigKeys.CommandsOnMaintenanceEnable).GetChildren()
+ .Select(b => b.Value!).ToArray();
+ var commandsOnDisable =
+ _configManager.Configuration.GetSection(ConfigKeys.CommandsOnMaintenanceDisable).GetChildren()
+ .Select(b => b.Value!).ToArray();
+
+ if (commandsOnEnable.Length <= 0 && commandsOnDisable.Length <= 0) return;
+
+ Torch.Invoke(() =>
+ {
+ switch (_maintenanceEnabled)
+ {
+ case true when commandsOnEnable.Length > 0:
+ {
+ foreach (var command in commandsOnEnable)
+ {
+ _commandManager.HandleCommandFromServer(command,
+ msg => Log.Info("Feedback from `{0}`: `{1}`", command, msg.Message));
+ }
+
+ break;
+ }
+ case false when commandsOnDisable.Length > 0:
+ {
+ foreach (var command in commandsOnDisable)
+ {
+ _commandManager.HandleCommandFromServer(command,
+ msg => Log.Info("Feedback from `{0}`: `{1}`", command, msg.Message));
+ }
+
+ break;
+ }
+ }
+ });
+ }
+
+ public override void Attach()
+ {
+ _eventManager.RegisterHandler(this);
+ _disposable = _configManager.Configuration.GetRequiredSection(ConfigKeys.MaintenanceEnabled).GetReloadToken()
+ .RegisterChangeCallback(
+ _ => MaintenanceEnabled = _configManager.Configuration.GetValue(ConfigKeys.MaintenanceEnabled),
+ null);
+ }
+
+ public override void Detach()
+ {
+ _disposable?.Dispose();
+ }
+
+ [EventHandler]
+ private void OnValidateAuthTicket(ref ValidateAuthTicketEvent info)
+ {
+ if (!MaintenanceEnabled) return;
+
+ var steamId = info.SteamID;
+ var response = info.SteamResponse;
+
+ info.FutureVerdict = FutureVerdict();
+
+ async Task FutureVerdict()
+ {
+ if (await GetIsAllowedToJoinAsync(steamId)) return response;
+
+ Log.Info("Rejecting {0}", steamId);
+ return JoinResult.TicketCanceled;
+ }
+ }
+
+ private static async ValueTask GetIsAllowedToJoinAsync(ulong steamId)
+ {
+ try
+ {
+ var api = LuckPermsProvider.get();
+ var user = await api.getUserManager().loadUser(steamId.GetUuid()).ToTask();
+
+ return user.getCachedData().getPermissionData().checkPermission(BypassPermission).asBoolean();
+ }
+ catch (IllegalStateException)
+ {
+ // we dont have api initialized
+ }
+
+ var stringId = steamId.ToString();
+
+ return MySandboxGame.ConfigDedicated.Administrators.Any(
+ b => b == stringId || b == ConvertSteamIDFrom64(steamId));
+ }
+
+ private bool GetIsAllowedToJoin(ulong steamId)
+ {
+ try
+ {
+ var api = LuckPermsProvider.get();
+ return api.getPlayerAdapter(typeof(IPlayer)).getPermissionData(_multiplayerManager.Players[steamId])
+ .checkPermission(BypassPermission).asBoolean();
+ }
+ catch (IllegalStateException)
+ {
+ // we dont have api initialized
+ }
+
+ return _multiplayerManager.GetUserPromoteLevel(steamId) == MyPromoteLevel.Owner;
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/Managers/MaintenanceScheduleManager.cs b/Maintenance/Managers/MaintenanceScheduleManager.cs
new file mode 100644
index 0000000..ec8ca37
--- /dev/null
+++ b/Maintenance/Managers/MaintenanceScheduleManager.cs
@@ -0,0 +1,181 @@
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Configuration;
+using Torch.API.Managers;
+using Torch.Managers;
+
+namespace Maintenance.Managers;
+
+public class MaintenanceScheduleManager(string storagePath) : IManager
+{
+ [Manager.Dependency]
+ private readonly MaintenanceManager _maintenanceManager = null!;
+
+ [Manager.Dependency]
+ private readonly ConfigManager _configManager = null!;
+
+ [Manager.Dependency]
+ private readonly LanguageManager _languageManager = null!;
+
+ [Manager.Dependency]
+ private readonly IChatManagerServer _chatManager = null!;
+
+ private MaintenanceSchedule _currentSchedule = MaintenanceSchedule.Default;
+
+ private readonly FileInfo _scheduleFile = new(Path.Combine(storagePath, ".schedule"));
+
+ public MaintenanceSchedule CurrentSchedule
+ {
+ get => _currentSchedule;
+ private set
+ {
+ if (value != _currentSchedule)
+ {
+ using var file = _scheduleFile.Create();
+ JsonSerializer.Serialize(file, value);
+ }
+
+ _currentSchedule = value;
+ }
+ }
+
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+
+ public void Attach()
+ {
+ Scheduler();
+
+ if (!_scheduleFile.Exists) return;
+
+ using (var file = _scheduleFile.OpenRead())
+ _currentSchedule = JsonSerializer.Deserialize(file)!;
+
+ _scheduleFile.Delete();
+
+ if (_currentSchedule is not { StartTime: null, EndTime: not null }) return;
+
+ if (!_configManager.Configuration.GetValue(ConfigKeys.ContinueEndTimerAfterRestart))
+ CurrentSchedule = MaintenanceSchedule.Default;
+
+ _maintenanceManager.MaintenanceEnabled = true;
+ }
+
+ private async void Scheduler()
+ {
+ var token = _cancellationTokenSource.Token;
+
+ var timerSecondsSection = _configManager.Configuration.GetSection(ConfigKeys.TimerBroadcastForSeconds);
+ var timerSeconds = timerSecondsSection.GetChildren().Select(b => b.Get()).ToArray();
+
+ using var disposable = timerSecondsSection.GetReloadToken()
+ .RegisterChangeCallback(_ => timerSeconds = timerSecondsSection.GetChildren().Select(b => b.Get()).ToArray(), null);
+
+ while (!token.IsCancellationRequested)
+ {
+ await Task.Delay(2000, token);
+
+ switch (_maintenanceManager.MaintenanceEnabled)
+ {
+ case false when _currentSchedule.StartTime.HasValue:
+ {
+ if (timerSeconds.Any(b => Math.Abs(_currentSchedule.Time.TotalSeconds - b) <= 1))
+ {
+ _chatManager.SendMessageAsOther("Maintenance",
+ Format(_currentSchedule.EndTime.HasValue
+ ? LangKeys.ScheduleTimerBroadcast
+ : LangKeys.StartTimerBroadcast));
+ }
+
+ if (_currentSchedule.Time.TotalSeconds <= 1)
+ {
+ _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.MaintenanceActivated));
+ _maintenanceManager.MaintenanceEnabled = true;
+ CurrentSchedule = CurrentSchedule with { StartTime = null };
+ }
+
+ break;
+ }
+ case true when _currentSchedule.EndTime.HasValue:
+ {
+ if (timerSeconds.Any(b => Math.Abs(_currentSchedule.Duration.TotalSeconds - b) <= 1))
+ {
+ _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.EndTimerBroadcast));
+ }
+
+ if (_currentSchedule.Duration.TotalSeconds <= 1)
+ {
+ _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.MaintenanceDeactivated));
+ _maintenanceManager.MaintenanceEnabled = false;
+ CurrentSchedule = CurrentSchedule with { EndTime = null };
+ }
+
+ break;
+ }
+ }
+ }
+
+ string Format(string key) => _languageManager.Format(key, _currentSchedule);
+ }
+
+ public void Detach()
+ {
+ _cancellationTokenSource.Cancel();
+ _cancellationTokenSource.Dispose();
+ }
+
+ public void ScheduleTimer(TimerType type, TimeSpan duration)
+ {
+ if (_maintenanceManager.MaintenanceEnabled && type == TimerType.Start)
+ throw new InvalidOperationException("Maintenance is already enabled");
+
+ if (type == TimerType.Start)
+ CurrentSchedule = CurrentSchedule with { StartTime = DateTimeOffset.Now + duration };
+ else
+ CurrentSchedule = CurrentSchedule with { EndTime = DateTimeOffset.Now + duration };
+ }
+
+ public void ScheduleMaintenance(TimeSpan startTime, TimeSpan endTime)
+ {
+ if (_maintenanceManager.MaintenanceEnabled)
+ throw new InvalidOperationException("Maintenance is already enabled");
+
+ var startDateTime = DateTimeOffset.Now + startTime;
+
+ CurrentSchedule = new(startDateTime, startDateTime + endTime);
+ }
+
+ public void CancelTimer(TimerType? type)
+ {
+ CurrentSchedule = type switch
+ {
+ TimerType.Start when !_currentSchedule.StartTime.HasValue => throw new InvalidOperationException(
+ "No start timer running"),
+ TimerType.Start => MaintenanceSchedule.Default,
+ TimerType.End when !_currentSchedule.EndTime.HasValue => throw new InvalidOperationException(
+ "No end timer running"),
+ TimerType.End => CurrentSchedule with { EndTime = null },
+ _ => MaintenanceSchedule.Default
+ };
+ }
+}
+
+public record MaintenanceSchedule(DateTimeOffset? StartTime, DateTimeOffset? EndTime)
+{
+ public static MaintenanceSchedule Default => new(null, null);
+
+ [JsonIgnore]
+ public TimeSpan Time => Round((StartTime ?? throw new InvalidOperationException("No start timer running"))
+ - DateTimeOffset.Now);
+ [JsonIgnore]
+ public TimeSpan Duration => Round((EndTime ?? throw new InvalidOperationException("No end timer running"))
+ - (StartTime ?? DateTimeOffset.Now));
+
+ private static TimeSpan Round(TimeSpan timeSpan) => TimeSpan.FromSeconds(Math.Round(timeSpan.TotalSeconds, 0));
+}
+
+public enum TimerType : byte
+{
+ Start,
+ End
+}
\ No newline at end of file
diff --git a/Maintenance/Patches/SteamQueryPatch.cs b/Maintenance/Patches/SteamQueryPatch.cs
new file mode 100644
index 0000000..2f9d3a4
--- /dev/null
+++ b/Maintenance/Patches/SteamQueryPatch.cs
@@ -0,0 +1,361 @@
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using Maintenance.Managers;
+using Microsoft.Extensions.Configuration;
+using NLog;
+using Sandbox;
+using Sandbox.Engine.Multiplayer;
+using Sandbox.Game.Multiplayer;
+using Sandbox.Game.World;
+using Steamworks;
+using Torch;
+using Torch.API;
+using Torch.API.Managers;
+using Torch.Managers.PatchManager;
+using Torch.Utils;
+using VRage.Game;
+using VRage.GameServices;
+using VRage.Library.Utils;
+using VRage.Utils;
+
+namespace Maintenance.Patches;
+
+[PatchShim]
+public static class SteamQueryPatch
+{
+ private static readonly ILogger Log = LogManager.GetCurrentClassLogger();
+
+ [ReflectedMethodInfo(null, "SteamServerEntryPoint", TypeName = "VRage.Steam.MySteamGameServer, VRage.Steam")]
+ private static readonly MethodInfo EntryPointMethod = null!;
+
+ [ReflectedMethodInfo(typeof(SteamQueryPatch), nameof(Prefix))]
+ private static readonly MethodInfo PrefixMethod = null!;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ private static ITorchBase Torch => TorchBase.Instance;
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ private static bool MaintenanceEnabled =>
+ Torch.CurrentSession?.Managers.GetManager().MaintenanceEnabled is true;
+
+ public static void Patch(PatchContext context)
+ {
+ context.GetPattern(EntryPointMethod).Prefixes.Add(PrefixMethod);
+ }
+
+ private static bool Prefix(IMyGameServer __instance, object argument)
+ {
+ var socket = (Socket)argument;
+ RunServerAsync(__instance, socket);
+
+ return false;
+ }
+
+ private static async void RunServerAsync(IMyGameServer server, Socket socket)
+ {
+ var localEndPoint = socket.LocalEndPoint;
+ var zeroEndPoint = new IPEndPoint(0L, 0);
+ // better to calc based on mtu but i dont care
+ var buffer = new byte[1500];
+
+ while (server.Running)
+ {
+ SocketReceiveFromResult result;
+ try
+ {
+ result = await socket.ReceiveFromAsync(new(buffer), SocketFlags.None, zeroEndPoint);
+ }
+ catch (SocketException ex)
+ {
+ if (!server.Running)
+ break;
+
+ try
+ {
+ socket.Close();
+ }
+ catch
+ {
+ // ignored
+ }
+
+ Log.Warn($"Received socket exception with error code: {ex.ErrorCode}, {ex.SocketErrorCode}", ex);
+ Log.Info("Attempting to create new socket.");
+ socket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ try
+ {
+ socket.Bind(localEndPoint);
+ continue;
+ }
+ catch (SocketException e)
+ {
+ Log.Fatal(e, "Error binding server endpoint");
+
+ socket.Close();
+ server.GetType().GetProperty("Running")?.SetValue(server, false);
+ GameServer.Shutdown();
+ break;
+ }
+ }
+
+ if (!CheckHeader(buffer.AsSpan(0, 4)))
+ continue;
+
+ if (result.ReceivedBytes > 4 && MySession.Static is not null)
+ {
+ Debug.Write("byte is ");
+ Debug.WriteLine(buffer[4].ToString("X"));
+ switch (buffer[4])
+ {
+ case 0x54:
+ await HandleInfoRequestAsync(socket, result.RemoteEndPoint);
+ continue;
+ // players request without challenge
+ case 0x55 when result.ReceivedBytes > 8 && !CheckHeader(buffer.AsSpan(5, 4)):
+ await HandlePlayersRequestAsync(socket, result.RemoteEndPoint);
+ continue;
+ }
+ }
+
+ var remoteEndPoint = (IPEndPoint)result.RemoteEndPoint;
+
+#pragma warning disable CS0618
+ SteamGameServer.HandleIncomingPacket(buffer, result.ReceivedBytes, (uint)remoteEndPoint.Address.Address,
+ (ushort)remoteEndPoint.Port);
+#pragma warning restore CS0618
+
+ int length;
+ while ((length = SteamGameServer.GetNextOutgoingPacket(buffer, buffer.Length, out var addr, out var port)) >
+ 0)
+ {
+#pragma warning disable CS0618
+ remoteEndPoint.Address.Address = addr;
+#pragma warning restore CS0618
+ remoteEndPoint.Port = port;
+
+ await socket.SendToAsync(new(buffer, 0, length), SocketFlags.None, remoteEndPoint);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool CheckHeader(Span header) =>
+ Unsafe.ReadUnaligned(ref header.GetPinnableReference()) == 0xFFFFFFFF;
+
+ private static Task HandlePlayersRequestAsync(Socket socket, EndPoint sender)
+ {
+ if (Sync.Players is null)
+ return Task.CompletedTask;
+
+ Debug.WriteLine($"players request from {sender}");
+ using var ms = new MemoryStream();
+
+ // header
+ ms.WriteNoAlloc(0xFFFFFFFF);
+ ms.WriteByte(0x44);
+
+ if (MaintenanceEnabled)
+ {
+ ms.WriteByte(0); // count
+ return SendAsync(ms, socket, sender);
+ }
+
+ var players = Sync.Clients.GetClients().Where(b => !b.IsLocal).Select(b => (b.DisplayName,
+ (float?)(DateTime.Now - Sync.Players.TryGetIdentity((long)b.SteamUserId)?.LastLoginTime)?.TotalSeconds ??
+ 0f)).ToArray();
+
+ var count = (byte)players.Length;
+
+ // total
+ ms.WriteByte(count);
+
+ for (byte i = 0; i < count; i++)
+ {
+ // i
+ ms.WriteByte(i);
+
+ var (name, duration) = players[i];
+
+ // name
+ WriteUtf8String(ms, name);
+
+ // score
+ ms.WriteNoAlloc(0U);
+
+ // duration
+ ms.WriteNoAlloc(duration);
+ }
+
+ return SendAsync(ms, socket, sender);
+ }
+
+ private static Task HandleInfoRequestAsync(Socket socket, EndPoint sender)
+ {
+ if (Sync.Players is null)
+ return Task.CompletedTask;
+
+ Debug.WriteLine($"info request from {sender}");
+ using var ms = new MemoryStream();
+
+ var mp = MyMultiplayer.Static;
+
+ // header
+ ms.WriteNoAlloc(0xFFFFFFFF);
+ ms.WriteByte(0x49);
+
+ // protocol
+ ms.WriteByte(0x11);
+
+ var maintenanceEnabled = MaintenanceEnabled;
+
+ // name
+ WriteUtf8String(ms, mp.HostName);
+
+ if (maintenanceEnabled)
+ {
+ var config = Torch.Managers.GetManager().Configuration;
+ var formatter = Torch.Managers.GetManager().Formatter;
+
+ if (config.GetValue(ConfigKeys.EnableWorldMessage))
+ {
+ var randomItem = config.GetSection(ConfigKeys.WorldMessage).GetChildren().Select(b => b.Value!).ToArray()
+ .GetRandomItem();
+ var schedule = Torch.CurrentSession.Managers.GetManager().CurrentSchedule;
+
+ // map
+ WriteUtf8String(ms, schedule.EndTime.HasValue ? formatter.Format(randomItem, schedule) : randomItem);
+ }
+ else
+ {
+ // map
+ WriteUtf8String(ms, mp.WorldName);
+ }
+ }
+ else
+ {
+ // map
+ WriteUtf8String(ms, mp.WorldName);
+ }
+
+ // folder
+ WriteUtf8String(ms, "Space Engineers");
+
+ // game full name
+ WriteUtf8String(ms, "Space Engineers");
+
+ // app id
+ ms.WriteNoAlloc((short)0);
+
+ // players
+ ms.WriteByte(maintenanceEnabled ? default : (byte)(Sync.Clients.Count - 1));
+
+ // max players
+ ms.WriteByte((byte)(maintenanceEnabled ? 0x00 : mp.MaxPlayers));
+
+ // bots
+ ms.WriteByte(0x00);
+
+ // server type
+ ms.WriteByte((byte)'d');
+
+ // env
+ ms.WriteByte((byte)'w');
+
+ // visibility
+ ms.WriteByte(0x00);
+
+ // vac
+ ms.WriteByte(0x01);
+
+ // version
+ WriteUtf8String(ms, MyFinalBuildConstants.APP_VERSION.Version.ToString());
+
+ // edf (GameID SteamID Keywords Port)
+ ms.WriteByte(177);
+
+ // edf port
+ ms.WriteNoAlloc((short)((IPEndPoint)socket.LocalEndPoint).Port);
+
+ // edf steam id
+ ms.WriteNoAlloc(Sync.MyId);
+
+ // edf keywords
+ var gameMode = mp.GameMode is MyGameModeEnum.Survival
+ ? $"S{mp.InventoryMultiplier}-{mp.BlocksInventoryMultiplier}-{mp.AssemblerMultiplier}-{mp.RefineryMultiplier}"
+ : "C";
+ WriteUtf8String(
+ ms,
+ $"groupId{MySandboxGame.ConfigDedicated.GroupID} version{MyFinalBuildConstants.APP_VERSION} datahasheRRN1/jJ7J2ZlR7GB1D5PDzn0sQ= mods{mp.ModCount} gamemode{gameMode} view{mp.SyncDistance}");
+
+ // edf game id
+ ms.WriteNoAlloc((ulong)244850);
+
+ return SendAsync(ms, socket, sender);
+ }
+
+ private static async Task SendAsync(MemoryStream ms, Socket socket, EndPoint sender)
+ {
+ Debug.WriteLine(string.Join(" ", ms.GetBuffer().Take((int)ms.Length).Select(b => b.ToString("X"))));
+
+ const int packetSize = 1200;
+
+ if (ms.Length < packetSize)
+ {
+ await socket.SendToAsync(new(ms.GetBuffer(), 0, (int)ms.Length), SocketFlags.None, sender);
+ return;
+ }
+
+ var id = (uint)MyRandom.Instance.Next();
+ id &= ~(1 << 31);
+
+ var msLength = ms.Length - 4;
+ for (var i = 0; i < msLength; i += packetSize)
+ {
+ var length = Math.Min(packetSize, (int)msLength - i);
+
+ var buffer = new byte[length + 10];
+
+ // header
+ Unsafe.WriteUnaligned(ref buffer.AsSpan(0, 4).GetPinnableReference(), 0xFFFFFFFE);
+
+ // id
+ Unsafe.WriteUnaligned(ref buffer.AsSpan(4, 4).GetPinnableReference(), id);
+
+ // total
+ buffer[8] = (byte)Math.Ceiling((float)msLength / packetSize);
+
+ // number
+ buffer[9] = (byte)(i / packetSize);
+
+ ms.GetBuffer().AsSpan(i + 4, length).CopyTo(buffer.AsSpan(10));
+
+ await socket.SendToAsync(new(buffer), SocketFlags.None, sender);
+ }
+ }
+
+ private static unsafe void WriteUtf8String(Stream stream, string str)
+ {
+ if (string.IsNullOrEmpty(str))
+ {
+ stream.WriteByte(0x00);
+ return;
+ }
+
+ var chars = str.AsSpan();
+ Span bytes = stackalloc byte[Encoding.UTF8.GetByteCount(str)];
+
+ var length = Encoding.UTF8.GetBytes((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(chars)),
+ chars.Length,
+ (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(bytes)),
+ bytes.Length);
+
+ stream.WriteNoAlloc((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(bytes)), 0, length);
+ stream.WriteByte(0x00);
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/Plugin.cs b/Maintenance/Plugin.cs
new file mode 100644
index 0000000..d24220d
--- /dev/null
+++ b/Maintenance/Plugin.cs
@@ -0,0 +1,21 @@
+using System.IO;
+using Maintenance.Managers;
+using Torch;
+using Torch.API;
+using Torch.API.Managers;
+using Torch.API.Session;
+
+namespace Maintenance;
+
+public class Plugin : TorchPluginBase
+{
+ public override void Init(ITorchBase torch)
+ {
+ var storagePath = Directory.CreateDirectory(Path.Combine(StoragePath, "maintenance")).FullName;
+
+ torch.Managers.AddManager(new ConfigManager(storagePath));
+ torch.Managers.AddManager(new LanguageManager(storagePath));
+ torch.Managers.GetManager().AddFactory(s => new MaintenanceManager(s.Torch));
+ torch.Managers.GetManager().AddFactory(_ => new MaintenanceScheduleManager(storagePath));
+ }
+}
\ No newline at end of file
diff --git a/Maintenance/Resources/config.yml b/Maintenance/Resources/config.yml
new file mode 100644
index 0000000..e1a3e9b
--- /dev/null
+++ b/Maintenance/Resources/config.yml
@@ -0,0 +1,32 @@
+# Enables maintenance mode.
+maintenance-enabled: false
+
+# Any extra commands inside the arrays will be executed when maintenance is enabled/disabled.
+# Example: commands-on-maintenance-enable: ["say hello!", "stop"]
+commands-on-maintenance-enable: []
+commands-on-maintenance-disable: []
+
+# If set to true, the world name for the servers list from this pool will be chosen.
+# If you put in multiple entries, one of them will be chosen randomly on every ping.
+# If running an endtimer, the time left can be displayed by including {duration} in the message.
+# Additionally you can set custom timestamp format with {duration:hh\\:mm\\:\\ss}. See all available formats at https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings
+enable-world-message: true
+world-message:
+# - "[MAINTENANCE]"
+# - "Back in {duration:hh\\:mm}"
+
+# Set this to false if you do not want players to be kicked when you enable maintenance (new connections will still be blocked).
+# ... I don't know why you would want that, but you can disable it. :p
+kick-online-players: true
+
+# Changes the language of command feedback/messages.
+# If you find missing translations or want to contribute a new language file, you are very welcome to message me on my Discord server! :)
+# Currently available are: en (English)
+language: en
+
+# If enabled and the server is restarted while running an endtimer, the timer will be continued after the restart.
+# If the timer ends while the server is offline, maintenance will be disabled as soon as the server starts again.
+continue-endtimer-after-restart: true
+
+# If using the timer command: In what intervals before enabling/disabling maintenance there will be a broadcast.
+timer-broadcast-for-seconds: [1200, 900, 600, 300, 120, 60, 30, 20, 10, 5, 4, 3, 2, 1]
\ No newline at end of file
diff --git a/Maintenance/Resources/translations/en.yml b/Maintenance/Resources/translations/en.yml
new file mode 100644
index 0000000..0b0e4b5
--- /dev/null
+++ b/Maintenance/Resources/translations/en.yml
@@ -0,0 +1,13 @@
+maintenanceActivated: "Maintenance mode is now activated."
+maintenanceDeactivated: "Maintenance mode is now deactivated."
+alreadyEnabled: "Maintenance is already enabled!"
+alreadyDisabled: "Maintenance is already disabled!"
+endtimerBroadcast: "Maintenance mode will be disabled in {duration}."
+endtimerStarted: "Started timer: Maintenance mode will be deactivated in {duration}."
+starttimerBroadcast: "Maintenance mode will be enabled in {time}."
+starttimerStarted: "Started timer: Maintenance mode will be activated in {time}."
+scheduletimerBroadcast: "Maintenance mode will be enabled in {time} and will last for {duration}."
+timerAlreadyRunning: "There is already a timer scheduled!"
+timerNotRunning: "There is currently no running timer."
+timerCancelled: "The current timer has been disabled."
+timerTooLong: "The number has to be less than 40320 (28 days)!"
\ No newline at end of file
diff --git a/Maintenance/manifest.xml b/Maintenance/manifest.xml
new file mode 100644
index 0000000..526796c
--- /dev/null
+++ b/Maintenance/manifest.xml
@@ -0,0 +1,6 @@
+
+
+ Maintenance
+ 42AF9955-AAA7-442F-9BF4-AAC4FA4A923F
+ v1.0.0
+
\ No newline at end of file
diff --git a/Maintenance/packages.lock.json b/Maintenance/packages.lock.json
new file mode 100644
index 0000000..a741a67
--- /dev/null
+++ b/Maintenance/packages.lock.json
@@ -0,0 +1,438 @@
+{
+ "version": 1,
+ "dependencies": {
+ ".NETFramework,Version=v4.8": {
+ "Alexinea.Extensions.Configuration.Yaml": {
+ "type": "Direct",
+ "requested": "[7.0.0, )",
+ "resolved": "7.0.0",
+ "contentHash": "gIeecqFF2YeDrGxRulJvyYCRbQcvmowNnQt5qDg2FUS3bzjtwgahHFioD+yWFGxBYR/vWGbP3h6PGoCUxOzTqA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "7.0.0",
+ "Microsoft.Extensions.Configuration.FileExtensions": "7.0.0",
+ "YamlDotNet": "12.1.0"
+ }
+ },
+ "LuckPerms.Torch.Api": {
+ "type": "Direct",
+ "requested": "[5.4.0, )",
+ "resolved": "5.4.0",
+ "contentHash": "jwmRs6dJupqqj2V5bf1vMleQIFqKxhsaLxiNvTFFrzsgFjPJf3TDMEe68Qf62/J1Z2gQKvVbSw/LCGCn+ZOWmw==",
+ "dependencies": {
+ "IKVM.Maven.Sdk": "1.6.1",
+ "LuckPerms.Torch.Utils": "1.0.0",
+ "Torch.Loader": "1.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Direct",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.FileExtensions": {
+ "type": "Direct",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "8.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
+ "Microsoft.Extensions.FileProviders.Physical": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.NETFramework.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.0.3, )",
+ "resolved": "1.0.3",
+ "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
+ "dependencies": {
+ "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
+ }
+ },
+ "PolySharp": {
+ "type": "Direct",
+ "requested": "[1.8.1, )",
+ "resolved": "1.8.1",
+ "contentHash": "T60CnqUsOC8JT2O9ixmBVBoA+0n4FIkRGwK4IuuPgncsJYd5m44s/IaNMQsCzZ7nxmPHa6A9DEaXSi1/ENx8iA=="
+ },
+ "SmartFormat": {
+ "type": "Direct",
+ "requested": "[3.3.0, )",
+ "resolved": "3.3.0",
+ "contentHash": "JN19FJuWZwW3G3euSQNTDLAr5wI8ok/VDcBfEN88cZPvzCQMPdyS9s4/fQlEuupXH+IV0wBEbodHVS51t2RIDQ==",
+ "dependencies": {
+ "System.Memory": "4.5.5",
+ "System.ValueTuple": "4.5.0"
+ }
+ },
+ "Torch.Server.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.3.1.260-master, )",
+ "resolved": "1.3.1.260-master",
+ "contentHash": "p9fHBwPI2BZDLO2PiSPvJxHQ7lksYf/20BZ0uUxMlSnJk/AvFUpjT6CMxJWow4UuAFG+NcPEI4VhxZ5x9jhGGA==",
+ "dependencies": {
+ "NLog": "4.4.12",
+ "Newtonsoft.Json": "12.0.2",
+ "SpaceEngineersDedicated.ReferenceAssemblies": "1.203.505"
+ }
+ },
+ "IKVM": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "CEgKzDhnBq5XoCt0ABREfIn6k0TqF2go7zFAiaI9cjXesPezfquVJRPPn9fl+hO90fOo7eCDnkgDn5B8DoZe1w==",
+ "dependencies": {
+ "IKVM.Image": "8.7.1",
+ "IKVM.MSBuild": "8.7.1",
+ "Mono.Posix": "7.1.0-final.1.21458.1",
+ "Mono.Unix": "7.1.0-final.1.21458.1",
+ "System.Buffers": "4.5.1",
+ "System.IO.Pipelines": "6.0.3",
+ "System.Memory": "4.5.5",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+ "System.Runtime.InteropServices.RuntimeInformation": "4.3.0",
+ "System.Text.Json": "6.0.6",
+ "System.ValueTuple": "4.5.0"
+ }
+ },
+ "IKVM.Image": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "RqwuWeb9cwqmcTKREb71odXvv5NO5+Zvo3SSG2CW1XumqCwCkm7sFdzulTYHEQcVs8GjDUlvLIEnrb6QLsqb7w==",
+ "dependencies": {
+ "IKVM.Image.runtime.linux-arm": "8.7.1",
+ "IKVM.Image.runtime.linux-arm64": "8.7.1",
+ "IKVM.Image.runtime.linux-musl-arm": "8.7.1",
+ "IKVM.Image.runtime.linux-musl-arm64": "8.7.1",
+ "IKVM.Image.runtime.linux-musl-x64": "8.7.1",
+ "IKVM.Image.runtime.linux-x64": "8.7.1",
+ "IKVM.Image.runtime.osx-arm64": "8.7.1",
+ "IKVM.Image.runtime.osx-x64": "8.7.1",
+ "IKVM.Image.runtime.win-arm64": "8.7.1",
+ "IKVM.Image.runtime.win-x64": "8.7.1",
+ "IKVM.Image.runtime.win-x86": "8.7.1"
+ }
+ },
+ "IKVM.Image.runtime.linux-arm": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "f12l6AKUfM8Q1Dv9ZbkW39Ok2URq8ar9buhvXFtCeDbXxi5euYyYfR/eehM3nSb0wTo3cyvGxqM/G5M/jI9WUg=="
+ },
+ "IKVM.Image.runtime.linux-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "o1URwAXZjfgdkS6R12ZA/l2oKhk5j9f98YjUiQfhzgt63ksVHjz3f5sRELDrsHUCAWNT2GR3ahlPHABuD3WT4A=="
+ },
+ "IKVM.Image.runtime.linux-musl-arm": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "+3ijUii2jwsl0KeZZ3nkiwKkie0qpskLerARjrKdKQ3ylDxVaSwxtQWGJT7Eiu1YpKbyKYXOMptHE1IQqO4Zkg=="
+ },
+ "IKVM.Image.runtime.linux-musl-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "9cXMSPAa8ODf99vuce0wnFV9urYy4iVjAp+pE4ieejqp/Jk2NCtKr7NJs+Q6shs3nsOl6A2aJnxZV5FGnd/wCg=="
+ },
+ "IKVM.Image.runtime.linux-musl-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "K3DO1q2kGYnnyFNZ3K27K3Q19yl5U+at4le9aA9O/Or3zpXi2c1GOFaA+hrVB7DzIrvxXGCR6qKGzM9K8goYmg=="
+ },
+ "IKVM.Image.runtime.linux-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "bG9Sara5Ft7JTf8iMS9RUea4Foi05Vdr7t7VA37SeloTTxlFmNUYSafo+X1U9eOYuv8XUueveid07E+KzMTSGQ=="
+ },
+ "IKVM.Image.runtime.osx-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "vShR5pUZJIsTsc1lUzsNdBINGjPZ20RrUIunQe9W/rCW63El3uKZq2Rboa2E50YXNG3BgmMaCEUmel3nqS8DQw=="
+ },
+ "IKVM.Image.runtime.osx-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "E/IFd3Eqj/6vxk1EqRBD5lpZGwI3qx/Uocqnt98UGr01HPO2ZvNs3Y2kmSmz3hA/xwDrjObCVrdYWKsfzoIwTw=="
+ },
+ "IKVM.Image.runtime.win-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "oSvRLYy3iWgdJyFkbmNr0pl4x9/1zbXvavybpOzTFa7xSg3dfAcysUi8nHgjNpd94z8udrOcKEbm8GQ5DKAdRg=="
+ },
+ "IKVM.Image.runtime.win-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "Od5ynm5wVyzV85CQ5VfUmQAvbrEKjRxEIR1Id2tHugVGPnL3cviEezbokg+tkByIubIx1mAjJ4HGqtcNqTDpmA=="
+ },
+ "IKVM.Image.runtime.win-x86": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "yBXBH2Ad8UjJ6ZhH6eqJyya8a22Et5lAUNQ0S8Vtexmay+oby3880bzvUkQ0rRKF1OceJxm6u3bLC58hnh16eQ=="
+ },
+ "IKVM.Maven.Sdk": {
+ "type": "Transitive",
+ "resolved": "1.6.1",
+ "contentHash": "Gb8bB12uYQGxiFggrT540Sy/71xYQr+8JSrauG5OlKiIcQhh6k3vI7I7hLQAWdd+bzdFYO7rs/BxdPzzV+j3Fg==",
+ "dependencies": {
+ "IKVM": "8.7.1"
+ }
+ },
+ "IKVM.MSBuild": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "gkSV0y7mZ25PgXw9cnTnVFhE/oa9XSxcPkXnYS2I4XQ+moKkPeuZokUJaFy/qN1hcJIL8s9UKo6kUNwjjEFLBQ==",
+ "dependencies": {
+ "IKVM.MSBuild.Tools": "8.7.1"
+ }
+ },
+ "IKVM.MSBuild.Tools": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "MJzc2ymPe27pY3Cz+s6yvr67RuVnAsFKuD0hSdN2wnERpz/K2wl9ZSaJ/egRJRrfs/0R2qqYo4Eer6H9Mcc4ag==",
+ "dependencies": {
+ "IKVM.MSBuild.Tools.runtime.linux-arm": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.linux-arm64": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.linux-x64": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.osx-arm64": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.osx-x64": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.win-arm64": "8.7.1",
+ "IKVM.MSBuild.Tools.runtime.win-x64": "8.7.1"
+ }
+ },
+ "IKVM.MSBuild.Tools.runtime.linux-arm": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "2X2meYtplgzEJpZEHpEdrnhbyzKlJmfkOfKMenMM1W707JQ2SsWTDHlWw7PwY3KzPKoDbqNWb69rHS9a99TnUQ=="
+ },
+ "IKVM.MSBuild.Tools.runtime.linux-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "BmhJBqbjn693fLo5G+YyXB7uDF11Ia5k1K7INk1zMYiBcfYZHJUEE29KZ5fvXgspjn6OEazgmOF/PxoxWTBmkQ=="
+ },
+ "IKVM.MSBuild.Tools.runtime.linux-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "zBaGJoJ4pvf3uPNfxreHPGGhQFWUqiLyGcDurTuPGix29T8c3TeCU4z5JAJkhGf7hat/s98pSzqt9T5uAahSDg=="
+ },
+ "IKVM.MSBuild.Tools.runtime.osx-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "XCG8udNMf7WjfUth7K1x63vP/Kra+4p/WByzMHNvLoM47x2XVX8OMyw1N6SGah+cHtsKL8VKkLdlct/b2Cpxbw=="
+ },
+ "IKVM.MSBuild.Tools.runtime.osx-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "boMF3LCpTrkbi2emU263gVHz/TYCgtCVhDMLXTojIaJ8guA5q/nuFufhYljpjECzxzVg3IUIX6jNBu4PEjC+HQ=="
+ },
+ "IKVM.MSBuild.Tools.runtime.win-arm64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "TRssQQBZbeXBKDtF8yo5bLk9mXmnvN1MUucLgP+t+6araaYUohsVJieMoDiJhcVqWgStv88+BHKLCexII/NUFw=="
+ },
+ "IKVM.MSBuild.Tools.runtime.win-x64": {
+ "type": "Transitive",
+ "resolved": "8.7.1",
+ "contentHash": "tS4FAjGeZ0Gvaabb6Jy74Haw20kZu9w8AP9P+h27aYJm9udKqc+XFHRlifPOnXazVbbPOW1PkUtrk4TEsLYeRw=="
+ },
+ "LuckPerms.Torch.Utils": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "lEapa9SvIZDOaD4/vJ05Ire5Ixl3lSLhg6LlEvArVoC1rBGGriNV/9h5rFwS+fVpJOvkUWvwuX6HPO7rfspDgA==",
+ "dependencies": {
+ "IKVM": "8.7.1"
+ }
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==",
+ "dependencies": {
+ "System.Threading.Tasks.Extensions": "4.5.4"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0",
+ "System.ValueTuple": "4.5.0"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Physical": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
+ "Microsoft.Extensions.FileSystemGlobbing": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.FileSystemGlobbing": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ=="
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
+ "dependencies": {
+ "System.Memory": "4.5.5",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ }
+ },
+ "Microsoft.NETFramework.ReferenceAssemblies.net48": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
+ },
+ "Mono.Posix": {
+ "type": "Transitive",
+ "resolved": "7.1.0-final.1.21458.1",
+ "contentHash": "xhil/0zRkA2MrkyMZXC3dPSDWOhq6YD0vYGl1VnBbjsEZfLQCu7+mJZ/ftnOd0r4qmeHVeNuW6Pt33NoxO671A==",
+ "dependencies": {
+ "Mono.Unix": "7.1.0-final.1.21458.1"
+ }
+ },
+ "Mono.Unix": {
+ "type": "Transitive",
+ "resolved": "7.1.0-final.1.21458.1",
+ "contentHash": "Rhxz4A7By8Q0wEgDqR+mioDsYXGrcYMYPiWE9bSaUKMpG8yAGArhetEQV5Ms6KhKCLdQTlPYLBKPZYoKbAvT/g=="
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "12.0.2",
+ "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA=="
+ },
+ "NLog": {
+ "type": "Transitive",
+ "resolved": "4.4.12",
+ "contentHash": "fODew3BFT2XhAuqhGo2ZYT9OJE0ciGEBfvKXOmAAvaDsP/3ROIf083p8QUnmwKKw4jCkVW06ObX6gn/eFi2Skg=="
+ },
+ "protobuf-net": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "kTGOK0E87473sOImOjgZOnz3kTC2aMLffoRWQLYNuBLJnwNNmjanF9IkevZ9Q7yYLeABQfcF3BpeepuMntMVNw=="
+ },
+ "SpaceEngineersDedicated.ReferenceAssemblies": {
+ "type": "Transitive",
+ "resolved": "1.203.505",
+ "contentHash": "Wq4GIn2ilHyFdLdVKdVhC7iGQ+1FdVsChKY6hyQluFYSSV7oe8bDc9aTZC8XgxNMKCZoQBSVyaYHxQD+74BySQ==",
+ "dependencies": {
+ "protobuf-net": "1.0.0"
+ }
+ },
+ "System.Buffers": {
+ "type": "Transitive",
+ "resolved": "4.5.1",
+ "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
+ },
+ "System.IO.Pipelines": {
+ "type": "Transitive",
+ "resolved": "6.0.3",
+ "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==",
+ "dependencies": {
+ "System.Buffers": "4.5.1",
+ "System.Memory": "4.5.4",
+ "System.Threading.Tasks.Extensions": "4.5.4"
+ }
+ },
+ "System.Memory": {
+ "type": "Transitive",
+ "resolved": "4.5.5",
+ "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
+ "dependencies": {
+ "System.Buffers": "4.5.1",
+ "System.Numerics.Vectors": "4.5.0",
+ "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+ }
+ },
+ "System.Numerics.Vectors": {
+ "type": "Transitive",
+ "resolved": "4.5.0",
+ "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
+ },
+ "System.Runtime.CompilerServices.Unsafe": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
+ },
+ "System.Runtime.InteropServices.RuntimeInformation": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw=="
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
+ "dependencies": {
+ "System.Buffers": "4.5.1",
+ "System.Memory": "4.5.4",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ }
+ },
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "6.0.6",
+ "contentHash": "GZ+62pLOr544jwSvyXv5ezSfzlFBTjLuPhgOS2dnKuknAA8dPNUGXLKTHf9XdsudU9JpbtweXnE4oEiKEB2T1Q==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "6.0.0",
+ "System.Buffers": "4.5.1",
+ "System.Memory": "4.5.4",
+ "System.Numerics.Vectors": "4.5.0",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+ "System.Text.Encodings.Web": "6.0.0",
+ "System.Threading.Tasks.Extensions": "4.5.4",
+ "System.ValueTuple": "4.5.0"
+ }
+ },
+ "System.Threading.Tasks.Extensions": {
+ "type": "Transitive",
+ "resolved": "4.5.4",
+ "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
+ "dependencies": {
+ "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+ }
+ },
+ "System.ValueTuple": {
+ "type": "Transitive",
+ "resolved": "4.5.0",
+ "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
+ },
+ "Torch.Loader": {
+ "type": "Transitive",
+ "resolved": "1.0.0",
+ "contentHash": "HkuAujKa9IqPPqoA1205teUPBxuNOC9z0xZJkrMlFT0htH02X0ieZ5qh4onwyV10qVKiRBCSLkc5tA8lp1l5ig=="
+ },
+ "YamlDotNet": {
+ "type": "Transitive",
+ "resolved": "12.1.0",
+ "contentHash": "iP6tzi3DJ16wyTtEClbG8W6epvc+MvKSBdNcpllzhal40C94WzjWxF2aszcoOSjbESsWXsLY+NoRewgfo8ah6Q=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/TorchPlugins.sln b/TorchPlugins.sln
index c9a2615..2963931 100644
--- a/TorchPlugins.sln
+++ b/TorchPlugins.sln
@@ -27,6 +27,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libs", "libs", "{862C7244-2
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "heh", "heh\heh.csproj", "{927CB303-E699-4716-A62E-232AE1125159}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maintenance", "Maintenance\Maintenance.csproj", "{66F3BF72-D663-44E0-B54C-5193E39839B9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LuckPerms.Torch.Utils", "LuckPerms.Torch.Utils\LuckPerms.Torch.Utils.csproj", "{615FD610-A3E6-457F-B8B9-C9D1235A4B2C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -65,6 +69,14 @@ Global
{927CB303-E699-4716-A62E-232AE1125159}.Debug|Any CPU.Build.0 = Debug|Any CPU
{927CB303-E699-4716-A62E-232AE1125159}.Release|Any CPU.ActiveCfg = Release|Any CPU
{927CB303-E699-4716-A62E-232AE1125159}.Release|Any CPU.Build.0 = Release|Any CPU
+ {66F3BF72-D663-44E0-B54C-5193E39839B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {66F3BF72-D663-44E0-B54C-5193E39839B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {66F3BF72-D663-44E0-B54C-5193E39839B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {66F3BF72-D663-44E0-B54C-5193E39839B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {615FD610-A3E6-457F-B8B9-C9D1235A4B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {615FD610-A3E6-457F-B8B9-C9D1235A4B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {615FD610-A3E6-457F-B8B9-C9D1235A4B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {615FD610-A3E6-457F-B8B9-C9D1235A4B2C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AD9B7D1E-386A-4EF2-B475-BCB770537035} = {06CD2354-307D-4A1C-B46B-1D9EB3AAE742}
@@ -75,5 +87,7 @@ Global
{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13} = {557A4A51-B8ED-4CA0-866D-D18D219129F3}
{8F9D910F-FFE6-4010-921F-5872ACF638BB} = {557A4A51-B8ED-4CA0-866D-D18D219129F3}
{927CB303-E699-4716-A62E-232AE1125159} = {862C7244-258E-4BFD-B271-9AA2D3FBE916}
+ {66F3BF72-D663-44E0-B54C-5193E39839B9} = {2C911BD8-8B11-460E-AB7E-16552949A6FC}
+ {615FD610-A3E6-457F-B8B9-C9D1235A4B2C} = {862C7244-258E-4BFD-B271-9AA2D3FBE916}
EndGlobalSection
EndGlobal
diff --git a/heh/heh.csproj b/heh/heh.csproj
index 3954188..4025015 100644
--- a/heh/heh.csproj
+++ b/heh/heh.csproj
@@ -16,6 +16,6 @@
-
+