diff --git a/Jenkins/jenkins-grab-se.ps1 b/Jenkins/jenkins-grab-se.ps1 index 7670e02..f94193c 100644 --- a/Jenkins/jenkins-grab-se.ps1 +++ b/Jenkins/jenkins-grab-se.ps1 @@ -1,6 +1,6 @@ pushd -$steamData = "C:/Steam/Data/" +$steamData = "C:/Steam/Data-playtest/" $steamCMDPath = "C:/Steam/steamcmd/" $steamCMDZip = "C:/Steam/steamcmd.zip" @@ -17,6 +17,6 @@ if (!(Test-Path $steamCMDPath)) { } cd "$steamData" -& "$steamCMDPath/steamcmd.exe" "+login anonymous" "+force_install_dir $steamData" "+app_update 298740" "+quit" +& "$steamCMDPath/steamcmd.exe" "+login anonymous" "+force_install_dir $steamData" "+app_update 298740 validate" "+quit" -popd \ No newline at end of file +popd diff --git a/Jenkinsfile b/Jenkinsfile index e554cf4..14b2267 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,7 +22,7 @@ node { stage('Acquire SE') { bat 'powershell -File Jenkins/jenkins-grab-se.ps1' bat 'IF EXIST GameBinaries RMDIR GameBinaries' - bat 'mklink /J GameBinaries "C:/Steam/Data/DedicatedServer64/"' + bat 'mklink /J GameBinaries "C:/Steam/Data-playtest/DedicatedServer64/"' } stage('Acquire NuGet Packages') { @@ -31,7 +31,7 @@ node { stage('Build') { currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim() - if (env.BRANCH_NAME == "master") { + if (env.BRANCH_NAME == "master" || env.BRANCH_NAME == "Patron" || env.BRANCH_NAME == "publictest") { buildMode = "Release" } else { buildMode = "Debug" diff --git a/NLog-user.config b/NLog-user.config new file mode 100644 index 0000000..9b10655 --- /dev/null +++ b/NLog-user.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NLog.config b/NLog.config index 2559bd8..350ac1f 100644 --- a/NLog.config +++ b/NLog.config @@ -4,6 +4,8 @@ + + @@ -15,6 +17,7 @@ + diff --git a/README.md b/README.md index f80cbe2..35ed4cd 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,8 @@ Torch is the successor to SE Server Extender and gives server admins the tools t * Unzip the Torch release into its own directory and run the executable. It will automatically download the SE DS and generate the other necessary files. - If you already have a DS installed you can unzip the Torch files into the folder that contains the DedicatedServer64 folder. -## Torch.Client -* An optional client-side version of Torch. More documentation to come. - # Building To build Torch you must first have a complete SE Dedicated installation somewhere. Before you open the solution, run the Setup batch file and enter the path of that installation's DedicatedServer64 folder. The script will make a symlink to that folder so the Torch solution can find the DLL references it needs. -In both cases you will need to set the InstancePath in TorchConfig.xml to an existing dedicated server instance as Torch can't fully generate it on its own yet. - -# Official Plugins -Install plugins by unzipping them into the 'Plugins' folder which should be in the same location as the Torch files. If it doesn't exist you can simply create it. -* [Essentials](https://github.com/TorchAPI/Essentials): Adds a slew of chat commands and other tools to help manage your server. -* [Concealment](https://github.com/TorchAPI/Concealment): Adds game logic and physics optimizations that significantly improve sim speed. - If you have a more enjoyable server experience because of Torch, please consider supporting us on Patreon. -[![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269)! +[![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269) diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs index b8b2e68..9b1a335 100644 --- a/Torch.API/ITorchBase.cs +++ b/Torch.API/ITorchBase.cs @@ -104,7 +104,7 @@ namespace Torch.API /// /// Restart the Torch instance, blocking until the restart has been performed. /// - void Restart(); + void Restart(bool save = true); /// /// Initializes a save of the game. @@ -154,6 +154,8 @@ namespace Torch.API /// Raised when the server's Init() method has completed. /// event Action Initialized; + + TimeSpan ElapsedPlayTime { get; set; } } /// diff --git a/Torch.API/ITorchConfig.cs b/Torch.API/ITorchConfig.cs index 2b41621..ae7e99d 100644 --- a/Torch.API/ITorchConfig.cs +++ b/Torch.API/ITorchConfig.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Torch { @@ -12,12 +13,17 @@ namespace Torch string InstancePath { get; set; } bool NoGui { get; set; } bool NoUpdate { get; set; } - List Plugins { get; set; } + List Plugins { get; set; } + bool LocalPlugins { get; set; } bool RestartOnCrash { get; set; } bool ShouldUpdatePlugins { get; } bool ShouldUpdateTorch { get; } int TickTimeout { get; set; } string WaitForPID { get; set; } + string ChatName { get; set; } + string ChatColor { get; set; } + string TestPlugin { get; set; } + bool DisconnectOnRestart { get; set; } bool Save(string path = null); } diff --git a/Torch.API/InformationalVersion.cs b/Torch.API/InformationalVersion.cs index 5310234..ef80213 100644 --- a/Torch.API/InformationalVersion.cs +++ b/Torch.API/InformationalVersion.cs @@ -7,36 +7,36 @@ using System.Threading.Tasks; namespace Torch.API { /// - /// Version in the form v#.#.#.#-info + /// Version in the form v#.#.#.#-branch /// public class InformationalVersion { public Version Version { get; set; } - public string[] Information { get; set; } + public string Branch { get; set; } public static bool TryParse(string input, out InformationalVersion version) { version = default(InformationalVersion); var trim = input.TrimStart('v'); - var info = trim.Split('-'); + var info = trim.Split(new[]{'-'}, 2); if (!Version.TryParse(info[0], out Version result)) return false; version = new InformationalVersion { Version = result }; if (info.Length > 1) - version.Information = info.Skip(1).ToArray(); - + version.Branch = info[1]; + return true; } /// public override string ToString() { - if (Information == null || Information.Length == 0) + if (Branch == null) return $"v{Version}"; - return $"v{Version}-{string.Join("-", Information)}"; + return $"v{Version}-{string.Join("-", Branch)}"; } public static explicit operator InformationalVersion(Version v) @@ -48,5 +48,15 @@ namespace Torch.API { return v.Version; } + + public static bool operator >(InformationalVersion lhs, InformationalVersion rhs) + { + return lhs.Version > rhs.Version; + } + + public static bool operator <(InformationalVersion lhs, InformationalVersion rhs) + { + return lhs.Version < rhs.Version; + } } } diff --git a/Torch.API/Managers/IChatManagerClient.cs b/Torch.API/Managers/IChatManagerClient.cs index 2be2fc8..5d6adf7 100644 --- a/Torch.API/Managers/IChatManagerClient.cs +++ b/Torch.API/Managers/IChatManagerClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Sandbox.Engine.Multiplayer; +using Sandbox.Game.Gui; using Sandbox.Game.Multiplayer; using VRage.Game; using VRage.Network; @@ -28,6 +29,8 @@ namespace Torch.API.Managers AuthorSteamId = null; Author = author; Message = message; + Channel = ChatChannel.Global; + Target = 0; Font = font; } @@ -38,12 +41,14 @@ namespace Torch.API.Managers /// Author's steam ID /// Message /// Font - public TorchChatMessage(string author, ulong authorSteamId, string message, string font = MyFontEnum.Blue) + public TorchChatMessage(string author, ulong authorSteamId, string message, ChatChannel channel, long target, string font = MyFontEnum.Blue) { Timestamp = DateTime.Now; AuthorSteamId = authorSteamId; Author = author; Message = message; + Channel = channel; + Target = target; Font = font; } @@ -53,12 +58,14 @@ namespace Torch.API.Managers /// Author's steam ID /// Message /// Font - public TorchChatMessage(ulong authorSteamId, string message, string font = MyFontEnum.Blue) + public TorchChatMessage(ulong authorSteamId, string message, ChatChannel channel, long target, string font = MyFontEnum.Blue) { Timestamp = DateTime.Now; AuthorSteamId = authorSteamId; Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player"; Message = message; + Channel = channel; + Target = target; Font = font; } @@ -79,6 +86,14 @@ namespace Torch.API.Managers /// public readonly string Message; /// + /// The chat channel the message is part of. + /// + public readonly ChatChannel Channel; + /// + /// The intended recipient of the message. + /// + public readonly long Target; + /// /// The font, or null if default. /// public readonly string Font; diff --git a/Torch.API/Managers/IChatManagerServer.cs b/Torch.API/Managers/IChatManagerServer.cs index 52bd75f..3803b58 100644 --- a/Torch.API/Managers/IChatManagerServer.cs +++ b/Torch.API/Managers/IChatManagerServer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using VRage.Collections; using VRage.Network; namespace Torch.API.Managers @@ -41,5 +42,24 @@ namespace Torch.API.Managers /// Font to use /// Player to send the message to, or everyone by default void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0); + + /// + /// Mute user from global chat. + /// + /// + /// + bool MuteUser(ulong steamId); + + /// + /// Unmute user from global chat. + /// + /// + /// + bool UnmuteUser(ulong steamId); + + /// + /// Users which are not allowed to chat. + /// + HashSetReader MutedUsers { get; } } } diff --git a/Torch.API/Plugins/ITorchPlugin.cs b/Torch.API/Plugins/ITorchPlugin.cs index 35a34df..fd7c83f 100644 --- a/Torch.API/Plugins/ITorchPlugin.cs +++ b/Torch.API/Plugins/ITorchPlugin.cs @@ -34,5 +34,22 @@ namespace Torch.API.Plugins /// This is called on the game thread after each tick. /// void Update(); + + /// + /// Plugin's enabled state. Mainly for UI niceness + /// + PluginState State { get; } + } + + public enum PluginState + { + NotInitialized, + DisabledError, + DisabledUser, + UpdateRequired, + UninstallRequested, + NotInstalled, + MissingDependency, + Enabled } } diff --git a/Torch.API/Session/ITorchSessionManager.cs b/Torch.API/Session/ITorchSessionManager.cs index bfa3b88..1b13e95 100644 --- a/Torch.API/Session/ITorchSessionManager.cs +++ b/Torch.API/Session/ITorchSessionManager.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using Torch.API.Managers; +using VRage.Game; namespace Torch.API.Session { @@ -47,5 +49,29 @@ namespace Torch.API.Session /// true if removed, false if not present /// If the factory is null bool RemoveFactory(SessionManagerFactoryDel factory); + + /// + /// Add a mod to be injected into client's world download. + /// + /// + /// + bool AddOverrideMod(ulong modId); + + /// + /// Removes a mod from the injected mod list. + /// + /// + /// + bool RemoveOverrideMod(ulong modId); + + /// + /// List over mods that will be injected into client world downloads. + /// + IReadOnlyCollection OverrideMods { get; } + + /// + /// Event raised when injected mod list changes. + /// + event Action OverrideModsChanged; } } diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index c7137e0..028a6bb 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -31,6 +31,7 @@ prompt MinimumRecommendedRules.ruleset $(SolutionDir)\bin\x64\Release\Torch.API.xml + 1591 @@ -38,6 +39,10 @@ ..\GameBinaries\HavokWrapper.dll False + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + True + ..\packages\NLog.4.4.12\lib\net45\NLog.dll True @@ -188,6 +193,8 @@ + + diff --git a/Torch.API/WebAPI/JenkinsQuery.cs b/Torch.API/WebAPI/JenkinsQuery.cs new file mode 100644 index 0000000..d4620e5 --- /dev/null +++ b/Torch.API/WebAPI/JenkinsQuery.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NLog; + +namespace Torch.API.WebAPI +{ + public class JenkinsQuery + { + private const string BRANCH_QUERY = "https://build.torchapi.net/job/Torch/job/Torch/job/{0}/" + API_PATH; + private const string ARTIFACT_PATH = "artifact/bin/torch-server.zip"; + private const string API_PATH = "api/json"; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private static JenkinsQuery _instance; + public static JenkinsQuery Instance => _instance ?? (_instance = new JenkinsQuery()); + private HttpClient _client; + + private JenkinsQuery() + { + _client = new HttpClient(); + } + + public async Task GetLatestVersion(string branch) + { + var h = await _client.GetAsync(string.Format(BRANCH_QUERY, branch)); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Branch query failed with code {h.StatusCode}"); + if(h.StatusCode == HttpStatusCode.NotFound) + Log.Error("This likely means you're trying to update a branch that is not public on Jenkins. Sorry :("); + return null; + } + + string r = await h.Content.ReadAsStringAsync(); + + BranchResponse response; + try + { + response = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize branch response!"); + return null; + } + + h = await _client.GetAsync($"{response.LastStableBuild.URL}{API_PATH}"); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Job query failed with code {h.StatusCode}"); + return null; + } + + r = await h.Content.ReadAsStringAsync(); + + Job job; + try + { + job = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize job response!"); + return null; + } + return job; + } + + public async Task DownloadRelease(Job job, string path) + { + var h = await _client.GetAsync(job.URL + ARTIFACT_PATH); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Job download failed with code {h.StatusCode}"); + return false; + } + var s = await h.Content.ReadAsStreamAsync(); + using (var fs = new FileStream(path, FileMode.Create)) + { + await s.CopyToAsync(fs); + await fs.FlushAsync(); + } + return true; + } + + } + + public class BranchResponse + { + public string Name; + public string URL; + public Build LastBuild; + public Build LastStableBuild; + } + + public class Build + { + public int Number; + public string URL; + } + + public class Job + { + public int Number; + public bool Building; + public string Description; + public string Result; + public string URL; + private InformationalVersion _version; + + public InformationalVersion Version + { + get + { + if (_version == null) + InformationalVersion.TryParse(Description, out _version); + + return _version; + } + } + } +} diff --git a/Torch.API/WebAPI/PluginQuery.cs b/Torch.API/WebAPI/PluginQuery.cs new file mode 100644 index 0000000..a13e5a8 --- /dev/null +++ b/Torch.API/WebAPI/PluginQuery.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NLog; + +namespace Torch.API.WebAPI +{ + public class PluginQuery + { + private const string ALL_QUERY = "https://torchapi.net/api/plugins"; + private const string PLUGIN_QUERY = "https://torchapi.net/api/plugins/{0}"; + private readonly HttpClient _client; + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private static PluginQuery _instance; + public static PluginQuery Instance => _instance ?? (_instance = new PluginQuery()); + + private PluginQuery() + { + _client = new HttpClient(); + } + + public async Task QueryAll() + { + var h = await _client.GetAsync(ALL_QUERY); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Plugin query returned response {h.StatusCode}"); + return null; + } + + var r = await h.Content.ReadAsStringAsync(); + + PluginResponse response; + try + { + response = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize plugin query response!"); + return null; + } + return response; + } + + public async Task QueryOne(Guid guid) + { + return await QueryOne(guid.ToString()); + } + + public async Task QueryOne(string guid) + { + + var h = await _client.GetAsync(string.Format(PLUGIN_QUERY, guid)); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Plugin query returned response {h.StatusCode}"); + return null; + } + + var r = await h.Content.ReadAsStringAsync(); + + PluginFullItem response; + try + { + response = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize plugin query response!"); + return null; + } + return response; + } + + public async Task DownloadPlugin(Guid guid, string path = null) + { + return await DownloadPlugin(guid.ToString(), path); + } + + public async Task DownloadPlugin(string guid, string path = null) + { + var item = await QueryOne(guid); + return await DownloadPlugin(item, path); + } + + public async Task DownloadPlugin(PluginFullItem item, string path = null) + { + try + { + path = path ?? $"Plugins\\{item.Name}.zip"; + string relpath = Path.GetDirectoryName(path); + + Directory.CreateDirectory(relpath); + + var h = await _client.GetAsync(string.Format(PLUGIN_QUERY, item.ID)); + string res = await h.Content.ReadAsStringAsync(); + var response = JsonConvert.DeserializeObject(res); + if (response.Versions.Length == 0) + { + Log.Error($"Selected plugin {item.Name} does not have any versions to download!"); + return false; + } + var version = response.Versions.FirstOrDefault(v => v.Version == response.LatestVersion); + if (version == null) + { + Log.Error($"Could not find latest version for selected plugin {item.Name}"); + return false; + } + var s = await _client.GetStreamAsync(version.URL); + + if(File.Exists(path)) + File.Delete(path); + + using (var f = File.Create(path)) + { + await s.CopyToAsync(f); + await f.FlushAsync(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to download plugin!"); + } + + return true; + } + } + + public class PluginResponse + { + public PluginItem[] Plugins; + public int Count; + } + + public class PluginItem + { + public string ID; + public string Name; + public string Author; + public string Description; + public string LatestVersion; + + public override string ToString() + { + return Name; + } + } + + public class PluginFullItem : PluginItem + { + public VersionItem[] Versions; + } + + public class VersionItem + { + public string Version; + public string Note; + public bool IsBeta; + public string URL; + } +} diff --git a/Torch.API/packages.config b/Torch.API/packages.config index 9d88a31..fbd3f14 100644 --- a/Torch.API/packages.config +++ b/Torch.API/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/Torch.Client.Tests/Torch.Client.Tests.csproj b/Torch.Client.Tests/Torch.Client.Tests.csproj index f57c10c..c1dc43a 100644 --- a/Torch.Client.Tests/Torch.Client.Tests.csproj +++ b/Torch.Client.Tests/Torch.Client.Tests.csproj @@ -86,6 +86,7 @@ + diff --git a/Torch.Client.Tests/app.config b/Torch.Client.Tests/app.config new file mode 100644 index 0000000..a73892d --- /dev/null +++ b/Torch.Client.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Torch.Client/Torch.Client.csproj b/Torch.Client/Torch.Client.csproj index 40f2506..1ecb37a 100644 --- a/Torch.Client/Torch.Client.csproj +++ b/Torch.Client/Torch.Client.csproj @@ -36,6 +36,7 @@ MinimumRecommendedRules.ruleset true $(SolutionDir)\bin\x64\Release\Torch.Client.xml + 1591 torchicon.ico @@ -146,6 +147,7 @@ ResXFileCodeGenerator Resources.Designer.cs + SettingsSingleFileGenerator diff --git a/Torch.Client/TorchClientConfig.cs b/Torch.Client/TorchClientConfig.cs index fda35e0..ba6e5b3 100644 --- a/Torch.Client/TorchClientConfig.cs +++ b/Torch.Client/TorchClientConfig.cs @@ -23,6 +23,8 @@ namespace Torch.Client public bool NoGui { get; set; } = false; public bool RestartOnCrash { get; set; } = false; public string WaitForPID { get; set; } = null; + public string ChatName { get; set; } + public string ChatColor { get; set; } public bool Save(string path = null) { diff --git a/Torch.Client/app.config b/Torch.Client/app.config new file mode 100644 index 0000000..a73892d --- /dev/null +++ b/Torch.Client/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Torch.Mod/Messages/JoinServerMessage.cs b/Torch.Mod/Messages/JoinServerMessage.cs new file mode 100644 index 0000000..ffc8ab0 --- /dev/null +++ b/Torch.Mod/Messages/JoinServerMessage.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; +using ProtoBuf; +using Sandbox.ModAPI; + +namespace Torch.Mod.Messages +{ + [ProtoContract] + public class JoinServerMessage : MessageBase + { + [ProtoMember(201)] + public int Delay; + [ProtoMember(202)] + public string Address; + + private JoinServerMessage() + { + + } + + public JoinServerMessage(string address) + { + Address = address; + } + + public JoinServerMessage(string address, int delay) + { + Address = address; + Delay = delay; + } + + public override void ProcessClient() + { + if (TorchModCore.Debug) + { + MyAPIGateway.Utilities.ShowMessage("Torch", $"Joining server {Address} with delay {Delay}"); + } + + if (Delay <= 0) + { + MyAPIGateway.Multiplayer.JoinServer(Address); + return; + } + + MyAPIGateway.Parallel.StartBackground(() => + { + MyAPIGateway.Parallel.Sleep(Delay); + MyAPIGateway.Multiplayer.JoinServer(Address); + }); + } + + public override void ProcessServer() + { + } + } +} diff --git a/Torch.Mod/Messages/MessageBase.cs b/Torch.Mod/Messages/MessageBase.cs index 3a00738..55bc140 100644 --- a/Torch.Mod/Messages/MessageBase.cs +++ b/Torch.Mod/Messages/MessageBase.cs @@ -11,6 +11,7 @@ namespace Torch.Mod.Messages [ProtoInclude(1, typeof(DialogMessage))] [ProtoInclude(2, typeof(NotificationMessage))] [ProtoInclude(3, typeof(VoxelResetMessage))] + [ProtoInclude(4, typeof(JoinServerMessage))] #endregion [ProtoContract] @@ -28,7 +29,7 @@ namespace Torch.Mod.Messages internal ulong[] Ignore; internal byte[] CompressedData; } - + public enum MessageTarget { /// diff --git a/Torch.Mod/ModCommunication.cs b/Torch.Mod/ModCommunication.cs index 2c7b492..76be249 100644 --- a/Torch.Mod/ModCommunication.cs +++ b/Torch.Mod/ModCommunication.cs @@ -10,6 +10,7 @@ using Torch.Mod.Messages; using VRage; using VRage.Collections; using VRage.Game.ModAPI; +using VRage.Network; using VRage.Utils; using Task = ParallelTasks.Task; @@ -50,6 +51,10 @@ namespace Torch.Mod { var m = _messagePool.Get(); m.CompressedData = bytes; +#if TORCH + m.SenderId = MyEventContext.Current.Sender.Value; +#endif + _processing.Add(m); } @@ -59,10 +64,19 @@ namespace Torch.Mod { try { - var m = _processing.Take(); + MessageBase m; + try + { + m = _processing.Take(); + } + catch + { + continue; + } + MyLog.Default.WriteLineAndConsole($"Processing message: {m.GetType().Name}"); - if (m is IncomingMessage) + if (m is IncomingMessage) //process incoming messages { MessageBase i; try @@ -78,50 +92,55 @@ namespace Torch.Mod continue; } + if (TorchModCore.Debug) + MyAPIGateway.Utilities.ShowMessage("Torch", $"Received message of type {i.GetType().Name}"); + if (MyAPIGateway.Multiplayer.IsServer) i.ProcessServer(); else i.ProcessClient(); } - else + else //process outgoing messages { + if (TorchModCore.Debug) + MyAPIGateway.Utilities.ShowMessage("Torch", $"Sending message of type {m.GetType().Name}"); + var b = MyAPIGateway.Utilities.SerializeToBinary(m); m.CompressedData = MyCompression.Compress(b); - MyAPIGateway.Utilities.InvokeOnGameThread(() => + switch (m.TargetType) { + case MessageTarget.Single: + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, m.Target); + break; + case MessageTarget.Server: + MyAPIGateway.Multiplayer.SendMessageToServer(NET_ID, m.CompressedData); + break; + case MessageTarget.AllClients: + MyAPIGateway.Players.GetPlayers(_playerCache); + foreach (var p in _playerCache) + { + if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId) + continue; + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId); + } - switch (m.TargetType) - { - case MessageTarget.Single: - MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, m.Target); - break; - case MessageTarget.Server: - MyAPIGateway.Multiplayer.SendMessageToServer(NET_ID, m.CompressedData); - break; - case MessageTarget.AllClients: - MyAPIGateway.Players.GetPlayers(_playerCache); - foreach (var p in _playerCache) - { - if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId) - continue; - MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId); - } - break; - case MessageTarget.AllExcept: - MyAPIGateway.Players.GetPlayers(_playerCache); - foreach (var p in _playerCache) - { - if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId || m.Ignore.Contains(p.SteamUserId)) - continue; - MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId); - } - break; - default: - throw new Exception(); - } - _playerCache.Clear(); - }); + break; + case MessageTarget.AllExcept: + MyAPIGateway.Players.GetPlayers(_playerCache); + foreach (var p in _playerCache) + { + if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId || m.Ignore.Contains(p.SteamUserId)) + continue; + MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId); + } + + break; + default: + throw new Exception(); + } + + _playerCache.Clear(); } } catch (Exception ex) @@ -130,7 +149,7 @@ namespace Torch.Mod } } - MyLog.Default.WriteLineAndConsole("TORCH MOD: COMMUNICATION THREAD: EXIT SIGNAL RECEIVED!"); + MyLog.Default.WriteLineAndConsole("TORCH MOD: INFO: Communication thread shut down successfully! THIS IS NOT AN ERROR"); //exit signal received. Clean everything and GTFO _processing?.Dispose(); _processing = null; diff --git a/Torch.Mod/Torch.Mod.projitems b/Torch.Mod/Torch.Mod.projitems index 632d99a..8ab2e95 100644 --- a/Torch.Mod/Torch.Mod.projitems +++ b/Torch.Mod/Torch.Mod.projitems @@ -10,6 +10,7 @@ + diff --git a/Torch.Mod/TorchModCore.cs b/Torch.Mod/TorchModCore.cs index 83f3a33..877ca8c 100644 --- a/Torch.Mod/TorchModCore.cs +++ b/Torch.Mod/TorchModCore.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Sandbox.ModAPI; using VRage.Game.Components; namespace Torch.Mod @@ -12,6 +13,7 @@ namespace Torch.Mod { public const ulong MOD_ID = 1406994352; private static bool _init; + public static bool Debug; public override void UpdateAfterSimulation() { @@ -20,12 +22,24 @@ namespace Torch.Mod _init = true; ModCommunication.Register(); + MyAPIGateway.Utilities.MessageEntered += Utilities_MessageEntered; + } + + private void Utilities_MessageEntered(string messageText, ref bool sendToOthers) + { + if (messageText == "@!debug") + { + Debug = !Debug; + MyAPIGateway.Utilities.ShowMessage("Torch", $"Debug: {Debug}"); + sendToOthers = false; + } } protected override void UnloadData() { try { + MyAPIGateway.Utilities.MessageEntered -= Utilities_MessageEntered; ModCommunication.Unregister(); } catch diff --git a/Torch.Server.Tests/Torch.Server.Tests.csproj b/Torch.Server.Tests/Torch.Server.Tests.csproj index 157bedf..cf95a9a 100644 --- a/Torch.Server.Tests/Torch.Server.Tests.csproj +++ b/Torch.Server.Tests/Torch.Server.Tests.csproj @@ -92,6 +92,7 @@ + diff --git a/Torch.Server.Tests/app.config b/Torch.Server.Tests/app.config new file mode 100644 index 0000000..a73892d --- /dev/null +++ b/Torch.Server.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Torch.Server/Initializer.cs b/Torch.Server/Initializer.cs index 3e83980..a63d20b 100644 --- a/Torch.Server/Initializer.cs +++ b/Torch.Server/Initializer.cs @@ -16,7 +16,6 @@ using NLog.Targets; using Sandbox.Engine.Utils; using Torch.Utils; using VRage.FileSystem; -using VRage.Library.Exceptions; namespace Torch.Server { @@ -55,6 +54,15 @@ quit"; AppDomain.CurrentDomain.UnhandledException += HandleException; #endif +#if DEBUG + //enables logging debug messages when built in debug mode. Amazing. + LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "main"); + LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "console"); + LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "wpf"); + LogManager.ReconfigExistingLoggers(); + Log.Debug("Debug logging enabled."); +#endif + // This is what happens when Keen is bad and puts extensions into the System namespace. if (!Enumerable.Contains(args, "-noupdate")) RunSteamCmd(); @@ -62,10 +70,30 @@ quit"; var basePath = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); var apiSource = Path.Combine(basePath, "DedicatedServer64", "steam_api64.dll"); var apiTarget = Path.Combine(basePath, "steam_api64.dll"); - + if (!File.Exists(apiTarget)) + { File.Copy(apiSource, apiTarget); + } + else if (File.GetLastWriteTime(apiTarget) < File.GetLastWriteTime(apiSource)) + { + File.Delete(apiTarget); + File.Copy(apiSource, apiTarget); + } + var havokSource = Path.Combine(basePath, "DedicatedServer64", "Havok.dll"); + var havokTarget = Path.Combine(basePath, "Havok.dll"); + + if (!File.Exists(havokTarget)) + { + File.Copy(havokSource, havokTarget); + } + else if (File.GetLastWriteTime(havokTarget) < File.GetLastWriteTime(havokSource)) + { + File.Delete(havokTarget); + File.Copy(havokSource, havokTarget); + } + _config = InitConfig(); if (!_config.Parse(args)) return false; @@ -97,28 +125,34 @@ quit"; public void Run() { _server = new TorchServer(_config); - var init = Task.Run(() => _server.Init()).ContinueWith(x => - { - if (!x.IsFaulted) - return; - Log.Error("Error initializing server."); - LogException(x.Exception); - }); - if (!_config.NoGui) + if (_config.NoGui) { - if (_config.Autostart) - init.ContinueWith(x => _server.Start()); - - Log.Info("Showing UI"); - Console.SetOut(TextWriter.Null); - NativeMethods.FreeConsole(); - new TorchUI(_server).ShowDialog(); + _server.Init(); + _server.Start(); } else { - init.Wait(); - _server.Start(); +#if !DEBUG + if (!_config.IndependentConsole) + { + Console.SetOut(TextWriter.Null); + NativeMethods.FreeConsole(); + } +#endif + + var gameThread = new Thread(() => + { + _server.Init(); + + if (_config.Autostart) + _server.Start(); + }); + + gameThread.Start(); + + var ui = new TorchUI(_server); + ui.ShowDialog(); } } @@ -164,9 +198,10 @@ quit"; File.Delete(STEAMCMD_ZIP); log.Info("SteamCMD downloaded successfully!"); } - catch + catch (Exception e) { log.Error("Failed to download SteamCMD, unable to update the DS."); + log.Error(e); return; } } @@ -180,43 +215,53 @@ quit"; StandardOutputEncoding = Encoding.ASCII }; var cmd = Process.Start(steamCmdProc); - + // ReSharper disable once PossibleNullReferenceException while (!cmd.HasExited) { - log.Info(cmd.StandardOutput.ReadLine()); + log.Info(cmd.StandardOutput.ReadToEnd()); Thread.Sleep(100); } } private void LogException(Exception ex) - { + { + if (ex is AggregateException ag) + { + foreach (var e in ag.InnerExceptions) + LogException(e); + + return; + } + + Log.Fatal(ex); + + if (ex is ReflectionTypeLoadException extl) + { + foreach (var exl in extl.LoaderExceptions) + LogException(exl); + + return; + } + if (ex.InnerException != null) { LogException(ex.InnerException); } - - Log.Fatal(ex); - - if (ex is ReflectionTypeLoadException exti) - foreach (Exception exl in exti.LoaderExceptions) - LogException(exl); - - if (ex is AggregateException ag) - foreach (Exception e in ag.InnerExceptions) - LogException(e); } private void HandleException(object sender, UnhandledExceptionEventArgs e) { + _server.FatalException = true; var ex = (Exception)e.ExceptionObject; LogException(ex); if (MyFakes.ENABLE_MINIDUMP_SENDING) { string path = Path.Combine(MyFileSystem.UserDataPath, "Minidump.dmp"); Log.Info($"Generating minidump at {path}"); - MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo; - MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present); + Log.Error("Keen broke the minidump, sorry."); + //MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo; + //MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present); } LogManager.Flush(); if (_config.RestartOnCrash) @@ -235,4 +280,4 @@ quit"; Process.GetCurrentProcess().Kill(); } } -} \ No newline at end of file +} diff --git a/Torch.Server/Managers/InstanceManager.cs b/Torch.Server/Managers/InstanceManager.cs index 5011932..92f14e9 100644 --- a/Torch.Server/Managers/InstanceManager.cs +++ b/Torch.Server/Managers/InstanceManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; @@ -14,6 +15,7 @@ using Sandbox.Game; using Sandbox.Game.Gui; using Torch.API; using Torch.API.Managers; +using Torch.Collections; using Torch.Managers; using Torch.Mod; using Torch.Server.ViewModels; @@ -69,8 +71,16 @@ namespace Torch.Server.Managers foreach (var f in worldFolders) { - if (!string.IsNullOrEmpty(f) && File.Exists(Path.Combine(f, "Sandbox.sbc"))) - DedicatedConfig.Worlds.Add(new WorldViewModel(f)); + try + { + if (!string.IsNullOrEmpty(f) && File.Exists(Path.Combine(f, "Sandbox.sbc"))) + DedicatedConfig.Worlds.Add(new WorldViewModel(f)); + } + catch (Exception ex) + { + Log.Error("Failed to load world at path: " + f); + continue; + } } if (DedicatedConfig.Worlds.Count == 0) @@ -87,14 +97,32 @@ namespace Torch.Server.Managers public void SelectWorld(string worldPath, bool modsOnly = true) { DedicatedConfig.LoadWorld = worldPath; - DedicatedConfig.SelectedWorld = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == worldPath); + + var worldInfo = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == worldPath); + try + { + if (worldInfo?.Checkpoint == null) + { + worldInfo = new WorldViewModel(worldPath); + DedicatedConfig.Worlds.Add(worldInfo); + } + } + catch (Exception ex) + { + Log.Error("Failed to load world at path: " + worldPath); + DedicatedConfig.LoadWorld = null; + return; + } + + DedicatedConfig.SelectedWorld = worldInfo; if (DedicatedConfig.SelectedWorld?.Checkpoint != null) { DedicatedConfig.Mods.Clear(); //remove the Torch mod to avoid running multiple copies of it - DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); - foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) - DedicatedConfig.Mods.Add(m.PublishedFileId); + DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); + foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods) + DedicatedConfig.Mods.Add(new ModItemInfo(m)); + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); } } @@ -106,9 +134,10 @@ namespace Torch.Server.Managers { DedicatedConfig.Mods.Clear(); //remove the Torch mod to avoid running multiple copies of it - DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); - foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) - DedicatedConfig.Mods.Add(m.PublishedFileId); + DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); + foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods) + DedicatedConfig.Mods.Add(new ModItemInfo(m)); + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); } } @@ -119,17 +148,16 @@ namespace Torch.Server.Managers private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true) { - var sb = new StringBuilder(); - foreach (var mod in world.Checkpoint.Mods) - sb.AppendLine(mod.PublishedFileId.ToString()); - - DedicatedConfig.Mods = world.Checkpoint.Mods.Select(x => x.PublishedFileId).ToList(); + var mods = new MtObservableList(); + foreach (var mod in world.WorldConfiguration.Mods) + mods.Add(new ModItemInfo(mod)); + DedicatedConfig.Mods = mods; Log.Debug("Loaded mod list from world"); if (!modsOnly) - DedicatedConfig.SessionSettings = world.Checkpoint.Settings; + DedicatedConfig.SessionSettings = world.WorldConfiguration.Settings; } private void ImportWorldConfig(bool modsOnly = true) @@ -151,7 +179,10 @@ namespace Torch.Server.Managers return; } - DedicatedConfig.Mods = checkpoint.Mods.Select(x => x.PublishedFileId).ToList(); + var mods = new MtObservableList(); + foreach (var mod in checkpoint.Mods) + mods.Add(new ModItemInfo(mod)); + DedicatedConfig.Mods = mods; Log.Debug("Loaded mod list from world"); @@ -167,29 +198,33 @@ namespace Torch.Server.Managers public void SaveConfig() { + if (((TorchServer)Torch).HasRun) + { + Log.Warn("Checkpoint cache is stale, not saving dedicated config."); + return; + } + DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME)); Log.Info("Saved dedicated config."); try { - var sandboxPath = Path.Combine(DedicatedConfig.LoadWorld, "Sandbox.sbc"); - MyObjectBuilderSerializer.DeserializeXML(sandboxPath, out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes); - if (checkpoint == null) + var world = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == DedicatedConfig.LoadWorld) ?? new WorldViewModel(DedicatedConfig.LoadWorld); + + world.Checkpoint.SessionName = DedicatedConfig.WorldName; + world.WorldConfiguration.Settings = DedicatedConfig.SessionSettings; + world.WorldConfiguration.Mods.Clear(); + + foreach (var mod in DedicatedConfig.Mods) { - Log.Error($"Failed to load {DedicatedConfig.LoadWorld}, checkpoint null ({sizeInBytes} bytes, instance {Torch.Config.InstancePath})"); - return; + var savedMod = new MyObjectBuilder_Checkpoint.ModItem(mod.Name, mod.PublishedFileId, mod.FriendlyName); + savedMod.IsDependency = mod.IsDependency; + world.WorldConfiguration.Mods.Add(savedMod); } + Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync()); - checkpoint.SessionName = DedicatedConfig.WorldName; - checkpoint.Settings = DedicatedConfig.SessionSettings; - checkpoint.Mods.Clear(); - - foreach (var modId in DedicatedConfig.Mods) - checkpoint.Mods.Add(new MyObjectBuilder_Checkpoint.ModItem(modId)); + world.SaveSandbox(); - MyObjectBuilderSerializer.SerializeXML(sandboxPath, false, checkpoint); - - //MyLocalCache.SaveCheckpoint(checkpoint, DedicatedConfig.LoadWorld); Log.Info("Saved world config."); } catch (Exception e) @@ -223,35 +258,63 @@ namespace Torch.Server.Managers public string WorldPath { get; } public long WorldSizeKB { get; } private string _checkpointPath; + private string _worldConfigPath; public CheckpointViewModel Checkpoint { get; private set; } + + public WorldConfigurationViewModel WorldConfiguration { get; private set; } public WorldViewModel(string worldPath) { - WorldPath = worldPath; - WorldSizeKB = new DirectoryInfo(worldPath).GetFiles().Sum(x => x.Length) / 1024; - _checkpointPath = Path.Combine(WorldPath, "Sandbox.sbc"); - FolderName = Path.GetFileName(worldPath); - BeginLoadCheckpoint(); + try + { + WorldPath = worldPath; + WorldSizeKB = new DirectoryInfo(worldPath).GetFiles().Sum(x => x.Length) / 1024; + _checkpointPath = Path.Combine(WorldPath, "Sandbox.sbc"); + _worldConfigPath = Path.Combine(WorldPath, "Sandbox_config.sbc"); + FolderName = Path.GetFileName(worldPath); + LoadSandbox(); + } + catch (ArgumentException ex) + { + Log.Error($"World view model failed to load the path: {worldPath} Please ensure this is a valid path."); + throw; //rethrow to be handled further up the stack + } } - public async Task SaveCheckpointAsync() + public void SaveSandbox() { - await Task.Run(() => - { - using (var f = File.Open(_checkpointPath, FileMode.Create)) - MyObjectBuilderSerializer.SerializeXML(f, Checkpoint); - }); + using (var f = File.Open(_checkpointPath, FileMode.Create)) + MyObjectBuilderSerializer.SerializeXML(f, Checkpoint); + + using (var f = File.Open(_worldConfigPath, FileMode.Create)) + MyObjectBuilderSerializer.SerializeXML(f, WorldConfiguration); } - private void BeginLoadCheckpoint() + private void LoadSandbox() { - //Task.Run(() => + MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint); + Checkpoint = new CheckpointViewModel(checkpoint); + + // migrate old saves + if (File.Exists(_worldConfigPath)) { - Log.Info($"Preloading checkpoint {_checkpointPath}"); - MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint); - Checkpoint = new CheckpointViewModel(checkpoint); - OnPropertyChanged(nameof(Checkpoint)); - }//); + MyObjectBuilderSerializer.DeserializeXML(_worldConfigPath, out MyObjectBuilder_WorldConfiguration worldConfig); + WorldConfiguration = new WorldConfigurationViewModel(worldConfig); + } + else + { + WorldConfiguration = new WorldConfigurationViewModel(new MyObjectBuilder_WorldConfiguration + { + Mods = checkpoint.Mods, + Settings = checkpoint.Settings + }); + + checkpoint.Mods = null; + checkpoint.Settings = null; + } + + OnPropertyChanged(nameof(Checkpoint)); + OnPropertyChanged(nameof(WorldConfiguration)); } } } diff --git a/Torch.Server/Managers/MultiplayerManagerDedicated.cs b/Torch.Server/Managers/MultiplayerManagerDedicated.cs index 002938b..58616a1 100644 --- a/Torch.Server/Managers/MultiplayerManagerDedicated.cs +++ b/Torch.Server/Managers/MultiplayerManagerDedicated.cs @@ -209,26 +209,21 @@ namespace Torch.Server.Managers //Largely copied from SE private void ValidateAuthTicketResponse(ulong steamId, JoinResult response, ulong steamOwner) { - //SteamNetworking.GetP2PSessionState(new CSteamID(steamId), out P2PSessionState_t state); - //state.GetRemoteIP(); - MyP2PSessionState statehack = new MyP2PSessionState(); - VRage.Steam.MySteamService.Static.Peer2Peer.GetSessionState(steamId, ref statehack); - var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray()); + var state = new MyP2PSessionState(); + MySteamServiceWrapper.Static.Peer2Peer.GetSessionState(steamId, ref state); + var ip = new IPAddress(BitConverter.GetBytes(state.RemoteIP).Reverse().ToArray()); Torch.CurrentSession.KeenSession.PromotedUsers.TryGetValue(steamId, out MyPromoteLevel promoteLevel); _log.Debug($"ValidateAuthTicketResponse(user={steamId}, response={response}, owner={steamOwner}, permissions={promoteLevel})"); _log.Info($"Connection attempt by {steamId} from {ip}"); - // TODO implement IP bans - var config = (TorchConfig) Torch.Config; - if (config.EnableWhitelist && !config.Whitelist.Contains(steamId)) + + if (Players.ContainsKey(steamId)) { - _log.Warn($"Rejecting user {steamId} because they are not whitelisted in Torch.cfg."); - UserRejected(steamId, JoinResult.NotInGroup); + _log.Warn($"Player {steamId} has already joined!"); + UserRejected(steamId, JoinResult.AlreadyJoined); } - else if(config.EnableReservedSlots && config.ReservedPlayers.Contains(steamId)) - UserAccepted(steamId); else if (Torch.CurrentSession.KeenSession.OnlineMode == MyOnlineModeEnum.OFFLINE && promoteLevel < MyPromoteLevel.Admin) { @@ -252,40 +247,48 @@ namespace Torch.Server.Managers private void RunEvent(ValidateAuthTicketEvent info) { - MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info); + JoinResult internalAuth; - if (info.FutureVerdict == null) + + if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID)) + internalAuth = JoinResult.BannedByAdmins; + else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) || + _isClientKicked(MyMultiplayer.Static, info.SteamOwner)) + internalAuth = JoinResult.KickedRecently; + else if (info.SteamResponse == JoinResult.OK) { - if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID)) - CommitVerdict(info.SteamID, JoinResult.BannedByAdmins); - else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) || - _isClientKicked(MyMultiplayer.Static, info.SteamOwner)) - CommitVerdict(info.SteamID, JoinResult.KickedRecently); - else if (info.SteamResponse == JoinResult.OK) + var config = (TorchConfig) Torch.Config; + if (config.EnableWhitelist && !config.Whitelist.Contains(info.SteamID)) { - //Admins can bypass member limit - if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) || - MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID))) - CommitVerdict(info.SteamID, JoinResult.OK); - //Server counts as a client, so subtract 1 from MemberCount - else if (MyMultiplayer.Static.MemberLimit > 0 && - MyMultiplayer.Static.MemberCount - 1 >= MyMultiplayer.Static.MemberLimit) - CommitVerdict(info.SteamID, JoinResult.ServerFull); - else if (MySandboxGame.ConfigDedicated.GroupID == 0uL) - CommitVerdict(info.SteamID, JoinResult.OK); - else - { - if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer)) - CommitVerdict(info.SteamID, JoinResult.OK); - else - CommitVerdict(info.SteamID, JoinResult.NotInGroup); - } + _log.Warn($"Rejecting user {info.SteamID} because they are not whitelisted in Torch.cfg."); + internalAuth = JoinResult.NotInGroup; } + else if (MySandboxGame.ConfigDedicated.Reserved.Contains(info.SteamID)) + internalAuth = JoinResult.OK; + //Admins can bypass member limit + else if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) || + MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID))) + internalAuth = JoinResult.OK; + //Server counts as a client, so subtract 1 from MemberCount + else if (MyMultiplayer.Static.MemberLimit > 0 && + MyMultiplayer.Static.MemberCount - 1 >= MyMultiplayer.Static.MemberLimit) + internalAuth = JoinResult.ServerFull; + else if (MySandboxGame.ConfigDedicated.GroupID == 0uL) + internalAuth = JoinResult.OK; else - CommitVerdict(info.SteamID, info.SteamResponse); - - return; + { + if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer)) + internalAuth = JoinResult.OK; + else + internalAuth = JoinResult.NotInGroup; + } } + else + internalAuth = info.SteamResponse; + + info.FutureVerdict = Task.FromResult(internalAuth); + + MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info); info.FutureVerdict.ContinueWith((task) => { @@ -339,4 +342,4 @@ namespace Torch.Server.Managers #endregion } -} \ No newline at end of file +} diff --git a/Torch.Server/Managers/RemoteAPIManager.cs b/Torch.Server/Managers/RemoteAPIManager.cs new file mode 100644 index 0000000..645a426 --- /dev/null +++ b/Torch.Server/Managers/RemoteAPIManager.cs @@ -0,0 +1,40 @@ +using NLog; +using Sandbox; +using Torch.API; +using Torch.Managers; +using VRage.Dedicated.RemoteAPI; + +namespace Torch.Server.Managers +{ + public class RemoteAPIManager : Manager + { + /// + public RemoteAPIManager(ITorchBase torchInstance) : base(torchInstance) + { + + } + + /// + public override void Attach() + { + Torch.GameStateChanged += TorchOnGameStateChanged; + base.Attach(); + } + + /// + public override void Detach() + { + Torch.GameStateChanged -= TorchOnGameStateChanged; + base.Detach(); + } + + private void TorchOnGameStateChanged(MySandboxGame game, TorchGameState newstate) + { + if (newstate == TorchGameState.Loading && MySandboxGame.ConfigDedicated.RemoteApiEnabled && !string.IsNullOrEmpty(MySandboxGame.ConfigDedicated.RemoteSecurityKey)) + { + var myRemoteServer = new MyRemoteServer(MySandboxGame.ConfigDedicated.RemoteApiPort, MySandboxGame.ConfigDedicated.RemoteSecurityKey); + LogManager.GetCurrentClassLogger().Info($"Remote API started on port {myRemoteServer.Port}"); + } + } + } +} \ No newline at end of file diff --git a/Torch.Server/Patches/WorldLoadExceptionPatch.cs b/Torch.Server/Patches/WorldLoadExceptionPatch.cs new file mode 100644 index 0000000..a1cbcaf --- /dev/null +++ b/Torch.Server/Patches/WorldLoadExceptionPatch.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using NLog; +using Sandbox; +using Torch.Managers.PatchManager; +using Torch.Managers.PatchManager.MSIL; + +namespace Torch.Patches +{ + /// + /// Patches MySandboxGame.InitQuickLaunch to rethrow exceptions caught during session load. + /// + [PatchShim] + public static class WorldLoadExceptionPatch + { + private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); + + public static void Patch(PatchContext ctx) + { + ctx.GetPattern(typeof(MySandboxGame).GetMethod("InitQuickLaunch", BindingFlags.Instance | BindingFlags.NonPublic)) + .Transpilers.Add(typeof(WorldLoadExceptionPatch).GetMethod(nameof(Transpile), BindingFlags.Static | BindingFlags.NonPublic)); + } + + private static IEnumerable Transpile(IEnumerable method) + { + var msil = method.ToList(); + for (var i = 0; i < msil.Count; i++) + { + if (msil[i].TryCatchOperations.All(x => x.Type != MsilTryCatchOperationType.BeginClauseBlock)) + continue; + + for (; i < msil.Count; i++) + { + if (msil[i].OpCode != OpCodes.Leave) + continue; + + msil[i] = new MsilInstruction(OpCodes.Rethrow); + break; + } + } + return msil; + } + } +} \ No newline at end of file diff --git a/Torch.Server/Program.cs b/Torch.Server/Program.cs index 0bc4d7e..2e36d46 100644 --- a/Torch.Server/Program.cs +++ b/Torch.Server/Program.cs @@ -2,12 +2,15 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net; using System.Reflection; using System.ServiceProcess; using System.Text; using System.Threading; +using Microsoft.VisualBasic.Devices; using NLog; +using NLog.Fluent; using NLog.Targets; using Torch.Utils; @@ -21,21 +24,46 @@ namespace Torch.Server [STAThread] public static void Main(string[] args) { + + Target.Register("FlowDocument"); //Ensures that all the files are downloaded in the Torch directory. var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); var binDir = Path.Combine(workingDir, "DedicatedServer64"); Directory.SetCurrentDirectory(workingDir); + //HACK for block skins update + var badDlls = new[] + { + "System.Security.Principal.Windows.dll", + }; + + try + { + foreach (var file in badDlls) + { + if (File.Exists(file)) + File.Delete(file); + } + } + catch (Exception e) + { + var log = LogManager.GetCurrentClassLogger(); + log.Error($"Error updating. Please delete the following files from the Torch root folder manually:\r\n{string.Join("\r\n", badDlls)}"); + log.Error(e); + return; + } + if (!TorchLauncher.IsTorchWrapped()) { TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName, args, binDir); return; } - if (!Environment.UserInteractive) + // Breaks on Windows Server 2019 + if (!new ComputerInfo().OSFullName.Contains("Server 2019") && !Environment.UserInteractive) { - using (var service = new TorchService()) + using (var service = new TorchService(args)) ServiceBase.Run(service); return; } @@ -47,4 +75,4 @@ namespace Torch.Server initializer.Run(); } } -} \ No newline at end of file +} diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj index ed9e330..000951b 100644 --- a/Torch.Server/Torch.Server.csproj +++ b/Torch.Server/Torch.Server.csproj @@ -70,6 +70,9 @@ ..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll + + ..\packages\Markdown.Xaml.1.0.0\lib\net45\Markdown.Xaml.dll + False ..\GameBinaries\Microsoft.CodeAnalysis.dll @@ -80,13 +83,25 @@ ..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll False - - ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll + + + + ..\GameBinaries\netstandard.dll + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + True ..\packages\NLog.4.4.12\lib\net45\NLog.dll True + + ..\packages\protobuf-net.2.4.0\lib\net40\protobuf-net.dll + False ..\GameBinaries\Sandbox.Common.dll @@ -121,11 +136,24 @@ ..\GameBinaries\Steamworks.NET.dll + + ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll + True + + + + + ..\packages\System.Security.AccessControl.4.4.0\lib\net461\System.Security.AccessControl.dll + + + ..\packages\System.Security.Principal.Windows.4.4.0\lib\net461\System.Security.Principal.Windows.dll + + @@ -180,15 +208,8 @@ ..\GameBinaries\VRage.Math.dll False - - False - ..\GameBinaries\VRage.Native.dll - False - - - False - ..\GameBinaries\VRage.OpenVRWrapper.dll - False + + ..\GameBinaries\VRage.Platform.Windows.dll False @@ -225,9 +246,11 @@ + + @@ -245,15 +268,29 @@ + + + + CharacterView.xaml + + + ModListControl.xaml + + + PluginBrowser.xaml + + + RoleEditor.xaml + ThemeControl.xaml @@ -414,6 +451,14 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -430,6 +475,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -469,9 +518,10 @@ false + - copy "$(SolutionDir)NLog.config" "$(TargetDir)" + copy "$(SolutionDir)NLog.config" "$(TargetDir)" & copy "$(SolutionDir)NLog-user.config" "$(TargetDir)" \ No newline at end of file diff --git a/Torch.Server/TorchConfig.cs b/Torch.Server/TorchConfig.cs index f686315..80ea8ff 100644 --- a/Torch.Server/TorchConfig.cs +++ b/Torch.Server/TorchConfig.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Xml.Serialization; using Newtonsoft.Json; using NLog; +using VRage.Game; namespace Torch.Server { @@ -20,9 +21,38 @@ namespace Torch.Server [Arg("instancename", "The name of the Torch instance.")] public string InstanceName { get; set; } + + private string _instancePath; + /// [Arg("instancepath", "Server data folder where saves and mods are stored.")] - public string InstancePath { get; set; } + public string InstancePath + { + get => _instancePath; + set + { + if(String.IsNullOrEmpty(value)) + { + _instancePath = value; + return; + } + try + { + if(value.Contains("\"")) + throw new InvalidOperationException(); + + var s = Path.GetFullPath(value); + Console.WriteLine(s); //prevent compiler opitmization - just in case + } + catch (Exception ex) + { + _log.Error(ex, "Invalid path assigned to InstancePath! Please report this immediately! Value: " + value); + //throw; + } + + _instancePath = value; + } + } /// [XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")] @@ -58,18 +88,37 @@ namespace Torch.Server public int TickTimeout { get; set; } = 60; /// - public List Plugins { get; set; } = new List(); + [Arg("plugins", "Starts Torch with the given plugin GUIDs (space delimited).")] + public List Plugins { get; set; } = new List(); + + [Arg("localplugins", "Loads all pluhins from disk, ignores the plugins defined in config.")] + public bool LocalPlugins { get; set; } + + [Arg("disconnect", "When server restarts, all clients are rejected to main menu to prevent auto rejoin")] + public bool DisconnectOnRestart { get; set; } + + public string ChatName { get; set; } = "Server"; + + public string ChatColor { get; set; } = "Red"; public bool EnableWhitelist { get; set; } = false; public HashSet Whitelist { get; set; } = new HashSet(); - internal Point WindowSize { get; set; } = new Point(800, 600); - internal Point WindowPosition { get; set; } = new Point(); + public Point WindowSize { get; set; } = new Point(800, 600); + public Point WindowPosition { get; set; } = new Point(); public string LastUsedTheme { get; set; } = "Torch Theme"; - public bool EnableReservedSlots { get; set; } = false; - public HashSet ReservedPlayers { get; set; } = new HashSet(); + //Prevent reserved players being written to disk, but allow it to be read + //remove this when ReservedPlayers is removed + private bool ShouldSerializeReservedPlayers() => false; + + [Arg("console", "Keeps a separate console window open after the main UI loads.")] + public bool IndependentConsole { get; set; } = false; + + [XmlIgnore] + [Arg("testplugin", "Path to a plugin to debug. For development use only.")] + public string TestPlugin { get; set; } [XmlIgnore] private string _path; diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs index 4832c9c..76cd9b8 100644 --- a/Torch.Server/TorchServer.cs +++ b/Torch.Server/TorchServer.cs @@ -19,6 +19,7 @@ using Torch.API.Managers; using Torch.API.Session; using Torch.Commands; using Torch.Mod; +using Torch.Mod.Messages; using Torch.Server.Commands; using Torch.Server.Managers; using Torch.Utils; @@ -26,6 +27,7 @@ using VRage; using VRage.Dedicated; using VRage.Dedicated.RemoteAPI; using VRage.GameServices; +using VRage.Scripting; using VRage.Steam; using Timer = System.Threading.Timer; @@ -37,14 +39,18 @@ namespace Torch.Server { public class TorchServer : TorchBase, ITorchServer { + private bool _hasRun; private bool _canRun; private TimeSpan _elapsedPlayTime; - private bool _hasRun; private bool _isRunning; private float _simRatio; private ServerState _state; private Stopwatch _uptime; private Timer _watchdog; + private int _players; + private MultiplayerManagerDedicated _multiplayerManagerDedicated; + + internal bool FatalException { get; set; } /// public TorchServer(TorchConfig config = null) @@ -52,12 +58,15 @@ namespace Torch.Server DedicatedInstance = new InstanceManager(this); AddManager(DedicatedInstance); AddManager(new EntityControlManager(this)); + AddManager(new RemoteAPIManager(this)); Config = config ?? new TorchConfig(); var sessionManager = Managers.GetManager(); sessionManager.AddFactory(x => new MultiplayerManagerDedicated(this)); } - + + public bool HasRun { get => _hasRun; set => SetValue(ref _hasRun, value); } + /// public float SimulationRatio { get => _simRatio; set => SetValue(ref _simRatio, value); } @@ -92,6 +101,8 @@ namespace Torch.Server /// public string InstancePath => Config?.InstancePath; + public int OnlinePlayers { get => _players; private set => SetValue(ref _players, value); } + /// public override void Init() { @@ -111,7 +122,7 @@ namespace Torch.Server if (State != ServerState.Stopped) return; - if (_hasRun) + if (IsRunning || HasRun) { Restart(); return; @@ -119,16 +130,11 @@ namespace Torch.Server State = ServerState.Starting; IsRunning = true; + HasRun = true; CanRun = false; - _hasRun = true; Log.Info("Starting server."); MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model; - if (MySandboxGame.ConfigDedicated.RemoteApiEnabled && !string.IsNullOrEmpty(MySandboxGame.ConfigDedicated.RemoteSecurityKey)) - { - var myRemoteServer = new MyRemoteServer(MySandboxGame.ConfigDedicated.RemoteApiPort, MySandboxGame.ConfigDedicated.RemoteSecurityKey); - Log.Info($"Remote API started on port {myRemoteServer.Port}"); - } - + _uptime = Stopwatch.StartNew(); base.Start(); } @@ -150,9 +156,15 @@ namespace Torch.Server /// /// Restart the program. /// - public override void Restart() + public override void Restart(bool save = true) { - if (IsRunning) + if (Config.DisconnectOnRestart) + { + ModCommunication.SendMessageToClients(new JoinServerMessage("0.0.0.0:25555")); + Log.Info("Ejected all players from server for restart."); + } + + if (IsRunning && save) Save().ContinueWith(DoRestart, this, TaskContinuationOptions.RunContinuationsAsynchronously); else DoRestart(null, this); @@ -186,6 +198,7 @@ namespace Torch.Server if (newState == TorchSessionState.Loaded) { + _multiplayerManagerDedicated = CurrentSession.Managers.GetManager(); CurrentSession.Managers.GetManager().RegisterCommandModule(typeof(WhitelistCommands)); ModCommunication.Register(); } @@ -195,8 +208,7 @@ namespace Torch.Server public override void Init(object gameInstance) { base.Init(gameInstance); - var game = gameInstance as MySandboxGame; - if (game != null && MySession.Static != null) + if (gameInstance is MySandboxGame && MySession.Static != null) State = ServerState.Running; else State = ServerState.Stopped; @@ -210,6 +222,7 @@ namespace Torch.Server SimulationRatio = Math.Min(Sync.ServerSimulationRatio, 1); var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds)); ElapsedPlayTime = elapsed; + OnlinePlayers = _multiplayerManagerDedicated?.Players.Count ?? 0; if (_watchdog == null && Config.TickTimeout > 0) { @@ -223,10 +236,16 @@ namespace Torch.Server private static void CheckServerResponding(object state) { + var server = (TorchServer)state; var mre = new ManualResetEvent(false); - ((TorchServer)state).Invoke(() => mre.Set()); + server.Invoke(() => mre.Set()); if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) { + if (server.FatalException) + { + server._watchdog.Dispose(); + return; + } #if DEBUG Log.Error( $"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds."); diff --git a/Torch.Server/TorchService.cs b/Torch.Server/TorchService.cs index e33fda3..1a659b8 100644 --- a/Torch.Server/TorchService.cs +++ b/Torch.Server/TorchService.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ServiceProcess; +using System.Threading; using NLog; using Torch.API; @@ -12,12 +14,14 @@ namespace Torch.Server { class TorchService : ServiceBase { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); public const string Name = "Torch (SEDS)"; - private TorchServer _server; private Initializer _initializer; + private string[] _args; - public TorchService() + public TorchService(string[] args) { + _args = args; var workingDir = new FileInfo(typeof(TorchService).Assembly.Location).Directory.ToString(); Directory.SetCurrentDirectory(workingDir); _initializer = new Initializer(workingDir); @@ -29,19 +33,21 @@ namespace Torch.Server } /// - protected override void OnStart(string[] args) + protected override void OnStart(string[] _) { - base.OnStart(args); + base.OnStart(_args); - _initializer.Initialize(args); + _initializer.Initialize(_args); _initializer.Run(); } /// protected override void OnStop() { - _server.Stop(); - base.OnStop(); + var mre = new ManualResetEvent(false); + Task.Run(() => _initializer.Server.Stop()); + if (!mre.WaitOne(TimeSpan.FromMinutes(1))) + Process.GetCurrentProcess().Kill(); } } } diff --git a/Torch.Server/ViewModels/CheckpointViewModel.cs b/Torch.Server/ViewModels/CheckpointViewModel.cs index 2d5643f..d1882b6 100644 --- a/Torch.Server/ViewModels/CheckpointViewModel.cs +++ b/Torch.Server/ViewModels/CheckpointViewModel.cs @@ -16,12 +16,12 @@ namespace Torch.Server.ViewModels public class CheckpointViewModel : ViewModel { private MyObjectBuilder_Checkpoint _checkpoint; - private SessionSettingsViewModel _sessionSettings; + //private SessionSettingsViewModel _sessionSettings; public CheckpointViewModel(MyObjectBuilder_Checkpoint checkpoint) { _checkpoint = checkpoint; - _sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings); + //_sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings); } public static implicit operator MyObjectBuilder_Checkpoint(CheckpointViewModel model) @@ -59,15 +59,15 @@ namespace Torch.Server.ViewModels public SerializableDictionary ControlledEntities { get => _checkpoint.ControlledEntities; set => SetValue(ref _checkpoint.ControlledEntities, value); } - public SessionSettingsViewModel Settings - { - get => _sessionSettings; - set - { - SetValue(ref _sessionSettings, value); - _checkpoint.Settings = _sessionSettings; - } - } + //public SessionSettingsViewModel Settings + //{ + // get => _sessionSettings; + // set + // { + // SetValue(ref _sessionSettings, value); + // _checkpoint.Settings = _sessionSettings; + // } + //} public MyObjectBuilder_ScriptManager ScriptManagerData => throw new NotImplementedException(); @@ -75,7 +75,7 @@ namespace Torch.Server.ViewModels public MyObjectBuilder_FactionCollection Factions => throw new NotImplementedException(); - public List Mods { get => _checkpoint.Mods; set => SetValue(ref _checkpoint.Mods, value); } + //public List Mods { get => _checkpoint.Mods; set => SetValue(ref _checkpoint.Mods, value); } public SerializableDictionary PromotedUsers { get => _checkpoint.PromotedUsers; set => SetValue(ref _checkpoint.PromotedUsers, value); } diff --git a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs index 3ee6ee8..a417eef 100644 --- a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs +++ b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs @@ -10,6 +10,8 @@ using Torch.Collections; using Torch.Server.Managers; using VRage.Game; using VRage.Game.ModAPI; +using Torch.Utils.SteamWorkshopTools; +using Torch.Collections; namespace Torch.Server.ViewModels { @@ -27,8 +29,9 @@ namespace Torch.Server.ViewModels public ConfigDedicatedViewModel(MyConfigDedicated configDedicated) { _config = configDedicated; - _config.IgnoreLastSession = true; + //_config.IgnoreLastSession = true; SessionSettings = new SessionSettingsViewModel(_config.SessionSettings); + Task.Run(() => UpdateAllModInfosAsync()); } public void Save(string path = null) @@ -37,7 +40,7 @@ namespace Torch.Server.ViewModels _config.SessionSettings = _sessionSettings; // Never ever - _config.IgnoreLastSession = true; + //_config.IgnoreLastSession = true; _config.Save(path); } @@ -73,12 +76,61 @@ namespace Torch.Server.ViewModels } } + public async Task UpdateAllModInfosAsync(Action messageHandler = null) + { + if (Mods.Count() == 0) + return; + + var ids = Mods.Select(m => m.PublishedFileId); + var workshopService = WebAPI.Instance; + Dictionary modInfos = null; + + try + { + modInfos = (await workshopService.GetPublishedFileDetails(ids.ToArray())); + } + catch (Exception e) + { + Log.Error(e.Message); + return; + } + + Log.Info($"Mods Info successfully retrieved!"); + + foreach (var mod in Mods) + { + if (!modInfos.ContainsKey(mod.PublishedFileId) || modInfos[mod.PublishedFileId] == null) + { + Log.Error($"Failed to retrieve info for mod with workshop id '{mod.PublishedFileId}'!"); + } + //else if (!modInfo.Tags.Contains("")) + else + { + mod.FriendlyName = modInfos[mod.PublishedFileId].Title; + mod.Description = modInfos[mod.PublishedFileId].Description; + //mod.Name = modInfos[mod.PublishedFileId].FileName; + } + } + + } + public List Administrators { get => _config.Administrators; set => SetValue(x => _config.Administrators = x, value); } public List Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); } - - private List _mods = new List(); - public List Mods { get => _mods; set => SetValue(x => _mods = x, value); } + + private MtObservableList _mods = new MtObservableList(); + public MtObservableList Mods + { + get => _mods; + set + { + SetValue(x => _mods = x, value); + Task.Run(() => UpdateAllModInfosAsync()); + } + } + + public List Reserved { get => _config.Reserved; set => SetValue(x => _config.Reserved = x, value); } + public int AsteroidAmount { get => _config.AsteroidAmount; set => SetValue(x => _config.AsteroidAmount = x, value); } @@ -90,8 +142,12 @@ namespace Torch.Server.ViewModels public string ServerName { get => _config.ServerName; set => SetValue(x => _config.ServerName = x, value); } + public string ServerDescription { get => _config.ServerDescription; set => SetValue(x => _config.ServerDescription = x, value); } + public bool PauseGameWhenEmpty { get => _config.PauseGameWhenEmpty; set => SetValue(x => _config.PauseGameWhenEmpty = x, value); } + public bool AutodetectDependencies { get => _config.AutodetectDependencies; set => SetValue(x => _config.AutodetectDependencies = x, value); } + public string PremadeCheckpointPath { get => _config.PremadeCheckpointPath; set => SetValue(x => _config.PremadeCheckpointPath = x, value); } public string LoadWorld { get => _config.LoadWorld; set => SetValue(x => _config.LoadWorld = x, value); } diff --git a/Torch.Server/ViewModels/Entities/CharacterViewModel.cs b/Torch.Server/ViewModels/Entities/CharacterViewModel.cs index e111666..fe55549 100644 --- a/Torch.Server/ViewModels/Entities/CharacterViewModel.cs +++ b/Torch.Server/ViewModels/Entities/CharacterViewModel.cs @@ -4,14 +4,24 @@ namespace Torch.Server.ViewModels.Entities { public class CharacterViewModel : EntityViewModel { + private MyCharacter _character; public CharacterViewModel(MyCharacter character, EntityTreeViewModel tree) : base(character, tree) { - character.ControllerInfo.ControlAcquired += (x) => { OnPropertyChanged(nameof(Name)); }; - character.ControllerInfo.ControlReleased += (x) => { OnPropertyChanged(nameof(Name)); }; + _character = character; + character.ControllerInfo.ControlAcquired += ControllerInfo_ControlAcquired; + character.ControllerInfo.ControlReleased += ControllerInfo_ControlAcquired; + } + + private void ControllerInfo_ControlAcquired(Sandbox.Game.World.MyEntityController obj) + { + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(CanDelete)); } public CharacterViewModel() { } + + public override bool CanDelete => _character.ControllerInfo?.Controller?.Player == null; } } diff --git a/Torch.Server/ViewModels/Entities/EntityViewModel.cs b/Torch.Server/ViewModels/Entities/EntityViewModel.cs index c3502d6..0f52e40 100644 --- a/Torch.Server/ViewModels/Entities/EntityViewModel.cs +++ b/Torch.Server/ViewModels/Entities/EntityViewModel.cs @@ -1,8 +1,14 @@ -using System.Windows.Controls; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Controls; +using NLog; +using Sandbox.Game.Entities; using Sandbox.Game.World; using Torch.API.Managers; using Torch.Collections; using Torch.Server.Managers; +using Torch.Utils; using VRage.Game.Entity; using VRage.Game.ModAPI; using VRage.ModAPI; @@ -14,6 +20,8 @@ namespace Torch.Server.ViewModels.Entities { protected EntityTreeViewModel Tree { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + private IMyEntity _backing; public IMyEntity Entity { @@ -43,6 +51,86 @@ namespace Torch.Server.ViewModels.Entities } } + private string _descriptiveName; + public string DescriptiveName + { + get => _descriptiveName ?? (_descriptiveName = GetSortedName(EntityTreeViewModel.SortEnum.Name)); + set => _descriptiveName = value; + } + + public virtual string GetSortedName(EntityTreeViewModel.SortEnum sort) + { + switch (sort) + { + case EntityTreeViewModel.SortEnum.Name: + return Name; + case EntityTreeViewModel.SortEnum.Size: + return $"{Name} ({Entity.WorldVolume.Radius * 2:N}m)"; + case EntityTreeViewModel.SortEnum.Speed: + return $"{Name} ({Entity.Physics?.LinearVelocity.Length() ?? 0:N}m/s)"; + case EntityTreeViewModel.SortEnum.BlockCount: + if (Entity is MyCubeGrid grid) + return $"{Name} ({grid.BlocksCount} blocks)"; + return Name; + case EntityTreeViewModel.SortEnum.DistFromCenter: + return $"{Name} ({Entity.GetPosition().Length():N}m)"; + case EntityTreeViewModel.SortEnum.Owner: + if (Entity is MyCubeGrid g) + return $"{Name} ({g.GetGridOwnerName()})"; + return Name; + default: + throw new ArgumentOutOfRangeException(nameof(sort), sort, null); + } + } + + public virtual int CompareToSort(EntityViewModel other, EntityTreeViewModel.SortEnum sort) + { + if (other == null) + return -1; + + switch (sort) + { + case EntityTreeViewModel.SortEnum.Name: + if (Name == null) + { + if (other.Name == null) + return 0; + return 1; + } + if (other.Name == null) + return -1; + return string.Compare(Name, other.Name, StringComparison.InvariantCultureIgnoreCase); + case EntityTreeViewModel.SortEnum.Size: + return Entity.WorldVolume.Radius.CompareTo(other.Entity.WorldVolume.Radius); + case EntityTreeViewModel.SortEnum.Speed: + if (Entity.Physics == null) + { + if (other.Entity.Physics == null) + return 0; + return -1; + } + if (other.Entity.Physics == null) + return 1; + return Entity.Physics.LinearVelocity.LengthSquared().CompareTo(other.Entity.Physics.LinearVelocity.LengthSquared()); + case EntityTreeViewModel.SortEnum.BlockCount: + { + if (Entity is MyCubeGrid ga && other.Entity is MyCubeGrid gb) + return ga.BlocksCount.CompareTo(gb.BlocksCount); + goto case EntityTreeViewModel.SortEnum.Name; + } + case EntityTreeViewModel.SortEnum.DistFromCenter: + return Entity.GetPosition().LengthSquared().CompareTo(other.Entity.GetPosition().LengthSquared()); + case EntityTreeViewModel.SortEnum.Owner: + { + if (Entity is MyCubeGrid ga && other.Entity is MyCubeGrid gb) + return string.Compare(ga.GetGridOwnerName(), gb.GetGridOwnerName(), StringComparison.InvariantCultureIgnoreCase); + goto case EntityTreeViewModel.SortEnum.Name; + } + default: + throw new ArgumentOutOfRangeException(nameof(sort), sort, null); + } + } + public virtual string Position { get => Entity?.GetPosition().ToString(); @@ -59,7 +147,7 @@ namespace Torch.Server.ViewModels.Entities public virtual bool CanStop => Entity.Physics?.Enabled ?? false; - public virtual bool CanDelete => !(Entity is IMyCharacter); + public virtual bool CanDelete => true; public virtual void Delete() { @@ -76,5 +164,20 @@ namespace Torch.Server.ViewModels.Entities { } + + public class Comparer : IComparer + { + private EntityTreeViewModel.SortEnum _sort; + + public Comparer(EntityTreeViewModel.SortEnum sort) + { + _sort = sort; + } + + public int Compare(EntityViewModel x, EntityViewModel y) + { + return x.CompareToSort(y, _sort); + } + } } } diff --git a/Torch.Server/ViewModels/Entities/GridViewModel.cs b/Torch.Server/ViewModels/Entities/GridViewModel.cs index 50db707..9a6229c 100644 --- a/Torch.Server/ViewModels/Entities/GridViewModel.cs +++ b/Torch.Server/ViewModels/Entities/GridViewModel.cs @@ -50,17 +50,14 @@ namespace Torch.Server.ViewModels.Entities Blocks { get; } = new MtObservableSortedDictionary>( CubeBlockDefinitionComparer.Default); - - /// - public string DescriptiveName { get; } - + public GridViewModel() { } public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree) { - DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)"; + //DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)"; Blocks.Add(_fillerDefinition, new MtObservableSortedDictionary()); } diff --git a/Torch.Server/ViewModels/EntityTreeViewModel.cs b/Torch.Server/ViewModels/EntityTreeViewModel.cs index 54b8e7a..3ef4472 100644 --- a/Torch.Server/ViewModels/EntityTreeViewModel.cs +++ b/Torch.Server/ViewModels/EntityTreeViewModel.cs @@ -12,11 +12,21 @@ using VRage.ModAPI; using System.Windows.Threading; using NLog; using Torch.Collections; +using Torch.Server.Views.Entities; namespace Torch.Server.ViewModels { public class EntityTreeViewModel : ViewModel { + public enum SortEnum + { + Name, + Size, + Speed, + Owner, + BlockCount, + DistFromCenter, + } private static readonly Logger _log = LogManager.GetCurrentClassLogger(); //TODO: these should be sorted sets for speed @@ -26,7 +36,13 @@ namespace Torch.Server.ViewModels public MtObservableSortedDictionary VoxelMaps { get; set; } = new MtObservableSortedDictionary(); public Dispatcher ControlDispatcher => _control.Dispatcher; + public SortedView SortedGrids { get; } + public SortedView SortedCharacters { get; } + public SortedView SortedFloatingObjects { get; } + public SortedView SortedVoxelMaps { get; } + private EntityViewModel _currentEntity; + private SortEnum _currentSort; private UserControl _control; public EntityViewModel CurrentEntity @@ -35,6 +51,12 @@ namespace Torch.Server.ViewModels set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); } } + public SortEnum CurrentSort + { + get => _currentSort; + set => SetValue(ref _currentSort, value); + } + // I hate you today WPF public EntityTreeViewModel() : this(null) { @@ -43,6 +65,11 @@ namespace Torch.Server.ViewModels public EntityTreeViewModel(UserControl control) { _control = control; + var comparer = new EntityViewModel.Comparer(_currentSort); + SortedGrids = new SortedView(Grids.Values, comparer); + SortedCharacters = new SortedView(Characters.Values, comparer); + SortedFloatingObjects = new SortedView(FloatingObjects.Values, comparer); + SortedVoxelMaps = new SortedView(VoxelMaps.Values, comparer); } public void Init() @@ -85,16 +112,16 @@ namespace Torch.Server.ViewModels switch (obj) { case MyCubeGrid grid: - Grids.Add(obj.EntityId, new GridViewModel(grid, this)); + Grids.Add(grid.EntityId, new GridViewModel(grid, this)); break; case MyCharacter character: - Characters.Add(obj.EntityId, new CharacterViewModel(character, this)); + Characters.Add(character.EntityId, new CharacterViewModel(character, this)); break; case MyFloatingObject floating: - FloatingObjects.Add(obj.EntityId, new FloatingObjectViewModel(floating, this)); + FloatingObjects.Add(floating.EntityId, new FloatingObjectViewModel(floating, this)); break; case MyVoxelBase voxel: - VoxelMaps.Add(obj.EntityId, new VoxelMapViewModel(voxel, this)); + VoxelMaps.Add(voxel.EntityId, new VoxelMapViewModel(voxel, this)); break; } } diff --git a/Torch.Server/ViewModels/ModItemInfo.cs b/Torch.Server/ViewModels/ModItemInfo.cs new file mode 100644 index 0000000..9212fa1 --- /dev/null +++ b/Torch.Server/ViewModels/ModItemInfo.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using System.Runtime.CompilerServices; +using NLog; +using VRage.Game; +using Torch.Server.Annotations; +using Torch.Utils.SteamWorkshopTools; + +namespace Torch.Server.ViewModels +{ + /// + /// Wrapper around VRage.Game.Objectbuilder_Checkpoint.ModItem + /// that holds additional meta information + /// (e.g. workshop description) + /// + public class ModItemInfo : ViewModel + { + MyObjectBuilder_Checkpoint.ModItem _modItem; + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + /// + /// Human friendly name of the mod + /// + public string FriendlyName + { + get { return _modItem.FriendlyName; } + set { + SetValue(ref _modItem.FriendlyName, value); + } + } + + /// + /// Workshop ID of the mod + /// + public ulong PublishedFileId + { + get { return _modItem.PublishedFileId; } + set + { + SetValue(ref _modItem.PublishedFileId, value); + } + } + + /// + /// Local filename of the mod + /// + public string Name + { + get { return _modItem.Name; } + set + { + SetValue(ref _modItem.FriendlyName, value); + } + } + + /// + /// Whether or not the mod was added + /// because another mod depends on it + /// + public bool IsDependency + { + get { return _modItem.IsDependency; } + set + { + SetValue(ref _modItem.IsDependency, value); + } + } + + private string _description; + /// + /// Workshop description of the mod + /// + public string Description + { + get { return _description; } + set + { + SetValue(ref _description, value); + } + } + + /// + /// Constructor, returns a new ModItemInfo instance + /// + /// The wrapped mod + public ModItemInfo(MyObjectBuilder_Checkpoint.ModItem mod) + { + _modItem = mod; + } + + /// + /// Retrieve information about the + /// wrapped mod from the workhop asynchronously + /// via the Steam web API. + /// + /// + public async Task UpdateModInfoAsync() + { + var msg = ""; + var workshopService = WebAPI.Instance; + PublishedItemDetails modInfo = null; + try + { + modInfo = (await workshopService.GetPublishedFileDetails(new ulong[] { PublishedFileId }))?[PublishedFileId]; + } + catch( Exception e ) + { + Log.Error(e.Message); + } + if (modInfo == null) + { + Log.Error($"Failed to retrieve mod with workshop id '{PublishedFileId}'!"); + return false; + } + //else if (!modInfo.Tags.Contains("")) + else + { + Log.Info($"Mod Info successfully retrieved!"); + FriendlyName = modInfo.Title; + Description = modInfo.Description; + //Name = modInfo.FileName; + return true; + } + } + } +} diff --git a/Torch.Server/ViewModels/PluginViewModel.cs b/Torch.Server/ViewModels/PluginViewModel.cs index 21fe310..561bfce 100644 --- a/Torch.Server/ViewModels/PluginViewModel.cs +++ b/Torch.Server/ViewModels/PluginViewModel.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; +using NLog; using Torch.API; using Torch.API.Plugins; using Torch.Server.Views; @@ -17,13 +19,25 @@ namespace Torch.Server.ViewModels public string Name { get; } public ITorchPlugin Plugin { get; } + private static Logger _log = LogManager.GetCurrentClassLogger(); + public PluginViewModel(ITorchPlugin plugin) { Plugin = plugin; if (Plugin is IWpfPlugin p) - Control = p.GetControl(); - + { + try + { + Control = p.GetControl(); + } + catch (Exception ex) + { + _log.Error(ex, $"Exception loading interface for plugin {Plugin.Name}! Plugin interface will not be available!"); + Control = null; + } + } + Name = $"{plugin.Name} ({plugin.Version})"; ThemeControl.UpdateDynamicControls += UpdateResourceDict; @@ -38,5 +52,55 @@ namespace Torch.Server.ViewModels this.Control.Resources.MergedDictionaries.Clear(); this.Control.Resources.MergedDictionaries.Add(dictionary); } + + public Brush Color + { + get { + switch (Plugin.State) + { + case PluginState.NotInitialized: + case PluginState.MissingDependency: + case PluginState.DisabledError: + return Brushes.Red; + case PluginState.UpdateRequired: + return Brushes.DodgerBlue; + case PluginState.UninstallRequested: + return Brushes.Gold; + case PluginState.NotInstalled: + case PluginState.DisabledUser: + return Brushes.Gray; + case PluginState.Enabled: + return Brushes.Transparent; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public string ToolTip + { + get { switch (Plugin.State) + { + case PluginState.NotInitialized: + return "Error during load."; + case PluginState.DisabledError: + return "Disabled due to error on load."; + case PluginState.DisabledUser: + return "Disabled."; + case PluginState.UpdateRequired: + return "Update required."; + case PluginState.UninstallRequested: + return "Marked for uninstall."; + case PluginState.NotInstalled: + return "Not installed. Click 'Enable'"; + case PluginState.Enabled: + return string.Empty; + case PluginState.MissingDependency: + return "Dependency missing. Check the log."; + default: + throw new ArgumentOutOfRangeException(); + } + } + } } } diff --git a/Torch.Server/ViewModels/SessionSettingsViewModel.cs b/Torch.Server/ViewModels/SessionSettingsViewModel.cs index 9f43d01..6037485 100644 --- a/Torch.Server/ViewModels/SessionSettingsViewModel.cs +++ b/Torch.Server/ViewModels/SessionSettingsViewModel.cs @@ -21,8 +21,11 @@ namespace Torch.Server.ViewModels [Torch.Views.Display(Description = "The type of the game online mode.", Name = "Online Mode", GroupName = "Others")] public MyOnlineModeEnum OnlineMode { get => _settings.OnlineMode; set => SetValue(ref _settings.OnlineMode, value); } - [Torch.Views.Display(Description = "The multiplier for inventory size.", Name = "Inventory Size", GroupName = "Multipliers")] - public float InventorySizeMultiplier { get => _settings.InventorySizeMultiplier; set => SetValue(ref _settings.InventorySizeMultiplier, value); } + [Torch.Views.Display(Description = "The multiplier for character inventory size.", Name = "Character Inventory Size", GroupName = "Multipliers")] + public float CharacterInventorySizeMultiplier { get => _settings.InventorySizeMultiplier; set => SetValue(ref _settings.InventorySizeMultiplier, value); } + + [Torch.Views.Display(Description = "The multiplier for block inventory size.", Name = "Block Inventory Size", GroupName = "Multipliers")] + public float BlockInventorySizeMultiplier { get => _settings.BlocksInventorySizeMultiplier; set => SetValue(ref _settings.BlocksInventorySizeMultiplier, value); } [Torch.Views.Display(Description = "The multiplier for assembler speed.", Name = "Assembler Speed", GroupName = "Multipliers")] public float AssemblerSpeedMultiplier { get => _settings.AssemblerSpeedMultiplier; set => SetValue(ref _settings.AssemblerSpeedMultiplier, value); } @@ -120,8 +123,14 @@ namespace Torch.Server.ViewModels public bool EnableSaving { get => _settings.EnableSaving; set => SetValue(ref _settings.EnableSaving, value); } [Torch.Views.Display(Description = "Enables respawn screen.", Name = "Enable Respawn Screen in the Game", GroupName = "Players")] - public bool EnableRespawnScreen { get => _settings.EnableRespawnScreen; set => SetValue(ref _settings.EnableRespawnScreen, value); } - + public bool StartInRespawnScreen { get => _settings.StartInRespawnScreen; set => SetValue(ref _settings.StartInRespawnScreen, value); } + + [Torch.Views.Display(Description = "Enables research.", Name = "Enable Research", GroupName = "Players")] + public bool EnableResearch { get => _settings.EnableResearch; set => SetValue(ref _settings.EnableResearch, value); } + + [Torch.Views.Display(Description = "Enables Good.bot hints.", Name = "Enable Good.bot hints", GroupName = "Players")] + public bool EnableGoodBotHints { get => _settings.EnableGoodBotHints; set => SetValue(ref _settings.EnableGoodBotHints, value); } + [Torch.Views.Display(Description = "Enables infinite ammunition in survival game mode.", Name = "Enable Infinite Ammunition in Survival", GroupName = "Others")] public bool InfiniteAmmo { get => _settings.InfiniteAmmo; set => SetValue(ref _settings.InfiniteAmmo, value); } @@ -255,6 +264,45 @@ namespace Torch.Server.ViewModels [Torch.Views.Display(Description = "Defines character removal threshold for trash removal system. If player disconnects it will remove his character after this time.\n Set to 0 to disable.", Name = "Character Removal Threshold [mins]", GroupName = "Trash Removal")] public int PlayerCharacterRemovalThreshold { get => _settings.PlayerCharacterRemovalThreshold; set => SetValue(ref _settings.PlayerCharacterRemovalThreshold, value); } + [Torch.Views.Display(Description = "Sets optimal distance in meters when spawning new players near others.", Name = "Optimal Spawn Distance", GroupName = "Players")] + public float OptimalSpawnDistance { get => _settings.OptimalSpawnDistance; set => SetValue(ref _settings.OptimalSpawnDistance, value); } + + [Torch.Views.Display(Description = "Enables automatic respawn at nearest available respawn point.", Name = "Enable Auto Respawn", GroupName = "Players")] + public bool EnableAutoRespawn { get => _settings.EnableAutorespawn; set => SetValue(ref _settings.EnableAutorespawn, value); } + + [Torch.Views.Display(Description = "The number of NPC factions generated on the start of the world.", Name = "NPC Factions Count", GroupName = "NPCs")] + public int TradeFactionsCount { get => _settings.TradeFactionsCount; set => SetValue(ref _settings.TradeFactionsCount, value); } + + [Torch.Views.Display(Description = "The inner radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Inner Radius", GroupName = "NPCs")] + public double StationsDistanceInnerRadius { get => _settings.StationsDistanceInnerRadius; set => SetValue(ref _settings.StationsDistanceInnerRadius, value); } + + [Torch.Views.Display(Description = "The outer radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Outer Radius Start", GroupName = "NPCs")] + public double StationsDistanceOuterRadiusStart { get => _settings.StationsDistanceOuterRadiusStart; set => SetValue(ref _settings.StationsDistanceOuterRadiusStart, value); } + + [Torch.Views.Display(Description = "The outer radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Outer Radius End", GroupName = "NPCs")] + public double StationsDistanceOuterRadiusEnd { get => _settings.StationsDistanceOuterRadiusEnd; set => SetValue(ref _settings.StationsDistanceOuterRadiusEnd, value); } + + [Torch.Views.Display(Description = "Time period between two economy updates in seconds.", Name = "Economy tick time", GroupName = "NPCs")] + public int EconomyTickInSeconds { get => _settings.EconomyTickInSeconds; set => SetValue(ref _settings.EconomyTickInSeconds, value); } + + [Torch.Views.Display(Description = "If enabled bounty contracts will be available on stations.", Name = "Enable Bounty Contracts", GroupName = "Players")] + public bool EnableBountyContracts { get => _settings.EnableBountyContracts; set => SetValue(ref _settings.EnableBountyContracts, value); } + + [Torch.Views.Display(Description = "Resource deposits count coefficient for generated world content (voxel generator version > 2).", Name = "Deposits Count Coefficient", GroupName = "Environment")] + public float DepositsCountCoefficient { get => _settings.DepositsCountCoefficient; set => SetValue(ref _settings.DepositsCountCoefficient, value); } + + [Torch.Views.Display(Description = "Resource deposit size denominator for generated world content (voxel generator version > 2).", Name = "Deposit Size Denominator", GroupName = "Environment")] + public float DepositSideDenominator { get => _settings.DepositSizeDenominator; set => SetValue(ref _settings.DepositSizeDenominator, value); } + + [Torch.Views.Display(Description = "Enables economy features.", Name = "Enable Economy", GroupName = "NPCs")] + public bool EnableEconomy { get => _settings.EnableEconomy; set => SetValue(ref _settings.EnableEconomy, value); } + + [Torch.Views.Display(Description = "Enables system for voxel reverting.", Name = "Enable Voxel Reverting", GroupName = "Trash Removal")] + public bool VoxelTrashRemovalEnabled { get => _settings.VoxelTrashRemovalEnabled; set => SetValue(ref _settings.VoxelTrashRemovalEnabled, value); } + + [Torch.Views.Display(Description = "Allows super gridding exploit to be used.", Name = "Enable Supergridding", GroupName = "Others")] + public bool EnableSupergridding { get => _settings.EnableSupergridding; set => SetValue(ref _settings.EnableSupergridding, value); } + public SessionSettingsViewModel(MyObjectBuilder_SessionSettings settings) { _settings = settings; diff --git a/Torch.Server/ViewModels/WorldConfigurationViewModel.cs b/Torch.Server/ViewModels/WorldConfigurationViewModel.cs new file mode 100644 index 0000000..40f704e --- /dev/null +++ b/Torch.Server/ViewModels/WorldConfigurationViewModel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using VRage.Game; + +namespace Torch.Server.ViewModels +{ + public class WorldConfigurationViewModel : ViewModel + { + private readonly MyObjectBuilder_WorldConfiguration _worldConfiguration; + private SessionSettingsViewModel _sessionSettings; + + public WorldConfigurationViewModel(MyObjectBuilder_WorldConfiguration worldConfiguration) + { + _worldConfiguration = worldConfiguration; + _sessionSettings = new SessionSettingsViewModel(worldConfiguration.Settings); + } + + public static implicit operator MyObjectBuilder_WorldConfiguration(WorldConfigurationViewModel model) + { + return model._worldConfiguration; + } + + public List Mods { get => _worldConfiguration.Mods; set => SetValue(ref _worldConfiguration.Mods, value); } + + public SessionSettingsViewModel Settings + { + get => _sessionSettings; + set + { + SetValue(ref _sessionSettings, value); + _worldConfiguration.Settings = _sessionSettings; + } + } + } +} \ No newline at end of file diff --git a/Torch.Server/Views/ChatControl.xaml b/Torch.Server/Views/ChatControl.xaml index a5bc39a..79c1148 100644 --- a/Torch.Server/Views/ChatControl.xaml +++ b/Torch.Server/Views/ChatControl.xaml @@ -17,8 +17,8 @@ - - + + diff --git a/Torch.Server/Views/ChatControl.xaml.cs b/Torch.Server/Views/ChatControl.xaml.cs index 242a8d1..ac9f834 100644 --- a/Torch.Server/Views/ChatControl.xaml.cs +++ b/Torch.Server/Views/ChatControl.xaml.cs @@ -20,6 +20,8 @@ using NLog; using Torch; using Sandbox; using Sandbox.Engine.Multiplayer; +using Sandbox.Game.Gui; +using Sandbox.Game.Multiplayer; using Sandbox.Game.World; using Torch.API; using Torch.API.Managers; @@ -131,6 +133,15 @@ namespace Torch.Server bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight; var span = new Span(); span.Inlines.Add($"{msg.Timestamp} "); + switch (msg.Channel) + { + case ChatChannel.Faction: + span.Inlines.Add(new Run($"[{MySession.Static.Factions.TryGetFactionById(msg.Target)?.Tag ?? "???"}] ") { Foreground = Brushes.Green }); + break; + case ChatChannel.Private: + span.Inlines.Add(new Run($"[to {MySession.Static.Players.TryGetIdentity(msg.Target)?.DisplayName ?? "???"}] ") { Foreground = Brushes.DeepPink }); + break; + } span.Inlines.Add(new Run(msg.Author) { Foreground = LookupBrush(msg.Font) }); span.Inlines.Add($": {msg.Message}"); span.Inlines.Add(new LineBreak()); @@ -163,10 +174,18 @@ namespace Torch.Server var commands = _server.CurrentSession?.Managers.GetManager(); if (commands != null && commands.IsCommand(text)) { - InsertMessage(new TorchChatMessage("Server", text, MyFontEnum.DarkBlue)); + InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, text, TorchBase.Instance.Config.ChatColor)); _server.Invoke(() => { - commands.HandleCommandFromServer(text); + var responses = commands.HandleCommandFromServer(text); + if (responses == null) + { + InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, "Invalid command.", TorchBase.Instance.Config.ChatColor)); + return; + } + + foreach (var response in responses) + InsertMessage(response); }); } else diff --git a/Torch.Server/Views/ConfigControl.xaml b/Torch.Server/Views/ConfigControl.xaml index f6c7f04..79c856f 100644 --- a/Torch.Server/Views/ConfigControl.xaml +++ b/Torch.Server/Views/ConfigControl.xaml @@ -27,6 +27,7 @@