Merge branch 'master' of git://github.com/TorchAPI/Torch
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
pushd
|
||||
|
||||
$steamData = "C:/Steam/Data/"
|
||||
$steamData = "C:/Steam/Data-playtest/"
|
||||
$steamCMDPath = "C:/Steam/steamcmd/"
|
||||
$steamCMDZip = "C:/Steam/steamcmd.zip"
|
||||
|
||||
@@ -17,6 +17,6 @@ if (!(Test-Path $steamCMDPath)) {
|
||||
}
|
||||
|
||||
cd "$steamData"
|
||||
& "$steamCMDPath/steamcmd.exe" "+login anonymous" "+force_install_dir $steamData" "+app_update 298740" "+quit"
|
||||
& "$steamCMDPath/steamcmd.exe" "+login anonymous" "+force_install_dir $steamData" "+app_update 298740 validate" "+quit"
|
||||
|
||||
popd
|
4
Jenkinsfile
vendored
4
Jenkinsfile
vendored
@@ -22,7 +22,7 @@ node {
|
||||
stage('Acquire SE') {
|
||||
bat 'powershell -File Jenkins/jenkins-grab-se.ps1'
|
||||
bat 'IF EXIST GameBinaries RMDIR GameBinaries'
|
||||
bat 'mklink /J GameBinaries "C:/Steam/Data/DedicatedServer64/"'
|
||||
bat 'mklink /J GameBinaries "C:/Steam/Data-playtest/DedicatedServer64/"'
|
||||
}
|
||||
|
||||
stage('Acquire NuGet Packages') {
|
||||
@@ -31,7 +31,7 @@ node {
|
||||
|
||||
stage('Build') {
|
||||
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim()
|
||||
if (env.BRANCH_NAME == "master") {
|
||||
if (env.BRANCH_NAME == "master" || env.BRANCH_NAME == "Patron" || env.BRANCH_NAME == "publictest") {
|
||||
buildMode = "Release"
|
||||
} else {
|
||||
buildMode = "Debug"
|
||||
|
21
NLog-user.config
Normal file
21
NLog-user.config
Normal 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>
|
@@ -4,6 +4,8 @@
|
||||
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
|
||||
<variable name="logContent" value="${message:withException=true}"/>
|
||||
|
||||
<include file="NLog-user.config"/>
|
||||
|
||||
<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" />
|
||||
@@ -15,6 +17,7 @@
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
<!-- Do not define custom rules here. Use NLog-user.config -->
|
||||
<logger name="Keen" minlevel="Warn" writeTo="main"/>
|
||||
<logger name="Keen" minlevel="Info" writeTo="console, wpf"/>
|
||||
<logger name="Keen" minlevel="Debug" writeTo="keen" final="true" />
|
||||
|
12
README.md
12
README.md
@@ -18,18 +18,8 @@ Torch is the successor to SE Server Extender and gives server admins the tools t
|
||||
* Unzip the Torch release into its own directory and run the executable. It will automatically download the SE DS and generate the other necessary files.
|
||||
- If you already have a DS installed you can unzip the Torch files into the folder that contains the DedicatedServer64 folder.
|
||||
|
||||
## Torch.Client
|
||||
* An optional client-side version of Torch. More documentation to come.
|
||||
|
||||
# Building
|
||||
To build Torch you must first have a complete SE Dedicated installation somewhere. Before you open the solution, run the Setup batch file and enter the path of that installation's DedicatedServer64 folder. The script will make a symlink to that folder so the Torch solution can find the DLL references it needs.
|
||||
|
||||
In both cases you will need to set the InstancePath in TorchConfig.xml to an existing dedicated server instance as Torch can't fully generate it on its own yet.
|
||||
|
||||
# Official Plugins
|
||||
Install plugins by unzipping them into the 'Plugins' folder which should be in the same location as the Torch files. If it doesn't exist you can simply create it.
|
||||
* [Essentials](https://github.com/TorchAPI/Essentials): Adds a slew of chat commands and other tools to help manage your server.
|
||||
* [Concealment](https://github.com/TorchAPI/Concealment): Adds game logic and physics optimizations that significantly improve sim speed.
|
||||
|
||||
If you have a more enjoyable server experience because of Torch, please consider supporting us on Patreon.
|
||||
[](https://www.patreon.com/bePatron?u=847269)!
|
||||
[](https://www.patreon.com/bePatron?u=847269)
|
||||
|
@@ -104,7 +104,7 @@ namespace Torch.API
|
||||
/// <summary>
|
||||
/// Restart the Torch instance, blocking until the restart has been performed.
|
||||
/// </summary>
|
||||
void Restart();
|
||||
void Restart(bool save = true);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a save of the game.
|
||||
@@ -154,6 +154,8 @@ namespace Torch.API
|
||||
/// Raised when the server's Init() method has completed.
|
||||
/// </summary>
|
||||
event Action<ITorchServer> Initialized;
|
||||
|
||||
TimeSpan ElapsedPlayTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Torch
|
||||
{
|
||||
@@ -12,12 +13,17 @@ namespace Torch
|
||||
string InstancePath { get; set; }
|
||||
bool NoGui { get; set; }
|
||||
bool NoUpdate { get; set; }
|
||||
List<string> Plugins { get; set; }
|
||||
List<Guid> Plugins { get; set; }
|
||||
bool LocalPlugins { get; set; }
|
||||
bool RestartOnCrash { get; set; }
|
||||
bool ShouldUpdatePlugins { get; }
|
||||
bool ShouldUpdateTorch { get; }
|
||||
int TickTimeout { get; set; }
|
||||
string WaitForPID { get; set; }
|
||||
string ChatName { get; set; }
|
||||
string ChatColor { get; set; }
|
||||
string TestPlugin { get; set; }
|
||||
bool DisconnectOnRestart { get; set; }
|
||||
|
||||
bool Save(string path = null);
|
||||
}
|
||||
|
@@ -7,25 +7,25 @@ using System.Threading.Tasks;
|
||||
namespace Torch.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Version in the form v#.#.#.#-info
|
||||
/// Version in the form v#.#.#.#-branch
|
||||
/// </summary>
|
||||
public class InformationalVersion
|
||||
{
|
||||
public Version Version { get; set; }
|
||||
public string[] Information { get; set; }
|
||||
public string Branch { get; set; }
|
||||
|
||||
public static bool TryParse(string input, out InformationalVersion version)
|
||||
{
|
||||
version = default(InformationalVersion);
|
||||
var trim = input.TrimStart('v');
|
||||
var info = trim.Split('-');
|
||||
var info = trim.Split(new[]{'-'}, 2);
|
||||
if (!Version.TryParse(info[0], out Version result))
|
||||
return false;
|
||||
|
||||
version = new InformationalVersion { Version = result };
|
||||
|
||||
if (info.Length > 1)
|
||||
version.Information = info.Skip(1).ToArray();
|
||||
version.Branch = info[1];
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -33,10 +33,10 @@ namespace Torch.API
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (Information == null || Information.Length == 0)
|
||||
if (Branch == null)
|
||||
return $"v{Version}";
|
||||
|
||||
return $"v{Version}-{string.Join("-", Information)}";
|
||||
return $"v{Version}-{string.Join("-", Branch)}";
|
||||
}
|
||||
|
||||
public static explicit operator InformationalVersion(Version v)
|
||||
@@ -48,5 +48,15 @@ namespace Torch.API
|
||||
{
|
||||
return v.Version;
|
||||
}
|
||||
|
||||
public static bool operator >(InformationalVersion lhs, InformationalVersion rhs)
|
||||
{
|
||||
return lhs.Version > rhs.Version;
|
||||
}
|
||||
|
||||
public static bool operator <(InformationalVersion lhs, InformationalVersion rhs)
|
||||
{
|
||||
return lhs.Version < rhs.Version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Game.Gui;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
using VRage.Game;
|
||||
using VRage.Network;
|
||||
@@ -28,6 +29,8 @@ namespace Torch.API.Managers
|
||||
AuthorSteamId = null;
|
||||
Author = author;
|
||||
Message = message;
|
||||
Channel = ChatChannel.Global;
|
||||
Target = 0;
|
||||
Font = font;
|
||||
}
|
||||
|
||||
@@ -38,12 +41,14 @@ namespace Torch.API.Managers
|
||||
/// <param name="authorSteamId">Author's steam ID</param>
|
||||
/// <param name="message">Message</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;
|
||||
AuthorSteamId = authorSteamId;
|
||||
Author = author;
|
||||
Message = message;
|
||||
Channel = channel;
|
||||
Target = target;
|
||||
Font = font;
|
||||
}
|
||||
|
||||
@@ -53,12 +58,14 @@ namespace Torch.API.Managers
|
||||
/// <param name="authorSteamId">Author's steam ID</param>
|
||||
/// <param name="message">Message</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;
|
||||
AuthorSteamId = authorSteamId;
|
||||
Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player";
|
||||
Message = message;
|
||||
Channel = channel;
|
||||
Target = target;
|
||||
Font = font;
|
||||
}
|
||||
|
||||
@@ -79,6 +86,14 @@ namespace Torch.API.Managers
|
||||
/// </summary>
|
||||
public readonly string Message;
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public readonly string Font;
|
||||
|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VRage.Collections;
|
||||
using VRage.Network;
|
||||
|
||||
namespace Torch.API.Managers
|
||||
@@ -41,5 +42,24 @@ namespace Torch.API.Managers
|
||||
/// <param name="font">Font to use</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);
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
|
@@ -34,5 +34,22 @@ namespace Torch.API.Plugins
|
||||
/// This is called on the game thread after each tick.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Torch.API.Managers;
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.API.Session
|
||||
{
|
||||
@@ -47,5 +49,29 @@ namespace Torch.API.Session
|
||||
/// <returns>true if removed, false if not present</returns>
|
||||
/// <exception cref="ArgumentNullException">If the factory is null</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.API.xml</DocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="HavokWrapper, Version=1.0.6278.22649, Culture=neutral, processorArchitecture=AMD64">
|
||||
@@ -38,6 +39,10 @@
|
||||
<HintPath>..\GameBinaries\HavokWrapper.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</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">
|
||||
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
@@ -188,6 +193,8 @@
|
||||
<Compile Include="Session\ITorchSessionManager.cs" />
|
||||
<Compile Include="Session\TorchSessionState.cs" />
|
||||
<Compile Include="TorchGameState.cs" />
|
||||
<Compile Include="WebAPI\JenkinsQuery.cs" />
|
||||
<Compile Include="WebAPI\PluginQuery.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
130
Torch.API/WebAPI/JenkinsQuery.cs
Normal file
130
Torch.API/WebAPI/JenkinsQuery.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
168
Torch.API/WebAPI/PluginQuery.cs
Normal file
168
Torch.API/WebAPI/PluginQuery.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<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" />
|
||||
</packages>
|
@@ -86,6 +86,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
11
Torch.Client.Tests/app.config
Normal file
11
Torch.Client.Tests/app.config
Normal 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>
|
@@ -36,6 +36,7 @@
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Client.xml</DocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>torchicon.ico</ApplicationIcon>
|
||||
@@ -146,6 +147,7 @@
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
|
@@ -23,6 +23,8 @@ namespace Torch.Client
|
||||
public bool NoGui { get; set; } = false;
|
||||
public bool RestartOnCrash { get; set; } = false;
|
||||
public string WaitForPID { get; set; } = null;
|
||||
public string ChatName { get; set; }
|
||||
public string ChatColor { get; set; }
|
||||
|
||||
public bool Save(string path = null)
|
||||
{
|
||||
|
11
Torch.Client/app.config
Normal file
11
Torch.Client/app.config
Normal 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>
|
57
Torch.Mod/Messages/JoinServerMessage.cs
Normal file
57
Torch.Mod/Messages/JoinServerMessage.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ namespace Torch.Mod.Messages
|
||||
[ProtoInclude(1, typeof(DialogMessage))]
|
||||
[ProtoInclude(2, typeof(NotificationMessage))]
|
||||
[ProtoInclude(3, typeof(VoxelResetMessage))]
|
||||
[ProtoInclude(4, typeof(JoinServerMessage))]
|
||||
#endregion
|
||||
|
||||
[ProtoContract]
|
||||
|
@@ -10,6 +10,7 @@ using Torch.Mod.Messages;
|
||||
using VRage;
|
||||
using VRage.Collections;
|
||||
using VRage.Game.ModAPI;
|
||||
using VRage.Network;
|
||||
using VRage.Utils;
|
||||
using Task = ParallelTasks.Task;
|
||||
|
||||
@@ -50,6 +51,10 @@ namespace Torch.Mod
|
||||
{
|
||||
var m = _messagePool.Get();
|
||||
m.CompressedData = bytes;
|
||||
#if TORCH
|
||||
m.SenderId = MyEventContext.Current.Sender.Value;
|
||||
#endif
|
||||
|
||||
_processing.Add(m);
|
||||
}
|
||||
|
||||
@@ -59,10 +64,19 @@ namespace Torch.Mod
|
||||
{
|
||||
try
|
||||
{
|
||||
var m = _processing.Take();
|
||||
MessageBase m;
|
||||
try
|
||||
{
|
||||
m = _processing.Take();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MyLog.Default.WriteLineAndConsole($"Processing message: {m.GetType().Name}");
|
||||
|
||||
if (m is IncomingMessage)
|
||||
if (m is IncomingMessage) //process incoming messages
|
||||
{
|
||||
MessageBase i;
|
||||
try
|
||||
@@ -78,19 +92,22 @@ namespace Torch.Mod
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TorchModCore.Debug)
|
||||
MyAPIGateway.Utilities.ShowMessage("Torch", $"Received message of type {i.GetType().Name}");
|
||||
|
||||
if (MyAPIGateway.Multiplayer.IsServer)
|
||||
i.ProcessServer();
|
||||
else
|
||||
i.ProcessClient();
|
||||
}
|
||||
else
|
||||
else //process outgoing messages
|
||||
{
|
||||
if (TorchModCore.Debug)
|
||||
MyAPIGateway.Utilities.ShowMessage("Torch", $"Sending message of type {m.GetType().Name}");
|
||||
|
||||
var b = MyAPIGateway.Utilities.SerializeToBinary(m);
|
||||
m.CompressedData = MyCompression.Compress(b);
|
||||
|
||||
MyAPIGateway.Utilities.InvokeOnGameThread(() =>
|
||||
{
|
||||
|
||||
switch (m.TargetType)
|
||||
{
|
||||
case MessageTarget.Single:
|
||||
@@ -107,6 +124,7 @@ namespace Torch.Mod
|
||||
continue;
|
||||
MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId);
|
||||
}
|
||||
|
||||
break;
|
||||
case MessageTarget.AllExcept:
|
||||
MyAPIGateway.Players.GetPlayers(_playerCache);
|
||||
@@ -116,12 +134,13 @@ namespace Torch.Mod
|
||||
continue;
|
||||
MyAPIGateway.Multiplayer.SendMessageTo(NET_ID, m.CompressedData, p.SteamUserId);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
_playerCache.Clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -130,7 +149,7 @@ namespace Torch.Mod
|
||||
}
|
||||
}
|
||||
|
||||
MyLog.Default.WriteLineAndConsole("TORCH MOD: COMMUNICATION THREAD: EXIT SIGNAL RECEIVED!");
|
||||
MyLog.Default.WriteLineAndConsole("TORCH MOD: INFO: Communication thread shut down successfully! THIS IS NOT AN ERROR");
|
||||
//exit signal received. Clean everything and GTFO
|
||||
_processing?.Dispose();
|
||||
_processing = null;
|
||||
|
@@ -10,6 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Messages\IncomingMessage.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Messages\JoinServerMessage.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Messages\NotificationMessage.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Messages\DialogMessage.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Messages\MessageBase.cs" />
|
||||
|
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Sandbox.ModAPI;
|
||||
using VRage.Game.Components;
|
||||
|
||||
namespace Torch.Mod
|
||||
@@ -12,6 +13,7 @@ namespace Torch.Mod
|
||||
{
|
||||
public const ulong MOD_ID = 1406994352;
|
||||
private static bool _init;
|
||||
public static bool Debug;
|
||||
|
||||
public override void UpdateAfterSimulation()
|
||||
{
|
||||
@@ -20,12 +22,24 @@ namespace Torch.Mod
|
||||
|
||||
_init = true;
|
||||
ModCommunication.Register();
|
||||
MyAPIGateway.Utilities.MessageEntered += Utilities_MessageEntered;
|
||||
}
|
||||
|
||||
private void Utilities_MessageEntered(string messageText, ref bool sendToOthers)
|
||||
{
|
||||
if (messageText == "@!debug")
|
||||
{
|
||||
Debug = !Debug;
|
||||
MyAPIGateway.Utilities.ShowMessage("Torch", $"Debug: {Debug}");
|
||||
sendToOthers = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnloadData()
|
||||
{
|
||||
try
|
||||
{
|
||||
MyAPIGateway.Utilities.MessageEntered -= Utilities_MessageEntered;
|
||||
ModCommunication.Unregister();
|
||||
}
|
||||
catch
|
||||
|
@@ -92,6 +92,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
11
Torch.Server.Tests/app.config
Normal file
11
Torch.Server.Tests/app.config
Normal 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>
|
@@ -16,7 +16,6 @@ using NLog.Targets;
|
||||
using Sandbox.Engine.Utils;
|
||||
using Torch.Utils;
|
||||
using VRage.FileSystem;
|
||||
using VRage.Library.Exceptions;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
@@ -55,6 +54,15 @@ quit";
|
||||
AppDomain.CurrentDomain.UnhandledException += HandleException;
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
//enables logging debug messages when built in debug mode. Amazing.
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "main");
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "console");
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Debug, "wpf");
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
Log.Debug("Debug logging enabled.");
|
||||
#endif
|
||||
|
||||
// This is what happens when Keen is bad and puts extensions into the System namespace.
|
||||
if (!Enumerable.Contains(args, "-noupdate"))
|
||||
RunSteamCmd();
|
||||
@@ -64,7 +72,27 @@ quit";
|
||||
var apiTarget = Path.Combine(basePath, "steam_api64.dll");
|
||||
|
||||
if (!File.Exists(apiTarget))
|
||||
{
|
||||
File.Copy(apiSource, apiTarget);
|
||||
}
|
||||
else if (File.GetLastWriteTime(apiTarget) < File.GetLastWriteTime(apiSource))
|
||||
{
|
||||
File.Delete(apiTarget);
|
||||
File.Copy(apiSource, apiTarget);
|
||||
}
|
||||
|
||||
var havokSource = Path.Combine(basePath, "DedicatedServer64", "Havok.dll");
|
||||
var havokTarget = Path.Combine(basePath, "Havok.dll");
|
||||
|
||||
if (!File.Exists(havokTarget))
|
||||
{
|
||||
File.Copy(havokSource, havokTarget);
|
||||
}
|
||||
else if (File.GetLastWriteTime(havokTarget) < File.GetLastWriteTime(havokSource))
|
||||
{
|
||||
File.Delete(havokTarget);
|
||||
File.Copy(havokSource, havokTarget);
|
||||
}
|
||||
|
||||
_config = InitConfig();
|
||||
if (!_config.Parse(args))
|
||||
@@ -97,28 +125,34 @@ quit";
|
||||
public void Run()
|
||||
{
|
||||
_server = new TorchServer(_config);
|
||||
var init = Task.Run(() => _server.Init()).ContinueWith(x =>
|
||||
{
|
||||
if (!x.IsFaulted)
|
||||
return;
|
||||
|
||||
Log.Error("Error initializing server.");
|
||||
LogException(x.Exception);
|
||||
});
|
||||
if (!_config.NoGui)
|
||||
if (_config.NoGui)
|
||||
{
|
||||
if (_config.Autostart)
|
||||
init.ContinueWith(x => _server.Start());
|
||||
|
||||
Log.Info("Showing UI");
|
||||
Console.SetOut(TextWriter.Null);
|
||||
NativeMethods.FreeConsole();
|
||||
new TorchUI(_server).ShowDialog();
|
||||
_server.Init();
|
||||
_server.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
init.Wait();
|
||||
#if !DEBUG
|
||||
if (!_config.IndependentConsole)
|
||||
{
|
||||
Console.SetOut(TextWriter.Null);
|
||||
NativeMethods.FreeConsole();
|
||||
}
|
||||
#endif
|
||||
|
||||
var gameThread = new Thread(() =>
|
||||
{
|
||||
_server.Init();
|
||||
|
||||
if (_config.Autostart)
|
||||
_server.Start();
|
||||
});
|
||||
|
||||
gameThread.Start();
|
||||
|
||||
var ui = new TorchUI(_server);
|
||||
ui.ShowDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +198,10 @@ quit";
|
||||
File.Delete(STEAMCMD_ZIP);
|
||||
log.Info("SteamCMD downloaded successfully!");
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error("Failed to download SteamCMD, unable to update the DS.");
|
||||
log.Error(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -184,39 +219,49 @@ quit";
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
while (!cmd.HasExited)
|
||||
{
|
||||
log.Info(cmd.StandardOutput.ReadLine());
|
||||
log.Info(cmd.StandardOutput.ReadToEnd());
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogException(Exception ex)
|
||||
{
|
||||
if (ex.InnerException != null)
|
||||
if (ex is AggregateException ag)
|
||||
{
|
||||
LogException(ex.InnerException);
|
||||
foreach (var e in ag.InnerExceptions)
|
||||
LogException(e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Fatal(ex);
|
||||
|
||||
if (ex is ReflectionTypeLoadException exti)
|
||||
foreach (Exception exl in exti.LoaderExceptions)
|
||||
if (ex is ReflectionTypeLoadException extl)
|
||||
{
|
||||
foreach (var exl in extl.LoaderExceptions)
|
||||
LogException(exl);
|
||||
|
||||
if (ex is AggregateException ag)
|
||||
foreach (Exception e in ag.InnerExceptions)
|
||||
LogException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
LogException(ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
_server.FatalException = true;
|
||||
var ex = (Exception)e.ExceptionObject;
|
||||
LogException(ex);
|
||||
if (MyFakes.ENABLE_MINIDUMP_SENDING)
|
||||
{
|
||||
string path = Path.Combine(MyFileSystem.UserDataPath, "Minidump.dmp");
|
||||
Log.Info($"Generating minidump at {path}");
|
||||
MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo;
|
||||
MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present);
|
||||
Log.Error("Keen broke the minidump, sorry.");
|
||||
//MyMiniDump.Options options = MyMiniDump.Options.WithProcessThreadData | MyMiniDump.Options.WithThreadInfo;
|
||||
//MyMiniDump.Write(path, options, MyMiniDump.ExceptionInfo.Present);
|
||||
}
|
||||
LogManager.Flush();
|
||||
if (_config.RestartOnCrash)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -14,6 +15,7 @@ using Sandbox.Game;
|
||||
using Sandbox.Game.Gui;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Collections;
|
||||
using Torch.Managers;
|
||||
using Torch.Mod;
|
||||
using Torch.Server.ViewModels;
|
||||
@@ -68,10 +70,18 @@ namespace Torch.Server.Managers
|
||||
var worldFolders = Directory.EnumerateDirectories(Path.Combine(Torch.Config.InstancePath, "Saves"));
|
||||
|
||||
foreach (var f in worldFolders)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(f) && File.Exists(Path.Combine(f, "Sandbox.sbc")))
|
||||
DedicatedConfig.Worlds.Add(new WorldViewModel(f));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Failed to load world at path: " + f);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (DedicatedConfig.Worlds.Count == 0)
|
||||
{
|
||||
@@ -87,14 +97,32 @@ namespace Torch.Server.Managers
|
||||
public void SelectWorld(string worldPath, bool modsOnly = true)
|
||||
{
|
||||
DedicatedConfig.LoadWorld = worldPath;
|
||||
DedicatedConfig.SelectedWorld = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == worldPath);
|
||||
|
||||
var worldInfo = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == worldPath);
|
||||
try
|
||||
{
|
||||
if (worldInfo?.Checkpoint == null)
|
||||
{
|
||||
worldInfo = new WorldViewModel(worldPath);
|
||||
DedicatedConfig.Worlds.Add(worldInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Failed to load world at path: " + worldPath);
|
||||
DedicatedConfig.LoadWorld = null;
|
||||
return;
|
||||
}
|
||||
|
||||
DedicatedConfig.SelectedWorld = worldInfo;
|
||||
if (DedicatedConfig.SelectedWorld?.Checkpoint != null)
|
||||
{
|
||||
DedicatedConfig.Mods.Clear();
|
||||
//remove the Torch mod to avoid running multiple copies of it
|
||||
DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
|
||||
foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods)
|
||||
DedicatedConfig.Mods.Add(m.PublishedFileId);
|
||||
DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
|
||||
foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods)
|
||||
DedicatedConfig.Mods.Add(new ModItemInfo(m));
|
||||
Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +134,10 @@ namespace Torch.Server.Managers
|
||||
{
|
||||
DedicatedConfig.Mods.Clear();
|
||||
//remove the Torch mod to avoid running multiple copies of it
|
||||
DedicatedConfig.SelectedWorld.Checkpoint.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
|
||||
foreach (var m in DedicatedConfig.SelectedWorld.Checkpoint.Mods)
|
||||
DedicatedConfig.Mods.Add(m.PublishedFileId);
|
||||
DedicatedConfig.SelectedWorld.WorldConfiguration.Mods.RemoveAll(m => m.PublishedFileId == TorchModCore.MOD_ID);
|
||||
foreach (var m in DedicatedConfig.SelectedWorld.WorldConfiguration.Mods)
|
||||
DedicatedConfig.Mods.Add(new ModItemInfo(m));
|
||||
Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,17 +148,16 @@ namespace Torch.Server.Managers
|
||||
|
||||
private void ImportWorldConfig(WorldViewModel world, bool modsOnly = true)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var mod in world.Checkpoint.Mods)
|
||||
sb.AppendLine(mod.PublishedFileId.ToString());
|
||||
|
||||
DedicatedConfig.Mods = world.Checkpoint.Mods.Select(x => x.PublishedFileId).ToList();
|
||||
var mods = new MtObservableList<ModItemInfo>();
|
||||
foreach (var mod in world.WorldConfiguration.Mods)
|
||||
mods.Add(new ModItemInfo(mod));
|
||||
DedicatedConfig.Mods = mods;
|
||||
|
||||
|
||||
Log.Debug("Loaded mod list from world");
|
||||
|
||||
if (!modsOnly)
|
||||
DedicatedConfig.SessionSettings = world.Checkpoint.Settings;
|
||||
DedicatedConfig.SessionSettings = world.WorldConfiguration.Settings;
|
||||
}
|
||||
|
||||
private void ImportWorldConfig(bool modsOnly = true)
|
||||
@@ -151,7 +179,10 @@ namespace Torch.Server.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
DedicatedConfig.Mods = checkpoint.Mods.Select(x => x.PublishedFileId).ToList();
|
||||
var mods = new MtObservableList<ModItemInfo>();
|
||||
foreach (var mod in checkpoint.Mods)
|
||||
mods.Add(new ModItemInfo(mod));
|
||||
DedicatedConfig.Mods = mods;
|
||||
|
||||
Log.Debug("Loaded mod list from world");
|
||||
|
||||
@@ -167,29 +198,33 @@ namespace Torch.Server.Managers
|
||||
|
||||
public void SaveConfig()
|
||||
{
|
||||
if (((TorchServer)Torch).HasRun)
|
||||
{
|
||||
Log.Warn("Checkpoint cache is stale, not saving dedicated config.");
|
||||
return;
|
||||
}
|
||||
|
||||
DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
|
||||
Log.Info("Saved dedicated config.");
|
||||
|
||||
try
|
||||
{
|
||||
var sandboxPath = Path.Combine(DedicatedConfig.LoadWorld, "Sandbox.sbc");
|
||||
MyObjectBuilderSerializer.DeserializeXML(sandboxPath, out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes);
|
||||
if (checkpoint == null)
|
||||
var world = DedicatedConfig.Worlds.FirstOrDefault(x => x.WorldPath == DedicatedConfig.LoadWorld) ?? new WorldViewModel(DedicatedConfig.LoadWorld);
|
||||
|
||||
world.Checkpoint.SessionName = DedicatedConfig.WorldName;
|
||||
world.WorldConfiguration.Settings = DedicatedConfig.SessionSettings;
|
||||
world.WorldConfiguration.Mods.Clear();
|
||||
|
||||
foreach (var mod in DedicatedConfig.Mods)
|
||||
{
|
||||
Log.Error($"Failed to load {DedicatedConfig.LoadWorld}, checkpoint null ({sizeInBytes} bytes, instance {Torch.Config.InstancePath})");
|
||||
return;
|
||||
var savedMod = new MyObjectBuilder_Checkpoint.ModItem(mod.Name, mod.PublishedFileId, mod.FriendlyName);
|
||||
savedMod.IsDependency = mod.IsDependency;
|
||||
world.WorldConfiguration.Mods.Add(savedMod);
|
||||
}
|
||||
Task.Run(() => DedicatedConfig.UpdateAllModInfosAsync());
|
||||
|
||||
checkpoint.SessionName = DedicatedConfig.WorldName;
|
||||
checkpoint.Settings = DedicatedConfig.SessionSettings;
|
||||
checkpoint.Mods.Clear();
|
||||
world.SaveSandbox();
|
||||
|
||||
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.");
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -223,35 +258,63 @@ namespace Torch.Server.Managers
|
||||
public string WorldPath { get; }
|
||||
public long WorldSizeKB { get; }
|
||||
private string _checkpointPath;
|
||||
private string _worldConfigPath;
|
||||
public CheckpointViewModel Checkpoint { get; private set; }
|
||||
|
||||
public WorldConfigurationViewModel WorldConfiguration { get; private set; }
|
||||
|
||||
public WorldViewModel(string worldPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
WorldPath = worldPath;
|
||||
WorldSizeKB = new DirectoryInfo(worldPath).GetFiles().Sum(x => x.Length) / 1024;
|
||||
_checkpointPath = Path.Combine(WorldPath, "Sandbox.sbc");
|
||||
_worldConfigPath = Path.Combine(WorldPath, "Sandbox_config.sbc");
|
||||
FolderName = Path.GetFileName(worldPath);
|
||||
BeginLoadCheckpoint();
|
||||
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()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
public void SaveSandbox()
|
||||
{
|
||||
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(() =>
|
||||
{
|
||||
Log.Info($"Preloading checkpoint {_checkpointPath}");
|
||||
MyObjectBuilderSerializer.DeserializeXML(_checkpointPath, out MyObjectBuilder_Checkpoint checkpoint);
|
||||
Checkpoint = new CheckpointViewModel(checkpoint);
|
||||
|
||||
// migrate old saves
|
||||
if (File.Exists(_worldConfigPath))
|
||||
{
|
||||
MyObjectBuilderSerializer.DeserializeXML(_worldConfigPath, out MyObjectBuilder_WorldConfiguration worldConfig);
|
||||
WorldConfiguration = new WorldConfigurationViewModel(worldConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
WorldConfiguration = new WorldConfigurationViewModel(new MyObjectBuilder_WorldConfiguration
|
||||
{
|
||||
Mods = checkpoint.Mods,
|
||||
Settings = checkpoint.Settings
|
||||
});
|
||||
|
||||
checkpoint.Mods = null;
|
||||
checkpoint.Settings = null;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Checkpoint));
|
||||
}//);
|
||||
OnPropertyChanged(nameof(WorldConfiguration));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -209,26 +209,21 @@ namespace Torch.Server.Managers
|
||||
//Largely copied from SE
|
||||
private void ValidateAuthTicketResponse(ulong steamId, JoinResult response, ulong steamOwner)
|
||||
{
|
||||
//SteamNetworking.GetP2PSessionState(new CSteamID(steamId), out P2PSessionState_t state);
|
||||
//state.GetRemoteIP();
|
||||
MyP2PSessionState statehack = new MyP2PSessionState();
|
||||
VRage.Steam.MySteamService.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
|
||||
var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray());
|
||||
var state = new MyP2PSessionState();
|
||||
MySteamServiceWrapper.Static.Peer2Peer.GetSessionState(steamId, ref state);
|
||||
var ip = new IPAddress(BitConverter.GetBytes(state.RemoteIP).Reverse().ToArray());
|
||||
|
||||
Torch.CurrentSession.KeenSession.PromotedUsers.TryGetValue(steamId, out MyPromoteLevel promoteLevel);
|
||||
|
||||
_log.Debug($"ValidateAuthTicketResponse(user={steamId}, response={response}, owner={steamOwner}, permissions={promoteLevel})");
|
||||
|
||||
_log.Info($"Connection attempt by {steamId} from {ip}");
|
||||
// TODO implement IP bans
|
||||
var config = (TorchConfig) Torch.Config;
|
||||
if (config.EnableWhitelist && !config.Whitelist.Contains(steamId))
|
||||
|
||||
if (Players.ContainsKey(steamId))
|
||||
{
|
||||
_log.Warn($"Rejecting user {steamId} because they are not whitelisted in Torch.cfg.");
|
||||
UserRejected(steamId, JoinResult.NotInGroup);
|
||||
_log.Warn($"Player {steamId} has already joined!");
|
||||
UserRejected(steamId, JoinResult.AlreadyJoined);
|
||||
}
|
||||
else if(config.EnableReservedSlots && config.ReservedPlayers.Contains(steamId))
|
||||
UserAccepted(steamId);
|
||||
else if (Torch.CurrentSession.KeenSession.OnlineMode == MyOnlineModeEnum.OFFLINE &&
|
||||
promoteLevel < MyPromoteLevel.Admin)
|
||||
{
|
||||
@@ -252,40 +247,48 @@ namespace Torch.Server.Managers
|
||||
|
||||
private void RunEvent(ValidateAuthTicketEvent info)
|
||||
{
|
||||
MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info);
|
||||
JoinResult internalAuth;
|
||||
|
||||
|
||||
if (info.FutureVerdict == null)
|
||||
{
|
||||
if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID))
|
||||
CommitVerdict(info.SteamID, JoinResult.BannedByAdmins);
|
||||
internalAuth = JoinResult.BannedByAdmins;
|
||||
else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) ||
|
||||
_isClientKicked(MyMultiplayer.Static, info.SteamOwner))
|
||||
CommitVerdict(info.SteamID, JoinResult.KickedRecently);
|
||||
internalAuth = JoinResult.KickedRecently;
|
||||
else if (info.SteamResponse == JoinResult.OK)
|
||||
{
|
||||
var config = (TorchConfig) Torch.Config;
|
||||
if (config.EnableWhitelist && !config.Whitelist.Contains(info.SteamID))
|
||||
{
|
||||
_log.Warn($"Rejecting user {info.SteamID} because they are not whitelisted in Torch.cfg.");
|
||||
internalAuth = JoinResult.NotInGroup;
|
||||
}
|
||||
else if (MySandboxGame.ConfigDedicated.Reserved.Contains(info.SteamID))
|
||||
internalAuth = JoinResult.OK;
|
||||
//Admins can bypass member limit
|
||||
if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) ||
|
||||
else if (MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) ||
|
||||
MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID)))
|
||||
CommitVerdict(info.SteamID, JoinResult.OK);
|
||||
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)
|
||||
CommitVerdict(info.SteamID, JoinResult.ServerFull);
|
||||
internalAuth = JoinResult.ServerFull;
|
||||
else if (MySandboxGame.ConfigDedicated.GroupID == 0uL)
|
||||
CommitVerdict(info.SteamID, JoinResult.OK);
|
||||
internalAuth = JoinResult.OK;
|
||||
else
|
||||
{
|
||||
if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer))
|
||||
CommitVerdict(info.SteamID, JoinResult.OK);
|
||||
internalAuth = JoinResult.OK;
|
||||
else
|
||||
CommitVerdict(info.SteamID, JoinResult.NotInGroup);
|
||||
internalAuth = JoinResult.NotInGroup;
|
||||
}
|
||||
}
|
||||
else
|
||||
CommitVerdict(info.SteamID, info.SteamResponse);
|
||||
internalAuth = info.SteamResponse;
|
||||
|
||||
return;
|
||||
}
|
||||
info.FutureVerdict = Task.FromResult(internalAuth);
|
||||
|
||||
MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info);
|
||||
|
||||
info.FutureVerdict.ContinueWith((task) =>
|
||||
{
|
||||
|
40
Torch.Server/Managers/RemoteAPIManager.cs
Normal file
40
Torch.Server/Managers/RemoteAPIManager.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
Torch.Server/Patches/WorldLoadExceptionPatch.cs
Normal file
48
Torch.Server/Patches/WorldLoadExceptionPatch.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,12 +2,15 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.ServiceProcess;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.VisualBasic.Devices;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
using NLog.Targets;
|
||||
using Torch.Utils;
|
||||
|
||||
@@ -21,21 +24,46 @@ namespace Torch.Server
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
||||
|
||||
Target.Register<FlowDocumentTarget>("FlowDocument");
|
||||
//Ensures that all the files are downloaded in the Torch directory.
|
||||
var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString();
|
||||
var binDir = Path.Combine(workingDir, "DedicatedServer64");
|
||||
Directory.SetCurrentDirectory(workingDir);
|
||||
|
||||
//HACK for block skins update
|
||||
var badDlls = new[]
|
||||
{
|
||||
"System.Security.Principal.Windows.dll",
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in badDlls)
|
||||
{
|
||||
if (File.Exists(file))
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var log = LogManager.GetCurrentClassLogger();
|
||||
log.Error($"Error updating. Please delete the following files from the Torch root folder manually:\r\n{string.Join("\r\n", badDlls)}");
|
||||
log.Error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TorchLauncher.IsTorchWrapped())
|
||||
{
|
||||
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName, args, binDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Environment.UserInteractive)
|
||||
// Breaks on Windows Server 2019
|
||||
if (!new ComputerInfo().OSFullName.Contains("Server 2019") && !Environment.UserInteractive)
|
||||
{
|
||||
using (var service = new TorchService())
|
||||
using (var service = new TorchService(args))
|
||||
ServiceBase.Run(service);
|
||||
return;
|
||||
}
|
||||
|
@@ -70,6 +70,9 @@
|
||||
<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>
|
||||
</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">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.dll</HintPath>
|
||||
@@ -80,13 +83,25 @@
|
||||
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Reference Include="Microsoft.VisualBasic" />
|
||||
<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 Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</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">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath>
|
||||
@@ -121,11 +136,24 @@
|
||||
<HintPath>..\GameBinaries\Steamworks.NET.dll</HintPath>
|
||||
</Reference>
|
||||
<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.Configuration" />
|
||||
<Reference Include="System.Configuration.Install" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<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.Windows.Forms" />
|
||||
<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>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="VRage.Native, Version=0.0.0.0, Culture=neutral, processorArchitecture=AMD64">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<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 Include="VRage.Platform.Windows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
|
||||
<HintPath>..\GameBinaries\VRage.Platform.Windows.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="VRage.Render, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
@@ -225,9 +246,11 @@
|
||||
<Compile Include="Managers\InstanceManager.cs" />
|
||||
<Compile Include="Managers\MultiplayerManagerDedicatedEventShim.cs" />
|
||||
<Compile Include="Managers\MultiplayerManagerDedicatedPatchShim.cs" />
|
||||
<Compile Include="Managers\RemoteAPIManager.cs" />
|
||||
<Compile Include="NativeMethods.cs" />
|
||||
<Compile Include="Initializer.cs" />
|
||||
<Compile Include="Patches\PromotePatch.cs" />
|
||||
<Compile Include="Patches\WorldLoadExceptionPatch.cs" />
|
||||
<Compile Include="Properties\Annotations.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TorchConfig.cs" />
|
||||
@@ -245,15 +268,29 @@
|
||||
<Compile Include="ViewModels\Entities\CharacterViewModel.cs" />
|
||||
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
|
||||
<Compile Include="ViewModels\Entities\EntityControlViewModel.cs" />
|
||||
<Compile Include="ViewModels\ModItemInfo.cs" />
|
||||
<Compile Include="ViewModels\SessionSettingsViewModel.cs" />
|
||||
<Compile Include="ViewModels\WorldConfigurationViewModel.cs" />
|
||||
<Compile Include="Views\Converters\DefinitionToIdConverter.cs" />
|
||||
<Compile Include="Views\Converters\BooleanAndConverter.cs" />
|
||||
<Compile Include="Views\Converters\ListConverter.cs" />
|
||||
<Compile Include="MultiTextWriter.cs" />
|
||||
<Compile Include="RichTextBoxWriter.cs" />
|
||||
<Compile Include="Views\Converters\ListConverterWorkshopId.cs" />
|
||||
<Compile Include="Views\Converters\ModToIdConverter.cs" />
|
||||
<Compile Include="Views\Entities\CharacterView.xaml.cs">
|
||||
<DependentUpon>CharacterView.xaml</DependentUpon>
|
||||
</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">
|
||||
<DependentUpon>ThemeControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -414,6 +451,14 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</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">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -430,6 +475,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\RoleEditor.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\SessionSettingsView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -469,9 +518,10 @@
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)"</PostBuildEvent>
|
||||
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)" & copy "$(SolutionDir)NLog-user.config" "$(TargetDir)"</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
@@ -5,6 +5,7 @@ using System.Windows;
|
||||
using System.Xml.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
@@ -20,9 +21,38 @@ namespace Torch.Server
|
||||
[Arg("instancename", "The name of the Torch instance.")]
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
|
||||
private string _instancePath;
|
||||
|
||||
/// <inheritdoc />
|
||||
[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 />
|
||||
[XmlIgnore, Arg("noupdate", "Disable automatically downloading game and plugin updates.")]
|
||||
@@ -58,18 +88,37 @@ namespace Torch.Server
|
||||
public int TickTimeout { get; set; } = 60;
|
||||
|
||||
/// <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 HashSet<ulong> Whitelist { get; set; } = new HashSet<ulong>();
|
||||
|
||||
internal Point WindowSize { get; set; } = new Point(800, 600);
|
||||
internal Point WindowPosition { get; set; } = new Point();
|
||||
public Point WindowSize { get; set; } = new Point(800, 600);
|
||||
public Point WindowPosition { get; set; } = new Point();
|
||||
|
||||
public string LastUsedTheme { get; set; } = "Torch Theme";
|
||||
|
||||
public bool EnableReservedSlots { get; set; } = false;
|
||||
public HashSet<ulong> ReservedPlayers { get; set; } = new HashSet<ulong>();
|
||||
//Prevent reserved players being written to disk, but allow it to be read
|
||||
//remove this when ReservedPlayers is removed
|
||||
private bool ShouldSerializeReservedPlayers() => false;
|
||||
|
||||
[Arg("console", "Keeps a separate console window open after the main UI loads.")]
|
||||
public bool IndependentConsole { get; set; } = false;
|
||||
|
||||
[XmlIgnore]
|
||||
[Arg("testplugin", "Path to a plugin to debug. For development use only.")]
|
||||
public string TestPlugin { get; set; }
|
||||
|
||||
[XmlIgnore]
|
||||
private string _path;
|
||||
|
@@ -19,6 +19,7 @@ using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Commands;
|
||||
using Torch.Mod;
|
||||
using Torch.Mod.Messages;
|
||||
using Torch.Server.Commands;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Utils;
|
||||
@@ -26,6 +27,7 @@ using VRage;
|
||||
using VRage.Dedicated;
|
||||
using VRage.Dedicated.RemoteAPI;
|
||||
using VRage.GameServices;
|
||||
using VRage.Scripting;
|
||||
using VRage.Steam;
|
||||
using Timer = System.Threading.Timer;
|
||||
|
||||
@@ -37,14 +39,18 @@ namespace Torch.Server
|
||||
{
|
||||
public class TorchServer : TorchBase, ITorchServer
|
||||
{
|
||||
private bool _hasRun;
|
||||
private bool _canRun;
|
||||
private TimeSpan _elapsedPlayTime;
|
||||
private bool _hasRun;
|
||||
private bool _isRunning;
|
||||
private float _simRatio;
|
||||
private ServerState _state;
|
||||
private Stopwatch _uptime;
|
||||
private Timer _watchdog;
|
||||
private int _players;
|
||||
private MultiplayerManagerDedicated _multiplayerManagerDedicated;
|
||||
|
||||
internal bool FatalException { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TorchServer(TorchConfig config = null)
|
||||
@@ -52,12 +58,15 @@ namespace Torch.Server
|
||||
DedicatedInstance = new InstanceManager(this);
|
||||
AddManager(DedicatedInstance);
|
||||
AddManager(new EntityControlManager(this));
|
||||
AddManager(new RemoteAPIManager(this));
|
||||
Config = config ?? new TorchConfig();
|
||||
|
||||
var sessionManager = Managers.GetManager<ITorchSessionManager>();
|
||||
sessionManager.AddFactory(x => new MultiplayerManagerDedicated(this));
|
||||
}
|
||||
|
||||
public bool HasRun { get => _hasRun; set => SetValue(ref _hasRun, value); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float SimulationRatio { get => _simRatio; set => SetValue(ref _simRatio, value); }
|
||||
|
||||
@@ -92,6 +101,8 @@ namespace Torch.Server
|
||||
/// <inheritdoc />
|
||||
public string InstancePath => Config?.InstancePath;
|
||||
|
||||
public int OnlinePlayers { get => _players; private set => SetValue(ref _players, value); }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init()
|
||||
{
|
||||
@@ -111,7 +122,7 @@ namespace Torch.Server
|
||||
if (State != ServerState.Stopped)
|
||||
return;
|
||||
|
||||
if (_hasRun)
|
||||
if (IsRunning || HasRun)
|
||||
{
|
||||
Restart();
|
||||
return;
|
||||
@@ -119,15 +130,10 @@ namespace Torch.Server
|
||||
|
||||
State = ServerState.Starting;
|
||||
IsRunning = true;
|
||||
HasRun = true;
|
||||
CanRun = false;
|
||||
_hasRun = true;
|
||||
Log.Info("Starting server.");
|
||||
MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model;
|
||||
if (MySandboxGame.ConfigDedicated.RemoteApiEnabled && !string.IsNullOrEmpty(MySandboxGame.ConfigDedicated.RemoteSecurityKey))
|
||||
{
|
||||
var myRemoteServer = new MyRemoteServer(MySandboxGame.ConfigDedicated.RemoteApiPort, MySandboxGame.ConfigDedicated.RemoteSecurityKey);
|
||||
Log.Info($"Remote API started on port {myRemoteServer.Port}");
|
||||
}
|
||||
|
||||
_uptime = Stopwatch.StartNew();
|
||||
base.Start();
|
||||
@@ -150,9 +156,15 @@ namespace Torch.Server
|
||||
/// <summary>
|
||||
/// Restart the program.
|
||||
/// </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);
|
||||
else
|
||||
DoRestart(null, this);
|
||||
@@ -186,6 +198,7 @@ namespace Torch.Server
|
||||
|
||||
if (newState == TorchSessionState.Loaded)
|
||||
{
|
||||
_multiplayerManagerDedicated = CurrentSession.Managers.GetManager<MultiplayerManagerDedicated>();
|
||||
CurrentSession.Managers.GetManager<CommandManager>().RegisterCommandModule(typeof(WhitelistCommands));
|
||||
ModCommunication.Register();
|
||||
}
|
||||
@@ -195,8 +208,7 @@ namespace Torch.Server
|
||||
public override void Init(object gameInstance)
|
||||
{
|
||||
base.Init(gameInstance);
|
||||
var game = gameInstance as MySandboxGame;
|
||||
if (game != null && MySession.Static != null)
|
||||
if (gameInstance is MySandboxGame && MySession.Static != null)
|
||||
State = ServerState.Running;
|
||||
else
|
||||
State = ServerState.Stopped;
|
||||
@@ -210,6 +222,7 @@ namespace Torch.Server
|
||||
SimulationRatio = Math.Min(Sync.ServerSimulationRatio, 1);
|
||||
var elapsed = TimeSpan.FromSeconds(Math.Floor(_uptime.Elapsed.TotalSeconds));
|
||||
ElapsedPlayTime = elapsed;
|
||||
OnlinePlayers = _multiplayerManagerDedicated?.Players.Count ?? 0;
|
||||
|
||||
if (_watchdog == null && Config.TickTimeout > 0)
|
||||
{
|
||||
@@ -223,10 +236,16 @@ namespace Torch.Server
|
||||
|
||||
private static void CheckServerResponding(object state)
|
||||
{
|
||||
var server = (TorchServer)state;
|
||||
var mre = new ManualResetEvent(false);
|
||||
((TorchServer)state).Invoke(() => mre.Set());
|
||||
server.Invoke(() => mre.Set());
|
||||
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
|
||||
{
|
||||
if (server.FatalException)
|
||||
{
|
||||
server._watchdog.Dispose();
|
||||
return;
|
||||
}
|
||||
#if DEBUG
|
||||
Log.Error(
|
||||
$"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds.");
|
||||
|
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.ServiceProcess;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
|
||||
@@ -12,12 +14,14 @@ namespace Torch.Server
|
||||
{
|
||||
class TorchService : ServiceBase
|
||||
{
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
public const string Name = "Torch (SEDS)";
|
||||
private TorchServer _server;
|
||||
private Initializer _initializer;
|
||||
private string[] _args;
|
||||
|
||||
public TorchService()
|
||||
public TorchService(string[] args)
|
||||
{
|
||||
_args = args;
|
||||
var workingDir = new FileInfo(typeof(TorchService).Assembly.Location).Directory.ToString();
|
||||
Directory.SetCurrentDirectory(workingDir);
|
||||
_initializer = new Initializer(workingDir);
|
||||
@@ -29,19 +33,21 @@ namespace Torch.Server
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnStop()
|
||||
{
|
||||
_server.Stop();
|
||||
base.OnStop();
|
||||
var mre = new ManualResetEvent(false);
|
||||
Task.Run(() => _initializer.Server.Stop());
|
||||
if (!mre.WaitOne(TimeSpan.FromMinutes(1)))
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,12 +16,12 @@ namespace Torch.Server.ViewModels
|
||||
public class CheckpointViewModel : ViewModel
|
||||
{
|
||||
private MyObjectBuilder_Checkpoint _checkpoint;
|
||||
private SessionSettingsViewModel _sessionSettings;
|
||||
//private SessionSettingsViewModel _sessionSettings;
|
||||
|
||||
public CheckpointViewModel(MyObjectBuilder_Checkpoint checkpoint)
|
||||
{
|
||||
_checkpoint = checkpoint;
|
||||
_sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings);
|
||||
//_sessionSettings = new SessionSettingsViewModel(_checkpoint.Settings);
|
||||
}
|
||||
|
||||
public static implicit operator MyObjectBuilder_Checkpoint(CheckpointViewModel model)
|
||||
@@ -59,15 +59,15 @@ namespace Torch.Server.ViewModels
|
||||
|
||||
public SerializableDictionary<long, MyObjectBuilder_Checkpoint.PlayerId> ControlledEntities { get => _checkpoint.ControlledEntities; set => SetValue(ref _checkpoint.ControlledEntities, value); }
|
||||
|
||||
public SessionSettingsViewModel Settings
|
||||
{
|
||||
get => _sessionSettings;
|
||||
set
|
||||
{
|
||||
SetValue(ref _sessionSettings, value);
|
||||
_checkpoint.Settings = _sessionSettings;
|
||||
}
|
||||
}
|
||||
//public SessionSettingsViewModel Settings
|
||||
//{
|
||||
// get => _sessionSettings;
|
||||
// set
|
||||
// {
|
||||
// SetValue(ref _sessionSettings, value);
|
||||
// _checkpoint.Settings = _sessionSettings;
|
||||
// }
|
||||
//}
|
||||
|
||||
public MyObjectBuilder_ScriptManager ScriptManagerData => throw new NotImplementedException();
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace Torch.Server.ViewModels
|
||||
|
||||
public MyObjectBuilder_FactionCollection Factions => throw new NotImplementedException();
|
||||
|
||||
public List<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); }
|
||||
|
||||
|
@@ -10,6 +10,8 @@ using Torch.Collections;
|
||||
using Torch.Server.Managers;
|
||||
using VRage.Game;
|
||||
using VRage.Game.ModAPI;
|
||||
using Torch.Utils.SteamWorkshopTools;
|
||||
using Torch.Collections;
|
||||
|
||||
namespace Torch.Server.ViewModels
|
||||
{
|
||||
@@ -27,8 +29,9 @@ namespace Torch.Server.ViewModels
|
||||
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
|
||||
{
|
||||
_config = configDedicated;
|
||||
_config.IgnoreLastSession = true;
|
||||
//_config.IgnoreLastSession = true;
|
||||
SessionSettings = new SessionSettingsViewModel(_config.SessionSettings);
|
||||
Task.Run(() => UpdateAllModInfosAsync());
|
||||
}
|
||||
|
||||
public void Save(string path = null)
|
||||
@@ -37,7 +40,7 @@ namespace Torch.Server.ViewModels
|
||||
|
||||
_config.SessionSettings = _sessionSettings;
|
||||
// Never ever
|
||||
_config.IgnoreLastSession = true;
|
||||
//_config.IgnoreLastSession = true;
|
||||
_config.Save(path);
|
||||
}
|
||||
|
||||
@@ -73,12 +76,61 @@ namespace Torch.Server.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAllModInfosAsync(Action<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<ulong> Banned { get => _config.Banned; set => SetValue(x => _config.Banned = x, value); }
|
||||
|
||||
private List<ulong> _mods = new List<ulong>();
|
||||
public List<ulong> Mods { get => _mods; set => SetValue(x => _mods = x, value); }
|
||||
private MtObservableList<ModItemInfo> _mods = new MtObservableList<ModItemInfo>();
|
||||
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); }
|
||||
|
||||
@@ -90,8 +142,12 @@ namespace Torch.Server.ViewModels
|
||||
|
||||
public string ServerName { get => _config.ServerName; set => SetValue(x => _config.ServerName = x, value); }
|
||||
|
||||
public string ServerDescription { get => _config.ServerDescription; set => SetValue(x => _config.ServerDescription = x, value); }
|
||||
|
||||
public bool PauseGameWhenEmpty { get => _config.PauseGameWhenEmpty; set => SetValue(x => _config.PauseGameWhenEmpty = x, value); }
|
||||
|
||||
public bool AutodetectDependencies { get => _config.AutodetectDependencies; set => SetValue(x => _config.AutodetectDependencies = x, value); }
|
||||
|
||||
public string PremadeCheckpointPath { get => _config.PremadeCheckpointPath; set => SetValue(x => _config.PremadeCheckpointPath = x, value); }
|
||||
|
||||
public string LoadWorld { get => _config.LoadWorld; set => SetValue(x => _config.LoadWorld = x, value); }
|
||||
|
@@ -4,14 +4,24 @@ namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
public class CharacterViewModel : EntityViewModel
|
||||
{
|
||||
private MyCharacter _character;
|
||||
public CharacterViewModel(MyCharacter character, EntityTreeViewModel tree) : base(character, tree)
|
||||
{
|
||||
character.ControllerInfo.ControlAcquired += (x) => { OnPropertyChanged(nameof(Name)); };
|
||||
character.ControllerInfo.ControlReleased += (x) => { OnPropertyChanged(nameof(Name)); };
|
||||
_character = character;
|
||||
character.ControllerInfo.ControlAcquired += ControllerInfo_ControlAcquired;
|
||||
character.ControllerInfo.ControlReleased += ControllerInfo_ControlAcquired;
|
||||
}
|
||||
|
||||
private void ControllerInfo_ControlAcquired(Sandbox.Game.World.MyEntityController obj)
|
||||
{
|
||||
OnPropertyChanged(nameof(Name));
|
||||
OnPropertyChanged(nameof(CanDelete));
|
||||
}
|
||||
|
||||
public CharacterViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public override bool CanDelete => _character.ControllerInfo?.Controller?.Player == null;
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,14 @@
|
||||
using System.Windows.Controls;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Controls;
|
||||
using NLog;
|
||||
using Sandbox.Game.Entities;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Utils;
|
||||
using VRage.Game.Entity;
|
||||
using VRage.Game.ModAPI;
|
||||
using VRage.ModAPI;
|
||||
@@ -14,6 +20,8 @@ namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
protected EntityTreeViewModel Tree { get; }
|
||||
|
||||
private static Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private IMyEntity _backing;
|
||||
public IMyEntity Entity
|
||||
{
|
||||
@@ -43,6 +51,86 @@ namespace Torch.Server.ViewModels.Entities
|
||||
}
|
||||
}
|
||||
|
||||
private string _descriptiveName;
|
||||
public string DescriptiveName
|
||||
{
|
||||
get => _descriptiveName ?? (_descriptiveName = GetSortedName(EntityTreeViewModel.SortEnum.Name));
|
||||
set => _descriptiveName = value;
|
||||
}
|
||||
|
||||
public virtual string GetSortedName(EntityTreeViewModel.SortEnum sort)
|
||||
{
|
||||
switch (sort)
|
||||
{
|
||||
case EntityTreeViewModel.SortEnum.Name:
|
||||
return Name;
|
||||
case EntityTreeViewModel.SortEnum.Size:
|
||||
return $"{Name} ({Entity.WorldVolume.Radius * 2:N}m)";
|
||||
case EntityTreeViewModel.SortEnum.Speed:
|
||||
return $"{Name} ({Entity.Physics?.LinearVelocity.Length() ?? 0:N}m/s)";
|
||||
case EntityTreeViewModel.SortEnum.BlockCount:
|
||||
if (Entity is MyCubeGrid grid)
|
||||
return $"{Name} ({grid.BlocksCount} blocks)";
|
||||
return Name;
|
||||
case EntityTreeViewModel.SortEnum.DistFromCenter:
|
||||
return $"{Name} ({Entity.GetPosition().Length():N}m)";
|
||||
case EntityTreeViewModel.SortEnum.Owner:
|
||||
if (Entity is MyCubeGrid g)
|
||||
return $"{Name} ({g.GetGridOwnerName()})";
|
||||
return Name;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(sort), sort, null);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual int CompareToSort(EntityViewModel other, EntityTreeViewModel.SortEnum sort)
|
||||
{
|
||||
if (other == null)
|
||||
return -1;
|
||||
|
||||
switch (sort)
|
||||
{
|
||||
case EntityTreeViewModel.SortEnum.Name:
|
||||
if (Name == null)
|
||||
{
|
||||
if (other.Name == null)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
if (other.Name == null)
|
||||
return -1;
|
||||
return string.Compare(Name, other.Name, StringComparison.InvariantCultureIgnoreCase);
|
||||
case EntityTreeViewModel.SortEnum.Size:
|
||||
return Entity.WorldVolume.Radius.CompareTo(other.Entity.WorldVolume.Radius);
|
||||
case EntityTreeViewModel.SortEnum.Speed:
|
||||
if (Entity.Physics == null)
|
||||
{
|
||||
if (other.Entity.Physics == null)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
if (other.Entity.Physics == null)
|
||||
return 1;
|
||||
return Entity.Physics.LinearVelocity.LengthSquared().CompareTo(other.Entity.Physics.LinearVelocity.LengthSquared());
|
||||
case EntityTreeViewModel.SortEnum.BlockCount:
|
||||
{
|
||||
if (Entity is MyCubeGrid ga && other.Entity is MyCubeGrid gb)
|
||||
return ga.BlocksCount.CompareTo(gb.BlocksCount);
|
||||
goto case EntityTreeViewModel.SortEnum.Name;
|
||||
}
|
||||
case EntityTreeViewModel.SortEnum.DistFromCenter:
|
||||
return Entity.GetPosition().LengthSquared().CompareTo(other.Entity.GetPosition().LengthSquared());
|
||||
case EntityTreeViewModel.SortEnum.Owner:
|
||||
{
|
||||
if (Entity is MyCubeGrid ga && other.Entity is MyCubeGrid gb)
|
||||
return string.Compare(ga.GetGridOwnerName(), gb.GetGridOwnerName(), StringComparison.InvariantCultureIgnoreCase);
|
||||
goto case EntityTreeViewModel.SortEnum.Name;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(sort), sort, null);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Position
|
||||
{
|
||||
get => Entity?.GetPosition().ToString();
|
||||
@@ -59,7 +147,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
|
||||
public virtual bool CanStop => Entity.Physics?.Enabled ?? false;
|
||||
|
||||
public virtual bool CanDelete => !(Entity is IMyCharacter);
|
||||
public virtual bool CanDelete => true;
|
||||
|
||||
public virtual void Delete()
|
||||
{
|
||||
@@ -76,5 +164,20 @@ namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class Comparer : IComparer<EntityViewModel>
|
||||
{
|
||||
private EntityTreeViewModel.SortEnum _sort;
|
||||
|
||||
public Comparer(EntityTreeViewModel.SortEnum sort)
|
||||
{
|
||||
_sort = sort;
|
||||
}
|
||||
|
||||
public int Compare(EntityViewModel x, EntityViewModel y)
|
||||
{
|
||||
return x.CompareToSort(y, _sort);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -51,16 +51,13 @@ namespace Torch.Server.ViewModels.Entities
|
||||
new MtObservableSortedDictionary<MyCubeBlockDefinition, MtObservableSortedDictionary<long, BlockViewModel>>(
|
||||
CubeBlockDefinitionComparer.Default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DescriptiveName { get; }
|
||||
|
||||
public GridViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public GridViewModel(MyCubeGrid grid, EntityTreeViewModel tree) : base(grid, tree)
|
||||
{
|
||||
DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)";
|
||||
//DescriptiveName = $"{grid.DisplayName} ({grid.BlocksCount} blocks)";
|
||||
Blocks.Add(_fillerDefinition, new MtObservableSortedDictionary<long, BlockViewModel>());
|
||||
}
|
||||
|
||||
|
@@ -12,11 +12,21 @@ using VRage.ModAPI;
|
||||
using System.Windows.Threading;
|
||||
using NLog;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.Views.Entities;
|
||||
|
||||
namespace Torch.Server.ViewModels
|
||||
{
|
||||
public class EntityTreeViewModel : ViewModel
|
||||
{
|
||||
public enum SortEnum
|
||||
{
|
||||
Name,
|
||||
Size,
|
||||
Speed,
|
||||
Owner,
|
||||
BlockCount,
|
||||
DistFromCenter,
|
||||
}
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
//TODO: these should be sorted sets for speed
|
||||
@@ -26,7 +36,13 @@ namespace Torch.Server.ViewModels
|
||||
public MtObservableSortedDictionary<long, VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableSortedDictionary<long, VoxelMapViewModel>();
|
||||
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 SortEnum _currentSort;
|
||||
private UserControl _control;
|
||||
|
||||
public EntityViewModel CurrentEntity
|
||||
@@ -35,6 +51,12 @@ namespace Torch.Server.ViewModels
|
||||
set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); }
|
||||
}
|
||||
|
||||
public SortEnum CurrentSort
|
||||
{
|
||||
get => _currentSort;
|
||||
set => SetValue(ref _currentSort, value);
|
||||
}
|
||||
|
||||
// I hate you today WPF
|
||||
public EntityTreeViewModel() : this(null)
|
||||
{
|
||||
@@ -43,6 +65,11 @@ namespace Torch.Server.ViewModels
|
||||
public EntityTreeViewModel(UserControl control)
|
||||
{
|
||||
_control = control;
|
||||
var comparer = new EntityViewModel.Comparer(_currentSort);
|
||||
SortedGrids = new SortedView<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()
|
||||
@@ -85,16 +112,16 @@ namespace Torch.Server.ViewModels
|
||||
switch (obj)
|
||||
{
|
||||
case MyCubeGrid grid:
|
||||
Grids.Add(obj.EntityId, new GridViewModel(grid, this));
|
||||
Grids.Add(grid.EntityId, new GridViewModel(grid, this));
|
||||
break;
|
||||
case MyCharacter character:
|
||||
Characters.Add(obj.EntityId, new CharacterViewModel(character, this));
|
||||
Characters.Add(character.EntityId, new CharacterViewModel(character, this));
|
||||
break;
|
||||
case MyFloatingObject floating:
|
||||
FloatingObjects.Add(obj.EntityId, new FloatingObjectViewModel(floating, this));
|
||||
FloatingObjects.Add(floating.EntityId, new FloatingObjectViewModel(floating, this));
|
||||
break;
|
||||
case MyVoxelBase voxel:
|
||||
VoxelMaps.Add(obj.EntityId, new VoxelMapViewModel(voxel, this));
|
||||
VoxelMaps.Add(voxel.EntityId, new VoxelMapViewModel(voxel, this));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
131
Torch.Server/ViewModels/ModItemInfo.cs
Normal file
131
Torch.Server/ViewModels/ModItemInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
using Torch.API.Plugins;
|
||||
using Torch.Server.Views;
|
||||
@@ -17,12 +19,24 @@ namespace Torch.Server.ViewModels
|
||||
public string Name { get; }
|
||||
public ITorchPlugin Plugin { get; }
|
||||
|
||||
private static Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public PluginViewModel(ITorchPlugin plugin)
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
if (Plugin is IWpfPlugin p)
|
||||
{
|
||||
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})";
|
||||
|
||||
@@ -38,5 +52,55 @@ namespace Torch.Server.ViewModels
|
||||
this.Control.Resources.MergedDictionaries.Clear();
|
||||
this.Control.Resources.MergedDictionaries.Add(dictionary);
|
||||
}
|
||||
|
||||
public Brush Color
|
||||
{
|
||||
get {
|
||||
switch (Plugin.State)
|
||||
{
|
||||
case PluginState.NotInitialized:
|
||||
case PluginState.MissingDependency:
|
||||
case PluginState.DisabledError:
|
||||
return Brushes.Red;
|
||||
case PluginState.UpdateRequired:
|
||||
return Brushes.DodgerBlue;
|
||||
case PluginState.UninstallRequested:
|
||||
return Brushes.Gold;
|
||||
case PluginState.NotInstalled:
|
||||
case PluginState.DisabledUser:
|
||||
return Brushes.Gray;
|
||||
case PluginState.Enabled:
|
||||
return Brushes.Transparent;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ToolTip
|
||||
{
|
||||
get { switch (Plugin.State)
|
||||
{
|
||||
case PluginState.NotInitialized:
|
||||
return "Error during load.";
|
||||
case PluginState.DisabledError:
|
||||
return "Disabled due to error on load.";
|
||||
case PluginState.DisabledUser:
|
||||
return "Disabled.";
|
||||
case PluginState.UpdateRequired:
|
||||
return "Update required.";
|
||||
case PluginState.UninstallRequested:
|
||||
return "Marked for uninstall.";
|
||||
case PluginState.NotInstalled:
|
||||
return "Not installed. Click 'Enable'";
|
||||
case PluginState.Enabled:
|
||||
return string.Empty;
|
||||
case PluginState.MissingDependency:
|
||||
return "Dependency missing. Check the log.";
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -21,8 +21,11 @@ namespace Torch.Server.ViewModels
|
||||
[Torch.Views.Display(Description = "The type of the game online mode.", Name = "Online Mode", GroupName = "Others")]
|
||||
public MyOnlineModeEnum OnlineMode { get => _settings.OnlineMode; set => SetValue(ref _settings.OnlineMode, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The multiplier for inventory size.", Name = "Inventory Size", GroupName = "Multipliers")]
|
||||
public float InventorySizeMultiplier { get => _settings.InventorySizeMultiplier; set => SetValue(ref _settings.InventorySizeMultiplier, value); }
|
||||
[Torch.Views.Display(Description = "The multiplier for character inventory size.", Name = "Character Inventory Size", GroupName = "Multipliers")]
|
||||
public float CharacterInventorySizeMultiplier { get => _settings.InventorySizeMultiplier; set => SetValue(ref _settings.InventorySizeMultiplier, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The multiplier for block inventory size.", Name = "Block Inventory Size", GroupName = "Multipliers")]
|
||||
public float BlockInventorySizeMultiplier { get => _settings.BlocksInventorySizeMultiplier; set => SetValue(ref _settings.BlocksInventorySizeMultiplier, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The multiplier for assembler speed.", Name = "Assembler Speed", GroupName = "Multipliers")]
|
||||
public float AssemblerSpeedMultiplier { get => _settings.AssemblerSpeedMultiplier; set => SetValue(ref _settings.AssemblerSpeedMultiplier, value); }
|
||||
@@ -120,7 +123,13 @@ namespace Torch.Server.ViewModels
|
||||
public bool EnableSaving { get => _settings.EnableSaving; set => SetValue(ref _settings.EnableSaving, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables respawn screen.", Name = "Enable Respawn Screen in the Game", GroupName = "Players")]
|
||||
public bool EnableRespawnScreen { get => _settings.EnableRespawnScreen; set => SetValue(ref _settings.EnableRespawnScreen, value); }
|
||||
public bool StartInRespawnScreen { get => _settings.StartInRespawnScreen; set => SetValue(ref _settings.StartInRespawnScreen, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables research.", Name = "Enable Research", GroupName = "Players")]
|
||||
public bool EnableResearch { get => _settings.EnableResearch; set => SetValue(ref _settings.EnableResearch, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables Good.bot hints.", Name = "Enable Good.bot hints", GroupName = "Players")]
|
||||
public bool EnableGoodBotHints { get => _settings.EnableGoodBotHints; set => SetValue(ref _settings.EnableGoodBotHints, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables infinite ammunition in survival game mode.", Name = "Enable Infinite Ammunition in Survival", GroupName = "Others")]
|
||||
public bool InfiniteAmmo { get => _settings.InfiniteAmmo; set => SetValue(ref _settings.InfiniteAmmo, value); }
|
||||
@@ -255,6 +264,45 @@ namespace Torch.Server.ViewModels
|
||||
[Torch.Views.Display(Description = "Defines character removal threshold for trash removal system. If player disconnects it will remove his character after this time.\n Set to 0 to disable.", Name = "Character Removal Threshold [mins]", GroupName = "Trash Removal")]
|
||||
public int PlayerCharacterRemovalThreshold { get => _settings.PlayerCharacterRemovalThreshold; set => SetValue(ref _settings.PlayerCharacterRemovalThreshold, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Sets optimal distance in meters when spawning new players near others.", Name = "Optimal Spawn Distance", GroupName = "Players")]
|
||||
public float OptimalSpawnDistance { get => _settings.OptimalSpawnDistance; set => SetValue(ref _settings.OptimalSpawnDistance, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables automatic respawn at nearest available respawn point.", Name = "Enable Auto Respawn", GroupName = "Players")]
|
||||
public bool EnableAutoRespawn { get => _settings.EnableAutorespawn; set => SetValue(ref _settings.EnableAutorespawn, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The number of NPC factions generated on the start of the world.", Name = "NPC Factions Count", GroupName = "NPCs")]
|
||||
public int TradeFactionsCount { get => _settings.TradeFactionsCount; set => SetValue(ref _settings.TradeFactionsCount, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The inner radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Inner Radius", GroupName = "NPCs")]
|
||||
public double StationsDistanceInnerRadius { get => _settings.StationsDistanceInnerRadius; set => SetValue(ref _settings.StationsDistanceInnerRadius, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The outer radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Outer Radius Start", GroupName = "NPCs")]
|
||||
public double StationsDistanceOuterRadiusStart { get => _settings.StationsDistanceOuterRadiusStart; set => SetValue(ref _settings.StationsDistanceOuterRadiusStart, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "The outer radius [m] (center is in 0,0,0), where stations can spawn. Does not affect planet-bound stations (surface Outposts and Orbital stations).", Name = "Stations Outer Radius End", GroupName = "NPCs")]
|
||||
public double StationsDistanceOuterRadiusEnd { get => _settings.StationsDistanceOuterRadiusEnd; set => SetValue(ref _settings.StationsDistanceOuterRadiusEnd, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Time period between two economy updates in seconds.", Name = "Economy tick time", GroupName = "NPCs")]
|
||||
public int EconomyTickInSeconds { get => _settings.EconomyTickInSeconds; set => SetValue(ref _settings.EconomyTickInSeconds, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "If enabled bounty contracts will be available on stations.", Name = "Enable Bounty Contracts", GroupName = "Players")]
|
||||
public bool EnableBountyContracts { get => _settings.EnableBountyContracts; set => SetValue(ref _settings.EnableBountyContracts, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Resource deposits count coefficient for generated world content (voxel generator version > 2).", Name = "Deposits Count Coefficient", GroupName = "Environment")]
|
||||
public float DepositsCountCoefficient { get => _settings.DepositsCountCoefficient; set => SetValue(ref _settings.DepositsCountCoefficient, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Resource deposit size denominator for generated world content (voxel generator version > 2).", Name = "Deposit Size Denominator", GroupName = "Environment")]
|
||||
public float DepositSideDenominator { get => _settings.DepositSizeDenominator; set => SetValue(ref _settings.DepositSizeDenominator, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables economy features.", Name = "Enable Economy", GroupName = "NPCs")]
|
||||
public bool EnableEconomy { get => _settings.EnableEconomy; set => SetValue(ref _settings.EnableEconomy, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Enables system for voxel reverting.", Name = "Enable Voxel Reverting", GroupName = "Trash Removal")]
|
||||
public bool VoxelTrashRemovalEnabled { get => _settings.VoxelTrashRemovalEnabled; set => SetValue(ref _settings.VoxelTrashRemovalEnabled, value); }
|
||||
|
||||
[Torch.Views.Display(Description = "Allows super gridding exploit to be used.", Name = "Enable Supergridding", GroupName = "Others")]
|
||||
public bool EnableSupergridding { get => _settings.EnableSupergridding; set => SetValue(ref _settings.EnableSupergridding, value); }
|
||||
|
||||
public SessionSettingsViewModel(MyObjectBuilder_SessionSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
|
34
Torch.Server/ViewModels/WorldConfigurationViewModel.cs
Normal file
34
Torch.Server/ViewModels/WorldConfigurationViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,8 +17,8 @@
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</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>
|
||||
<TextBox Grid.Column="0" x:Name="Message" DockPanel.Dock="Left" Margin="5,5,5,5" KeyDown="Message_OnKeyDown"></TextBox>
|
||||
<Button Grid.Column="1" Content="Send" DockPanel.Dock="Right" Width="50" Margin="5" Click="SendButton_Click"></Button>
|
||||
<TextBox Grid.Column="0" x:Name="Message" Margin="5" KeyDown="Message_OnKeyDown"></TextBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
@@ -20,6 +20,8 @@ using NLog;
|
||||
using Torch;
|
||||
using Sandbox;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Game.Gui;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
@@ -131,6 +133,15 @@ namespace Torch.Server
|
||||
bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight;
|
||||
var span = new Span();
|
||||
span.Inlines.Add($"{msg.Timestamp} ");
|
||||
switch (msg.Channel)
|
||||
{
|
||||
case ChatChannel.Faction:
|
||||
span.Inlines.Add(new Run($"[{MySession.Static.Factions.TryGetFactionById(msg.Target)?.Tag ?? "???"}] ") { Foreground = Brushes.Green });
|
||||
break;
|
||||
case ChatChannel.Private:
|
||||
span.Inlines.Add(new Run($"[to {MySession.Static.Players.TryGetIdentity(msg.Target)?.DisplayName ?? "???"}] ") { Foreground = Brushes.DeepPink });
|
||||
break;
|
||||
}
|
||||
span.Inlines.Add(new Run(msg.Author) { Foreground = LookupBrush(msg.Font) });
|
||||
span.Inlines.Add($": {msg.Message}");
|
||||
span.Inlines.Add(new LineBreak());
|
||||
@@ -163,10 +174,18 @@ namespace Torch.Server
|
||||
var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
|
||||
if (commands != null && commands.IsCommand(text))
|
||||
{
|
||||
InsertMessage(new TorchChatMessage("Server", text, MyFontEnum.DarkBlue));
|
||||
InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, text, TorchBase.Instance.Config.ChatColor));
|
||||
_server.Invoke(() =>
|
||||
{
|
||||
commands.HandleCommandFromServer(text);
|
||||
var responses = commands.HandleCommandFromServer(text);
|
||||
if (responses == null)
|
||||
{
|
||||
InsertMessage(new TorchChatMessage(TorchBase.Instance.Config.ChatName, "Invalid command.", TorchBase.Instance.Config.ChatColor));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var response in responses)
|
||||
InsertMessage(response);
|
||||
});
|
||||
}
|
||||
else
|
||||
|
@@ -27,6 +27,7 @@
|
||||
<DockPanel Grid.Row="0">
|
||||
<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="New World" Margin="3" Padding="3" DockPanel.Dock="Right" Click="NewWorld_OnClick"/>
|
||||
<ComboBox x:Name="WorldList" ItemsSource="{Binding Worlds}" SelectedItem="{Binding SelectedWorld}" Margin="3"
|
||||
SelectionChanged="Selector_OnSelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
@@ -57,6 +58,7 @@
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -67,6 +69,9 @@
|
||||
<TextBox Text="{Binding ServerName}" Margin="3,0,3,3" Width="160" />
|
||||
<Label Content="World Name" />
|
||||
<TextBox Text="{Binding WorldName}" Margin="3,0,3,3" Width="160" />
|
||||
<Label Content="Server Description" />
|
||||
<TextBox Text="{Binding ServerDescription}" Margin="3,0,3,3" Width="160" Height="100"
|
||||
AcceptsReturn="true" VerticalScrollBarVisibility="Auto"/>
|
||||
<Label Content="Whitelist Group ID" />
|
||||
<TextBox Margin="3,0,3,3" Width="160" Style="{StaticResource ValidatedTextBox}">
|
||||
<TextBox.Text>
|
||||
@@ -87,13 +92,19 @@
|
||||
<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 Grid.Column="1" Margin="3">
|
||||
<Label Content="Mods" />
|
||||
<Label Content="Administrators" />
|
||||
<TextBox Text="{Binding Administrators, Converter={StaticResource ListConverterString}}"
|
||||
Margin="3"
|
||||
Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" />
|
||||
<Button Content="Edit Roles" Click="RoleEdit_Onlick" Margin="3"/>
|
||||
<Label Content="Reserved Players" />
|
||||
<TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"
|
||||
Style="{StaticResource ValidatedTextBox}">
|
||||
<TextBox.Text>
|
||||
<Binding Path="Mods" UpdateSourceTrigger="PropertyChanged"
|
||||
<Binding Path="Reserved" UpdateSourceTrigger="PropertyChanged"
|
||||
ValidatesOnDataErrors="True" NotifyOnValidationError="True"
|
||||
Converter="{StaticResource ListConverterUInt64}">
|
||||
<Binding.ValidationRules>
|
||||
@@ -102,10 +113,6 @@
|
||||
</Binding>
|
||||
</TextBox.Text>
|
||||
</TextBox>
|
||||
<Label Content="Administrators" />
|
||||
<TextBox Text="{Binding Administrators, Converter={StaticResource ListConverterString}}"
|
||||
Margin="3"
|
||||
Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto" />
|
||||
<Label Content="Banned Players" />
|
||||
<TextBox Margin="3" Height="100" AcceptsReturn="true" VerticalScrollBarVisibility="Auto"
|
||||
Style="{StaticResource ValidatedTextBox}">
|
||||
@@ -121,6 +128,7 @@
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button Grid.Row="1" Content="Save Config" Margin="3" Click="Save_OnClick" />
|
||||
</Grid>
|
||||
<views:PropertyGrid Grid.Column="1" Margin="3" DataContext="{Binding SessionSettings}" IgnoreDisplay ="True" />
|
||||
|
@@ -12,6 +12,8 @@ using Torch.API.Managers;
|
||||
using Torch.Server.Annotations;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Server.ViewModels;
|
||||
using Torch.Views;
|
||||
using VRage.Game.ModAPI;
|
||||
|
||||
namespace Torch.Server.Views
|
||||
{
|
||||
@@ -116,5 +118,28 @@ namespace Torch.Server.Views
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private void NewWorld_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var c = new WorldGeneratorDialog(_instanceManager);
|
||||
c.Show();
|
||||
}
|
||||
|
||||
private void RoleEdit_Onlick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//var w = new RoleEditor(_instanceManager.DedicatedConfig.SelectedWorld);
|
||||
//w.Show();
|
||||
var d = new RoleEditor();
|
||||
var w = _instanceManager.DedicatedConfig.SelectedWorld;
|
||||
|
||||
if (w == null)
|
||||
{
|
||||
MessageBox.Show("A world is not selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
d.Edit(w.Checkpoint.PromotedUsers.Dictionary);
|
||||
_instanceManager.DedicatedConfig.Administrators = w.Checkpoint.PromotedUsers.Dictionary.Where(k => k.Value >= MyPromoteLevel.Admin).Select(k => k.Key.ToString()).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
77
Torch.Server/Views/Converters/ListConverterWorkshopId.cs
Normal file
77
Torch.Server/Views/Converters/ListConverterWorkshopId.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
53
Torch.Server/Views/Converters/ModToIdConverter.cs
Normal file
53
Torch.Server/Views/Converters/ModToIdConverter.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,10 +22,11 @@
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TreeView Grid.Row="0" Margin="3" DockPanel.Dock="Top" SelectedItemChanged="TreeView_OnSelectedItemChanged"
|
||||
TreeViewItem.Expanded="TreeViewItem_OnExpanded">
|
||||
TreeViewItem.Expanded="TreeViewItem_OnExpanded" Name="EntityTree">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:GridViewModel}"
|
||||
ItemsSource="{Binding Path=Blocks}">
|
||||
@@ -46,46 +47,47 @@
|
||||
</HierarchicalDataTemplate>
|
||||
<HierarchicalDataTemplate DataType="{x:Type entities:VoxelMapViewModel}"
|
||||
ItemsSource="{Binding AttachedGrids}">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text="{Binding DescriptiveName}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
<TreeViewItem ItemsSource="{Binding Path=Grids.Values}">
|
||||
<TreeViewItem ItemsSource="{Binding Path=SortedGrids}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding Grids.Count, StringFormat=Grids ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
</TreeViewItem>
|
||||
<TreeViewItem ItemsSource="{Binding Characters.Values}">
|
||||
<TreeViewItem ItemsSource="{Binding SortedCharacters}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding Characters.Count, StringFormat=Characters ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
<TreeViewItem.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text="{Binding DescriptiveName}" />
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
<TreeViewItem ItemsSource="{Binding VoxelMaps.Values}">
|
||||
<TreeViewItem ItemsSource="{Binding SortedVoxelMaps}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding VoxelMaps.Count, StringFormat=Voxel Maps ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
<TreeViewItem.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text="{Binding DescriptiveName}" />
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
<TreeViewItem ItemsSource="{Binding FloatingObjects.Values}">
|
||||
<TreeViewItem ItemsSource="{Binding SortedFloatingObjects}">
|
||||
<TreeViewItem.Header>
|
||||
<TextBlock Text="{Binding FloatingObjects.Count, StringFormat=Floating Objects ({0})}" />
|
||||
</TreeViewItem.Header>
|
||||
<TreeViewItem.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text="{Binding DescriptiveName}" />
|
||||
</DataTemplate>
|
||||
</TreeViewItem.ItemTemplate>
|
||||
</TreeViewItem>
|
||||
</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}"
|
||||
Margin="3" />
|
||||
<Button Content="Stop" Click="Stop_OnClick" IsEnabled="{Binding CurrentEntity.CanStop}" Margin="3" />
|
||||
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,6 +14,7 @@ using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using NLog;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.ViewModels;
|
||||
using Torch.Server.ViewModels.Blocks;
|
||||
using Torch.Server.ViewModels.Entities;
|
||||
@@ -29,12 +31,15 @@ namespace Torch.Server.Views
|
||||
{
|
||||
public EntityTreeViewModel Entities { get; set; }
|
||||
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public EntitiesControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Entities = new EntityTreeViewModel(this);
|
||||
DataContext = Entities;
|
||||
Entities.Init();
|
||||
SortCombo.ItemsSource = Enum.GetNames(typeof(EntityTreeViewModel.SortEnum));
|
||||
}
|
||||
|
||||
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||
@@ -77,5 +82,30 @@ namespace Torch.Server.Views
|
||||
if (item.DataContext is ILazyLoad l)
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
Torch.Server/Views/Extensions.cs
Normal file
24
Torch.Server/Views/Extensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
127
Torch.Server/Views/ModListControl.xaml
Normal file
127
Torch.Server/Views/ModListControl.xaml
Normal 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>
|
284
Torch.Server/Views/ModListControl.xaml.cs
Normal file
284
Torch.Server/Views/ModListControl.xaml.cs
Normal 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();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -80,6 +80,9 @@ namespace Torch.Server
|
||||
|
||||
private void KickButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PlayerList.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
|
||||
try
|
||||
{
|
||||
@@ -93,6 +96,9 @@ namespace Torch.Server
|
||||
|
||||
private void BanButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PlayerList.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
|
||||
try
|
||||
{
|
||||
@@ -106,6 +112,9 @@ namespace Torch.Server
|
||||
|
||||
private void PromoteButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PlayerList.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
|
||||
try
|
||||
{
|
||||
@@ -119,6 +128,9 @@ namespace Torch.Server
|
||||
|
||||
private void DemoteButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PlayerList.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
|
||||
try
|
||||
{
|
||||
|
124
Torch.Server/Views/PluginBrowser.xaml
Normal file
124
Torch.Server/Views/PluginBrowser.xaml
Normal 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>
|
||||
|
||||
|
114
Torch.Server/Views/PluginBrowser.xaml.cs
Normal file
114
Torch.Server/Views/PluginBrowser.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -22,11 +22,18 @@
|
||||
<ListView Grid.Row="0" ItemsSource="{Binding Plugins}" SelectedItem="{Binding SelectedPlugin}" Margin="3">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock Text="{Binding Name}" Background="{Binding Color}" ToolTip="{Binding ToolTip}"/>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</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>
|
||||
<ScrollViewer Name="PScroll" Grid.Column="1" Margin="3">
|
||||
<Frame NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
|
||||
|
@@ -41,7 +41,9 @@ namespace Torch.Server.Views
|
||||
{
|
||||
if (propertyChangedEventArgs.PropertyName == nameof(PluginManagerViewModel.SelectedPlugin))
|
||||
{
|
||||
if (((PluginManagerViewModel)DataContext).SelectedPlugin.Control is PropertyGrid)
|
||||
var plugin = ((PluginManagerViewModel)DataContext).SelectedPlugin;
|
||||
|
||||
if (plugin.Control is PropertyGrid || !plugin.Control.GetScrollContainer())
|
||||
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
|
||||
else
|
||||
PScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||
@@ -71,5 +73,11 @@ namespace Torch.Server.Views
|
||||
if (_plugins?.PluginDir != null)
|
||||
Process.Start(_plugins.PluginDir);
|
||||
}
|
||||
|
||||
private void BrowsPlugins_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var browser = new PluginBrowser();
|
||||
browser.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,5 +18,7 @@
|
||||
</Style>
|
||||
<converters:ListConverter x:Key="ListConverterString" Type="system:String"/>
|
||||
<converters:ListConverter x:Key="ListConverterUInt64" Type="system:UInt64"/>
|
||||
<converters:ModToListIdConverter x:Key="ModToListIdConverter"/>
|
||||
<converters:ListConverterWorkshopId x:Key="ListConverterWorkshopId"/>
|
||||
<converters:BooleanAndConverter x:Key="BooleanAndConverter"/>
|
||||
</ResourceDictionary>
|
52
Torch.Server/Views/RoleEditor.xaml
Normal file
52
Torch.Server/Views/RoleEditor.xaml
Normal 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>
|
||||
|
123
Torch.Server/Views/RoleEditor.xaml.cs
Normal file
123
Torch.Server/Views/RoleEditor.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -57,7 +57,7 @@
|
||||
</Label>
|
||||
<Label x:Name="LabelPlayers">
|
||||
<Label.Content>
|
||||
<TextBlock ></TextBlock>
|
||||
<TextBlock Text="{Binding OnlinePlayers, StringFormat=Players: {0}}"/>
|
||||
</Label.Content>
|
||||
</Label>
|
||||
</StackPanel>
|
||||
@@ -66,7 +66,7 @@
|
||||
<RichTextBox x:Name="ConsoleText" VerticalScrollBarVisibility="Visible" FontFamily="Consolas" IsReadOnly="True"/>
|
||||
</TabItem>
|
||||
<TabItem Header="Configuration">
|
||||
<Grid IsEnabled="{Binding CanRun}">
|
||||
<Grid IsEnabled="{Binding Path=HasRun, Converter={StaticResource InverseBool}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
@@ -80,6 +80,9 @@
|
||||
<views:ConfigControl Grid.Row="1" x:Name="ConfigControl" Margin="3" DockPanel.Dock="Bottom" IsEnabled="{Binding CanRun}"/>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
<TabItem Header="Mods">
|
||||
<views:ModListControl/>
|
||||
</TabItem>
|
||||
<TabItem Header="Chat/Players">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@@ -10,6 +10,7 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
@@ -31,8 +32,13 @@ namespace Torch.Server
|
||||
private TorchServer _server;
|
||||
private TorchConfig _config;
|
||||
|
||||
private bool _autoscrollLog = true;
|
||||
|
||||
public TorchUI(TorchServer server)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
Width = 800;
|
||||
Height = 600;
|
||||
_config = (TorchConfig)server.Config;
|
||||
_server = server;
|
||||
//TODO: data binding for whole server
|
||||
@@ -41,10 +47,10 @@ namespace Torch.Server
|
||||
|
||||
AttachConsole();
|
||||
|
||||
Left = _config.WindowPosition.X;
|
||||
Top = _config.WindowPosition.Y;
|
||||
Width = _config.WindowSize.X;
|
||||
Height = _config.WindowSize.Y;
|
||||
//Left = _config.WindowPosition.X;
|
||||
//Top = _config.WindowPosition.Y;
|
||||
//Width = _config.WindowSize.X;
|
||||
//Height = _config.WindowSize.Y;
|
||||
|
||||
Chat.BindServer(server);
|
||||
PlayerList.BindServer(server);
|
||||
@@ -54,6 +60,14 @@ namespace Torch.Server
|
||||
Themes.uiSource = this;
|
||||
Themes.SetConfig(_config);
|
||||
Title = $"{_config.InstanceName} - Torch {server.TorchVersion}, SE {server.GameVersion}";
|
||||
|
||||
Loaded += TorchUI_Loaded;
|
||||
}
|
||||
|
||||
private void TorchUI_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var scrollViewer = FindDescendant<ScrollViewer>(ConsoleText);
|
||||
scrollViewer.ScrollChanged += ConsoleText_OnScrollChanged;
|
||||
}
|
||||
|
||||
private void AttachConsole()
|
||||
@@ -66,7 +80,52 @@ namespace Torch.Server
|
||||
doc = (wrapped?.WrappedTarget as FlowDocumentTarget)?.Document;
|
||||
}
|
||||
ConsoleText.Document = doc ?? new FlowDocument(new Paragraph(new Run("No target!")));
|
||||
ConsoleText.TextChanged += (sender, args) => ConsoleText.ScrollToEnd();
|
||||
ConsoleText.TextChanged += ConsoleText_OnTextChanged;
|
||||
}
|
||||
|
||||
public static T FindDescendant<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)
|
||||
@@ -97,10 +156,14 @@ namespace Torch.Server
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
var newSize = new Point((int)Width, (int)Height);
|
||||
_config.WindowSize = newSize;
|
||||
var newPos = new Point((int)Left, (int)Top);
|
||||
_config.WindowPosition = newPos;
|
||||
// Can't save here or you'll persist all the command line arguments
|
||||
//
|
||||
//var newSize = new Point((int)Width, (int)Height);
|
||||
//_config.WindowSize = newSize;
|
||||
//var newPos = new Point((int)Left, (int)Top);
|
||||
//_config.WindowPosition = newPos;
|
||||
|
||||
//_config.Save(); //you idiot
|
||||
|
||||
if (_server?.State == ServerState.Running)
|
||||
_server.Stop();
|
||||
|
@@ -5,14 +5,15 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Torch.Server"
|
||||
xmlns:views="clr-namespace:Torch.Server.Views"
|
||||
xmlns:views1="clr-namespace:Torch.Views;assembly=Torch"
|
||||
mc:Ignorable="d"
|
||||
Title="WorldGeneratorDialog" Height="300" Width="700">
|
||||
Title="WorldGeneratorDialog" Height="500" Width="700">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250"/>
|
||||
<ColumnDefinition Width="440"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</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>
|
||||
<DataTemplate DataType="local:PremadeCheckpointItem">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
@@ -26,13 +27,18 @@
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</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">
|
||||
<Label Content="World Name: "/>
|
||||
<TextBox x:Name="WorldName" Width="300" Margin="3"/>
|
||||
</StackPanel>
|
||||
<views:SessionSettingsView/>
|
||||
<Button Content="Create World" Click="ButtonBase_OnClick"/>
|
||||
</StackPanel>
|
||||
<views1:PropertyGrid Grid.Row="1" x:Name="SettingsView" Margin="3"/>
|
||||
<Button Grid.Row="2" Content="Create World" Click="ButtonBase_OnClick" Margin ="3"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
@@ -13,7 +13,15 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using NLog;
|
||||
using Sandbox.Definitions;
|
||||
using Sandbox.Engine.Networking;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Server.ViewModels;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.Dedicated;
|
||||
using VRage.FileSystem;
|
||||
using VRage.Game;
|
||||
using VRage.Game.Localization;
|
||||
using VRage.Utils;
|
||||
|
||||
@@ -26,19 +34,25 @@ namespace Torch.Server
|
||||
{
|
||||
private InstanceManager _instanceManager;
|
||||
private List<PremadeCheckpointItem> _checkpoints = new List<PremadeCheckpointItem>();
|
||||
private PremadeCheckpointItem _currentItem;
|
||||
|
||||
[ReflectedStaticMethod(Type = typeof(ConfigForm), Name = "LoadLocalization")]
|
||||
private static Action _loadLocalization;
|
||||
|
||||
public WorldGeneratorDialog(InstanceManager instanceManager)
|
||||
{
|
||||
_instanceManager = instanceManager;
|
||||
InitializeComponent();
|
||||
|
||||
MyDefinitionManager.Static.LoadScenarios();
|
||||
var scenarios = MyDefinitionManager.Static.GetScenarioDefinitions();
|
||||
MyDefinitionManager.Static.UnloadData();
|
||||
foreach (var scenario in scenarios)
|
||||
_loadLocalization();
|
||||
var scenarios = MyLocalCache.GetAvailableWorldInfos(Path.Combine(MyFileSystem.ContentPath, "CustomWorlds"));
|
||||
foreach (var tup in scenarios)
|
||||
{
|
||||
//TODO: Load localization
|
||||
_checkpoints.Add(new PremadeCheckpointItem { Name = scenario.DisplayNameText, Icon = @"C:\Users\jgross\Documents\Projects\TorchAPI\Torch\bin\x64\Release\Content\CustomWorlds\Empty World\thumb.jpg" });
|
||||
string directory = tup.Item1;
|
||||
MyWorldInfo info = tup.Item2;
|
||||
string localizedName = MyTexts.GetString(MyStringId.GetOrCompute(info.SessionName));
|
||||
var checkpoint = MyLocalCache.LoadCheckpoint(directory, out _);
|
||||
checkpoint.OnlineMode = MyOnlineModeEnum.PUBLIC;
|
||||
_checkpoints.Add(new PremadeCheckpointItem { Name = localizedName, Icon = Path.Combine(directory, "thumb.jpg"), Path = directory, Checkpoint = checkpoint});
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -59,20 +73,36 @@ namespace Torch.Server
|
||||
|
||||
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
/*
|
||||
var worldPath = Path.Combine("Instance", "Saves", WorldName.Text);
|
||||
var checkpointItem = (PremadeCheckpointItem)PremadeCheckpoints.SelectedItem;
|
||||
string worldName = string.IsNullOrEmpty(WorldName.Text) ? _currentItem.Name : WorldName.Text;
|
||||
|
||||
var worldPath = Path.Combine(TorchBase.Instance.Config.InstancePath, "Saves", worldName);
|
||||
var checkpoint = _currentItem.Checkpoint;
|
||||
if (Directory.Exists(worldPath))
|
||||
{
|
||||
MessageBox.Show("World already exists with that name.");
|
||||
return;
|
||||
}
|
||||
Directory.CreateDirectory(worldPath);
|
||||
foreach (var file in Directory.EnumerateFiles(checkpointItem.Path, "*", SearchOption.AllDirectories))
|
||||
foreach (var file in Directory.EnumerateFiles(_currentItem.Path, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
File.Copy(file, Path.Combine(worldPath, file.Replace($"{checkpointItem.Path}\\", "")));
|
||||
File.Copy(file, Path.Combine(worldPath, file.Replace($"{_currentItem.Path}\\", "")));
|
||||
}
|
||||
_instanceManager.SelectWorld(worldPath, false);*/
|
||||
|
||||
checkpoint.SessionName = worldName;
|
||||
|
||||
MyLocalCache.SaveCheckpoint(checkpoint, worldPath);
|
||||
|
||||
|
||||
_instanceManager.SelectWorld(worldPath, false);
|
||||
_instanceManager.ImportSelectedWorldConfig();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void PremadeCheckpoints_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var selected = (PremadeCheckpointItem)PremadeCheckpoints.SelectedItem;
|
||||
_currentItem = selected;
|
||||
SettingsView.DataContext = new SessionSettingsViewModel(_currentItem.Checkpoint.Settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,5 +111,6 @@ namespace Torch.Server
|
||||
public string Path { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public MyObjectBuilder_Checkpoint Checkpoint { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,14 @@
|
||||
<packages>
|
||||
<package id="ControlzEx" version="3.0.2.4" 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="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="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>
|
@@ -82,6 +82,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
11
Torch.Tests/app.config
Normal file
11
Torch.Tests/app.config
Normal 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>
|
@@ -14,6 +14,7 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7AD02A71-1D4C-48F9-A8C1-789A5512424F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
NLog.config = NLog.config
|
||||
NLog-user.config = NLog-user.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Tests", "Torch.Tests\Torch.Tests.csproj", "{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}"
|
||||
@@ -55,10 +56,8 @@ Global
|
||||
{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}.Release|x64.Build.0 = Release|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Debug|x64.Build.0 = Debug|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.ActiveCfg = Release|x64
|
||||
{E36DF745-260B-4956-A2E8-09F08B2E7161}.Release|x64.Build.0 = Release|x64
|
||||
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64
|
||||
@@ -79,10 +78,8 @@ Global
|
||||
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = Debug|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.ActiveCfg = Release|x64
|
||||
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
272
Torch/Collections/BinaryMinHeap.cs
Normal file
272
Torch/Collections/BinaryMinHeap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
@@ -123,10 +124,10 @@ namespace Torch.Collections
|
||||
private readonly Timer _flushEventQueue;
|
||||
private const int _eventRaiseDelay = 50;
|
||||
|
||||
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
|
||||
new Queue<NotifyCollectionChangedEventArgs>();
|
||||
private readonly ConcurrentQueue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
|
||||
new ConcurrentQueue<NotifyCollectionChangedEventArgs>();
|
||||
|
||||
private readonly Queue<string> _propertyEventQueue = new Queue<string>();
|
||||
private readonly ConcurrentQueue<string> _propertyEventQueue = new ConcurrentQueue<string>();
|
||||
|
||||
private void FlushEventQueue(object data)
|
||||
{
|
||||
@@ -137,7 +138,8 @@ namespace Torch.Collections
|
||||
// :/, but works better
|
||||
bool reset = _collectionEventQueue.Count > 0;
|
||||
if (reset)
|
||||
_collectionEventQueue.Clear();
|
||||
while (_collectionEventQueue.Count > 0)
|
||||
_collectionEventQueue.TryDequeue(out _);
|
||||
else
|
||||
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
|
||||
_collectionChangedEvent.Raise(this, e);
|
||||
|
@@ -13,7 +13,7 @@ namespace Torch.Collections
|
||||
/// Multithread safe, observable list
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Initializes a new instance of the MtObservableList class that is empty and has the default initial capacity.
|
||||
@@ -114,16 +114,34 @@ namespace Torch.Collections
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
comparer = comparer ?? Comparer<TKey>.Default;
|
||||
if (Backing is List<T> lst)
|
||||
lst.Sort(new TransformComparer<T, TKey>(selector, comparer));
|
||||
else
|
||||
Backing.Sort(new TransformComparer<T, TKey>(selector, comparer));
|
||||
}
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the list using the given comparer./>
|
||||
/// </summary>
|
||||
public void Sort(IComparer<T> comparer)
|
||||
{
|
||||
List<T> sortedItems = Backing.OrderBy(selector, comparer).ToList();
|
||||
Backing.Clear();
|
||||
foreach (T v in sortedItems)
|
||||
Backing.Add(v);
|
||||
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/>
|
||||
|
144
Torch/Collections/SortedView.cs
Normal file
144
Torch/Collections/SortedView.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
132
Torch/Collections/SystemSortedView.cs
Normal file
132
Torch/Collections/SystemSortedView.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
|
||||
namespace Torch
|
||||
{
|
||||
@@ -12,6 +13,7 @@ namespace Torch
|
||||
{
|
||||
private readonly string _argPrefix;
|
||||
private readonly Dictionary<ArgAttribute, PropertyInfo> _args = new Dictionary<ArgAttribute, PropertyInfo>();
|
||||
private readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
protected CommandLine(string argPrefix = "-")
|
||||
{
|
||||
@@ -89,6 +91,24 @@ namespace Torch
|
||||
|
||||
if (property.Value.PropertyType == typeof(string))
|
||||
property.Value.SetValue(this, args[++i]);
|
||||
|
||||
if (property.Value.PropertyType == typeof(List<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
|
||||
|
@@ -55,10 +55,17 @@ namespace Torch.Commands
|
||||
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>()
|
||||
?.SendMessageAsOther(sender, message, font, _steamIdSender);
|
||||
//hack: Backwards compatibility 20190416
|
||||
if (sender == "Server")
|
||||
{
|
||||
sender = null;
|
||||
font = null;
|
||||
}
|
||||
|
||||
var chat = Torch.CurrentSession.Managers.GetManager<IChatManagerServer>();
|
||||
chat?.SendMessageAsOther(sender, message, font, _steamIdSender);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 command = Commands.GetCommand(cmdText, out string argText);
|
||||
if (command == null)
|
||||
return false;
|
||||
return null;
|
||||
var cmdPath = string.Join(".", command.Path);
|
||||
|
||||
var splitArgs = Regex.Matches(argText, "(\"[^\"]+\"|\\S+)").Cast<Match>().Select(x => x.ToString().Replace("\"", "")).ToList();
|
||||
_log.Trace($"Invoking {cmdPath} for server.");
|
||||
var context = new CommandContext(Torch, command.Plugin, Sync.MyId, argText, splitArgs);
|
||||
var context = new ConsoleCommandContext(Torch, command.Plugin, Sync.MyId, argText, splitArgs);
|
||||
if (command.TryInvoke(context))
|
||||
_log.Info($"Server ran command '{message}'");
|
||||
else
|
||||
context.Respond($"Invalid Syntax: {command.SyntaxHelp}");
|
||||
return true;
|
||||
return context.Responses;
|
||||
}
|
||||
|
||||
public void HandleCommand(TorchChatMessage msg, ref bool consumed)
|
||||
@@ -130,7 +130,7 @@ namespace Torch.Commands
|
||||
if (!HasPermission(steamId, command))
|
||||
{
|
||||
_log.Info($"{player.DisplayName} tried to use command {cmdPath} without permission");
|
||||
_chatManager.SendMessageAsOther("Server", $"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", MyFontEnum.Red, steamId);
|
||||
_chatManager.SendMessageAsOther(Torch.Config.ChatName, $"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", Torch.Config.ChatColor, steamId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
30
Torch/Commands/ConsoleCommandContext.cs
Normal file
30
Torch/Commands/ConsoleCommandContext.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using NLog;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
using Sandbox.ModAPI;
|
||||
using Steamworks;
|
||||
@@ -28,6 +29,9 @@ namespace Torch.Commands
|
||||
{
|
||||
private static bool _restartPending = false;
|
||||
private static bool _cancelRestart = false;
|
||||
private bool _stopPending = false;
|
||||
private bool _cancelStop = false;
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
[Command("whatsmyip")]
|
||||
[Permission(MyPromoteLevel.None)]
|
||||
@@ -37,7 +41,7 @@ namespace Torch.Commands
|
||||
steamId = Context.Player.SteamUserId;
|
||||
|
||||
VRage.GameServices.MyP2PSessionState statehack = new VRage.GameServices.MyP2PSessionState();
|
||||
VRage.Steam.MySteamService.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
|
||||
MySteamServiceWrapper.Static.Peer2Peer.GetSessionState(steamId, ref statehack);
|
||||
var ip = new IPAddress(BitConverter.GetBytes(statehack.RemoteIP).Reverse().ToArray());
|
||||
Context.Respond($"Your IP is {ip}");
|
||||
}
|
||||
@@ -57,10 +61,16 @@ namespace Torch.Commands
|
||||
if (node != null)
|
||||
{
|
||||
var command = node.Command;
|
||||
var children = node.Subcommands.Select(x => x.Key);
|
||||
var children = node.Subcommands.Where(e => Context.Player == null || e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (Context.Player != null && command?.MinimumPromoteLevel > Context.Player.PromoteLevel)
|
||||
{
|
||||
Context.Respond("You are not authorized to use this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command != null)
|
||||
{
|
||||
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
|
||||
@@ -94,11 +104,11 @@ namespace Torch.Commands
|
||||
if (node != null)
|
||||
{
|
||||
var command = node.Command;
|
||||
var children = node.Subcommands.Select(x => x.Key);
|
||||
var children = node.Subcommands.Where(e => e.Value.Command?.MinimumPromoteLevel <= Context.Player.PromoteLevel).Select(x => x.Key);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (command != null)
|
||||
if (command != null && (Context.Player == null || command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
|
||||
{
|
||||
sb.AppendLine($"Syntax: {command.SyntaxHelp}");
|
||||
sb.Append(command.HelpText);
|
||||
@@ -114,7 +124,7 @@ namespace Torch.Commands
|
||||
var sb = new StringBuilder();
|
||||
foreach (var command in commandManager.Commands.WalkTree())
|
||||
{
|
||||
if (command.IsCommand)
|
||||
if (command.IsCommand && (Context.Player == null || command.Command.MinimumPromoteLevel <= Context.Player.PromoteLevel))
|
||||
sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}");
|
||||
}
|
||||
|
||||
@@ -146,9 +156,25 @@ namespace Torch.Commands
|
||||
}
|
||||
|
||||
[Command("stop", "Stops the server.")]
|
||||
public void Stop(bool save = true)
|
||||
public void Stop(bool save = true, int countdownSeconds = 0)
|
||||
{
|
||||
Context.Respond("Stopping server.");
|
||||
if (_stopPending)
|
||||
{
|
||||
Context.Respond("A stop is already pending.");
|
||||
return;
|
||||
}
|
||||
|
||||
_stopPending = true;
|
||||
Task.Run(() =>
|
||||
{
|
||||
var countdown = StopCountdown(countdownSeconds, save).GetEnumerator();
|
||||
while (countdown.MoveNext())
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
});
|
||||
|
||||
/*Context.Respond("Stopping server.");
|
||||
if (save)
|
||||
DoSave()?.ContinueWith((a, mod) =>
|
||||
{
|
||||
@@ -157,7 +183,7 @@ namespace Torch.Commands
|
||||
torch.Stop();
|
||||
}, this, TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
else
|
||||
Context.Torch.Stop();
|
||||
Context.Torch.Stop();*/
|
||||
}
|
||||
|
||||
[Command("restart", "Restarts the server.")]
|
||||
@@ -170,6 +196,7 @@ namespace Torch.Commands
|
||||
}
|
||||
|
||||
_restartPending = true;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var countdown = RestartCountdown(countdownSeconds, save).GetEnumerator();
|
||||
@@ -196,6 +223,68 @@ namespace Torch.Commands
|
||||
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)
|
||||
{
|
||||
for (var i = countdown; i >= 0; i--)
|
||||
@@ -226,15 +315,18 @@ namespace Torch.Commands
|
||||
else
|
||||
{
|
||||
if (save)
|
||||
Context.Torch.Save().ContinueWith(x => Restart());
|
||||
else
|
||||
Restart();
|
||||
{
|
||||
Log.Info("Savin game before restart.");
|
||||
Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>()
|
||||
.SendMessageAsSelf($"Saving game before restart.");
|
||||
}
|
||||
|
||||
Log.Info("Restarting server.");
|
||||
Context.Torch.Invoke(() => Context.Torch.Restart(save));
|
||||
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
void Restart() => Context.Torch.Invoke(() => Context.Torch.Restart());
|
||||
}
|
||||
|
||||
private string Pluralize(int num)
|
||||
@@ -255,7 +347,7 @@ namespace Torch.Commands
|
||||
|
||||
private Task DoSave()
|
||||
{
|
||||
Task<GameSaveResult> task = Context.Torch.Save(60 * 1000, true);
|
||||
Task<GameSaveResult> task = Context.Torch.Save(300 * 1000, true);
|
||||
if (task == null)
|
||||
{
|
||||
Context.Respond("Save failed, a save is already in progress");
|
||||
@@ -290,5 +382,11 @@ namespace Torch.Commands
|
||||
}
|
||||
}, this, TaskContinuationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
[Command("uptime", "Check how long the server has been online.")]
|
||||
public void Uptime()
|
||||
{
|
||||
Context.Respond(((ITorchServer)Context.Torch).ElapsedPlayTime.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
Torch/Extensions/LinqExtensions.cs
Normal file
41
Torch/Extensions/LinqExtensions.cs
Normal 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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Utils;
|
||||
using VRage.Game;
|
||||
using VRageMath;
|
||||
|
||||
namespace Torch.Managers.ChatManager
|
||||
{
|
||||
@@ -38,17 +39,26 @@ namespace Torch.Managers.ChatManager
|
||||
{
|
||||
if (Sandbox.Engine.Platform.Game.IsDedicated)
|
||||
{
|
||||
// Sending invalid color to clients will crash them. KEEEN
|
||||
var color = Torch.Config.ChatColor;
|
||||
if (!StringUtils.IsFontEnum(Torch.Config.ChatColor))
|
||||
{
|
||||
_log.Warn("Invalid chat font color! Defaulting to 'Red'");
|
||||
color = MyFontEnum.Red;
|
||||
}
|
||||
|
||||
var scripted = new ScriptedChatMsg()
|
||||
{
|
||||
Author = "Server",
|
||||
Font = MyFontEnum.Red,
|
||||
Author = Torch.Config.ChatName,
|
||||
Font = color,
|
||||
Text = message,
|
||||
Target = 0
|
||||
};
|
||||
MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
|
||||
}
|
||||
else
|
||||
MyMultiplayer.Static.SendChatMessage(message);
|
||||
throw new NotImplementedException("Chat system changes broke this");
|
||||
//MyMultiplayer.Static.SendChatMessage(message);
|
||||
}
|
||||
else if (HasHud)
|
||||
MyHud.Chat.ShowMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", message);
|
||||
@@ -59,12 +69,6 @@ namespace Torch.Managers.ChatManager
|
||||
{
|
||||
if (HasHud)
|
||||
MyHud.Chat?.ShowMessage(author, message, font);
|
||||
MySession.Static.GlobalChatHistory.GlobalChatHistory.Chat.Enqueue(new MyGlobalChatItem()
|
||||
{
|
||||
Author = author,
|
||||
AuthorFont = font,
|
||||
Text = message
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -76,10 +80,10 @@ namespace Torch.Managers.ChatManager
|
||||
{
|
||||
_chatMessageRecievedReplacer = _chatMessageReceivedFactory.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);
|
||||
_scriptedChatMessageRecievedReplacer.Replace(
|
||||
new Action<string, string, string>(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
|
||||
new Action<string, string, string, Color>(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -113,7 +117,7 @@ namespace Torch.Managers.ChatManager
|
||||
{
|
||||
if (!sendToOthers)
|
||||
return;
|
||||
var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText);
|
||||
var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText, ChatChannel.Global, 0);
|
||||
bool consumed = RaiseMessageRecieved(torchMsg);
|
||||
if (!consumed)
|
||||
consumed = OfflineMessageProcessor(torchMsg);
|
||||
@@ -130,19 +134,19 @@ namespace Torch.Managers.ChatManager
|
||||
}
|
||||
|
||||
|
||||
private void Multiplayer_ChatMessageReceived(ulong steamUserId, string message)
|
||||
private void Multiplayer_ChatMessageReceived(ulong steamUserId, string messageText, ChatChannel channel, long targetId, string customAuthorName)
|
||||
{
|
||||
var torchMsg = new TorchChatMessage(steamUserId, message,
|
||||
var torchMsg = new TorchChatMessage(steamUserId, messageText, channel, targetId,
|
||||
(steamUserId == MyGameService.UserId) ? MyFontEnum.DarkBlue : MyFontEnum.Blue);
|
||||
if (!RaiseMessageRecieved(torchMsg) && HasHud)
|
||||
_hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, message);
|
||||
_hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, messageText, channel, targetId, customAuthorName);
|
||||
}
|
||||
|
||||
private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font)
|
||||
private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font, Color color)
|
||||
{
|
||||
var torchMsg = new TorchChatMessage(author, message, font);
|
||||
if (!RaiseMessageRecieved(torchMsg) && HasHud)
|
||||
_hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font);
|
||||
_hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font, color);
|
||||
}
|
||||
|
||||
protected bool RaiseMessageRecieved(TorchChatMessage msg)
|
||||
@@ -158,9 +162,9 @@ namespace Torch.Managers.ChatManager
|
||||
protected static bool HasHud => !Sandbox.Engine.Platform.Game.IsDedicated;
|
||||
|
||||
[ReflectedMethod(Name = _hudChatMessageReceivedName)]
|
||||
private static Action<MyHudChat, ulong, string> _hudChatMessageReceived;
|
||||
private static Action<MyHudChat, ulong, string, ChatChannel, long, string> _hudChatMessageReceived;
|
||||
[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)]
|
||||
private static Func<ReflectedEventReplacer> _chatMessageReceivedFactory;
|
||||
|
@@ -13,38 +13,72 @@ using Sandbox.Game.Multiplayer;
|
||||
using Sandbox.Game.World;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.Collections;
|
||||
using VRage.Library.Collections;
|
||||
using VRage.Network;
|
||||
|
||||
namespace Torch.Managers.ChatManager
|
||||
{
|
||||
[PatchShim]
|
||||
internal static class ChatInterceptPatch
|
||||
{
|
||||
private static ChatManagerServer _chatManager;
|
||||
private static ChatManagerServer ChatManager => _chatManager ?? (_chatManager = TorchBase.Instance.CurrentSession.Managers.GetManager<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
|
||||
{
|
||||
[Dependency(Optional = true)]
|
||||
private INetworkManager _networkManager;
|
||||
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Logger _chatLog = LogManager.GetLogger("Chat");
|
||||
|
||||
private readonly ChatIntercept _chatIntercept;
|
||||
private readonly HashSet<ulong> _muted = new HashSet<ulong>();
|
||||
/// <inheritdoc />
|
||||
public HashSetReader<ulong> MutedUsers => _muted;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
_chatIntercept = new ChatIntercept(this);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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 />
|
||||
public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0)
|
||||
{
|
||||
if (targetSteamId == Sync.MyId)
|
||||
{
|
||||
RaiseMessageRecieved(new TorchChatMessage(authorId, message));
|
||||
RaiseMessageRecieved(new TorchChatMessage(authorId, message, ChatChannel.Global, 0));
|
||||
return;
|
||||
}
|
||||
if (MyMultiplayer.Static == null)
|
||||
@@ -89,44 +123,15 @@ namespace Torch.Managers.ChatManager
|
||||
}
|
||||
var scripted = new ScriptedChatMsg()
|
||||
{
|
||||
Author = author,
|
||||
Author = author ?? Torch.Config.ChatName,
|
||||
Text = message,
|
||||
Font = font,
|
||||
Font = font ?? Torch.Config.ChatColor,
|
||||
Target = Sync.Players.TryGetIdentityId(targetSteamId)
|
||||
};
|
||||
_chatLog.Info($"{author} (to {GetMemberName(targetSteamId)}): {message}");
|
||||
MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
|
||||
}
|
||||
|
||||
|
||||
/// <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 />
|
||||
protected override bool OfflineMessageProcessor(TorchChatMessage msg)
|
||||
{
|
||||
@@ -137,84 +142,25 @@ namespace Torch.Managers.ChatManager
|
||||
return consumed;
|
||||
}
|
||||
|
||||
private void MpStaticChatMessageReceived(ulong a, string b)
|
||||
{
|
||||
var tmp = false;
|
||||
RaiseMessageRecieved(new ChatMsg()
|
||||
{
|
||||
Author = a,
|
||||
Text = b
|
||||
}, ref tmp);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
var torchMsg = new TorchChatMessage(GetMemberName(message.Author), message.Author, message.Text);
|
||||
var torchMsg = new TorchChatMessage(GetMemberName(message.Author), message.Author, message.Text, (ChatChannel)message.Channel, message.TargetId);
|
||||
if (_muted.Contains(message.Author))
|
||||
{
|
||||
consumed = true;
|
||||
_chatLog.Warn($"MUTED USER: [{torchMsg.Channel}:{torchMsg.Target}] {torchMsg.Author}: {torchMsg.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
MessageProcessing?.Invoke(torchMsg, ref consumed);
|
||||
|
||||
if (!consumed)
|
||||
_chatLog.Info($"{torchMsg.Author}: {torchMsg.Message}");
|
||||
_chatLog.Info($"[{torchMsg.Channel}:{torchMsg.Target}] {torchMsg.Author}: {torchMsg.Message}");
|
||||
}
|
||||
|
||||
public static string GetMemberName(ulong steamId)
|
||||
{
|
||||
return MyMultiplayer.Static?.GetMemberName(steamId) ?? $"user_{steamId}";
|
||||
}
|
||||
|
||||
internal class ChatIntercept : NetworkHandlerBase, INetworkHandler
|
||||
{
|
||||
private readonly ChatManagerServer _chatManager;
|
||||
private bool? _unitTestResult;
|
||||
|
||||
public ChatIntercept(ChatManagerServer chatManager)
|
||||
{
|
||||
_chatManager = chatManager;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,14 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
|
||||
namespace Torch.Managers
|
||||
{
|
||||
public class FilesystemManager : Manager
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
/// <summary>
|
||||
/// Temporary directory for Torch that is cleared every time the program is started.
|
||||
/// </summary>
|
||||
@@ -22,30 +24,46 @@ namespace Torch.Managers
|
||||
|
||||
public FilesystemManager(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
var temp = Path.Combine(Path.GetTempPath(), "Torch");
|
||||
TempDirectory = Directory.CreateDirectory(temp).FullName;
|
||||
var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName;
|
||||
TempDirectory = Directory.CreateDirectory(Path.Combine(torch, "tmp")).FullName;
|
||||
TorchDirectory = torch;
|
||||
|
||||
_log.Debug($"Clearing tmp directory at {TempDirectory}");
|
||||
ClearTemp();
|
||||
}
|
||||
|
||||
private void ClearTemp()
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(TempDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
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>
|
||||
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
|
||||
/// </summary>
|
||||
public void SoftDelete(string file)
|
||||
public void SoftDelete(string path, string file)
|
||||
{
|
||||
if (!File.Exists(file))
|
||||
string source = Path.Combine(path, file);
|
||||
if (!File.Exists(source))
|
||||
return;
|
||||
var rand = Path.GetRandomFileName();
|
||||
var dest = Path.Combine(TempDirectory, rand);
|
||||
File.Move(file, dest);
|
||||
File.Move(source, rand);
|
||||
string rsource = Path.Combine(path, rand);
|
||||
File.Move(rsource, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -76,7 +76,21 @@ namespace Torch.Managers
|
||||
|
||||
private static StringBuilder PrepareLog(MyLog log)
|
||||
{
|
||||
return _tmpStringBuilder.Value.Clear().Append(' ', _getIndentByThread(log, _getThreadId(log)) * 3);
|
||||
try
|
||||
{
|
||||
var v = _tmpStringBuilder.Value;
|
||||
v.Clear();
|
||||
var i = _getThreadId(log);
|
||||
var t = _getIndentByThread(log, i);
|
||||
v.Append(' ', t * 3);
|
||||
return v;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Error(e);
|
||||
return _tmpStringBuilder.Value.Clear();
|
||||
}
|
||||
//return _tmpStringBuilder.Value.Clear().Append(' ', _getIndentByThread(log, _getThreadId(log)) * 3);
|
||||
}
|
||||
|
||||
private static bool PrefixWriteLine(MyLog __instance, string msg)
|
||||
@@ -117,7 +131,15 @@ namespace Torch.Managers
|
||||
|
||||
private static bool PrefixLogFormatted(MyLog __instance, MyLogSeverity severity, string format, object[] args)
|
||||
{
|
||||
_log.Log(LogLevelFor(severity), PrepareLog(__instance).AppendFormat(format, args));
|
||||
// Sometimes this is called with a pre-formatted string and no args
|
||||
// and causes a crash when the format string contains braces
|
||||
var sb = PrepareLog(__instance);
|
||||
if (args != null && args.Length > 0)
|
||||
sb.AppendFormat(format, args);
|
||||
else
|
||||
sb.Append(format);
|
||||
|
||||
_log.Log(LogLevelFor(severity), sb);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@@ -116,7 +116,7 @@ namespace Torch.Managers
|
||||
protected void RaiseClientJoined(ulong steamId)
|
||||
{
|
||||
var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected };
|
||||
_log.Info($"Player {vm.Name} joined ({vm.SteamId}");
|
||||
_log.Info($"Player {vm.Name} joined ({vm.SteamId})");
|
||||
Players.Add(steamId, vm);
|
||||
PlayerJoined?.Invoke(vm);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,396 +1,133 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Utils;
|
||||
using VRage;
|
||||
using VRage.Library.Collections;
|
||||
using VRage.Network;
|
||||
using VRageMath;
|
||||
|
||||
namespace Torch.Managers
|
||||
{
|
||||
public class NetworkManager : Manager, INetworkManager
|
||||
public static class NetworkManager
|
||||
{
|
||||
private static Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string _myTransportLayerField = "TransportLayer";
|
||||
private const string _transportHandlersField = "m_handlers";
|
||||
private readonly HashSet<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
|
||||
|
||||
private static Dictionary<MethodInfo, Delegate> _delegateCache = new Dictionary<MethodInfo, Delegate>();
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
private static Func<T, TA> GetDelegate<T, TA>(MethodInfo method) where TA : class
|
||||
{
|
||||
//default(EndpointId) tells the network to broadcast the message
|
||||
RaiseEvent(method, obj, default(EndpointId), args);
|
||||
if (!_delegateCache.TryGetValue(method, out var del))
|
||||
{
|
||||
del = (Func<T, TA>)(x => Delegate.CreateDelegate(typeof(TA), x, method) as TA);
|
||||
_delegateCache[method] = del;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
return (Func<T, TA>)del;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public static void RaiseEvent<T1>(T1 instance, MethodInfo method, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
|
||||
{
|
||||
if (method == null)
|
||||
throw new ArgumentNullException(nameof(method), "MethodInfo cannot be null!");
|
||||
var del = GetDelegate<T1, Action>(method);
|
||||
|
||||
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)
|
||||
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()))}");
|
||||
MyMultiplayer.RaiseEvent(instance, del, target);
|
||||
}
|
||||
|
||||
//create a generic method of DispatchEvent and invoke to inject our data into the network
|
||||
var dispatch = DispatchEventInfo.MakeGenericMethod(argTypes);
|
||||
dispatch.Invoke(MyMultiplayer.ReplicationLayer, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
public static void RaiseEvent<T1, T2>(T1 instance, MethodInfo method, T2 arg1, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
|
||||
{
|
||||
//default(EndpointId) tells the network to broadcast the message
|
||||
RaiseStaticEvent(method, default(EndpointId), args);
|
||||
var del = GetDelegate<T1, Action<T2>> (method);
|
||||
|
||||
MyMultiplayer.RaiseEvent(instance, del, arg1, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
public static void RaiseEvent<T1, T2, T3>(T1 instance, MethodInfo method, T2 arg1, T3 arg2, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner
|
||||
{
|
||||
RaiseEvent(method, null, new EndpointId(steamId), args);
|
||||
var del = GetDelegate<T1, Action<T2, T3>>(method);
|
||||
|
||||
MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
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
|
||||
{
|
||||
RaiseEvent(method, null, endpoint, args);
|
||||
var del = GetDelegate<T1, Action<T2, T3, T4>>(method);
|
||||
|
||||
MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, target);
|
||||
}
|
||||
|
||||
private CallSite TryGetStaticCallSite(MethodInfo method)
|
||||
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
|
||||
{
|
||||
MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer);
|
||||
if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result))
|
||||
throw new MissingMemberException("Provided event target not found!");
|
||||
return result;
|
||||
var del = GetDelegate<T1, Action<T2, T3, T4, T5>>(method);
|
||||
|
||||
MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, target);
|
||||
}
|
||||
|
||||
private CallSite TryGetCallSite(MethodInfo method, object arg)
|
||||
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
|
||||
{
|
||||
MySynchronizedTypeInfo typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(arg.GetType());
|
||||
if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result))
|
||||
throw new MissingMemberException("Provided event target not found!");
|
||||
return result;
|
||||
var del = GetDelegate<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
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ namespace Torch.Managers.PatchManager
|
||||
private DynamicMethod AllocatePatchMethod()
|
||||
{
|
||||
Debug.Assert(_method.DeclaringType != null);
|
||||
var methodName = _method.Name + $"_{_patchSalt++}";
|
||||
var methodName = "Patched_" + _method.DeclaringType.FullName + _method.Name + $"_{_patchSalt++}";
|
||||
var returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
|
||||
var parameters = _method.GetParameters();
|
||||
var parameterTypes = (_method.IsStatic ? Enumerable.Empty<Type>() : new[] {typeof(object)})
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NLog;
|
||||
|
||||
namespace Torch.Managers.PatchManager
|
||||
{
|
||||
@@ -10,6 +11,7 @@ namespace Torch.Managers.PatchManager
|
||||
public sealed class PatchContext
|
||||
{
|
||||
private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>();
|
||||
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
internal PatchContext()
|
||||
{
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,8 +10,8 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using Octokit;
|
||||
using Torch.API;
|
||||
using Torch.API.WebAPI;
|
||||
|
||||
namespace Torch.Managers
|
||||
{
|
||||
@@ -21,7 +21,6 @@ namespace Torch.Managers
|
||||
public class UpdateManager : Manager
|
||||
{
|
||||
private Timer _updatePollTimer;
|
||||
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
|
||||
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
|
||||
private Logger _log = LogManager.GetCurrentClassLogger();
|
||||
[Dependency]
|
||||
@@ -43,48 +42,33 @@ namespace Torch.Managers
|
||||
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()
|
||||
{
|
||||
// Doesn't work properly or reliably, TODO update when Jenkins is fully configured
|
||||
return;
|
||||
|
||||
if (!Torch.Config.GetTorchUpdates)
|
||||
if (Torch.Config.NoUpdate || !Torch.Config.GetTorchUpdates)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false);
|
||||
if (releaseInfo.Item1 > Torch.TorchVersion)
|
||||
var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Branch);
|
||||
if (job == null)
|
||||
{
|
||||
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
|
||||
_log.Info("Failed to fetch latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.Version > Torch.TorchVersion)
|
||||
{
|
||||
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {job.Version}");
|
||||
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip");
|
||||
new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
|
||||
//new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
|
||||
if (!await JenkinsQuery.Instance.DownloadRelease(job, updateName))
|
||||
{
|
||||
_log.Warn("Failed to download new release!");
|
||||
return;
|
||||
}
|
||||
UpdateFromZip(updateName, _torchDir);
|
||||
File.Delete(updateName);
|
||||
_log.Warn($"Torch version {releaseInfo.Item1} has been installed, please restart Torch to finish the process.");
|
||||
_log.Warn($"Torch version {job.Version} has been installed, please restart Torch to finish the process.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -104,12 +88,16 @@ namespace Torch.Managers
|
||||
{
|
||||
foreach (var file in zip.Entries)
|
||||
{
|
||||
if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName)))
|
||||
continue;
|
||||
|
||||
_log.Debug($"Unzipping {file.FullName}");
|
||||
var targetFile = Path.Combine(extractPath, file.FullName);
|
||||
_fsManager.SoftDelete(targetFile);
|
||||
_fsManager.SoftDelete(extractPath, file.FullName);
|
||||
file.ExtractToFile(targetFile, true);
|
||||
}
|
||||
|
||||
zip.ExtractToDirectory(extractPath);
|
||||
//zip.ExtractToDirectory(extractPath); //throws exceptions sometimes?
|
||||
}
|
||||
}
|
||||
|
||||
|
29
Torch/MySteamServiceWrapper.cs
Normal file
29
Torch/MySteamServiceWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ namespace Torch.Patches
|
||||
public static class GameAnalyticsPatch
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
private static Action<ILogger> _setLogger;
|
||||
private static Action<ILogger, ILogger> _setLogger;
|
||||
|
||||
public static void Patch(PatchContext ctx)
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace Torch.Patches
|
||||
return;
|
||||
}
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
_setLogger = loggerField?.CreateSetter<ILogger>();
|
||||
_setLogger = loggerField?.CreateSetter<ILogger, ILogger>();
|
||||
FixLogging();
|
||||
|
||||
ConstructorInfo ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], new ParameterModifier[0]);
|
||||
@@ -42,7 +42,7 @@ namespace Torch.Patches
|
||||
|
||||
private static void FixLogging()
|
||||
{
|
||||
_setLogger(LogManager.GetLogger("GameAnalytics"));
|
||||
_setLogger(null, LogManager.GetLogger("GameAnalytics"));
|
||||
if (!(LogManager.Configuration is XmlLoggingConfiguration))
|
||||
LogManager.Configuration = new XmlLoggingConfiguration(Path.Combine(
|
||||
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) ?? Environment.CurrentDirectory, "NLog.config"));
|
||||
|
@@ -19,7 +19,7 @@ namespace Torch.Patches
|
||||
|
||||
internal static void Patch(PatchContext target)
|
||||
{
|
||||
ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] { typeof(string[]) });
|
||||
ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] { typeof(string[]), typeof(IntPtr) });
|
||||
if (ctor == null)
|
||||
throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
|
||||
target.GetPattern(ctor).Prefixes.Add(MethodRef(PrefixConstructor));
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user