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.
-[](https://www.patreon.com/bePatron?u=847269)!
+[](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 @@
+
@@ -57,70 +58,77 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Torch.Server/Views/ConfigControl.xaml.cs b/Torch.Server/Views/ConfigControl.xaml.cs
index 33dde95..69cc551 100644
--- a/Torch.Server/Views/ConfigControl.xaml.cs
+++ b/Torch.Server/Views/ConfigControl.xaml.cs
@@ -12,6 +12,8 @@ using Torch.API.Managers;
using Torch.Server.Annotations;
using Torch.Server.Managers;
using Torch.Server.ViewModels;
+using Torch.Views;
+using VRage.Game.ModAPI;
namespace Torch.Server.Views
{
@@ -116,5 +118,28 @@ namespace Torch.Server.Views
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
+
+ private void NewWorld_OnClick(object sender, RoutedEventArgs e)
+ {
+ var c = new WorldGeneratorDialog(_instanceManager);
+ c.Show();
+ }
+
+ private void RoleEdit_Onlick(object sender, RoutedEventArgs e)
+ {
+ //var w = new RoleEditor(_instanceManager.DedicatedConfig.SelectedWorld);
+ //w.Show();
+ var d = new RoleEditor();
+ var w = _instanceManager.DedicatedConfig.SelectedWorld;
+
+ if (w == null)
+ {
+ MessageBox.Show("A world is not selected.");
+ return;
+ }
+
+ d.Edit(w.Checkpoint.PromotedUsers.Dictionary);
+ _instanceManager.DedicatedConfig.Administrators = w.Checkpoint.PromotedUsers.Dictionary.Where(k => k.Value >= MyPromoteLevel.Admin).Select(k => k.Key.ToString()).ToList();
+ }
}
}
diff --git a/Torch.Server/Views/Converters/ListConverterWorkshopId.cs b/Torch.Server/Views/Converters/ListConverterWorkshopId.cs
new file mode 100644
index 0000000..f58afac
--- /dev/null
+++ b/Torch.Server/Views/Converters/ListConverterWorkshopId.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+using Torch.Server.ViewModels;
+using VRage.Game;
+
+namespace Torch.Server.Views.Converters
+{
+ class ListConverterWorkshopId : IValueConverter
+ {
+ public Type Type { get; set; }
+
+ ///
+ /// Converts a list of ModItemInfo objects into a list of their workshop IDs (PublishedFileIds).
+ ///
+ ///
+ /// Expected to contain a list of ModItemInfo objects
+ ///
+ /// This parameter will be ignored
+ /// This parameter will be ignored
+ /// This parameter will be ignored
+ /// A string containing the workshop ids of all mods, one per line
+ public object Convert(object valueList, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (!(valueList is IList list))
+ throw new InvalidOperationException("Value is not the proper type.");
+
+ var sb = new StringBuilder();
+ foreach (var item in list)
+ {
+ sb.AppendLine(((ModItemInfo) item).PublishedFileId.ToString());
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Converts a list of workshop ids into a list of ModItemInfo objects
+ ///
+ /// A string containing workshop ids separated by new lines
+ /// This parameter will be ignored
+ ///
+ /// A list of ModItemInfos which should
+ /// contain the requestted mods
+ /// (or they will be dropped)
+ ///
+ /// This parameter will be ignored
+ /// A list of ModItemInfo objects
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(Type));
+ var mods = parameter as ICollection;
+ if (mods == null)
+ throw new ArgumentException("parameter needs to be of type ICollection!");
+ var input = ((string)value).Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var item in input)
+ {
+ if( ulong.TryParse(item, out ulong id))
+ {
+ var mod = mods.FirstOrDefault((m) => m.PublishedFileId == id);
+ if (mod != null)
+ list.Add(mod);
+ else
+ list.Add(new MyObjectBuilder_Checkpoint.ModItem(id));
+ }
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/Torch.Server/Views/Converters/ModToIdConverter.cs b/Torch.Server/Views/Converters/ModToIdConverter.cs
new file mode 100644
index 0000000..512bbf2
--- /dev/null
+++ b/Torch.Server/Views/Converters/ModToIdConverter.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Windows.Data;
+using System.Threading.Tasks;
+using Torch.Server.ViewModels;
+using NLog;
+using Torch.Collections;
+
+namespace Torch.Server.Views.Converters
+{
+ ///
+ /// A converter to get the index of a ModItemInfo object within a collection of ModItemInfo objects
+ ///
+ public class ModToListIdConverter : IMultiValueConverter
+ {
+ ///
+ /// Converts a ModItemInfo object into its index within a Collection of ModItemInfo objects
+ ///
+ ///
+ /// Expected to contain a ModItemInfo object at index 0
+ /// and a Collection of ModItemInfo objects at index 1
+ ///
+ /// This parameter will be ignored
+ /// This parameter will be ignored
+ /// This parameter will be ignored
+ /// the index of the mod within the provided mod list.
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ //if (targetType != typeof(int))
+ // throw new NotSupportedException("ModToIdConverter can only convert mods into int values or vise versa!");
+ var mod = (ModItemInfo) values[0];
+ var theModList = (MtObservableList) values[1];
+ return theModList.IndexOf(mod);
+ }
+
+ ///
+ /// It is not supported to reverse this converter
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Raises a NotSupportedException
+ public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException("ModToIdConverter can not convert back!");
+ }
+ }
+}
diff --git a/Torch.Server/Views/EntitiesControl.xaml b/Torch.Server/Views/EntitiesControl.xaml
index d7472f0..e75c507 100644
--- a/Torch.Server/Views/EntitiesControl.xaml
+++ b/Torch.Server/Views/EntitiesControl.xaml
@@ -22,10 +22,11 @@
+
+ TreeViewItem.Expanded="TreeViewItem_OnExpanded" Name="EntityTree">
@@ -46,46 +47,47 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
diff --git a/Torch.Server/Views/EntitiesControl.xaml.cs b/Torch.Server/Views/EntitiesControl.xaml.cs
index 1095912..4cdfa45 100644
--- a/Torch.Server/Views/EntitiesControl.xaml.cs
+++ b/Torch.Server/Views/EntitiesControl.xaml.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -13,6 +14,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using NLog;
+using Torch.Collections;
using Torch.Server.ViewModels;
using Torch.Server.ViewModels.Blocks;
using Torch.Server.ViewModels.Entities;
@@ -29,14 +31,17 @@ namespace Torch.Server.Views
{
public EntityTreeViewModel Entities { get; set; }
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
public EntitiesControl()
{
InitializeComponent();
Entities = new EntityTreeViewModel(this);
DataContext = Entities;
Entities.Init();
+ SortCombo.ItemsSource = Enum.GetNames(typeof(EntityTreeViewModel.SortEnum));
}
-
+
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs
diff --git a/Torch.Server/Views/PluginsControl.xaml.cs b/Torch.Server/Views/PluginsControl.xaml.cs
index 3000f9c..dbdc623 100644
--- a/Torch.Server/Views/PluginsControl.xaml.cs
+++ b/Torch.Server/Views/PluginsControl.xaml.cs
@@ -41,7 +41,9 @@ namespace Torch.Server.Views
{
if (propertyChangedEventArgs.PropertyName == nameof(PluginManagerViewModel.SelectedPlugin))
{
- if (((PluginManagerViewModel)DataContext).SelectedPlugin.Control is PropertyGrid)
+ var plugin = ((PluginManagerViewModel)DataContext).SelectedPlugin;
+
+ if (plugin.Control is PropertyGrid || !plugin.Control.GetScrollContainer())
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
else
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
@@ -71,5 +73,11 @@ namespace Torch.Server.Views
if (_plugins?.PluginDir != null)
Process.Start(_plugins.PluginDir);
}
+
+ private void BrowsPlugins_OnClick(object sender, RoutedEventArgs e)
+ {
+ var browser = new PluginBrowser();
+ browser.Show();
+ }
}
}
diff --git a/Torch.Server/Views/Resources.xaml b/Torch.Server/Views/Resources.xaml
index 4f313ed..14154a7 100644
--- a/Torch.Server/Views/Resources.xaml
+++ b/Torch.Server/Views/Resources.xaml
@@ -18,5 +18,7 @@
+
+
\ No newline at end of file
diff --git a/Torch.Server/Views/RoleEditor.xaml b/Torch.Server/Views/RoleEditor.xaml
new file mode 100644
index 0000000..24d100e
--- /dev/null
+++ b/Torch.Server/Views/RoleEditor.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Torch.Server/Views/RoleEditor.xaml.cs b/Torch.Server/Views/RoleEditor.xaml.cs
new file mode 100644
index 0000000..d1106ce
--- /dev/null
+++ b/Torch.Server/Views/RoleEditor.xaml.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+using Torch.Server.Managers;
+using Torch.Views;
+using VRage.Game.ModAPI;
+
+namespace Torch.Server.Views
+{
+ ///
+ /// Interaction logic for RoleEditor.xaml
+ ///
+ public partial class RoleEditor : Window
+ {
+ public RoleEditor()
+ {
+ InitializeComponent();
+ DataContext = Items;
+ }
+
+ public ObservableCollection Items { get; } = new ObservableCollection();
+ private Type _itemType;
+
+ private Action _commitChanges;
+ public MyPromoteLevel BulkPromote { get; set; } = MyPromoteLevel.Scripter;
+
+ public void Edit(IDictionary dict)
+ {
+ Items.Clear();
+ var dictType = dict.GetType();
+ _itemType = typeof(DictionaryItem<,>).MakeGenericType(dictType.GenericTypeArguments[0], dictType.GenericTypeArguments[1]);
+
+ foreach (var key in dict.Keys)
+ {
+ Items.Add((IDictionaryItem)Activator.CreateInstance(_itemType, key, dict[key]));
+ }
+
+ ItemGrid.ItemsSource = Items;
+
+ _commitChanges = () =>
+ {
+ dict.Clear();
+ foreach (var item in Items)
+ {
+ dict[item.Key] = item.Value;
+ }
+ };
+
+ Show();
+ }
+
+ private void Cancel_OnClick(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ private void Ok_OnClick(object sender, RoutedEventArgs e)
+ {
+ _commitChanges?.Invoke();
+ Close();
+ }
+
+ public interface IDictionaryItem
+ {
+ object Key { get; set; }
+ object Value { get; set; }
+ }
+
+ public class DictionaryItem : ViewModel, IDictionaryItem
+ {
+ private TKey _key;
+ private TValue _value;
+
+ object IDictionaryItem.Key { get => _key; set => SetValue(ref _key, (TKey)value); }
+ object IDictionaryItem.Value { get => _value; set => SetValue(ref _value, (TValue)value); }
+
+ public TKey Key { get => _key; set => SetValue(ref _key, value); }
+ public TValue Value { get => _value; set => SetValue(ref _value, value); }
+
+ public DictionaryItem()
+ {
+ _key = default(TKey);
+ _value = default(TValue);
+ }
+
+ public DictionaryItem(TKey key, TValue value)
+ {
+ _key = key;
+ _value = value;
+ }
+ }
+
+ private void AddNew_OnClick(object sender, RoutedEventArgs e)
+ {
+ Items.Add((IDictionaryItem)Activator.CreateInstance(_itemType));
+ }
+
+ private void BulkEdit(object sender, RoutedEventArgs e)
+ {
+ List l = Items.Where(i => i.Value.Equals(BulkPromote)).Select(i => (ulong)i.Key).ToList();
+ var w = new CollectionEditor();
+ w.Edit((ICollection)l, "Bulk edit");
+ var r = Items.Where(j => j.Value.Equals(BulkPromote) || l.Contains((ulong)j.Key)).ToList();
+ foreach (var k in r)
+ Items.Remove(k);
+ foreach (var m in l)
+ Items.Add(new DictionaryItem(m, BulkPromote));
+ }
+ }
+}
diff --git a/Torch.Server/Views/TorchUI.xaml b/Torch.Server/Views/TorchUI.xaml
index ce10d64..6a402d0 100644
--- a/Torch.Server/Views/TorchUI.xaml
+++ b/Torch.Server/Views/TorchUI.xaml
@@ -57,7 +57,7 @@
@@ -66,7 +66,7 @@
-
+
@@ -80,6 +80,9 @@
+
+
+
diff --git a/Torch.Server/Views/TorchUI.xaml.cs b/Torch.Server/Views/TorchUI.xaml.cs
index 29a8fdf..88c97a0 100644
--- a/Torch.Server/Views/TorchUI.xaml.cs
+++ b/Torch.Server/Views/TorchUI.xaml.cs
@@ -10,6 +10,7 @@ using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
+using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
@@ -31,8 +32,13 @@ namespace Torch.Server
private TorchServer _server;
private TorchConfig _config;
+ private bool _autoscrollLog = true;
+
public TorchUI(TorchServer server)
{
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+ Width = 800;
+ Height = 600;
_config = (TorchConfig)server.Config;
_server = server;
//TODO: data binding for whole server
@@ -41,10 +47,10 @@ namespace Torch.Server
AttachConsole();
- Left = _config.WindowPosition.X;
- Top = _config.WindowPosition.Y;
- Width = _config.WindowSize.X;
- Height = _config.WindowSize.Y;
+ //Left = _config.WindowPosition.X;
+ //Top = _config.WindowPosition.Y;
+ //Width = _config.WindowSize.X;
+ //Height = _config.WindowSize.Y;
Chat.BindServer(server);
PlayerList.BindServer(server);
@@ -54,6 +60,14 @@ namespace Torch.Server
Themes.uiSource = this;
Themes.SetConfig(_config);
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
+
+ Loaded += TorchUI_Loaded;
+ }
+
+ private void TorchUI_Loaded(object sender, RoutedEventArgs e)
+ {
+ var scrollViewer = FindDescendant(ConsoleText);
+ scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
}
private void AttachConsole()
@@ -66,7 +80,52 @@ namespace Torch.Server
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document;
}
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!")));
- ConsoleText.TextChanged += (sender, args) => ConsoleText.ScrollToEnd();
+ ConsoleText.TextChanged += ConsoleText_OnTextChanged;
+ }
+
+ public static T FindDescendant(DependencyObject obj) where T : DependencyObject
+ {
+ if (obj == null) return default(T);
+ int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
+ if (numberChildren == 0) return default(T);
+
+ for (int i = 0; i < numberChildren; i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(obj, i);
+ if (child is T)
+ {
+ return (T)child;
+ }
+ }
+
+ for (int i = 0; i < numberChildren; i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(obj, i);
+ var potentialMatch = FindDescendant(child);
+ if (potentialMatch != default(T))
+ {
+ return potentialMatch;
+ }
+ }
+
+ return default(T);
+ }
+
+ private void ConsoleText_OnTextChanged(object sender, TextChangedEventArgs args)
+ {
+ var textBox = (RichTextBox) sender;
+ if (_autoscrollLog)
+ ConsoleText.ScrollToEnd();
+ }
+
+ private void ConsoleText_OnScrollChanged(object sender, ScrollChangedEventArgs e)
+ {
+ var scrollViewer = (ScrollViewer) sender;
+ if (e.ExtentHeightChange == 0)
+ {
+ // User change.
+ _autoscrollLog = scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight;
+ }
}
public void LoadConfig(TorchConfig config)
@@ -97,10 +156,14 @@ namespace Torch.Server
protected override void OnClosing(CancelEventArgs e)
{
- var newSize = new Point((int)Width, (int)Height);
- _config.WindowSize = newSize;
- var newPos = new Point((int)Left, (int)Top);
- _config.WindowPosition = newPos;
+ // Can't save here or you'll persist all the command line arguments
+ //
+ //var newSize = new Point((int)Width, (int)Height);
+ //_config.WindowSize = newSize;
+ //var newPos = new Point((int)Left, (int)Top);
+ //_config.WindowPosition = newPos;
+
+ //_config.Save(); //you idiot
if (_server?.State == ServerState.Running)
_server.Stop();
diff --git a/Torch.Server/Views/WorldGeneratorDialog.xaml b/Torch.Server/Views/WorldGeneratorDialog.xaml
index c06deae..65fec77 100644
--- a/Torch.Server/Views/WorldGeneratorDialog.xaml
+++ b/Torch.Server/Views/WorldGeneratorDialog.xaml
@@ -5,14 +5,15 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server"
xmlns:views="clr-namespace:Torch.Server.Views"
+ xmlns:views1="clr-namespace:Torch.Views;assembly=Torch"
mc:Ignorable="d"
- Title="WorldGeneratorDialog" Height="300" Width="700">
+ Title="WorldGeneratorDialog" Height="500" Width="700">
-
+
-
+
@@ -26,13 +27,18 @@
-
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/Torch.Server/Views/WorldGeneratorDialog.xaml.cs b/Torch.Server/Views/WorldGeneratorDialog.xaml.cs
index 0dc3c4b..d46019a 100644
--- a/Torch.Server/Views/WorldGeneratorDialog.xaml.cs
+++ b/Torch.Server/Views/WorldGeneratorDialog.xaml.cs
@@ -13,7 +13,15 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using NLog;
using Sandbox.Definitions;
+using Sandbox.Engine.Networking;
+using Sandbox.Game.World;
using Torch.Server.Managers;
+using Torch.Server.ViewModels;
+using Torch.Utils;
+using VRage;
+using VRage.Dedicated;
+using VRage.FileSystem;
+using VRage.Game;
using VRage.Game.Localization;
using VRage.Utils;
@@ -26,19 +34,25 @@ namespace Torch.Server
{
private InstanceManager _instanceManager;
private List _checkpoints = new List();
+ private PremadeCheckpointItem _currentItem;
+
+ [ReflectedStaticMethod(Type = typeof(ConfigForm), Name = "LoadLocalization")]
+ private static Action _loadLocalization;
public WorldGeneratorDialog(InstanceManager instanceManager)
{
_instanceManager = instanceManager;
InitializeComponent();
-
- MyDefinitionManager.Static.LoadScenarios();
- var scenarios = MyDefinitionManager.Static.GetScenarioDefinitions();
- MyDefinitionManager.Static.UnloadData();
- foreach (var scenario in scenarios)
+ _loadLocalization();
+ var scenarios = MyLocalCache.GetAvailableWorldInfos(Path.Combine(MyFileSystem.ContentPath, "CustomWorlds"));
+ foreach (var tup in scenarios)
{
- //TODO: Load localization
- _checkpoints.Add(new PremadeCheckpointItem { Name = scenario.DisplayNameText, Icon = @"C:\Users\jgross\Documents\Projects\TorchAPI\Torch\bin\x64\Release\Content\CustomWorlds\Empty World\thumb.jpg" });
+ string directory = tup.Item1;
+ MyWorldInfo info = tup.Item2;
+ string localizedName = MyTexts.GetString(MyStringId.GetOrCompute(info.SessionName));
+ var checkpoint = MyLocalCache.LoadCheckpoint(directory, out _);
+ checkpoint.OnlineMode = MyOnlineModeEnum.PUBLIC;
+ _checkpoints.Add(new PremadeCheckpointItem { Name = localizedName, Icon = Path.Combine(directory, "thumb.jpg"), Path = directory, Checkpoint = checkpoint});
}
/*
@@ -56,23 +70,39 @@ namespace Torch.Server
}*/
PremadeCheckpoints.ItemsSource = _checkpoints;
}
-
+
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
- /*
- var worldPath = Path.Combine("Instance", "Saves", WorldName.Text);
- var checkpointItem = (PremadeCheckpointItem)PremadeCheckpoints.SelectedItem;
+ string worldName = string.IsNullOrEmpty(WorldName.Text) ? _currentItem.Name : WorldName.Text;
+
+ var worldPath = Path.Combine(TorchBase.Instance.Config.InstancePath, "Saves", worldName);
+ var checkpoint = _currentItem.Checkpoint;
if (Directory.Exists(worldPath))
{
MessageBox.Show("World already exists with that name.");
return;
}
Directory.CreateDirectory(worldPath);
- foreach (var file in Directory.EnumerateFiles(checkpointItem.Path, "*", SearchOption.AllDirectories))
+ foreach (var file in Directory.EnumerateFiles(_currentItem.Path, "*", SearchOption.AllDirectories))
{
- File.Copy(file, Path.Combine(worldPath, file.Replace($"{checkpointItem.Path}\\", "")));
+ File.Copy(file, Path.Combine(worldPath, file.Replace($"{_currentItem.Path}\\", "")));
}
- _instanceManager.SelectWorld(worldPath, false);*/
+
+ checkpoint.SessionName = worldName;
+
+ MyLocalCache.SaveCheckpoint(checkpoint, worldPath);
+
+
+ _instanceManager.SelectWorld(worldPath, false);
+ _instanceManager.ImportSelectedWorldConfig();
+ Close();
+ }
+
+ private void PremadeCheckpoints_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var selected = (PremadeCheckpointItem)PremadeCheckpoints.SelectedItem;
+ _currentItem = selected;
+ SettingsView.DataContext = new SessionSettingsViewModel(_currentItem.Checkpoint.Settings);
}
}
@@ -81,5 +111,6 @@ namespace Torch.Server
public string Path { get; set; }
public string Name { get; set; }
public string Icon { get; set; }
+ public MyObjectBuilder_Checkpoint Checkpoint { get; set; }
}
}
diff --git a/Torch.Server/packages.config b/Torch.Server/packages.config
index efe1c27..b1ff266 100644
--- a/Torch.Server/packages.config
+++ b/Torch.Server/packages.config
@@ -2,7 +2,14 @@
+
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Torch.Tests/Torch.Tests.csproj b/Torch.Tests/Torch.Tests.csproj
index 50977de..f4cf215 100644
--- a/Torch.Tests/Torch.Tests.csproj
+++ b/Torch.Tests/Torch.Tests.csproj
@@ -82,6 +82,7 @@
+
diff --git a/Torch.Tests/app.config b/Torch.Tests/app.config
new file mode 100644
index 0000000..a73892d
--- /dev/null
+++ b/Torch.Tests/app.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Torch.sln b/Torch.sln
index 9f2f29b..2cca71d 100644
--- a/Torch.sln
+++ b/Torch.sln
@@ -14,6 +14,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7AD02A71-1D4C-48F9-A8C1-789A5512424F}"
ProjectSection(SolutionItems) = preProject
NLog.config = NLog.config
+ NLog-user.config = NLog-user.config
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Tests", "Torch.Tests\Torch.Tests.csproj", "{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}"
@@ -55,10 +56,8 @@ Global
{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|Any CPU.ActiveCfg = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.ActiveCfg = Debug|x64
- {E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|Any CPU.ActiveCfg = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.ActiveCfg = Release|x64
- {E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = Release|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|Any CPU.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64
@@ -79,10 +78,8 @@ Global
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|Any CPU.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.ActiveCfg = Debug|x64
- {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|Any CPU.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.ActiveCfg = Release|x64
- {632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Torch/Collections/BinaryMinHeap.cs b/Torch/Collections/BinaryMinHeap.cs
new file mode 100644
index 0000000..78f0294
--- /dev/null
+++ b/Torch/Collections/BinaryMinHeap.cs
@@ -0,0 +1,272 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Torch.Collections
+{
+ public class BinaryMinHeap where TKey : IComparable
+ {
+ private struct HeapItem
+ {
+ public TKey Key { get; }
+ public TValue Value { get; }
+
+ public HeapItem(TKey key, TValue value)
+ {
+ Key = key;
+ Value = value;
+ }
+ }
+
+ private HeapItem[] _store;
+ private readonly IComparer _comparer;
+
+ public int Capacity { get; private set; }
+ public int Count { get; private set; }
+
+ public bool Full => Count == Capacity;
+
+ public BinaryMinHeap(int initialCapacity = 32, IComparer comparer = null)
+ {
+ _store = new HeapItem[initialCapacity];
+ Count = 0;
+ Capacity = initialCapacity;
+ _comparer = comparer ?? Comparer.Default;
+ }
+
+ public void Insert(TValue value, TKey key)
+ {
+ EnsureCapacity(Capacity + 1);
+
+ var item = new HeapItem(key, value);
+
+ _store[Count] = item;
+
+ Up(Count);
+ Count++;
+ }
+
+ public TValue Min()
+ {
+ return _store[0].Value;
+ }
+
+ public TKey MinKey()
+ {
+ return _store[0].Key;
+ }
+
+ public TValue RemoveMin()
+ {
+ TValue toReturn = _store[0].Value;
+
+ if (Count != 1)
+ {
+ SwapIndices(Count - 1, 0);
+ _store[Count - 1] = default(HeapItem);
+ Count--;
+ Down(0);
+ }
+ else
+ {
+ Count--;
+ _store[0] = default(HeapItem);
+ }
+
+ return toReturn;
+ }
+
+ public TValue RemoveMax()
+ {
+ Debug.Assert(Count > 0);
+
+ var maxIndex = 0;
+
+ var maxItem = _store[0];
+
+ for (var i = 1; i < Count; ++i)
+ {
+ var c = _store[i];
+ if (_comparer.Compare(maxItem.Key, c.Key) < 0)
+ {
+ maxIndex = i;
+ maxItem = c;
+ }
+ }
+
+ if (maxIndex != Count)
+ {
+ SwapIndices(Count - 1, maxIndex);
+ Up(maxIndex);
+ }
+ Count--;
+
+ return maxItem.Value;
+ }
+
+ public TValue Remove(TValue value, IEqualityComparer comparer = null)
+ {
+ if (Count == 0)
+ return default(TValue);
+
+ if (comparer == null)
+ comparer = EqualityComparer.Default;
+
+ var itemIndex = -1;
+
+ for (var i = 0; i < Count; ++i)
+ {
+ if (comparer.Equals(value, _store[i].Value))
+ {
+ itemIndex = i;
+ break;
+ }
+ }
+
+ if (itemIndex != Count && itemIndex != -1)
+ {
+ TValue removed = _store[itemIndex].Value;
+
+ SwapIndices(Count - 1, itemIndex);
+ Up(itemIndex);
+ Down(itemIndex);
+
+ Count--;
+ return removed;
+ }
+ else
+ return default(TValue);
+ }
+
+ public TValue Remove(TKey key)
+ {
+ Debug.Assert(Count > 0);
+
+ var itemIndex = 0;
+
+ for (var i = 1; i < Count; ++i)
+ {
+ if (_comparer.Compare(key, _store[i].Key) == 0)
+ itemIndex = i;
+ }
+
+ TValue removed;
+
+ if (itemIndex != Count)
+ {
+ removed = _store[itemIndex].Value;
+
+ SwapIndices(Count - 1, itemIndex);
+ Up(itemIndex);
+ Down(itemIndex);
+ }
+ else
+ removed = default(TValue);
+
+ Count--;
+
+ return removed;
+ }
+
+ public void Clear()
+ {
+ Array.Clear(_store, 0, Capacity);
+ Count = 0;
+ }
+
+ private void Up(int index)
+ {
+ if (index == 0)
+ return;
+ int parentIndex = (index - 1) / 2;
+ HeapItem swap = _store[index];
+ if (_comparer.Compare(_store[parentIndex].Key, swap.Key) <= 0)
+ return;
+
+ while (true)
+ {
+ SwapIndices(parentIndex, index);
+ index = parentIndex;
+
+ if (index == 0)
+ break;
+ parentIndex = (index - 1) / 2;
+ if (_comparer.Compare(_store[parentIndex].Key, swap.Key) <= 0)
+ break;
+ }
+
+ InsertItem(ref swap, index);
+ }
+
+ private void Down(int index)
+ {
+ if (Count == index + 1)
+ return;
+
+ int left = index * 2 + 1;
+ int right = left + 1;
+
+ HeapItem swap = _store[index];
+
+ while (right <= Count) // While the current node has children
+ {
+ var nLeft = _store[left];
+ var nRight = _store[right];
+
+ if (right == Count || _comparer.Compare(nLeft.Key, nRight.Key) < 0) // Only the left child exists or the left child is smaller
+ {
+ if (_comparer.Compare(swap.Key, nLeft.Key) <= 0)
+ break;
+
+ SwapIndices(left, index);
+
+ index = left;
+ left = index * 2 + 1;
+ right = left + 1;
+ }
+ else // Right child exists and is smaller
+ {
+ if (_comparer.Compare(swap.Key, nRight.Key) <= 0)
+ break;
+
+ SwapIndices(right, index);
+
+ index = right;
+ left = index * 2 + 1;
+ right = left + 1;
+ }
+ }
+
+ InsertItem(ref swap, index);
+ }
+
+ private void SwapIndices(int fromIndex, int toIndex)
+ {
+ _store[toIndex] = _store[fromIndex];
+ }
+
+ private void InsertItem(ref HeapItem fromItem, int toIndex)
+ {
+ _store[toIndex] = fromItem;
+ }
+
+ public void EnsureCapacity(int capacity)
+ {
+ if (_store.Length >= capacity)
+ return;
+
+ //double capacity until we reach the minimum requested capacity (or greater)
+ int newcap = Capacity * 2;
+ while (newcap < capacity)
+ newcap *= 2;
+
+ var newArray = new HeapItem[newcap];
+ Array.Copy(_store, newArray, Capacity);
+
+ _store = newArray;
+ Capacity = newcap;
+ }
+ }
+}
diff --git a/Torch/Collections/MtObservableCollectionBase.cs b/Torch/Collections/MtObservableCollectionBase.cs
index 580e682..0473e8d 100644
--- a/Torch/Collections/MtObservableCollectionBase.cs
+++ b/Torch/Collections/MtObservableCollectionBase.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -123,10 +124,10 @@ namespace Torch.Collections
private readonly Timer _flushEventQueue;
private const int _eventRaiseDelay = 50;
- private readonly Queue _collectionEventQueue =
- new Queue();
+ private readonly ConcurrentQueue _collectionEventQueue =
+ new ConcurrentQueue();
- private readonly Queue _propertyEventQueue = new Queue();
+ private readonly ConcurrentQueue _propertyEventQueue = new ConcurrentQueue();
private void FlushEventQueue(object data)
{
@@ -137,7 +138,8 @@ namespace Torch.Collections
// :/, but works better
bool reset = _collectionEventQueue.Count > 0;
if (reset)
- _collectionEventQueue.Clear();
+ while (_collectionEventQueue.Count > 0)
+ _collectionEventQueue.TryDequeue(out _);
else
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
_collectionChangedEvent.Raise(this, e);
diff --git a/Torch/Collections/MtObservableList.cs b/Torch/Collections/MtObservableList.cs
index af21eb0..e846b3e 100644
--- a/Torch/Collections/MtObservableList.cs
+++ b/Torch/Collections/MtObservableList.cs
@@ -13,7 +13,7 @@ namespace Torch.Collections
/// Multithread safe, observable list
///
/// Value type
- public class MtObservableList : MtObservableCollection, T>, IList, IList
+ public class MtObservableList : MtObservableCollection, T>, IList, IList
{
///
/// Initializes a new instance of the MtObservableList class that is empty and has the default initial capacity.
@@ -114,16 +114,34 @@ namespace Torch.Collections
using (Lock.WriteUsing())
{
comparer = comparer ?? Comparer.Default;
- if (Backing is List lst)
- lst.Sort(new TransformComparer(selector, comparer));
- else
- {
- List sortedItems = Backing.OrderBy(selector, comparer).ToList();
- Backing.Clear();
- foreach (T v in sortedItems)
- Backing.Add(v);
- }
+ Backing.Sort(new TransformComparer(selector, comparer));
}
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move));
+ }
+
+ ///
+ /// Sorts the list using the given comparer./>
+ ///
+ public void Sort(IComparer comparer)
+ {
+ using (DeferredUpdate())
+ using (Lock.WriteUsing())
+ {
+ Backing.Sort(comparer);
+ }
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move));
+ }
+
+ ///
+ /// Searches the entire list for an element using the specified comparer and returns the zero-based index of the element.
+ ///
+ ///
+ ///
+ ///
+ public int BinarySearch(T item, IComparer comparer = null)
+ {
+ using(Lock.ReadUsing())
+ return Backing.BinarySearch(item, comparer ?? Comparer.Default);
}
///
diff --git a/Torch/Collections/SortedView.cs b/Torch/Collections/SortedView.cs
new file mode 100644
index 0000000..392e0dc
--- /dev/null
+++ b/Torch/Collections/SortedView.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Havok;
+using NLog;
+
+namespace Torch.Collections
+{
+ public class SortedView: IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged
+ {
+ private readonly Logger _log = LogManager.GetCurrentClassLogger();
+ private readonly MtObservableCollectionBase _backing;
+ private IComparer _comparer;
+ private readonly List _store;
+
+ public SortedView(MtObservableCollectionBase backing, IComparer comparer)
+ {
+ _comparer = comparer;
+ _backing = backing;
+ _store = new List(_backing.Count);
+ _store.AddRange(_backing);
+ _backing.CollectionChanged += backing_CollectionChanged;
+ _backing.PropertyChanged += backing_PropertyChanged;
+ }
+
+ private void backing_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ OnPropertyChanged(e.PropertyName);
+ }
+
+ private void backing_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ InsertSorted(e.NewItems);
+ CollectionChanged?.Invoke(this, e);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ _store.RemoveAll(r => e.OldItems.Contains(r));
+ CollectionChanged?.Invoke(this, e);
+ break;
+ case NotifyCollectionChangedAction.Move:
+ case NotifyCollectionChangedAction.Replace:
+ case NotifyCollectionChangedAction.Reset:
+ Refresh();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _store.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public int Count => _backing.Count;
+
+ private void InsertSorted(IEnumerable items)
+ {
+ foreach (var t in items)
+ InsertSorted((T)t);
+ }
+
+ private int InsertSorted(T item, IComparer comparer = null)
+ {
+ if (comparer == null)
+ comparer = _comparer;
+
+ if (_store.Count == 0 || comparer == null)
+ {
+ _store.Add(item);
+ return 0;
+ }
+ if(comparer.Compare(_store[_store.Count - 1], item) <= 0)
+ {
+ _store.Add(item);
+ return _store.Count - 1;
+ }
+ if(comparer.Compare(_store[0], item) >= 0)
+ {
+ _store.Insert(0, item);
+ return 0;
+ }
+ int index = _store.BinarySearch(item);
+ if (index < 0)
+ index = ~index;
+ _store.Insert(index, item);
+ return index;
+ }
+
+ public void Sort(IComparer comparer = null)
+ {
+ if (comparer == null)
+ comparer = _comparer;
+
+ if (comparer == null)
+ return;
+
+ _store.Sort(comparer);
+
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ public void Refresh()
+ {
+ _store.Clear();
+ //_store.AddRange(_backing);
+ _store.EnsureCapacity(_backing.Count);
+ foreach (var e in _backing)
+ _store.Add(e);
+ Sort();
+ }
+
+ public void SetComparer(IComparer comparer, bool resort = true)
+ {
+ _comparer = comparer;
+ if(resort)
+ Sort();
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/Torch/Collections/SystemSortedView.cs b/Torch/Collections/SystemSortedView.cs
new file mode 100644
index 0000000..d1e3f14
--- /dev/null
+++ b/Torch/Collections/SystemSortedView.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Torch.Collections
+{
+ public class SystemSortedView : IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged
+ {
+ private readonly ObservableCollection _backing;
+ private IComparer _comparer;
+ private readonly List _store;
+
+ public SystemSortedView(ObservableCollection backing, IComparer comparer)
+ {
+ _comparer = comparer;
+ _backing = backing;
+ _store = new List(_backing.Count);
+ _store.AddRange(_backing);
+ _backing.CollectionChanged += backing_CollectionChanged;
+ }
+
+ private void backing_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ InsertSorted(e.NewItems);
+ CollectionChanged?.Invoke(this, e);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ _store.RemoveAll(r => e.OldItems.Contains(r));
+ CollectionChanged?.Invoke(this, e);
+ break;
+ case NotifyCollectionChangedAction.Move:
+ case NotifyCollectionChangedAction.Replace:
+ case NotifyCollectionChangedAction.Reset:
+ Refresh();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _store.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public int Count => _backing.Count;
+
+ private void InsertSorted(IEnumerable items)
+ {
+ foreach (var t in items)
+ InsertSorted((T)t);
+ }
+
+ private int InsertSorted(T item, IComparer comparer = null)
+ {
+ if (comparer == null)
+ comparer = _comparer;
+
+ if (_store.Count == 0 || comparer == null)
+ {
+ _store.Add(item);
+ return 0;
+ }
+ if (comparer.Compare(_store[_store.Count - 1], item) <= 0)
+ {
+ _store.Add(item);
+ return _store.Count - 1;
+ }
+ if (comparer.Compare(_store[0], item) >= 0)
+ {
+ _store.Insert(0, item);
+ return 0;
+ }
+ int index = _store.BinarySearch(item);
+ if (index < 0)
+ index = ~index;
+ _store.Insert(index, item);
+ return index;
+ }
+
+ public void Sort(IComparer comparer = null)
+ {
+ if (comparer == null)
+ comparer = _comparer;
+
+ if (comparer == null)
+ return;
+
+ _store.Sort(comparer);
+
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ public void Refresh()
+ {
+ _store.Clear();
+ _store.AddRange(_backing);
+ Sort();
+ }
+
+ public void SetComparer(IComparer comparer, bool resort = true)
+ {
+ _comparer = comparer;
+ if (resort)
+ Sort();
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/Torch/CommandLine.cs b/Torch/CommandLine.cs
index 505f235..c851427 100644
--- a/Torch/CommandLine.cs
+++ b/Torch/CommandLine.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Reflection;
using System.Text;
+using NLog;
namespace Torch
{
@@ -12,6 +13,7 @@ namespace Torch
{
private readonly string _argPrefix;
private readonly Dictionary _args = new Dictionary();
+ private readonly Logger _log = LogManager.GetCurrentClassLogger();
protected CommandLine(string argPrefix = "-")
{
@@ -89,6 +91,24 @@ namespace Torch
if (property.Value.PropertyType == typeof(string))
property.Value.SetValue(this, args[++i]);
+
+ if (property.Value.PropertyType == typeof(List))
+ {
+ i++;
+ var l = new List(16);
+ while (i < args.Length && !args[i].StartsWith(_argPrefix))
+ {
+ if (Guid.TryParse(args[i], out Guid g))
+ {
+ l.Add(g);
+ _log.Info($"added plugin {g}");
+ }
+ else
+ _log.Warn($"Failed to parse GUID {args[i]}");
+ i++;
+ }
+ property.Value.SetValue(this, l);
+ }
}
}
catch
diff --git a/Torch/Commands/CommandContext.cs b/Torch/Commands/CommandContext.cs
index 97a9203..e2ed0db 100644
--- a/Torch/Commands/CommandContext.cs
+++ b/Torch/Commands/CommandContext.cs
@@ -55,10 +55,17 @@ namespace Torch.Commands
Args = args ?? new List();
}
- public virtual void Respond(string message, string sender = "Server", string font = MyFontEnum.Blue)
+ public virtual void Respond(string message, string sender = null, string font = null)
{
- Torch.CurrentSession.Managers.GetManager()
- ?.SendMessageAsOther(sender, message, font, _steamIdSender);
+ //hack: Backwards compatibility 20190416
+ if (sender == "Server")
+ {
+ sender = null;
+ font = null;
+ }
+
+ var chat = Torch.CurrentSession.Managers.GetManager();
+ chat?.SendMessageAsOther(sender, message, font, _steamIdSender);
}
}
}
\ No newline at end of file
diff --git a/Torch/Commands/CommandManager.cs b/Torch/Commands/CommandManager.cs
index a7912aa..8482183 100644
--- a/Torch/Commands/CommandManager.cs
+++ b/Torch/Commands/CommandManager.cs
@@ -81,22 +81,22 @@ namespace Torch.Commands
}
}
- public bool HandleCommandFromServer(string message)
+ public List HandleCommandFromServer(string message)
{
var cmdText = new string(message.Skip(1).ToArray());
var command = Commands.GetCommand(cmdText, out string argText);
if (command == null)
- return false;
+ return null;
var cmdPath = string.Join(".", command.Path);
var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast().Select(x => x.ToString().Replace("\"", "")).ToList();
_log.Trace($"Invoking {cmdPath} for server.");
- var context = new CommandContext(Torch, command.Plugin, Sync.MyId, argText, splitArgs);
+ var context = new ConsoleCommandContext(Torch, command.Plugin, Sync.MyId, argText, splitArgs);
if (command.TryInvoke(context))
_log.Info($"Server ran command '{message}'");
else
context.Respond($"Invalid Syntax: {command.SyntaxHelp}");
- return true;
+ return context.Responses;
}
public void HandleCommand(TorchChatMessage msg, ref bool consumed)
@@ -130,7 +130,7 @@ namespace Torch.Commands
if (!HasPermission(steamId, command))
{
_log.Info($"{player.DisplayName} tried to use command {cmdPath} without permission");
- _chatManager.SendMessageAsOther("Server", $"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", MyFontEnum.Red, steamId);
+ _chatManager.SendMessageAsOther(Torch.Config.ChatName, $"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", Torch.Config.ChatColor, steamId);
return;
}
diff --git a/Torch/Commands/ConsoleCommandContext.cs b/Torch/Commands/ConsoleCommandContext.cs
new file mode 100644
index 0000000..49fcfe1
--- /dev/null
+++ b/Torch/Commands/ConsoleCommandContext.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Text;
+using Torch.API;
+using Torch.API.Managers;
+using Torch.API.Plugins;
+
+namespace Torch.Commands
+{
+ public class ConsoleCommandContext : CommandContext
+ {
+ public List Responses = new List();
+ private bool _flag;
+
+ ///
+ public ConsoleCommandContext(ITorchBase torch, ITorchPlugin plugin, ulong steamIdSender, string rawArgs = null, List args = null)
+ : base(torch, plugin, steamIdSender, rawArgs, args) { }
+
+ ///
+ public override void Respond(string message, string sender = null, string font = null)
+ {
+ if (sender == "Server")
+ {
+ sender = null;
+ font = null;
+ }
+
+ Responses.Add(new TorchChatMessage(sender ?? TorchBase.Instance.Config.ChatName, message, font ?? TorchBase.Instance.Config.ChatColor));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Torch/Commands/TorchCommands.cs b/Torch/Commands/TorchCommands.cs
index 26d676e..2e4ee66 100644
--- a/Torch/Commands/TorchCommands.cs
+++ b/Torch/Commands/TorchCommands.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
+using NLog;
using Sandbox.Game.Multiplayer;
using Sandbox.ModAPI;
using Steamworks;
@@ -28,6 +29,9 @@ namespace Torch.Commands
{
private static bool _restartPending = false;
private static bool _cancelRestart = false;
+ private bool _stopPending = false;
+ private bool _cancelStop = false;
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
[Command("whatsmyip")]
[Permission(MyPromoteLevel.None)]
@@ -35,9 +39,9 @@ namespace Torch.Commands
{
if (steamId == 0)
steamId = Context.Player.SteamUserId;
-
+
VRage.GameServices.MyP2PSessionState statehack = new VRage.GameServices.MyP2PSessionState();
- VRage.Steam.MySteamService.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
+ MySteamServiceWrapper.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray());
Context.Respond($"Your IP is {ip}");
}
@@ -57,10 +61,16 @@ namespace Torch.Commands
if (node != null)
{
var command = node.Command;
- var children = node.Subcommands.Select(x => x.Key);
+ var children = node.Subcommands.Where(e => Context.Player == null || e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
var sb = new StringBuilder();
+ if (Context.Player != null && command?.MinimumPromoteLevel > Context.Player.PromoteLevel)
+ {
+ Context.Respond("You are not authorized to use this command.");
+ return;
+ }
+
if (command != null)
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
@@ -94,11 +104,11 @@ namespace Torch.Commands
if (node != null)
{
var command = node.Command;
- var children = node.Subcommands.Select(x => x.Key);
+ var children = node.Subcommands.Where(e => e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
var sb = new StringBuilder();
- if (command != null)
+ if (command != null && (Context.Player == null || command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
{
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText);
@@ -114,7 +124,7 @@ namespace Torch.Commands
var sb = new StringBuilder();
foreach (var command in commandManager.Commands.WalkTree())
{
- if (command.IsCommand)
+ if (command.IsCommand && (Context.Player == null || command.Command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
}
@@ -146,9 +156,25 @@ namespace Torch.Commands
}
[Command("stop", "Stops the server.")]
- public void Stop(bool save = true)
+ public void Stop(bool save = true, int countdownSeconds = 0)
{
- Context.Respond("Stopping server.");
+ if (_stopPending)
+ {
+ Context.Respond("A stop is already pending.");
+ return;
+ }
+
+ _stopPending = true;
+ Task.Run(() =>
+ {
+ var countdown = StopCountdown(countdownSeconds, save).GetEnumerator();
+ while (countdown.MoveNext())
+ {
+ Thread.Sleep(1000);
+ }
+ });
+
+ /*Context.Respond("Stopping server.");
if (save)
DoSave()?.ContinueWith((a, mod) =>
{
@@ -157,7 +183,7 @@ namespace Torch.Commands
torch.Stop();
}, this, TaskContinuationOptions.RunContinuationsAsynchronously);
else
- Context.Torch.Stop();
+ Context.Torch.Stop();*/
}
[Command("restart", "Restarts the server.")]
@@ -170,6 +196,7 @@ namespace Torch.Commands
}
_restartPending = true;
+
Task.Run(() =>
{
var countdown = RestartCountdown(countdownSeconds, save).GetEnumerator();
@@ -195,6 +222,68 @@ namespace Torch.Commands
else
Context.Respond("A restart is not pending.");
}
+
+ [Command("stop cancel", "Cancel a pending stop.")]
+ public void CancelStop()
+ {
+ if (_restartPending)
+ _cancelStop = true;
+ else
+ Context.Respond("Server Stop is not pending.");
+ }
+
+ private IEnumerable StopCountdown(int countdown, bool save)
+ {
+ for (var i = countdown; i >= 0; i--)
+ {
+ if (_cancelStop)
+ {
+ Context.Torch.CurrentSession.Managers.GetManager()
+ .SendMessageAsSelf($"Stop cancelled.");
+
+ _stopPending = false;
+ _cancelStop = false;
+ yield break;
+ }
+
+ if (i >= 60 && i % 60 == 0)
+ {
+ Context.Torch.CurrentSession.Managers.GetManager()
+ .SendMessageAsSelf($"Stopping server in {i / 60} minute{Pluralize(i / 60)}.");
+ yield return null;
+ }
+ else if (i > 0)
+ {
+ if (i < 11)
+ Context.Torch.CurrentSession.Managers.GetManager()
+ .SendMessageAsSelf($"Stopping server in {i} second{Pluralize(i)}.");
+ yield return null;
+ }
+ else
+ {
+ if (save)
+ {
+ Log.Info("Saving game before stop.");
+ Context.Torch.CurrentSession.Managers.GetManager()
+ .SendMessageAsSelf($"Saving game before stop.");
+ DoSave()?.ContinueWith((a, mod) =>
+ {
+ ITorchBase torch = (mod as CommandModule)?.Context?.Torch;
+ Debug.Assert(torch != null);
+ torch.Stop();
+ }, this, TaskContinuationOptions.RunContinuationsAsynchronously);
+ }
+ else
+ {
+ Log.Info("Stopping server.");
+ Context.Torch.Invoke(() => Context.Torch.Stop());
+ }
+
+
+ yield break;
+ }
+ }
+ }
private IEnumerable RestartCountdown(int countdown, bool save)
{
@@ -226,15 +315,18 @@ namespace Torch.Commands
else
{
if (save)
- Context.Torch.Save().ContinueWith(x => Restart());
- else
- Restart();
+ {
+ Log.Info("Savin game before restart.");
+ Context.Torch.CurrentSession.Managers.GetManager()
+ .SendMessageAsSelf($"Saving game before restart.");
+ }
+
+ Log.Info("Restarting server.");
+ Context.Torch.Invoke(() => Context.Torch.Restart(save));
yield break;
}
}
-
- void Restart() => Context.Torch.Invoke(() => Context.Torch.Restart());
}
private string Pluralize(int num)
@@ -255,7 +347,7 @@ namespace Torch.Commands
private Task DoSave()
{
- Task task = Context.Torch.Save(60 * 1000, true);
+ Task task = Context.Torch.Save(300 * 1000, true);
if (task == null)
{
Context.Respond("Save failed, a save is already in progress");
@@ -290,5 +382,11 @@ namespace Torch.Commands
}
}, this, TaskContinuationOptions.RunContinuationsAsynchronously);
}
+
+ [Command("uptime", "Check how long the server has been online.")]
+ public void Uptime()
+ {
+ Context.Respond(((ITorchServer)Context.Torch).ElapsedPlayTime.ToString());
+ }
}
}
diff --git a/Torch/Extensions/LinqExtensions.cs b/Torch/Extensions/LinqExtensions.cs
new file mode 100644
index 0000000..4fc4c53
--- /dev/null
+++ b/Torch/Extensions/LinqExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+
+namespace Torch
+{
+ public static class LinqExtensions
+ {
+ public static IEnumerable TSort( this IEnumerable source, Func> dependencies, bool throwOnCycle = false )
+ {
+ var sorted = new List();
+ var visited = new HashSet();
+
+ foreach( var item in source )
+ Visit( item, visited, sorted, dependencies, throwOnCycle );
+
+ return sorted;
+ }
+
+ private static void Visit( T item, HashSet visited, List sorted, Func> dependencies, bool throwOnCycle )
+ {
+ if( !visited.Contains( item ) )
+ {
+ visited.Add( item );
+
+ var resolvedDependencies = dependencies(item);
+ if (resolvedDependencies != null)
+ {
+ foreach (var dep in resolvedDependencies)
+ Visit(dep, visited, sorted, dependencies, throwOnCycle);
+ }
+
+ sorted.Add( item );
+ }
+ else
+ {
+ if( throwOnCycle && !sorted.Contains( item ) )
+ throw new Exception( "Cyclic dependency found" );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Torch/Managers/ChatManager/ChatManagerClient.cs b/Torch/Managers/ChatManager/ChatManagerClient.cs
index 0ded357..94c6646 100644
--- a/Torch/Managers/ChatManager/ChatManagerClient.cs
+++ b/Torch/Managers/ChatManager/ChatManagerClient.cs
@@ -15,6 +15,7 @@ using Torch.API;
using Torch.API.Managers;
using Torch.Utils;
using VRage.Game;
+using VRageMath;
namespace Torch.Managers.ChatManager
{
@@ -38,17 +39,26 @@ namespace Torch.Managers.ChatManager
{
if (Sandbox.Engine.Platform.Game.IsDedicated)
{
+ // Sending invalid color to clients will crash them. KEEEN
+ var color = Torch.Config.ChatColor;
+ if (!StringUtils.IsFontEnum(Torch.Config.ChatColor))
+ {
+ _log.Warn("Invalid chat font color! Defaulting to 'Red'");
+ color = MyFontEnum.Red;
+ }
+
var scripted = new ScriptedChatMsg()
{
- Author = "Server",
- Font = MyFontEnum.Red,
+ Author = Torch.Config.ChatName,
+ Font = color,
Text = message,
Target = 0
};
MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
}
else
- MyMultiplayer.Static.SendChatMessage(message);
+ throw new NotImplementedException("Chat system changes broke this");
+ //MyMultiplayer.Static.SendChatMessage(message);
}
else if (HasHud)
MyHud.Chat.ShowMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", message);
@@ -59,12 +69,6 @@ namespace Torch.Managers.ChatManager
{
if (HasHud)
MyHud.Chat?.ShowMessage(author, message, font);
- MySession.Static.GlobalChatHistory.GlobalChatHistory.Chat.Enqueue(new MyGlobalChatItem()
- {
- Author = author,
- AuthorFont = font,
- Text = message
- });
}
///
@@ -76,10 +80,10 @@ namespace Torch.Managers.ChatManager
{
_chatMessageRecievedReplacer = _chatMessageReceivedFactory.Invoke();
_scriptedChatMessageRecievedReplacer = _scriptedChatMessageReceivedFactory.Invoke();
- _chatMessageRecievedReplacer.Replace(new Action(Multiplayer_ChatMessageReceived),
+ _chatMessageRecievedReplacer.Replace(new Action(Multiplayer_ChatMessageReceived),
MyMultiplayer.Static);
_scriptedChatMessageRecievedReplacer.Replace(
- new Action(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
+ new Action(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
}
else
{
@@ -113,7 +117,7 @@ namespace Torch.Managers.ChatManager
{
if (!sendToOthers)
return;
- var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText);
+ var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText, ChatChannel.Global, 0);
bool consumed = RaiseMessageRecieved(torchMsg);
if (!consumed)
consumed = OfflineMessageProcessor(torchMsg);
@@ -130,19 +134,19 @@ namespace Torch.Managers.ChatManager
}
- private void Multiplayer_ChatMessageReceived(ulong steamUserId, string message)
+ private void Multiplayer_ChatMessageReceived(ulong steamUserId, string messageText, ChatChannel channel, long targetId, string customAuthorName)
{
- var torchMsg = new TorchChatMessage(steamUserId, message,
+ var torchMsg = new TorchChatMessage(steamUserId, messageText, channel, targetId,
(steamUserId == MyGameService.UserId) ? MyFontEnum.DarkBlue : MyFontEnum.Blue);
if (!RaiseMessageRecieved(torchMsg) && HasHud)
- _hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, message);
+ _hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, messageText, channel, targetId, customAuthorName);
}
- private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font)
+ private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font, Color color)
{
var torchMsg = new TorchChatMessage(author, message, font);
if (!RaiseMessageRecieved(torchMsg) && HasHud)
- _hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font);
+ _hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font, color);
}
protected bool RaiseMessageRecieved(TorchChatMessage msg)
@@ -158,9 +162,9 @@ namespace Torch.Managers.ChatManager
protected static bool HasHud => !Sandbox.Engine.Platform.Game.IsDedicated;
[ReflectedMethod(Name = _hudChatMessageReceivedName)]
- private static Action _hudChatMessageReceived;
+ private static Action _hudChatMessageReceived;
[ReflectedMethod(Name = _hudChatScriptedMessageReceivedName)]
- private static Action _hudChatScriptedMessageReceived;
+ private static Action _hudChatScriptedMessageReceived;
[ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ChatMessageReceived), typeof(MyHudChat), _hudChatMessageReceivedName)]
private static Func _chatMessageReceivedFactory;
diff --git a/Torch/Managers/ChatManager/ChatManagerServer.cs b/Torch/Managers/ChatManager/ChatManagerServer.cs
index ba92ad3..253d928 100644
--- a/Torch/Managers/ChatManager/ChatManagerServer.cs
+++ b/Torch/Managers/ChatManager/ChatManagerServer.cs
@@ -13,38 +13,72 @@ using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
+using Torch.Managers.PatchManager;
using Torch.Utils;
using VRage;
+using VRage.Collections;
using VRage.Library.Collections;
using VRage.Network;
namespace Torch.Managers.ChatManager
{
+ [PatchShim]
+ internal static class ChatInterceptPatch
+ {
+ private static ChatManagerServer _chatManager;
+ private static ChatManagerServer ChatManager => _chatManager ?? (_chatManager = TorchBase.Instance.CurrentSession.Managers.GetManager());
+
+ internal static void Patch(PatchContext context)
+ {
+ var target = typeof(MyMultiplayerBase).GetMethod("OnChatMessageReceived_Server", BindingFlags.Static | BindingFlags.NonPublic);
+ var patchMethod = typeof(ChatInterceptPatch).GetMethod(nameof(PrefixMessageProcessing), BindingFlags.Static | BindingFlags.NonPublic);
+ context.GetPattern(target).Prefixes.Add(patchMethod);
+ }
+
+ private static bool PrefixMessageProcessing(ref ChatMsg msg)
+ {
+ var consumed = false;
+ ChatManager.RaiseMessageRecieved(msg, ref consumed);
+ return !consumed;
+ }
+ }
+
public class ChatManagerServer : ChatManagerClient, IChatManagerServer
{
- [Dependency(Optional = true)]
- private INetworkManager _networkManager;
-
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static readonly Logger _chatLog = LogManager.GetLogger("Chat");
- private readonly ChatIntercept _chatIntercept;
+ private readonly HashSet _muted = new HashSet();
+ ///
+ public HashSetReader MutedUsers => _muted;
///
public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
{
- _chatIntercept = new ChatIntercept(this);
+
}
///
public event MessageProcessingDel MessageProcessing;
+ ///
+ public bool MuteUser(ulong steamId)
+ {
+ return _muted.Add(steamId);
+ }
+
+ ///
+ public bool UnmuteUser(ulong steamId)
+ {
+ return _muted.Remove(steamId);
+ }
+
///
public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0)
{
if (targetSteamId == Sync.MyId)
{
- RaiseMessageRecieved(new TorchChatMessage(authorId, message));
+ RaiseMessageRecieved(new TorchChatMessage(authorId, message, ChatChannel.Global, 0));
return;
}
if (MyMultiplayer.Static == null)
@@ -89,44 +123,15 @@ namespace Torch.Managers.ChatManager
}
var scripted = new ScriptedChatMsg()
{
- Author = author,
+ Author = author ?? Torch.Config.ChatName,
Text = message,
- Font = font,
+ Font = font ?? Torch.Config.ChatColor,
Target = Sync.Players.TryGetIdentityId(targetSteamId)
};
_chatLog.Info($"{author} (to {GetMemberName(targetSteamId)}): {message}");
MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
}
-
- ///
- public override void Attach()
- {
- base.Attach();
- if (_networkManager != null)
- try
- {
- _networkManager.RegisterNetworkHandler(_chatIntercept);
- _log.Debug("Initialized network intercept for chat messages");
- return;
- }
- catch
- {
- // Discard exception and use second method
- }
-
- if (MyMultiplayer.Static != null)
- {
- MyMultiplayer.Static.ChatMessageReceived += MpStaticChatMessageReceived;
- _log.Warn(
- "Failed to initialize network intercept, we can't discard chat messages! Falling back to another method.");
- }
- else
- {
- _log.Debug("Using offline message processor");
- }
- }
-
///
protected override bool OfflineMessageProcessor(TorchChatMessage msg)
{
@@ -137,84 +142,25 @@ namespace Torch.Managers.ChatManager
return consumed;
}
- private void MpStaticChatMessageReceived(ulong a, string b)
- {
- var tmp = false;
- RaiseMessageRecieved(new ChatMsg()
- {
- Author = a,
- Text = b
- }, ref tmp);
- }
-
- ///
- public override void Detach()
- {
- if (MyMultiplayer.Static != null)
- MyMultiplayer.Static.ChatMessageReceived -= MpStaticChatMessageReceived;
- _networkManager?.UnregisterNetworkHandler(_chatIntercept);
- base.Detach();
- }
-
internal void RaiseMessageRecieved(ChatMsg message, ref bool consumed)
{
- var torchMsg = new TorchChatMessage(GetMemberName(message.Author), message.Author, message.Text);
+ var torchMsg = new TorchChatMessage(GetMemberName(message.Author), message.Author, message.Text, (ChatChannel)message.Channel, message.TargetId);
+ if (_muted.Contains(message.Author))
+ {
+ consumed = true;
+ _chatLog.Warn($"MUTED USER: [{torchMsg.Channel}:{torchMsg.Target}] {torchMsg.Author}: {torchMsg.Message}");
+ return;
+ }
+
MessageProcessing?.Invoke(torchMsg, ref consumed);
if (!consumed)
- _chatLog.Info($"{torchMsg.Author}: {torchMsg.Message}");
+ _chatLog.Info($"[{torchMsg.Channel}:{torchMsg.Target}] {torchMsg.Author}: {torchMsg.Message}");
}
public static string GetMemberName(ulong steamId)
{
return MyMultiplayer.Static?.GetMemberName(steamId) ?? $"user_{steamId}";
}
-
- internal class ChatIntercept : NetworkHandlerBase, INetworkHandler
- {
- private readonly ChatManagerServer _chatManager;
- private bool? _unitTestResult;
-
- public ChatIntercept(ChatManagerServer chatManager)
- {
- _chatManager = chatManager;
- }
-
- ///
- public override bool CanHandle(CallSite site)
- {
- if (site.MethodInfo.Name != "OnChatMessageRecieved")
- return false;
-
- if (_unitTestResult.HasValue)
- return _unitTestResult.Value;
-
- ParameterInfo[] parameters = site.MethodInfo.GetParameters();
- if (parameters.Length != 1)
- {
- _unitTestResult = false;
- return false;
- }
-
- if (parameters[0].ParameterType != typeof(ChatMsg))
- _unitTestResult = false;
-
- _unitTestResult = true;
-
- return _unitTestResult.Value;
- }
-
- ///
- public override bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet)
- {
- var msg = new ChatMsg();
- Serialize(site.MethodInfo, stream, ref msg);
-
- var consumed = false;
- _chatManager.RaiseMessageRecieved(msg, ref consumed);
-
- return consumed;
- }
- }
}
}
diff --git a/Torch/Managers/FilesystemManager.cs b/Torch/Managers/FilesystemManager.cs
index b0fdcb1..4ea665f 100644
--- a/Torch/Managers/FilesystemManager.cs
+++ b/Torch/Managers/FilesystemManager.cs
@@ -4,12 +4,14 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using NLog;
using Torch.API;
namespace Torch.Managers
{
public class FilesystemManager : Manager
{
+ private static readonly Logger _log = LogManager.GetCurrentClassLogger();
///
/// Temporary directory for Torch that is cleared every time the program is started.
///
@@ -22,30 +24,46 @@ namespace Torch.Managers
public FilesystemManager(ITorchBase torchInstance) : base(torchInstance)
{
- var temp = Path.Combine(Path.GetTempPath(), "Torch");
- TempDirectory = Directory.CreateDirectory(temp).FullName;
var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName;
+ TempDirectory = Directory.CreateDirectory(Path.Combine(torch, "tmp")).FullName;
TorchDirectory = torch;
+ _log.Debug($"Clearing tmp directory at {TempDirectory}");
ClearTemp();
}
private void ClearTemp()
{
foreach (var file in Directory.GetFiles(TempDirectory, "*", SearchOption.AllDirectories))
- File.Delete(file);
+ {
+ try
+ {
+ File.Delete(file);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ _log.Debug($"Failed to delete file {file}, it's probably in use by another process'");
+ }
+ catch (Exception ex)
+ {
+ _log.Warn($"Unhandled exception when clearing temp files. You may ignore this. {ex}");
+ }
+ }
}
///
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
///
- public void SoftDelete(string file)
+ public void SoftDelete(string path, string file)
{
- if (!File.Exists(file))
+ string source = Path.Combine(path, file);
+ if (!File.Exists(source))
return;
var rand = Path.GetRandomFileName();
var dest = Path.Combine(TempDirectory, rand);
- File.Move(file, dest);
+ File.Move(source, rand);
+ string rsource = Path.Combine(path, rand);
+ File.Move(rsource, dest);
}
}
}
diff --git a/Torch/Managers/KeenLogPatch.cs b/Torch/Managers/KeenLogPatch.cs
index dfa9c2a..08e70c8 100644
--- a/Torch/Managers/KeenLogPatch.cs
+++ b/Torch/Managers/KeenLogPatch.cs
@@ -76,7 +76,21 @@ namespace Torch.Managers
private static StringBuilder PrepareLog(MyLog log)
{
- return _tmpStringBuilder.Value.Clear().Append(' ', _getIndentByThread(log, _getThreadId(log)) * 3);
+ try
+ {
+ var v = _tmpStringBuilder.Value;
+ v.Clear();
+ var i = _getThreadId(log);
+ var t = _getIndentByThread(log, i);
+ v.Append(' ', t * 3);
+ return v;
+ }
+ catch (Exception e)
+ {
+ _log.Error(e);
+ return _tmpStringBuilder.Value.Clear();
+ }
+ //return _tmpStringBuilder.Value.Clear().Append(' ', _getIndentByThread(log, _getThreadId(log)) * 3);
}
private static bool PrefixWriteLine(MyLog __instance, string msg)
@@ -117,7 +131,15 @@ namespace Torch.Managers
private static bool PrefixLogFormatted(MyLog __instance, MyLogSeverity severity, string format, object[] args)
{
- _log.Log(LogLevelFor(severity), PrepareLog(__instance).AppendFormat(format, args));
+ // Sometimes this is called with a pre-formatted string and no args
+ // and causes a crash when the format string contains braces
+ var sb = PrepareLog(__instance);
+ if (args != null && args.Length > 0)
+ sb.AppendFormat(format, args);
+ else
+ sb.Append(format);
+
+ _log.Log(LogLevelFor(severity), sb);
return false;
}
diff --git a/Torch/Managers/MultiplayerManagerBase.cs b/Torch/Managers/MultiplayerManagerBase.cs
index d892a8e..30bf775 100644
--- a/Torch/Managers/MultiplayerManagerBase.cs
+++ b/Torch/Managers/MultiplayerManagerBase.cs
@@ -116,7 +116,7 @@ namespace Torch.Managers
protected void RaiseClientJoined(ulong steamId)
{
var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected };
- _log.Info($"Player {vm.Name} joined ({vm.SteamId}");
+ _log.Info($"Player {vm.Name} joined ({vm.SteamId})");
Players.Add(steamId, vm);
PlayerJoined?.Invoke(vm);
}
diff --git a/Torch/Managers/NetworkManager/NetworkHandlerBase.cs b/Torch/Managers/NetworkManager/NetworkHandlerBase.cs
deleted file mode 100644
index dc3ce19..0000000
--- a/Torch/Managers/NetworkManager/NetworkHandlerBase.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-using VRage;
-using VRage.Library.Collections;
-using VRage.Network;
-using VRage.Serialization;
-
-namespace Torch.Managers
-{
- public abstract class NetworkHandlerBase
- {
- ///
- /// Check the method name and do unit tests on parameters in here.
- ///
- ///
- ///
- public abstract bool CanHandle(CallSite site);
-
- ///
- /// Performs action on network packet. Return value of true means the packet has been handled, and will not be passed on to the game server.
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public abstract bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
-
- ///
- /// Extracts method arguments from the bitstream or packs them back in, depending on stream read mode.
- ///
- ///
- ///
- ///
- ///
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1)
- {
- var s1 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
- var s3 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
- var info3 = MySerializeInfo.CreateForParameter(args, 2);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- MySerializationHelpers.CreateAndRead(stream, out arg3, s3, info3);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- MySerializationHelpers.Write(stream, ref arg3, s3, info3);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
- var s3 = MyFactory.GetSerializer();
- var s4 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
- var info3 = MySerializeInfo.CreateForParameter(args, 2);
- var info4 = MySerializeInfo.CreateForParameter(args, 3);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- MySerializationHelpers.CreateAndRead(stream, out arg3, s3, info3);
- MySerializationHelpers.CreateAndRead(stream, out arg4, s4, info4);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- MySerializationHelpers.Write(stream, ref arg3, s3, info3);
- MySerializationHelpers.Write(stream, ref arg4, s4, info4);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4, ref T5 arg5)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
- var s3 = MyFactory.GetSerializer();
- var s4 = MyFactory.GetSerializer();
- var s5 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
- var info3 = MySerializeInfo.CreateForParameter(args, 2);
- var info4 = MySerializeInfo.CreateForParameter(args, 3);
- var info5 = MySerializeInfo.CreateForParameter(args, 4);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- MySerializationHelpers.CreateAndRead(stream, out arg3, s3, info3);
- MySerializationHelpers.CreateAndRead(stream, out arg4, s4, info4);
- MySerializationHelpers.CreateAndRead(stream, out arg5, s5, info5);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- MySerializationHelpers.Write(stream, ref arg3, s3, info3);
- MySerializationHelpers.Write(stream, ref arg4, s4, info4);
- MySerializationHelpers.Write(stream, ref arg5, s5, info5);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4, ref T5 arg5, ref T6 arg6)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
- var s3 = MyFactory.GetSerializer();
- var s4 = MyFactory.GetSerializer();
- var s5 = MyFactory.GetSerializer();
- var s6 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
- var info3 = MySerializeInfo.CreateForParameter(args, 2);
- var info4 = MySerializeInfo.CreateForParameter(args, 3);
- var info5 = MySerializeInfo.CreateForParameter(args, 4);
- var info6 = MySerializeInfo.CreateForParameter(args, 5);
-
- if (stream.Reading)
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- MySerializationHelpers.CreateAndRead(stream, out arg3, s3, info3);
- MySerializationHelpers.CreateAndRead(stream, out arg4, s4, info4);
- MySerializationHelpers.CreateAndRead(stream, out arg5, s5, info5);
- MySerializationHelpers.CreateAndRead(stream, out arg6, s6, info6);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- MySerializationHelpers.Write(stream, ref arg3, s3, info3);
- MySerializationHelpers.Write(stream, ref arg4, s4, info4);
- MySerializationHelpers.Write(stream, ref arg5, s5, info5);
- MySerializationHelpers.Write(stream, ref arg6, s6, info6);
- }
- }
-
- public void Serialize(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4, ref T5 arg5, ref T6 arg6, ref T7 arg7)
- {
- var s1 = MyFactory.GetSerializer();
- var s2 = MyFactory.GetSerializer();
- var s3 = MyFactory.GetSerializer();
- var s4 = MyFactory.GetSerializer();
- var s5 = MyFactory.GetSerializer();
- var s6 = MyFactory.GetSerializer();
- var s7 = MyFactory.GetSerializer();
-
- var args = info.GetParameters();
- var info1 = MySerializeInfo.CreateForParameter(args, 0);
- var info2 = MySerializeInfo.CreateForParameter(args, 1);
- var info3 = MySerializeInfo.CreateForParameter(args, 2);
- var info4 = MySerializeInfo.CreateForParameter(args, 3);
- var info5 = MySerializeInfo.CreateForParameter(args, 4);
- var info6 = MySerializeInfo.CreateForParameter(args, 5);
- var info7 = MySerializeInfo.CreateForParameter(args, 6);
-
- if ( stream.Reading )
- {
- MySerializationHelpers.CreateAndRead(stream, out arg1, s1, info1);
- MySerializationHelpers.CreateAndRead(stream, out arg2, s2, info2);
- MySerializationHelpers.CreateAndRead(stream, out arg3, s3, info3);
- MySerializationHelpers.CreateAndRead(stream, out arg4, s4, info4);
- MySerializationHelpers.CreateAndRead(stream, out arg5, s5, info5);
- MySerializationHelpers.CreateAndRead(stream, out arg6, s6, info6);
- MySerializationHelpers.CreateAndRead(stream, out arg7, s7, info7);
- }
- else
- {
- MySerializationHelpers.Write(stream, ref arg1, s1, info1);
- MySerializationHelpers.Write(stream, ref arg2, s2, info2);
- MySerializationHelpers.Write(stream, ref arg3, s3, info3);
- MySerializationHelpers.Write(stream, ref arg4, s4, info4);
- MySerializationHelpers.Write(stream, ref arg5, s5, info5);
- MySerializationHelpers.Write(stream, ref arg6, s6, info6);
- MySerializationHelpers.Write(stream, ref arg7, s7, info7);
- }
- }
- }
-}
diff --git a/Torch/Managers/NetworkManager/NetworkManager.cs b/Torch/Managers/NetworkManager/NetworkManager.cs
index 0fd8d69..1bb9a95 100644
--- a/Torch/Managers/NetworkManager/NetworkManager.cs
+++ b/Torch/Managers/NetworkManager/NetworkManager.cs
@@ -1,396 +1,133 @@
using System;
-using System.Collections;
using System.Collections.Generic;
-using System.Linq;
using System.Reflection;
-using System.Threading.Tasks;
using NLog;
using Sandbox.Engine.Multiplayer;
-using Sandbox.Game.Multiplayer;
using Torch.API;
using Torch.API.Managers;
-using Torch.Utils;
-using VRage;
-using VRage.Library.Collections;
using VRage.Network;
using VRageMath;
namespace Torch.Managers
{
- public class NetworkManager : Manager, INetworkManager
+ public static class NetworkManager
{
private static Logger _log = LogManager.GetCurrentClassLogger();
-
- private const string _myTransportLayerField = "TransportLayer";
- private const string _transportHandlersField = "m_handlers";
- private readonly HashSet _networkHandlers = new HashSet();
- private bool _init;
-
- private const int MAX_ARGUMENT = 6;
- private const int GENERIC_PARAMETERS = 8;
- private const int DISPATCH_PARAMETERS = 10;
- private static readonly DBNull DbNull = DBNull.Value;
- private static MethodInfo _dispatchInfo;
-
- private static MethodInfo DispatchEventInfo => _dispatchInfo ?? (_dispatchInfo = typeof(MyReplicationLayerBase).GetMethod("DispatchEvent", BindingFlags.NonPublic | BindingFlags.Instance));
-
- [ReflectedGetter(Name = "m_typeTable")]
- private static Func _typeTableGetter;
- [ReflectedGetter(Name = "m_methodInfoLookup")]
- private static Func> _methodInfoLookupGetter;
- [ReflectedMethod(Type = typeof(MyReplicationLayer), Name = "GetObjectByNetworkId")]
- private static Func _getObjectByNetworkId;
-
- public NetworkManager(ITorchBase torchInstance) : base(torchInstance)
- {
-
- }
-
- private static bool ReflectionUnitTest(bool suppress = false)
- {
- try
- {
- var syncLayerType = typeof(MySyncLayer);
- var transportLayerField = syncLayerType.GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance);
-
- if (transportLayerField == null)
- throw new TypeLoadException("Could not find internal type for TransportLayer");
-
- var transportLayerType = transportLayerField.FieldType;
-
- if (!Reflection.HasField(transportLayerType, _transportHandlersField))
- throw new TypeLoadException("Could not find Handlers field");
-
- return true;
- }
- catch (TypeLoadException ex)
- {
- _log.Error(ex);
- if (suppress)
- return false;
- throw;
- }
- }
- ///
- public override void Attach()
- {
- if (_init)
- return;
-
- _init = true;
-
- if (!ReflectionUnitTest())
- throw new InvalidOperationException("Reflection unit test failed.");
-
- //don't bother with nullchecks here, it was all handled in ReflectionUnitTest
- var transportType = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType;
- var transportInstance = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.Static.SyncLayer);
- var handlers = (IDictionary)transportType.GetField(_transportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
- var handlerTypeField = handlers.GetType().GenericTypeArguments[0].GetField("messageId"); //Should be MyTransportLayer.HandlerId
- object id = null;
- foreach (var key in handlers.Keys)
- {
- if ((MyMessageId)handlerTypeField.GetValue(key) != MyMessageId.RPC)
- continue;
-
- id = key;
- break;
- }
- if (id == null)
- throw new InvalidOperationException("RPC handler not found.");
-
- //remove Keen's network listener
- handlers.Remove(id);
- //replace it with our own
- handlers.Add(id, new Action(OnEvent));
-
- //PrintDebug();
-
- _log.Debug("Initialized network intercept");
- }
-
- ///
- public override void Detach()
- {
- // TODO reverse what was done in Attach
- }
-
- #region Network Intercept
-
- //TODO: Change this to a method patch so I don't have to try to keep up with Keen.
- ///
- /// This is the main body of the network intercept system. When messages come in from clients, they are processed here
- /// before being passed on to the game server.
- ///
- /// DO NOT modify this method unless you're absolutely sure of what you're doing. This can very easily destabilize the game!
- ///
- ///
- private void OnEvent(MyPacket packet)
- {
- if (_networkHandlers.Count == 0)
- {
- //pass the message back to the game server
- try
- {
- ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet);
- }
- catch (Exception ex)
- {
- _log.Error(ex);
- //crash after logging, bad things could happen if we continue on with bad data
- throw;
- }
- return;
- }
-
- var stream = new BitStream();
- stream.ResetRead(packet.BitStream);
-
- var networkId = stream.ReadNetworkId();
- //this value is unused, but removing this line corrupts the rest of the stream
- var blockedNetworkId = stream.ReadNetworkId();
- var eventId = (uint)stream.ReadUInt16();
- bool flag = stream.ReadBool();
- Vector3D? position = new Vector3D?();
- if (flag)
- position = new Vector3D?(stream.ReadVector3D());
-
- CallSite site;
- object obj;
- if (networkId.IsInvalid) // Static event
- {
- site = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).StaticEventTable.Get(eventId);
- obj = null;
- }
- else // Instance event
- {
- //var sendAs = ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).GetObjectByNetworkId(networkId);
- var sendAs = _getObjectByNetworkId((MyReplicationLayer)MyMultiplayer.ReplicationLayer, networkId);
- if (sendAs == null)
- {
- return;
- }
- var typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(sendAs.GetType());
- var eventCount = typeInfo.EventTable.Count;
- if (eventId < eventCount) // Directly
- {
- obj = sendAs;
- site = typeInfo.EventTable.Get(eventId);
- }
- else // Through proxy
- {
- obj = ((IMyProxyTarget)sendAs).Target;
- typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(obj.GetType());
- site = typeInfo.EventTable.Get(eventId - (uint)eventCount); // Subtract max id of Proxy
- }
- }
-
- //we're handling the network live in the game thread, this needs to go as fast as possible
- var discard = false;
- foreach (var handler in _networkHandlers)
- //Parallel.ForEach(_networkHandlers, handler =>
- {
- try
- {
- if (handler.CanHandle(site))
- discard |= handler.Handle(packet.Sender.Id.Value, site, stream, obj, packet);
- }
- catch (Exception ex)
- {
- //ApplicationLog.Error(ex.ToString());
- _log.Error(ex);
- }
- }
-
- //one of the handlers wants us to discard this packet
- if (discard)
- return;
-
- //pass the message back to the game server
- try
- {
- ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet);
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Error processing network event!");
- _log.Error(ex);
- //crash after logging, bad things could happen if we continue on with bad data
- throw;
- }
- }
-
-
- ///
- public void RegisterNetworkHandler(INetworkHandler handler)
- {
- var handlerType = handler.GetType().FullName;
- var toRemove = new List();
- foreach (var item in _networkHandlers)
- {
- if (item.GetType().FullName == handlerType)
- {
- //if (ExtenderOptions.IsDebugging)
- _log.Error("Network handler already registered! " + handlerType);
- toRemove.Add(item);
- }
- }
-
- foreach (var oldHandler in toRemove)
- _networkHandlers.Remove(oldHandler);
-
- _networkHandlers.Add(handler);
- }
-
- ///
- public bool UnregisterNetworkHandler(INetworkHandler handler)
- {
- return _networkHandlers.Remove(handler);
- }
-
- public void RegisterNetworkHandlers(params INetworkHandler[] handlers)
- {
- foreach (var handler in handlers)
- RegisterNetworkHandler(handler);
- }
-
- #endregion
-
#region Network Injection
+ private static Dictionary _delegateCache = new Dictionary();
- ///
- /// Broadcasts an event to all connected clients
- ///
- ///
- ///
- ///
- public void RaiseEvent(MethodInfo method, object obj, params object[] args)
+ private static Func GetDelegate(MethodInfo method) where TA : class
{
- //default(EndpointId) tells the network to broadcast the message
- RaiseEvent(method, obj, default(EndpointId), args);
- }
-
- ///
- /// Sends an event to one client by SteamId
- ///
- ///
- ///
- ///
- ///
- public void RaiseEvent(MethodInfo method, object obj, ulong steamId, params object[] args)
- {
- RaiseEvent(method, obj, new EndpointId(steamId), args);
- }
-
- ///
- /// Sends an event to one client
- ///
- ///
- ///
- ///
- ///
- public void RaiseEvent(MethodInfo method, object obj, EndpointId endpoint, params object[] args)
- {
- if (method == null)
- throw new ArgumentNullException(nameof(method), "MethodInfo cannot be null!");
-
- if (args.Length > MAX_ARGUMENT)
- throw new ArgumentOutOfRangeException(nameof(args), $"Cannot pass more than {MAX_ARGUMENT} arguments!");
-
- var owner = obj as IMyEventOwner;
- if (obj != null && owner == null)
- throw new InvalidCastException("Provided event target is not of type IMyEventOwner!");
-
- if (!method.HasAttribute())
- throw new CustomAttributeFormatException("Provided event target does not have the Event attribute! Replication will not succeed!");
-
- //array to hold arguments to pass into DispatchEvent
- object[] arguments = new object[DISPATCH_PARAMETERS];
-
-
- arguments[0] = obj == null ? TryGetStaticCallSite(method) : TryGetCallSite(method, obj);
- arguments[1] = endpoint;
- arguments[2] = owner;
-
- //copy supplied arguments into the reflection arguments
- for (var i = 0; i < args.Length; i++)
- arguments[i + 3] = args[i];
-
- //pad the array out with DBNull, skip last element
- //last element should stay null (this is for blocking events -- not used?)
- for (var j = args.Length + 3; j < arguments.Length - 1; j++)
- arguments[j] = DbNull;
-
- //create an array of Types so we can create a generic method
- var argTypes = new Type[GENERIC_PARAMETERS];
-
- //any null arguments (not DBNull) must be of type IMyEventOwner
- for (var k = 2; k < arguments.Length; k++)
- argTypes[k - 2] = arguments[k]?.GetType() ?? typeof(IMyEventOwner);
-
- var parameters = method.GetParameters();
- for (var i = 0; i < parameters.Length; i++)
+ if (!_delegateCache.TryGetValue(method, out var del))
{
- if (argTypes[i + 1] != parameters[i].ParameterType)
- throw new TypeLoadException($"Type mismatch on method parameters. Expected {string.Join(", ", parameters.Select(p => p.ParameterType.ToString()))} got {string.Join(", ", argTypes.Select(t => t.ToString()))}");
+ del = (Func)(x => Delegate.CreateDelegate(typeof(TA), x, method) as TA);
+ _delegateCache[method] = del;
}
- //create a generic method of DispatchEvent and invoke to inject our data into the network
- var dispatch = DispatchEventInfo.MakeGenericMethod(argTypes);
- dispatch.Invoke(MyMultiplayer.ReplicationLayer, arguments);
+ return (Func)del;
}
- ///
- /// Broadcasts a static event to all connected clients
- ///
- ///
- ///
- public void RaiseStaticEvent(MethodInfo method, params object[] args)
+ public static void RaiseEvent(T1 instance, MethodInfo method, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
- //default(EndpointId) tells the network to broadcast the message
- RaiseStaticEvent(method, default(EndpointId), args);
+ var del = GetDelegate(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, target);
}
- ///
- /// Sends a static event to one client by SteamId
- ///
- ///
- ///
- ///
- public void RaiseStaticEvent(MethodInfo method, ulong steamId, params object[] args)
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
- RaiseEvent(method, null, new EndpointId(steamId), args);
+ var del = GetDelegate> (method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, target);
}
- ///
- /// Sends a static event to one client
- ///
- ///
- ///
- ///
- public void RaiseStaticEvent(MethodInfo method, EndpointId endpoint, params object[] args)
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
- RaiseEvent(method, null, endpoint, args);
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, target);
}
- private CallSite TryGetStaticCallSite(MethodInfo method)
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
- MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer);
- if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result))
- throw new MissingMemberException("Provided event target not found!");
- return result;
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, target);
}
- private CallSite TryGetCallSite(MethodInfo method, object arg)
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
- MySynchronizedTypeInfo typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(arg.GetType());
- if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result))
- throw new MissingMemberException("Provided event target not found!");
- return result;
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, target);
}
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, arg5, target);
+ }
+
+ public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, arg5, arg6, target);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, target, position);
+ }
+
+ public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, EndpointId target = default(EndpointId), Vector3D? position = null)
+ {
+ var del = GetDelegate>(method);
+
+ MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, arg6, target, position);
+ }
#endregion
+
+
}
}
diff --git a/Torch/Managers/PatchManager/DecoratedMethod.cs b/Torch/Managers/PatchManager/DecoratedMethod.cs
index 010a315..8c2e120 100644
--- a/Torch/Managers/PatchManager/DecoratedMethod.cs
+++ b/Torch/Managers/PatchManager/DecoratedMethod.cs
@@ -83,7 +83,7 @@ namespace Torch.Managers.PatchManager
private DynamicMethod AllocatePatchMethod()
{
Debug.Assert(_method.DeclaringType != null);
- var methodName = _method.Name + $"_{_patchSalt++}";
+ var methodName = "Patched_" + _method.DeclaringType.FullName + _method.Name + $"_{_patchSalt++}";
var returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
var parameters = _method.GetParameters();
var parameterTypes = (_method.IsStatic ? Enumerable.Empty() : new[] {typeof(object)})
diff --git a/Torch/Managers/PatchManager/PatchContext.cs b/Torch/Managers/PatchManager/PatchContext.cs
index a36225a..354c217 100644
--- a/Torch/Managers/PatchManager/PatchContext.cs
+++ b/Torch/Managers/PatchManager/PatchContext.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using NLog;
namespace Torch.Managers.PatchManager
{
@@ -10,6 +11,7 @@ namespace Torch.Managers.PatchManager
public sealed class PatchContext
{
private readonly Dictionary _rewritePatterns = new Dictionary();
+ private static readonly Logger Log = LogManager.GetCurrentClassLogger();
internal PatchContext()
{
diff --git a/Torch/Managers/ScriptingManager.cs b/Torch/Managers/ScriptingManager.cs
deleted file mode 100644
index 11f73df..0000000
--- a/Torch/Managers/ScriptingManager.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Sandbox.ModAPI;
-using Torch.API;
-using Torch.API.Managers;
-using Torch.API.ModAPI;
-using Torch.API.ModAPI.Ingame;
-using VRage.Scripting;
-
-namespace Torch.Managers
-{
- public class ScriptingManager : IManager
- {
- private MyScriptWhitelist _whitelist;
-
- public void Attach()
- {
- _whitelist = MyScriptCompiler.Static.Whitelist;
- MyScriptCompiler.Static.AddConditionalCompilationSymbols("TORCH");
- MyScriptCompiler.Static.AddReferencedAssemblies(typeof(ITorchBase).Assembly.Location);
- MyScriptCompiler.Static.AddImplicitIngameNamespacesFromTypes(typeof(GridExtensions));
-
- using (var whitelist = _whitelist.OpenBatch())
- {
- whitelist.AllowNamespaceOfTypes(MyWhitelistTarget.ModApi, typeof(TorchAPI));
- whitelist.AllowNamespaceOfTypes(MyWhitelistTarget.Both, typeof(GridExtensions));
- }
-
- /*
- //dump whitelist
- var whitelist = new StringBuilder();
- foreach (var pair in MyScriptCompiler.Static.Whitelist.GetWhitelist())
- {
- var split = pair.Key.Split(',');
- whitelist.AppendLine("|-");
- whitelist.AppendLine($"|{pair.Value} || {split[0]} || {split[1]}");
- }
- Log.Info(whitelist);*/
- }
-
- public void Detach()
- {
- // TODO unregister whitelist patches
- }
-
- public void UnwhitelistType(Type t)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs
index bdaaf32..30ec6ce 100644
--- a/Torch/Managers/UpdateManager.cs
+++ b/Torch/Managers/UpdateManager.cs
@@ -10,8 +10,8 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using NLog;
-using Octokit;
using Torch.API;
+using Torch.API.WebAPI;
namespace Torch.Managers
{
@@ -21,12 +21,11 @@ namespace Torch.Managers
public class UpdateManager : Manager
{
private Timer _updatePollTimer;
- private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
private Logger _log = LogManager.GetCurrentClassLogger();
[Dependency]
private FilesystemManager _fsManager;
-
+
public UpdateManager(ITorchBase torchInstance) : base(torchInstance)
{
//_updatePollTimer = new Timer(TimerElapsed, this, TimeSpan.Zero, TimeSpan.FromMinutes(5));
@@ -42,49 +41,34 @@ namespace Torch.Managers
{
CheckAndUpdateTorch();
}
-
- private async Task> TryGetLatestArchiveUrl(string owner, string name)
- {
- try
- {
- var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false);
- if (latest == null)
- return new Tuple(new Version(), null);
-
- var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
- if (zip == null)
- _log.Error($"Latest release of {owner}/{name} does not contain a zip archive.");
- if (!latest.TagName.TryExtractVersion(out Version version))
- _log.Error($"Unable to parse version tag for {owner}/{name}");
- return new Tuple(version, zip?.BrowserDownloadUrl);
- }
- catch (Exception e)
- {
- _log.Error($"An error occurred getting release information for '{owner}/{name}'");
- _log.Error(e);
- return default(Tuple);
- }
- }
-
+
private async void CheckAndUpdateTorch()
{
- // Doesn't work properly or reliably, TODO update when Jenkins is fully configured
- return;
-
- if (!Torch.Config.GetTorchUpdates)
+ if (Torch.Config.NoUpdate || !Torch.Config.GetTorchUpdates)
return;
try
{
- var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false);
- if (releaseInfo.Item1 > Torch.TorchVersion)
+ var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Branch);
+ if (job == null)
{
- _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
+ _log.Info("Failed to fetch latest version.");
+ return;
+ }
+
+ if (job.Version > Torch.TorchVersion)
+ {
+ _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {job.Version}");
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip");
- new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
+ //new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
+ if (!await JenkinsQuery.Instance.DownloadRelease(job, updateName))
+ {
+ _log.Warn("Failed to download new release!");
+ return;
+ }
UpdateFromZip(updateName, _torchDir);
File.Delete(updateName);
- _log.Warn($"Torch version {releaseInfo.Item1} has been installed, please restart Torch to finish the process.");
+ _log.Warn($"Torch version {job.Version} has been installed, please restart Torch to finish the process.");
}
else
{
@@ -104,12 +88,16 @@ namespace Torch.Managers
{
foreach (var file in zip.Entries)
{
+ if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName)))
+ continue;
+
_log.Debug($"Unzipping {file.FullName}");
var targetFile = Path.Combine(extractPath, file.FullName);
- _fsManager.SoftDelete(targetFile);
+ _fsManager.SoftDelete(extractPath, file.FullName);
+ file.ExtractToFile(targetFile, true);
}
- zip.ExtractToDirectory(extractPath);
+ //zip.ExtractToDirectory(extractPath); //throws exceptions sometimes?
}
}
diff --git a/Torch/MySteamServiceWrapper.cs b/Torch/MySteamServiceWrapper.cs
new file mode 100644
index 0000000..c74995a
--- /dev/null
+++ b/Torch/MySteamServiceWrapper.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Reflection;
+using System.Windows.Data;
+using VRage.GameServices;
+
+namespace Torch
+{
+ ///
+ /// Provides static accessor for MySteamService because Keen made it internal
+ ///
+ public static class MySteamServiceWrapper
+ {
+ private static readonly MethodInfo _getGameService;
+
+ public static IMyGameService Static => (IMyGameService)_getGameService.Invoke(null, null);
+
+ static MySteamServiceWrapper()
+ {
+ var type = Type.GetType("VRage.Steam.MySteamService, VRage.Steam");
+ var prop = type.GetProperty("Static", BindingFlags.Static | BindingFlags.Public);
+ _getGameService = prop.GetGetMethod();
+ }
+
+ public static IMyGameService Init(bool dedicated, uint appId)
+ {
+ return (IMyGameService)Activator.CreateInstance(Type.GetType("VRage.Steam.MySteamService, VRage.Steam"), dedicated, appId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Torch/Patches/GameAnalyticsPatch.cs b/Torch/Patches/GameAnalyticsPatch.cs
index 66bcf72..2a38d91 100644
--- a/Torch/Patches/GameAnalyticsPatch.cs
+++ b/Torch/Patches/GameAnalyticsPatch.cs
@@ -12,7 +12,7 @@ namespace Torch.Patches
public static class GameAnalyticsPatch
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
- private static Action _setLogger;
+ private static Action _setLogger;
public static void Patch(PatchContext ctx)
{
@@ -27,7 +27,7 @@ namespace Torch.Patches
return;
}
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
- _setLogger = loggerField?.CreateSetter();
+ _setLogger = loggerField?.CreateSetter();
FixLogging();
ConstructorInfo ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], new ParameterModifier[0]);
@@ -42,7 +42,7 @@ namespace Torch.Patches
private static void FixLogging()
{
- _setLogger(LogManager.GetLogger("GameAnalytics"));
+ _setLogger(null, LogManager.GetLogger("GameAnalytics"));
if (!(LogManager.Configuration is XmlLoggingConfiguration))
LogManager.Configuration = new XmlLoggingConfiguration(Path.Combine(
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? Environment.CurrentDirectory, "NLog.config"));
diff --git a/Torch/Patches/GameStatePatchShim.cs b/Torch/Patches/GameStatePatchShim.cs
index b2af4a7..f939189 100644
--- a/Torch/Patches/GameStatePatchShim.cs
+++ b/Torch/Patches/GameStatePatchShim.cs
@@ -19,7 +19,7 @@ namespace Torch.Patches
internal static void Patch(PatchContext target)
{
- ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] { typeof(string[]) });
+ ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] { typeof(string[]), typeof(IntPtr) });
if (ctor == null)
throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
target.GetPattern(ctor).Prefixes.Add(MethodRef(PrefixConstructor));
diff --git a/Torch/Patches/PhysicsMemoryPatch.cs b/Torch/Patches/PhysicsMemoryPatch.cs
new file mode 100644
index 0000000..bf94ad9
--- /dev/null
+++ b/Torch/Patches/PhysicsMemoryPatch.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Havok;
+using Sandbox;
+using Sandbox.Engine.Physics;
+using Sandbox.Game.World;
+using Torch.Managers.PatchManager;
+using Torch.Mod;
+using Torch.Mod.Messages;
+using VRage.Game;
+
+namespace Torch.Patches
+{
+ [PatchShim]
+ public static class PhysicsMemoryPatch
+ {
+ public static void Patch(PatchContext ctx)
+ {
+ ctx.GetPattern(typeof(MyPhysics).GetMethod("StepWorldsInternal", BindingFlags.NonPublic | BindingFlags.Instance)).Prefixes.Add(typeof(PhysicsMemoryPatch).GetMethod(nameof(PrefixPhysics)));
+ }
+
+ public static bool NotifiedFailure { get; private set; }
+
+ public static bool PrefixPhysics()
+ {
+ if (!HkBaseSystem.IsOutOfMemory)
+ return true;
+
+ if (NotifiedFailure)
+ return false;
+
+ NotifiedFailure = true;
+ ModCommunication.SendMessageToClients(new NotificationMessage("Havok has run out of memory. Server will restart in 30 seconds!", 60000, MyFontEnum.Red));
+ //save the session NOW before anything moves due to weird physics.
+ MySession.Static.Save();
+ //pause the game, for funsies
+ MySandboxGame.IsPaused = true;
+
+ //nasty hack
+ Task.Run(() =>
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(30));
+ TorchBase.Instance.Restart();
+ });
+
+ return false;
+ }
+ }
+}
diff --git a/Torch/Patches/SessionDownloadPatch.cs b/Torch/Patches/SessionDownloadPatch.cs
index d7d8986..d739bcd 100644
--- a/Torch/Patches/SessionDownloadPatch.cs
+++ b/Torch/Patches/SessionDownloadPatch.cs
@@ -7,6 +7,8 @@ using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Game.World;
+using Torch.API.Session;
+using Torch.API.Managers;
using Torch.Managers.PatchManager;
using Torch.Mod;
using VRage.Game;
@@ -16,6 +18,10 @@ namespace Torch.Patches
[PatchShim]
internal static class SessionDownloadPatch
{
+ private static ITorchSessionManager _sessionManager;
+ private static ITorchSessionManager SessionManager => _sessionManager ?? (_sessionManager = TorchBase.Instance.Managers.GetManager());
+
+
internal static void Patch(PatchContext context)
{
context.GetPattern(typeof(MySession).GetMethod(nameof(MySession.GetWorld))).Suffixes.Add(typeof(SessionDownloadPatch).GetMethod(nameof(SuffixGetWorld), BindingFlags.Static | BindingFlags.NonPublic));
@@ -27,7 +33,7 @@ namespace Torch.Patches
//copy this list so mods added here don't propagate up to the real session
__result.Checkpoint.Mods = __result.Checkpoint.Mods.ToList();
- __result.Checkpoint.Mods.Add(new MyObjectBuilder_Checkpoint.ModItem(TorchModCore.MOD_ID));
+ __result.Checkpoint.Mods.AddRange(SessionManager.OverrideMods);
}
}
}
diff --git a/Torch/Patches/TorchAsyncSaving.cs b/Torch/Patches/TorchAsyncSaving.cs
index c20c59d..4d4a23e 100644
--- a/Torch/Patches/TorchAsyncSaving.cs
+++ b/Torch/Patches/TorchAsyncSaving.cs
@@ -62,11 +62,11 @@ namespace Torch.Patches
if (!Game.IsDedicated)
TakeSaveScreenshot();
- tmpSnapshot.SaveParallel(() =>
+ tmpSnapshot.SaveParallel(() => true, () =>
{
if (!Game.IsDedicated && MySession.Static != null)
ShowWorldSaveResult(tmpSnapshot.SavingSuccess);
- saveTaskSource.SetResult(tmpSnapshot.SavingSuccess ? GameSaveResult.Success : GameSaveResult.FailedToSaveToDisk);
+ saveTaskSource.TrySetResult(tmpSnapshot.SavingSuccess ? GameSaveResult.Success : GameSaveResult.FailedToSaveToDisk);
});
});
return saveTaskSource.Task;
diff --git a/Torch/Persistent.cs b/Torch/Persistent.cs
index cddc182..52b7a33 100644
--- a/Torch/Persistent.cs
+++ b/Torch/Persistent.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
+using NLog;
namespace Torch
{
@@ -17,6 +18,7 @@ namespace Torch
/// Data class type
public sealed class Persistent : IDisposable where T : new()
{
+ private static Logger _log = LogManager.GetCurrentClassLogger();
public string Path { get; set; }
private T _data;
public T Data
@@ -78,10 +80,18 @@ namespace Torch
if (File.Exists(path))
{
- var ser = new XmlSerializer(typeof(T));
- using (var f = File.OpenText(path))
+ try
{
- config = new Persistent(path, (T)ser.Deserialize(f));
+ var ser = new XmlSerializer(typeof(T));
+ using (var f = File.OpenText(path))
+ {
+ config = new Persistent(path, (T)ser.Deserialize(f));
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex);
+ config = null;
}
}
if (config == null)
diff --git a/Torch/Plugins/PluginDependency.cs b/Torch/Plugins/PluginDependency.cs
new file mode 100644
index 0000000..4727efe
--- /dev/null
+++ b/Torch/Plugins/PluginDependency.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Torch
+{
+ public class PluginDependency
+ {
+ ///
+ /// A unique identifier for the plugin that identifies the dependency.
+ ///
+ public Guid Plugin { get; set; }
+
+ ///
+ /// The plugin minimum version. This must include a string in the format of #[.#[.#]] for update checking purposes.
+ ///
+ public string MinVersion { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs
index df10ef2..6ac9445 100644
--- a/Torch/Plugins/PluginManager.cs
+++ b/Torch/Plugins/PluginManager.cs
@@ -7,14 +7,15 @@ using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using NLog;
-using Octokit;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using Torch.API.Session;
+using Torch.API.WebAPI;
using Torch.Collections;
using Torch.Commands;
using Torch.Utils;
@@ -24,36 +25,57 @@ namespace Torch.Managers
///
public class PluginManager : Manager, IPluginManager
{
- private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
+ private class PluginItem
+ {
+ public string Filename { get; set; }
+ public string Path { get; set; }
+ public PluginManifest Manifest { get; set; }
+ public bool IsZip { get; set; }
+ public List ResolvedDependencies { get; set; }
+ }
+
private static Logger _log = LogManager.GetCurrentClassLogger();
+
private const string MANIFEST_NAME = "manifest.xml";
+
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
private readonly MtObservableSortedDictionary _plugins = new MtObservableSortedDictionary();
+ private CommandManager _mgr;
+
#pragma warning disable 649
[Dependency]
private ITorchSessionManager _sessionManager;
#pragma warning restore 649
-
+
///
public IReadOnlyDictionary Plugins => _plugins.AsReadOnlyObservable();
public event Action> PluginsLoaded;
-
+
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{
if (!Directory.Exists(PluginDir))
Directory.CreateDirectory(PluginDir);
}
-
+
///
/// Updates loaded plugins in parallel.
///
public void UpdatePlugins()
{
foreach (var plugin in _plugins.Values)
- plugin.Update();
+ {
+ try
+ {
+ plugin.Update();
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Plugin {plugin.Name} threw an exception during update!");
+ }
+ }
}
-
+
///
public override void Attach()
{
@@ -63,18 +85,18 @@ namespace Torch.Managers
private void SessionManagerOnSessionStateChanged(ITorchSession session, TorchSessionState newState)
{
- var mgr = session.Managers.GetManager();
- if (mgr == null)
+ _mgr = session.Managers.GetManager();
+ if (_mgr == null)
return;
switch (newState)
{
case TorchSessionState.Loaded:
foreach (ITorchPlugin plugin in _plugins.Values)
- mgr.RegisterPluginCommands(plugin);
+ _mgr.RegisterPluginCommands(plugin);
return;
case TorchSessionState.Unloading:
foreach (ITorchPlugin plugin in _plugins.Values)
- mgr.UnregisterPluginCommands(plugin);
+ _mgr.UnregisterPluginCommands(plugin);
return;
case TorchSessionState.Loading:
case TorchSessionState.Unloaded:
@@ -82,7 +104,7 @@ namespace Torch.Managers
return;
}
}
-
+
///
/// Unloads all plugins.
///
@@ -97,34 +119,85 @@ namespace Torch.Managers
public void LoadPlugins()
{
- if (Torch.Config.ShouldUpdatePlugins)
- DownloadPluginUpdates();
-
_log.Info("Loading plugins...");
- var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
- foreach (var item in pluginItems)
+
+ if (!string.IsNullOrEmpty(Torch.Config.TestPlugin))
{
- var path = Path.Combine(PluginDir, item);
- var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
- var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
- if (manifest == null)
+ _log.Info($"Loading plugin for debug at {Torch.Config.TestPlugin}");
+
+ foreach (var item in GetLocalPlugins(Torch.Config.TestPlugin, true))
{
- _log.Warn($"Item '{item}' is missing a manifest, skipping.");
- continue;
+ _log.Info(item.Path);
+ LoadPlugin(item);
}
- if (_plugins.ContainsKey(manifest.Guid))
+ foreach (var plugin in _plugins.Values)
{
- _log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {_plugins[manifest.Guid].Name}");
- continue;
+ plugin.Init(Torch);
}
-
- if (isZip)
- LoadPluginFromZip(path);
- else
- LoadPluginFromFolder(path);
+ _log.Info($"Loaded {_plugins.Count} plugins.");
+ PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
+ return;
}
+ var pluginItems = GetLocalPlugins(PluginDir);
+ var pluginsToLoad = new List();
+ foreach (var item in pluginItems)
+ {
+ var pluginItem = item;
+ if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
+ {
+ // We have some missing dependencies.
+ // Future fix would be to download them, but instead for now let's
+ // just warn the user it's missing
+ foreach(var missingPlugin in missingPlugins)
+ _log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
+ continue;
+ }
+
+ pluginsToLoad.Add(pluginItem);
+ }
+
+ // Download any plugin updates.
+ bool updatesGotten = DownloadPluginUpdates(pluginsToLoad);
+
+ if (updatesGotten)
+ {
+ // Resort the plugins just in case updates changed load hints.
+ pluginItems = GetLocalPlugins(PluginDir);
+ pluginsToLoad.Clear();
+ foreach (var item in pluginItems)
+ {
+ var pluginItem = item;
+ if (!TryValidatePluginDependencies(pluginItems, ref pluginItem, out var missingPlugins))
+ {
+ foreach (var missingPlugin in missingPlugins)
+ _log.Warn($"{item.Manifest.Name} is missing dependency {missingPlugin}. Skipping plugin.");
+ continue;
+ }
+
+ pluginsToLoad.Add(pluginItem);
+ }
+ }
+
+ // Sort based on dependencies.
+ try
+ {
+ pluginsToLoad = pluginsToLoad.TSort(item => item.ResolvedDependencies)
+ .ToList();
+ }
+ catch (Exception e)
+ {
+ // This will happen on cylic dependencies.
+ _log.Error(e);
+ }
+
+ // Actually load the plugins now.
+ foreach (var item in pluginsToLoad)
+ {
+ LoadPlugin(item);
+ }
+
foreach (var plugin in _plugins.Values)
{
plugin.Init(Torch);
@@ -133,188 +206,211 @@ namespace Torch.Managers
PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly());
}
- private void DownloadPluginUpdates()
+ //debug flag is set when the user asks us to run with a specific plugin for plugin development debug
+ //please do not change references to this arg unless you are very sure you know what you're doing
+ private List GetLocalPlugins(string pluginDir, bool debug = false)
{
- //TODO
- _log.Warn("Automatic plugin updates are disabled in this build of Torch while the system is reworked.");
- return;
+ var firstLoad = Torch.Config.Plugins.Count == 0;
+
+ var pluginItems = Directory.EnumerateFiles(pluginDir, "*.zip")
+ .Union(Directory.EnumerateDirectories(pluginDir));
+ if (debug)
+ pluginItems = pluginItems.Union(new List {pluginDir});
+ var results = new List();
+ foreach (var item in pluginItems)
+ {
+ var path = Path.Combine(pluginDir, item);
+ var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
+ var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
+
+ if (manifest == null)
+ {
+ if (!debug)
+ {
+ _log.Warn($"Item '{item}' is missing a manifest, skipping.");
+ continue;
+ }
+ manifest = new PluginManifest()
+ {
+ Guid = new Guid(),
+ Version = "0",
+ Name = "TEST"
+ };
+ }
+
+ var duplicatePlugin = results.FirstOrDefault(r => r.Manifest.Guid == manifest.Guid);
+ if (duplicatePlugin != null)
+ {
+ _log.Warn(
+ $"The GUID provided by {manifest.Name} ({item}) is already in use by {duplicatePlugin.Manifest.Name}.");
+ continue;
+ }
+
+ if (!Torch.Config.LocalPlugins && !debug)
+ {
+ if (isZip && !Torch.Config.Plugins.Contains(manifest.Guid))
+ {
+ if (!firstLoad)
+ {
+ _log.Warn($"Plugin {manifest.Name} ({item}) exists in the plugin directory, but is not listed in torch.cfg. Skipping load!");
+ continue;
+ }
+ _log.Info($"First-time load: Plugin {manifest.Name} added to torch.cfg.");
+ Torch.Config.Plugins.Add(manifest.Guid);
+ }
+ }
+
+ results.Add(new PluginItem
+ {
+ Filename = item,
+ IsZip = isZip,
+ Manifest = manifest,
+ Path = path
+ });
+ }
+
+ if (!Torch.Config.LocalPlugins && firstLoad)
+ Torch.Config.Save();
+
+ return results;
+ }
+
+ private bool DownloadPluginUpdates(List plugins)
+ {
_log.Info("Checking for plugin updates...");
var count = 0;
- var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
- Parallel.ForEach(pluginItems, async item =>
+ Task.WhenAll(plugins.Select(async item =>
{
- PluginManifest manifest = null;
try
{
- var path = Path.Combine(PluginDir, item);
- var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
- manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
- if (manifest == null)
+ if (!item.IsZip)
{
- _log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
+ _log.Warn($"Unzipped plugins cannot be auto-updated. Skipping plugin {item}");
+ return;
+ }
+ item.Manifest.Version.TryExtractVersion(out Version currentVersion);
+ var latest = await PluginQuery.Instance.QueryOne(item.Manifest.Guid);
+
+ if (latest?.LatestVersion == null)
+ {
+ _log.Warn($"Plugin {item.Manifest.Name} does not have any releases on torchapi.net. Cannot update.");
return;
}
- manifest.Version.TryExtractVersion(out Version currentVersion);
- var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false);
+ latest.LatestVersion.TryExtractVersion(out Version newVersion);
- if (currentVersion == null || latest.Item1 == null)
+ if (currentVersion == null || newVersion == null)
{
- _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'");
+ _log.Error($"Error parsing version from manifest or website for plugin '{item.Manifest.Name}.'");
return;
}
- if (latest.Item1 <= currentVersion)
+ if (newVersion <= currentVersion)
{
- _log.Debug($"{manifest.Name} {manifest.Version} is up to date.");
+ _log.Debug($"{item.Manifest.Name} {item.Manifest.Version} is up to date.");
return;
}
- _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}.");
- await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false);
- count++;
- }
- catch (NotFoundException)
- {
- _log.Warn($"GitHub repository not found for {manifest.Name}");
+ _log.Info($"Updating plugin '{item.Manifest.Name}' from {currentVersion} to {newVersion}.");
+ await PluginQuery.Instance.DownloadPlugin(latest, item.Path);
+ Interlocked.Increment(ref count);
}
catch (Exception e)
{
- _log.Warn($"An error occurred updating the plugin {manifest.Name}.");
+ _log.Warn($"An error occurred updating the plugin {item.Manifest.Name}.");
_log.Warn(e);
}
- });
+ }));
_log.Info($"Updated {count} plugins.");
+ return count > 0;
}
-
- private async Task> GetLatestArchiveAsync(string repository)
- {
- try
- {
- var split = repository.Split('/');
- var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false);
- if (!latest.TagName.TryExtractVersion(out Version latestVersion))
- {
- _log.Error($"Unable to parse version tag for the latest release of '{repository}.'");
- }
-
- var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase));
- if (zipAsset == null)
- {
- _log.Error($"Unable to find archive for the latest release of '{repository}.'");
- }
-
- return new Tuple(latestVersion, zipAsset?.BrowserDownloadUrl);
- }
- catch (Exception e)
- {
- _log.Error($"Unable to get the latest release of '{repository}.'");
- _log.Error(e);
- return default(Tuple);
- }
- }
-
- private Task UpdatePluginAsync(string localPath, string downloadUrl)
- {
- if (File.Exists(localPath))
- File.Delete(localPath);
-
- if (Directory.Exists(localPath))
- Directory.Delete(localPath, true);
-
- var fileName = downloadUrl.Split('/').Last();
- var filePath = Path.Combine(PluginDir, fileName);
-
- return new WebClient().DownloadFileTaskAsync(downloadUrl, filePath);
- }
-
- private void LoadPluginFromFolder(string directory)
+
+ private void LoadPlugin(PluginItem item)
{
var assemblies = new List();
- var files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToList();
- var manifest = GetManifestFromDirectory(directory);
- if (manifest == null)
+ var loaded = AppDomain.CurrentDomain.GetAssemblies();
+
+ if (item.IsZip)
{
- _log.Warn($"Directory {directory} is missing a manifest, skipping load.");
- return;
- }
-
- foreach (var file in files)
- {
- if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
- continue;
-
- using (var stream = File.OpenRead(file))
+ using (var zipFile = ZipFile.OpenRead(item.Path))
{
- var data = stream.ReadToEnd();
- byte[] symbol = null;
- var symbolPath = Path.Combine(Path.GetDirectoryName(file) ?? ".",
- Path.GetFileNameWithoutExtension(file) + ".pdb");
- if (File.Exists(symbolPath))
- try
- {
- using (var symbolStream = File.OpenRead(symbolPath))
- symbol = symbolStream.ReadToEnd();
- }
- catch (Exception e)
- {
- _log.Warn(e, $"Failed to read debugging symbols from {symbolPath}");
- }
- assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
- }
- }
-
- RegisterAllAssemblies(assemblies);
- InstantiatePlugin(manifest, assemblies);
- }
-
- private void LoadPluginFromZip(string path)
- {
- PluginManifest manifest;
- var assemblies = new List();
- using (var zipFile = ZipFile.OpenRead(path))
- {
- manifest = GetManifestFromZip(path);
- if (manifest == null)
- {
- _log.Warn($"Zip file {path} is missing a manifest, skipping.");
- return;
- }
-
- foreach (var entry in zipFile.Entries)
- {
- if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
- continue;
-
-
- using (var stream = entry.Open())
+ foreach (var entry in zipFile.Entries)
{
- var data = stream.ReadToEnd((int)entry.Length);
- byte[] symbol = null;
- var symbolEntryName = entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
- var symbolEntry = zipFile.GetEntry(symbolEntryName);
- if (symbolEntry != null)
- try
- {
- using (var symbolStream = symbolEntry.Open())
- symbol = symbolStream.ReadToEnd((int)symbolEntry.Length);
- }
- catch (Exception e)
- {
- _log.Warn(e, $"Failed to read debugging symbols from {path}:{symbolEntryName}");
- }
- assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
+ if (!entry.Name.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ //if (loaded.Any(a => entry.Name.Contains(a.GetName().Name)))
+ // continue;
+
+
+ using (var stream = entry.Open())
+ {
+ var data = stream.ReadToEnd((int) entry.Length);
+ byte[] symbol = null;
+ var symbolEntryName =
+ entry.FullName.Substring(0, entry.FullName.Length - "dll".Length) + "pdb";
+ var symbolEntry = zipFile.GetEntry(symbolEntryName);
+ if (symbolEntry != null)
+ try
+ {
+ using (var symbolStream = symbolEntry.Open())
+ symbol = symbolStream.ReadToEnd((int) symbolEntry.Length);
+ }
+ catch (Exception e)
+ {
+ _log.Warn(e, $"Failed to read debugging symbols from {item.Filename}:{symbolEntryName}");
+ }
+
+ assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
+ }
}
}
}
+ else
+ {
+ var files = Directory
+ .EnumerateFiles(item.Path, "*.*", SearchOption.AllDirectories)
+ .ToList();
+
+ foreach (var file in files)
+ {
+ if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+ //if (loaded.Any(a => file.Contains(a.GetName().Name)))
+ // continue;
+
+ using (var stream = File.OpenRead(file))
+ {
+ var data = stream.ReadToEnd();
+ byte[] symbol = null;
+ var symbolPath = Path.Combine(Path.GetDirectoryName(file) ?? ".",
+ Path.GetFileNameWithoutExtension(file) + ".pdb");
+ if (File.Exists(symbolPath))
+ try
+ {
+ using (var symbolStream = File.OpenRead(symbolPath))
+ symbol = symbolStream.ReadToEnd();
+ }
+ catch (Exception e)
+ {
+ _log.Warn(e, $"Failed to read debugging symbols from {symbolPath}");
+ }
+
+ assemblies.Add(symbol != null ? Assembly.Load(data, symbol) : Assembly.Load(data));
+ }
+ }
+
+
+ }
+
RegisterAllAssemblies(assemblies);
- InstantiatePlugin(manifest, assemblies);
+ InstantiatePlugin(item.Manifest, assemblies);
}
-
+
private void RegisterAllAssemblies(IReadOnlyCollection assemblies)
{
Assembly ResolveDependentAssembly(object sender, ResolveEventArgs args)
@@ -342,38 +438,12 @@ namespace Torch.Managers
TorchBase.RegisterAuxAssembly(asm);
}
}
-
+
private static bool IsAssemblyCompatible(AssemblyName a, AssemblyName b)
{
return a.Name == b.Name && a.Version.Major == b.Version.Major && a.Version.Minor == b.Version.Minor;
}
-
-
- private PluginManifest GetManifestFromZip(string path)
- {
- using (var zipFile = ZipFile.OpenRead(path))
- {
- foreach (var entry in zipFile.Entries)
- {
- if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
- continue;
-
- using (var stream = new StreamReader(entry.Open()))
- {
- return PluginManifest.Load(stream);
- }
- }
- }
-
- return null;
- }
-
- private PluginManifest GetManifestFromDirectory(string directory)
- {
- var path = Path.Combine(directory, MANIFEST_NAME);
- return !File.Exists(path) ? null : PluginManifest.Load(path);
- }
-
+
private void InstantiatePlugin(PluginManifest manifest, IEnumerable assemblies)
{
Type pluginType = null;
@@ -385,6 +455,9 @@ namespace Torch.Managers
if (!type.GetInterfaces().Contains(typeof(ITorchPlugin)))
continue;
+ if (type.IsAbstract)
+ continue;
+
_log.Info($"Loading plugin at {type.FullName}");
if (pluginType != null)
@@ -423,13 +496,98 @@ namespace Torch.Managers
}
_log.Info($"Loading plugin '{manifest.Name}' ({manifest.Version})");
- var plugin = (TorchPluginBase)Activator.CreateInstance(pluginType);
+ TorchPluginBase plugin;
+ try
+ {
+ plugin = (TorchPluginBase)Activator.CreateInstance(pluginType);
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Plugin {manifest.Name} threw an exception during instantiation! Not loading!");
+ return;
+ }
plugin.Manifest = manifest;
plugin.StoragePath = Torch.Config.InstancePath;
plugin.Torch = Torch;
_plugins.Add(manifest.Guid, plugin);
}
+
+ private PluginManifest GetManifestFromZip(string path)
+ {
+ try
+ {
+ using (var zipFile = ZipFile.OpenRead(path))
+ {
+ foreach (var entry in zipFile.Entries)
+ {
+ if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = new StreamReader(entry.Open()))
+ {
+ return PluginManifest.Load(stream);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, $"Error opening zip! File is likely corrupt. File at {path} will be deleted and re-acquired on the next restart!");
+ File.Delete(path);
+ }
+
+ return null;
+ }
+
+ private bool TryValidatePluginDependencies(List items, ref PluginItem item, out List missingDependencies)
+ {
+ var dependencies = new List();
+ missingDependencies = new List();
+
+ foreach (var pluginDependency in item.Manifest.Dependencies)
+ {
+ var dependency = items
+ .FirstOrDefault(pi => pi?.Manifest.Guid == pluginDependency.Plugin);
+ if (dependency == null)
+ {
+ missingDependencies.Add(pluginDependency.Plugin);
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(pluginDependency.MinVersion)
+ && dependency.Manifest.Version.TryExtractVersion(out var dependencyVersion)
+ && pluginDependency.MinVersion.TryExtractVersion(out var minVersion))
+ {
+ // really only care about version if it is defined.
+ if (dependencyVersion < minVersion)
+ {
+ // If dependency version is too low, we can try to update. Otherwise
+ // it's a missing dependency.
+
+ // For now let's just warn the user. bitMuse is lazy.
+ _log.Warn($"{dependency.Manifest.Name} is below the requested version for {item.Manifest.Name}."
+ + Environment.NewLine
+ + $" Desired version: {pluginDependency.MinVersion}, Available version: {dependency.Manifest.Version}");
+ missingDependencies.Add(pluginDependency.Plugin);
+ continue;
+ }
+ }
+
+ dependencies.Add(dependency);
+ }
+
+ item.ResolvedDependencies = dependencies;
+ if (missingDependencies.Count > 0)
+ return false;
+ return true;
+ }
+
+ private PluginManifest GetManifestFromDirectory(string directory)
+ {
+ var path = Path.Combine(directory, MANIFEST_NAME);
+ return !File.Exists(path) ? null : PluginManifest.Load(path);
+ }
///
public IEnumerator GetEnumerator()
diff --git a/Torch/Plugins/PluginManifest.cs b/Torch/Plugins/PluginManifest.cs
index 13618a9..59ec853 100644
--- a/Torch/Plugins/PluginManifest.cs
+++ b/Torch/Plugins/PluginManifest.cs
@@ -23,8 +23,12 @@ namespace Torch
///
/// A GitHub repository in the format of Author/Repository to retrieve plugin updates.
///
+ [Obsolete("Updates no longer check git. Updates are hosted only on torchapi.net")]
public string Repository { get; set; }
+ //xml tomfoolery
+ public bool ShouldSerializeRepository() => false;
+
///
/// The plugin version. This must include a string in the format of #[.#[.#]] for update checking purposes.
///
@@ -33,7 +37,7 @@ namespace Torch
///
/// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
///
- public List Dependencies { get; } = new List();
+ public List Dependencies { get; } = new List();
public void Save(string path)
{
diff --git a/Torch/Session/TorchSessionManager.cs b/Torch/Session/TorchSessionManager.cs
index 08ed019..d8fab39 100644
--- a/Torch/Session/TorchSessionManager.cs
+++ b/Torch/Session/TorchSessionManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -9,7 +10,9 @@ using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers;
+using Torch.Mod;
using Torch.Session;
+using VRage.Game;
namespace Torch.Session
{
@@ -21,6 +24,15 @@ namespace Torch.Session
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private TorchSession _currentSession;
+ private readonly Dictionary _overrideMods;
+
+ public event Action OverrideModsChanged;
+
+ ///
+ /// List of mods that will be injected into client world downloads.
+ ///
+ public IReadOnlyCollection OverrideMods => _overrideMods.Values;
+
///
public event TorchSessionStateChangedDel SessionStateChanged;
@@ -31,6 +43,8 @@ namespace Torch.Session
public TorchSessionManager(ITorchBase torchInstance) : base(torchInstance)
{
+ _overrideMods = new Dictionary();
+ _overrideMods.Add(TorchModCore.MOD_ID, new MyObjectBuilder_Checkpoint.ModItem(TorchModCore.MOD_ID));
}
///
@@ -49,6 +63,27 @@ namespace Torch.Session
return _factories.Remove(factory);
}
+ ///
+ public bool AddOverrideMod(ulong modId)
+ {
+ if (_overrideMods.ContainsKey(modId))
+ return false;
+ var item = new MyObjectBuilder_Checkpoint.ModItem(modId);
+ _overrideMods.Add(modId, item);
+
+ OverrideModsChanged?.Invoke(new CollectionChangeEventArgs(CollectionChangeAction.Add, item));
+ return true;
+ }
+
+ ///
+ public bool RemoveOverrideMod(ulong modId)
+ {
+ if(_overrideMods.TryGetValue(modId, out var item))
+ OverrideModsChanged?.Invoke(new CollectionChangeEventArgs(CollectionChangeAction.Remove, item));
+
+ return _overrideMods.Remove(modId);
+ }
+
#region Session events
private void SetState(TorchSessionState state)
diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj
index 1a50d4f..c25afce 100644
--- a/Torch/Torch.csproj
+++ b/Torch/Torch.csproj
@@ -33,7 +33,13 @@
MinimumRecommendedRules.ruleset
$(SolutionDir)\bin\x64\Release\Torch.xml
true
+ 1591
+
+
+
+
+
..\packages\ControlzEx.3.0.2.4\lib\net45\ControlzEx.dll
@@ -47,6 +53,9 @@
..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll
+
+ ..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll
+
False
..\GameBinaries\Newtonsoft.Json.dll
@@ -55,11 +64,11 @@
..\packages\NLog.4.4.12\lib\net45\NLog.dll
True
-
- ..\packages\Octokit.0.24.0\lib\net45\Octokit.dll
-
+
+ ..\packages\protobuf-net.2.4.0\lib\net40\protobuf-net.dll
+
..\GameBinaries\Sandbox.Common.dll
False
@@ -84,14 +93,30 @@
..\GameBinaries\SpaceEngineers.ObjectBuilders.XmlSerializers.dll
False
+
+ ..\packages\SteamKit2.2.1.0\lib\netstandard2.0\SteamKit2.dll
+
..\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
+
+
..\packages\ControlzEx.3.0.2.4\lib\net45\System.Windows.Interactivity.dll
True
@@ -143,6 +168,12 @@
..\GameBinaries\VRage.OpenVRWrapper.dll
False
+
+ ..\GameBinaries\VRage.Platform.Windows.dll
+
+
+ ..\GameBinaries\VRage.Platform.Windows.dll
+
..\GameBinaries\VRage.Render.dll
False
@@ -165,14 +196,19 @@
Properties\AssemblyVersion.cs
+
+
+
+
+
@@ -205,11 +241,14 @@
+
+
+
@@ -227,12 +266,12 @@
-
+
@@ -246,7 +285,7 @@
-
+
@@ -256,6 +295,7 @@
+
@@ -295,6 +335,7 @@
+
diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs
index 8f661f8..0658a60 100644
--- a/Torch/TorchBase.cs
+++ b/Torch/TorchBase.cs
@@ -49,6 +49,7 @@ using VRage.Game.SessionComponents;
using VRage.GameServices;
using VRage.Library;
using VRage.ObjectBuilders;
+using VRage.Platform.Windows;
using VRage.Plugins;
using VRage.Scripting;
using VRage.Steam;
@@ -64,6 +65,7 @@ namespace Torch
{
static TorchBase()
{
+ MyVRageWindows.Init("SpaceEngineersDedicated", MySandboxGame.Log, null, false);
ReflectedManager.Process(typeof(TorchBase).Assembly);
ReflectedManager.Process(typeof(ITorchBase).Assembly);
PatchManager.AddPatchShim(typeof(GameStatePatchShim));
@@ -153,7 +155,6 @@ namespace Torch
Plugins = new PluginManager(this);
var sessionManager = new TorchSessionManager(this);
- sessionManager.AddFactory((x) => MyMultiplayer.Static?.SyncLayer != null ? new NetworkManager(this) : null);
sessionManager.AddFactory((x) => Sync.IsServer ? new ChatManagerServer(this) : new ChatManagerClient(this));
sessionManager.AddFactory((x) => Sync.IsServer ? new CommandManager(this) : null);
sessionManager.AddFactory((x) => new EntityManager(this));
@@ -229,7 +230,6 @@ namespace Torch
MySandboxGame.Static.Invoke(action, caller);
}
-
///
[MethodImpl(MethodImplOptions.NoInlining)]
public void InvokeBlocking(Action action, int timeoutMs = -1, [CallerMemberName] string caller = "")
@@ -382,12 +382,14 @@ namespace Torch
{
if (exclusive)
{
- if (MyAsyncSaving.InProgress || Interlocked.Increment(ref _inProgressSaves) != 1)
+ if (MyAsyncSaving.InProgress || _inProgressSaves > 0)
{
Log.Error("Failed to save game, game is already saving");
return null;
}
}
+
+ Interlocked.Increment(ref _inProgressSaves);
return TorchAsyncSaving.Save(this, timeoutMs).ContinueWith((task, torchO) =>
{
var torch = (TorchBase) torchO;
@@ -411,6 +413,7 @@ namespace Torch
Game.SignalStart();
if (!Game.WaitFor(VRageGame.GameState.Running))
Log.Warn("Failed to wait for the game to be started");
+ Invoke(() => Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US"));
}
///
@@ -423,7 +426,7 @@ namespace Torch
}
///
- public abstract void Restart();
+ public abstract void Restart(bool save = true);
///
public virtual void Init(object gameInstance)
@@ -485,6 +488,7 @@ namespace Torch
if (_registeredAuxAssemblies.Add(asm))
{
ReflectedManager.Process(asm);
+ PatchManager.AddPatchShims(asm);
}
}
}
diff --git a/Torch/TorchPluginBase.cs b/Torch/TorchPluginBase.cs
index 64859d9..5e0b59f 100644
--- a/Torch/TorchPluginBase.cs
+++ b/Torch/TorchPluginBase.cs
@@ -28,6 +28,7 @@ namespace Torch
}
public virtual void Update() { }
+ public PluginState State { get; }
public virtual void Dispose() { }
}
diff --git a/Torch/Utils/MiscExtensions.cs b/Torch/Utils/MiscExtensions.cs
index 272b20d..46078dc 100644
--- a/Torch/Utils/MiscExtensions.cs
+++ b/Torch/Utils/MiscExtensions.cs
@@ -1,11 +1,11 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
-using System.Text;
using System.Threading;
-using System.Threading.Tasks;
+using Sandbox.Engine.Multiplayer;
+using Sandbox.Game.Entities;
+using Sandbox.Game.World;
using Steamworks;
namespace Torch.Utils
@@ -44,8 +44,10 @@ namespace Torch.Utils
int count = stream.Read(buffer, streamPosition, buffer.Length - streamPosition);
if (count == 0)
break;
+
streamPosition += count;
}
+
var result = new byte[streamPosition];
Array.Copy(buffer, 0, result, 0, result.Length);
_streamBuffer.Value.SetTarget(buffer);
@@ -57,5 +59,23 @@ namespace Torch.Utils
// What is endianness anyway?
return new IPAddress(BitConverter.GetBytes(state.m_nRemoteIP).Reverse().ToArray());
}
+
+ public static string GetGridOwnerName(this MyCubeGrid grid)
+ {
+ if (grid.BigOwners.Count == 0 || grid.BigOwners[0] == 0)
+ return "nobody";
+
+ var identityId = grid.BigOwners[0];
+
+ if (MySession.Static.Players.IdentityIsNpc(identityId))
+ {
+ var identity = MySession.Static.Players.TryGetIdentity(identityId);
+ return identity.DisplayName;
+ }
+ else
+ {
+ return MyMultiplayer.Static.GetMemberName(MySession.Static.Players.TryGetSteamId(identityId));
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/Torch/Utils/SteamWorkshopTools/KeyValueExtensions.cs b/Torch/Utils/SteamWorkshopTools/KeyValueExtensions.cs
new file mode 100644
index 0000000..1b37983
--- /dev/null
+++ b/Torch/Utils/SteamWorkshopTools/KeyValueExtensions.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.Linq;
+using NLog;
+using SteamKit2;
+
+namespace Torch.Utils.SteamWorkshopTools
+{
+ public static class KeyValueExtensions
+ {
+ private static Logger Log = LogManager.GetLogger("SteamWorkshopService");
+
+ public static T GetValueOrDefault(this KeyValue kv, string key)
+ {
+ kv.TryGetValueOrDefault(key, out T result);
+ return result;
+ }
+ public static bool TryGetValueOrDefault(this KeyValue kv, string key, out T typedValue)
+ {
+ var match = kv.Children?.Find((KeyValue item) => item.Name == key);
+ object result = default(T);
+ if (match == null)
+ {
+ typedValue = (T) result;
+ return false;
+ }
+
+ var value = match.Value ?? "";
+
+ try
+ {
+ var converter = TypeDescriptor.GetConverter(typeof(T));
+ result = converter.ConvertFromString(value);
+ typedValue = (T)result;
+ return true;
+ }
+ catch (NotSupportedException)
+ {
+ throw new Exception($"Unexpected Type '{typeof(T)}'!");
+ }
+ }
+ }
+}
diff --git a/Torch/Utils/SteamWorkshopTools/PublishedItemDetails.cs b/Torch/Utils/SteamWorkshopTools/PublishedItemDetails.cs
new file mode 100644
index 0000000..4842919
--- /dev/null
+++ b/Torch/Utils/SteamWorkshopTools/PublishedItemDetails.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Torch.Utils.SteamWorkshopTools
+{
+ public class PublishedItemDetails
+ {
+ public ulong PublishedFileId;
+ public uint Views;
+ public uint Subscriptions;
+ public DateTime TimeUpdated;
+ public DateTime TimeCreated;
+ public string Description;
+ public string Title;
+ public string FileUrl;
+ public long FileSize;
+ public string FileName;
+ public ulong ConsumerAppId;
+ public ulong CreatorAppId;
+ public ulong Creator;
+ public string[] Tags;
+ }
+}
diff --git a/Torch/Utils/SteamWorkshopTools/WebAPI.cs b/Torch/Utils/SteamWorkshopTools/WebAPI.cs
new file mode 100644
index 0000000..f957172
--- /dev/null
+++ b/Torch/Utils/SteamWorkshopTools/WebAPI.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Serialization;
+using System.IO;
+using System.Net;
+using NLog;
+using SteamKit2;
+using System.Net.Http;
+
+namespace Torch.Utils.SteamWorkshopTools
+{
+ public class WebAPI
+ {
+ private static Logger Log = LogManager.GetLogger("SteamWorkshopService");
+ public const uint AppID = 244850U;
+ public string Username { get; private set; }
+ private string password;
+ public bool IsReady { get; private set; }
+ public bool IsRunning { get; private set; }
+ private TaskCompletionSource logonTaskCompletionSource;
+
+ private SteamClient steamClient;
+ private CallbackManager cbManager;
+ private SteamUser steamUser;
+
+ private static WebAPI _instance;
+ public static WebAPI Instance
+ {
+ get
+ {
+ return _instance ?? (_instance = new WebAPI());
+ }
+ }
+
+ private WebAPI()
+ {
+ steamClient = new SteamClient();
+ cbManager = new CallbackManager(steamClient);
+
+ IsRunning = true;
+ }
+
+ public async Task Logon(string user = "anonymous", string pw = "")
+ {
+ if (string.IsNullOrEmpty(user))
+ throw new ArgumentNullException("User can't be null!");
+ if (!user.Equals("anonymous") && !pw.Equals(""))
+ throw new ArgumentNullException("Password can't be null if user is not anonymous!");
+
+ Username = user;
+ password = pw;
+
+ logonTaskCompletionSource = new TaskCompletionSource();
+
+ steamUser = steamClient.GetHandler();
+ cbManager.Subscribe(OnConnected);
+ cbManager.Subscribe(OnDisconnected);
+ cbManager.Subscribe(OnLoggedOn);
+ cbManager.Subscribe(OnLoggedOff);
+
+ Log.Info("Connecting to Steam...");
+
+ steamClient.Connect();
+
+ await logonTaskCompletionSource.Task;
+ return logonTaskCompletionSource.Task.Result;
+ }
+
+ public void CancelLogon()
+ {
+ logonTaskCompletionSource?.SetCanceled();
+ }
+
+ public async Task> GetPublishedFileDetails(IEnumerable workshopIds)
+ {
+ //if (!IsReady)
+ // throw new Exception("SteamWorkshopService not initialized!");
+
+ using (dynamic remoteStorage = SteamKit2.WebAPI.GetInterface("ISteamRemoteStorage"))
+ {
+ KeyValue allFilesDetails = null ;
+ remoteStorage.Timeout = TimeSpan.FromSeconds(30);
+ allFilesDetails = await Task.Run(delegate {
+ try
+ {
+ return remoteStorage.GetPublishedFileDetails1(
+ itemcount: workshopIds.Count(),
+ publishedfileids: workshopIds,
+ method: HttpMethod.Post);
+ // var ifaceArgs = new Dictionary();
+ // ifaceArgs["itemcount"] = workshopIds.Count().ToString();
+ // no idea if that formatting is correct - in fact I get a 404 response
+ // ifaceArgs["publishedfileids"] = string.Join(",", workshopIds);
+ // return remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", args: ifaceArgs);
+ }
+ catch (HttpRequestException e)
+ {
+ Log.Error($"Fetching File Details failed: {e.Message}");
+ return null;
+ }
+ });
+ if (allFilesDetails == null)
+ return null;
+ //fileDetails = remoteStorage.Call(HttpMethod.Post, "GetPublishedFileDetails", 1, new Dictionary() { { "itemcount", workshopIds.Count().ToString() }, { "publishedfileids", workshopIds.ToString() } });
+ var detailsList = allFilesDetails?.Children.Find((KeyValue kv) => kv.Name == "publishedfiledetails")?.Children;
+ var resultCount = allFilesDetails?.GetValueOrDefault("resultcount");
+ if( detailsList == null || resultCount == null)
+ {
+ Log.Error("Received invalid data: ");
+#if DEBUG
+ if(allFilesDetails != null)
+ PrintKeyValue(allFilesDetails);
+ return null;
+#endif
+ }
+ if ( detailsList.Count != workshopIds.Count() || resultCount != workshopIds.Count())
+ {
+ Log.Error($"Received unexpected number of fileDetails. Expected: {workshopIds.Count()}, Received: {resultCount}");
+ return null;
+ }
+
+ var result = new Dictionary();
+ for( int i = 0; i < resultCount; i++ )
+ {
+ var fileDetails = detailsList[i];
+
+ var tagContainer = fileDetails.Children.Find(item => item.Name == "tags");
+ List tags = new List();
+ if (tagContainer != null)
+ foreach (var tagKv in tagContainer.Children)
+ {
+ var tag = tagKv.Children.Find(item => item.Name == "tag")?.Value;
+ if( tag != null)
+ tags.Add(tag);
+ }
+
+ var publishedFileId = fileDetails.GetValueOrDefault("publishedfileid");
+ result[publishedFileId] = new PublishedItemDetails()
+ {
+ PublishedFileId = publishedFileId,
+ Views = fileDetails.GetValueOrDefault("views"),
+ Subscriptions = fileDetails.GetValueOrDefault("subscriptions"),
+ TimeUpdated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault("time_updated")).DateTime,
+ TimeCreated = DateTimeOffset.FromUnixTimeSeconds(fileDetails.GetValueOrDefault("time_created")).DateTime,
+ Description = fileDetails.GetValueOrDefault("description"),
+ Title = fileDetails.GetValueOrDefault("title"),
+ FileUrl = fileDetails.GetValueOrDefault("file_url"),
+ FileSize = fileDetails.GetValueOrDefault("file_size"),
+ FileName = fileDetails.GetValueOrDefault("filename"),
+ ConsumerAppId = fileDetails.GetValueOrDefault