My New Year's resolution is to stop making commits like these

This commit is contained in:
John Gross
2017-01-01 16:37:30 -08:00
parent 81037b502a
commit 6e0922b805
33 changed files with 780 additions and 249 deletions

33
TestPlugin/Class1.cs Normal file
View File

@@ -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
{
/// <inheritdoc />
public override void Init(ITorchBase torch)
{
base.Init(torch);
Torch.Log.Write($"Plugin init {Name}");
}
/// <inheritdoc />
public override void Update()
{
Torch.Log.Write($"Plugin update {Name}");
}
/// <inheritdoc />
public override void Unload()
{
Torch.Log.Write($"Plugin unload {Name}");
}
}
}

View File

@@ -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")]

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{ABD18A6C-F638-44E9-8E55-DEDEA321C600}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>TestPlugin</RootNamespace>
<AssemblyName>TestPlugin</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="VRage">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\VRage.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}</Project>
<Name>Torch.API</Name>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -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);
}
}

View File

@@ -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<ITorchPlugin>
{
ListReader<IPlugin> 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);
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PistonAPI
namespace Torch.API
{
public class PluginAttribute : Attribute
{

View File

@@ -53,6 +53,7 @@
<ItemGroup>
<Compile Include="ConnectionState.cs" />
<Compile Include="IChatItem.cs" />
<Compile Include="ILogger.cs" />
<Compile Include="IMultiplayer.cs" />
<Compile Include="IPlayer.cs" />
<Compile Include="IPluginManager.cs" />

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -67,8 +67,10 @@
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\SteamSDK.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
@@ -104,6 +106,12 @@
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="TorchService.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="TorchServiceInstaller.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
<Compile Include="Views\AddWorkshopItemsDialog.xaml.cs">
<DependentUpon>AddWorkshopItemsDialog.xaml</DependentUpon>

View File

@@ -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;
}

View File

@@ -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;
}
/// <inheritdoc />
protected override void OnStart(string[] args)
{
base.OnStart(args);
_server.Init();
_server.Start();
}
/// <inheritdoc />
protected override void OnStop()
{
_server.Stop();
base.OnStop();
}
/// <inheritdoc />
protected override void OnShutdown()
{
base.OnShutdown();
}
/// <inheritdoc />
protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
{
return base.OnPowerEvent(powerStatus);
}
}
}

View File

@@ -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);
}
/// <inheritdoc />
public override void Install(IDictionary stateSaver)
{
GetServiceName();
base.Install(stateSaver);
}
/// <inheritdoc />
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;
}
}
}

View File

@@ -88,7 +88,7 @@ namespace Torch.Server
private void BtnRestart_Click(object sender, RoutedEventArgs e)
{
Program.FullRestart();
//Program.FullRestart();
}
}
}

View File

@@ -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

View File

@@ -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; }
/// <summary>
/// Specifies where to add the class's commands in the command tree.
/// </summary>
/// <param name="path">Command path, e.g. "/admin config" -> "admin, config"</param>
public CategoryAttribute(params string[] path)
{
Path = path;
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
namespace Torch.Commands
{
public class ChatCommandAttribute : Attribute
{
public string Name { get; }
public ChatCommandAttribute(string name)
{
Name = name;
}
}
}

View File

@@ -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<CommandContext> Invoke;
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace Torch.Commands
{
public class CommandAttribute : Attribute
{
public string Name { get; }
/// <summary>
/// Specifies a command to add to the command tree.
/// </summary>
/// <param name="name"></param>
public CommandAttribute(string name)
{
Name = name;
}
}
}

View File

@@ -5,22 +5,26 @@ namespace Torch.Commands
{
public struct CommandContext
{
public string Argument;
public string[] Args;
public ulong SteamId;
/// <summary>
/// Splits the argument by single words and quoted blocks.
/// </summary>
/// <returns></returns>
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;*/
}
}
}

View File

@@ -2,7 +2,7 @@
namespace Torch.Commands
{
public class ChatCommandModule
public class CommandModule
{
public ITorchPlugin Plugin { get; set; }
public ITorchBase Server { get; set; }

View File

@@ -10,7 +10,7 @@ namespace Torch.Commands
public ITorchBase Server { get; }
public char Prefix { get; set; }
public Dictionary<string, ChatCommand> Commands { get; } = new Dictionary<string, ChatCommand>();
public Dictionary<string, Command> Commands { get; } = new Dictionary<string, Command>();
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<ChatCommandAttribute>();
var commandAttrib = method.GetCustomAttribute<CommandAttribute>();
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
};

View File

@@ -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<string, Node> RootNodes { get; } = new Dictionary<string, Node>();
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<string, Node> Nodes { get; } = new Dictionary<string, Node>();
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
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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<IPlugin> Plugins => MyPlugins.Plugins;
private List<IPlugin> _plugins;
private readonly ITorchBase _torch;
public const string PluginDir = "Plugins";
public PluginManager()
private readonly List<TorchPluginBase> _plugins = new List<TorchPluginBase>();
public PluginManager(ITorchBase torch)
{
_torch = torch;
if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir);
GetPluginList();
}
/// <summary>
/// Get a reference to the internal VRage plugin list.
/// </summary>
private void GetPluginList()
{
_plugins = typeof(MyPlugins).GetField("m_plugins", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as List<IPlugin>;
}
/// <summary>
/// Get a plugin's name from its <see cref="PluginAttribute"/> or its type name.
/// </summary>
public string GetPluginName(Type pluginType)
{
var attr = pluginType.GetCustomAttribute<PluginAttribute>();
return attr?.Name ?? pluginType.Name;
}
/// <summary>
/// Load all plugins in the <see cref="PluginDir"/> folder.
/// </summary>
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);
foreach (var type in asm.GetExportedTypes())
{
if (type.IsSubclassOf(typeof(TorchPluginBase)))
_plugins.Add((TorchPluginBase)Activator.CreateInstance(type));
}
}
}
/// <summary>
/// Load a plugin into the game.
/// </summary>
/// <param name="plugin"></param>
public void LoadPlugin(IPlugin plugin)
public async void ReloadPluginAsync(ITorchPlugin plugin)
{
Logger.Write($"Loading plugin: {GetPluginName(plugin.GetType())}");
plugin.Init(MySandboxGame.Static);
_plugins.Add(plugin);
}
/// <summary>
/// Get the names of all the subfolders in the Plugins directory.
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Load all plugins in the specified folder.
/// </summary>
/// <param name="folderName">Folder in the <see cref="PluginDir"/> directory</param>
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.");
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);
}
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())
{
if (type.GetInterfaces().Contains(typeof(IPlugin)))
{
var inst = (IPlugin)Activator.CreateInstance(type);
MySandboxGame.Static.Invoke(() => LoadPlugin(inst));
}
}
}
}
/// <summary>
/// Unload a plugin from the game.
/// </summary>
public void UnloadPlugin(IPlugin plugin)
{
_plugins.Remove(plugin);
plugin.Dispose();
}
/// <summary>
/// Reload a plugin.
/// </summary>
/// <param name="plugin"></param>
/// <param name="forceNonPiston">Reload a non-Piston plugin</param>
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<ITorchPlugin> 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);

View File

@@ -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;

View File

@@ -121,18 +121,20 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="Commands\ChatCommand.cs" />
<Compile Include="Commands\ChatCommandAttribute.cs" />
<Compile Include="Commands\ChatCommandModule.cs" />
<Compile Include="Commands\CategoryAttribute.cs" />
<Compile Include="Commands\Command.cs" />
<Compile Include="Commands\CommandAttribute.cs" />
<Compile Include="Commands\CommandModule.cs" />
<Compile Include="Commands\CommandContext.cs" />
<Compile Include="Commands\CommandSystem.cs" />
<Compile Include="Commands\CommandTree.cs" />
<Compile Include="Logger.cs" />
<Compile Include="MultiplayerManager.cs" />
<Compile Include="TorchBase.cs" />
<Compile Include="TorchPlugin.cs" />
<Compile Include="Player.cs" />
<Compile Include="Collections\PlayerInfoCache.cs" />
<Compile Include="SteamService.cs" />
<Compile Include="TorchPluginBase.cs" />
<Compile Include="ViewModels\ChatItem.cs" />
<Compile Include="ViewModels\ModViewModel.cs" />
<Compile Include="Collections\MTObservableCollection.cs" />

View File

@@ -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
{
/// <summary>
/// Dirty hack because *keen*
/// Use only if absolutely necessary.
/// </summary>
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)
/// <summary>
/// Invokes an action on the game thread.
/// </summary>
/// <param name="action"></param>
public void Invoke(Action action)
{
MySandboxGame.Static.Invoke(action);
}
@@ -42,24 +58,41 @@ namespace Torch
/// Invokes an action on the game thread asynchronously.
/// </summary>
/// <param name="action"></param>
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));
}
/// <summary>
/// Invokes an action on the game thread and blocks until it is completed.
/// </summary>
/// <param name="action"></param>
public void InvokeBlocking(Action action)
{
if (action == null)
return;
if (Thread.CurrentThread == MySandboxGame.Static.UpdateThread)
{
Debug.Assert(false, $"{nameof(InvokeBlocking)} should not be called on the game thread.");
action.Invoke();
return;
}
var e = new AutoResetEvent(false);
MySandboxGame.Static.Invoke(() =>
{
try
{
action?.Invoke();
action.Invoke();
}
catch (Exception ex)
{
@@ -73,12 +106,19 @@ namespace Torch
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();
}
}

106
Torch/TorchPluginBase.cs Normal file
View File

@@ -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<GuidAttribute>()?.Value;
if (id == null)
throw new InvalidOperationException($"{asm.FullName} has no Guid attribute.");
Id = new Guid(id);
var ver = asm.GetCustomAttribute<AssemblyVersionAttribute>()?.Version;
if (ver == null)
throw new InvalidOperationException($"{asm.FullName} has no AssemblyVersion attribute.");
Version = new Version(ver);
var name = asm.GetCustomAttribute<AssemblyTitleAttribute>()?.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
}
}