Merge branch 'master' of git://github.com/TorchAPI/Torch

This commit is contained in:
sirhamsteralot
2019-12-25 23:13:24 +01:00
122 changed files with 5018 additions and 1486 deletions

View File

@@ -1,6 +1,6 @@
pushd pushd
$steamData = "C:/Steam/Data/" $steamData = "C:/Steam/Data-playtest/"
$steamCMDPath = "C:/Steam/steamcmd/" $steamCMDPath = "C:/Steam/steamcmd/"
$steamCMDZip = "C:/Steam/steamcmd.zip" $steamCMDZip = "C:/Steam/steamcmd.zip"
@@ -17,6 +17,6 @@ if (!(Test-Path $steamCMDPath)) {
} }
cd "$steamData" 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 popd

4
Jenkinsfile vendored
View File

@@ -22,7 +22,7 @@ node {
stage('Acquire SE') { stage('Acquire SE') {
bat 'powershell -File Jenkins/jenkins-grab-se.ps1' bat 'powershell -File Jenkins/jenkins-grab-se.ps1'
bat 'IF EXIST GameBinaries RMDIR GameBinaries' 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') { stage('Acquire NuGet Packages') {
@@ -31,7 +31,7 @@ node {
stage('Build') { stage('Build') {
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim() 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" buildMode = "Release"
} else { } else {
buildMode = "Debug" buildMode = "Debug"

21
NLog-user.config Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
<variable name="logContent" value="${message:withException=true}"/>
<targets async="true">
<target xsi:type="Null" name="null" formatMessage="false" />
<target xsi:type="File" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Keen-${shortdate}.log" />
<target xsi:type="File" name="main" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Torch-${shortdate}.log" />
<target xsi:type="File" name="chat" layout="${longdate} ${message}" fileName="Logs\Chat.log" />
<target xsi:type="ColoredConsole" name="console" layout="${var:logStamp} ${logger:shortName=true}: ${var:logContent}" />
<target xsi:type="File" name="patch" layout="${var:logContent}" fileName="Logs\patch.log"/>
<target xsi:type="FlowDocument" name="wpf" layout="${var:logStamp} ${logger:shortName=true}: ${var:logContent}" />
</targets>
<rules>
<!-- Define custom rules below. The example line will pipe all debug output to log file, in-UI console, and independent console. -->
<!--<logger name="*" minlevel="Debug" writeTo="main, console, wpf" />-->
</rules>
</nlog>

View File

@@ -4,6 +4,8 @@
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" /> <variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
<variable name="logContent" value="${message:withException=true}"/> <variable name="logContent" value="${message:withException=true}"/>
<include file="NLog-user.config"/>
<targets async="true"> <targets async="true">
<target xsi:type="Null" name="null" formatMessage="false" /> <target xsi:type="Null" name="null" formatMessage="false" />
<target xsi:type="File" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Keen-${shortdate}.log" /> <target xsi:type="File" name="keen" layout="${var:logStamp} ${logger}: ${var:logContent}" fileName="Logs\Keen-${shortdate}.log" />
@@ -15,6 +17,7 @@
</targets> </targets>
<rules> <rules>
<!-- Do not define custom rules here. Use NLog-user.config -->
<logger name="Keen" minlevel="Warn" writeTo="main"/> <logger name="Keen" minlevel="Warn" writeTo="main"/>
<logger name="Keen" minlevel="Info" writeTo="console, wpf"/> <logger name="Keen" minlevel="Info" writeTo="console, wpf"/>
<logger name="Keen" minlevel="Debug" writeTo="keen" final="true" /> <logger name="Keen" minlevel="Debug" writeTo="keen" final="true" />

View File

@@ -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. * 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. - 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 # 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. 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. If you have a more enjoyable server experience because of Torch, please consider supporting us on Patreon.
[![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269)! [![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269)

View File

@@ -104,7 +104,7 @@ namespace Torch.API
/// <summary> /// <summary>
/// Restart the Torch instance, blocking until the restart has been performed. /// Restart the Torch instance, blocking until the restart has been performed.
/// </summary> /// </summary>
void Restart(); void Restart(bool save = true);
/// <summary> /// <summary>
/// Initializes a save of the game. /// Initializes a save of the game.
@@ -154,6 +154,8 @@ namespace Torch.API
/// Raised when the server's Init() method has completed. /// Raised when the server's Init() method has completed.
/// </summary> /// </summary>
event Action<ITorchServer> Initialized; event Action<ITorchServer> Initialized;
TimeSpan ElapsedPlayTime { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace Torch namespace Torch
{ {
@@ -12,12 +13,17 @@ namespace Torch
string InstancePath { get; set; } string InstancePath { get; set; }
bool NoGui { get; set; } bool NoGui { get; set; }
bool NoUpdate { get; set; } bool NoUpdate { get; set; }
List<string> Plugins { get; set; } List<Guid> Plugins { get; set; }
bool LocalPlugins { get; set; }
bool RestartOnCrash { get; set; } bool RestartOnCrash { get; set; }
bool ShouldUpdatePlugins { get; } bool ShouldUpdatePlugins { get; }
bool ShouldUpdateTorch { get; } bool ShouldUpdateTorch { get; }
int TickTimeout { get; set; } int TickTimeout { get; set; }
string WaitForPID { 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); bool Save(string path = null);
} }

View File

@@ -7,25 +7,25 @@ using System.Threading.Tasks;
namespace Torch.API namespace Torch.API
{ {
/// <summary> /// <summary>
/// Version in the form v#.#.#.#-info /// Version in the form v#.#.#.#-branch
/// </summary> /// </summary>
public class InformationalVersion public class InformationalVersion
{ {
public Version Version { get; set; } public Version Version { get; set; }
public string[] Information { get; set; } public string Branch { get; set; }
public static bool TryParse(string input, out InformationalVersion version) public static bool TryParse(string input, out InformationalVersion version)
{ {
version = default(InformationalVersion); version = default(InformationalVersion);
var trim = input.TrimStart('v'); var trim = input.TrimStart('v');
var info = trim.Split('-'); var info = trim.Split(new[]{'-'}, 2);
if (!Version.TryParse(info[0], out Version result)) if (!Version.TryParse(info[0], out Version result))
return false; return false;
version = new InformationalVersion { Version = result }; version = new InformationalVersion { Version = result };
if (info.Length > 1) if (info.Length > 1)
version.Information = info.Skip(1).ToArray(); version.Branch = info[1];
return true; return true;
} }
@@ -33,10 +33,10 @@ namespace Torch.API
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
if (Information == null || Information.Length == 0) if (Branch == null)
return $"v{Version}"; return $"v{Version}";
return $"v{Version}-{string.Join("-", Information)}"; return $"v{Version}-{string.Join("-", Branch)}";
} }
public static explicit operator InformationalVersion(Version v) public static explicit operator InformationalVersion(Version v)
@@ -48,5 +48,15 @@ namespace Torch.API
{ {
return v.Version; 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;
}
} }
} }

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Gui;
using Sandbox.Game.Multiplayer; using Sandbox.Game.Multiplayer;
using VRage.Game; using VRage.Game;
using VRage.Network; using VRage.Network;
@@ -28,6 +29,8 @@ namespace Torch.API.Managers
AuthorSteamId = null; AuthorSteamId = null;
Author = author; Author = author;
Message = message; Message = message;
Channel = ChatChannel.Global;
Target = 0;
Font = font; Font = font;
} }
@@ -38,12 +41,14 @@ namespace Torch.API.Managers
/// <param name="authorSteamId">Author's steam ID</param> /// <param name="authorSteamId">Author's steam ID</param>
/// <param name="message">Message</param> /// <param name="message">Message</param>
/// <param name="font">Font</param> /// <param name="font">Font</param>
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; Timestamp = DateTime.Now;
AuthorSteamId = authorSteamId; AuthorSteamId = authorSteamId;
Author = author; Author = author;
Message = message; Message = message;
Channel = channel;
Target = target;
Font = font; Font = font;
} }
@@ -53,12 +58,14 @@ namespace Torch.API.Managers
/// <param name="authorSteamId">Author's steam ID</param> /// <param name="authorSteamId">Author's steam ID</param>
/// <param name="message">Message</param> /// <param name="message">Message</param>
/// <param name="font">Font</param> /// <param name="font">Font</param>
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; Timestamp = DateTime.Now;
AuthorSteamId = authorSteamId; AuthorSteamId = authorSteamId;
Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player"; Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player";
Message = message; Message = message;
Channel = channel;
Target = target;
Font = font; Font = font;
} }
@@ -79,6 +86,14 @@ namespace Torch.API.Managers
/// </summary> /// </summary>
public readonly string Message; public readonly string Message;
/// <summary> /// <summary>
/// The chat channel the message is part of.
/// </summary>
public readonly ChatChannel Channel;
/// <summary>
/// The intended recipient of the message.
/// </summary>
public readonly long Target;
/// <summary>
/// The font, or null if default. /// The font, or null if default.
/// </summary> /// </summary>
public readonly string Font; public readonly string Font;

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using VRage.Collections;
using VRage.Network; using VRage.Network;
namespace Torch.API.Managers namespace Torch.API.Managers
@@ -41,5 +42,24 @@ namespace Torch.API.Managers
/// <param name="font">Font to use</param> /// <param name="font">Font to use</param>
/// <param name="targetSteamId">Player to send the message to, or everyone by default</param> /// <param name="targetSteamId">Player to send the message to, or everyone by default</param>
void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0); void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0);
/// <summary>
/// Mute user from global chat.
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
bool MuteUser(ulong steamId);
/// <summary>
/// Unmute user from global chat.
/// </summary>
/// <param name="steamId"></param>
/// <returns></returns>
bool UnmuteUser(ulong steamId);
/// <summary>
/// Users which are not allowed to chat.
/// </summary>
HashSetReader<ulong> MutedUsers { get; }
} }
} }

View File

@@ -34,5 +34,22 @@ namespace Torch.API.Plugins
/// This is called on the game thread after each tick. /// This is called on the game thread after each tick.
/// </summary> /// </summary>
void Update(); void Update();
/// <summary>
/// Plugin's enabled state. Mainly for UI niceness
/// </summary>
PluginState State { get; }
}
public enum PluginState
{
NotInitialized,
DisabledError,
DisabledUser,
UpdateRequired,
UninstallRequested,
NotInstalled,
MissingDependency,
Enabled
} }
} }

View File

@@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Torch.API.Managers; using Torch.API.Managers;
using VRage.Game;
namespace Torch.API.Session namespace Torch.API.Session
{ {
@@ -47,5 +49,29 @@ namespace Torch.API.Session
/// <returns>true if removed, false if not present</returns> /// <returns>true if removed, false if not present</returns>
/// <exception cref="ArgumentNullException">If the factory is null</exception> /// <exception cref="ArgumentNullException">If the factory is null</exception>
bool RemoveFactory(SessionManagerFactoryDel factory); bool RemoveFactory(SessionManagerFactoryDel factory);
/// <summary>
/// Add a mod to be injected into client's world download.
/// </summary>
/// <param name="modId"></param>
/// <returns></returns>
bool AddOverrideMod(ulong modId);
/// <summary>
/// Removes a mod from the injected mod list.
/// </summary>
/// <param name="modId"></param>
/// <returns></returns>
bool RemoveOverrideMod(ulong modId);
/// <summary>
/// List over mods that will be injected into client world downloads.
/// </summary>
IReadOnlyCollection<MyObjectBuilder_Checkpoint.ModItem> OverrideMods { get; }
/// <summary>
/// Event raised when injected mod list changes.
/// </summary>
event Action<CollectionChangeEventArgs> OverrideModsChanged;
} }
} }

View File

@@ -31,6 +31,7 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.API.xml</DocumentationFile> <DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.API.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="HavokWrapper, Version=1.0.6278.22649, Culture=neutral, processorArchitecture=AMD64"> <Reference Include="HavokWrapper, Version=1.0.6278.22649, Culture=neutral, processorArchitecture=AMD64">
@@ -38,6 +39,10 @@
<HintPath>..\GameBinaries\HavokWrapper.dll</HintPath> <HintPath>..\GameBinaries\HavokWrapper.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private> <Private>True</Private>
@@ -188,6 +193,8 @@
<Compile Include="Session\ITorchSessionManager.cs" /> <Compile Include="Session\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" /> <Compile Include="Session\TorchSessionState.cs" />
<Compile Include="TorchGameState.cs" /> <Compile Include="TorchGameState.cs" />
<Compile Include="WebAPI\JenkinsQuery.cs" />
<Compile Include="WebAPI\PluginQuery.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -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<Job> 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<BranchResponse>(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<Job>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize job response!");
return null;
}
return job;
}
public async Task<bool> 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;
}
}
}
}

View File

@@ -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<PluginResponse> 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<PluginResponse>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize plugin query response!");
return null;
}
return response;
}
public async Task<PluginFullItem> QueryOne(Guid guid)
{
return await QueryOne(guid.ToString());
}
public async Task<PluginFullItem> 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<PluginFullItem>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize plugin query response!");
return null;
}
return response;
}
public async Task<bool> DownloadPlugin(Guid guid, string path = null)
{
return await DownloadPlugin(guid.ToString(), path);
}
public async Task<bool> DownloadPlugin(string guid, string path = null)
{
var item = await QueryOne(guid);
return await DownloadPlugin(item, path);
}
public async Task<bool> 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<PluginFullItem>(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;
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" /> <package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" /> <package id="NLog" version="4.4.12" targetFramework="net461" />
</packages> </packages>

View File

@@ -86,6 +86,7 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.4.0.0" newVersion="2.4.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -36,6 +36,7 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit> <Prefer32Bit>true</Prefer32Bit>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Client.xml</DocumentationFile> <DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Client.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<ApplicationIcon>torchicon.ico</ApplicationIcon> <ApplicationIcon>torchicon.ico</ApplicationIcon>
@@ -146,6 +147,7 @@
<Generator>ResXFileCodeGenerator</Generator> <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
<None Include="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
<None Include="Properties\Settings.settings"> <None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator> <Generator>SettingsSingleFileGenerator</Generator>

View File

@@ -23,6 +23,8 @@ namespace Torch.Client
public bool NoGui { get; set; } = false; public bool NoGui { get; set; } = false;
public bool RestartOnCrash { get; set; } = false; public bool RestartOnCrash { get; set; } = false;
public string WaitForPID { get; set; } = null; public string WaitForPID { get; set; } = null;
public string ChatName { get; set; }
public string ChatColor { get; set; }
public bool Save(string path = null) public bool Save(string path = null)
{ {

11
Torch.Client/app.config Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.4.0.0" newVersion="2.4.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

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

View File

@@ -11,6 +11,7 @@ namespace Torch.Mod.Messages
[ProtoInclude(1, typeof(DialogMessage))] [ProtoInclude(1, typeof(DialogMessage))]
[ProtoInclude(2, typeof(NotificationMessage))] [ProtoInclude(2, typeof(NotificationMessage))]
[ProtoInclude(3, typeof(VoxelResetMessage))] [ProtoInclude(3, typeof(VoxelResetMessage))]
[ProtoInclude(4, typeof(JoinServerMessage))]
#endregion #endregion
[ProtoContract] [ProtoContract]

View File

@@ -10,6 +10,7 @@ using Torch.Mod.Messages;
using VRage; using VRage;
using VRage.Collections; using VRage.Collections;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
using VRage.Network;
using VRage.Utils; using VRage.Utils;
using Task = ParallelTasks.Task; using Task = ParallelTasks.Task;
@@ -50,6 +51,10 @@ namespace Torch.Mod
{ {
var m = _messagePool.Get(); var m = _messagePool.Get();
m.CompressedData = bytes; m.CompressedData = bytes;
#if TORCH
m.SenderId = MyEventContext.Current.Sender.Value;
#endif
_processing.Add(m); _processing.Add(m);
} }
@@ -59,10 +64,19 @@ namespace Torch.Mod
{ {
try try
{ {
var m = _processing.Take(); MessageBase m;
try
{
m = _processing.Take();
}
catch
{
continue;
}
MyLog.Default.WriteLineAndConsole($"Processing message: {m.GetType().Name}"); MyLog.Default.WriteLineAndConsole($"Processing message: {m.GetType().Name}");
if (m is IncomingMessage) if (m is IncomingMessage) //process incoming messages
{ {
MessageBase i; MessageBase i;
try try
@@ -78,50 +92,55 @@ namespace Torch.Mod
continue; continue;
} }
if (TorchModCore.Debug)
MyAPIGateway.Utilities.ShowMessage("Torch", $"Received message of type {i.GetType().Name}");
if (MyAPIGateway.Multiplayer.IsServer) if (MyAPIGateway.Multiplayer.IsServer)
i.ProcessServer(); i.ProcessServer();
else else
i.ProcessClient(); 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); var b = MyAPIGateway.Utilities.SerializeToBinary(m);
m.CompressedData = MyCompression.Compress(b); 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) break;
{ case MessageTarget.AllExcept:
case MessageTarget.Single: MyAPIGateway.Players.GetPlayers(_playerCache);
MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, m.Target); foreach (var p in _playerCache)
break; {
case MessageTarget.Server: if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId || m.Ignore.Contains(p.SteamUserId))
MyAPIGateway.Multiplayer.SendMessageToServer(NET_ID, m.CompressedData); continue;
break; MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId);
case MessageTarget.AllClients: }
MyAPIGateway.Players.GetPlayers(_playerCache);
foreach (var p in _playerCache) break;
{ default:
if (p.SteamUserId == MyAPIGateway.Multiplayer.MyId) throw new Exception();
continue; }
MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId);
} _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) 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 //exit signal received. Clean everything and GTFO
_processing?.Dispose(); _processing?.Dispose();
_processing = null; _processing = null;

View File

@@ -10,6 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Messages\IncomingMessage.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Messages\IncomingMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\JoinServerMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\NotificationMessage.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Messages\NotificationMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\DialogMessage.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Messages\DialogMessage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Messages\MessageBase.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Messages\MessageBase.cs" />

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sandbox.ModAPI;
using VRage.Game.Components; using VRage.Game.Components;
namespace Torch.Mod namespace Torch.Mod
@@ -12,6 +13,7 @@ namespace Torch.Mod
{ {
public const ulong MOD_ID = 1406994352; public const ulong MOD_ID = 1406994352;
private static bool _init; private static bool _init;
public static bool Debug;
public override void UpdateAfterSimulation() public override void UpdateAfterSimulation()
{ {
@@ -20,12 +22,24 @@ namespace Torch.Mod
_init = true; _init = true;
ModCommunication.Register(); 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() protected override void UnloadData()
{ {
try try
{ {
MyAPIGateway.Utilities.MessageEntered -= Utilities_MessageEntered;
ModCommunication.Unregister(); ModCommunication.Unregister();
} }
catch catch

View File

@@ -92,6 +92,7 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.4.0.0" newVersion="2.4.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -16,7 +16,6 @@ using NLog.Targets;
using Sandbox.Engine.Utils; using Sandbox.Engine.Utils;
using Torch.Utils; using Torch.Utils;
using VRage.FileSystem; using VRage.FileSystem;
using VRage.Library.Exceptions;
namespace Torch.Server namespace Torch.Server
{ {
@@ -55,6 +54,15 @@ quit";
AppDomain.CurrentDomain.UnhandledException += HandleException; AppDomain.CurrentDomain.UnhandledException += HandleException;
#endif #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. // This is what happens when Keen is bad and puts extensions into the System namespace.
if (!Enumerable.Contains(args, "-noupdate")) if (!Enumerable.Contains(args, "-noupdate"))
RunSteamCmd(); RunSteamCmd();
@@ -64,7 +72,27 @@ quit";
var apiTarget = Path.Combine(basePath, "steam_api64.dll"); var apiTarget = Path.Combine(basePath, "steam_api64.dll");
if (!File.Exists(apiTarget)) if (!File.Exists(apiTarget))
{
File.Copy(apiSource, 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(); _config = InitConfig();
if (!_config.Parse(args)) if (!_config.Parse(args))
@@ -97,28 +125,34 @@ quit";
public void Run() public void Run()
{ {
_server = new TorchServer(_config); _server = new TorchServer(_config);
var init = Task.Run(() => _server.Init()).ContinueWith(x =>
{
if (!x.IsFaulted)
return;
Log.Error("Error initializing server."); if (_config.NoGui)
LogException(x.Exception);
});
if (!_config.NoGui)
{ {
if (_config.Autostart) _server.Init();
init.ContinueWith(x => _server.Start()); _server.Start();
Log.Info("Showing UI");
Console.SetOut(TextWriter.Null);
NativeMethods.FreeConsole();
new TorchUI(_server).ShowDialog();
} }
else else
{ {
init.Wait(); #if !DEBUG
_server.Start(); 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); File.Delete(STEAMCMD_ZIP);
log.Info("SteamCMD downloaded successfully!"); log.Info("SteamCMD downloaded successfully!");
} }
catch catch (Exception e)
{ {
log.Error("Failed to download SteamCMD, unable to update the DS."); log.Error("Failed to download SteamCMD, unable to update the DS.");
log.Error(e);
return; return;
} }
} }
@@ -184,39 +219,49 @@ quit";
// ReSharper disable once PossibleNullReferenceException // ReSharper disable once PossibleNullReferenceException
while (!cmd.HasExited) while (!cmd.HasExited)
{ {
log.Info(cmd.StandardOutput.ReadLine()); log.Info(cmd.StandardOutput.ReadToEnd());
Thread.Sleep(100); Thread.Sleep(100);
} }
} }
private void LogException(Exception ex) private void LogException(Exception ex)
{ {
if (ex.InnerException != null) if (ex is AggregateException ag)
{ {
LogException(ex.InnerException); foreach (var e in ag.InnerExceptions)
LogException(e);
return;
} }
Log.Fatal(ex); Log.Fatal(ex);
if (ex is ReflectionTypeLoadException exti) if (ex is ReflectionTypeLoadException extl)
foreach (Exception exl in exti.LoaderExceptions) {
foreach (var exl in extl.LoaderExceptions)
LogException(exl); LogException(exl);
if (ex is AggregateException ag) return;
foreach (Exception e in ag.InnerExceptions) }
LogException(e);
if (ex.InnerException != null)
{
LogException(ex.InnerException);
}
} }
private void HandleException(object sender, UnhandledExceptionEventArgs e) private void HandleException(object sender, UnhandledExceptionEventArgs e)
{ {
_server.FatalException = true;
var ex = (Exception)e.ExceptionObject; var ex = (Exception)e.ExceptionObject;
LogException(ex); LogException(ex);
if (MyFakes.ENABLE_MINIDUMP_SENDING) if (MyFakes.ENABLE_MINIDUMP_SENDING)
{ {
string path = Path.Combine(MyFileSystem.UserDataPath, "Minidump.dmp"); string path = Path.Combine(MyFileSystem.UserDataPath, "Minidump.dmp");
Log.Info($"Generating minidump at {path}"); Log.Info($"Generating minidump at {path}");
MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo; Log.Error("Keen broke the minidump, sorry.");
MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present); //MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo;
//MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present);
} }
LogManager.Flush(); LogManager.Flush();
if (_config.RestartOnCrash) if (_config.RestartOnCrash)

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -14,6 +15,7 @@ using Sandbox.Game;
using Sandbox.Game.Gui; using Sandbox.Game.Gui;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Collections;
using Torch.Managers; using Torch.Managers;
using Torch.Mod; using Torch.Mod;
using Torch.Server.ViewModels; using Torch.Server.ViewModels;
@@ -69,8 +71,16 @@ namespace Torch.Server.Managers
foreach (var f in worldFolders) foreach (var f in worldFolders)
{ {
if (!string.IsNullOrEmpty(f) && File.Exists(Path.Combine(f, "Sandbox.sbc"))) try
DedicatedConfig.Worlds.Add(new WorldViewModel(f)); {
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) if (DedicatedConfig.Worlds.Count == 0)
@@ -87,14 +97,32 @@ namespace Torch.Server.Managers
public void SelectWorld(string worldPath, bool modsOnly = true) public void SelectWorld(string worldPath, bool modsOnly = true)
{ {
DedicatedConfig.LoadWorld = worldPath; 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) if (DedicatedConfig.SelectedWorld?.Checkpoint != null)
{ {
DedicatedConfig.Mods.Clear(); DedicatedConfig.Mods.Clear();
//remove the Torch mod to avoid running multiple copies of it //remove the Torch mod to avoid running multiple copies of it
DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods)
DedicatedConfig.Mods.Add(m.PublishedFileId); DedicatedConfig.Mods.Add(new ModItemInfo(m));
Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync());
} }
} }
@@ -106,9 +134,10 @@ namespace Torch.Server.Managers
{ {
DedicatedConfig.Mods.Clear(); DedicatedConfig.Mods.Clear();
//remove the Torch mod to avoid running multiple copies of it //remove the Torch mod to avoid running multiple copies of it
DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID); DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods) foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods)
DedicatedConfig.Mods.Add(m.PublishedFileId); 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) private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true)
{ {
var sb = new StringBuilder(); var mods = new MtObservableList<ModItemInfo>();
foreach (var mod in world.Checkpoint.Mods) foreach (var mod in world.WorldConfiguration.Mods)
sb.AppendLine(mod.PublishedFileId.ToString()); mods.Add(new ModItemInfo(mod));
DedicatedConfig.Mods = mods;
DedicatedConfig.Mods = world.Checkpoint.Mods.Select(x => x.PublishedFileId).ToList();
Log.Debug("Loaded mod list from world"); Log.Debug("Loaded mod list from world");
if (!modsOnly) if (!modsOnly)
DedicatedConfig.SessionSettings = world.Checkpoint.Settings; DedicatedConfig.SessionSettings = world.WorldConfiguration.Settings;
} }
private void ImportWorldConfig(bool modsOnly = true) private void ImportWorldConfig(bool modsOnly = true)
@@ -151,7 +179,10 @@ namespace Torch.Server.Managers
return; return;
} }
DedicatedConfig.Mods = checkpoint.Mods.Select(x => x.PublishedFileId).ToList(); var mods = new MtObservableList<ModItemInfo>();
foreach (var mod in checkpoint.Mods)
mods.Add(new ModItemInfo(mod));
DedicatedConfig.Mods = mods;
Log.Debug("Loaded mod list from world"); Log.Debug("Loaded mod list from world");
@@ -167,29 +198,33 @@ namespace Torch.Server.Managers
public void SaveConfig() 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)); DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
Log.Info("Saved dedicated config."); Log.Info("Saved dedicated config.");
try try
{ {
var sandboxPath = Path.Combine(DedicatedConfig.LoadWorld, "Sandbox.sbc"); var world = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == DedicatedConfig.LoadWorld) ?? new WorldViewModel(DedicatedConfig.LoadWorld);
MyObjectBuilderSerializer.DeserializeXML(sandboxPath, out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes);
if (checkpoint == null) 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})"); var savedMod = new MyObjectBuilder_Checkpoint.ModItem(mod.Name, mod.PublishedFileId, mod.FriendlyName);
return; savedMod.IsDependency = mod.IsDependency;
world.WorldConfiguration.Mods.Add(savedMod);
} }
Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync());
checkpoint.SessionName = DedicatedConfig.WorldName; world.SaveSandbox();
checkpoint.Settings = DedicatedConfig.SessionSettings;
checkpoint.Mods.Clear();
foreach (var modId in DedicatedConfig.Mods)
checkpoint.Mods.Add(new MyObjectBuilder_Checkpoint.ModItem(modId));
MyObjectBuilderSerializer.SerializeXML(sandboxPath, false, checkpoint);
//MyLocalCache.SaveCheckpoint(checkpoint, DedicatedConfig.LoadWorld);
Log.Info("Saved world config."); Log.Info("Saved world config.");
} }
catch (Exception e) catch (Exception e)
@@ -223,35 +258,63 @@ namespace Torch.Server.Managers
public string WorldPath { get; } public string WorldPath { get; }
public long WorldSizeKB { get; } public long WorldSizeKB { get; }
private string _checkpointPath; private string _checkpointPath;
private string _worldConfigPath;
public CheckpointViewModel Checkpoint { get; private set; } public CheckpointViewModel Checkpoint { get; private set; }
public WorldConfigurationViewModel WorldConfiguration { get; private set; }
public WorldViewModel(string worldPath) public WorldViewModel(string worldPath)
{ {
WorldPath = worldPath; try
WorldSizeKB = new DirectoryInfo(worldPath).GetFiles().Sum(x => x.Length) / 1024; {
_checkpointPath = Path.Combine(WorldPath, "Sandbox.sbc"); WorldPath = worldPath;
FolderName = Path.GetFileName(worldPath); WorldSizeKB = new DirectoryInfo(worldPath).GetFiles().Sum(x => x.Length) / 1024;
BeginLoadCheckpoint(); _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(_worldConfigPath, out MyObjectBuilder_WorldConfiguration worldConfig);
MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint); WorldConfiguration = new WorldConfigurationViewModel(worldConfig);
Checkpoint = new CheckpointViewModel(checkpoint); }
OnPropertyChanged(nameof(Checkpoint)); 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));
} }
} }
} }

View File

@@ -209,26 +209,21 @@ namespace Torch.Server.Managers
//Largely copied from SE //Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamId, JoinResult response, ulong steamOwner) private void ValidateAuthTicketResponse(ulong steamId, JoinResult response, ulong steamOwner)
{ {
//SteamNetworking.GetP2PSessionState(new CSteamID(steamId), out P2PSessionState_t state); var state = new MyP2PSessionState();
//state.GetRemoteIP(); MySteamServiceWrapper.Static.Peer2Peer.GetSessionState(steamId, ref state);
MyP2PSessionState statehack = new MyP2PSessionState(); var ip = new IPAddress(BitConverter.GetBytes(state.RemoteIP).Reverse().ToArray());
VRage.Steam.MySteamService.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray());
Torch.CurrentSession.KeenSession.PromotedUsers.TryGetValue(steamId, out MyPromoteLevel promoteLevel); Torch.CurrentSession.KeenSession.PromotedUsers.TryGetValue(steamId, out MyPromoteLevel promoteLevel);
_log.Debug($"ValidateAuthTicketResponse(user={steamId}, response={response}, owner={steamOwner}, permissions={promoteLevel})"); _log.Debug($"ValidateAuthTicketResponse(user={steamId}, response={response}, owner={steamOwner}, permissions={promoteLevel})");
_log.Info($"Connection attempt by {steamId} from {ip}"); _log.Info($"Connection attempt by {steamId} from {ip}");
// TODO implement IP bans
var config = (TorchConfig) Torch.Config; if (Players.ContainsKey(steamId))
if (config.EnableWhitelist && !config.Whitelist.Contains(steamId))
{ {
_log.Warn($"Rejecting user {steamId} because they are not whitelisted in Torch.cfg."); _log.Warn($"Player {steamId} has already joined!");
UserRejected(steamId, JoinResult.NotInGroup); UserRejected(steamId, JoinResult.AlreadyJoined);
} }
else if(config.EnableReservedSlots && config.ReservedPlayers.Contains(steamId))
UserAccepted(steamId);
else if (Torch.CurrentSession.KeenSession.OnlineMode == MyOnlineModeEnum.OFFLINE && else if (Torch.CurrentSession.KeenSession.OnlineMode == MyOnlineModeEnum.OFFLINE &&
promoteLevel < MyPromoteLevel.Admin) promoteLevel < MyPromoteLevel.Admin)
{ {
@@ -252,40 +247,48 @@ namespace Torch.Server.Managers
private void RunEvent(ValidateAuthTicketEvent info) 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)) var config = (TorchConfig) Torch.Config;
CommitVerdict(info.SteamID, JoinResult.BannedByAdmins); if (config.EnableWhitelist && !config.Whitelist.Contains(info.SteamID))
else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) ||
_isClientKicked(MyMultiplayer.Static, info.SteamOwner))
CommitVerdict(info.SteamID, JoinResult.KickedRecently);
else if (info.SteamResponse == JoinResult.OK)
{ {
//Admins can bypass member limit _log.Warn($"Rejecting user {info.SteamID} because they are not whitelisted in Torch.cfg.");
if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) || internalAuth = JoinResult.NotInGroup;
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);
}
} }
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 else
CommitVerdict(info.SteamID, info.SteamResponse); {
if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer))
return; internalAuth = JoinResult.OK;
else
internalAuth = JoinResult.NotInGroup;
}
} }
else
internalAuth = info.SteamResponse;
info.FutureVerdict = Task.FromResult(internalAuth);
MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info);
info.FutureVerdict.ContinueWith((task) => info.FutureVerdict.ContinueWith((task) =>
{ {

View File

@@ -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
{
/// <inheritdoc />
public RemoteAPIManager(ITorchBase torchInstance) : base(torchInstance)
{
}
/// <inheritdoc />
public override void Attach()
{
Torch.GameStateChanged += TorchOnGameStateChanged;
base.Attach();
}
/// <inheritdoc />
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}");
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Patches MySandboxGame.InitQuickLaunch to rethrow exceptions caught during session load.
/// </summary>
[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<MsilInstruction> Transpile(IEnumerable<MsilInstruction> 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;
}
}
}

View File

@@ -2,12 +2,15 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.ServiceProcess; using System.ServiceProcess;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Microsoft.VisualBasic.Devices;
using NLog; using NLog;
using NLog.Fluent;
using NLog.Targets; using NLog.Targets;
using Torch.Utils; using Torch.Utils;
@@ -21,21 +24,46 @@ namespace Torch.Server
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
Target.Register<FlowDocumentTarget>("FlowDocument"); Target.Register<FlowDocumentTarget>("FlowDocument");
//Ensures that all the files are downloaded in the Torch directory. //Ensures that all the files are downloaded in the Torch directory.
var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString(); var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString();
var binDir = Path.Combine(workingDir, "DedicatedServer64"); var binDir = Path.Combine(workingDir, "DedicatedServer64");
Directory.SetCurrentDirectory(workingDir); 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()) if (!TorchLauncher.IsTorchWrapped())
{ {
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName, args, binDir); TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName, args, binDir);
return; 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); ServiceBase.Run(service);
return; return;
} }

View File

@@ -70,6 +70,9 @@
<Reference Include="MahApps.Metro, Version=1.6.1.4, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="MahApps.Metro, Version=1.6.1.4, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll</HintPath> <HintPath>..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll</HintPath>
</Reference> </Reference>
<Reference Include="Markdown.Xaml, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Markdown.Xaml.1.0.0\lib\net45\Markdown.Xaml.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <Reference Include="Microsoft.CodeAnalysis, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.dll</HintPath> <HintPath>..\GameBinaries\Microsoft.CodeAnalysis.dll</HintPath>
@@ -80,13 +83,25 @@
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll</HintPath> <HintPath>..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <Reference Include="Microsoft.VisualBasic" />
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath> <Reference Include="Microsoft.Win32.Registry, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Win32.Registry.4.4.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
<HintPath>..\GameBinaries\netstandard.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="protobuf-net, Version=2.4.0.0, Culture=neutral, PublicKeyToken=257b51d87d2e4d67, processorArchitecture=MSIL">
<HintPath>..\packages\protobuf-net.2.4.0\lib\net40\protobuf-net.dll</HintPath>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath> <HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath>
@@ -121,11 +136,24 @@
<HintPath>..\GameBinaries\Steamworks.NET.dll</HintPath> <HintPath>..\GameBinaries\Steamworks.NET.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.Annotations, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.ComponentModel.DataAnnotations" /> <Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Configuration" />
<Reference Include="System.Configuration.Install" /> <Reference Include="System.Configuration.Install" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" /> <Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Security.AccessControl, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.AccessControl.4.4.0\lib\net461\System.Security.AccessControl.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal.Windows, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.Principal.Windows.4.4.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <Reference Include="System.Windows.Interactivity, Version=4.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
@@ -180,15 +208,8 @@
<HintPath>..\GameBinaries\VRage.Math.dll</HintPath> <HintPath>..\GameBinaries\VRage.Math.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="VRage.Native, Version=0.0.0.0, Culture=neutral, processorArchitecture=AMD64"> <Reference Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<SpecificVersion>False</SpecificVersion> <HintPath>..\GameBinaries\VRage.Platform.Windows.dll</HintPath>
<HintPath>..\GameBinaries\VRage.Native.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.OpenVRWrapper, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\VRage.OpenVRWrapper.dll</HintPath>
<Private>False</Private>
</Reference> </Reference>
<Reference Include="VRage.Render, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="VRage.Render, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
@@ -225,9 +246,11 @@
<Compile Include="Managers\InstanceManager.cs" /> <Compile Include="Managers\InstanceManager.cs" />
<Compile Include="Managers\MultiplayerManagerDedicatedEventShim.cs" /> <Compile Include="Managers\MultiplayerManagerDedicatedEventShim.cs" />
<Compile Include="Managers\MultiplayerManagerDedicatedPatchShim.cs" /> <Compile Include="Managers\MultiplayerManagerDedicatedPatchShim.cs" />
<Compile Include="Managers\RemoteAPIManager.cs" />
<Compile Include="NativeMethods.cs" /> <Compile Include="NativeMethods.cs" />
<Compile Include="Initializer.cs" /> <Compile Include="Initializer.cs" />
<Compile Include="Patches\PromotePatch.cs" /> <Compile Include="Patches\PromotePatch.cs" />
<Compile Include="Patches\WorldLoadExceptionPatch.cs" />
<Compile Include="Properties\Annotations.cs" /> <Compile Include="Properties\Annotations.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TorchConfig.cs" /> <Compile Include="TorchConfig.cs" />
@@ -245,15 +268,29 @@
<Compile Include="ViewModels\Entities\CharacterViewModel.cs" /> <Compile Include="ViewModels\Entities\CharacterViewModel.cs" />
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" /> <Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
<Compile Include="ViewModels\Entities\EntityControlViewModel.cs" /> <Compile Include="ViewModels\Entities\EntityControlViewModel.cs" />
<Compile Include="ViewModels\ModItemInfo.cs" />
<Compile Include="ViewModels\SessionSettingsViewModel.cs" /> <Compile Include="ViewModels\SessionSettingsViewModel.cs" />
<Compile Include="ViewModels\WorldConfigurationViewModel.cs" />
<Compile Include="Views\Converters\DefinitionToIdConverter.cs" /> <Compile Include="Views\Converters\DefinitionToIdConverter.cs" />
<Compile Include="Views\Converters\BooleanAndConverter.cs" /> <Compile Include="Views\Converters\BooleanAndConverter.cs" />
<Compile Include="Views\Converters\ListConverter.cs" /> <Compile Include="Views\Converters\ListConverter.cs" />
<Compile Include="MultiTextWriter.cs" /> <Compile Include="MultiTextWriter.cs" />
<Compile Include="RichTextBoxWriter.cs" /> <Compile Include="RichTextBoxWriter.cs" />
<Compile Include="Views\Converters\ListConverterWorkshopId.cs" />
<Compile Include="Views\Converters\ModToIdConverter.cs" />
<Compile Include="Views\Entities\CharacterView.xaml.cs"> <Compile Include="Views\Entities\CharacterView.xaml.cs">
<DependentUpon>CharacterView.xaml</DependentUpon> <DependentUpon>CharacterView.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Views\Extensions.cs" />
<Compile Include="Views\ModListControl.xaml.cs">
<DependentUpon>ModListControl.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PluginBrowser.xaml.cs">
<DependentUpon>PluginBrowser.xaml</DependentUpon>
</Compile>
<Compile Include="Views\RoleEditor.xaml.cs">
<DependentUpon>RoleEditor.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ThemeControl.xaml.cs"> <Compile Include="Views\ThemeControl.xaml.cs">
<DependentUpon>ThemeControl.xaml</DependentUpon> <DependentUpon>ThemeControl.xaml</DependentUpon>
</Compile> </Compile>
@@ -414,6 +451,14 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\ModListControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PluginBrowser.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PluginsControl.xaml"> <Page Include="Views\PluginsControl.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -430,6 +475,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\RoleEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\SessionSettingsView.xaml"> <Page Include="Views\SessionSettingsView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -469,9 +518,10 @@
<Install>false</Install> <Install>false</Install>
</BootstrapperPackage> </BootstrapperPackage>
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" /> <Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)"</PostBuildEvent> <PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)" &amp; copy "$(SolutionDir)NLog-user.config" "$(TargetDir)"</PostBuildEvent>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -5,6 +5,7 @@ using System.Windows;
using System.Xml.Serialization; using System.Xml.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json;
using NLog; using NLog;
using VRage.Game;
namespace Torch.Server namespace Torch.Server
{ {
@@ -20,9 +21,38 @@ namespace Torch.Server
[Arg("instancename", "The name of the Torch instance.")] [Arg("instancename", "The name of the Torch instance.")]
public string InstanceName { get; set; } public string InstanceName { get; set; }
private string _instancePath;
/// <inheritdoc /> /// <inheritdoc />
[Arg("instancepath", "Server data folder where saves and mods are stored.")] [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;
}
}
/// <inheritdoc /> /// <inheritdoc />
[XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")] [XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")]
@@ -58,18 +88,37 @@ namespace Torch.Server
public int TickTimeout { get; set; } = 60; public int TickTimeout { get; set; } = 60;
/// <inheritdoc /> /// <inheritdoc />
public List<string> Plugins { get; set; } = new List<string>(); [Arg("plugins", "Starts Torch with the given plugin GUIDs (space delimited).")]
public List<Guid> Plugins { get; set; } = new List<Guid>();
[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 bool EnableWhitelist { get; set; } = false;
public HashSet<ulong> Whitelist { get; set; } = new HashSet<ulong>(); public HashSet<ulong> Whitelist { get; set; } = new HashSet<ulong>();
internal Point WindowSize { get; set; } = new Point(800, 600); public Point WindowSize { get; set; } = new Point(800, 600);
internal Point WindowPosition { get; set; } = new Point(); public Point WindowPosition { get; set; } = new Point();
public string LastUsedTheme { get; set; } = "Torch Theme"; public string LastUsedTheme { get; set; } = "Torch Theme";
public bool EnableReservedSlots { get; set; } = false; //Prevent reserved players being written to disk, but allow it to be read
public HashSet<ulong> ReservedPlayers { get; set; } = new HashSet<ulong>(); //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] [XmlIgnore]
private string _path; private string _path;

View File

@@ -19,6 +19,7 @@ using Torch.API.Managers;
using Torch.API.Session; using Torch.API.Session;
using Torch.Commands; using Torch.Commands;
using Torch.Mod; using Torch.Mod;
using Torch.Mod.Messages;
using Torch.Server.Commands; using Torch.Server.Commands;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Utils; using Torch.Utils;
@@ -26,6 +27,7 @@ using VRage;
using VRage.Dedicated; using VRage.Dedicated;
using VRage.Dedicated.RemoteAPI; using VRage.Dedicated.RemoteAPI;
using VRage.GameServices; using VRage.GameServices;
using VRage.Scripting;
using VRage.Steam; using VRage.Steam;
using Timer = System.Threading.Timer; using Timer = System.Threading.Timer;
@@ -37,14 +39,18 @@ namespace Torch.Server
{ {
public class TorchServer : TorchBase, ITorchServer public class TorchServer : TorchBase, ITorchServer
{ {
private bool _hasRun;
private bool _canRun; private bool _canRun;
private TimeSpan _elapsedPlayTime; private TimeSpan _elapsedPlayTime;
private bool _hasRun;
private bool _isRunning; private bool _isRunning;
private float _simRatio; private float _simRatio;
private ServerState _state; private ServerState _state;
private Stopwatch _uptime; private Stopwatch _uptime;
private Timer _watchdog; private Timer _watchdog;
private int _players;
private MultiplayerManagerDedicated _multiplayerManagerDedicated;
internal bool FatalException { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public TorchServer(TorchConfig config = null) public TorchServer(TorchConfig config = null)
@@ -52,12 +58,15 @@ namespace Torch.Server
DedicatedInstance = new InstanceManager(this); DedicatedInstance = new InstanceManager(this);
AddManager(DedicatedInstance); AddManager(DedicatedInstance);
AddManager(new EntityControlManager(this)); AddManager(new EntityControlManager(this));
AddManager(new RemoteAPIManager(this));
Config = config ?? new TorchConfig(); Config = config ?? new TorchConfig();
var sessionManager = Managers.GetManager<ITorchSessionManager>(); var sessionManager = Managers.GetManager<ITorchSessionManager>();
sessionManager.AddFactory(x => new MultiplayerManagerDedicated(this)); sessionManager.AddFactory(x => new MultiplayerManagerDedicated(this));
} }
public bool HasRun { get => _hasRun; set => SetValue(ref _hasRun, value); }
/// <inheritdoc /> /// <inheritdoc />
public float SimulationRatio { get => _simRatio; set => SetValue(ref _simRatio, value); } public float SimulationRatio { get => _simRatio; set => SetValue(ref _simRatio, value); }
@@ -92,6 +101,8 @@ namespace Torch.Server
/// <inheritdoc /> /// <inheritdoc />
public string InstancePath => Config?.InstancePath; public string InstancePath => Config?.InstancePath;
public int OnlinePlayers { get => _players; private set => SetValue(ref _players, value); }
/// <inheritdoc /> /// <inheritdoc />
public override void Init() public override void Init()
{ {
@@ -111,7 +122,7 @@ namespace Torch.Server
if (State != ServerState.Stopped) if (State != ServerState.Stopped)
return; return;
if (_hasRun) if (IsRunning || HasRun)
{ {
Restart(); Restart();
return; return;
@@ -119,15 +130,10 @@ namespace Torch.Server
State = ServerState.Starting; State = ServerState.Starting;
IsRunning = true; IsRunning = true;
HasRun = true;
CanRun = false; CanRun = false;
_hasRun = true;
Log.Info("Starting server."); Log.Info("Starting server.");
MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model; 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(); _uptime = Stopwatch.StartNew();
base.Start(); base.Start();
@@ -150,9 +156,15 @@ namespace Torch.Server
/// <summary> /// <summary>
/// Restart the program. /// Restart the program.
/// </summary> /// </summary>
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); Save().ContinueWith(DoRestart, this, TaskContinuationOptions.RunContinuationsAsynchronously);
else else
DoRestart(null, this); DoRestart(null, this);
@@ -186,6 +198,7 @@ namespace Torch.Server
if (newState == TorchSessionState.Loaded) if (newState == TorchSessionState.Loaded)
{ {
_multiplayerManagerDedicated = CurrentSession.Managers.GetManager<MultiplayerManagerDedicated>();
CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands)); CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands));
ModCommunication.Register(); ModCommunication.Register();
} }
@@ -195,8 +208,7 @@ namespace Torch.Server
public override void Init(object gameInstance) public override void Init(object gameInstance)
{ {
base.Init(gameInstance); base.Init(gameInstance);
var game = gameInstance as MySandboxGame; if (gameInstance is MySandboxGame && MySession.Static != null)
if (game != null && MySession.Static != null)
State = ServerState.Running; State = ServerState.Running;
else else
State = ServerState.Stopped; State = ServerState.Stopped;
@@ -210,6 +222,7 @@ namespace Torch.Server
SimulationRatio = Math.Min(Sync.ServerSimulationRatio, 1); SimulationRatio = Math.Min(Sync.ServerSimulationRatio, 1);
var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds)); var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds));
ElapsedPlayTime = elapsed; ElapsedPlayTime = elapsed;
OnlinePlayers = _multiplayerManagerDedicated?.Players.Count ?? 0;
if (_watchdog == null && Config.TickTimeout > 0) if (_watchdog == null && Config.TickTimeout > 0)
{ {
@@ -223,10 +236,16 @@ namespace Torch.Server
private static void CheckServerResponding(object state) private static void CheckServerResponding(object state)
{ {
var server = (TorchServer)state;
var mre = new ManualResetEvent(false); var mre = new ManualResetEvent(false);
((TorchServer)state).Invoke(() => mre.Set()); server.Invoke(() => mre.Set());
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
{ {
if (server.FatalException)
{
server._watchdog.Dispose();
return;
}
#if DEBUG #if DEBUG
Log.Error( Log.Error(
$"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds."); $"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds.");

View File

@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.ServiceProcess; using System.ServiceProcess;
using System.Threading;
using NLog; using NLog;
using Torch.API; using Torch.API;
@@ -12,12 +14,14 @@ namespace Torch.Server
{ {
class TorchService : ServiceBase class TorchService : ServiceBase
{ {
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public const string Name = "Torch (SEDS)"; public const string Name = "Torch (SEDS)";
private TorchServer _server;
private Initializer _initializer; private Initializer _initializer;
private string[] _args;
public TorchService() public TorchService(string[] args)
{ {
_args = args;
var workingDir = new FileInfo(typeof(TorchService).Assembly.Location).Directory.ToString(); var workingDir = new FileInfo(typeof(TorchService).Assembly.Location).Directory.ToString();
Directory.SetCurrentDirectory(workingDir); Directory.SetCurrentDirectory(workingDir);
_initializer = new Initializer(workingDir); _initializer = new Initializer(workingDir);
@@ -29,19 +33,21 @@ namespace Torch.Server
} }
/// <inheritdoc /> /// <inheritdoc />
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(); _initializer.Run();
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void OnStop() protected override void OnStop()
{ {
_server.Stop(); var mre = new ManualResetEvent(false);
base.OnStop(); Task.Run(() => _initializer.Server.Stop());
if (!mre.WaitOne(TimeSpan.FromMinutes(1)))
Process.GetCurrentProcess().Kill();
} }
} }
} }

View File

@@ -16,12 +16,12 @@ namespace Torch.Server.ViewModels
public class CheckpointViewModel : ViewModel public class CheckpointViewModel : ViewModel
{ {
private MyObjectBuilder_Checkpoint _checkpoint; private MyObjectBuilder_Checkpoint _checkpoint;
private SessionSettingsViewModel _sessionSettings; //private SessionSettingsViewModel _sessionSettings;
public CheckpointViewModel(MyObjectBuilder_Checkpoint checkpoint) public CheckpointViewModel(MyObjectBuilder_Checkpoint checkpoint)
{ {
_checkpoint = checkpoint; _checkpoint = checkpoint;
_sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings); //_sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings);
} }
public static implicit operator MyObjectBuilder_Checkpoint(CheckpointViewModel model) public static implicit operator MyObjectBuilder_Checkpoint(CheckpointViewModel model)
@@ -59,15 +59,15 @@ namespace Torch.Server.ViewModels
public SerializableDictionary<long, MyObjectBuilder_Checkpoint.PlayerId> ControlledEntities { get => _checkpoint.ControlledEntities; set => SetValue(ref _checkpoint.ControlledEntities, value); } public SerializableDictionary<long, MyObjectBuilder_Checkpoint.PlayerId> ControlledEntities { get => _checkpoint.ControlledEntities; set => SetValue(ref _checkpoint.ControlledEntities, value); }
public SessionSettingsViewModel Settings //public SessionSettingsViewModel Settings
{ //{
get => _sessionSettings; // get => _sessionSettings;
set // set
{ // {
SetValue(ref _sessionSettings, value); // SetValue(ref _sessionSettings, value);
_checkpoint.Settings = _sessionSettings; // _checkpoint.Settings = _sessionSettings;
} // }
} //}
public MyObjectBuilder_ScriptManager ScriptManagerData => throw new NotImplementedException(); public MyObjectBuilder_ScriptManager ScriptManagerData => throw new NotImplementedException();
@@ -75,7 +75,7 @@ namespace Torch.Server.ViewModels
public MyObjectBuilder_FactionCollection Factions => throw new NotImplementedException(); public MyObjectBuilder_FactionCollection Factions => throw new NotImplementedException();
public List<MyObjectBuilder_Checkpoint.ModItem> Mods { get => _checkpoint.Mods; set => SetValue(ref _checkpoint.Mods, value); } //public List<MyObjectBuilder_Checkpoint.ModItem> Mods { get => _checkpoint.Mods; set => SetValue(ref _checkpoint.Mods, value); }
public SerializableDictionary<ulong, MyPromoteLevel> PromotedUsers { get => _checkpoint.PromotedUsers; set => SetValue(ref _checkpoint.PromotedUsers, value); } public SerializableDictionary<ulong, MyPromoteLevel> PromotedUsers { get => _checkpoint.PromotedUsers; set => SetValue(ref _checkpoint.PromotedUsers, value); }

View File

@@ -10,6 +10,8 @@ using Torch.Collections;
using Torch.Server.Managers; using Torch.Server.Managers;
using VRage.Game; using VRage.Game;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
using Torch.Utils.SteamWorkshopTools;
using Torch.Collections;
namespace Torch.Server.ViewModels namespace Torch.Server.ViewModels
{ {
@@ -27,8 +29,9 @@ namespace Torch.Server.ViewModels
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated) public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
{ {
_config = configDedicated; _config = configDedicated;
_config.IgnoreLastSession = true; //_config.IgnoreLastSession = true;
SessionSettings = new SessionSettingsViewModel(_config.SessionSettings); SessionSettings = new SessionSettingsViewModel(_config.SessionSettings);
Task.Run(() => UpdateAllModInfosAsync());
} }
public void Save(string path = null) public void Save(string path = null)
@@ -37,7 +40,7 @@ namespace Torch.Server.ViewModels
_config.SessionSettings = _sessionSettings; _config.SessionSettings = _sessionSettings;
// Never ever // Never ever
_config.IgnoreLastSession = true; //_config.IgnoreLastSession = true;
_config.Save(path); _config.Save(path);
} }
@@ -73,12 +76,61 @@ namespace Torch.Server.ViewModels
} }
} }
public async Task UpdateAllModInfosAsync(Action<string> messageHandler = null)
{
if (Mods.Count() == 0)
return;
var ids = Mods.Select(m => m.PublishedFileId);
var workshopService = WebAPI.Instance;
Dictionary<ulong, PublishedItemDetails> 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<string> Administrators { get => _config.Administrators; set => SetValue(x => _config.Administrators = x, value); } public List<string> Administrators { get => _config.Administrators; set => SetValue(x => _config.Administrators = x, value); }
public List<ulong> Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); } public List<ulong> Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); }
private List<ulong> _mods = new List<ulong>(); private MtObservableList<ModItemInfo> _mods = new MtObservableList<ModItemInfo>();
public List<ulong> Mods { get => _mods; set => SetValue(x => _mods = x, value); } public MtObservableList<ModItemInfo> Mods
{
get => _mods;
set
{
SetValue(x => _mods = x, value);
Task.Run(() => UpdateAllModInfosAsync());
}
}
public List<ulong> Reserved { get => _config.Reserved; set => SetValue(x => _config.Reserved = x, value); }
public int AsteroidAmount { get => _config.AsteroidAmount; set => SetValue(x => _config.AsteroidAmount = 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 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 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 PremadeCheckpointPath { get => _config.PremadeCheckpointPath; set => SetValue(x => _config.PremadeCheckpointPath = x, value); }
public string LoadWorld { get => _config.LoadWorld; set => SetValue(x => _config.LoadWorld = x, value); } public string LoadWorld { get => _config.LoadWorld; set => SetValue(x => _config.LoadWorld = x, value); }

View File

@@ -4,14 +4,24 @@ namespace Torch.Server.ViewModels.Entities
{ {
public class CharacterViewModel : EntityViewModel public class CharacterViewModel : EntityViewModel
{ {
private MyCharacter _character;
public CharacterViewModel(MyCharacter character, EntityTreeViewModel tree) : base(character, tree) public CharacterViewModel(MyCharacter character, EntityTreeViewModel tree) : base(character, tree)
{ {
character.ControllerInfo.ControlAcquired += (x) => { OnPropertyChanged(nameof(Name)); }; _character = character;
character.ControllerInfo.ControlReleased += (x) => { OnPropertyChanged(nameof(Name)); }; 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 CharacterViewModel()
{ {
} }
public override bool CanDelete => _character.ControllerInfo?.Controller?.Player == null;
} }
} }

View File

@@ -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 Sandbox.Game.World;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Collections; using Torch.Collections;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Utils;
using VRage.Game.Entity; using VRage.Game.Entity;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
using VRage.ModAPI; using VRage.ModAPI;
@@ -14,6 +20,8 @@ namespace Torch.Server.ViewModels.Entities
{ {
protected EntityTreeViewModel Tree { get; } protected EntityTreeViewModel Tree { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private IMyEntity _backing; private IMyEntity _backing;
public IMyEntity Entity 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 public virtual string Position
{ {
get => Entity?.GetPosition().ToString(); get => Entity?.GetPosition().ToString();
@@ -59,7 +147,7 @@ namespace Torch.Server.ViewModels.Entities
public virtual bool CanStop => Entity.Physics?.Enabled ?? false; public virtual bool CanStop => Entity.Physics?.Enabled ?? false;
public virtual bool CanDelete => !(Entity is IMyCharacter); public virtual bool CanDelete => true;
public virtual void Delete() public virtual void Delete()
{ {
@@ -76,5 +164,20 @@ namespace Torch.Server.ViewModels.Entities
{ {
} }
public class Comparer : IComparer<EntityViewModel>
{
private EntityTreeViewModel.SortEnum _sort;
public Comparer(EntityTreeViewModel.SortEnum sort)
{
_sort = sort;
}
public int Compare(EntityViewModel x, EntityViewModel y)
{
return x.CompareToSort(y, _sort);
}
}
} }
} }

View File

@@ -51,16 +51,13 @@ namespace Torch.Server.ViewModels.Entities
new MtObservableSortedDictionary<MyCubeBlockDefinition, MtObservableSortedDictionary<long, BlockViewModel>>( new MtObservableSortedDictionary<MyCubeBlockDefinition, MtObservableSortedDictionary<long, BlockViewModel>>(
CubeBlockDefinitionComparer.Default); CubeBlockDefinitionComparer.Default);
/// <inheritdoc />
public string DescriptiveName { get; }
public GridViewModel() public GridViewModel()
{ {
} }
public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree) 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<long, BlockViewModel>()); Blocks.Add(_fillerDefinition, new MtObservableSortedDictionary<long, BlockViewModel>());
} }

View File

@@ -12,11 +12,21 @@ using VRage.ModAPI;
using System.Windows.Threading; using System.Windows.Threading;
using NLog; using NLog;
using Torch.Collections; using Torch.Collections;
using Torch.Server.Views.Entities;
namespace Torch.Server.ViewModels namespace Torch.Server.ViewModels
{ {
public class EntityTreeViewModel : ViewModel public class EntityTreeViewModel : ViewModel
{ {
public enum SortEnum
{
Name,
Size,
Speed,
Owner,
BlockCount,
DistFromCenter,
}
private static readonly Logger _log = LogManager.GetCurrentClassLogger(); private static readonly Logger _log = LogManager.GetCurrentClassLogger();
//TODO: these should be sorted sets for speed //TODO: these should be sorted sets for speed
@@ -26,7 +36,13 @@ namespace Torch.Server.ViewModels
public MtObservableSortedDictionary<long, VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableSortedDictionary<long, VoxelMapViewModel>(); public MtObservableSortedDictionary<long, VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableSortedDictionary<long, VoxelMapViewModel>();
public Dispatcher ControlDispatcher => _control.Dispatcher; public Dispatcher ControlDispatcher => _control.Dispatcher;
public SortedView<GridViewModel> SortedGrids { get; }
public SortedView<CharacterViewModel> SortedCharacters { get; }
public SortedView<EntityViewModel> SortedFloatingObjects { get; }
public SortedView<VoxelMapViewModel> SortedVoxelMaps { get; }
private EntityViewModel _currentEntity; private EntityViewModel _currentEntity;
private SortEnum _currentSort;
private UserControl _control; private UserControl _control;
public EntityViewModel CurrentEntity public EntityViewModel CurrentEntity
@@ -35,6 +51,12 @@ namespace Torch.Server.ViewModels
set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); } set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); }
} }
public SortEnum CurrentSort
{
get => _currentSort;
set => SetValue(ref _currentSort, value);
}
// I hate you today WPF // I hate you today WPF
public EntityTreeViewModel() : this(null) public EntityTreeViewModel() : this(null)
{ {
@@ -43,6 +65,11 @@ namespace Torch.Server.ViewModels
public EntityTreeViewModel(UserControl control) public EntityTreeViewModel(UserControl control)
{ {
_control = control; _control = control;
var comparer = new EntityViewModel.Comparer(_currentSort);
SortedGrids = new SortedView<GridViewModel>(Grids.Values, comparer);
SortedCharacters = new SortedView<CharacterViewModel>(Characters.Values, comparer);
SortedFloatingObjects = new SortedView<EntityViewModel>(FloatingObjects.Values, comparer);
SortedVoxelMaps = new SortedView<VoxelMapViewModel>(VoxelMaps.Values, comparer);
} }
public void Init() public void Init()
@@ -85,16 +112,16 @@ namespace Torch.Server.ViewModels
switch (obj) switch (obj)
{ {
case MyCubeGrid grid: case MyCubeGrid grid:
Grids.Add(obj.EntityId, new GridViewModel(grid, this)); Grids.Add(grid.EntityId, new GridViewModel(grid, this));
break; break;
case MyCharacter character: case MyCharacter character:
Characters.Add(obj.EntityId, new CharacterViewModel(character, this)); Characters.Add(character.EntityId, new CharacterViewModel(character, this));
break; break;
case MyFloatingObject floating: case MyFloatingObject floating:
FloatingObjects.Add(obj.EntityId, new FloatingObjectViewModel(floating, this)); FloatingObjects.Add(floating.EntityId, new FloatingObjectViewModel(floating, this));
break; break;
case MyVoxelBase voxel: case MyVoxelBase voxel:
VoxelMaps.Add(obj.EntityId, new VoxelMapViewModel(voxel, this)); VoxelMaps.Add(voxel.EntityId, new VoxelMapViewModel(voxel, this));
break; break;
} }
} }

View File

@@ -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
{
/// <summary>
/// Wrapper around VRage.Game.Objectbuilder_Checkpoint.ModItem
/// that holds additional meta information
/// (e.g. workshop description)
/// </summary>
public class ModItemInfo : ViewModel
{
MyObjectBuilder_Checkpoint.ModItem _modItem;
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Human friendly name of the mod
/// </summary>
public string FriendlyName
{
get { return _modItem.FriendlyName; }
set {
SetValue(ref _modItem.FriendlyName, value);
}
}
/// <summary>
/// Workshop ID of the mod
/// </summary>
public ulong PublishedFileId
{
get { return _modItem.PublishedFileId; }
set
{
SetValue(ref _modItem.PublishedFileId, value);
}
}
/// <summary>
/// Local filename of the mod
/// </summary>
public string Name
{
get { return _modItem.Name; }
set
{
SetValue(ref _modItem.FriendlyName, value);
}
}
/// <summary>
/// Whether or not the mod was added
/// because another mod depends on it
/// </summary>
public bool IsDependency
{
get { return _modItem.IsDependency; }
set
{
SetValue(ref _modItem.IsDependency, value);
}
}
private string _description;
/// <summary>
/// Workshop description of the mod
/// </summary>
public string Description
{
get { return _description; }
set
{
SetValue(ref _description, value);
}
}
/// <summary>
/// Constructor, returns a new ModItemInfo instance
/// </summary>
/// <param name="mod">The wrapped mod</param>
public ModItemInfo(MyObjectBuilder_Checkpoint.ModItem mod)
{
_modItem = mod;
}
/// <summary>
/// Retrieve information about the
/// wrapped mod from the workhop asynchronously
/// via the Steam web API.
/// </summary>
/// <returns></returns>
public async Task<bool> 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;
}
}
}
}

View File

@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using NLog;
using Torch.API; using Torch.API;
using Torch.API.Plugins; using Torch.API.Plugins;
using Torch.Server.Views; using Torch.Server.Views;
@@ -17,12 +19,24 @@ namespace Torch.Server.ViewModels
public string Name { get; } public string Name { get; }
public ITorchPlugin Plugin { get; } public ITorchPlugin Plugin { get; }
private static Logger _log = LogManager.GetCurrentClassLogger();
public PluginViewModel(ITorchPlugin plugin) public PluginViewModel(ITorchPlugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
if (Plugin is IWpfPlugin p) 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})"; Name = $"{plugin.Name} ({plugin.Version})";
@@ -38,5 +52,55 @@ namespace Torch.Server.ViewModels
this.Control.Resources.MergedDictionaries.Clear(); this.Control.Resources.MergedDictionaries.Clear();
this.Control.Resources.MergedDictionaries.Add(dictionary); 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();
}
}
}
} }
} }

View File

@@ -21,8 +21,11 @@ namespace Torch.Server.ViewModels
[Torch.Views.Display(Description = "The type of the game online mode.", Name = "Online Mode", GroupName = "Others")] [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); } 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")] [Torch.Views.Display(Description = "The multiplier for character inventory size.", Name = "Character Inventory Size", GroupName = "Multipliers")]
public float InventorySizeMultiplier { get => _settings.InventorySizeMultiplier; set => SetValue(ref _settings.InventorySizeMultiplier, value); } 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")] [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); } public float AssemblerSpeedMultiplier { get => _settings.AssemblerSpeedMultiplier; set => SetValue(ref _settings.AssemblerSpeedMultiplier, value); }
@@ -120,7 +123,13 @@ namespace Torch.Server.ViewModels
public bool EnableSaving { get => _settings.EnableSaving; set => SetValue(ref _settings.EnableSaving, value); } 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")] [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")] [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); } 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")] [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); } 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) public SessionSettingsViewModel(MyObjectBuilder_SessionSettings settings)
{ {
_settings = settings; _settings = settings;

View File

@@ -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<MyObjectBuilder_Checkpoint.ModItem> Mods { get => _worldConfiguration.Mods; set => SetValue(ref _worldConfiguration.Mods, value); }
public SessionSettingsViewModel Settings
{
get => _sessionSettings;
set
{
SetValue(ref _sessionSettings, value);
_worldConfiguration.Settings = _sessionSettings;
}
}
}
}

View File

@@ -17,8 +17,8 @@
<ColumnDefinition/> <ColumnDefinition/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="1" x:Name="Send" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5,5,5,5" Click="SendButton_Click"></Button> <Button Grid.Column="1" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5" Click="SendButton_Click"></Button>
<TextBox Grid.Column="0" x:Name="Message" DockPanel.Dock="Left" Margin="5,5,5,5" KeyDown="Message_OnKeyDown"></TextBox> <TextBox Grid.Column="0" x:Name="Message" Margin="5" KeyDown="Message_OnKeyDown"></TextBox>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -20,6 +20,8 @@ using NLog;
using Torch; using Torch;
using Sandbox; using Sandbox;
using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Gui;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World; using Sandbox.Game.World;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
@@ -131,6 +133,15 @@ namespace Torch.Server
bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight; bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight;
var span = new Span(); var span = new Span();
span.Inlines.Add($"{msg.Timestamp} "); 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(new Run(msg.Author) { Foreground = LookupBrush(msg.Font) });
span.Inlines.Add($": {msg.Message}"); span.Inlines.Add($": {msg.Message}");
span.Inlines.Add(new LineBreak()); span.Inlines.Add(new LineBreak());
@@ -163,10 +174,18 @@ namespace Torch.Server
var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>(); var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
if (commands != null && commands.IsCommand(text)) 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(() => _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 else

View File

@@ -27,6 +27,7 @@
<DockPanel Grid.Row="0"> <DockPanel Grid.Row="0">
<Label Content="World:" DockPanel.Dock="Left" /> <Label Content="World:" DockPanel.Dock="Left" />
<Button Content="Import World Config" Margin="3" Padding="3" DockPanel.Dock="Right" Click="ImportConfig_OnClick" ToolTip="Override the DS config with the one from the selected world." IsEnabled="{Binding ElementName=WorldList, Path=Items.Count, Mode=OneWay}"/> <Button Content="Import World Config" Margin="3" Padding="3" DockPanel.Dock="Right" Click="ImportConfig_OnClick" ToolTip="Override the DS config with the one from the selected world." IsEnabled="{Binding ElementName=WorldList, Path=Items.Count, Mode=OneWay}"/>
<Button Content="New World" Margin="3" Padding="3" DockPanel.Dock="Right" Click="NewWorld_OnClick"/>
<ComboBox x:Name="WorldList" ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3" <ComboBox x:Name="WorldList" ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3"
SelectionChanged="Selector_OnSelectionChanged"> SelectionChanged="Selector_OnSelectionChanged">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
@@ -57,70 +58,77 @@
<RowDefinition /> <RowDefinition />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid> <ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid.ColumnDefinitions> <Grid>
<ColumnDefinition Width="Auto" /> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> <ColumnDefinition />
<StackPanel Grid.Column="0" Margin="3" DockPanel.Dock="Left"> </Grid.ColumnDefinitions>
<Label Content="Server Name" /> <StackPanel Grid.Column="0" Margin="3" DockPanel.Dock="Left">
<TextBox Text="{Binding ServerName}" Margin="3,0,3,3" Width="160" /> <Label Content="Server Name" />
<Label Content="World Name" /> <TextBox Text="{Binding ServerName}" Margin="3,0,3,3" Width="160" />
<TextBox Text="{Binding WorldName}" Margin="3,0,3,3" Width="160" /> <Label Content="World Name" />
<Label Content="Whitelist Group ID" /> <TextBox Text="{Binding WorldName}" Margin="3,0,3,3" Width="160" />
<TextBox Margin="3,0,3,3" Width="160" Style="{StaticResource ValidatedTextBox}"> <Label Content="Server Description" />
<TextBox.Text> <TextBox Text="{Binding ServerDescription}" Margin="3,0,3,3" Width="160" Height="100"
<Binding Path="GroupId" UpdateSourceTrigger="PropertyChanged" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"/>
ValidatesOnDataErrors="True" NotifyOnValidationError="True"> <Label Content="Whitelist Group ID" />
<Binding.ValidationRules> <TextBox Margin="3,0,3,3" Width="160" Style="{StaticResource ValidatedTextBox}">
<validationRules:NumberValidationRule /> <TextBox.Text>
</Binding.ValidationRules> <Binding Path="GroupId" UpdateSourceTrigger="PropertyChanged"
</Binding> ValidatesOnDataErrors="True" NotifyOnValidationError="True">
</TextBox.Text> <Binding.ValidationRules>
</TextBox> <validationRules:NumberValidationRule />
<Label Content="Server IP" /> </Binding.ValidationRules>
<StackPanel Orientation="Horizontal" Margin="3,0,3,3"> </Binding>
<TextBox Text="{Binding IP}" Width="100" Height="20" /> </TextBox.Text>
<Label Content=":" Width="12" /> </TextBox>
<TextBox Text="{Binding Port}" Width="48" Height="20" /> <Label Content="Server IP" />
<StackPanel Orientation="Horizontal" Margin="3,0,3,3">
<TextBox Text="{Binding IP}" Width="100" Height="20" />
<Label Content=":" Width="12" />
<TextBox Text="{Binding Port}" Width="48" Height="20" />
</StackPanel>
<Label Content="Server Password"/>
<TextBox Text="{Binding Password}" Width="160"/>
<CheckBox IsChecked="{Binding PauseGameWhenEmpty}" Content="Pause When Empty" Margin="3" />
<CheckBox IsChecked="{Binding AutodetectDependencies}" Content="Auto Detect Dependencies" Margin="3" />
</StackPanel> </StackPanel>
<Label Content="Server Password"/> <StackPanel Grid.Column="1" Margin="3">
<TextBox Text="{Binding Password}" Width="160"/> <Label Content="Administrators" />
<CheckBox IsChecked="{Binding PauseGameWhenEmpty}" Content="Pause When Empty" Margin="3" /> <TextBox Text="{Binding Administrators, Converter={StaticResource ListConverterString}}"
</StackPanel> Margin="3"
<StackPanel Grid.Column="1" Margin="3"> Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" />
<Label Content="Mods" /> <Button Content="Edit Roles" Click="RoleEdit_Onlick" Margin="3"/>
<TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" <Label Content="Reserved Players" />
Style="{StaticResource ValidatedTextBox}"> <TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"
<TextBox.Text> Style="{StaticResource ValidatedTextBox}">
<Binding Path="Mods" UpdateSourceTrigger="PropertyChanged" <TextBox.Text>
ValidatesOnDataErrors="True" NotifyOnValidationError="True" <Binding Path="Reserved" UpdateSourceTrigger="PropertyChanged"
Converter="{StaticResource ListConverterUInt64}"> ValidatesOnDataErrors="True" NotifyOnValidationError="True"
<Binding.ValidationRules> Converter="{StaticResource ListConverterUInt64}">
<validationRules:ListConverterValidationRule Type="system:UInt64" /> <Binding.ValidationRules>
</Binding.ValidationRules> <validationRules:ListConverterValidationRule Type="system:UInt64" />
</Binding> </Binding.ValidationRules>
</TextBox.Text> </Binding>
</TextBox> </TextBox.Text>
<Label Content="Administrators" /> </TextBox>
<TextBox Text="{Binding Administrators, Converter={StaticResource ListConverterString}}" <Label Content="Banned Players" />
Margin="3" <TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"
Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" /> Style="{StaticResource ValidatedTextBox}">
<Label Content="Banned Players" /> <TextBox.Text>
<TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" <Binding Path="Banned" UpdateSourceTrigger="PropertyChanged"
Style="{StaticResource ValidatedTextBox}"> ValidatesOnDataErrors="True" NotifyOnValidationError="True"
<TextBox.Text> Converter="{StaticResource ListConverterUInt64}">
<Binding Path="Banned" UpdateSourceTrigger="PropertyChanged" <Binding.ValidationRules>
ValidatesOnDataErrors="True" NotifyOnValidationError="True" <validationRules:ListConverterValidationRule Type="system:UInt64" />
Converter="{StaticResource ListConverterUInt64}"> </Binding.ValidationRules>
<Binding.ValidationRules> </Binding>
<validationRules:ListConverterValidationRule Type="system:UInt64" /> </TextBox.Text>
</Binding.ValidationRules> </TextBox>
</Binding> </StackPanel>
</TextBox.Text> </Grid>
</TextBox> </ScrollViewer>
</StackPanel>
</Grid>
<Button Grid.Row="1" Content="Save Config" Margin="3" Click="Save_OnClick" /> <Button Grid.Row="1" Content="Save Config" Margin="3" Click="Save_OnClick" />
</Grid> </Grid>
<views:PropertyGrid Grid.Column="1" Margin="3" DataContext="{Binding SessionSettings}" IgnoreDisplay ="True" /> <views:PropertyGrid Grid.Column="1" Margin="3" DataContext="{Binding SessionSettings}" IgnoreDisplay ="True" />

View File

@@ -12,6 +12,8 @@ using Torch.API.Managers;
using Torch.Server.Annotations; using Torch.Server.Annotations;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Server.ViewModels; using Torch.Server.ViewModels;
using Torch.Views;
using VRage.Game.ModAPI;
namespace Torch.Server.Views namespace Torch.Server.Views
{ {
@@ -116,5 +118,28 @@ namespace Torch.Server.Views
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 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();
}
} }
} }

View File

@@ -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; }
/// <summary>
/// Converts a list of ModItemInfo objects into a list of their workshop IDs (PublishedFileIds).
/// </summary>
/// <param name="valueList">
/// Expected to contain a list of ModItemInfo objects
/// </param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">This parameter will be ignored</param>
/// <param name="culture"> This parameter will be ignored</param>
/// <returns>A string containing the workshop ids of all mods, one per line</returns>
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();
}
/// <summary>
/// Converts a list of workshop ids into a list of ModItemInfo objects
/// </summary>
/// <param name="value">A string containing workshop ids separated by new lines</param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">
/// A list of ModItemInfos which should
/// contain the requestted mods
/// (or they will be dropped)
/// </param>
/// <param name="culture">This parameter will be ignored</param>
/// <returns>A list of ModItemInfo objects</returns>
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<ModItemInfo>;
if (mods == null)
throw new ArgumentException("parameter needs to be of type ICollection<ModItemInfo>!");
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// A converter to get the index of a ModItemInfo object within a collection of ModItemInfo objects
/// </summary>
public class ModToListIdConverter : IMultiValueConverter
{
/// <summary>
/// Converts a ModItemInfo object into its index within a Collection of ModItemInfo objects
/// </summary>
/// <param name="values">
/// Expected to contain a ModItemInfo object at index 0
/// and a Collection of ModItemInfo objects at index 1
/// </param>
/// <param name="targetType">This parameter will be ignored</param>
/// <param name="parameter">This parameter will be ignored</param>
/// <param name="culture"> This parameter will be ignored</param>
/// <returns>the index of the mod within the provided mod list.</returns>
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<ModItemInfo>) values[1];
return theModList.IndexOf(mod);
}
/// <summary>
/// It is not supported to reverse this converter
/// </summary>
/// <param name="values"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns>Raises a NotSupportedException</returns>
public object[] ConvertBack(object values, Type[] targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException("ModToIdConverter can not convert back!");
}
}
}

View File

@@ -22,10 +22,11 @@
<Grid Grid.Column="0"> <Grid Grid.Column="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TreeView Grid.Row="0" Margin="3" DockPanel.Dock="Top" SelectedItemChanged="TreeView_OnSelectedItemChanged" <TreeView Grid.Row="0" Margin="3" DockPanel.Dock="Top" SelectedItemChanged="TreeView_OnSelectedItemChanged"
TreeViewItem.Expanded="TreeViewItem_OnExpanded"> TreeViewItem.Expanded="TreeViewItem_OnExpanded" Name="EntityTree">
<TreeView.Resources> <TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type entities:GridViewModel}" <HierarchicalDataTemplate DataType="{x:Type entities:GridViewModel}"
ItemsSource="{Binding Path=Blocks}"> ItemsSource="{Binding Path=Blocks}">
@@ -46,46 +47,47 @@
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type entities:VoxelMapViewModel}" <HierarchicalDataTemplate DataType="{x:Type entities:VoxelMapViewModel}"
ItemsSource="{Binding AttachedGrids}"> ItemsSource="{Binding AttachedGrids}">
<TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding DescriptiveName}" />
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
</TreeView.Resources> </TreeView.Resources>
<TreeViewItem ItemsSource="{Binding Path=Grids.Values}"> <TreeViewItem ItemsSource="{Binding Path=SortedGrids}">
<TreeViewItem.Header> <TreeViewItem.Header>
<TextBlock Text="{Binding Grids.Count, StringFormat=Grids ({0})}" /> <TextBlock Text="{Binding Grids.Count, StringFormat=Grids ({0})}" />
</TreeViewItem.Header> </TreeViewItem.Header>
</TreeViewItem> </TreeViewItem>
<TreeViewItem ItemsSource="{Binding Characters.Values}"> <TreeViewItem ItemsSource="{Binding SortedCharacters}">
<TreeViewItem.Header> <TreeViewItem.Header>
<TextBlock Text="{Binding Characters.Count, StringFormat=Characters ({0})}" /> <TextBlock Text="{Binding Characters.Count, StringFormat=Characters ({0})}" />
</TreeViewItem.Header> </TreeViewItem.Header>
<TreeViewItem.ItemTemplate> <TreeViewItem.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding DescriptiveName}" />
</DataTemplate> </DataTemplate>
</TreeViewItem.ItemTemplate> </TreeViewItem.ItemTemplate>
</TreeViewItem> </TreeViewItem>
<TreeViewItem ItemsSource="{Binding VoxelMaps.Values}"> <TreeViewItem ItemsSource="{Binding SortedVoxelMaps}">
<TreeViewItem.Header> <TreeViewItem.Header>
<TextBlock Text="{Binding VoxelMaps.Count, StringFormat=Voxel Maps ({0})}" /> <TextBlock Text="{Binding VoxelMaps.Count, StringFormat=Voxel Maps ({0})}" />
</TreeViewItem.Header> </TreeViewItem.Header>
<TreeViewItem.ItemTemplate> <TreeViewItem.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding DescriptiveName}" />
</DataTemplate> </DataTemplate>
</TreeViewItem.ItemTemplate> </TreeViewItem.ItemTemplate>
</TreeViewItem> </TreeViewItem>
<TreeViewItem ItemsSource="{Binding FloatingObjects.Values}"> <TreeViewItem ItemsSource="{Binding SortedFloatingObjects}">
<TreeViewItem.Header> <TreeViewItem.Header>
<TextBlock Text="{Binding FloatingObjects.Count, StringFormat=Floating Objects ({0})}" /> <TextBlock Text="{Binding FloatingObjects.Count, StringFormat=Floating Objects ({0})}" />
</TreeViewItem.Header> </TreeViewItem.Header>
<TreeViewItem.ItemTemplate> <TreeViewItem.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding DescriptiveName}" />
</DataTemplate> </DataTemplate>
</TreeViewItem.ItemTemplate> </TreeViewItem.ItemTemplate>
</TreeViewItem> </TreeViewItem>
</TreeView> </TreeView>
<StackPanel Grid.Row="1" DockPanel.Dock="Bottom"> <ComboBox Grid.Row="1" Margin="3" Name="SortCombo" SelectionChanged="SortCombo_SelectionChanged"/>
<StackPanel Grid.Row="2" DockPanel.Dock="Bottom">
<Button Content="Delete" Click="Delete_OnClick" IsEnabled="{Binding CurrentEntity.CanDelete}" <Button Content="Delete" Click="Delete_OnClick" IsEnabled="{Binding CurrentEntity.CanDelete}"
Margin="3" /> Margin="3" />
<Button Content="Stop" Click="Stop_OnClick" IsEnabled="{Binding CurrentEntity.CanStop}" Margin="3" /> <Button Content="Stop" Click="Stop_OnClick" IsEnabled="{Binding CurrentEntity.CanStop}" Margin="3" />

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -13,6 +14,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using NLog; using NLog;
using Torch.Collections;
using Torch.Server.ViewModels; using Torch.Server.ViewModels;
using Torch.Server.ViewModels.Blocks; using Torch.Server.ViewModels.Blocks;
using Torch.Server.ViewModels.Entities; using Torch.Server.ViewModels.Entities;
@@ -29,12 +31,15 @@ namespace Torch.Server.Views
{ {
public EntityTreeViewModel Entities { get; set; } public EntityTreeViewModel Entities { get; set; }
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
public EntitiesControl() public EntitiesControl()
{ {
InitializeComponent(); InitializeComponent();
Entities = new EntityTreeViewModel(this); Entities = new EntityTreeViewModel(this);
DataContext = Entities; DataContext = Entities;
Entities.Init(); Entities.Init();
SortCombo.ItemsSource = Enum.GetNames(typeof(EntityTreeViewModel.SortEnum));
} }
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -77,5 +82,30 @@ namespace Torch.Server.Views
if (item.DataContext is ILazyLoad l) if (item.DataContext is ILazyLoad l)
l.Load(); l.Load();
} }
private void SortCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var sort = (EntityTreeViewModel.SortEnum)SortCombo.SelectedIndex;
var comparer = new EntityViewModel.Comparer(sort);
Task[] sortTasks = new Task[4];
Entities.CurrentSort = sort;
Entities.SortedCharacters.SetComparer(comparer);
Entities.SortedFloatingObjects.SetComparer(comparer);
Entities.SortedGrids.SetComparer(comparer);
Entities.SortedVoxelMaps.SetComparer(comparer);
foreach (var i in Entities.SortedCharacters)
i.DescriptiveName = i.GetSortedName(sort);
foreach (var i in Entities.SortedFloatingObjects)
i.DescriptiveName = i.GetSortedName(sort);
foreach (var i in Entities.SortedGrids)
i.DescriptiveName = i.GetSortedName(sort);
foreach (var i in Entities.SortedVoxelMaps)
i.DescriptiveName = i.GetSortedName(sort);
}
} }
} }

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Torch.Server.Views
{
public static class Extensions
{
public static readonly DependencyProperty ScrollContainerProperty = DependencyProperty.RegisterAttached("ScrollContainer", typeof(bool), typeof(Extensions), new PropertyMetadata(true));
public static bool GetScrollContainer(this UIElement ui)
{
return (bool)ui.GetValue(ScrollContainerProperty);
}
public static void SetScrollContainer(this UIElement ui, bool value)
{
ui.SetValue(ScrollContainerProperty, value);
}
}
}

View File

@@ -0,0 +1,127 @@
<UserControl x:Class="Torch.Server.Views.ModListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Torch.Server.ViewModels"
xmlns:s="clr-namespace:System"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
MouseMove="UserControl_MouseMove">
<!--<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>-->
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="Grid" x:Key="RootGridStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding Mode=OneWay, UpdateSourceTrigger=PropertyChanged, BindingGroupName=RootEnabledBinding}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.DataContext>
<viewModels:ConfigDedicatedViewModel />
</UserControl.DataContext>
<Grid Style="{StaticResource RootGridStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500px"/>
<ColumnDefinition Width="10px"/>
<ColumnDefinition Width="*" MinWidth="200px"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="80px"/>
</Grid.RowDefinitions>
<DataGrid Name="ModList" Grid.Column="0" Grid.ColumnSpan="1" ItemsSource="{Binding UpdateSourceTrigger=PropertyChanged}"
Sorting="ModList_Sorting"
SelectionMode="Single"
SelectionUnit="FullRow"
AllowDrop="True"
CanUserReorderColumns="False"
CanUserSortColumns="True"
PreviewMouseLeftButtonDown="ModList_MouseLeftButtonDown"
MouseLeftButtonUp="ModList_MouseLeftButtonUp"
SelectedCellsChanged="ModList_Selected"
AutoGenerateColumns="False">
<!--:DesignSource="{d:DesignInstance Type={x:Type MyObjectBuilder_Checkpoint:ModItem, CreateList=True}}">-->
<DataGrid.Columns>
<DataGridTextColumn Header="Load Order"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource ModToListIdConverter}" StringFormat="{}{0}">
<Binding />
<Binding ElementName="ModList" Path="DataContext"></Binding>
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn Header="Workshop Id"
IsReadOnly="True"
Binding="{Binding PublishedFileId, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}">
</DataGridTextColumn>
<DataGridTextColumn Header="Name"
Width="*"
IsReadOnly="True"
Binding="{Binding FriendlyName, NotifyOnTargetUpdated=True, UpdateSourceTrigger=PropertyChanged}">
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDependency}" Value="True">
<Setter Property="Foreground" Value="#222222"/>
<Setter Property="Background" Value="#FFCCAA"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsDependency}" Value="True"/>
<Condition Binding="{Binding ElementName=ShowDependencyModsCheckBox, Path=IsChecked}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Height" Value="0px"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.ItemContainerStyle>
</DataGrid>
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Background="#1b2838">
<TextBlock Name="ModDescription" TextWrapping="Wrap" Foreground="White" Padding="2px"
Text="{Binding ElementName=ModList, Path=SelectedItem.Description}">
</TextBlock>
</ScrollViewer>
<Grid Grid.Row="2" Margin="0 0 0 6px">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition MinHeight="40px"/>
</Grid.RowDefinitions>
<CheckBox Name="ShowDependencyModsCheckBox" VerticalAlignment="Center"
HorizontalAlignment="Left" Margin="6px 0" Grid.Column="0" Grid.Row="0"/>
<Label Content="Show Dependency Mods" Padding="0" Margin="6px 0" Grid.Column="1" VerticalAlignment="Center"/>
<Label Content="ID/URL:" Padding="0" Margin="6px 0" HorizontalAlignment="Left"
VerticalAlignment="Center" Grid.Column="0" Grid.Row="1"/>
<TextBox Name="AddModIDTextBox" Grid.Column="1" VerticalContentAlignment="Center"
HorizontalAlignment="Stretch" MinWidth="100px" Margin="6px 4px" Grid.Row="1"/>
<Button Content="Add" Grid.Column="2" Margin="6px 0" Width="60px" Height="40px" Click="AddBtn_OnClick" Grid.Row="1"/>
<Button Content="Remove" Grid.Column="3" Margin="6px 0" Width="60px" Height="40px" Click="RemoveBtn_OnClick" Grid.Row="1"
IsEnabled="{Binding ElementName=ModList, Path=SelectedItems.Count}"/>
<Button Content="Bulk Edit" Grid.Column="4" Margin="6px 0" Width="60px" Height="40px" Click="BulkButton_OnClick" Grid.Row="1"/>
</Grid>
<Button Content="Save Config" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="6px" Grid.Column="3" Width="80px" Height="40px" Click="SaveBtn_OnClick"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
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.Navigation;
using System.Windows.Shapes;
using System.Runtime.CompilerServices;
using System.Windows.Threading;
using VRage.Game;
using NLog;
using Torch.Server.Managers;
using Torch.API.Managers;
using Torch.Server.ViewModels;
using Torch.Server.Annotations;
using Torch.Collections;
using Torch.Views;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for ModListControl.xaml
/// </summary>
public partial class ModListControl : UserControl, INotifyPropertyChanged
{
private static Logger Log = LogManager.GetLogger(nameof(ModListControl));
private InstanceManager _instanceManager;
ModItemInfo _draggedMod;
bool _hasOrderChanged = false;
bool _isSortedByLoadOrder = true;
//private List<BindingExpression> _bindingExpressions = new List<BindingExpression>();
/// <summary>
/// Constructor for ModListControl
/// </summary>
public ModListControl()
{
InitializeComponent();
_instanceManager = TorchBase.Instance.Managers.GetManager<InstanceManager>();
_instanceManager.InstanceLoaded += _instanceManager_InstanceLoaded;
//var mods = _instanceManager.DedicatedConfig?.Mods;
//if( mods != null)
// DataContext = new ObservableCollection<MyObjectBuilder_Checkpoint.ModItem>();
DataContext = _instanceManager.DedicatedConfig?.Mods;
// Gets called once all children are loaded
//Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(ApplyStyles));
}
private void ModListControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
throw new NotImplementedException();
}
private void ResetSorting()
{
CollectionViewSource.GetDefaultView(ModList.ItemsSource).SortDescriptions.Clear();
}
private void _instanceManager_InstanceLoaded(ConfigDedicatedViewModel obj)
{
Log.Info("Instance loaded.");
Dispatcher.Invoke(() => {
DataContext = obj?.Mods ?? new MtObservableList<ModItemInfo>();
UpdateLayout();
((MtObservableList<ModItemInfo>)DataContext).CollectionChanged += OnModlistUpdate;
});
}
private void OnModlistUpdate(object sender, NotifyCollectionChangedEventArgs e)
{
ModList.Items.Refresh();
//if (e.Action == NotifyCollectionChangedAction.Remove)
// _instanceManager.SaveConfig();
}
private void SaveBtn_OnClick(object sender, RoutedEventArgs e)
{
_instanceManager.SaveConfig();
}
private void AddBtn_OnClick(object sender, RoutedEventArgs e)
{
if (TryExtractId(AddModIDTextBox.Text, out ulong id))
{
var mod = new ModItemInfo(new MyObjectBuilder_Checkpoint.ModItem(id));
//mod.PublishedFileId = id;
_instanceManager.DedicatedConfig.Mods.Add(mod);
Task.Run(mod.UpdateModInfoAsync)
.ContinueWith((t) =>
{
Dispatcher.Invoke(() =>
{
_instanceManager.DedicatedConfig.Save();
});
});
AddModIDTextBox.Text = "";
}
else
{
AddModIDTextBox.BorderBrush = Brushes.Red;
Log.Warn("Invalid mod id!");
MessageBox.Show("Invalid mod id!");
}
}
private void RemoveBtn_OnClick(object sender, RoutedEventArgs e)
{
var modList = ((MtObservableList<ModItemInfo>)DataContext);
if (ModList.SelectedItem is ModItemInfo mod && modList.Contains(mod))
modList.Remove(mod);
}
private bool TryExtractId(string input, out ulong result)
{
var match = Regex.Match(input, @"(?<=id=)\d+").Value;
bool success;
if (string.IsNullOrEmpty(match))
success = ulong.TryParse(input, out result);
else
success = ulong.TryParse(match, out result);
return success;
}
private void ModList_Sorting(object sender, DataGridSortingEventArgs e)
{
Log.Info($"Sorting by '{e.Column.Header}'");
if (e.Column == ModList.Columns[0])
{
var dataView = CollectionViewSource.GetDefaultView(ModList.ItemsSource);
dataView.SortDescriptions.Clear();
dataView.Refresh();
_isSortedByLoadOrder = true;
}
else
_isSortedByLoadOrder = false;
}
private void ModList_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//return;
_draggedMod = (ModItemInfo) TryFindRowAtPoint((UIElement) sender, e.GetPosition(ModList))?.DataContext;
//DraggedMod = (ModItemInfo) ModList.SelectedItem;
}
private static DataGridRow TryFindRowAtPoint(UIElement reference, Point point)
{
var element = reference.InputHitTest(point) as DependencyObject;
if (element == null)
return null;
if (element is DataGridRow row)
return row;
else
return TryFindParent<DataGridRow>(element);
}
private static T TryFindParent<T>(DependencyObject child) where T : DependencyObject
{
DependencyObject parent;
if (child == null)
return null;
if (child is ContentElement contentElement)
{
parent = ContentOperations.GetParent(contentElement);
if (parent == null && child is FrameworkContentElement fce)
parent = fce.Parent;
}
else
{
parent = VisualTreeHelper.GetParent(child);
}
if (parent is T result)
return result;
else
return TryFindParent<T>(parent);
}
private void UserControl_MouseMove(object sender, MouseEventArgs e)
{
if (_draggedMod == null)
return;
if (!_isSortedByLoadOrder)
return;
var targetMod = (ModItemInfo)TryFindRowAtPoint((UIElement)sender, e.GetPosition(ModList))?.DataContext;
if( targetMod != null && !ReferenceEquals(_draggedMod, targetMod))
{
_hasOrderChanged = true;
var modList = (MtObservableList<ModItemInfo>)DataContext;
//modList.Move(modList.IndexOf(_draggedMod), modList.IndexOf(targetMod));
modList.RemoveAt(modList.IndexOf(_draggedMod));
modList.Insert(modList.IndexOf(targetMod), _draggedMod);
ModList.Items.Refresh();
ModList.SelectedItem = _draggedMod;
}
}
private void ModList_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!_isSortedByLoadOrder)
{
var targetMod = (ModItemInfo)TryFindRowAtPoint((UIElement)sender, e.GetPosition(ModList))?.DataContext;
if (targetMod != null && !ReferenceEquals(_draggedMod, targetMod))
{
var msg = "Drag and drop is only available when sorted by load order!";
Log.Warn(msg);
MessageBox.Show(msg);
}
}
//if (DraggedMod != null && HasOrderChanged)
//Log.Info("Dragging over, saving...");
//_instanceManager.SaveConfig();
_draggedMod = null;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ModList_Selected(object sender, SelectedCellsChangedEventArgs e)
{
if (_draggedMod != null)
ModList.SelectedItem = _draggedMod;
else if( e.AddedCells.Count > 0)
ModList.SelectedItem = e.AddedCells[0].Item;
}
private void BulkButton_OnClick(object sender, RoutedEventArgs e)
{
var editor = new CollectionEditor();
//let's see just how poorly we can do this
var modList = ((MtObservableList<ModItemInfo>)DataContext).ToList();
var idList = modList.Select(m => m.PublishedFileId).ToList();
var tasks = new List<Task>();
//blocking
editor.Edit<ulong>(idList, "Mods");
modList.RemoveAll(m => !idList.Contains(m.PublishedFileId));
foreach (var id in idList)
{
if (!modList.Any(m => m.PublishedFileId == id))
{
var mod = new ModItemInfo(new MyObjectBuilder_Checkpoint.ModItem(id));
tasks.Add(Task.Run(mod.UpdateModInfoAsync));
modList.Add(mod);
}
}
_instanceManager.DedicatedConfig.Mods.Clear();
foreach (var mod in modList)
_instanceManager.DedicatedConfig.Mods.Add(mod);
if (tasks.Any())
Task.WaitAll(tasks.ToArray());
Dispatcher.Invoke(() =>
{
_instanceManager.DedicatedConfig.Save();
});
}
}
}

View File

@@ -80,6 +80,9 @@ namespace Torch.Server
private void KickButton_Click(object sender, RoutedEventArgs e) private void KickButton_Click(object sender, RoutedEventArgs e)
{ {
if (PlayerList.SelectedItem == null)
return;
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
try try
{ {
@@ -93,6 +96,9 @@ namespace Torch.Server
private void BanButton_Click(object sender, RoutedEventArgs e) private void BanButton_Click(object sender, RoutedEventArgs e)
{ {
if (PlayerList.SelectedItem == null)
return;
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
try try
{ {
@@ -106,6 +112,9 @@ namespace Torch.Server
private void PromoteButton_OnClick(object sender, RoutedEventArgs e) private void PromoteButton_OnClick(object sender, RoutedEventArgs e)
{ {
if (PlayerList.SelectedItem == null)
return;
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
try try
{ {
@@ -119,6 +128,9 @@ namespace Torch.Server
private void DemoteButton_OnClick(object sender, RoutedEventArgs e) private void DemoteButton_OnClick(object sender, RoutedEventArgs e)
{ {
if (PlayerList.SelectedItem == null)
return;
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
try try
{ {

View File

@@ -0,0 +1,124 @@
<Window x:Class="Torch.Server.Views.PluginBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Markdown.Xaml;assembly=Markdown.Xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:views="clr-namespace:Torch.Server.Views"
mc:Ignorable="d"
Title="PluginBrowser" Height="400" Width="600"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<Style TargetType="FlowDocument" x:Key="DocumentStyle">
<Setter Property="FontFamily"
Value="Calibri" />
<Setter Property="TextAlignment"
Value="Left" />
</Style>
<Style x:Key="H1Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="42" />
<Setter Property="Foreground"
Value="#ff000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H2Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="20" />
<Setter Property="Foreground"
Value="#ff000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H3Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="20" />
<Setter Property="Foreground"
Value="#99000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H4Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="14" />
<Setter Property="Foreground"
Value="#99000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="LinkStyle"
TargetType="Hyperlink">
<Setter Property="TextDecorations"
Value="None" />
</Style>
<Style x:Key="ImageStyle"
TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode"
Value="NearestNeighbor" />
<Style.Triggers>
<Trigger Property="Tag"
Value="imageright">
<Setter Property="Margin"
Value="20,0,0,0" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="SeparatorStyle"
TargetType="Line">
<Setter Property="X2"
Value="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=FlowDocumentScrollViewer}}" />
<Setter Property="Stroke"
Value="#99000000" />
<Setter Property="StrokeThickness"
Value="2" />
</Style>
<local:Markdown x:Key="Markdown"
DocumentStyle="{StaticResource DocumentStyle}"
Heading1Style="{StaticResource H1Style}"
Heading2Style="{StaticResource H2Style}"
Heading3Style="{StaticResource H3Style}"
Heading4Style="{StaticResource H4Style}"
LinkStyle="{StaticResource LinkStyle}"
ImageStyle="{StaticResource ImageStyle}"
SeparatorStyle="{StaticResource SeparatorStyle}"
AssetPathRoot="{x:Static system:Environment.CurrentDirectory}"/>
<local:TextToFlowDocumentConverter x:Key="TextToFlowDocumentConverter"
Markdown="{StaticResource Markdown}"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Name="PluginsList" Width="150" Height="Auto" Margin="3" ItemsSource="{Binding Plugins}" SelectionChanged="PluginsList_SelectionChanged">
</ListView>
<Button Name="DownloadButton" Grid.Row ="1" Content="Download" Margin="3" Height="30" Click="DownloadButton_OnClick" IsEnabled="False"/>
</Grid>
<FlowDocumentScrollViewer Grid.Column="1" Name="MarkdownFlow" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="3" Document="{Binding CurrentDescription, Converter={StaticResource TextToFlowDocumentConverter}}"/>
</Grid>
</Window>

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
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 Newtonsoft.Json;
using NLog;
using Torch.API.WebAPI;
using Torch.Collections;
using Torch.Server.Annotations;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for PluginBrowser.xaml
/// </summary>
public partial class PluginBrowser : Window, INotifyPropertyChanged
{
private static Logger Log = LogManager.GetCurrentClassLogger();
public MtObservableList<PluginItem> Plugins { get; set; } = new MtObservableList<PluginItem>();
public PluginItem CurrentItem { get; set; }
private string _description = "Loading data from server, please wait..";
public string CurrentDescription
{
get { return _description; }
set
{
_description = value;
OnPropertyChanged();
}
}
public PluginBrowser()
{
InitializeComponent();
Task.Run(async () =>
{
var res = await PluginQuery.Instance.QueryAll();
if (res == null)
return;
foreach (var item in res.Plugins)
Plugins.Add(item);
PluginsList.Dispatcher.Invoke(() => PluginsList.SelectedIndex = 0);
});
MarkdownFlow.CommandBindings.Add(new CommandBinding(NavigationCommands.GoToPage, (sender, e) => OpenUri((string)e.Parameter)));
}
public static bool IsValidUri(string uri)
{
if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute))
return false;
Uri tmp;
if (!Uri.TryCreate(uri, UriKind.Absolute, out tmp))
return false;
return tmp.Scheme == Uri.UriSchemeHttp || tmp.Scheme == Uri.UriSchemeHttps;
}
public static bool OpenUri(string uri)
{
if (!IsValidUri(uri))
return false;
System.Diagnostics.Process.Start(uri);
return true;
}
private void PluginsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CurrentItem = (PluginItem)PluginsList.SelectedItem;
CurrentDescription = CurrentItem.Description;
DownloadButton.IsEnabled = !string.IsNullOrEmpty(CurrentItem.LatestVersion);
}
private void DownloadButton_OnClick(object sender, RoutedEventArgs e)
{
var item = CurrentItem;
TorchBase.Instance.Config.Plugins.Add(new Guid(item.ID));
TorchBase.Instance.Config.Save();
Task.Run(async () =>
{
var result = await PluginQuery.Instance.DownloadPlugin(item.ID);
MessageBox.Show(result ? "Plugin downloaded successfully! Please restart the server to load changes."
: "Plugin failed to download! See log for details.",
"Plugin Downloader",
MessageBoxButton.OK);
});
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -22,11 +22,18 @@
<ListView Grid.Row="0" ItemsSource="{Binding Plugins}" SelectedItem="{Binding SelectedPlugin}" Margin="3"> <ListView Grid.Row="0" ItemsSource="{Binding Plugins}" SelectedItem="{Binding SelectedPlugin}" Margin="3">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}" Background="{Binding Color}" ToolTip="{Binding ToolTip}"/>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/> <Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="Open Folder" Margin="3" Click="OpenFolder_OnClick"/>
<Button Grid.Row="0" Grid.Column="1" Content="Browse Plugins" Margin="3" Click="BrowsPlugins_OnClick"/>
</Grid>
</Grid> </Grid>
<ScrollViewer Name="PScroll" Grid.Column="1" Margin="3"> <ScrollViewer Name="PScroll" Grid.Column="1" Margin="3">
<Frame NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/> <Frame NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>

View File

@@ -41,7 +41,9 @@ namespace Torch.Server.Views
{ {
if (propertyChangedEventArgs.PropertyName == nameof(PluginManagerViewModel.SelectedPlugin)) 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; PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
else else
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
@@ -71,5 +73,11 @@ namespace Torch.Server.Views
if (_plugins?.PluginDir != null) if (_plugins?.PluginDir != null)
Process.Start(_plugins.PluginDir); Process.Start(_plugins.PluginDir);
} }
private void BrowsPlugins_OnClick(object sender, RoutedEventArgs e)
{
var browser = new PluginBrowser();
browser.Show();
}
} }
} }

View File

@@ -18,5 +18,7 @@
</Style> </Style>
<converters:ListConverter x:Key="ListConverterString" Type="system:String"/> <converters:ListConverter x:Key="ListConverterString" Type="system:String"/>
<converters:ListConverter x:Key="ListConverterUInt64" Type="system:UInt64"/> <converters:ListConverter x:Key="ListConverterUInt64" Type="system:UInt64"/>
<converters:ModToListIdConverter x:Key="ModToListIdConverter"/>
<converters:ListConverterWorkshopId x:Key="ListConverterWorkshopId"/>
<converters:BooleanAndConverter x:Key="BooleanAndConverter"/> <converters:BooleanAndConverter x:Key="BooleanAndConverter"/>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,52 @@
<Window x:Class="Torch.Server.Views.RoleEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server.Views"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:modApi="clr-namespace:VRage.Game.ModAPI;assembly=VRage.Game"
mc:Ignorable="d"
Title="RoleEditor" Height="300" Width="300">
<Window.Resources>
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="GetEnumValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="modApi:MyPromoteLevel"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid x:Name="ItemGrid" AutoGenerateColumns="false" CanUserAddRows="true" Grid.Row="0">
<DataGrid.Columns>
<DataGridTextColumn Width="5*" Header="Key" Binding="{Binding Key}"/>
<DataGridComboBoxColumn Width ="5*" Header="Value" ItemsSource="{Binding Source={StaticResource GetEnumValues}}" SelectedValueBinding="{Binding Value, Mode=TwoWay}"/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Content="Add New" Margin="5" Click="AddNew_OnClick"></Button>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Cancel" Margin="5" Click="Cancel_OnClick" />
<Button Grid.Column="1" Content="OK" Margin="5" Click="Ok_OnClick" />
</Grid>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox Name="BulkSelect" ItemsSource="{Binding Source={StaticResource GetEnumValues}}" SelectedValue ="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RoleEditor}},
Path = BulkPromote, Mode=TwoWay}" Margin="5"/>
<Button Grid.Column="1" Content="Bulk edit" Margin ="5" Click="BulkEdit"/>
</Grid>
</Grid>
</Window>

View File

@@ -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
{
/// <summary>
/// Interaction logic for RoleEditor.xaml
/// </summary>
public partial class RoleEditor : Window
{
public RoleEditor()
{
InitializeComponent();
DataContext = Items;
}
public ObservableCollection<IDictionaryItem> Items { get; } = new ObservableCollection<IDictionaryItem>();
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<TKey, TValue> : 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<ulong> l = Items.Where(i => i.Value.Equals(BulkPromote)).Select(i => (ulong)i.Key).ToList();
var w = new CollectionEditor();
w.Edit((ICollection<ulong>)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<ulong, MyPromoteLevel>(m, BulkPromote));
}
}
}

View File

@@ -57,7 +57,7 @@
</Label> </Label>
<Label x:Name="LabelPlayers"> <Label x:Name="LabelPlayers">
<Label.Content> <Label.Content>
<TextBlock ></TextBlock> <TextBlock Text="{Binding OnlinePlayers, StringFormat=Players: {0}}"/>
</Label.Content> </Label.Content>
</Label> </Label>
</StackPanel> </StackPanel>
@@ -66,7 +66,7 @@
<RichTextBox x:Name="ConsoleText" VerticalScrollBarVisibility="Visible" FontFamily="Consolas" IsReadOnly="True"/> <RichTextBox x:Name="ConsoleText" VerticalScrollBarVisibility="Visible" FontFamily="Consolas" IsReadOnly="True"/>
</TabItem> </TabItem>
<TabItem Header="Configuration"> <TabItem Header="Configuration">
<Grid IsEnabled="{Binding CanRun}"> <Grid IsEnabled="{Binding Path=HasRun, Converter={StaticResource InverseBool}}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition/> <RowDefinition/>
@@ -80,6 +80,9 @@
<views:ConfigControl Grid.Row="1" x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" IsEnabled="{Binding CanRun}"/> <views:ConfigControl Grid.Row="1" x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" IsEnabled="{Binding CanRun}"/>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem Header="Mods">
<views:ModListControl/>
</TabItem>
<TabItem Header="Chat/Players"> <TabItem Header="Chat/Players">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>

View File

@@ -10,6 +10,7 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
@@ -31,8 +32,13 @@ namespace Torch.Server
private TorchServer _server; private TorchServer _server;
private TorchConfig _config; private TorchConfig _config;
private bool _autoscrollLog = true;
public TorchUI(TorchServer server) public TorchUI(TorchServer server)
{ {
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Width = 800;
Height = 600;
_config = (TorchConfig)server.Config; _config = (TorchConfig)server.Config;
_server = server; _server = server;
//TODO: data binding for whole server //TODO: data binding for whole server
@@ -41,10 +47,10 @@ namespace Torch.Server
AttachConsole(); AttachConsole();
Left = _config.WindowPosition.X; //Left = _config.WindowPosition.X;
Top = _config.WindowPosition.Y; //Top = _config.WindowPosition.Y;
Width = _config.WindowSize.X; //Width = _config.WindowSize.X;
Height = _config.WindowSize.Y; //Height = _config.WindowSize.Y;
Chat.BindServer(server); Chat.BindServer(server);
PlayerList.BindServer(server); PlayerList.BindServer(server);
@@ -54,6 +60,14 @@ namespace Torch.Server
Themes.uiSource = this; Themes.uiSource = this;
Themes.SetConfig(_config); Themes.SetConfig(_config);
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}"; Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
Loaded += TorchUI_Loaded;
}
private void TorchUI_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindDescendant<ScrollViewer>(ConsoleText);
scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
} }
private void AttachConsole() private void AttachConsole()
@@ -66,7 +80,52 @@ namespace Torch.Server
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document; doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document;
} }
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!"))); 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<T>(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<T>(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) public void LoadConfig(TorchConfig config)
@@ -97,10 +156,14 @@ namespace Torch.Server
protected override void OnClosing(CancelEventArgs e) protected override void OnClosing(CancelEventArgs e)
{ {
var newSize = new Point((int)Width, (int)Height); // Can't save here or you'll persist all the command line arguments
_config.WindowSize = newSize; //
var newPos = new Point((int)Left, (int)Top); //var newSize = new Point((int)Width, (int)Height);
_config.WindowPosition = newPos; //_config.WindowSize = newSize;
//var newPos = new Point((int)Left, (int)Top);
//_config.WindowPosition = newPos;
//_config.Save(); //you idiot
if (_server?.State == ServerState.Running) if (_server?.State == ServerState.Running)
_server.Stop(); _server.Stop();

View File

@@ -5,14 +5,15 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Torch.Server" xmlns:local="clr-namespace:Torch.Server"
xmlns:views="clr-namespace:Torch.Server.Views" xmlns:views="clr-namespace:Torch.Server.Views"
xmlns:views1="clr-namespace:Torch.Views;assembly=Torch"
mc:Ignorable="d" mc:Ignorable="d"
Title="WorldGeneratorDialog" Height="300" Width="700"> Title="WorldGeneratorDialog" Height="500" Width="700">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/> <ColumnDefinition Width="250"/>
<ColumnDefinition Width="440"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ListView Grid.Column="0" x:Name="PremadeCheckpoints" ScrollViewer.CanContentScroll="False" HorizontalContentAlignment="Center" Margin="3"> <ListView Grid.Column="0" x:Name="PremadeCheckpoints" ScrollViewer.CanContentScroll="False" HorizontalContentAlignment="Center" Margin="3" SelectionChanged="PremadeCheckpoints_SelectionChanged">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate DataType="local:PremadeCheckpointItem"> <DataTemplate DataType="local:PremadeCheckpointItem">
<StackPanel HorizontalAlignment="Center"> <StackPanel HorizontalAlignment="Center">
@@ -26,13 +27,18 @@
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
<StackPanel Grid.Column="1" Margin="3"> <Grid Grid.Column="1" Margin="3">
<Grid.RowDefinitions>
<RowDefinition Height ="Auto"/>
<RowDefinition Height ="*"/>
<RowDefinition Height ="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Label Content="World Name: "/> <Label Content="World Name: "/>
<TextBox x:Name="WorldName" Width="300" Margin="3"/> <TextBox x:Name="WorldName" Width="300" Margin="3"/>
</StackPanel> </StackPanel>
<views:SessionSettingsView/> <views1:PropertyGrid Grid.Row="1" x:Name="SettingsView" Margin="3"/>
<Button Content="Create World" Click="ButtonBase_OnClick"/> <Button Grid.Row="2" Content="Create World" Click="ButtonBase_OnClick" Margin ="3"/>
</StackPanel> </Grid>
</Grid> </Grid>
</Window> </Window>

View File

@@ -13,7 +13,15 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using NLog; using NLog;
using Sandbox.Definitions; using Sandbox.Definitions;
using Sandbox.Engine.Networking;
using Sandbox.Game.World;
using Torch.Server.Managers; 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.Game.Localization;
using VRage.Utils; using VRage.Utils;
@@ -26,19 +34,25 @@ namespace Torch.Server
{ {
private InstanceManager _instanceManager; private InstanceManager _instanceManager;
private List<PremadeCheckpointItem> _checkpoints = new List<PremadeCheckpointItem>(); private List<PremadeCheckpointItem> _checkpoints = new List<PremadeCheckpointItem>();
private PremadeCheckpointItem _currentItem;
[ReflectedStaticMethod(Type = typeof(ConfigForm), Name = "LoadLocalization")]
private static Action _loadLocalization;
public WorldGeneratorDialog(InstanceManager instanceManager) public WorldGeneratorDialog(InstanceManager instanceManager)
{ {
_instanceManager = instanceManager; _instanceManager = instanceManager;
InitializeComponent(); InitializeComponent();
_loadLocalization();
MyDefinitionManager.Static.LoadScenarios(); var scenarios = MyLocalCache.GetAvailableWorldInfos(Path.Combine(MyFileSystem.ContentPath, "CustomWorlds"));
var scenarios = MyDefinitionManager.Static.GetScenarioDefinitions(); foreach (var tup in scenarios)
MyDefinitionManager.Static.UnloadData();
foreach (var scenario in scenarios)
{ {
//TODO: Load localization string directory = tup.Item1;
_checkpoints.Add(new PremadeCheckpointItem { Name = scenario.DisplayNameText, Icon = @"C:\Users\jgross\Documents\Projects\TorchAPI\Torch\bin\x64\Release\Content\CustomWorlds\Empty World\thumb.jpg" }); 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});
} }
/* /*
@@ -59,20 +73,36 @@ namespace Torch.Server
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{ {
/* string worldName = string.IsNullOrEmpty(WorldName.Text) ? _currentItem.Name : WorldName.Text;
var worldPath = Path.Combine("Instance", "Saves", WorldName.Text);
var checkpointItem = (PremadeCheckpointItem)PremadeCheckpoints.SelectedItem; var worldPath = Path.Combine(TorchBase.Instance.Config.InstancePath, "Saves", worldName);
var checkpoint = _currentItem.Checkpoint;
if (Directory.Exists(worldPath)) if (Directory.Exists(worldPath))
{ {
MessageBox.Show("World already exists with that name."); MessageBox.Show("World already exists with that name.");
return; return;
} }
Directory.CreateDirectory(worldPath); 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 Path { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Icon { get; set; } public string Icon { get; set; }
public MyObjectBuilder_Checkpoint Checkpoint { get; set; }
} }
} }

View File

@@ -2,7 +2,14 @@
<packages> <packages>
<package id="ControlzEx" version="3.0.2.4" targetFramework="net461" /> <package id="ControlzEx" version="3.0.2.4" targetFramework="net461" />
<package id="MahApps.Metro" version="1.6.1" targetFramework="net461" /> <package id="MahApps.Metro" version="1.6.1" targetFramework="net461" />
<package id="Markdown.Xaml" version="1.0.0" targetFramework="net461" />
<package id="Microsoft.Win32.Registry" version="4.4.0" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" /> <package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" /> <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" /> <package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="protobuf-net" version="2.4.0" targetFramework="net461" />
<package id="SteamKit2" version="2.1.0" targetFramework="net461" />
<package id="System.ComponentModel.Annotations" version="4.5.0" targetFramework="net461" />
<package id="System.Security.AccessControl" version="4.4.0" targetFramework="net461" />
<package id="System.Security.Principal.Windows" version="4.4.0" targetFramework="net461" />
</packages> </packages>

View File

@@ -82,6 +82,7 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

11
Torch.Tests/app.config Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="protobuf-net" publicKeyToken="257b51d87d2e4d67" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.4.0.0" newVersion="2.4.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -14,6 +14,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7AD02A71-1D4C-48F9-A8C1-789A5512424F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7AD02A71-1D4C-48F9-A8C1-789A5512424F}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
NLog.config = NLog.config NLog.config = NLog.config
NLog-user.config = NLog-user.config
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Tests", "Torch.Tests\Torch.Tests.csproj", "{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}" 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 {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|Any CPU.ActiveCfg = Debug|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.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|Any CPU.ActiveCfg = Release|x64
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.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|Any CPU.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64 {CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64
@@ -79,10 +78,8 @@ Global
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64 {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|Any CPU.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.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|Any CPU.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -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<TKey, TValue> 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<TKey> _comparer;
public int Capacity { get; private set; }
public int Count { get; private set; }
public bool Full => Count == Capacity;
public BinaryMinHeap(int initialCapacity = 32, IComparer<TKey> comparer = null)
{
_store = new HeapItem[initialCapacity];
Count = 0;
Capacity = initialCapacity;
_comparer = comparer ?? Comparer<TKey>.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<TValue> comparer = null)
{
if (Count == 0)
return default(TValue);
if (comparer == null)
comparer = EqualityComparer<TValue>.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;
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
@@ -123,10 +124,10 @@ namespace Torch.Collections
private readonly Timer _flushEventQueue; private readonly Timer _flushEventQueue;
private const int _eventRaiseDelay = 50; private const int _eventRaiseDelay = 50;
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue = private readonly ConcurrentQueue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
new Queue<NotifyCollectionChangedEventArgs>(); new ConcurrentQueue<NotifyCollectionChangedEventArgs>();
private readonly Queue<string> _propertyEventQueue = new Queue<string>(); private readonly ConcurrentQueue<string> _propertyEventQueue = new ConcurrentQueue<string>();
private void FlushEventQueue(object data) private void FlushEventQueue(object data)
{ {
@@ -137,7 +138,8 @@ namespace Torch.Collections
// :/, but works better // :/, but works better
bool reset = _collectionEventQueue.Count > 0; bool reset = _collectionEventQueue.Count > 0;
if (reset) if (reset)
_collectionEventQueue.Clear(); while (_collectionEventQueue.Count > 0)
_collectionEventQueue.TryDequeue(out _);
else else
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e)) while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
_collectionChangedEvent.Raise(this, e); _collectionChangedEvent.Raise(this, e);

View File

@@ -13,7 +13,7 @@ namespace Torch.Collections
/// Multithread safe, observable list /// Multithread safe, observable list
/// </summary> /// </summary>
/// <typeparam name="T">Value type</typeparam> /// <typeparam name="T">Value type</typeparam>
public class MtObservableList<T> : MtObservableCollection<IList<T>, T>, IList<T>, IList public class MtObservableList<T> : MtObservableCollection<List<T>, T>, IList<T>, IList
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the MtObservableList class that is empty and has the default initial capacity. /// 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()) using (Lock.WriteUsing())
{ {
comparer = comparer ?? Comparer<TKey>.Default; comparer = comparer ?? Comparer<TKey>.Default;
if (Backing is List<T> lst) Backing.Sort(new TransformComparer<T, TKey>(selector, comparer));
lst.Sort(new TransformComparer<T, TKey>(selector, comparer));
else
{
List<T> sortedItems = Backing.OrderBy(selector, comparer).ToList();
Backing.Clear();
foreach (T v in sortedItems)
Backing.Add(v);
}
} }
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move));
}
/// <summary>
/// Sorts the list using the given comparer./>
/// </summary>
public void Sort(IComparer<T> comparer)
{
using (DeferredUpdate())
using (Lock.WriteUsing())
{
Backing.Sort(comparer);
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move));
}
/// <summary>
/// Searches the entire list for an element using the specified comparer and returns the zero-based index of the element.
/// </summary>
/// <param name="item"></param>
/// <param name="comparer"></param>
/// <returns></returns>
public int BinarySearch(T item, IComparer<T> comparer = null)
{
using(Lock.ReadUsing())
return Backing.BinarySearch(item, comparer ?? Comparer<T>.Default);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -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<T>: IReadOnlyCollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
private readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly MtObservableCollectionBase<T> _backing;
private IComparer<T> _comparer;
private readonly List<T> _store;
public SortedView(MtObservableCollectionBase<T> backing, IComparer<T> comparer)
{
_comparer = comparer;
_backing = backing;
_store = new List<T>(_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<T> 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<T> 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<T> 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<T> 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));
}
}
}

View File

@@ -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<T> : IReadOnlyCollection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
private readonly ObservableCollection<T> _backing;
private IComparer<T> _comparer;
private readonly List<T> _store;
public SystemSortedView(ObservableCollection<T> backing, IComparer<T> comparer)
{
_comparer = comparer;
_backing = backing;
_store = new List<T>(_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<T> 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<T> 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<T> 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<T> 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));
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using NLog;
namespace Torch namespace Torch
{ {
@@ -12,6 +13,7 @@ namespace Torch
{ {
private readonly string _argPrefix; private readonly string _argPrefix;
private readonly Dictionary<ArgAttribute, PropertyInfo> _args = new Dictionary<ArgAttribute, PropertyInfo>(); private readonly Dictionary<ArgAttribute, PropertyInfo> _args = new Dictionary<ArgAttribute, PropertyInfo>();
private readonly Logger _log = LogManager.GetCurrentClassLogger();
protected CommandLine(string argPrefix = "-") protected CommandLine(string argPrefix = "-")
{ {
@@ -89,6 +91,24 @@ namespace Torch
if (property.Value.PropertyType == typeof(string)) if (property.Value.PropertyType == typeof(string))
property.Value.SetValue(this, args[++i]); property.Value.SetValue(this, args[++i]);
if (property.Value.PropertyType == typeof(List<Guid>))
{
i++;
var l = new List<Guid>(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 catch

View File

@@ -55,10 +55,17 @@ namespace Torch.Commands
Args = args ?? new List<string>(); Args = args ?? new List<string>();
} }
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<IChatManagerServer>() //hack: Backwards compatibility 20190416
?.SendMessageAsOther(sender, message, font, _steamIdSender); if (sender == "Server")
{
sender = null;
font = null;
}
var chat = Torch.CurrentSession.Managers.GetManager<IChatManagerServer>();
chat?.SendMessageAsOther(sender, message, font, _steamIdSender);
} }
} }
} }

View File

@@ -81,22 +81,22 @@ namespace Torch.Commands
} }
} }
public bool HandleCommandFromServer(string message) public List<TorchChatMessage> HandleCommandFromServer(string message)
{ {
var cmdText = new string(message.Skip(1).ToArray()); var cmdText = new string(message.Skip(1).ToArray());
var command = Commands.GetCommand(cmdText, out string argText); var command = Commands.GetCommand(cmdText, out string argText);
if (command == null) if (command == null)
return false; return null;
var cmdPath = string.Join(".", command.Path); var cmdPath = string.Join(".", command.Path);
var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast<Match>().Select(x => x.ToString().Replace("\"", "")).ToList(); var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast<Match>().Select(x => x.ToString().Replace("\"", "")).ToList();
_log.Trace($"Invoking {cmdPath} for server."); _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)) if (command.TryInvoke(context))
_log.Info($"Server ran command '{message}'"); _log.Info($"Server ran command '{message}'");
else else
context.Respond($"Invalid Syntax: {command.SyntaxHelp}"); context.Respond($"Invalid Syntax: {command.SyntaxHelp}");
return true; return context.Responses;
} }
public void HandleCommand(TorchChatMessage msg, ref bool consumed) public void HandleCommand(TorchChatMessage msg, ref bool consumed)
@@ -130,7 +130,7 @@ namespace Torch.Commands
if (!HasPermission(steamId, command)) if (!HasPermission(steamId, command))
{ {
_log.Info($"{player.DisplayName} tried to use command {cmdPath} without permission"); _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; return;
} }

View File

@@ -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<TorchChatMessage> Responses = new List<TorchChatMessage>();
private bool _flag;
/// <inheritdoc />
public ConsoleCommandContext(ITorchBase torch, ITorchPlugin plugin, ulong steamIdSender, string rawArgs = null, List<string> args = null)
: base(torch, plugin, steamIdSender, rawArgs, args) { }
/// <inheritdoc />
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));
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using NLog;
using Sandbox.Game.Multiplayer; using Sandbox.Game.Multiplayer;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using Steamworks; using Steamworks;
@@ -28,6 +29,9 @@ namespace Torch.Commands
{ {
private static bool _restartPending = false; private static bool _restartPending = false;
private static bool _cancelRestart = false; private static bool _cancelRestart = false;
private bool _stopPending = false;
private bool _cancelStop = false;
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
[Command("whatsmyip")] [Command("whatsmyip")]
[Permission(MyPromoteLevel.None)] [Permission(MyPromoteLevel.None)]
@@ -37,7 +41,7 @@ namespace Torch.Commands
steamId = Context.Player.SteamUserId; steamId = Context.Player.SteamUserId;
VRage.GameServices.MyP2PSessionState statehack = new VRage.GameServices.MyP2PSessionState(); 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()); var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray());
Context.Respond($"Your IP is {ip}"); Context.Respond($"Your IP is {ip}");
} }
@@ -57,10 +61,16 @@ namespace Torch.Commands
if (node != null) if (node != null)
{ {
var command = node.Command; 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(); 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) if (command != null)
{ {
sb.AppendLine($"Syntax: {command.SyntaxHelp}"); sb.AppendLine($"Syntax: {command.SyntaxHelp}");
@@ -94,11 +104,11 @@ namespace Torch.Commands
if (node != null) if (node != null)
{ {
var command = node.Command; 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(); var sb = new StringBuilder();
if (command != null) if (command != null && (Context.Player == null || command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
{ {
sb.AppendLine($"Syntax: {command.SyntaxHelp}"); sb.AppendLine($"Syntax: {command.SyntaxHelp}");
sb.Append(command.HelpText); sb.Append(command.HelpText);
@@ -114,7 +124,7 @@ namespace Torch.Commands
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var command in commandManager.Commands.WalkTree()) 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}"); sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
} }
@@ -146,9 +156,25 @@ namespace Torch.Commands
} }
[Command("stop", "Stops the server.")] [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) if (save)
DoSave()?.ContinueWith((a, mod) => DoSave()?.ContinueWith((a, mod) =>
{ {
@@ -157,7 +183,7 @@ namespace Torch.Commands
torch.Stop(); torch.Stop();
}, this, TaskContinuationOptions.RunContinuationsAsynchronously); }, this, TaskContinuationOptions.RunContinuationsAsynchronously);
else else
Context.Torch.Stop(); Context.Torch.Stop();*/
} }
[Command("restart", "Restarts the server.")] [Command("restart", "Restarts the server.")]
@@ -170,6 +196,7 @@ namespace Torch.Commands
} }
_restartPending = true; _restartPending = true;
Task.Run(() => Task.Run(() =>
{ {
var countdown = RestartCountdown(countdownSeconds, save).GetEnumerator(); var countdown = RestartCountdown(countdownSeconds, save).GetEnumerator();
@@ -196,6 +223,68 @@ namespace Torch.Commands
Context.Respond("A restart is not pending."); 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<IChatManagerClient>()
.SendMessageAsSelf($"Stop cancelled.");
_stopPending = false;
_cancelStop = false;
yield break;
}
if (i >= 60 && i % 60 == 0)
{
Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
.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<IChatManagerClient>()
.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<IChatManagerClient>()
.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) private IEnumerable RestartCountdown(int countdown, bool save)
{ {
for (var i = countdown; i >= 0; i--) for (var i = countdown; i >= 0; i--)
@@ -226,15 +315,18 @@ namespace Torch.Commands
else else
{ {
if (save) if (save)
Context.Torch.Save().ContinueWith(x => Restart()); {
else Log.Info("Savin game before restart.");
Restart(); Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
.SendMessageAsSelf($"Saving game before restart.");
}
Log.Info("Restarting server.");
Context.Torch.Invoke(() => Context.Torch.Restart(save));
yield break; yield break;
} }
} }
void Restart() => Context.Torch.Invoke(() => Context.Torch.Restart());
} }
private string Pluralize(int num) private string Pluralize(int num)
@@ -255,7 +347,7 @@ namespace Torch.Commands
private Task DoSave() private Task DoSave()
{ {
Task<GameSaveResult> task = Context.Torch.Save(60 * 1000, true); Task<GameSaveResult> task = Context.Torch.Save(300 * 1000, true);
if (task == null) if (task == null)
{ {
Context.Respond("Save failed, a save is already in progress"); Context.Respond("Save failed, a save is already in progress");
@@ -290,5 +382,11 @@ namespace Torch.Commands
} }
}, this, TaskContinuationOptions.RunContinuationsAsynchronously); }, this, TaskContinuationOptions.RunContinuationsAsynchronously);
} }
[Command("uptime", "Check how long the server has been online.")]
public void Uptime()
{
Context.Respond(((ITorchServer)Context.Torch).ElapsedPlayTime.ToString());
}
} }
} }

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace Torch
{
public static class LinqExtensions
{
public static IEnumerable<T> TSort<T>( this IEnumerable<T> source, Func<T, IEnumerable<T>> dependencies, bool throwOnCycle = false )
{
var sorted = new List<T>();
var visited = new HashSet<T>();
foreach( var item in source )
Visit( item, visited, sorted, dependencies, throwOnCycle );
return sorted;
}
private static void Visit<T>( T item, HashSet<T> visited, List<T> sorted, Func<T, IEnumerable<T>> 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" );
}
}
}
}

View File

@@ -15,6 +15,7 @@ using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Utils; using Torch.Utils;
using VRage.Game; using VRage.Game;
using VRageMath;
namespace Torch.Managers.ChatManager namespace Torch.Managers.ChatManager
{ {
@@ -38,17 +39,26 @@ namespace Torch.Managers.ChatManager
{ {
if (Sandbox.Engine.Platform.Game.IsDedicated) 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() var scripted = new ScriptedChatMsg()
{ {
Author = "Server", Author = Torch.Config.ChatName,
Font = MyFontEnum.Red, Font = color,
Text = message, Text = message,
Target = 0 Target = 0
}; };
MyMultiplayerBase.SendScriptedChatMessage(ref scripted); MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
} }
else else
MyMultiplayer.Static.SendChatMessage(message); throw new NotImplementedException("Chat system changes broke this");
//MyMultiplayer.Static.SendChatMessage(message);
} }
else if (HasHud) else if (HasHud)
MyHud.Chat.ShowMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", message); MyHud.Chat.ShowMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", message);
@@ -59,12 +69,6 @@ namespace Torch.Managers.ChatManager
{ {
if (HasHud) if (HasHud)
MyHud.Chat?.ShowMessage(author, message, font); MyHud.Chat?.ShowMessage(author, message, font);
MySession.Static.GlobalChatHistory.GlobalChatHistory.Chat.Enqueue(new MyGlobalChatItem()
{
Author = author,
AuthorFont = font,
Text = message
});
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -76,10 +80,10 @@ namespace Torch.Managers.ChatManager
{ {
_chatMessageRecievedReplacer = _chatMessageReceivedFactory.Invoke(); _chatMessageRecievedReplacer = _chatMessageReceivedFactory.Invoke();
_scriptedChatMessageRecievedReplacer = _scriptedChatMessageReceivedFactory.Invoke(); _scriptedChatMessageRecievedReplacer = _scriptedChatMessageReceivedFactory.Invoke();
_chatMessageRecievedReplacer.Replace(new Action<ulong, string>(Multiplayer_ChatMessageReceived), _chatMessageRecievedReplacer.Replace(new Action<ulong, string, ChatChannel, long, string>(Multiplayer_ChatMessageReceived),
MyMultiplayer.Static); MyMultiplayer.Static);
_scriptedChatMessageRecievedReplacer.Replace( _scriptedChatMessageRecievedReplacer.Replace(
new Action<string, string, string>(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static); new Action<string, string, string, Color>(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
} }
else else
{ {
@@ -113,7 +117,7 @@ namespace Torch.Managers.ChatManager
{ {
if (!sendToOthers) if (!sendToOthers)
return; 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); bool consumed = RaiseMessageRecieved(torchMsg);
if (!consumed) if (!consumed)
consumed = OfflineMessageProcessor(torchMsg); 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); (steamUserId == MyGameService.UserId) ? MyFontEnum.DarkBlue : MyFontEnum.Blue);
if (!RaiseMessageRecieved(torchMsg) && HasHud) 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); var torchMsg = new TorchChatMessage(author, message, font);
if (!RaiseMessageRecieved(torchMsg) && HasHud) if (!RaiseMessageRecieved(torchMsg) && HasHud)
_hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font); _hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font, color);
} }
protected bool RaiseMessageRecieved(TorchChatMessage msg) protected bool RaiseMessageRecieved(TorchChatMessage msg)
@@ -158,9 +162,9 @@ namespace Torch.Managers.ChatManager
protected static bool HasHud => !Sandbox.Engine.Platform.Game.IsDedicated; protected static bool HasHud => !Sandbox.Engine.Platform.Game.IsDedicated;
[ReflectedMethod(Name = _hudChatMessageReceivedName)] [ReflectedMethod(Name = _hudChatMessageReceivedName)]
private static Action<MyHudChat, ulong, string> _hudChatMessageReceived; private static Action<MyHudChat, ulong, string, ChatChannel, long, string> _hudChatMessageReceived;
[ReflectedMethod(Name = _hudChatScriptedMessageReceivedName)] [ReflectedMethod(Name = _hudChatScriptedMessageReceivedName)]
private static Action<MyHudChat, string, string, string> _hudChatScriptedMessageReceived; private static Action<MyHudChat, string, string, string, Color> _hudChatScriptedMessageReceived;
[ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ChatMessageReceived), typeof(MyHudChat), _hudChatMessageReceivedName)] [ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ChatMessageReceived), typeof(MyHudChat), _hudChatMessageReceivedName)]
private static Func<ReflectedEventReplacer> _chatMessageReceivedFactory; private static Func<ReflectedEventReplacer> _chatMessageReceivedFactory;

View File

@@ -13,38 +13,72 @@ using Sandbox.Game.Multiplayer;
using Sandbox.Game.World; using Sandbox.Game.World;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Managers.PatchManager;
using Torch.Utils; using Torch.Utils;
using VRage; using VRage;
using VRage.Collections;
using VRage.Library.Collections; using VRage.Library.Collections;
using VRage.Network; using VRage.Network;
namespace Torch.Managers.ChatManager namespace Torch.Managers.ChatManager
{ {
[PatchShim]
internal static class ChatInterceptPatch
{
private static ChatManagerServer _chatManager;
private static ChatManagerServer ChatManager => _chatManager ?? (_chatManager = TorchBase.Instance.CurrentSession.Managers.GetManager<ChatManagerServer>());
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 public class ChatManagerServer : ChatManagerClient, IChatManagerServer
{ {
[Dependency(Optional = true)]
private INetworkManager _networkManager;
private static readonly Logger _log = LogManager.GetCurrentClassLogger(); private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static readonly Logger _chatLog = LogManager.GetLogger("Chat"); private static readonly Logger _chatLog = LogManager.GetLogger("Chat");
private readonly ChatIntercept _chatIntercept; private readonly HashSet<ulong> _muted = new HashSet<ulong>();
/// <inheritdoc />
public HashSetReader<ulong> MutedUsers => _muted;
/// <inheritdoc /> /// <inheritdoc />
public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance) public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
{ {
_chatIntercept = new ChatIntercept(this);
} }
/// <inheritdoc /> /// <inheritdoc />
public event MessageProcessingDel MessageProcessing; public event MessageProcessingDel MessageProcessing;
/// <inheritdoc />
public bool MuteUser(ulong steamId)
{
return _muted.Add(steamId);
}
/// <inheritdoc />
public bool UnmuteUser(ulong steamId)
{
return _muted.Remove(steamId);
}
/// <inheritdoc /> /// <inheritdoc />
public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0) public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0)
{ {
if (targetSteamId == Sync.MyId) if (targetSteamId == Sync.MyId)
{ {
RaiseMessageRecieved(new TorchChatMessage(authorId, message)); RaiseMessageRecieved(new TorchChatMessage(authorId, message, ChatChannel.Global, 0));
return; return;
} }
if (MyMultiplayer.Static == null) if (MyMultiplayer.Static == null)
@@ -89,44 +123,15 @@ namespace Torch.Managers.ChatManager
} }
var scripted = new ScriptedChatMsg() var scripted = new ScriptedChatMsg()
{ {
Author = author, Author = author ?? Torch.Config.ChatName,
Text = message, Text = message,
Font = font, Font = font ?? Torch.Config.ChatColor,
Target = Sync.Players.TryGetIdentityId(targetSteamId) Target = Sync.Players.TryGetIdentityId(targetSteamId)
}; };
_chatLog.Info($"{author} (to {GetMemberName(targetSteamId)}): {message}"); _chatLog.Info($"{author} (to {GetMemberName(targetSteamId)}): {message}");
MyMultiplayerBase.SendScriptedChatMessage(ref scripted); MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
} }
/// <inheritdoc/>
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");
}
}
/// <inheritdoc /> /// <inheritdoc />
protected override bool OfflineMessageProcessor(TorchChatMessage msg) protected override bool OfflineMessageProcessor(TorchChatMessage msg)
{ {
@@ -137,84 +142,25 @@ namespace Torch.Managers.ChatManager
return consumed; return consumed;
} }
private void MpStaticChatMessageReceived(ulong a, string b)
{
var tmp = false;
RaiseMessageRecieved(new ChatMsg()
{
Author = a,
Text = b
}, ref tmp);
}
/// <inheritdoc/>
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) 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); MessageProcessing?.Invoke(torchMsg, ref consumed);
if (!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) public static string GetMemberName(ulong steamId)
{ {
return MyMultiplayer.Static?.GetMemberName(steamId) ?? $"user_{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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
}
} }
} }

View File

@@ -4,12 +4,14 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog;
using Torch.API; using Torch.API;
namespace Torch.Managers namespace Torch.Managers
{ {
public class FilesystemManager : Manager public class FilesystemManager : Manager
{ {
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
/// <summary> /// <summary>
/// Temporary directory for Torch that is cleared every time the program is started. /// Temporary directory for Torch that is cleared every time the program is started.
/// </summary> /// </summary>
@@ -22,30 +24,46 @@ namespace Torch.Managers
public FilesystemManager(ITorchBase torchInstance) : base(torchInstance) 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; var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName;
TempDirectory = Directory.CreateDirectory(Path.Combine(torch, "tmp")).FullName;
TorchDirectory = torch; TorchDirectory = torch;
_log.Debug($"Clearing tmp directory at {TempDirectory}");
ClearTemp(); ClearTemp();
} }
private void ClearTemp() private void ClearTemp()
{ {
foreach (var file in Directory.GetFiles(TempDirectory, "*", SearchOption.AllDirectories)) 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}");
}
}
} }
/// <summary> /// <summary>
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts. /// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
/// </summary> /// </summary>
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; return;
var rand = Path.GetRandomFileName(); var rand = Path.GetRandomFileName();
var dest = Path.Combine(TempDirectory, rand); var dest = Path.Combine(TempDirectory, rand);
File.Move(file, dest); File.Move(source, rand);
string rsource = Path.Combine(path, rand);
File.Move(rsource, dest);
} }
} }
} }

View File

@@ -76,7 +76,21 @@ namespace Torch.Managers
private static StringBuilder PrepareLog(MyLog log) 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) 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) 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; return false;
} }

View File

@@ -116,7 +116,7 @@ namespace Torch.Managers
protected void RaiseClientJoined(ulong steamId) protected void RaiseClientJoined(ulong steamId)
{ {
var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected }; var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected };
_log.Info($"Player {vm.Name} joined ({vm.SteamId}"); _log.Info($"Player {vm.Name} joined ({vm.SteamId})");
Players.Add(steamId, vm); Players.Add(steamId, vm);
PlayerJoined?.Invoke(vm); PlayerJoined?.Invoke(vm);
} }

View File

@@ -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
{
/// <summary>
/// Check the method name and do unit tests on parameters in here.
/// </summary>
/// <param name="site"></param>
/// <returns></returns>
public abstract bool CanHandle(CallSite site);
/// <summary>
/// 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.
/// </summary>
/// <param name="remoteUserId"></param>
/// <param name="site"></param>
/// <param name="stream"></param>
/// <param name="obj"></param>
/// <param name="packet"></param>
/// <returns></returns>
public abstract bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
/// <summary>
/// Extracts method arguments from the bitstream or packs them back in, depending on stream read mode.
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <param name="info"></param>
/// <param name="stream"></param>
/// <param name="arg1"></param>
public void Serialize<T1>(MethodInfo info, BitStream stream, ref T1 arg1)
{
var s1 = MyFactory.GetSerializer<T1>();
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<T1, T2>(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2)
{
var s1 = MyFactory.GetSerializer<T1>();
var s2 = MyFactory.GetSerializer<T2>();
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<T1, T2, T3>(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3)
{
var s1 = MyFactory.GetSerializer<T1>();
var s2 = MyFactory.GetSerializer<T2>();
var s3 = MyFactory.GetSerializer<T3>();
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<T1, T2, T3, T4>(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4)
{
var s1 = MyFactory.GetSerializer<T1>();
var s2 = MyFactory.GetSerializer<T2>();
var s3 = MyFactory.GetSerializer<T3>();
var s4 = MyFactory.GetSerializer<T4>();
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<T1, T2, T3, T4, T5>(MethodInfo info, BitStream stream, ref T1 arg1, ref T2 arg2, ref T3 arg3, ref T4 arg4, ref T5 arg5)
{
var s1 = MyFactory.GetSerializer<T1>();
var s2 = MyFactory.GetSerializer<T2>();
var s3 = MyFactory.GetSerializer<T3>();
var s4 = MyFactory.GetSerializer<T4>();
var s5 = MyFactory.GetSerializer<T5>();
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<T1, T2, T3, T4, T5, T6>(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<T1>();
var s2 = MyFactory.GetSerializer<T2>();
var s3 = MyFactory.GetSerializer<T3>();
var s4 = MyFactory.GetSerializer<T4>();
var s5 = MyFactory.GetSerializer<T5>();
var s6 = MyFactory.GetSerializer<T6>();
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<T1, T2, T3, T4, T5, T6, T7>(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<T1>();
var s2 = MyFactory.GetSerializer<T2>();
var s3 = MyFactory.GetSerializer<T3>();
var s4 = MyFactory.GetSerializer<T4>();
var s5 = MyFactory.GetSerializer<T5>();
var s6 = MyFactory.GetSerializer<T6>();
var s7 = MyFactory.GetSerializer<T7>();
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);
}
}
}
}

View File

@@ -1,396 +1,133 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using NLog; using NLog;
using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Utils;
using VRage;
using VRage.Library.Collections;
using VRage.Network; using VRage.Network;
using VRageMath; using VRageMath;
namespace Torch.Managers namespace Torch.Managers
{ {
public class NetworkManager : Manager, INetworkManager public static class NetworkManager
{ {
private static Logger _log = LogManager.GetCurrentClassLogger(); private static Logger _log = LogManager.GetCurrentClassLogger();
private const string _myTransportLayerField = "TransportLayer";
private const string _transportHandlersField = "m_handlers";
private readonly HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
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<MyReplicationLayerBase, MyTypeTable> _typeTableGetter;
[ReflectedGetter(Name = "m_methodInfoLookup")]
private static Func<MyEventTable, Dictionary<MethodInfo, CallSite>> _methodInfoLookupGetter;
[ReflectedMethod(Type = typeof(MyReplicationLayer), Name = "GetObjectByNetworkId")]
private static Func<MyReplicationLayer, NetworkId, IMyNetObject> _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;
}
}
/// <inheritdoc/>
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<MyPacket>(OnEvent));
//PrintDebug();
_log.Debug("Initialized network intercept");
}
/// <inheritdoc/>
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.
/// <summary>
/// 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!
/// </summary>
/// <param name="packet"></param>
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;
}
}
/// <inheritdoc />
public void RegisterNetworkHandler(INetworkHandler handler)
{
var handlerType = handler.GetType().FullName;
var toRemove = new List<INetworkHandler>();
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);
}
/// <inheritdoc />
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 #region Network Injection
private static Dictionary<MethodInfo, Delegate> _delegateCache = new Dictionary<MethodInfo, Delegate>();
/// <summary> private static Func<T, TA> GetDelegate<T, TA>(MethodInfo method) where TA : class
/// Broadcasts an event to all connected clients
/// </summary>
/// <param name="method"></param>
/// <param name="obj"></param>
/// <param name="args"></param>
public void RaiseEvent(MethodInfo method, object obj, params object[] args)
{ {
//default(EndpointId) tells the network to broadcast the message if (!_delegateCache.TryGetValue(method, out var del))
RaiseEvent(method, obj, default(EndpointId), args);
}
/// <summary>
/// Sends an event to one client by SteamId
/// </summary>
/// <param name="method"></param>
/// <param name="obj"></param>
/// <param name="steamId"></param>
/// <param name="args"></param>
public void RaiseEvent(MethodInfo method, object obj, ulong steamId, params object[] args)
{
RaiseEvent(method, obj, new EndpointId(steamId), args);
}
/// <summary>
/// Sends an event to one client
/// </summary>
/// <param name="method"></param>
/// <param name="obj"></param>
/// <param name="endpoint"></param>
/// <param name="args"></param>
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<EventAttribute>())
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 (argTypes[i + 1] != parameters[i].ParameterType) del = (Func<T, TA>)(x => Delegate.CreateDelegate(typeof(TA), x, method) as TA);
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()))}"); _delegateCache[method] = del;
} }
//create a generic method of DispatchEvent and invoke to inject our data into the network return (Func<T, TA>)del;
var dispatch = DispatchEventInfo.MakeGenericMethod(argTypes);
dispatch.Invoke(MyMultiplayer.ReplicationLayer, arguments);
} }
/// <summary> public static void RaiseEvent<T1>(T1 instance, MethodInfo method, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
/// Broadcasts a static event to all connected clients
/// </summary>
/// <param name="method"></param>
/// <param name="args"></param>
public void RaiseStaticEvent(MethodInfo method, params object[] args)
{ {
//default(EndpointId) tells the network to broadcast the message var del = GetDelegate<T1, Action>(method);
RaiseStaticEvent(method, default(EndpointId), args);
MyMultiplayer.RaiseEvent(instance, del, target);
} }
/// <summary> public static void RaiseEvent<T1, T2>(T1 instance, MethodInfo method, T2 arg1, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
/// Sends a static event to one client by SteamId
/// </summary>
/// <param name="method"></param>
/// <param name="steamId"></param>
/// <param name="args"></param>
public void RaiseStaticEvent(MethodInfo method, ulong steamId, params object[] args)
{ {
RaiseEvent(method, null, new EndpointId(steamId), args); var del = GetDelegate<T1, Action<T2>> (method);
MyMultiplayer.RaiseEvent(instance, del, arg1, target);
} }
/// <summary> public static void RaiseEvent<T1, T2, T3>(T1 instance, MethodInfo method, T2 arg1, T3 arg2, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
/// Sends a static event to one client
/// </summary>
/// <param name="method"></param>
/// <param name="endpoint"></param>
/// <param name="args"></param>
public void RaiseStaticEvent(MethodInfo method, EndpointId endpoint, params object[] args)
{ {
RaiseEvent(method, null, endpoint, args); var del = GetDelegate<T1, Action<T2, T3>>(method);
MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, target);
} }
private CallSite TryGetStaticCallSite(MethodInfo method) public static void RaiseEvent<T1, T2, T3, T4>(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{ {
MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer); var del = GetDelegate<T1, Action<T2, T3, T4>>(method);
if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result))
throw new MissingMemberException("Provided event target not found!"); MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, target);
return result;
} }
private CallSite TryGetCallSite(MethodInfo method, object arg) public static void RaiseEvent<T1, T2, T3, T4, T5>(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()); var del = GetDelegate<T1, Action<T2, T3, T4, T5>>(method);
if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result))
throw new MissingMemberException("Provided event target not found!"); MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, target);
return result;
} }
public static void RaiseEvent<T1, T2, T3, T4, T5, T6>(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
{
var del = GetDelegate<T1, Action<T2, T3, T4, T5, T6>>(method);
MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, arg5, target);
}
public static void RaiseEvent<T1, T2, T3, T4, T5, T6, T7>(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<T1, Action<T2, T3, T4, T5, T6, T7>>(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<IMyEventOwner, Action>(method);
MyMultiplayer.RaiseStaticEvent(del, target, position);
}
public static void RaiseStaticEvent<T1>(MethodInfo method, T1 arg1, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, target, position);
}
public static void RaiseStaticEvent<T1, T2>(MethodInfo method, T1 arg1, T2 arg2, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1, T2>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, target, position);
}
public static void RaiseStaticEvent<T1, T2, T3>(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1, T2, T3>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, target, position);
}
public static void RaiseStaticEvent<T1, T2, T3, T4>(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1, T2, T3, T4>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, target, position);
}
public static void RaiseStaticEvent<T1, T2, T3, T4, T5>(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1, T2, T3, T4, T5>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, target, position);
}
public static void RaiseStaticEvent<T1, T2, T3, T4, T5, T6>(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, EndpointId target = default(EndpointId), Vector3D? position = null)
{
var del = GetDelegate<IMyEventOwner, Action<T1, T2, T3, T4, T5, T6>>(method);
MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, arg6, target, position);
}
#endregion #endregion
} }
} }

View File

@@ -83,7 +83,7 @@ namespace Torch.Managers.PatchManager
private DynamicMethod AllocatePatchMethod() private DynamicMethod AllocatePatchMethod()
{ {
Debug.Assert(_method.DeclaringType != null); 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 returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
var parameters = _method.GetParameters(); var parameters = _method.GetParameters();
var parameterTypes = (_method.IsStatic ? Enumerable.Empty<Type>() : new[] {typeof(object)}) var parameterTypes = (_method.IsStatic ? Enumerable.Empty<Type>() : new[] {typeof(object)})

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using NLog;
namespace Torch.Managers.PatchManager namespace Torch.Managers.PatchManager
{ {
@@ -10,6 +11,7 @@ namespace Torch.Managers.PatchManager
public sealed class PatchContext public sealed class PatchContext
{ {
private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>(); private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>();
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
internal PatchContext() internal PatchContext()
{ {

View File

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

View File

@@ -10,8 +10,8 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using Octokit;
using Torch.API; using Torch.API;
using Torch.API.WebAPI;
namespace Torch.Managers namespace Torch.Managers
{ {
@@ -21,7 +21,6 @@ namespace Torch.Managers
public class UpdateManager : Manager public class UpdateManager : Manager
{ {
private Timer _updatePollTimer; private Timer _updatePollTimer;
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName; private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
private Logger _log = LogManager.GetCurrentClassLogger(); private Logger _log = LogManager.GetCurrentClassLogger();
[Dependency] [Dependency]
@@ -43,48 +42,33 @@ namespace Torch.Managers
CheckAndUpdateTorch(); CheckAndUpdateTorch();
} }
private async Task<Tuple<Version, string>> TryGetLatestArchiveUrl(string owner, string name)
{
try
{
var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false);
if (latest == null)
return new Tuple<Version, string>(new Version(), null);
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
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, string>(version, zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
_log.Error(e);
return default(Tuple<Version, string>);
}
}
private async void CheckAndUpdateTorch() private async void CheckAndUpdateTorch()
{ {
// Doesn't work properly or reliably, TODO update when Jenkins is fully configured if (Torch.Config.NoUpdate || !Torch.Config.GetTorchUpdates)
return;
if (!Torch.Config.GetTorchUpdates)
return; return;
try try
{ {
var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false); var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Branch);
if (releaseInfo.Item1 > Torch.TorchVersion) 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"); 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); UpdateFromZip(updateName, _torchDir);
File.Delete(updateName); 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 else
{ {
@@ -104,12 +88,16 @@ namespace Torch.Managers
{ {
foreach (var file in zip.Entries) 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}"); _log.Debug($"Unzipping {file.FullName}");
var targetFile = Path.Combine(extractPath, 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?
} }
} }

View File

@@ -0,0 +1,29 @@
using System;
using System.Reflection;
using System.Windows.Data;
using VRage.GameServices;
namespace Torch
{
/// <summary>
/// Provides static accessor for MySteamService because Keen made it internal
/// </summary>
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);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Torch.Patches
public static class GameAnalyticsPatch public static class GameAnalyticsPatch
{ {
private static readonly Logger _log = LogManager.GetCurrentClassLogger(); private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static Action<ILogger> _setLogger; private static Action<ILogger, ILogger> _setLogger;
public static void Patch(PatchContext ctx) public static void Patch(PatchContext ctx)
{ {
@@ -27,7 +27,7 @@ namespace Torch.Patches
return; return;
} }
RuntimeHelpers.RunClassConstructor(type.TypeHandle); RuntimeHelpers.RunClassConstructor(type.TypeHandle);
_setLogger = loggerField?.CreateSetter<ILogger>(); _setLogger = loggerField?.CreateSetter<ILogger, ILogger>();
FixLogging(); FixLogging();
ConstructorInfo ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], new ParameterModifier[0]); 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() private static void FixLogging()
{ {
_setLogger(LogManager.GetLogger("GameAnalytics")); _setLogger(null, LogManager.GetLogger("GameAnalytics"));
if (!(LogManager.Configuration is XmlLoggingConfiguration)) if (!(LogManager.Configuration is XmlLoggingConfiguration))
LogManager.Configuration = new XmlLoggingConfiguration(Path.Combine( LogManager.Configuration = new XmlLoggingConfiguration(Path.Combine(
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? Environment.CurrentDirectory, "NLog.config")); Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? Environment.CurrentDirectory, "NLog.config"));

View File

@@ -19,7 +19,7 @@ namespace Torch.Patches
internal static void Patch(PatchContext target) 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) if (ctor == null)
throw new ArgumentException("Can't find constructor MySandboxGame(string[])"); throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
target.GetPattern(ctor).Prefixes.Add(MethodRef(PrefixConstructor)); target.GetPattern(ctor).Prefixes.Add(MethodRef(PrefixConstructor));

Some files were not shown because too many files have changed in this diff Show More