From 6e0922b8050fefa47deb9f0e00596cc4cbf8f353 Mon Sep 17 00:00:00 2001 From: John Gross Date: Sun, 1 Jan 2017 16:37:30 -0800 Subject: [PATCH] My New Year's resolution is to stop making commits like these --- TestPlugin/Class1.cs | 33 ++++ TestPlugin/Properties/AssemblyInfo.cs | 36 ++++ TestPlugin/TestPlugin.csproj | 61 +++++++ Torch/TorchPlugin.cs => Torch.API/ILogger.cs | 7 +- Torch.API/IPluginManager.cs | 13 +- Torch.API/ITorchBase.cs | 6 +- Torch.API/ITorchPlugin.cs | 13 +- Torch.API/PluginAttribute.cs | 2 +- Torch.API/Torch.API.csproj | 1 + Torch.Client/TorchClient.cs | 13 +- Torch.Client/TorchSettingsScreen.cs | 11 +- Torch.Server/Program.cs | 42 ++--- Torch.Server/Torch.Server.csproj | 8 + Torch.Server/TorchServer.cs | 11 +- Torch.Server/TorchService.cs | 54 ++++++ Torch.Server/TorchServiceInstaller.cs | 61 +++++++ Torch.Server/Views/TorchUI.xaml.cs | 2 +- Torch.sln | 20 +++ Torch/Commands/CategoryAttribute.cs | 22 +++ Torch/Commands/ChatCommandAttribute.cs | 13 -- Torch/Commands/{ChatCommand.cs => Command.cs} | 5 +- Torch/Commands/CommandAttribute.cs | 18 ++ Torch/Commands/CommandContext.cs | 12 +- ...{ChatCommandModule.cs => CommandModule.cs} | 2 +- Torch/Commands/CommandSystem.cs | 12 +- Torch/Commands/CommandTree.cs | 99 +++++++++++ Torch/Logger.cs | 53 +++++- Torch/MultiplayerManager.cs | 14 +- Torch/PluginManager.cs | 156 +++++------------- Torch/SteamHelper.cs | 17 +- Torch/Torch.csproj | 10 +- Torch/TorchBase.cs | 96 +++++++---- Torch/TorchPluginBase.cs | 106 ++++++++++++ 33 files changed, 780 insertions(+), 249 deletions(-) create mode 100644 TestPlugin/Class1.cs create mode 100644 TestPlugin/Properties/AssemblyInfo.cs create mode 100644 TestPlugin/TestPlugin.csproj rename Torch/TorchPlugin.cs => Torch.API/ILogger.cs (51%) create mode 100644 Torch.Server/TorchService.cs create mode 100644 Torch.Server/TorchServiceInstaller.cs create mode 100644 Torch/Commands/CategoryAttribute.cs delete mode 100644 Torch/Commands/ChatCommandAttribute.cs rename Torch/Commands/{ChatCommand.cs => Command.cs} (58%) create mode 100644 Torch/Commands/CommandAttribute.cs rename Torch/Commands/{ChatCommandModule.cs => CommandModule.cs} (81%) create mode 100644 Torch/Commands/CommandTree.cs create mode 100644 Torch/TorchPluginBase.cs diff --git a/TestPlugin/Class1.cs b/TestPlugin/Class1.cs new file mode 100644 index 0000000..3782fac --- /dev/null +++ b/TestPlugin/Class1.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Torch; +using Torch.API; +using VRage.Plugins; + +namespace TestPlugin +{ + public class Plugin : TorchPluginBase + { + /// + public override void Init(ITorchBase torch) + { + base.Init(torch); + Torch.Log.Write($"Plugin init {Name}"); + } + + /// + public override void Update() + { + Torch.Log.Write($"Plugin update {Name}"); + } + + /// + public override void Unload() + { + Torch.Log.Write($"Plugin unload {Name}"); + } + } +} diff --git a/TestPlugin/Properties/AssemblyInfo.cs b/TestPlugin/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..95ea997 --- /dev/null +++ b/TestPlugin/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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("TestPlugin")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestPlugin")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[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("abd18a6c-f638-44e9-8e55-dedea321c600")] + +// 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")] diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj new file mode 100644 index 0000000..944eaf4 --- /dev/null +++ b/TestPlugin/TestPlugin.csproj @@ -0,0 +1,61 @@ + + + + + Debug + AnyCPU + {ABD18A6C-F638-44E9-8E55-DEDEA321C600} + Library + Properties + TestPlugin + TestPlugin + v4.6.1 + 512 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + + + + + + + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.dll + + + + + + + + + {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2} + Torch.API + + + {7e01635c-3b67-472e-bcd6-c5539564f214} + Torch + + + + \ No newline at end of file diff --git a/Torch/TorchPlugin.cs b/Torch.API/ILogger.cs similarity index 51% rename from Torch/TorchPlugin.cs rename to Torch.API/ILogger.cs index 0138c1a..ca2e01d 100644 --- a/Torch/TorchPlugin.cs +++ b/Torch.API/ILogger.cs @@ -4,10 +4,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Torch +namespace Torch.API { - public class TorchPlugin + public interface ILogger { - + void Write(string message); + void WriteException(Exception e); } } diff --git a/Torch.API/IPluginManager.cs b/Torch.API/IPluginManager.cs index 142976f..32e30e9 100644 --- a/Torch.API/IPluginManager.cs +++ b/Torch.API/IPluginManager.cs @@ -1,21 +1,12 @@ using System; +using System.Collections.Generic; using VRage.Collections; using VRage.Plugins; namespace Torch.API { - public interface IPluginManager + public interface IPluginManager : IEnumerable { - ListReader Plugins { get; } - string[] GetPluginFolders(); - string GetPluginName(Type pluginType); - void LoadAllPlugins(); - void LoadPlugin(IPlugin plugin); - void LoadPluginFolder(string folderName); - void ReloadAll(); - void ReloadPlugin(IPlugin plugin, bool forceNonPiston = false); - bool UnblockDll(string fileName); - void UnloadPlugin(IPlugin plugin); } } \ No newline at end of file diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs index 140bd51..dc56bbe 100644 --- a/Torch.API/ITorchBase.cs +++ b/Torch.API/ITorchBase.cs @@ -11,8 +11,10 @@ namespace Torch.API event Action SessionLoaded; IMultiplayer Multiplayer { get; } IPluginManager Plugins { get; } - void DoGameAction(Action action); - Task DoGameActionAsync(Action action); + ILogger Log { get; set; } + void Invoke(Action action); + void InvokeBlocking(Action action); + Task InvokeAsync(Action action); string[] RunArgs { get; set; } void Start(); void Stop(); diff --git a/Torch.API/ITorchPlugin.cs b/Torch.API/ITorchPlugin.cs index 55d7af2..21d7026 100644 --- a/Torch.API/ITorchPlugin.cs +++ b/Torch.API/ITorchPlugin.cs @@ -4,13 +4,18 @@ using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using VRage.Plugins; namespace Torch.API { - public interface ITorchPlugin : IPlugin + public interface ITorchPlugin { - void Init(ITorchBase torch); - void Reload(); + Guid Id { get; } + Version Version { get; } + string Name { get; } + bool Enabled { get; set; } + + void Init(ITorchBase torchBase); + void Update(); + void Unload(); } } diff --git a/Torch.API/PluginAttribute.cs b/Torch.API/PluginAttribute.cs index e172736..d5b3984 100644 --- a/Torch.API/PluginAttribute.cs +++ b/Torch.API/PluginAttribute.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PistonAPI +namespace Torch.API { public class PluginAttribute : Attribute { diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index 840aed1..58ff30e 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -53,6 +53,7 @@ + diff --git a/Torch.Client/TorchClient.cs b/Torch.Client/TorchClient.cs index e0a331b..adb6a03 100644 --- a/Torch.Client/TorchClient.cs +++ b/Torch.Client/TorchClient.cs @@ -15,7 +15,7 @@ using VRageRender; namespace Torch.Client { - class TorchClient : TorchBase, ITorchClient + public class TorchClient : TorchBase, ITorchClient { private MyCommonProgramStartup _startup; private IMyRender _renderer; @@ -24,6 +24,8 @@ namespace Torch.Client public override void Init() { + base.Init(); + if (!File.Exists("steam_appid.txt")) { Directory.SetCurrentDirectory(Path.GetDirectoryName(typeof(VRage.FastResourceLock).Assembly.Location) + "\\.."); @@ -83,11 +85,18 @@ namespace Torch.Client { using (var spaceEngineersGame = new SpaceEngineersGame(_services, RunArgs)) { - Logger.Write("Starting client..."); + Log.Write("Starting client..."); + spaceEngineersGame.OnGameLoaded += SpaceEngineersGame_OnGameLoaded; spaceEngineersGame.Run(); } } + private void SpaceEngineersGame_OnGameLoaded(object sender, EventArgs e) + { + Log.Write("Loading plugins"); + Plugins.LoadAllPlugins(); + } + public override void Stop() { MySandboxGame.ExitThreadSafe(); diff --git a/Torch.Client/TorchSettingsScreen.cs b/Torch.Client/TorchSettingsScreen.cs index e2346ed..1a58081 100644 --- a/Torch.Client/TorchSettingsScreen.cs +++ b/Torch.Client/TorchSettingsScreen.cs @@ -23,9 +23,16 @@ namespace Torch.Client { base.RecreateControls(constructor); AddCaption(MyStringId.GetOrCompute("Torch Settings"), null, new Vector2(0, 0), 1.2f); - var pluginList = new MyGuiControlListbox {VisibleRowsCount = 20}; - foreach (var name in TorchBase.Instance.Plugins.GetPluginFolders()) + var pluginList = new MyGuiControlListbox + { + VisibleRowsCount = 10, + }; + + foreach (var plugin in TorchBase.Instance.Plugins.Plugins) + { + var name = TorchBase.Instance.Plugins.GetPluginName(plugin.GetType()); pluginList.Items.Add(new MyGuiControlListbox.Item(new StringBuilder(name))); + } Controls.Add(pluginList); } } diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index f07646b..b98f54b 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.ServiceProcess; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -15,38 +16,23 @@ namespace Torch.Server { public static class Program { - private static readonly ITorchServer _server = new TorchServer(); - private static TorchUI _ui; + private static ITorchServer _server; - [STAThread] public static void Main(string[] args) { - _server.Init(); - _server.RunArgs = new[] { "-console" }; - _ui = new TorchUI(_server); - - if (args.Contains("-nogui")) - _server.Start(); + if (!Environment.UserInteractive) + { + using (var service = new TorchService()) + { + ServiceBase.Run(service); + } + } else - StartUI(); - - if (args.Contains("-autostart") && !_server.IsRunning) - new Thread(() => _server.Start()).Start(); - - Dispatcher.Run(); - } - - public static void StartUI() - { - Thread.CurrentThread.Name = "UI Thread"; - _ui.Show(); - } - - public static void FullRestart() - { - _server.Stop(); - Process.Start("TorchServer.exe", "-autostart"); - Environment.Exit(1); + { + _server = new TorchServer(); + _server.Init(); + _server.Start(); + } } } } diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index 694f13f..d11058d 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -67,8 +67,10 @@ C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\SteamSDK.dll + + @@ -104,6 +106,12 @@ + + Component + + + Component + AddWorkshopItemsDialog.xaml diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index 08c4a4b..027f6d5 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -33,11 +33,12 @@ namespace Torch.Server internal TorchServer() { MySession.OnLoading += OnSessionLoading; - Plugins = new PluginManager(); } public override void Init() { + base.Init(); + SpaceEngineersGame.SetupBasicGameInfo(); SpaceEngineersGame.SetupPerGameSettings(); MyPerGameSettings.SendLogToKeen = false; @@ -78,7 +79,7 @@ namespace Torch.Server throw new InvalidOperationException("Server is already running."); IsRunning = true; - Logger.Write("Starting server."); + Log.Write("Starting server."); if (MySandboxGame.Log.LogEnabled) MySandboxGame.Log.Close(); @@ -93,13 +94,13 @@ namespace Torch.Server { if (Thread.CurrentThread.ManagedThreadId != ServerThread?.ManagedThreadId) { - Logger.Write("Requesting server stop."); + Log.Write("Requesting server stop."); MySandboxGame.Static.Invoke(Stop); _stopHandle.WaitOne(); return; } - Logger.Write("Stopping server."); + Log.Write("Stopping server."); MySession.Static.Save(); MySession.Static.Unload(); MySandboxGame.Static.Exit(); @@ -111,7 +112,7 @@ namespace Torch.Server VRage.Input.MyInput.UnloadData(); CleanupProfilers(); - Logger.Write("Server stopped."); + Log.Write("Server stopped."); _stopHandle.Set(); IsRunning = false; } diff --git a/Torch.Server/TorchService.cs b/Torch.Server/TorchService.cs new file mode 100644 index 0000000..18ca189 --- /dev/null +++ b/Torch.Server/TorchService.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.ServiceProcess; +using Torch.API; + +namespace Torch.Server +{ + class TorchService : ServiceBase + { + public const string Name = "Torch (SEDS)"; + private readonly ITorchServer _server = new TorchServer(); + + public TorchService() + { + ServiceName = Name; + EventLog.Log = "Application"; + + CanHandlePowerEvent = true; + CanHandleSessionChangeEvent = false; + CanPauseAndContinue = false; + CanStop = true; + } + + /// + protected override void OnStart(string[] args) + { + base.OnStart(args); + _server.Init(); + _server.Start(); + } + + /// + protected override void OnStop() + { + _server.Stop(); + base.OnStop(); + } + + /// + protected override void OnShutdown() + { + base.OnShutdown(); + } + + /// + protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus) + { + return base.OnPowerEvent(powerStatus); + } + } +} diff --git a/Torch.Server/TorchServiceInstaller.cs b/Torch.Server/TorchServiceInstaller.cs new file mode 100644 index 0000000..40ad73c --- /dev/null +++ b/Torch.Server/TorchServiceInstaller.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; +using System.ServiceProcess; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.Server +{ + [RunInstaller(true)] + class TorchServiceInstaller : Installer + { + private ServiceInstaller _serviceInstaller; + + public TorchServiceInstaller() + { + var serviceProcessInstaller = new ServiceProcessInstaller(); + _serviceInstaller = new ServiceInstaller(); + + serviceProcessInstaller.Account = ServiceAccount.LocalSystem; + serviceProcessInstaller.Username = null; + serviceProcessInstaller.Password = null; + + _serviceInstaller.DisplayName = "Torch (SEDS)"; + _serviceInstaller.Description = "Service for Torch (SE Dedicated Server)"; + _serviceInstaller.StartType = ServiceStartMode.Manual; + + _serviceInstaller.ServiceName = TorchService.Name; + + Installers.Add(serviceProcessInstaller); + Installers.Add(_serviceInstaller); + } + + /// + public override void Install(IDictionary stateSaver) + { + GetServiceName(); + base.Install(stateSaver); + } + + /// + public override void Uninstall(IDictionary savedState) + { + GetServiceName(); + base.Uninstall(savedState); + } + + private void GetServiceName() + { + var name = Context.Parameters["name"]; + if (string.IsNullOrEmpty(name)) + return; + + _serviceInstaller.DisplayName = name; + _serviceInstaller.ServiceName = name; + } + } +} diff --git a/Torch.Server/Views/TorchUI.xaml.cs b/Torch.Server/Views/TorchUI.xaml.cs index 20aa003..e70e3ef 100644 --- a/Torch.Server/Views/TorchUI.xaml.cs +++ b/Torch.Server/Views/TorchUI.xaml.cs @@ -88,7 +88,7 @@ namespace Torch.Server private void BtnRestart_Click(object sender, RoutedEventArgs e) { - Program.FullRestart(); + //Program.FullRestart(); } } } diff --git a/Torch.sln b/Torch.sln index b060048..0fa7473 100644 --- a/Torch.sln +++ b/Torch.sln @@ -13,32 +13,52 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Server", "Torch.Serve EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Launcher", "Torch.Launcher\Torch.Launcher.csproj", "{19292801-5B9C-4EE0-961F-0FA37B3A6C3D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "TestPlugin\TestPlugin.csproj", "{ABD18A6C-F638-44E9-8E55-DEDEA321C600}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|Any CPU.ActiveCfg = Debug|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|x64.ActiveCfg = Debug|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Debug|x64.Build.0 = Debug|x64 + {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|Any CPU.ActiveCfg = Release|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|x64.ActiveCfg = Release|x64 {7E01635C-3B67-472E-BCD6-C5539564F214}.Release|x64.Build.0 = Release|x64 + {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|Any CPU.ActiveCfg = Debug|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|x64.ActiveCfg = Debug|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Debug|x64.Build.0 = Debug|x64 + {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|Any CPU.ActiveCfg = Release|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.ActiveCfg = Release|x64 {FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = Release|x64 + {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|Any CPU.ActiveCfg = Debug|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.ActiveCfg = Debug|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = Debug|x64 + {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|Any CPU.ActiveCfg = Release|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.ActiveCfg = Release|x64 {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = Release|x64 + {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|Any CPU.ActiveCfg = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64 + {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|Any CPU.ActiveCfg = Release|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.ActiveCfg = Release|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.Build.0 = Release|x64 + {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Debug|Any CPU.ActiveCfg = Debug|x64 {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Debug|x64.ActiveCfg = Debug|x64 {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Debug|x64.Build.0 = Debug|x64 + {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Release|Any CPU.ActiveCfg = Release|x64 {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Release|x64.ActiveCfg = Release|x64 {19292801-5B9C-4EE0-961F-0FA37B3A6C3D}.Release|x64.Build.0 = Release|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Debug|Any CPU.ActiveCfg = Debug|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Debug|x64.ActiveCfg = Debug|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Debug|x64.Build.0 = Debug|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Release|Any CPU.ActiveCfg = Release|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Release|x64.ActiveCfg = Release|x64 + {ABD18A6C-F638-44E9-8E55-DEDEA321C600}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Torch/Commands/CategoryAttribute.cs b/Torch/Commands/CategoryAttribute.cs new file mode 100644 index 0000000..eea96f6 --- /dev/null +++ b/Torch/Commands/CategoryAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.Commands +{ + public class CategoryAttribute : Attribute + { + public string[] Path { get; } + + /// + /// Specifies where to add the class's commands in the command tree. + /// + /// Command path, e.g. "/admin config" -> "admin, config" + public CategoryAttribute(params string[] path) + { + Path = path; + } + } +} diff --git a/Torch/Commands/ChatCommandAttribute.cs b/Torch/Commands/ChatCommandAttribute.cs deleted file mode 100644 index b10f5eb..0000000 --- a/Torch/Commands/ChatCommandAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Torch.Commands -{ - public class ChatCommandAttribute : Attribute - { - public string Name { get; } - public ChatCommandAttribute(string name) - { - Name = name; - } - } -} \ No newline at end of file diff --git a/Torch/Commands/ChatCommand.cs b/Torch/Commands/Command.cs similarity index 58% rename from Torch/Commands/ChatCommand.cs rename to Torch/Commands/Command.cs index 81b16c3..7f196fc 100644 --- a/Torch/Commands/ChatCommand.cs +++ b/Torch/Commands/Command.cs @@ -2,10 +2,11 @@ namespace Torch.Commands { - public class ChatCommand + public class Command { - public ChatCommandModule Module; + public CommandModule Module; public string Name; + public string[] Path; public Action Invoke; } } \ No newline at end of file diff --git a/Torch/Commands/CommandAttribute.cs b/Torch/Commands/CommandAttribute.cs new file mode 100644 index 0000000..5c982cd --- /dev/null +++ b/Torch/Commands/CommandAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace Torch.Commands +{ + public class CommandAttribute : Attribute + { + public string Name { get; } + + /// + /// Specifies a command to add to the command tree. + /// + /// + public CommandAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Torch/Commands/CommandContext.cs b/Torch/Commands/CommandContext.cs index f711afd..23ae0cb 100644 --- a/Torch/Commands/CommandContext.cs +++ b/Torch/Commands/CommandContext.cs @@ -5,22 +5,26 @@ namespace Torch.Commands { public struct CommandContext { - public string Argument; + public string[] Args; public ulong SteamId; /// /// Splits the argument by single words and quoted blocks. /// /// - public string[] SplitArgument() + public CommandContext(ulong steamId, params string[] args) { - var split = Regex.Split(Argument, "(\"[^\"]+\"|\\S+)"); + SteamId = steamId; + Args = args; + + /* + var split = Regex.Split(Args, "(\"[^\"]+\"|\\S+)"); for (var i = 0; i < split.Length; i++) { split[i] = Regex.Replace(split[i], "\"", ""); } - return split; + return split;*/ } } } \ No newline at end of file diff --git a/Torch/Commands/ChatCommandModule.cs b/Torch/Commands/CommandModule.cs similarity index 81% rename from Torch/Commands/ChatCommandModule.cs rename to Torch/Commands/CommandModule.cs index 53f9b94..2af21ac 100644 --- a/Torch/Commands/ChatCommandModule.cs +++ b/Torch/Commands/CommandModule.cs @@ -2,7 +2,7 @@ namespace Torch.Commands { - public class ChatCommandModule + public class CommandModule { public ITorchPlugin Plugin { get; set; } public ITorchBase Server { get; set; } diff --git a/Torch/Commands/CommandSystem.cs b/Torch/Commands/CommandSystem.cs index 5891642..25dead6 100644 --- a/Torch/Commands/CommandSystem.cs +++ b/Torch/Commands/CommandSystem.cs @@ -10,7 +10,7 @@ namespace Torch.Commands public ITorchBase Server { get; } public char Prefix { get; set; } - public Dictionary Commands { get; } = new Dictionary(); + public Dictionary Commands { get; } = new Dictionary(); public CommandSystem(ITorchBase server, char prefix = '/') { @@ -28,15 +28,15 @@ namespace Torch.Commands var assembly = plugin.GetType().Assembly; foreach (var type in assembly.ExportedTypes) { - if (!type.IsSubclassOf(typeof(ChatCommandModule))) + if (!type.IsSubclassOf(typeof(CommandModule))) continue; - var module = (ChatCommandModule)Activator.CreateInstance(type); + var module = (CommandModule)Activator.CreateInstance(type); module.Server = Server; module.Plugin = plugin; foreach (var method in type.GetMethods()) { - var commandAttrib = method.GetCustomAttribute(); + var commandAttrib = method.GetCustomAttribute(); if (commandAttrib == null) continue; @@ -54,7 +54,7 @@ namespace Torch.Commands continue; } - var command = new ChatCommand + var command = new Command { Module = module, Name = commandAttrib.Name, @@ -82,7 +82,7 @@ namespace Torch.Commands var context = new CommandContext { - Argument = arg, + Args = arg, SteamId = steamId }; diff --git a/Torch/Commands/CommandTree.cs b/Torch/Commands/CommandTree.cs new file mode 100644 index 0000000..c7f72ee --- /dev/null +++ b/Torch/Commands/CommandTree.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VRage.Library.Collections; + +namespace Torch.Commands +{ + public class CommandTree + { + private Dictionary RootNodes { get; } = new Dictionary(); + + public bool AddCommand(Command command) + { + var root = command.Path.First(); + var node = RootNodes.ContainsKey(root) ? RootNodes[root] : new Node(root); + + for (var i = 1; i < command.Path.Length; i++) + { + var current = command.Path[i]; + if (node.Nodes.ContainsKey(current)) + { + node = node.Nodes[current]; + continue; + } + + var newNode = new Node(current); + node.AddNode(newNode); + node = newNode; + } + + if (!node.IsEmpty) + return false; + + node.Command = command; + return true; + } + + public InvokeResult Invoke(ulong steamId, string[] command) + { + var root = command.First(); + if (!RootNodes.ContainsKey(root)) + return InvokeResult.NoCommand; + + var node = RootNodes[root]; + var args = new string[0]; + for (var i = 1; i < command.Length; i++) + { + var current = command[i]; + if (node.Nodes.ContainsKey(current)) + { + node = node.Nodes[current]; + continue; + } + + args = new string[command.Length - i]; + Array.Copy(command, i, args, 0, args.Length); + } + + if (!node.IsCommand) + return InvokeResult.NoCommand; + + //check permission here + + var context = new CommandContext(steamId, args); + node.Command.Invoke(context); + return InvokeResult.Success; + } + + private class Node + { + public Dictionary Nodes { get; } = new Dictionary(); + + public string Name { get; } + public Command Command { get; set; } + public bool IsCommand => Command != null; + public bool IsEmpty => !IsCommand && Nodes.Count == 0; + + public Node(string name, Command command = null) + { + Name = name; + Command = command; + } + + public void AddNode(Node node) + { + Nodes.Add(node.Name, node); + } + } + + public enum InvokeResult + { + Success, + NoCommand, + NoPermission + } + } +} diff --git a/Torch/Logger.cs b/Torch/Logger.cs index 292695a..6b435c0 100644 --- a/Torch/Logger.cs +++ b/Torch/Logger.cs @@ -1,21 +1,64 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Sandbox; +using Torch.API; using VRage.Utils; namespace Torch { - public static class Logger + public class Logger : ILogger { - public const string Prefix = "[TORCH]"; + public string Prefix = "[TORCH]"; + private StringBuilder _sb = new StringBuilder(); + private string _path; - public static void Write(string message) + public Logger(string path) { - var msg = $"{Prefix}: {message}"; - MyLog.Default.WriteLineAndConsole(msg); + _path = path; + if (File.Exists(_path)) + File.Delete(_path); + } + + public void Write(string message) + { + var msg = $"{GetInfo()}: {message}"; + Console.WriteLine(msg); + _sb.AppendLine(msg); + } + + public void WriteExceptionAndThrow(Exception e) + { + WriteException(e); + throw e; + } + + public void WriteException(Exception e) + { + _sb.AppendLine($"{GetInfo()}: {e.Message}"); + + foreach (var line in e.StackTrace.Split('\n')) + _sb.AppendLine($"\t{line}"); + } + + private string GetInfo() + { + return $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()} {Prefix}"; + } + + public void Flush() + { + File.AppendAllText(_path, _sb.ToString()); + _sb.Clear(); + } + + ~Logger() + { + Flush(); } } } diff --git a/Torch/MultiplayerManager.cs b/Torch/MultiplayerManager.cs index f7517b8..7d345f1 100644 --- a/Torch/MultiplayerManager.cs +++ b/Torch/MultiplayerManager.cs @@ -50,11 +50,11 @@ namespace Torch _torch.SessionLoaded += OnSessionLoaded; } - public void KickPlayer(ulong steamId) => _torch.DoGameActionAsync(() => MyMultiplayer.Static.KickClient(steamId)); + public void KickPlayer(ulong steamId) => _torch.InvokeAsync(() => MyMultiplayer.Static.KickClient(steamId)); public void BanPlayer(ulong steamId, bool banned = true) { - _torch.DoGameActionAsync(() => + _torch.InvokeAsync(() => { MyMultiplayer.Static.BanClient(steamId, banned); if (_gameOwnerIds.ContainsKey(steamId)) @@ -118,7 +118,7 @@ namespace Torch player.SetConnectionState(ConnectionState.Connected); } - Logger.Write($"{player.Name} connected."); + _torch.Log.Write($"{player.Name} connected."); PlayerJoined?.Invoke(player); } @@ -133,7 +133,7 @@ namespace Torch return; var player = Players[steamId]; - Logger.Write($"{player.Name} disconnected ({(ConnectionState)stateChange})."); + _torch.Log.Write($"{player.Name} disconnected ({(ConnectionState)stateChange})."); player.SetConnectionState((ConnectionState)stateChange); PlayerLeft?.Invoke(player); } @@ -179,16 +179,16 @@ namespace Torch //Largely copied from SE private void ValidateAuthTicketResponse(ulong steamID, AuthSessionResponseEnum response, ulong ownerSteamID) { - Logger.Write($"Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}"); + _torch.Log.Write($"Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}"); if (steamID != ownerSteamID) { - Logger.Write($"User {steamID} is using a game owned by {ownerSteamID}. Tracking..."); + _torch.Log.Write($"User {steamID} is using a game owned by {ownerSteamID}. Tracking..."); _gameOwnerIds[steamID] = ownerSteamID; if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID)) { - Logger.Write($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}..."); + _torch.Log.Write($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}..."); UserRejected(steamID, JoinResult.BannedByAdmins); BanPlayer(steamID, true); } diff --git a/Torch/PluginManager.cs b/Torch/PluginManager.cs index 0834f2c..a55decb 100644 --- a/Torch/PluginManager.cs +++ b/Torch/PluginManager.cs @@ -1,12 +1,14 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; -using PistonAPI; using Sandbox; using Torch.API; using VRage.Plugins; @@ -17,142 +19,60 @@ namespace Torch { public class PluginManager : IPluginManager { - //TODO: Disable reloading if the plugin has static elements because they prevent a full reload. - - public ListReader Plugins => MyPlugins.Plugins; - - private List _plugins; + private readonly ITorchBase _torch; public const string PluginDir = "Plugins"; - public PluginManager() + private readonly List _plugins = new List(); + + public PluginManager(ITorchBase torch) { + _torch = torch; + if (!Directory.Exists(PluginDir)) Directory.CreateDirectory(PluginDir); - - GetPluginList(); - } - - /// - /// Get a reference to the internal VRage plugin list. - /// - private void GetPluginList() - { - _plugins = typeof(MyPlugins).GetField("m_plugins", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as List; - } - - /// - /// Get a plugin's name from its or its type name. - /// - public string GetPluginName(Type pluginType) - { - var attr = pluginType.GetCustomAttribute(); - return attr?.Name ?? pluginType.Name; } /// /// Load all plugins in the folder. /// - public void LoadAllPlugins() + public void LoadPlugins() { - var pluginFolders = GetPluginFolders(); - foreach (var folder in pluginFolders) + var pluginsPath = Path.Combine(Directory.GetCurrentDirectory(), PluginDir); + var dlls = Directory.GetFiles(pluginsPath, "*.dll", SearchOption.AllDirectories); + foreach (var dllPath in dlls) { - LoadPluginFolder(folder); - } - } + UnblockDll(dllPath); + var asm = Assembly.LoadFrom(dllPath); - /// - /// Load a plugin into the game. - /// - /// - public void LoadPlugin(IPlugin plugin) - { - Logger.Write($"Loading plugin: {GetPluginName(plugin.GetType())}"); - plugin.Init(MySandboxGame.Static); - _plugins.Add(plugin); - } - - /// - /// Get the names of all the subfolders in the Plugins directory. - /// - /// - public string[] GetPluginFolders() - { - var dirs = Directory.GetDirectories(PluginDir); - for (var i = 0; i < dirs.Length; i++) - { - dirs[i] = dirs[i].Substring(PluginDir.Length + 1); - } - - return dirs; - } - - /// - /// Load all plugins in the specified folder. - /// - /// Folder in the directory - public void LoadPluginFolder(string folderName) - { - var relativeDir = Path.Combine(PluginDir, folderName); - if (!Directory.Exists(relativeDir)) - { - Logger.Write($"Plugin {folderName} does not exist in the Plugins folder."); - return; - } - - var fileNames = Directory.GetFiles(relativeDir, "*.dll"); - - foreach (var fileName in fileNames) - { - var fullPath = Path.Combine(Directory.GetCurrentDirectory(), fileName); - UnblockDll(fullPath); - var asm = Assembly.LoadFrom(fullPath); - - foreach (var type in asm.GetTypes()) + foreach (var type in asm.GetExportedTypes()) { - if (type.GetInterfaces().Contains(typeof(IPlugin))) - { - var inst = (IPlugin)Activator.CreateInstance(type); - MySandboxGame.Static.Invoke(() => LoadPlugin(inst)); - } + if (type.IsSubclassOf(typeof(TorchPluginBase))) + _plugins.Add((TorchPluginBase)Activator.CreateInstance(type)); } } } - /// - /// Unload a plugin from the game. - /// - public void UnloadPlugin(IPlugin plugin) + public async void ReloadPluginAsync(ITorchPlugin plugin) { - _plugins.Remove(plugin); - plugin.Dispose(); + var p = plugin as TorchPluginBase; + if (p == null) + return; + + var newPlugin = (TorchPluginBase)Activator.CreateInstance(p.GetType()); + _plugins.Add(newPlugin); + + await p.StopAsync(); + _plugins.Remove(p); + + newPlugin.Run(_torch, true); } - /// - /// Reload a plugin. - /// - /// - /// Reload a non-Piston plugin - public void ReloadPlugin(IPlugin plugin, bool forceNonPiston = false) - { - var p = plugin as ITorchPlugin; - if (p == null && forceNonPiston) - { - plugin.Dispose(); - plugin.Init(MySandboxGame.Static); - } - else - { - p?.Reload(); - } - } - - public void ReloadAll() + public void StartEnabledPlugins() { foreach (var plugin in _plugins) { - var p = plugin as ITorchPlugin; - p?.Reload(); + if (plugin.Enabled) + plugin.Run(_torch); } } @@ -161,6 +81,16 @@ namespace Torch return DeleteFile(fileName + ":Zone.Identifier"); } + public IEnumerator GetEnumerator() + { + return _plugins.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool DeleteFile(string name); diff --git a/Torch/SteamHelper.cs b/Torch/SteamHelper.cs index 13496d0..ab78053 100644 --- a/Torch/SteamHelper.cs +++ b/Torch/SteamHelper.cs @@ -8,6 +8,7 @@ using Sandbox; using Sandbox.Engine.Networking; using Sandbox.Engine.Platform; using SteamSDK; +using Torch.API; using VRage.Game; namespace Torch @@ -15,9 +16,12 @@ namespace Torch public static class SteamHelper { private static Thread _callbackThread; + private static ILogger _log; - public static void Init() + public static void Init(ILogger log) { + _log = log; + _callbackThread = new Thread(() => { while (true) @@ -52,7 +56,7 @@ namespace Torch } else { - Logger.Write($"Failed to get item info for {itemId}"); + _log.Write($"Failed to get item info for {itemId}"); } mre.Set(); @@ -68,20 +72,19 @@ namespace Torch public static SteamUGCDetails GetItemDetails(ulong itemId) { SteamUGCDetails details = default(SteamUGCDetails); - using (var mre = new ManualResetEvent(false)) + using (var re = new AutoResetEvent(false)) { SteamAPI.Instance.UGC.RequestUGCDetails(itemId, 0, (b, result) => { if (!b && result.Details.Result == Result.OK) details = result.Details; else - Logger.Write($"Failed to get item details for {itemId}"); + _log.Write($"Failed to get item details for {itemId}"); - mre.Set(); + re.Set(); }); - mre.WaitOne(); - mre.Reset(); + re.WaitOne(); } return details; diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 9b6085b..25dab89 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -121,18 +121,20 @@ - - - + + + + + - + diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs index b0cce23..b98f102 100644 --- a/Torch/TorchBase.cs +++ b/Torch/TorchBase.cs @@ -1,25 +1,32 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Sandbox; using Torch.API; +using VRage.Scripting; namespace Torch { public abstract class TorchBase : ITorchBase { + /// + /// Dirty hack because *keen* + /// Use only if absolutely necessary. + /// public static ITorchBase Instance { get; private set; } - public string[] RunArgs { get; set; } public IPluginManager Plugins { get; protected set; } public IMultiplayer Multiplayer { get; protected set; } - + public ILogger Log { get; set; } public event Action SessionLoaded; + private bool _init; + protected void InvokeSessionLoaded() { SessionLoaded?.Invoke(); @@ -27,13 +34,22 @@ namespace Torch protected TorchBase() { - RunArgs = new string[0]; + if (Instance != null) + throw new InvalidOperationException("A TorchBase instance already exists."); + Instance = this; - Plugins = new PluginManager(); + + Log = new Logger(Path.Combine(Directory.GetCurrentDirectory(), "TorchLog.log")); + RunArgs = new string[0]; + Plugins = new PluginManager(this); Multiplayer = new MultiplayerManager(this); } - public void DoGameAction(Action action) + /// + /// Invokes an action on the game thread. + /// + /// + public void Invoke(Action action) { MySandboxGame.Static.Invoke(action); } @@ -42,43 +58,67 @@ namespace Torch /// Invokes an action on the game thread asynchronously. /// /// - public Task DoGameActionAsync(Action action) + public Task InvokeAsync(Action action) { if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread) { - Debug.Assert(false, $"{nameof(DoGameActionAsync)} should not be called on the game thread."); + Debug.Assert(false, $"{nameof(InvokeAsync)} should not be called on the game thread."); action?.Invoke(); return Task.CompletedTask; } - return Task.Run(() => + return Task.Run(() => InvokeBlocking(action)); + } + + /// + /// Invokes an action on the game thread and blocks until it is completed. + /// + /// + public void InvokeBlocking(Action action) + { + if (action == null) + return; + + if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread) { - var e = new AutoResetEvent(false); + Debug.Assert(false, $"{nameof(InvokeBlocking)} should not be called on the game thread."); + action.Invoke(); + return; + } - MySandboxGame.Static.Invoke(() => + var e = new AutoResetEvent(false); + + MySandboxGame.Static.Invoke(() => + { + try { - try - { - action?.Invoke(); - } - catch (Exception ex) - { - //log - } - finally - { - e.Set(); - } - }); - - if(!e.WaitOne(60000)) - throw new TimeoutException("The game action timed out."); - + action.Invoke(); + } + catch (Exception ex) + { + //log + } + finally + { + e.Set(); + } }); + + if (!e.WaitOne(60000)) + throw new TimeoutException("The game action timed out."); + } + + public virtual void Init() + { + Debug.Assert(!_init, "Torch instance is already initialized."); + + _init = true; + MyScriptCompiler.Static.AddConditionalCompilationSymbols("TORCH"); + MyScriptCompiler.Static.AddReferencedAssemblies(typeof(ITorchBase).Assembly.Location); + MyScriptCompiler.Static.AddReferencedAssemblies(typeof(TorchBase).Assembly.Location); } public abstract void Start(); public abstract void Stop(); - public abstract void Init(); } } diff --git a/Torch/TorchPluginBase.cs b/Torch/TorchPluginBase.cs new file mode 100644 index 0000000..97540fa --- /dev/null +++ b/Torch/TorchPluginBase.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Torch.API; + +namespace Torch +{ + public abstract class TorchPluginBase : ITorchPlugin + { + public Guid Id { get; } + public Version Version { get; } + public string Name { get; } + public bool Enabled { get; set; } = true; + public bool IsRunning => !Loop.IsCompleted; + public ITorchBase Torch { get; private set; } + + protected TorchPluginBase() + { + var asm = Assembly.GetCallingAssembly(); + + var id = asm.GetCustomAttribute()?.Value; + if (id == null) + throw new InvalidOperationException($"{asm.FullName} has no Guid attribute."); + + Id = new Guid(id); + + var ver = asm.GetCustomAttribute()?.Version; + if (ver == null) + throw new InvalidOperationException($"{asm.FullName} has no AssemblyVersion attribute."); + + Version = new Version(ver); + + var name = asm.GetCustomAttribute()?.Title; + if (name == null) + throw new InvalidOperationException($"{asm.FullName} has no AssemblyTitle attribute."); + + Name = name; + } + + public virtual void Init(ITorchBase torch) + { + Torch = torch; + } + + public abstract void Update(); + public abstract void Unload(); + + #region Internal Loop Code + + internal CancellationTokenSource ctSource = new CancellationTokenSource(); + + internal Task Loop { get; private set; } = Task.CompletedTask; + private readonly TimeSpan _loopInterval = TimeSpan.FromSeconds(1d / 60d); + private bool _runLoop; + internal Task Run(ITorchBase torch, bool enable = false) + { + if (IsRunning) + throw new InvalidOperationException($"Plugin {Name} is already running."); + + if (!Enabled) + return Loop = Task.CompletedTask; + + _runLoop = true; + return Loop = Task.Run(() => + { + try + { + Init(torch); + + while (Enabled && !ctSource.Token.IsCancellationRequested) + { + ctSource.Token.ThrowIfCancellationRequested(); + var ts = Stopwatch.GetTimestamp(); + Update(); + var time = TimeSpan.FromTicks(Stopwatch.GetTimestamp() - ts); + + if (time < _loopInterval) + Task.Delay(_loopInterval - time); + } + + Unload(); + } + catch (Exception e) + { + torch.Log.Write($"Plugin {Name} threw an exception."); + torch.Log.WriteException(e); + throw; + } + }); + } + + internal async Task StopAsync() + { + ctSource.Cancel(); + await Loop; + } + + #endregion + } +}