Merge branch 'master' into master

This commit is contained in:
Alexander Qvist-Hellum
2017-07-07 00:39:00 +02:00
committed by GitHub
31 changed files with 597 additions and 319 deletions

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -8,37 +8,105 @@ using VRage.Game.ModAPI;
namespace Torch.API namespace Torch.API
{ {
/// <summary>
/// API for Torch functions shared between client and server.
/// </summary>
public interface ITorchBase public interface ITorchBase
{ {
/// <summary>
/// Fired when the session begins loading.
/// </summary>
event Action SessionLoading; event Action SessionLoading;
/// <summary>
/// Fired when the session finishes loading.
/// </summary>
event Action SessionLoaded; event Action SessionLoaded;
/// <summary>
/// Fires when the session begins unloading.
/// </summary>
event Action SessionUnloading; event Action SessionUnloading;
/// <summary>
/// Fired when the session finishes unloading.
/// </summary>
event Action SessionUnloaded; event Action SessionUnloaded;
/// <summary>
/// Configuration for the current instance.
/// </summary>
ITorchConfig Config { get; } ITorchConfig Config { get; }
/// <inheritdoc cref="IMultiplayerManager"/>
IMultiplayerManager Multiplayer { get; } IMultiplayerManager Multiplayer { get; }
/// <inheritdoc cref="IPluginManager"/>
IPluginManager Plugins { get; } IPluginManager Plugins { get; }
/// <summary>
/// The binary version of the current instance.
/// </summary>
Version TorchVersion { get; } Version TorchVersion { get; }
/// <summary>
/// Invoke an action on the game thread.
/// </summary>
void Invoke(Action action); void Invoke(Action action);
/// <summary>
/// Invoke an action on the game thread and block until it has completed.
/// If this is called on the game thread the action will execute immediately.
/// </summary>
void InvokeBlocking(Action action); void InvokeBlocking(Action action);
/// <summary>
/// Invoke an action on the game thread asynchronously.
/// </summary>
Task InvokeAsync(Action action); Task InvokeAsync(Action action);
string[] RunArgs { get; set; }
bool IsOnGameThread(); /// <summary>
/// Start the Torch instance.
/// </summary>
void Start(); void Start();
/// <summary>
/// Stop the Torch instance.
/// </summary>
void Stop(); void Stop();
/// <summary> /// <summary>
/// Initializes a save of the game. /// Initializes a save of the game.
/// </summary> /// </summary>
/// <param name="callerId">Id of the player who initiated the save.</param> /// <param name="callerId">Id of the player who initiated the save.</param>
void Save(long callerId); void Save(long callerId);
/// <summary>
/// Initialize the Torch instance.
/// </summary>
void Init(); void Init();
/// <summary>
/// Get an <see cref="IManager"/> that is part of the Torch instance.
/// </summary>
/// <typeparam name="T">Manager type</typeparam>
T GetManager<T>() where T : class, IManager; T GetManager<T>() where T : class, IManager;
} }
/// <summary>
/// API for the Torch server.
/// </summary>
public interface ITorchServer : ITorchBase public interface ITorchServer : ITorchBase
{ {
/// <summary>
/// Path of the dedicated instance folder.
/// </summary>
string InstancePath { get; } string InstancePath { get; }
} }
/// <summary>
/// API for the Torch client.
/// </summary>
public interface ITorchClient : ITorchBase public interface ITorchClient : ITorchBase
{ {

View File

@@ -1,14 +1,47 @@
namespace Torch using System.Collections.Generic;
namespace Torch
{ {
public interface ITorchConfig public interface ITorchConfig
{ {
/// <summary>
/// (server) Name of the instance.
/// </summary>
string InstanceName { get; set; } string InstanceName { get; set; }
/// <summary>
/// (server) Dedicated instance path.
/// </summary>
string InstancePath { get; set; } string InstancePath { get; set; }
bool RedownloadPlugins { get; set; }
bool AutomaticUpdates { get; set; } /// <summary>
/// Enable automatic Torch updates.
/// </summary>
bool GetTorchUpdates { get; set; }
/// <summary>
/// Enable automatic Torch updates.
/// </summary>
bool GetPluginUpdates { get; set; }
/// <summary>
/// Restart Torch automatically if it crashes.
/// </summary>
bool RestartOnCrash { get; set; } bool RestartOnCrash { get; set; }
/// <summary>
/// Time-out in seconds for the Torch watchdog (to detect a hung session).
/// </summary>
int TickTimeout { get; set; } int TickTimeout { get; set; }
/// <summary>
/// A list of plugins that should be installed.
/// </summary>
List<string> Plugins { get; }
/// <summary>
/// Saves the config.
/// </summary>
bool Save(string path = null); bool Save(string path = null);
} }
} }

View File

@@ -6,8 +6,14 @@ using System.Threading.Tasks;
namespace Torch.API.Managers namespace Torch.API.Managers
{ {
/// <summary>
/// Base interface for Torch managers.
/// </summary>
public interface IManager public interface IManager
{ {
/// <summary>
/// Initializes the manager. Called after Torch is initialized.
/// </summary>
void Init(); void Init();
} }
} }

View File

@@ -6,17 +6,56 @@ using VRage.Game.ModAPI;
namespace Torch.API.Managers namespace Torch.API.Managers
{ {
/// <summary>
/// Delegate for received messages.
/// </summary>
/// <param name="message">Message data.</param>
/// <param name="sendToOthers">Flag to broadcast message to other players.</param>
public delegate void MessageReceivedDel(IChatMessage message, ref bool sendToOthers); public delegate void MessageReceivedDel(IChatMessage message, ref bool sendToOthers);
/// <summary>
/// API for multiplayer related functions.
/// </summary>
public interface IMultiplayerManager : IManager public interface IMultiplayerManager : IManager
{ {
/// <summary>
/// Fired when a player joins.
/// </summary>
event Action<IPlayer> PlayerJoined; event Action<IPlayer> PlayerJoined;
/// <summary>
/// Fired when a player disconnects.
/// </summary>
event Action<IPlayer> PlayerLeft; event Action<IPlayer> PlayerLeft;
/// <summary>
/// Fired when a chat message is received.
/// </summary>
event MessageReceivedDel MessageReceived; event MessageReceivedDel MessageReceived;
/// <summary>
/// Send a chat message to all or one specific player.
/// </summary>
void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Blue); void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Blue);
/// <summary>
/// Kicks the player from the game.
/// </summary>
void KickPlayer(ulong steamId); void KickPlayer(ulong steamId);
/// <summary>
/// Bans or unbans a player from the game.
/// </summary>
void BanPlayer(ulong steamId, bool banned = true); void BanPlayer(ulong steamId, bool banned = true);
/// <summary>
/// Gets a player by their Steam64 ID or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerBySteamId(ulong id); IMyPlayer GetPlayerBySteamId(ulong id);
/// <summary>
/// Gets a player by their display name or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerByName(string name); IMyPlayer GetPlayerByName(string name);
} }
} }

View File

@@ -9,14 +9,30 @@ using VRage.Network;
namespace Torch.API.Managers namespace Torch.API.Managers
{ {
/// <summary>
/// API for the network intercept.
/// </summary>
public interface INetworkManager : IManager public interface INetworkManager : IManager
{ {
/// <summary>
/// Register a network handler.
/// </summary>
void RegisterNetworkHandler(INetworkHandler handler); void RegisterNetworkHandler(INetworkHandler handler);
} }
/// <summary>
/// Handler for multiplayer network messages.
/// </summary>
public interface INetworkHandler public interface INetworkHandler
{ {
/// <summary>
/// Returns if the handler can process the call site.
/// </summary>
bool CanHandle(CallSite callSite); bool CanHandle(CallSite callSite);
/// <summary>
/// Processes a network message.
/// </summary>
bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet); bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
} }
} }

View File

@@ -6,11 +6,29 @@ using VRage.Plugins;
namespace Torch.API.Managers namespace Torch.API.Managers
{ {
/// <summary>
/// API for the Torch plugin manager.
/// </summary>
public interface IPluginManager : IManager, IEnumerable<ITorchPlugin> public interface IPluginManager : IManager, IEnumerable<ITorchPlugin>
{ {
/// <summary>
/// Fired when plugins are loaded.
/// </summary>
event Action<List<ITorchPlugin>> PluginsLoaded; event Action<List<ITorchPlugin>> PluginsLoaded;
List<ITorchPlugin> Plugins { get; }
/// <summary>
/// Collection of loaded plugins.
/// </summary>
ObservableCollection<ITorchPlugin> Plugins { get; }
/// <summary>
/// Updates all loaded plugins.
/// </summary>
void UpdatePlugins(); void UpdatePlugins();
/// <summary>
/// Disposes all loaded plugins.
/// </summary>
void DisposePlugins(); void DisposePlugins();
} }
} }

View File

@@ -6,11 +6,29 @@ using System.Threading.Tasks;
namespace Torch.API namespace Torch.API
{ {
/// <summary>
/// Used to indicate the state of the dedicated server.
/// </summary>
public enum ServerState public enum ServerState
{ {
/// <summary>
/// The server is not running.
/// </summary>
Stopped, Stopped,
/// <summary>
/// The server is starting/loading the session.
/// </summary>
Starting, Starting,
/// <summary>
/// The server is running.
/// </summary>
Running, Running,
/// <summary>
/// The server encountered an error.
/// </summary>
Error Error
} }
} }

View File

@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("1.0.182.329")] [assembly: AssemblyVersion("1.0.186.642")]
[assembly: AssemblyFileVersion("1.0.182.329")] [assembly: AssemblyFileVersion("1.0.186.642")]

View File

@@ -45,6 +45,10 @@
<HintPath>..\packages\NLog.4.4.1\lib\net45\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.4.1\lib\net45\NLog.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath>
</Reference>
<Reference Include="Sandbox.Game"> <Reference Include="Sandbox.Game">
<HintPath>..\GameBinaries\Sandbox.Game.dll</HintPath> <HintPath>..\GameBinaries\Sandbox.Game.dll</HintPath>
<Private>False</Private> <Private>False</Private>

View File

@@ -9,6 +9,7 @@ using Sandbox;
using Sandbox.Engine.Platform; using Sandbox.Engine.Platform;
using Sandbox.Engine.Utils; using Sandbox.Engine.Utils;
using Sandbox.Game; using Sandbox.Game;
using Sandbox.ModAPI;
using SpaceEngineers.Game; using SpaceEngineers.Game;
using Torch.API; using Torch.API;
using VRage.FileSystem; using VRage.FileSystem;

View File

@@ -34,8 +34,8 @@ namespace Torch.Server
private static ITorchServer _server; private static ITorchServer _server;
private static Logger _log = LogManager.GetLogger("Torch"); private static Logger _log = LogManager.GetLogger("Torch");
private static bool _restartOnCrash; private static bool _restartOnCrash;
public static bool IsManualInstall; private static TorchConfig _config;
private static TorchCli _cli; private static bool _steamCmdDone;
/// <summary> /// <summary>
/// This method must *NOT* load any types/assemblies from the vanilla game, otherwise automatic updates will fail. /// This method must *NOT* load any types/assemblies from the vanilla game, otherwise automatic updates will fail.
@@ -46,10 +46,10 @@ namespace Torch.Server
//Ensures that all the files are downloaded in the Torch directory. //Ensures that all the files are downloaded in the Torch directory.
Directory.SetCurrentDirectory(new FileInfo(typeof(Program).Assembly.Location).Directory.ToString()); Directory.SetCurrentDirectory(new FileInfo(typeof(Program).Assembly.Location).Directory.ToString());
IsManualInstall = File.Exists("SpaceEngineersDedicated.exe"); foreach (var file in Directory.GetFiles(Directory.GetCurrentDirectory(), "*.old"))
if (!IsManualInstall) File.Delete(file);
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
if (!Environment.UserInteractive) if (!Environment.UserInteractive)
@@ -63,50 +63,40 @@ namespace Torch.Server
var configName = "TorchConfig.xml"; var configName = "TorchConfig.xml";
var configPath = Path.Combine(Directory.GetCurrentDirectory(), configName); var configPath = Path.Combine(Directory.GetCurrentDirectory(), configName);
TorchConfig options;
if (File.Exists(configName)) if (File.Exists(configName))
{ {
_log.Info($"Loading config {configPath}"); _log.Info($"Loading config {configPath}");
options = TorchConfig.LoadFrom(configPath); _config = TorchConfig.LoadFrom(configPath);
} }
else else
{ {
_log.Info($"Generating default config at {configPath}"); _log.Info($"Generating default config at {configPath}");
options = new TorchConfig(); _config = new TorchConfig {InstancePath = Path.GetFullPath("Instance")};
if (!IsManualInstall) _log.Warn("Would you like to enable automatic updates? (Y/n):");
var input = Console.ReadLine() ?? "";
var autoUpdate = string.IsNullOrEmpty(input) || input.Equals("y", StringComparison.InvariantCultureIgnoreCase);
_config.GetTorchUpdates = _config.GetPluginUpdates = autoUpdate;
if (autoUpdate)
{ {
//new ConfigManager().CreateInstance("Instance"); _log.Info("Automatic updates enabled.");
options.InstancePath = Path.GetFullPath("Instance"); RunSteamCmd();
_log.Warn("Would you like to enable automatic updates? (Y/n):");
var input = Console.ReadLine() ?? "";
var autoUpdate = !input.Equals("n", StringComparison.InvariantCultureIgnoreCase);
options.AutomaticUpdates = autoUpdate;
if (autoUpdate)
{
_log.Info("Automatic updates enabled, updating server.");
RunSteamCmd();
}
} }
//var setupDialog = new FirstTimeSetup { DataContext = options }; _config.Save(configPath);
//setupDialog.ShowDialog();
options.Save(configPath);
} }
_cli = new TorchCli { Config = options }; if (!_config.Parse(args))
if (!_cli.Parse(args))
return; return;
_log.Debug(_cli.ToString()); _log.Debug(_config.ToString());
if (!string.IsNullOrEmpty(_cli.WaitForPID)) if (!string.IsNullOrEmpty(_config.WaitForPID))
{ {
try try
{ {
var pid = int.Parse(_cli.WaitForPID); var pid = int.Parse(_config.WaitForPID);
var waitProc = Process.GetProcessById(pid); var waitProc = Process.GetProcessById(pid);
_log.Warn($"Waiting for process {pid} to exit."); _log.Warn($"Waiting for process {pid} to exit.");
waitProc.WaitForExit(); waitProc.WaitForExit();
@@ -117,18 +107,13 @@ namespace Torch.Server
} }
} }
_restartOnCrash = _cli.RestartOnCrash; _restartOnCrash = _config.RestartOnCrash;
if (options.AutomaticUpdates || _cli.Update) if (_config.GetTorchUpdates || _config.Update)
{ {
if (IsManualInstall) RunSteamCmd();
_log.Warn("Detected manual install, won't attempt to update DS");
else
{
RunSteamCmd();
}
} }
RunServer(options, _cli); RunServer(_config);
} }
private const string STEAMCMD_DIR = "steamcmd"; private const string STEAMCMD_DIR = "steamcmd";
@@ -142,6 +127,9 @@ quit";
public static void RunSteamCmd() public static void RunSteamCmd()
{ {
if (_steamCmdDone)
return;
var log = LogManager.GetLogger("SteamCMD"); var log = LogManager.GetLogger("SteamCMD");
if (!Directory.Exists(STEAMCMD_DIR)) if (!Directory.Exists(STEAMCMD_DIR))
@@ -187,38 +175,40 @@ quit";
log.Info(cmd.StandardOutput.ReadLine()); log.Info(cmd.StandardOutput.ReadLine());
Thread.Sleep(100); Thread.Sleep(100);
} }
_steamCmdDone = true;
} }
public static void RunServer(TorchConfig options, TorchCli cli) public static void RunServer(TorchConfig config)
{ {
/* /*
if (!parser.ParseArguments(args, options)) if (!parser.ParseArguments(args, config))
{ {
_log.Error($"Parsing arguments failed: {string.Join(" ", args)}"); _log.Error($"Parsing arguments failed: {string.Join(" ", args)}");
return; return;
} }
if (!string.IsNullOrEmpty(options.Config) && File.Exists(options.Config)) if (!string.IsNullOrEmpty(config.Config) && File.Exists(config.Config))
{ {
options = ServerConfig.LoadFrom(options.Config); config = ServerConfig.LoadFrom(config.Config);
parser.ParseArguments(args, options); parser.ParseArguments(args, config);
}*/ }*/
//RestartOnCrash autostart autosave=15 //RestartOnCrash autostart autosave=15
//gamepath ="C:\Program Files\Space Engineers DS" instance="Hydro Survival" instancepath="C:\ProgramData\SpaceEngineersDedicated\Hydro Survival" //gamepath ="C:\Program Files\Space Engineers DS" instance="Hydro Survival" instancepath="C:\ProgramData\SpaceEngineersDedicated\Hydro Survival"
/* /*
if (options.InstallService) if (config.InstallService)
{ {
var serviceName = $"\"Torch - {options.InstanceName}\""; var serviceName = $"\"Torch - {config.InstanceName}\"";
// Working on installing the service properly instead of with sc.exe // Working on installing the service properly instead of with sc.exe
_log.Info($"Installing service '{serviceName}"); _log.Info($"Installing service '{serviceName}");
var exePath = $"\"{Assembly.GetExecutingAssembly().Location}\""; var exePath = $"\"{Assembly.GetExecutingAssembly().Location}\"";
var createInfo = new ServiceCreateInfo var createInfo = new ServiceCreateInfo
{ {
Name = options.InstanceName, Name = config.InstanceName,
BinaryPath = exePath, BinaryPath = exePath,
}; };
_log.Info("Service Installed"); _log.Info("Service Installed");
@@ -238,7 +228,7 @@ quit";
return; return;
} }
if (options.UninstallService) if (config.UninstallService)
{ {
_log.Info("Uninstalling Torch service"); _log.Info("Uninstalling Torch service");
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
@@ -254,18 +244,18 @@ quit";
return; return;
}*/ }*/
_server = new TorchServer(options); _server = new TorchServer(config);
_server.Init(); _server.Init();
if (cli.NoGui || cli.Autostart) if (config.NoGui || config.Autostart)
{ {
new Thread(() => _server.Start()).Start(); new Thread(() => _server.Start()).Start();
} }
if (!cli.NoGui) if (!config.NoGui)
{ {
var ui = new TorchUI((TorchServer)_server); var ui = new TorchUI((TorchServer)_server);
ui.LoadConfig(options); ui.LoadConfig(config);
ui.ShowDialog(); ui.ShowDialog();
} }
} }
@@ -295,17 +285,9 @@ quit";
Thread.Sleep(5000); Thread.Sleep(5000);
if (_restartOnCrash) if (_restartOnCrash)
{ {
/* Throws an exception somehow and I'm too lazy to debug it.
try
{
if (MySession.Static != null && MySession.Static.AutoSaveInMinutes > 0)
MySession.Static.Save();
}
catch { }*/
var exe = typeof(Program).Assembly.Location; var exe = typeof(Program).Assembly.Location;
_cli.WaitForPID = Process.GetCurrentProcess().Id.ToString(); _config.WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, _cli.ToString()); Process.Start(exe, _config.ToString());
} }
//1627 = Function failed during execution. //1627 = Function failed during execution.
Environment.Exit(1627); Environment.Exit(1627);

View File

@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("1.0.182.329")] [assembly: AssemblyVersion("1.0.186.642")]
[assembly: AssemblyFileVersion("1.0.182.329")] [assembly: AssemblyFileVersion("1.0.186.642")]

View File

@@ -184,7 +184,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Managers\ConfigManager.cs" /> <Compile Include="Managers\ConfigManager.cs" />
<Compile Include="TorchCli.cs" />
<Compile Include="NativeMethods.cs" /> <Compile Include="NativeMethods.cs" />
<Compile Include="Properties\AssemblyInfo.cs"> <Compile Include="Properties\AssemblyInfo.cs">
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>

View File

@@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Server
{
public class TorchCli : CommandLine
{
public TorchConfig Config { get; set; }
[Arg("instancepath", "Server data folder where saves and mods are stored.")]
public string InstancePath { get => Config.InstancePath; set => Config.InstancePath = value; }
[Arg("noupdate", "Disable automatically downloading game and plugin updates.")]
public bool NoUpdate { get => !Config.AutomaticUpdates; set => Config.AutomaticUpdates = !value; }
[Arg("update", "Manually check for and install updates.")]
public bool Update { get; set; }
//TODO: backend code for this
//[Arg("worldpath", "Path to the game world folder to load.")]
public string WorldPath { get; set; }
[Arg("autostart", "Start the server immediately.")]
public bool Autostart { get; set; }
[Arg("restartoncrash", "Automatically restart the server if it crashes.")]
public bool RestartOnCrash { get => Config.RestartOnCrash; set => Config.RestartOnCrash = value; }
[Arg("nogui", "Do not show the Torch UI.")]
public bool NoGui { get; set; }
[Arg("silent", "Do not show the Torch UI or the command line.")]
public bool Silent { get; set; }
[Arg("waitforpid", "Makes Torch wait for another process to exit.")]
public string WaitForPID { get; set; }
}
}

View File

@@ -7,25 +7,54 @@ using NLog;
namespace Torch.Server namespace Torch.Server
{ {
public class TorchConfig : ITorchConfig public class TorchConfig : CommandLine, ITorchConfig
{ {
private static Logger _log = LogManager.GetLogger("Config"); private static Logger _log = LogManager.GetLogger("Config");
/// <inheritdoc />
[Arg("instancepath", "Server data folder where saves and mods are stored.")]
public string InstancePath { get; set; } public string InstancePath { get; set; }
public string InstanceName { get; set; }
#warning World Path not implemented /// <inheritdoc />
public string WorldPath { get; set; } [XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")]
public bool AutomaticUpdates { get; set; } = true; public bool NoUpdate { get => false; set => GetTorchUpdates = GetPluginUpdates = !value; }
public bool RedownloadPlugins { get; set; }
/// <inheritdoc />
[XmlIgnore, Arg("update", "Manually check for and install updates.")]
public bool Update { get; set; }
/// <inheritdoc />
[XmlIgnore, Arg("autostart", "Start the server immediately.")]
public bool Autostart { get; set; }
/// <inheritdoc />
[Arg("restartoncrash", "Automatically restart the server if it crashes.")]
public bool RestartOnCrash { get; set; } public bool RestartOnCrash { get; set; }
/// <summary>
/// How long in seconds to wait before automatically resetting a frozen server. /// <inheritdoc />
/// </summary> [XmlIgnore, Arg("nogui", "Do not show the Torch UI.")]
public bool NoGui { get; set; }
/// <inheritdoc />
[XmlIgnore, Arg("waitforpid", "Makes Torch wait for another process to exit.")]
public string WaitForPID { get; set; }
/// <inheritdoc />
[Arg("instancename", "The name of the Torch instance.")]
public string InstanceName { get; set; }
/// <inheritdoc />
public bool GetTorchUpdates { get; set; } = true;
/// <inheritdoc />
public bool GetPluginUpdates { get; set; } = true;
/// <inheritdoc />
public int TickTimeout { get; set; } = 60; public int TickTimeout { get; set; } = 60;
/// <summary>
/// A list of plugins to install or update. TODO /// <inheritdoc />
/// </summary>
public List<string> Plugins { get; set; } = new List<string>(); public List<string> Plugins { get; set; } = new List<string>();
internal Point WindowSize { get; set; } = new Point(800, 600); internal Point WindowSize { get; set; } = new Point(800, 600);
internal Point WindowPosition { get; set; } = new Point(); internal Point WindowPosition { get; set; } = new Point();
[NonSerialized] [NonSerialized]

View File

@@ -16,6 +16,7 @@ using Sandbox.Game.Multiplayer;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using SteamSDK; using SteamSDK;
using Torch.API; using Torch.API;
using Torch.Managers;
using VRage.Dedicated; using VRage.Dedicated;
using VRage.FileSystem; using VRage.FileSystem;
using VRage.Game; using VRage.Game;
@@ -37,7 +38,9 @@ namespace Torch.Server
public Thread GameThread { get; private set; } public Thread GameThread { get; private set; }
public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } } public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } }
public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } } public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } }
/// <inheritdoc />
public string InstanceName => Config?.InstanceName; public string InstanceName => Config?.InstanceName;
/// <inheritdoc />
public string InstancePath => Config?.InstancePath; public string InstancePath => Config?.InstancePath;
private bool _isRunning; private bool _isRunning;
@@ -53,6 +56,7 @@ namespace Torch.Server
MyFakes.ENABLE_INFINARIO = false; MyFakes.ENABLE_INFINARIO = false;
} }
/// <inheritdoc />
public override void Init() public override void Init()
{ {
base.Init(); base.Init();
@@ -81,7 +85,7 @@ namespace Torch.Server
RuntimeHelpers.RunClassConstructor(typeof(MyObjectBuilder_Base).TypeHandle); RuntimeHelpers.RunClassConstructor(typeof(MyObjectBuilder_Base).TypeHandle);
} }
public void InvokeBeforeRun() private void InvokeBeforeRun()
{ {
var contentPath = "Content"; var contentPath = "Content";
@@ -91,16 +95,7 @@ namespace Torch.Server
Log.Debug("MyFileSystem already initialized"); Log.Debug("MyFileSystem already initialized");
else else
{ {
if (Program.IsManualInstall) MyFileSystem.ExePath = Path.Combine(GetManager<FilesystemManager>().TorchDirectory, "DedicatedServer64");
{
var rootPath = new FileInfo(MyFileSystem.ExePath).Directory.FullName;
contentPath = Path.Combine(rootPath, "Content");
}
else
{
MyFileSystem.ExePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DedicatedServer64");
}
MyFileSystem.Init(contentPath, InstancePath); MyFileSystem.Init(contentPath, InstancePath);
} }
@@ -132,14 +127,13 @@ namespace Torch.Server
MySandboxGame.Config.Load(); MySandboxGame.Config.Load();
} }
/// <summary> /// <inheritdoc />
/// Start server on the current thread.
/// </summary>
public override void Start() public override void Start()
{ {
if (State != ServerState.Stopped) if (State != ServerState.Stopped)
return; return;
IsRunning = true;
GameThread = Thread.CurrentThread; GameThread = Thread.CurrentThread;
Config.Save(); Config.Save();
State = ServerState.Starting; State = ServerState.Starting;
@@ -196,9 +190,7 @@ namespace Torch.Server
Log.Debug("Server watchdog responded"); Log.Debug("Server watchdog responded");
} }
/// <summary> /// <inheritdoc />
/// Stop the server.
/// </summary>
public override void Stop() public override void Stop()
{ {
if (State == ServerState.Stopped) if (State == ServerState.Stopped)
@@ -220,6 +212,12 @@ namespace Torch.Server
Log.Info("Server stopped."); Log.Info("Server stopped.");
_stopHandle.Set(); _stopHandle.Set();
State = ServerState.Stopped; State = ServerState.Stopped;
IsRunning = false;
}
public void Restart()
{
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -41,6 +42,17 @@ namespace Torch.Server
_server = (TorchBase)server; _server = (TorchBase)server;
_multiplayer = (MultiplayerManager)server.Multiplayer; _multiplayer = (MultiplayerManager)server.Multiplayer;
DataContext = _multiplayer; DataContext = _multiplayer;
_multiplayer.ChatHistory.CollectionChanged += ChatHistory_CollectionChanged;
}
private void ChatHistory_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (VisualTreeHelper.GetChildrenCount(ChatItems) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(ChatItems, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
} }
private void SendButton_Click(object sender, RoutedEventArgs e) private void SendButton_Click(object sender, RoutedEventArgs e)

View File

@@ -5,14 +5,18 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server" xmlns:local="clr-namespace:Torch.Server"
xmlns:views="clr-namespace:Torch.Server.Views" xmlns:views="clr-namespace:Torch.Server.Views"
xmlns:converters="clr-namespace:Torch.Server.Views.Converters"
mc:Ignorable="d" mc:Ignorable="d"
Title="Torch"> Title="Torch">
<Window.Resources>
<converters:InverseBooleanConverter x:Key="inverseBool"/>
</Window.Resources>
<DockPanel> <DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="5,5,5,5" Orientation="Horizontal"> <StackPanel DockPanel.Dock="Top" Margin="5,5,5,5" Orientation="Horizontal">
<Button x:Name="BtnStart" Content="Start" Height="24" Width="75" Margin="5,0,5,0" <Button x:Name="BtnStart" Content="Start" Height="24" Width="75" Margin="5,0,5,0"
HorizontalAlignment="Left" Click="BtnStart_Click" IsDefault="True" /> HorizontalAlignment="Left" Click="BtnStart_Click" IsDefault="True" IsEnabled="{Binding IsRunning, Converter={StaticResource inverseBool}}"/>
<Button x:Name="BtnStop" Content="Stop" Height="24" Width="75" Margin="5,0,5,0" HorizontalAlignment="Left" <Button x:Name="BtnStop" Content="Stop" Height="24" Width="75" Margin="5,0,5,0" HorizontalAlignment="Left"
Click="BtnStop_Click" IsEnabled="False" /> Click="BtnStop_Click" IsEnabled="{Binding IsRunning}" />
<Label> <Label>
<Label.Content> <Label.Content>
<TextBlock Text="{Binding State, StringFormat=Status: {0}}"></TextBlock> <TextBlock Text="{Binding State, StringFormat=Status: {0}}"></TextBlock>
@@ -37,13 +41,13 @@
<TextBox x:Name="InstancePathBox" Margin="3" Height="20" <TextBox x:Name="InstancePathBox" Margin="3" Height="20"
TextChanged="InstancePathBox_OnTextChanged" IsEnabled="False" /> TextChanged="InstancePathBox_OnTextChanged" IsEnabled="False" />
</DockPanel> </DockPanel>
<views:ConfigControl x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" /> <views:ConfigControl x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" IsEnabled="{Binding IsRunning, Converter={StaticResource inverseBool}}"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="Chat/Players"> <TabItem Header="Chat/Players">
<DockPanel> <DockPanel>
<local:PlayerListControl x:Name="PlayerList" DockPanel.Dock="Right" Width="250" /> <local:PlayerListControl x:Name="PlayerList" DockPanel.Dock="Right" Width="250" />
<local:ChatControl x:Name="Chat" /> <local:ChatControl x:Name="Chat" IsEnabled="{Binding IsRunning}"/>
</DockPanel> </DockPanel>
</TabItem> </TabItem>
<TabItem Header="Entity Manager"> <TabItem Header="Entity Manager">

View File

@@ -65,18 +65,12 @@ namespace Torch.Server
private void BtnStart_Click(object sender, RoutedEventArgs e) private void BtnStart_Click(object sender, RoutedEventArgs e)
{ {
_config.Save(); _config.Save();
((Button) sender).IsEnabled = false;
BtnStop.IsEnabled = true;
ConfigControl.SaveConfig(); ConfigControl.SaveConfig();
new Thread(_server.Start).Start(); new Thread(_server.Start).Start();
} }
private void BtnStop_Click(object sender, RoutedEventArgs e) private void BtnStop_Click(object sender, RoutedEventArgs e)
{ {
_config.Save();
((Button) sender).IsEnabled = false;
//HACK: Uncomment when restarting is possible.
//BtnStart.IsEnabled = true;
_server.Stop(); _server.Stop();
} }

View File

@@ -10,7 +10,7 @@ using VRage.Network;
namespace Torch namespace Torch
{ {
public struct ChatMessage : IChatMessage public class ChatMessage : IChatMessage
{ {
public DateTime Timestamp { get; } public DateTime Timestamp { get; }
public ulong SteamId { get; } public ulong SteamId { get; }

View File

@@ -9,6 +9,7 @@ using System.Windows.Threading;
namespace Torch.Collections namespace Torch.Collections
{ {
[Serializable]
public class ObservableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged public class ObservableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
{ {
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch.API;
namespace Torch.Managers
{
public class FilesystemManager : Manager
{
/// <summary>
/// Temporary directory for Torch that is cleared every time the program is started.
/// </summary>
public string TempDirectory { get; }
/// <summary>
/// Directory that contains the current Torch assemblies.
/// </summary>
public string TorchDirectory { get; }
public FilesystemManager(ITorchBase torchInstance) : base(torchInstance)
{
var temp = Path.Combine(Path.GetTempPath(), "Torch");
TempDirectory = Directory.CreateDirectory(temp).FullName;
var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName;
TorchDirectory = torch;
ClearTemp();
}
private void ClearTemp()
{
foreach (var file in Directory.GetFiles(TempDirectory, "*", SearchOption.AllDirectories))
File.Delete(file);
}
/// <summary>
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
/// </summary>
public void SoftDelete(string file)
{
if (!File.Exists(file))
return;
var rand = Path.GetRandomFileName();
var dest = Path.Combine(TempDirectory, rand);
File.Move(file, dest);
}
}
}

View File

@@ -33,20 +33,21 @@ using VRage.Utils;
namespace Torch.Managers namespace Torch.Managers
{ {
/// <summary> /// <inheritdoc />
/// Provides a proxy to the game's multiplayer-related functions.
/// </summary>
public class MultiplayerManager : Manager, IMultiplayerManager public class MultiplayerManager : Manager, IMultiplayerManager
{ {
/// <inheritdoc />
public event Action<IPlayer> PlayerJoined; public event Action<IPlayer> PlayerJoined;
/// <inheritdoc />
public event Action<IPlayer> PlayerLeft; public event Action<IPlayer> PlayerLeft;
/// <inheritdoc />
public event MessageReceivedDel MessageReceived; public event MessageReceivedDel MessageReceived;
public MTObservableCollection<IChatMessage> ChatHistory { get; } = new MTObservableCollection<IChatMessage>(); public MTObservableCollection<IChatMessage> ChatHistory { get; } = new MTObservableCollection<IChatMessage>();
public ObservableDictionary<ulong, PlayerViewModel> Players { get; } = new ObservableDictionary<ulong, PlayerViewModel>(); public ObservableDictionary<ulong, PlayerViewModel> Players { get; } = new ObservableDictionary<ulong, PlayerViewModel>();
public IMyPlayer LocalPlayer => MySession.Static.LocalHumanPlayer; public IMyPlayer LocalPlayer => MySession.Static.LocalHumanPlayer;
private static readonly Logger _log = LogManager.GetLogger(nameof(MultiplayerManager)); private static readonly Logger Log = LogManager.GetLogger(nameof(MultiplayerManager));
private static readonly Logger _chatLog = LogManager.GetLogger("Chat"); private static readonly Logger ChatLog = LogManager.GetLogger("Chat");
private Dictionary<MyPlayer.PlayerId, MyPlayer> _onlinePlayers; private Dictionary<MyPlayer.PlayerId, MyPlayer> _onlinePlayers;
internal MultiplayerManager(ITorchBase torch) : base(torch) internal MultiplayerManager(ITorchBase torch) : base(torch)
@@ -65,12 +66,14 @@ namespace Torch.Managers
{ {
var message = ChatMessage.FromChatMsg(msg); var message = ChatMessage.FromChatMsg(msg);
ChatHistory.Add(message); ChatHistory.Add(message);
_chatLog.Info($"{message.Name}: {message.Message}"); ChatLog.Info($"{message.Name}: {message.Message}");
MessageReceived?.Invoke(message, ref sendToOthers); MessageReceived?.Invoke(message, ref sendToOthers);
} }
/// <inheritdoc />
public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId)); public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
/// <inheritdoc />
public void BanPlayer(ulong steamId, bool banned = true) public void BanPlayer(ulong steamId, bool banned = true)
{ {
Torch.Invoke(() => Torch.Invoke(() =>
@@ -81,12 +84,14 @@ namespace Torch.Managers
}); });
} }
/// <inheritdoc />
public IMyPlayer GetPlayerByName(string name) public IMyPlayer GetPlayerByName(string name)
{ {
ValidateOnlinePlayersList(); ValidateOnlinePlayersList();
return _onlinePlayers.FirstOrDefault(x => x.Value.DisplayName == name).Value; return _onlinePlayers.FirstOrDefault(x => x.Value.DisplayName == name).Value;
} }
/// <inheritdoc />
public IMyPlayer GetPlayerBySteamId(ulong steamId) public IMyPlayer GetPlayerBySteamId(ulong steamId)
{ {
ValidateOnlinePlayersList(); ValidateOnlinePlayersList();
@@ -94,14 +99,13 @@ namespace Torch.Managers
return p; return p;
} }
/// <inheritdoc />
public string GetSteamUsername(ulong steamId) public string GetSteamUsername(ulong steamId)
{ {
return MyMultiplayer.Static.GetMemberName(steamId); return MyMultiplayer.Static.GetMemberName(steamId);
} }
/// <summary> /// <inheritdoc />
/// Send a message in chat.
/// </summary>
public void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Red) public void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Red)
{ {
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", message)); ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", message));
@@ -126,7 +130,7 @@ namespace Torch.Managers
private void OnSessionLoaded() private void OnSessionLoaded()
{ {
_log.Info("Initializing Steam auth"); Log.Info("Initializing Steam auth");
MyMultiplayer.Static.ClientKicked += OnClientKicked; MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft; MyMultiplayer.Static.ClientLeft += OnClientLeft;
@@ -138,7 +142,7 @@ namespace Torch.Managers
SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus; SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus;
_members = (List<ulong>)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static); _members = (List<ulong>)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
_waitingForGroup = (HashSet<ulong>)typeof(MyDedicatedServerBase).GetField("m_waitingForGroup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static); _waitingForGroup = (HashSet<ulong>)typeof(MyDedicatedServerBase).GetField("m_waitingForGroup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
_log.Info("Steam auth initialized"); Log.Info("Steam auth initialized");
} }
private void OnClientKicked(ulong steamId) private void OnClientKicked(ulong steamId)
@@ -151,7 +155,7 @@ namespace Torch.Managers
Players.TryGetValue(steamId, out PlayerViewModel vm); Players.TryGetValue(steamId, out PlayerViewModel vm);
if (vm == null) if (vm == null)
vm = new PlayerViewModel(steamId); vm = new PlayerViewModel(steamId);
_log.Info($"{vm.Name} ({vm.SteamId}) {(ConnectionState)stateChange}."); Log.Info($"{vm.Name} ({vm.SteamId}) {(ConnectionState)stateChange}.");
PlayerLeft?.Invoke(vm); PlayerLeft?.Invoke(vm);
Players.Remove(steamId); Players.Remove(steamId);
} }
@@ -178,7 +182,7 @@ namespace Torch.Managers
if (handle.Method.Name == "GameServer_ValidateAuthTicketResponse") if (handle.Method.Name == "GameServer_ValidateAuthTicketResponse")
{ {
SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse -= handle as ValidateAuthTicketResponse; SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse -= handle as ValidateAuthTicketResponse;
_log.Debug("Removed GameServer_ValidateAuthTicketResponse"); Log.Debug("Removed GameServer_ValidateAuthTicketResponse");
} }
} }
} }
@@ -191,7 +195,7 @@ namespace Torch.Managers
if (handle.Method.Name == "GameServer_UserGroupStatus") if (handle.Method.Name == "GameServer_UserGroupStatus")
{ {
SteamServerAPI.Instance.GameServer.UserGroupStatus -= handle as UserGroupStatus; SteamServerAPI.Instance.GameServer.UserGroupStatus -= handle as UserGroupStatus;
_log.Debug("Removed GameServer_UserGroupStatus"); Log.Debug("Removed GameServer_UserGroupStatus");
} }
} }
} }
@@ -200,16 +204,16 @@ namespace Torch.Managers
//Largely copied from SE //Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, AuthSessionResponseEnum response, ulong ownerSteamID) private void ValidateAuthTicketResponse(ulong steamID, AuthSessionResponseEnum response, ulong ownerSteamID)
{ {
_log.Info($"Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}"); Log.Info($"Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}");
if (steamID != ownerSteamID) if (steamID != ownerSteamID)
{ {
_log.Info($"User {steamID} is using a game owned by {ownerSteamID}. Tracking..."); Log.Info($"User {steamID} is using a game owned by {ownerSteamID}. Tracking...");
_gameOwnerIds[steamID] = ownerSteamID; _gameOwnerIds[steamID] = ownerSteamID;
if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID)) if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID))
{ {
_log.Info($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}..."); Log.Info($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}...");
UserRejected(steamID, JoinResult.BannedByAdmins); UserRejected(steamID, JoinResult.BannedByAdmins);
BanPlayer(steamID); BanPlayer(steamID);
} }
@@ -306,7 +310,7 @@ namespace Torch.Managers
{ {
typeof(MyDedicatedServerBase).GetMethod("UserAccepted", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId}); typeof(MyDedicatedServerBase).GetMethod("UserAccepted", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId});
var vm = new PlayerViewModel(steamId) {State = ConnectionState.Connected}; var vm = new PlayerViewModel(steamId) {State = ConnectionState.Connected};
_log.Info($"Player {vm.Name} joined ({vm.SteamId})"); Log.Info($"Player {vm.Name} joined ({vm.SteamId})");
Players.Add(steamId, vm); Players.Add(steamId, vm);
PlayerJoined?.Invoke(vm); PlayerJoined?.Invoke(vm);
} }

View File

@@ -64,6 +64,11 @@ namespace Torch.Managers
/// Loads the network intercept system /// Loads the network intercept system
/// </summary> /// </summary>
public override void Init() public override void Init()
{
Torch.SessionLoaded += OnSessionLoaded;
}
private void OnSessionLoaded()
{ {
if (_init) if (_init)
return; return;

View File

@@ -5,42 +5,30 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using Sandbox;
using Sandbox.ModAPI;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.API.Plugins; using Torch.API.Plugins;
using Torch.Commands; using Torch.Commands;
using Torch.Managers;
using Torch.Updater;
using VRage.Plugins;
using VRage.Collections; using VRage.Collections;
using VRage.Library.Collections;
namespace Torch.Managers namespace Torch.Managers
{ {
public class PluginManager : IPluginManager /// <inheritdoc />
public class PluginManager : Manager, IPluginManager
{ {
private readonly ITorchBase _torch;
private static Logger _log = LogManager.GetLogger(nameof(PluginManager)); private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
private UpdateManager _updateManager;
public List<ITorchPlugin> Plugins { get; } = new List<ITorchPlugin>(); /// <inheritdoc />
public ObservableCollection<ITorchPlugin> Plugins { get; } = new ObservableCollection<ITorchPlugin>();
public float LastUpdateMs => _lastUpdateMs;
private volatile float _lastUpdateMs;
public event Action<List<ITorchPlugin>> PluginsLoaded; public event Action<List<ITorchPlugin>> PluginsLoaded;
public PluginManager(ITorchBase torch) public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{ {
_torch = torch;
if (!Directory.Exists(PluginDir)) if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir); Directory.CreateDirectory(PluginDir);
} }
@@ -50,11 +38,8 @@ namespace Torch.Managers
/// </summary> /// </summary>
public void UpdatePlugins() public void UpdatePlugins()
{ {
var s = Stopwatch.StartNew();
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
plugin.Update(); plugin.Update();
s.Stop();
_lastUpdateMs = (float)s.Elapsed.TotalMilliseconds;
} }
/// <summary> /// <summary>
@@ -70,40 +55,42 @@ namespace Torch.Managers
private void DownloadPlugins() private void DownloadPlugins()
{ {
_log.Info("Downloading plugins");
var updater = new PluginUpdater(this);
var folders = Directory.GetDirectories(PluginDir); var folders = Directory.GetDirectories(PluginDir);
var taskList = new List<Task>(); var taskList = new List<Task>();
if (_torch.Config.RedownloadPlugins)
_log.Warn("Force downloading all plugins because the RedownloadPlugins flag is set in the config"); //Copy list because we don't want to modify the config.
var toDownload = Torch.Config.Plugins.ToList();
foreach (var folder in folders) foreach (var folder in folders)
{ {
var manifestPath = Path.Combine(folder, "manifest.xml"); var manifestPath = Path.Combine(folder, "manifest.xml");
if (!File.Exists(manifestPath)) if (!File.Exists(manifestPath))
{ {
_log.Info($"No manifest in {folder}, skipping"); _log.Debug($"No manifest in {folder}, skipping");
continue; continue;
} }
_log.Info($"Checking for updates for {folder}");
var manifest = PluginManifest.Load(manifestPath); var manifest = PluginManifest.Load(manifestPath);
taskList.Add(updater.CheckAndUpdate(manifest, _torch.Config.RedownloadPlugins)); toDownload.Remove(manifest.Repository);
taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
}
foreach (var repository in toDownload)
{
var manifest = new PluginManifest {Repository = repository, Version = "0.0"};
taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
} }
Task.WaitAll(taskList.ToArray()); Task.WaitAll(taskList.ToArray());
_torch.Config.RedownloadPlugins = false;
} }
/// <summary> /// <inheritdoc />
/// Loads and creates instances of all plugins in the <see cref="PluginDir"/> folder. public override void Init()
/// </summary>
public void Init()
{ {
var commands = ((TorchBase)_torch).Commands; _updateManager = Torch.GetManager<UpdateManager>();
var commands = Torch.GetManager<CommandManager>();
if (_torch.Config.AutomaticUpdates) if (Torch.Config.GetPluginUpdates)
DownloadPlugins(); DownloadPlugins();
else else
_log.Warn("Automatic plugin updates are disabled."); _log.Warn("Automatic plugin updates are disabled.");
@@ -129,7 +116,7 @@ namespace Torch.Managers
throw new TypeLoadException($"Plugin '{type.FullName}' is missing a {nameof(PluginAttribute)}"); throw new TypeLoadException($"Plugin '{type.FullName}' is missing a {nameof(PluginAttribute)}");
_log.Info($"Loading plugin {plugin.Name} ({plugin.Version})"); _log.Info($"Loading plugin {plugin.Name} ({plugin.Version})");
plugin.StoragePath = _torch.Config.InstancePath; plugin.StoragePath = Torch.Config.InstancePath;
Plugins.Add(plugin); Plugins.Add(plugin);
commands.RegisterPluginCommands(plugin); commands.RegisterPluginCommands(plugin);
@@ -143,8 +130,8 @@ namespace Torch.Managers
} }
} }
Plugins.ForEach(p => p.Init(_torch)); Plugins.ForEach(p => p.Init(Torch));
PluginsLoaded?.Invoke(Plugins); PluginsLoaded?.Invoke(Plugins.ToList());
} }
public IEnumerator<ITorchPlugin> GetEnumerator() public IEnumerator<ITorchPlugin> GetEnumerator()

View File

@@ -1,28 +1,128 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Packaging;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog;
using Octokit;
using SteamSDK; using SteamSDK;
using Torch.API;
namespace Torch.Managers namespace Torch.Managers
{ {
/// <summary> /// <summary>
/// Handles updating of the DS and Torch plugins. /// Handles updating of the DS and Torch plugins.
/// </summary> /// </summary>
public class UpdateManager : IDisposable public class UpdateManager : Manager, IDisposable
{ {
private Timer _updatePollTimer; private Timer _updatePollTimer;
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
private Logger _log = LogManager.GetLogger(nameof(UpdateManager));
private FilesystemManager _fsManager;
public UpdateManager() public UpdateManager(ITorchBase torchInstance) : base(torchInstance)
{ {
_updatePollTimer = new Timer(CheckForUpdates, this, TimeSpan.Zero, TimeSpan.FromMinutes(5)); //_updatePollTimer = new Timer(TimerElapsed, this, TimeSpan.Zero, TimeSpan.FromMinutes(5));
} }
private void CheckForUpdates(object state) /// <inheritdoc />
public override void Init()
{ {
_fsManager = Torch.GetManager<FilesystemManager>();
CheckAndUpdateTorch();
}
private void TimerElapsed(object state)
{
CheckAndUpdateTorch();
}
private async Task<Tuple<Version, string>> GetLatestRelease(string owner, string name)
{
try
{
var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false);
if (latest == null)
return new Tuple<Version, string>(new Version(), null);
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
return new Tuple<Version, string>(new Version(latest.TagName ?? "0"), zip?.BrowserDownloadUrl);
}
catch (Exception)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
return new Tuple<Version, string>(new Version(), null);
}
}
public async Task CheckAndUpdatePlugin(PluginManifest manifest)
{
if (!Torch.Config.GetPluginUpdates)
return;
var name = manifest.Repository.Split('/');
if (name.Length != 2)
{
_log.Error($"'{manifest.Repository}' is not a valid GitHub repository.");
return;
}
var currentVersion = new Version(manifest.Version);
var releaseInfo = await GetLatestRelease(name[0], name[1]).ConfigureAwait(false);
if (releaseInfo.Item1 > currentVersion)
{
_log.Warn($"Updating {manifest.Repository} from version {currentVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, $"{name[0]}_{name[1]}.zip");
var updatePath = Path.Combine(_torchDir, "Plugins");
await new WebClient().DownloadFileTaskAsync(new Uri(releaseInfo.Item2), updateName).ConfigureAwait(false);
UpdateFromZip(updateName, updatePath);
File.Delete(updateName);
}
else
{
_log.Info($"{manifest.Repository} is up to date. ({currentVersion})");
}
}
private async void CheckAndUpdateTorch()
{
if (!Torch.Config.GetTorchUpdates)
return;
var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
if (releaseInfo.Item1 > Torch.TorchVersion)
{
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip");
new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
UpdateFromZip(updateName, _torchDir);
File.Delete(updateName);
}
else
{
_log.Info("Torch is up to date.");
}
}
private void UpdateFromZip(string zipFile, string extractPath)
{
using (var zip = ZipFile.OpenRead(zipFile))
{
foreach (var file in zip.Entries)
{
_log.Debug($"Unzipping {file.FullName}");
var targetFile = Path.Combine(extractPath, file.FullName);
_fsManager.SoftDelete(targetFile);
}
zip.ExtractToDirectory(extractPath);
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -10,12 +12,14 @@ namespace Torch
{ {
/// <summary> /// <summary>
/// Simple class that manages saving <see cref="Persistent{T}.Data"/> to disk using JSON serialization. /// Simple class that manages saving <see cref="Persistent{T}.Data"/> to disk using JSON serialization.
/// Can automatically save on changes by implementing <see cref="INotifyPropertyChanged"/> in the data class.
/// </summary> /// </summary>
/// <typeparam name="T">Data class type</typeparam> /// <typeparam name="T">Data class type</typeparam>
public sealed class Persistent<T> : IDisposable where T : new() public sealed class Persistent<T> : IDisposable where T : new()
{ {
public string Path { get; set; } public string Path { get; set; }
public T Data { get; private set; } public T Data { get; private set; }
private Timer _saveTimer;
~Persistent() ~Persistent()
{ {
@@ -24,8 +28,21 @@ namespace Torch
public Persistent(string path, T data = default(T)) public Persistent(string path, T data = default(T))
{ {
_saveTimer = new Timer(Callback);
Path = path; Path = path;
Data = data; Data = data;
if (Data is INotifyPropertyChanged npc)
npc.PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_saveTimer.Change(5000, -1);
}
private void Callback(object state)
{
Save();
} }
public void Save(string path = null) public void Save(string path = null)
@@ -65,6 +82,9 @@ namespace Torch
{ {
try try
{ {
if (Data is INotifyPropertyChanged npc)
npc.PropertyChanged -= OnPropertyChanged;
_saveTimer.Dispose();
Save(); Save();
} }
catch catch

View File

@@ -81,6 +81,8 @@
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Xaml" /> <Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@@ -161,19 +163,19 @@
<Compile Include="Commands\TorchCommands.cs" /> <Compile Include="Commands\TorchCommands.cs" />
<Compile Include="Managers\ChatManager.cs" /> <Compile Include="Managers\ChatManager.cs" />
<Compile Include="Managers\EntityManager.cs" /> <Compile Include="Managers\EntityManager.cs" />
<Compile Include="Managers\FilesystemManager.cs" />
<Compile Include="Managers\Manager.cs" /> <Compile Include="Managers\Manager.cs" />
<Compile Include="Managers\NetworkManager\NetworkHandlerBase.cs" /> <Compile Include="Managers\NetworkManager\NetworkHandlerBase.cs" />
<Compile Include="Managers\NetworkManager\NetworkManager.cs" /> <Compile Include="Managers\NetworkManager\NetworkManager.cs" />
<Compile Include="Managers\MultiplayerManager.cs" /> <Compile Include="Managers\MultiplayerManager.cs" />
<Compile Include="Managers\UpdateManager.cs" /> <Compile Include="Managers\UpdateManager.cs" />
<Compile Include="Persistent.cs" /> <Compile Include="Persistent.cs" />
<Compile Include="Updater\PluginManifest.cs" /> <Compile Include="PluginManifest.cs" />
<Compile Include="Reflection.cs" /> <Compile Include="Reflection.cs" />
<Compile Include="Managers\ScriptingManager.cs" /> <Compile Include="Managers\ScriptingManager.cs" />
<Compile Include="TorchBase.cs" /> <Compile Include="TorchBase.cs" />
<Compile Include="SteamService.cs" /> <Compile Include="SteamService.cs" />
<Compile Include="TorchPluginBase.cs" /> <Compile Include="TorchPluginBase.cs" />
<Compile Include="Updater\PluginUpdater.cs" />
<Compile Include="ViewModels\ModViewModel.cs" /> <Compile Include="ViewModels\ModViewModel.cs" />
<Compile Include="Collections\MTObservableCollection.cs" /> <Compile Include="Collections\MTObservableCollection.cs" />
<Compile Include="Extensions\MyPlayerCollectionExtensions.cs" /> <Compile Include="Extensions\MyPlayerCollectionExtensions.cs" />

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -41,22 +41,38 @@ namespace Torch
/// Use only if necessary, prefer dependency injection. /// Use only if necessary, prefer dependency injection.
/// </summary> /// </summary>
public static ITorchBase Instance { get; private set; } public static ITorchBase Instance { get; private set; }
/// <inheritdoc />
public ITorchConfig Config { get; protected set; } public ITorchConfig Config { get; protected set; }
protected static Logger Log { get; } = LogManager.GetLogger("Torch"); /// <inheritdoc />
public Version TorchVersion { get; protected set; } public Version TorchVersion { get; protected set; }
/// <inheritdoc />
public Version GameVersion { get; private set; } public Version GameVersion { get; private set; }
/// <inheritdoc />
public string[] RunArgs { get; set; } public string[] RunArgs { get; set; }
/// <inheritdoc />
public IPluginManager Plugins { get; protected set; } public IPluginManager Plugins { get; protected set; }
/// <inheritdoc />
public IMultiplayerManager Multiplayer { get; protected set; } public IMultiplayerManager Multiplayer { get; protected set; }
/// <inheritdoc />
public EntityManager Entities { get; protected set; } public EntityManager Entities { get; protected set; }
/// <inheritdoc />
public INetworkManager Network { get; protected set; } public INetworkManager Network { get; protected set; }
/// <inheritdoc />
public CommandManager Commands { get; protected set; } public CommandManager Commands { get; protected set; }
/// <inheritdoc />
public event Action SessionLoading; public event Action SessionLoading;
/// <inheritdoc />
public event Action SessionLoaded; public event Action SessionLoaded;
/// <inheritdoc />
public event Action SessionUnloading; public event Action SessionUnloading;
/// <inheritdoc />
public event Action SessionUnloaded; public event Action SessionUnloaded;
private readonly List<IManager> _managers;
/// <summary>
/// Common log for the Torch instance.
/// </summary>
protected static Logger Log { get; } = LogManager.GetLogger("Torch");
private readonly List<IManager> _managers;
private bool _init; private bool _init;
/// <summary> /// <summary>
@@ -79,22 +95,25 @@ namespace Torch
Network = new NetworkManager(this); Network = new NetworkManager(this);
Commands = new CommandManager(this); Commands = new CommandManager(this);
_managers = new List<IManager> {Network, Commands, Plugins, Multiplayer, Entities, new ChatManager(this)}; _managers = new List<IManager> { new FilesystemManager(this), new UpdateManager(this), Network, Commands, Plugins, Multiplayer, Entities, new ChatManager(this), };
TorchAPI.Instance = this; TorchAPI.Instance = this;
} }
/// <inheritdoc />
public ListReader<IManager> GetManagers() public ListReader<IManager> GetManagers()
{ {
return new ListReader<IManager>(_managers); return new ListReader<IManager>(_managers);
} }
/// <inheritdoc />
public T GetManager<T>() where T : class, IManager public T GetManager<T>() where T : class, IManager
{ {
return _managers.FirstOrDefault(m => m is T) as T; return _managers.FirstOrDefault(m => m is T) as T;
} }
/// <inheritdoc />
public bool AddManager<T>(T manager) where T : class, IManager public bool AddManager<T>(T manager) where T : class, IManager
{ {
if (_managers.Any(x => x is T)) if (_managers.Any(x => x is T))
@@ -208,6 +227,7 @@ namespace Torch
#endregion #endregion
/// <inheritdoc />
public virtual void Init() public virtual void Init()
{ {
Debug.Assert(!_init, "Torch instance is already initialized."); Debug.Assert(!_init, "Torch instance is already initialized.");
@@ -243,15 +263,14 @@ namespace Torch
MySession.OnUnloading += OnSessionUnloading; MySession.OnUnloading += OnSessionUnloading;
MySession.OnUnloaded += OnSessionUnloaded; MySession.OnUnloaded += OnSessionUnloaded;
RegisterVRagePlugin(); RegisterVRagePlugin();
foreach (var manager in _managers)
manager.Init();
_init = true; _init = true;
} }
private void OnSessionLoading() private void OnSessionLoading()
{ {
Log.Debug("Session loading"); Log.Debug("Session loading");
foreach (var manager in _managers)
manager.Init();
SessionLoading?.Invoke(); SessionLoading?.Invoke();
} }
@@ -292,11 +311,13 @@ namespace Torch
} }
/// <inheritdoc
public virtual void Start() public virtual void Start()
{ {
} }
/// <inheritdoc />
public virtual void Stop() { } public virtual void Stop() { }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Octokit;
using Torch.API;
using Torch.Managers;
using VRage.Compression;
namespace Torch.Updater
{
public class PluginUpdater
{
private readonly PluginManager _pluginManager;
private static readonly Logger Log = LogManager.GetLogger("PluginUpdater");
public PluginUpdater(PluginManager pm)
{
_pluginManager = pm;
}
public async Task CheckAndUpdate(PluginManifest manifest, bool force = false)
{
Log.Info($"Checking for update at {manifest.Repository}");
var split = manifest.Repository.Split('/');
if (split.Length != 2)
{
Log.Warn($"Manifest has an invalid repository name: {manifest.Repository}");
return;
}
var gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
var releases = await gitClient.Repository.Release.GetAll(split[0], split[1]).ConfigureAwait(false);
if (releases.Count == 0)
{
Log.Debug("No releases in repo");
return;
}
Version currentVersion;
Version latestVersion;
try
{
currentVersion = new Version(manifest.Version);
latestVersion = new Version(releases[0].TagName);
}
catch
{
Log.Warn("Invalid version number on manifest or GitHub release");
return;
}
if (force || latestVersion > currentVersion)
{
var webClient = new WebClient();
var assets = await gitClient.Repository.Release.GetAllAssets(split[0], split[1], releases[0].Id).ConfigureAwait(false);
foreach (var asset in assets)
{
if (asset.Name.EndsWith(".zip"))
{
Log.Debug(asset.BrowserDownloadUrl);
var localPath = Path.Combine(Path.GetTempPath(), asset.Name);
await webClient.DownloadFileTaskAsync(new Uri(asset.BrowserDownloadUrl), localPath).ConfigureAwait(false);
UnzipPlugin(localPath);
Log.Info($"Downloaded update for {manifest.Repository}");
return;
}
}
}
else
{
Log.Info($"{manifest.Repository} is up to date.");
}
}
public void UnzipPlugin(string zipName)
{
if (!File.Exists(zipName))
return;
MyZipArchive.ExtractToDirectory(zipName, _pluginManager.PluginDir);
}
}
}