From 7373dd37a623bf2dc520e660b5b6442827fc7a66 Mon Sep 17 00:00:00 2001 From: John Gross Date: Thu, 6 Jul 2017 14:44:29 -0700 Subject: [PATCH] Refactor, fix chat scroll, rework automatic update system, remove manual install method, add documentation --- Torch.API/ITorchBase.cs | 71 ++++++++++- Torch.API/ITorchConfig.cs | 39 ++++++- Torch.API/Managers/IManager.cs | 6 + Torch.API/Managers/IMultiplayerManager.cs | 39 +++++++ Torch.API/Managers/INetworkManager.cs | 16 +++ Torch.API/Managers/IPluginManager.cs | 20 +++- Torch.API/ServerState.cs | 18 +++ Torch.Client/Properties/AssemblyInfo.cs | 4 +- Torch.Client/Torch.Client.csproj | 4 + Torch.Client/TorchClient.cs | 1 + Torch.Server/Program.cs | 106 +++++++---------- Torch.Server/Properties/AssemblyInfo.cs | 4 +- Torch.Server/Torch.Server.csproj | 1 - Torch.Server/TorchCli.cs | 41 ------- Torch.Server/TorchConfig.cs | 53 +++++++-- Torch.Server/TorchServer.cs | 32 +++-- Torch.Server/Views/ChatControl.xaml.cs | 12 ++ Torch.Server/Views/TorchUI.xaml | 12 +- Torch.Server/Views/TorchUI.xaml.cs | 6 - Torch/ChatMessage.cs | 2 +- Torch/Collections/ObservableDictionary.cs | 1 + Torch/Managers/FilesystemManager.cs | 51 ++++++++ Torch/Managers/MultiplayerManager.cs | 40 ++++--- .../Managers/NetworkManager/NetworkManager.cs | 5 + Torch/Managers/PluginManager.cs | 65 +++++------ Torch/Managers/UpdateManager.cs | 110 +++++++++++++++++- Torch/Persistent.cs | 20 ++++ Torch/{Updater => }/PluginManifest.cs | 0 Torch/Torch.csproj | 6 +- Torch/TorchBase.cs | 38 ++++-- Torch/Updater/PluginUpdater.cs | 93 --------------- 31 files changed, 594 insertions(+), 322 deletions(-) delete mode 100644 Torch.Server/TorchCli.cs create mode 100644 Torch/Managers/FilesystemManager.cs rename Torch/{Updater => }/PluginManifest.cs (100%) delete mode 100644 Torch/Updater/PluginUpdater.cs diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs index e83aa63..2eecdba 100644 --- a/Torch.API/ITorchBase.cs +++ b/Torch.API/ITorchBase.cs @@ -8,32 +8,99 @@ using VRage.Game.ModAPI; namespace Torch.API { + /// + /// API for Torch functions shared between client and server. + /// public interface ITorchBase { + /// + /// Fired when the session begins loading. + /// event Action SessionLoading; + + /// + /// Fired when the session finishes loading. + /// event Action SessionLoaded; + + /// + /// Fires when the session begins unloading. + /// event Action SessionUnloading; + + /// + /// Fired when the session finishes unloading. + /// event Action SessionUnloaded; + + /// + /// Configuration for the current instance. + /// ITorchConfig Config { get; } + + /// IMultiplayerManager Multiplayer { get; } + + /// IPluginManager Plugins { get; } + + /// + /// The binary version of the current instance. + /// Version TorchVersion { get; } + + /// + /// Invoke an action on the game thread. + /// void Invoke(Action action); + + /// + /// 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. + /// void InvokeBlocking(Action action); + + /// + /// Invoke an action on the game thread asynchronously. + /// Task InvokeAsync(Action action); - string[] RunArgs { get; set; } - bool IsOnGameThread(); + + /// + /// Start the Torch instance. + /// void Start(); + + /// + /// Stop the Torch instance. + /// void Stop(); + + /// + /// Initialize the Torch instance. + /// void Init(); + + /// + /// Get an that is part of the Torch instance. + /// + /// Manager type T GetManager() where T : class, IManager; } + /// + /// API for the Torch server. + /// public interface ITorchServer : ITorchBase { + /// + /// Path of the dedicated instance folder. + /// string InstancePath { get; } } + /// + /// API for the Torch client. + /// public interface ITorchClient : ITorchBase { diff --git a/Torch.API/ITorchConfig.cs b/Torch.API/ITorchConfig.cs index 12d0328..574d837 100644 --- a/Torch.API/ITorchConfig.cs +++ b/Torch.API/ITorchConfig.cs @@ -1,14 +1,47 @@ -namespace Torch +using System.Collections.Generic; + +namespace Torch { public interface ITorchConfig { + /// + /// (server) Name of the instance. + /// string InstanceName { get; set; } + + /// + /// (server) Dedicated instance path. + /// string InstancePath { get; set; } - bool RedownloadPlugins { get; set; } - bool AutomaticUpdates { get; set; } + + /// + /// Enable automatic Torch updates. + /// + bool GetTorchUpdates { get; set; } + + /// + /// Enable automatic Torch updates. + /// + bool GetPluginUpdates { get; set; } + + /// + /// Restart Torch automatically if it crashes. + /// bool RestartOnCrash { get; set; } + + /// + /// Time-out in seconds for the Torch watchdog (to detect a hung session). + /// int TickTimeout { get; set; } + /// + /// A list of plugins that should be installed. + /// + List Plugins { get; } + + /// + /// Saves the config. + /// bool Save(string path = null); } } \ No newline at end of file diff --git a/Torch.API/Managers/IManager.cs b/Torch.API/Managers/IManager.cs index 6274262..e6e3059 100644 --- a/Torch.API/Managers/IManager.cs +++ b/Torch.API/Managers/IManager.cs @@ -6,8 +6,14 @@ using System.Threading.Tasks; namespace Torch.API.Managers { + /// + /// Base interface for Torch managers. + /// public interface IManager { + /// + /// Initializes the manager. Called after Torch is initialized. + /// void Init(); } } diff --git a/Torch.API/Managers/IMultiplayerManager.cs b/Torch.API/Managers/IMultiplayerManager.cs index 4a52844..509360d 100644 --- a/Torch.API/Managers/IMultiplayerManager.cs +++ b/Torch.API/Managers/IMultiplayerManager.cs @@ -6,17 +6,56 @@ using VRage.Game.ModAPI; namespace Torch.API.Managers { + /// + /// Delegate for received messages. + /// + /// Message data. + /// Flag to broadcast message to other players. public delegate void MessageReceivedDel(IChatMessage message, ref bool sendToOthers); + /// + /// API for multiplayer related functions. + /// public interface IMultiplayerManager : IManager { + /// + /// Fired when a player joins. + /// event Action PlayerJoined; + + /// + /// Fired when a player disconnects. + /// event Action PlayerLeft; + + /// + /// Fired when a chat message is received. + /// event MessageReceivedDel MessageReceived; + + /// + /// Send a chat message to all or one specific player. + /// void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Blue); + + /// + /// Kicks the player from the game. + /// void KickPlayer(ulong steamId); + + /// + /// Bans or unbans a player from the game. + /// void BanPlayer(ulong steamId, bool banned = true); + + /// + /// Gets a player by their Steam64 ID or returns null if the player isn't found. + /// IMyPlayer GetPlayerBySteamId(ulong id); + + /// + /// Gets a player by their display name or returns null if the player isn't found. + /// IMyPlayer GetPlayerByName(string name); } } \ No newline at end of file diff --git a/Torch.API/Managers/INetworkManager.cs b/Torch.API/Managers/INetworkManager.cs index dc14f60..548ec10 100644 --- a/Torch.API/Managers/INetworkManager.cs +++ b/Torch.API/Managers/INetworkManager.cs @@ -9,14 +9,30 @@ using VRage.Network; namespace Torch.API.Managers { + /// + /// API for the network intercept. + /// public interface INetworkManager : IManager { + /// + /// Register a network handler. + /// void RegisterNetworkHandler(INetworkHandler handler); } + /// + /// Handler for multiplayer network messages. + /// public interface INetworkHandler { + /// + /// Returns if the handler can process the call site. + /// bool CanHandle(CallSite callSite); + + /// + /// Processes a network message. + /// bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet); } } diff --git a/Torch.API/Managers/IPluginManager.cs b/Torch.API/Managers/IPluginManager.cs index 7d685ba..a1ad100 100644 --- a/Torch.API/Managers/IPluginManager.cs +++ b/Torch.API/Managers/IPluginManager.cs @@ -6,11 +6,29 @@ using VRage.Plugins; namespace Torch.API.Managers { + /// + /// API for the Torch plugin manager. + /// public interface IPluginManager : IManager, IEnumerable { + /// + /// Fired when plugins are loaded. + /// event Action> PluginsLoaded; - List Plugins { get; } + + /// + /// Collection of loaded plugins. + /// + ObservableCollection Plugins { get; } + + /// + /// Updates all loaded plugins. + /// void UpdatePlugins(); + + /// + /// Disposes all loaded plugins. + /// void DisposePlugins(); } } \ No newline at end of file diff --git a/Torch.API/ServerState.cs b/Torch.API/ServerState.cs index b80e8e2..7409680 100644 --- a/Torch.API/ServerState.cs +++ b/Torch.API/ServerState.cs @@ -6,11 +6,29 @@ using System.Threading.Tasks; namespace Torch.API { + /// + /// Used to indicate the state of the dedicated server. + /// public enum ServerState { + /// + /// The server is not running. + /// Stopped, + + /// + /// The server is starting/loading the session. + /// Starting, + + /// + /// The server is running. + /// Running, + + /// + /// The server encountered an error. + /// Error } } diff --git a/Torch.Client/Properties/AssemblyInfo.cs b/Torch.Client/Properties/AssemblyInfo.cs index 9e1b243..0b06134 100644 --- a/Torch.Client/Properties/AssemblyInfo.cs +++ b/Torch.Client/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("1.0.182.329")] -[assembly: AssemblyFileVersion("1.0.182.329")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.186.642")] +[assembly: AssemblyFileVersion("1.0.186.642")] \ No newline at end of file diff --git a/Torch.Client/Torch.Client.csproj b/Torch.Client/Torch.Client.csproj index b56672d..f279cf8 100644 --- a/Torch.Client/Torch.Client.csproj +++ b/Torch.Client/Torch.Client.csproj @@ -45,6 +45,10 @@ ..\packages\NLog.4.4.1\lib\net45\NLog.dll True + + False + ..\GameBinaries\Sandbox.Common.dll + ..\GameBinaries\Sandbox.Game.dll False diff --git a/Torch.Client/TorchClient.cs b/Torch.Client/TorchClient.cs index 92b26b6..350cb60 100644 --- a/Torch.Client/TorchClient.cs +++ b/Torch.Client/TorchClient.cs @@ -9,6 +9,7 @@ using Sandbox; using Sandbox.Engine.Platform; using Sandbox.Engine.Utils; using Sandbox.Game; +using Sandbox.ModAPI; using SpaceEngineers.Game; using Torch.API; using VRage.FileSystem; diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index c1823d3..ecf9a3f 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -34,8 +34,8 @@ namespace Torch.Server private static ITorchServer _server; private static Logger _log = LogManager.GetLogger("Torch"); private static bool _restartOnCrash; - public static bool IsManualInstall; - private static TorchCli _cli; + private static TorchConfig _config; + private static bool _steamCmdDone; /// /// 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. Directory.SetCurrentDirectory(new FileInfo(typeof(Program).Assembly.Location).Directory.ToString()); - IsManualInstall = File.Exists("SpaceEngineersDedicated.exe"); - if (!IsManualInstall) - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + foreach (var file in Directory.GetFiles(Directory.GetCurrentDirectory(), "*.old")) + File.Delete(file); + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; if (!Environment.UserInteractive) @@ -63,50 +63,40 @@ namespace Torch.Server var configName = "TorchConfig.xml"; var configPath = Path.Combine(Directory.GetCurrentDirectory(), configName); - TorchConfig options; if (File.Exists(configName)) { _log.Info($"Loading config {configPath}"); - options = TorchConfig.LoadFrom(configPath); + _config = TorchConfig.LoadFrom(configPath); } else { _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"); - options.InstancePath = Path.GetFullPath("Instance"); - - _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(); - } + _log.Info("Automatic updates enabled."); + RunSteamCmd(); } - //var setupDialog = new FirstTimeSetup { DataContext = options }; - //setupDialog.ShowDialog(); - options.Save(configPath); + _config.Save(configPath); } - _cli = new TorchCli { Config = options }; - if (!_cli.Parse(args)) + if (!_config.Parse(args)) return; - _log.Debug(_cli.ToString()); + _log.Debug(_config.ToString()); - if (!string.IsNullOrEmpty(_cli.WaitForPID)) + if (!string.IsNullOrEmpty(_config.WaitForPID)) { try { - var pid = int.Parse(_cli.WaitForPID); + var pid = int.Parse(_config.WaitForPID); var waitProc = Process.GetProcessById(pid); _log.Warn($"Waiting for process {pid} to exit."); 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) - _log.Warn("Detected manual install, won't attempt to update DS"); - else - { - RunSteamCmd(); - } + RunSteamCmd(); } - RunServer(options, _cli); + RunServer(_config); } private const string STEAMCMD_DIR = "steamcmd"; @@ -142,6 +127,9 @@ quit"; public static void RunSteamCmd() { + if (_steamCmdDone) + return; + var log = LogManager.GetLogger("SteamCMD"); if (!Directory.Exists(STEAMCMD_DIR)) @@ -187,38 +175,40 @@ quit"; log.Info(cmd.StandardOutput.ReadLine()); 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)}"); return; } - if (!string.IsNullOrEmpty(options.Config) && File.Exists(options.Config)) + if (!string.IsNullOrEmpty(config.Config) && File.Exists(config.Config)) { - options = ServerConfig.LoadFrom(options.Config); - parser.ParseArguments(args, options); + config = ServerConfig.LoadFrom(config.Config); + parser.ParseArguments(args, config); }*/ //RestartOnCrash autostart autosave=15 //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 _log.Info($"Installing service '{serviceName}"); var exePath = $"\"{Assembly.GetExecutingAssembly().Location}\""; var createInfo = new ServiceCreateInfo { - Name = options.InstanceName, + Name = config.InstanceName, BinaryPath = exePath, }; _log.Info("Service Installed"); @@ -238,7 +228,7 @@ quit"; return; } - if (options.UninstallService) + if (config.UninstallService) { _log.Info("Uninstalling Torch service"); var startInfo = new ProcessStartInfo @@ -254,18 +244,18 @@ quit"; return; }*/ - _server = new TorchServer(options); + _server = new TorchServer(config); _server.Init(); - if (cli.NoGui || cli.Autostart) + if (config.NoGui || config.Autostart) { new Thread(() => _server.Start()).Start(); } - if (!cli.NoGui) + if (!config.NoGui) { var ui = new TorchUI((TorchServer)_server); - ui.LoadConfig(options); + ui.LoadConfig(config); ui.ShowDialog(); } } @@ -295,17 +285,9 @@ quit"; Thread.Sleep(5000); 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; - _cli.WaitForPID = Process.GetCurrentProcess().Id.ToString(); - Process.Start(exe, _cli.ToString()); + _config.WaitForPID = Process.GetCurrentProcess().Id.ToString(); + Process.Start(exe, _config.ToString()); } //1627 = Function failed during execution. Environment.Exit(1627); diff --git a/Torch.Server/Properties/AssemblyInfo.cs b/Torch.Server/Properties/AssemblyInfo.cs index 9e1b243..0b06134 100644 --- a/Torch.Server/Properties/AssemblyInfo.cs +++ b/Torch.Server/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("1.0.182.329")] -[assembly: AssemblyFileVersion("1.0.182.329")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.186.642")] +[assembly: AssemblyFileVersion("1.0.186.642")] \ No newline at end of file diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index 5fd97a7..df199ad 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -184,7 +184,6 @@ - True diff --git a/Torch.Server/TorchCli.cs b/Torch.Server/TorchCli.cs deleted file mode 100644 index 4112993..0000000 --- a/Torch.Server/TorchCli.cs +++ /dev/null @@ -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; } - } -} diff --git a/Torch.Server/TorchConfig.cs b/Torch.Server/TorchConfig.cs index d8b0da7..16d37a4 100644 --- a/Torch.Server/TorchConfig.cs +++ b/Torch.Server/TorchConfig.cs @@ -7,25 +7,54 @@ using NLog; namespace Torch.Server { - public class TorchConfig : ITorchConfig + public class TorchConfig : CommandLine, ITorchConfig { private static Logger _log = LogManager.GetLogger("Config"); + /// + [Arg("instancepath", "Server data folder where saves and mods are stored.")] public string InstancePath { get; set; } - public string InstanceName { get; set; } -#warning World Path not implemented - public string WorldPath { get; set; } - public bool AutomaticUpdates { get; set; } = true; - public bool RedownloadPlugins { get; set; } + + /// + [XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")] + public bool NoUpdate { get => false; set => GetTorchUpdates = GetPluginUpdates = !value; } + + /// + [XmlIgnore, Arg("update", "Manually check for and install updates.")] + public bool Update { get; set; } + + /// + [XmlIgnore, Arg("autostart", "Start the server immediately.")] + public bool Autostart { get; set; } + + /// + [Arg("restartoncrash", "Automatically restart the server if it crashes.")] public bool RestartOnCrash { get; set; } - /// - /// How long in seconds to wait before automatically resetting a frozen server. - /// + + /// + [XmlIgnore, Arg("nogui", "Do not show the Torch UI.")] + public bool NoGui { get; set; } + + /// + [XmlIgnore, Arg("waitforpid", "Makes Torch wait for another process to exit.")] + public string WaitForPID { get; set; } + + /// + [Arg("instancename", "The name of the Torch instance.")] + public string InstanceName { get; set; } + + /// + public bool GetTorchUpdates { get; set; } = true; + + /// + public bool GetPluginUpdates { get; set; } = true; + + /// public int TickTimeout { get; set; } = 60; - /// - /// A list of plugins to install or update. TODO - /// + + /// public List Plugins { get; set; } = new List(); + internal Point WindowSize { get; set; } = new Point(800, 600); internal Point WindowPosition { get; set; } = new Point(); [NonSerialized] diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index a41eed5..9429a22 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -16,6 +16,7 @@ using Sandbox.Game.Multiplayer; using Sandbox.ModAPI; using SteamSDK; using Torch.API; +using Torch.Managers; using VRage.Dedicated; using VRage.FileSystem; using VRage.Game; @@ -37,7 +38,9 @@ namespace Torch.Server public Thread GameThread { get; private set; } public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } } public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } } + /// public string InstanceName => Config?.InstanceName; + /// public string InstancePath => Config?.InstancePath; private bool _isRunning; @@ -53,6 +56,7 @@ namespace Torch.Server MyFakes.ENABLE_INFINARIO = false; } + /// public override void Init() { base.Init(); @@ -81,7 +85,7 @@ namespace Torch.Server RuntimeHelpers.RunClassConstructor(typeof(MyObjectBuilder_Base).TypeHandle); } - public void InvokeBeforeRun() + private void InvokeBeforeRun() { var contentPath = "Content"; @@ -91,16 +95,7 @@ namespace Torch.Server Log.Debug("MyFileSystem already initialized"); else { - if (Program.IsManualInstall) - { - var rootPath = new FileInfo(MyFileSystem.ExePath).Directory.FullName; - contentPath = Path.Combine(rootPath, "Content"); - } - else - { - MyFileSystem.ExePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DedicatedServer64"); - } - + MyFileSystem.ExePath = Path.Combine(GetManager().TorchDirectory, "DedicatedServer64"); MyFileSystem.Init(contentPath, InstancePath); } @@ -132,14 +127,13 @@ namespace Torch.Server MySandboxGame.Config.Load(); } - /// - /// Start server on the current thread. - /// + /// public override void Start() { if (State != ServerState.Stopped) return; + IsRunning = true; GameThread = Thread.CurrentThread; Config.Save(); State = ServerState.Starting; @@ -196,9 +190,7 @@ namespace Torch.Server Log.Debug("Server watchdog responded"); } - /// - /// Stop the server. - /// + /// public override void Stop() { if (State == ServerState.Stopped) @@ -220,6 +212,12 @@ namespace Torch.Server Log.Info("Server stopped."); _stopHandle.Set(); State = ServerState.Stopped; + IsRunning = false; + } + + public void Restart() + { + } } } diff --git a/Torch.Server/Views/ChatControl.xaml.cs b/Torch.Server/Views/ChatControl.xaml.cs index 5e9ab16..52d4562 100644 --- a/Torch.Server/Views/ChatControl.xaml.cs +++ b/Torch.Server/Views/ChatControl.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -41,6 +42,17 @@ namespace Torch.Server _server = (TorchBase)server; _multiplayer = (MultiplayerManager)server.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) diff --git a/Torch.Server/Views/TorchUI.xaml b/Torch.Server/Views/TorchUI.xaml index c26438a..39506df 100644 --- a/Torch.Server/Views/TorchUI.xaml +++ b/Torch.Server/Views/TorchUI.xaml @@ -5,14 +5,18 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Torch.Server" xmlns:views="clr-namespace:Torch.Server.Views" + xmlns:converters="clr-namespace:Torch.Server.Views.Converters" mc:Ignorable="d" Title="Torch"> + + + public override void Init() + { + Torch.SessionLoaded += OnSessionLoaded; + } + + private void OnSessionLoaded() { if (_init) return; diff --git a/Torch/Managers/PluginManager.cs b/Torch/Managers/PluginManager.cs index f796192..6171645 100644 --- a/Torch/Managers/PluginManager.cs +++ b/Torch/Managers/PluginManager.cs @@ -5,42 +5,30 @@ 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 NLog; -using Sandbox; -using Sandbox.ModAPI; using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; using Torch.Commands; -using Torch.Managers; -using Torch.Updater; -using VRage.Plugins; using VRage.Collections; -using VRage.Library.Collections; namespace Torch.Managers { - public class PluginManager : IPluginManager + /// + public class PluginManager : Manager, IPluginManager { - private readonly ITorchBase _torch; private static Logger _log = LogManager.GetLogger(nameof(PluginManager)); public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); + private UpdateManager _updateManager; - public List Plugins { get; } = new List(); - - public float LastUpdateMs => _lastUpdateMs; - private volatile float _lastUpdateMs; + /// + public ObservableCollection Plugins { get; } = new ObservableCollection(); public event Action> PluginsLoaded; - public PluginManager(ITorchBase torch) + public PluginManager(ITorchBase torchInstance) : base(torchInstance) { - _torch = torch; - if (!Directory.Exists(PluginDir)) Directory.CreateDirectory(PluginDir); } @@ -50,11 +38,8 @@ namespace Torch.Managers /// public void UpdatePlugins() { - var s = Stopwatch.StartNew(); foreach (var plugin in Plugins) plugin.Update(); - s.Stop(); - _lastUpdateMs = (float)s.Elapsed.TotalMilliseconds; } /// @@ -70,40 +55,42 @@ namespace Torch.Managers private void DownloadPlugins() { - _log.Info("Downloading plugins"); - var updater = new PluginUpdater(this); - var folders = Directory.GetDirectories(PluginDir); var taskList = new List(); - 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) { var manifestPath = Path.Combine(folder, "manifest.xml"); if (!File.Exists(manifestPath)) { - _log.Info($"No manifest in {folder}, skipping"); + _log.Debug($"No manifest in {folder}, skipping"); continue; } - _log.Info($"Checking for updates for {folder}"); 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()); - _torch.Config.RedownloadPlugins = false; } - /// - /// Loads and creates instances of all plugins in the folder. - /// - public void Init() + /// + public override void Init() { - var commands = ((TorchBase)_torch).Commands; + _updateManager = Torch.GetManager(); + var commands = Torch.GetManager(); - if (_torch.Config.AutomaticUpdates) + if (Torch.Config.GetPluginUpdates) DownloadPlugins(); else _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)}"); _log.Info($"Loading plugin {plugin.Name} ({plugin.Version})"); - plugin.StoragePath = _torch.Config.InstancePath; + plugin.StoragePath = Torch.Config.InstancePath; Plugins.Add(plugin); commands.RegisterPluginCommands(plugin); @@ -143,8 +130,8 @@ namespace Torch.Managers } } - Plugins.ForEach(p => p.Init(_torch)); - PluginsLoaded?.Invoke(Plugins); + Plugins.ForEach(p => p.Init(Torch)); + PluginsLoaded?.Invoke(Plugins.ToList()); } public IEnumerator GetEnumerator() diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs index a5018a7..0e120cc 100644 --- a/Torch/Managers/UpdateManager.cs +++ b/Torch/Managers/UpdateManager.cs @@ -1,28 +1,128 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.IO.Packaging; using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using NLog; +using Octokit; using SteamSDK; +using Torch.API; namespace Torch.Managers { /// /// Handles updating of the DS and Torch plugins. /// - public class UpdateManager : IDisposable + public class UpdateManager : Manager, IDisposable { 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) + /// + public override void Init() { - + _fsManager = Torch.GetManager(); + CheckAndUpdateTorch(); + } + + private void TimerElapsed(object state) + { + CheckAndUpdateTorch(); + } + + private async Task> GetLatestRelease(string owner, string name) + { + try + { + var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false); + if (latest == null) + return new Tuple(new Version(), null); + + var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip")); + return new Tuple(new Version(latest.TagName ?? "0"), zip?.BrowserDownloadUrl); + } + catch (Exception) + { + _log.Error($"An error occurred getting release information for '{owner}/{name}'"); + return new Tuple(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); + } } /// diff --git a/Torch/Persistent.cs b/Torch/Persistent.cs index 2dd3d3e..ccc5a1b 100644 --- a/Torch/Persistent.cs +++ b/Torch/Persistent.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; @@ -10,12 +12,14 @@ namespace Torch { /// /// Simple class that manages saving to disk using JSON serialization. + /// Can automatically save on changes by implementing in the data class. /// /// Data class type public sealed class Persistent : IDisposable where T : new() { public string Path { get; set; } public T Data { get; private set; } + private Timer _saveTimer; ~Persistent() { @@ -24,8 +28,21 @@ namespace Torch public Persistent(string path, T data = default(T)) { + _saveTimer = new Timer(Callback); Path = path; 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) @@ -65,6 +82,9 @@ namespace Torch { try { + if (Data is INotifyPropertyChanged npc) + npc.PropertyChanged -= OnPropertyChanged; + _saveTimer.Dispose(); Save(); } catch diff --git a/Torch/Updater/PluginManifest.cs b/Torch/PluginManifest.cs similarity index 100% rename from Torch/Updater/PluginManifest.cs rename to Torch/PluginManifest.cs diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 8803230..ea93410 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -81,6 +81,8 @@ + + @@ -160,19 +162,19 @@ + - + - diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs index 7f3f471..ddb996a 100644 --- a/Torch/TorchBase.cs +++ b/Torch/TorchBase.cs @@ -41,22 +41,38 @@ namespace Torch /// Use only if necessary, prefer dependency injection. /// public static ITorchBase Instance { get; private set; } + /// public ITorchConfig Config { get; protected set; } - protected static Logger Log { get; } = LogManager.GetLogger("Torch"); + /// public Version TorchVersion { get; protected set; } + /// public Version GameVersion { get; private set; } + /// public string[] RunArgs { get; set; } + /// public IPluginManager Plugins { get; protected set; } + /// public IMultiplayerManager Multiplayer { get; protected set; } + /// public EntityManager Entities { get; protected set; } + /// public INetworkManager Network { get; protected set; } + /// public CommandManager Commands { get; protected set; } + /// public event Action SessionLoading; + /// public event Action SessionLoaded; + /// public event Action SessionUnloading; + /// public event Action SessionUnloaded; - private readonly List _managers; + /// + /// Common log for the Torch instance. + /// + protected static Logger Log { get; } = LogManager.GetLogger("Torch"); + private readonly List _managers; private bool _init; /// @@ -79,22 +95,25 @@ namespace Torch Network = new NetworkManager(this); Commands = new CommandManager(this); - _managers = new List {Network, Commands, Plugins, Multiplayer, Entities, new ChatManager(this)}; + _managers = new List { new FilesystemManager(this), new UpdateManager(this), Network, Commands, Plugins, Multiplayer, Entities, new ChatManager(this), }; TorchAPI.Instance = this; } + /// public ListReader GetManagers() { return new ListReader(_managers); } + /// public T GetManager() where T : class, IManager { return _managers.FirstOrDefault(m => m is T) as T; } + /// public bool AddManager(T manager) where T : class, IManager { if (_managers.Any(x => x is T)) @@ -104,11 +123,6 @@ namespace Torch return true; } - public bool IsOnGameThread() - { - return Thread.CurrentThread.ManagedThreadId == MySandboxGame.Static.UpdateThread.ManagedThreadId; - } - public async Task SaveGameAsync() { Log.Info("Saving game"); @@ -201,6 +215,7 @@ namespace Torch #endregion + /// public virtual void Init() { Debug.Assert(!_init, "Torch instance is already initialized."); @@ -236,15 +251,14 @@ namespace Torch MySession.OnUnloading += OnSessionUnloading; MySession.OnUnloaded += OnSessionUnloaded; RegisterVRagePlugin(); - + foreach (var manager in _managers) + manager.Init(); _init = true; } private void OnSessionLoading() { Log.Debug("Session loading"); - foreach (var manager in _managers) - manager.Init(); SessionLoading?.Invoke(); } @@ -279,11 +293,13 @@ namespace Torch pluginList.Add(this); } + /// public virtual void Start() { } + /// public virtual void Stop() { } /// diff --git a/Torch/Updater/PluginUpdater.cs b/Torch/Updater/PluginUpdater.cs deleted file mode 100644 index 355bfd3..0000000 --- a/Torch/Updater/PluginUpdater.cs +++ /dev/null @@ -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); - } - } -}