Merge branch 'staging'

This commit is contained in:
Westin Miller
2017-12-02 12:16:23 -08:00
144 changed files with 9750 additions and 1666 deletions

63
Jenkinsfile vendored
View File

@@ -1,3 +1,21 @@
def packageAndArchive(buildMode, packageName, exclude) {
zipFile = "bin\\${packageName}.zip"
packageDir = "bin\\${packageName}\\"
bat "IF EXIST ${zipFile} DEL ${zipFile}"
bat "IF EXIST ${packageDir} RMDIR /S /Q ${packageDir}"
bat "xcopy bin\\x64\\${buildMode} ${packageDir}"
if (exclude.length() > 0) {
bat "del ${packageDir}${exclude}"
}
if (buildMode == "Release") {
bat "del ${packageDir}*.pdb"
}
powershell "Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\"\$PWD\\${packageDir}\", \"\$PWD\\${zipFile}\")"
archiveArtifacts artifacts: zipFile, caseSensitive: false, onlyIfSuccessful: true
}
node { node {
stage('Checkout') { stage('Checkout') {
checkout scm checkout scm
@@ -16,12 +34,29 @@ node {
stage('Build') { stage('Build') {
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim() currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim()
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=Release /p:Platform=x64" if (env.BRANCH_NAME == "master") {
buildMode = "Release"
} else {
buildMode = "Debug"
}
bat "IF EXIST \"bin\" rmdir /Q /S \"bin\""
bat "IF EXIST \"bin-test\" rmdir /Q /S \"bin-test\""
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=${buildMode} /p:Platform=x64 /t:Clean"
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=${buildMode} /p:Platform=x64"
}
stage('Archive') {
archiveArtifacts artifacts: "bin/x64/${buildMode}/Torch*", caseSensitive: false, fingerprint: true, onlyIfSuccessful: true
packageAndArchive(buildMode, "torch-server", "Torch.Client*")
packageAndArchive(buildMode, "torch-client", "Torch.Server*")
} }
stage('Test') { stage('Test') {
bat 'IF NOT EXIST reports MKDIR reports' bat 'IF NOT EXIST reports MKDIR reports'
bat "\"packages/xunit.runner.console.2.2.0/tools/xunit.console.exe\" \"bin-test/x64/Release/Torch.Tests.dll\" \"bin-test/x64/Release/Torch.Server.Tests.dll\" \"bin-test/x64/Release/Torch.Client.Tests.dll\" -parallel none -xml \"reports/Torch.Tests.xml\"" bat "\"packages/xunit.runner.console.2.2.0/tools/xunit.console.exe\" \"bin-test/x64/${buildMode}/Torch.Tests.dll\" \"bin-test/x64/${buildMode}/Torch.Server.Tests.dll\" \"bin-test/x64/${buildMode}/Torch.Client.Tests.dll\" -parallel none -xml \"reports/Torch.Tests.xml\""
step([ step([
$class: 'XUnitBuilder', $class: 'XUnitBuilder',
thresholdMode: 1, thresholdMode: 1,
@@ -37,30 +72,14 @@ node {
]) ])
} }
stage('Archive') { if (env.BRANCH_NAME == "master") {
bat '''IF EXIST bin\\torch-server.zip DEL bin\\torch-server.zip
IF EXIST bin\\package-server RMDIR /S /Q bin\\package-server
xcopy bin\\x64\\Release bin\\package-server\\
del bin\\package-server\\Torch.Client*'''
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-server\\\", \\\"\$PWD\\bin\\torch-server.zip\\\")\""
archiveArtifacts artifacts: 'bin/torch-server.zip', caseSensitive: false, onlyIfSuccessful: true
bat '''IF EXIST bin\\torch-client.zip DEL bin\\torch-client.zip
IF EXIST bin\\package-client RMDIR /S /Q bin\\package-client
xcopy bin\\x64\\Release bin\\package-client\\
del bin\\package-client\\Torch.Server*'''
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-client\\\", \\\"\$PWD\\bin\\torch-client.zip\\\")\""
archiveArtifacts artifacts: 'bin/torch-client.zip', caseSensitive: false, onlyIfSuccessful: true
archiveArtifacts artifacts: 'bin/x64/Release/Torch*', caseSensitive: false, fingerprint: true, onlyIfSuccessful: true
}
gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim() gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim()
gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim() gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim()
if (gitVersion == gitSimpleVersion) { if (gitVersion == gitSimpleVersion) {
stage('Release') { stage('${buildMode}') {
withCredentials([usernamePassword(credentialsId: 'torch-github', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) { withCredentials([usernamePassword(credentialsId: 'torch-github', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
powershell "& ./Jenkins/release.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")" powershell "& ./Jenkins/${buildMode}.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")"
}
} }
} }
} }

View File

@@ -1,15 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 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> <targets>
<target xsi:type="File" name="main" layout="${time} [${level:uppercase=true}] ${logger}: ${message}" fileName="Logs\Torch-${shortdate}.log" /> <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="File" name="chat" layout="${longdate} ${message}" fileName="Logs\Chat.log" />
<target xsi:type="ColoredConsole" name="console" layout="${time} [${level:uppercase=true}] ${logger}: ${message}" /> <target xsi:type="ColoredConsole" name="console" layout="${var:logStamp} ${logger}: ${var:logContent}" />
<target xsi:type="File" name="patch" layout="${var:logContent}" fileName="Logs\patch.log"/>
</targets> </targets>
<rules> <rules>
<logger name="Keen" minlevel="Info" writeTo="console"/>
<logger name="Keen" minlevel="Debug" writeTo="keen" final="true" />
<logger name="Keen" writeTo="null" final="true" />
<logger name="*" minlevel="Info" writeTo="main, console" /> <logger name="*" minlevel="Info" writeTo="main, console" />
<logger name="Chat" minlevel="Info" writeTo="chat" /> <logger name="Chat" minlevel="Info" writeTo="chat" />
<!--<logger name="Torch.Managers.PatchManager.*" minlevel="Trace" writeTo="patch"/>-->
</rules> </rules>
</nlog> </nlog>

View File

@@ -0,0 +1,28 @@
using System;
namespace Torch.API.Event
{
/// <summary>
/// Attribute indicating that a method should be invoked when the event occurs.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class EventHandlerAttribute : Attribute
{
/// <summary>
/// Events are executed from low priority to high priority.
/// </summary>
/// <remarks>
/// While this may seem unintuitive this gives the high priority events the final say on changing/canceling events.
/// </remarks>
public int Priority { get; set; } = 0;
/// <summary>
/// Specifies if this handler should ignore a consumed event.
/// </summary>
/// <remarks>
/// If <see cref="SkipCancelled"/> is <em>true</em> and the event is cancelled by a lower priority handler this handler won't be invoked.
/// </remarks>
/// <seealso cref="IEvent.Cancelled"/>
public bool SkipCancelled { get; set; } = false;
}
}

11
Torch.API/Event/IEvent.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace Torch.API.Event
{
public interface IEvent
{
/// <summary>
/// An event that has been cancelled will no be processed in the default manner.
/// </summary>
/// <seealso cref="EventHandlerAttribute.SkipCancelled"/>
bool Cancelled { get; }
}
}

View File

@@ -0,0 +1,9 @@
namespace Torch.API.Event
{
/// <summary>
/// Interface used to tag an event handler. This does <b>not</b> register it with the event manager.
/// </summary>
public interface IEventHandler
{
}
}

View File

@@ -0,0 +1,27 @@
using System.Runtime.CompilerServices;
namespace Torch.API.Event
{
/// <summary>
/// Manager class responsible for registration of event handlers.
/// </summary>
public interface IEventManager
{
/// <summary>
/// Registers all event handler methods contained in the given instance
/// </summary>
/// <param name="handler">Instance to register</param>
/// <returns><b>true</b> if added, <b>false</b> otherwise</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
bool RegisterHandler(IEventHandler handler);
/// <summary>
/// Unregisters all event handler methods contained in the given instance
/// </summary>
/// <param name="handler">Instance to unregister</param>
/// <returns><b>true</b> if removed, <b>false</b> otherwise</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
bool UnregisterHandler(IEventHandler handler);
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API
{
public interface IChatMessage
{
/// <summary>
/// The time the message was created.
/// </summary>
DateTime Timestamp { get; }
/// <summary>
/// The SteamID of the message author.
/// </summary>
ulong SteamId { get; }
/// <summary>
/// The name of the message author.
/// </summary>
string Name { get; }
/// <summary>
/// The content of the message.
/// </summary>
string Message { get; }
}
}

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Torch.API.Managers; using Torch.API.Managers;
@@ -44,10 +45,6 @@ namespace Torch.API
/// </summary> /// </summary>
ITorchConfig Config { get; } ITorchConfig Config { get; }
/// <inheritdoc cref="IMultiplayerManager"/>
[Obsolete]
IMultiplayerManager Multiplayer { get; }
/// <inheritdoc cref="IPluginManager"/> /// <inheritdoc cref="IPluginManager"/>
[Obsolete] [Obsolete]
IPluginManager Plugins { get; } IPluginManager Plugins { get; }
@@ -55,6 +52,12 @@ namespace Torch.API
/// <inheritdoc cref="IDependencyManager"/> /// <inheritdoc cref="IDependencyManager"/>
IDependencyManager Managers { get; } IDependencyManager Managers { get; }
[Obsolete("Prefer using Managers.GetManager for global managers")]
T GetManager<T>() where T : class, IManager;
[Obsolete("Prefer using Managers.AddManager for global managers")]
bool AddManager<T>(T manager) where T : class, IManager;
/// <summary> /// <summary>
/// The binary version of the current instance. /// The binary version of the current instance.
/// </summary> /// </summary>
@@ -63,18 +66,18 @@ namespace Torch.API
/// <summary> /// <summary>
/// Invoke an action on the game thread. /// Invoke an action on the game thread.
/// </summary> /// </summary>
void Invoke(Action action); void Invoke(Action action, [CallerMemberName] string caller = "");
/// <summary> /// <summary>
/// Invoke an action on the game thread and block until it has completed. /// Invoke an action on the game thread and block until it has completed.
/// If this is called on the game thread the action will execute immediately. /// If this is called on the game thread the action will execute immediately.
/// </summary> /// </summary>
void InvokeBlocking(Action action); void InvokeBlocking(Action action, [CallerMemberName] string caller = "");
/// <summary> /// <summary>
/// Invoke an action on the game thread asynchronously. /// Invoke an action on the game thread asynchronously.
/// </summary> /// </summary>
Task InvokeAsync(Action action); Task InvokeAsync(Action action, [CallerMemberName] string caller = "");
/// <summary> /// <summary>
/// Start the Torch instance. /// Start the Torch instance.
@@ -101,6 +104,16 @@ namespace Torch.API
/// Initialize the Torch instance. /// Initialize the Torch instance.
/// </summary> /// </summary>
void Init(); void Init();
/// <summary>
/// The current state of the game this instance of torch is controlling.
/// </summary>
TorchGameState GameState { get; }
/// <summary>
/// Event raised when <see cref="GameState"/> changes.
/// </summary>
event TorchGameStateChangedDel GameStateChanged;
} }
/// <summary> /// <summary>
@@ -108,6 +121,11 @@ namespace Torch.API
/// </summary> /// </summary>
public interface ITorchServer : ITorchBase public interface ITorchServer : ITorchBase
{ {
/// <summary>
/// The current <see cref="ServerState"/>
/// </summary>
ServerState State { get; }
/// <summary> /// <summary>
/// Path of the dedicated instance folder. /// Path of the dedicated instance folder.
/// </summary> /// </summary>

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using VRage.Game;
using VRage.Network;
using VRage.Replication;
namespace Torch.API.Managers
{
/// <summary>
/// Represents a scripted or user chat message.
/// </summary>
public struct TorchChatMessage
{
/// <summary>
/// Creates a new torch chat message with the given author and message.
/// </summary>
/// <param name="author">Author's name</param>
/// <param name="message">Message</param>
/// <param name="font">Font</param>
public TorchChatMessage(string author, string message, string font = MyFontEnum.Blue)
{
Timestamp = DateTime.Now;
AuthorSteamId = null;
Author = author;
Message = message;
Font = font;
}
/// <summary>
/// Creates a new torch chat message with the given author and message.
/// </summary>
/// <param name="author">Author's name</param>
/// <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)
{
Timestamp = DateTime.Now;
AuthorSteamId = authorSteamId;
Author = author;
Message = message;
Font = font;
}
/// <summary>
/// Creates a new torch chat message with the given author and message.
/// </summary>
/// <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)
{
Timestamp = DateTime.Now;
AuthorSteamId = authorSteamId;
Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player";
Message = message;
Font = font;
}
/// <summary>
/// This message's timestamp.
/// </summary>
public readonly DateTime Timestamp;
/// <summary>
/// The author's steam ID, if available. Else, null.
/// </summary>
public readonly ulong? AuthorSteamId;
/// <summary>
/// The author's name, if available. Else, null.
/// </summary>
public readonly string Author;
/// <summary>
/// The message contents.
/// </summary>
public readonly string Message;
/// <summary>
/// The font, or null if default.
/// </summary>
public readonly string Font;
}
/// <summary>
/// Callback used to indicate that a messaage has been recieved.
/// </summary>
/// <param name="msg"></param>
/// <param name="consumed">If true, this event has been consumed and should be ignored</param>
public delegate void MessageRecievedDel(TorchChatMessage msg, ref bool consumed);
/// <summary>
/// Callback used to indicate the user is attempting to send a message locally.
/// </summary>
/// <param name="msg">Message the user is attempting to send</param>
/// <param name="consumed">If true, this event has been consumed and should be ignored</param>
public delegate void MessageSendingDel(string msg, ref bool consumed);
public interface IChatManagerClient : IManager
{
/// <summary>
/// Event that is raised when a message addressed to us is recieved. <see cref="MessageRecievedDel"/>
/// </summary>
event MessageRecievedDel MessageRecieved;
/// <summary>
/// Event that is raised when we are attempting to send a message. <see cref="MessageSendingDel"/>
/// </summary>
event MessageSendingDel MessageSending;
/// <summary>
/// Triggers the <see cref="MessageSending"/> event,
/// typically raised by the user entering text into the chat window.
/// </summary>
/// <param name="message">The message to send</param>
void SendMessageAsSelf(string message);
/// <summary>
/// Displays a message on the UI given an author name and a message.
/// </summary>
/// <param name="author">Author name</param>
/// <param name="message">Message content</param>
/// <param name="font">font to use</param>
void DisplayMessageOnSelf(string author, string message, string font = "Blue" );
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VRage.Network;
namespace Torch.API.Managers
{
/// <summary>
/// Callback used to indicate the server has recieved a message to process and forward on to others.
/// </summary>
/// <param name="authorId">Steam ID of the user sending a message</param>
/// <param name="msg">Message the user is attempting to send</param>
/// <param name="consumed">If true, this event has been consumed and should be ignored</param>
public delegate void MessageProcessingDel(TorchChatMessage msg, ref bool consumed);
public interface IChatManagerServer : IChatManagerClient
{
/// <summary>
/// Event triggered when the server has recieved a message and should process it. <see cref="MessageProcessingDel"/>
/// </summary>
event MessageProcessingDel MessageProcessing;
/// <summary>
/// Sends a message with the given author and message to the given player, or all players by default.
/// </summary>
/// <param name="authorId">Author's steam ID</param>
/// <param name="message">The message to send</param>
/// <param name="targetSteamId">Player to send the message to, or everyone by default</param>
void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0);
/// <summary>
/// Sends a scripted message with the given author and message to the given player, or all players by default.
/// </summary>
/// <param name="author">Author name</param>
/// <param name="message">The message to send</param>
/// <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);
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using VRage.Game;
using VRage.Game.ModAPI;
namespace Torch.API.Managers
{
/// <summary>
/// Delegate for received messages.
/// </summary>
/// <param name="message">Message data.</param>
/// <param name="sendToOthers">Flag to broadcast message to other players.</param>
public delegate void MessageReceivedDel(IChatMessage message, ref bool sendToOthers);
/// <summary>
/// API for multiplayer related functions.
/// </summary>
public interface IMultiplayerManager : IManager
{
/// <summary>
/// Fired when a player joins.
/// </summary>
event Action<IPlayer> PlayerJoined;
/// <summary>
/// Fired when a player disconnects.
/// </summary>
event Action<IPlayer> PlayerLeft;
/// <summary>
/// Fired when a chat message is received.
/// </summary>
event MessageReceivedDel MessageReceived;
/// <summary>
/// Send a chat message to all or one specific player.
/// </summary>
void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Blue);
/// <summary>
/// Kicks the player from the game.
/// </summary>
void KickPlayer(ulong steamId);
/// <summary>
/// Bans or unbans a player from the game.
/// </summary>
void BanPlayer(ulong steamId, bool banned = true);
/// <summary>
/// Gets a player by their Steam64 ID or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerBySteamId(ulong id);
/// <summary>
/// Gets a player by their display name or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerByName(string name);
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using VRage.Game;
using VRage.Game.ModAPI;
namespace Torch.API.Managers
{
/// <summary>
/// API for multiplayer related functions common to servers and clients.
/// </summary>
public interface IMultiplayerManagerBase : IManager
{
/// <summary>
/// Fired when a player joins.
/// </summary>
event Action<IPlayer> PlayerJoined;
/// <summary>
/// Fired when a player disconnects.
/// </summary>
event Action<IPlayer> PlayerLeft;
/// <summary>
/// Gets a player by their Steam64 ID or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerBySteamId(ulong id);
/// <summary>
/// Gets a player by their display name or returns null if the player isn't found.
/// </summary>
IMyPlayer GetPlayerByName(string name);
/// <summary>
/// Gets the steam username of a member's steam ID
/// </summary>
/// <param name="steamId">steam ID</param>
/// <returns>steam username</returns>
string GetSteamUsername(ulong steamId);
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Managers
{
public interface IMultiplayerManagerClient : IMultiplayerManagerBase
{
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Managers
{
/// <summary>
/// API for multiplayer functions that exist on servers and lobbies
/// </summary>
public interface IMultiplayerManagerServer : IMultiplayerManagerBase
{
/// <summary>
/// Kicks the player from the game.
/// </summary>
void KickPlayer(ulong steamId);
/// <summary>
/// Bans or unbans a player from the game.
/// </summary>
void BanPlayer(ulong steamId, bool banned = true);
/// <summary>
/// List of the banned SteamID's
/// </summary>
IReadOnlyList<ulong> BannedPlayers { get; }
/// <summary>
/// Checks if the player with the given SteamID is banned.
/// </summary>
/// <param name="steamId">The SteamID of the player.</param>
/// <returns>True if the player is banned; otherwise false.</returns>
bool IsBanned(ulong steamId);
}
}

View File

@@ -18,6 +18,12 @@ namespace Torch.API.Managers
/// Register a network handler. /// Register a network handler.
/// </summary> /// </summary>
void RegisterNetworkHandler(INetworkHandler handler); void RegisterNetworkHandler(INetworkHandler handler);
/// <summary>
/// Unregister a network handler.
/// </summary>
/// <returns>true if the handler was unregistered, false if it wasn't registered to begin with</returns>
bool UnregisterNetworkHandler(INetworkHandler handler);
} }
/// <summary> /// <summary>
@@ -33,6 +39,7 @@ namespace Torch.API.Managers
/// <summary> /// <summary>
/// Processes a network message. /// Processes a network message.
/// </summary> /// </summary>
/// <returns>true if the message should be discarded</returns>
bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet); bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
} }
} }

View File

@@ -14,12 +14,12 @@ namespace Torch.API.Managers
/// <summary> /// <summary>
/// Fired when plugins are loaded. /// Fired when plugins are loaded.
/// </summary> /// </summary>
event Action<IList<ITorchPlugin>> PluginsLoaded; event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
/// <summary> /// <summary>
/// Collection of loaded plugins. /// Collection of loaded plugins.
/// </summary> /// </summary>
IList<ITorchPlugin> Plugins { get; } IReadOnlyDictionary<Guid, ITorchPlugin> Plugins { get; }
/// <summary> /// <summary>
/// Updates all loaded plugins. /// Updates all loaded plugins.

View File

@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
/// <summary> /// <summary>
/// The version of the plugin. /// The version of the plugin.
/// </summary> /// </summary>
Version Version { get; } string Version { get; }
/// <summary> /// <summary>
/// The name of the plugin. /// The name of the plugin.

View File

@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
/// <summary> /// <summary>
/// Indicates that the given type should be loaded by the plugin manager as a plugin. /// Indicates that the given type should be loaded by the plugin manager as a plugin.
/// </summary> /// </summary>
[Obsolete("All plugin meta-information is now defined in the manifest.xml.")]
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute public class PluginAttribute : Attribute
{ {

View File

@@ -25,5 +25,15 @@ namespace Torch.API.Session
/// <inheritdoc cref="IDependencyManager"/> /// <inheritdoc cref="IDependencyManager"/>
IDependencyManager Managers { get; } IDependencyManager Managers { get; }
/// <summary>
/// The current state of the session
/// </summary>
TorchSessionState State { get; }
/// <summary>
/// Event raised when the <see cref="State"/> changes.
/// </summary>
event TorchSessionStateChangedDel StateChanged;
} }
} }

View File

@@ -27,6 +27,11 @@ namespace Torch.API.Session
/// </summary> /// </summary>
ITorchSession CurrentSession { get; } ITorchSession CurrentSession { get; }
/// <summary>
/// Raised when any <see cref="ITorchSession"/> <see cref="ITorchSession.State"/> changes.
/// </summary>
event TorchSessionStateChangedDel SessionStateChanged;
/// <summary> /// <summary>
/// Adds the given factory as a supplier for session based managers /// Adds the given factory as a supplier for session based managers
/// </summary> /// </summary>

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Session
{
/// <summary>
/// Represents the state of a <see cref="ITorchSession"/>
/// </summary>
public enum TorchSessionState
{
/// <summary>
/// The session has been created, and is now loading.
/// </summary>
Loading,
/// <summary>
/// The session has loaded, and is now running.
/// </summary>
Loaded,
/// <summary>
/// The session was running, and is now unloading.
/// </summary>
Unloading,
/// <summary>
/// The session was unloading, and is now unloaded and stopped.
/// </summary>
Unloaded
}
/// <summary>
/// Callback raised when a session's state changes
/// </summary>
/// <param name="session">The session who had a state change</param>
/// <param name="newState">The session's new state</param>
public delegate void TorchSessionStateChangedDel(ITorchSession session, TorchSessionState newState);
}

View File

@@ -160,15 +160,22 @@
<Link>Properties\AssemblyVersion.cs</Link> <Link>Properties\AssemblyVersion.cs</Link>
</Compile> </Compile>
<Compile Include="ConnectionState.cs" /> <Compile Include="ConnectionState.cs" />
<Compile Include="IChatMessage.cs" />
<Compile Include="ITorchConfig.cs" /> <Compile Include="ITorchConfig.cs" />
<Compile Include="Managers\DependencyManagerExtensions.cs" /> <Compile Include="Managers\DependencyManagerExtensions.cs" />
<Compile Include="Managers\DependencyProviderExtensions.cs" /> <Compile Include="Managers\DependencyProviderExtensions.cs" />
<Compile Include="Event\EventHandlerAttribute.cs" />
<Compile Include="Event\IEvent.cs" />
<Compile Include="Event\IEventHandler.cs" />
<Compile Include="Managers\IChatManagerClient.cs" />
<Compile Include="Managers\IChatManagerServer.cs" />
<Compile Include="Managers\IDependencyManager.cs" /> <Compile Include="Managers\IDependencyManager.cs" />
<Compile Include="Managers\IDependencyProvider.cs" /> <Compile Include="Managers\IDependencyProvider.cs" />
<Compile Include="Event\IEventManager.cs" />
<Compile Include="Managers\IManager.cs" /> <Compile Include="Managers\IManager.cs" />
<Compile Include="Managers\IMultiplayerManager.cs" /> <Compile Include="Managers\IMultiplayerManagerClient.cs" />
<Compile Include="Managers\IMultiplayerManagerBase.cs" />
<Compile Include="IPlayer.cs" /> <Compile Include="IPlayer.cs" />
<Compile Include="Managers\IMultiplayerManagerServer.cs" />
<Compile Include="Managers\INetworkManager.cs" /> <Compile Include="Managers\INetworkManager.cs" />
<Compile Include="Managers\IPluginManager.cs" /> <Compile Include="Managers\IPluginManager.cs" />
<Compile Include="Plugins\ITorchPlugin.cs" /> <Compile Include="Plugins\ITorchPlugin.cs" />
@@ -182,6 +189,8 @@
<Compile Include="ModAPI\TorchAPI.cs" /> <Compile Include="ModAPI\TorchAPI.cs" />
<Compile Include="Session\ITorchSession.cs" /> <Compile Include="Session\ITorchSession.cs" />
<Compile Include="Session\ITorchSessionManager.cs" /> <Compile Include="Session\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" />
<Compile Include="TorchGameState.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox;
namespace Torch.API
{
/// <summary>
/// Represents the state of a <see cref="MySandboxGame"/>
/// </summary>
public enum TorchGameState
{
/// <summary>
/// The game is currently being created.
/// </summary>
Creating,
/// <summary>
/// The game has been created and is ready to begin loading.
/// </summary>
Created,
/// <summary>
/// The game is currently loading.
/// </summary>
Loading,
/// <summary>
/// The game is fully loaded and ready to start sessions
/// </summary>
Loaded,
/// <summary>
/// The game is beginning the unload sequence
/// </summary>
Unloading,
/// <summary>
/// The game has been shutdown and is no longer active
/// </summary>
Unloaded
}
/// <summary>
/// Callback raised when a game's state changes
/// </summary>
/// <param name="game">The game who had a state change</param>
/// <param name="newState">The game's new state</param>
public delegate void TorchGameStateChangedDel(MySandboxGame game, TorchGameState newState);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Torch.Client; using Torch.Client;
using Torch.Tests; using Torch.Tests;
using Torch.Utils; using Torch.Utils;
@@ -29,6 +30,10 @@ namespace Torch.Client.Tests
public static IEnumerable<object[]> Invokers => Manager().Invokers; public static IEnumerable<object[]> Invokers => Manager().Invokers;
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
public static IEnumerable<object[]> Events => Manager().Events;
#region Binding #region Binding
[Theory] [Theory]
[MemberData(nameof(Getters))] [MemberData(nameof(Getters))]
@@ -62,6 +67,28 @@ namespace Torch.Client.Tests
if (field.Field.IsStatic) if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null)); Assert.NotNull(field.Field.GetValue(null));
} }
[Theory]
[MemberData(nameof(MemberInfo))]
public void TestBindingMemberInfo(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion #endregion
} }
} }

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
namespace Torch.Client.Manager
{
public class MultiplayerManagerClient : MultiplayerManagerBase, IMultiplayerManagerClient
{
/// <inheritdoc />
public MultiplayerManagerClient(ITorchBase torch) : base(torch) { }
/// <inheritdoc />
public override void Attach()
{
base.Attach();
MyMultiplayer.Static.ClientJoined += RaiseClientJoined;
}
/// <inheritdoc />
public override void Detach()
{
MyMultiplayer.Static.ClientJoined -= RaiseClientJoined;
base.Detach();
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
namespace Torch.Client.Manager
{
public class MultiplayerManagerLobby : MultiplayerManagerBase, IMultiplayerManagerServer
{
/// <inheritdoc />
public IReadOnlyList<ulong> BannedPlayers => new List<ulong>();
/// <inheritdoc />
public MultiplayerManagerLobby(ITorchBase torch) : base(torch) { }
/// <inheritdoc />
public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
/// <inheritdoc />
public void BanPlayer(ulong steamId, bool banned = true) => Torch.Invoke(() => MyMultiplayer.Static.BanClient(steamId, banned));
/// <inheritdoc />
public bool IsBanned(ulong steamId) => false;
/// <inheritdoc/>
public override void Attach()
{
base.Attach();
MyMultiplayer.Static.ClientJoined += RaiseClientJoined;
}
/// <inheritdoc/>
public override void Detach()
{
MyMultiplayer.Static.ClientJoined -= RaiseClientJoined;
base.Detach();
}
}
}

View File

@@ -121,6 +121,8 @@
<Compile Include="..\Versioning\AssemblyVersion.cs"> <Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link> <Link>Properties\AssemblyVersion.cs</Link>
</Compile> </Compile>
<Compile Include="Manager\MultiplayerManagerClient.cs" />
<Compile Include="Manager\MultiplayerManagerLobby.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TorchClient.cs" /> <Compile Include="TorchClient.cs" />
<Compile Include="TorchClientConfig.cs" /> <Compile Include="TorchClientConfig.cs" />
@@ -167,6 +169,7 @@
<ItemGroup> <ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" /> <Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" /> <Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup> <PropertyGroup>

View File

@@ -4,12 +4,17 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Windows; using System.Windows;
using Sandbox; using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking; using Sandbox.Engine.Networking;
using Sandbox.Engine.Platform; using Sandbox.Engine.Platform;
using Sandbox.Game; using Sandbox.Game;
using SpaceEngineers.Game; using SpaceEngineers.Game;
using VRage.Steam; using VRage.Steam;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Client.Manager;
using Torch.Session;
using VRage; using VRage;
using VRage.FileSystem; using VRage.FileSystem;
using VRage.GameServices; using VRage.GameServices;
@@ -27,6 +32,13 @@ namespace Torch.Client
public TorchClient() public TorchClient()
{ {
Config = new TorchClientConfig(); Config = new TorchClientConfig();
var sessionManager = Managers.GetManager<ITorchSessionManager>();
sessionManager.AddFactory((x) => MyMultiplayer.Static is MyMultiplayerLobby
? new MultiplayerManagerLobby(this)
: null);
sessionManager.AddFactory((x) => MyMultiplayer.Static is MyMultiplayerClientBase
? new MultiplayerManagerClient(this)
: null);
} }
public override void Init() public override void Init()

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sandbox.Game.Gui;
using Sandbox.Graphics; using Sandbox.Graphics;
using Sandbox.Graphics.GUI; using Sandbox.Graphics.GUI;
using Sandbox.Gui; using Sandbox.Gui;
@@ -16,6 +17,15 @@ namespace Torch.Client
{ {
public class TorchMainMenuScreen : MyGuiScreenMainMenu public class TorchMainMenuScreen : MyGuiScreenMainMenu
{ {
public TorchMainMenuScreen()
: this(false)
{
}
public TorchMainMenuScreen(bool pauseGame)
: base(pauseGame)
{
}
/// <inheritdoc /> /// <inheritdoc />
public override void RecreateControls(bool constructor) public override void RecreateControls(bool constructor)
{ {

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Torch.Tests; using Torch.Tests;
using Torch.Utils; using Torch.Utils;
using Xunit; using Xunit;
@@ -28,6 +29,10 @@ namespace Torch.Server.Tests
public static IEnumerable<object[]> Invokers => Manager().Invokers; public static IEnumerable<object[]> Invokers => Manager().Invokers;
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
public static IEnumerable<object[]> Events => Manager().Events;
#region Binding #region Binding
[Theory] [Theory]
[MemberData(nameof(Getters))] [MemberData(nameof(Getters))]
@@ -61,6 +66,17 @@ namespace Torch.Server.Tests
if (field.Field.IsStatic) if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null)); Assert.NotNull(field.Field.GetValue(null));
} }
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion #endregion
} }
} }

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -27,7 +28,6 @@ login anonymous
app_update 298740 app_update 298740
quit"; quit";
private TorchAssemblyResolver _resolver;
private TorchConfig _config; private TorchConfig _config;
private TorchServer _server; private TorchServer _server;
private string _basePath; private string _basePath;
@@ -45,12 +45,13 @@ quit";
if (_init) if (_init)
return false; return false;
#if !DEBUG
AppDomain.CurrentDomain.UnhandledException += HandleException; AppDomain.CurrentDomain.UnhandledException += HandleException;
#endif
if (!args.Contains("-noupdate")) if (!args.Contains("-noupdate"))
RunSteamCmd(); RunSteamCmd();
_resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64"));
_config = InitConfig(); _config = InitConfig();
if (!_config.Parse(args)) if (!_config.Parse(args))
return false; return false;
@@ -62,11 +63,11 @@ quit";
var pid = int.Parse(_config.WaitForPID); var pid = int.Parse(_config.WaitForPID);
var waitProc = Process.GetProcessById(pid); var waitProc = Process.GetProcessById(pid);
Log.Info("Continuing in 5 seconds."); Log.Info("Continuing in 5 seconds.");
Thread.Sleep(5000); Log.Warn($"Waiting for process {pid} to close");
if (!waitProc.HasExited) while (!waitProc.HasExited)
{ {
Log.Warn($"Killing old process {pid}."); Console.Write(".");
waitProc.Kill(); Thread.Sleep(1000);
} }
} }
@@ -94,8 +95,6 @@ quit";
} }
else else
_server.Start(); _server.Start();
_resolver?.Dispose();
} }
private TorchConfig InitConfig() private TorchConfig InitConfig()
@@ -165,21 +164,33 @@ quit";
} }
} }
private void LogException(Exception ex)
{
if (ex.InnerException != null)
{
LogException(ex.InnerException);
}
Log.Fatal(ex);
if (ex is ReflectionTypeLoadException exti)
foreach (Exception exl in exti.LoaderExceptions)
LogException(exl);
}
private void HandleException(object sender, UnhandledExceptionEventArgs e) private void HandleException(object sender, UnhandledExceptionEventArgs e)
{ {
var ex = (Exception)e.ExceptionObject; var ex = (Exception)e.ExceptionObject;
Log.Fatal(ex); LogException(ex);
Console.WriteLine("Exiting in 5 seconds."); Console.WriteLine("Exiting in 5 seconds.");
Thread.Sleep(5000); Thread.Sleep(5000);
LogManager.Flush();
if (_config.RestartOnCrash) if (_config.RestartOnCrash)
{ {
var exe = typeof(Program).Assembly.Location; var exe = typeof(Program).Assembly.Location;
_config.WaitForPID = Process.GetCurrentProcess().Id.ToString(); _config.WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, _config.ToString()); Process.Start(exe, _config.ToString());
} }
//1627 = Function failed during execution. Process.GetCurrentProcess().Kill();
Environment.Exit(1627);
} }
} }
} }

View File

@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using NLog;
using NLog.Fluent;
using Torch.API;
using Torch.Collections;
using Torch.Managers;
using Torch.Server.ViewModels.Entities;
using Torch.Utils;
namespace Torch.Server.Managers
{
/// <summary>
/// Manager that lets users bind random view models to entities in Torch's Entity Manager
/// </summary>
public class EntityControlManager : Manager
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Creates an entity control manager for the given instance of torch
/// </summary>
/// <param name="torchInstance">Torch instance</param>
internal EntityControlManager(ITorchBase torchInstance) : base(torchInstance)
{
}
private abstract class ModelFactory
{
private readonly ConditionalWeakTable<EntityViewModel, EntityControlViewModel> _models = new ConditionalWeakTable<EntityViewModel, EntityControlViewModel>();
public abstract Delegate Delegate { get; }
protected abstract EntityControlViewModel Create(EntityViewModel evm);
#pragma warning disable 649
[ReflectedGetter(Name = "Keys")]
private static readonly Func<ConditionalWeakTable<EntityViewModel, EntityControlViewModel>, ICollection<EntityViewModel>> _weakTableKeys;
#pragma warning restore 649
/// <summary>
/// Warning: Creates a giant list, avoid if possible.
/// </summary>
internal ICollection<EntityViewModel> Keys => _weakTableKeys(_models);
internal EntityControlViewModel GetOrCreate(EntityViewModel evm)
{
return _models.GetValue(evm, Create);
}
internal bool TryGet(EntityViewModel evm, out EntityControlViewModel res)
{
return _models.TryGetValue(evm, out res);
}
}
private class ModelFactory<T> : ModelFactory where T : EntityViewModel
{
private readonly Func<T, EntityControlViewModel> _factory;
public override Delegate Delegate => _factory;
internal ModelFactory(Func<T, EntityControlViewModel> factory)
{
_factory = factory;
}
protected override EntityControlViewModel Create(EntityViewModel evm)
{
if (evm is T m)
{
var result = _factory(m);
_log.Trace($"Model factory {_factory.Method} created {result} for {evm}");
return result;
}
return null;
}
}
private readonly List<ModelFactory> _modelFactories = new List<ModelFactory>();
private readonly List<Delegate> _controlFactories = new List<Delegate>();
private readonly List<WeakReference<EntityViewModel>> _boundEntityViewModels = new List<WeakReference<EntityViewModel>>();
private readonly ConditionalWeakTable<EntityViewModel, MtObservableList<EntityControlViewModel>> _boundViewModels = new ConditionalWeakTable<EntityViewModel, MtObservableList<EntityControlViewModel>>();
/// <summary>
/// This factory will be used to create component models for matching entity models.
/// </summary>
/// <typeparam name="TEntityBaseModel">entity model type to match</typeparam>
/// <param name="modelFactory">Method to create component model from entity model.</param>
public void RegisterModelFactory<TEntityBaseModel>(Func<TEntityBaseModel, EntityControlViewModel> modelFactory)
where TEntityBaseModel : EntityViewModel
{
if (!typeof(TEntityBaseModel).IsAssignableFrom(modelFactory.Method.GetParameters()[0].ParameterType))
throw new ArgumentException("Generic type must match lamda type", nameof(modelFactory));
lock (this)
{
var factory = new ModelFactory<TEntityBaseModel>(modelFactory);
_modelFactories.Add(factory);
var i = 0;
while (i < _boundEntityViewModels.Count)
{
if (_boundEntityViewModels[i].TryGetTarget(out EntityViewModel target) &&
_boundViewModels.TryGetValue(target, out MtObservableList<EntityControlViewModel> components))
{
if (target is TEntityBaseModel tent)
UpdateBinding(target, components);
i++;
}
else
_boundEntityViewModels.RemoveAtFast(i);
}
}
}
/// <summary>
/// Unregisters a factory registered with <see cref="RegisterModelFactory{TEntityBaseModel}"/>
/// </summary>
/// <typeparam name="TEntityBaseModel">entity model type to match</typeparam>
/// <param name="modelFactory">Method to create component model from entity model.</param>
public void UnregisterModelFactory<TEntityBaseModel>(Func<TEntityBaseModel, EntityControlViewModel> modelFactory)
where TEntityBaseModel : EntityViewModel
{
if (!typeof(TEntityBaseModel).IsAssignableFrom(modelFactory.Method.GetParameters()[0].ParameterType))
throw new ArgumentException("Generic type must match lamda type", nameof(modelFactory));
lock (this)
{
for (var i = 0; i < _modelFactories.Count; i++)
{
if (_modelFactories[i].Delegate == (Delegate)modelFactory)
{
foreach (var entry in _modelFactories[i].Keys)
if (_modelFactories[i].TryGet(entry, out EntityControlViewModel ecvm) && ecvm != null
&& _boundViewModels.TryGetValue(entry, out var binding))
binding.Remove(ecvm);
_modelFactories.RemoveAt(i);
break;
}
}
}
}
/// <summary>
/// This factory will be used to create controls for matching view models.
/// </summary>
/// <typeparam name="TEntityComponentModel">component model to match</typeparam>
/// <param name="controlFactory">Method to create control from component model</param>
public void RegisterControlFactory<TEntityComponentModel>(
Func<TEntityComponentModel, Control> controlFactory)
where TEntityComponentModel : EntityControlViewModel
{
if (!typeof(TEntityComponentModel).IsAssignableFrom(controlFactory.Method.GetParameters()[0].ParameterType))
throw new ArgumentException("Generic type must match lamda type", nameof(controlFactory));
lock (this)
{
_controlFactories.Add(controlFactory);
RefreshControls<TEntityComponentModel>();
}
}
///<summary>
/// Unregisters a factory registered with <see cref="RegisterControlFactory{TEntityComponentModel}"/>
/// </summary>
/// <typeparam name="TEntityComponentModel">component model to match</typeparam>
/// <param name="controlFactory">Method to create control from component model</param>
public void UnregisterControlFactory<TEntityComponentModel>(
Func<TEntityComponentModel, Control> controlFactory)
where TEntityComponentModel : EntityControlViewModel
{
if (!typeof(TEntityComponentModel).IsAssignableFrom(controlFactory.Method.GetParameters()[0].ParameterType))
throw new ArgumentException("Generic type must match lamda type", nameof(controlFactory));
lock (this)
{
_controlFactories.Remove(controlFactory);
RefreshControls<TEntityComponentModel>();
}
}
private void RefreshControls<TEntityComponentModel>() where TEntityComponentModel : EntityControlViewModel
{
var i = 0;
while (i < _boundEntityViewModels.Count)
{
if (_boundEntityViewModels[i].TryGetTarget(out EntityViewModel target) &&
_boundViewModels.TryGetValue(target, out MtObservableList<EntityControlViewModel> components))
{
foreach (EntityControlViewModel component in components)
if (component is TEntityComponentModel)
component.InvalidateControl();
i++;
}
else
_boundEntityViewModels.RemoveAtFast(i);
}
}
/// <summary>
/// Gets the models bound to the given entity view model.
/// </summary>
/// <param name="entity">view model to query</param>
/// <returns></returns>
public MtObservableList<EntityControlViewModel> BoundModels(EntityViewModel entity)
{
return _boundViewModels.GetValue(entity, CreateFreshBinding);
}
/// <summary>
/// Gets a control for the given view model type.
/// </summary>
/// <param name="model">model to create a control for</param>
/// <returns>control, or null if none</returns>
public Control CreateControl(EntityControlViewModel model)
{
lock (this)
foreach (Delegate factory in _controlFactories)
if (factory.Method.GetParameters()[0].ParameterType.IsInstanceOfType(model) &&
factory.DynamicInvoke(model) is Control result)
{
_log.Trace($"Control factory {factory.Method} created {result}");
return result;
}
_log.Warn($"No control created for {model}");
return null;
}
private MtObservableList<EntityControlViewModel> CreateFreshBinding(EntityViewModel key)
{
var binding = new MtObservableList<EntityControlViewModel>();
lock (this)
{
_boundEntityViewModels.Add(new WeakReference<EntityViewModel>(key));
}
binding.PropertyChanged += (x, args) =>
{
if (nameof(binding.IsObserved).Equals(args.PropertyName))
UpdateBinding(key, binding);
};
return binding;
}
private void UpdateBinding(EntityViewModel key, MtObservableList<EntityControlViewModel> binding)
{
if (!binding.IsObserved)
return;
lock (this)
{
foreach (ModelFactory factory in _modelFactories)
{
var result = factory.GetOrCreate(key);
if (result != null && !binding.Contains(result))
binding.Add(result);
}
}
}
}
}

View File

@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
using NLog.Fluent;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using Torch.Utils;
using Torch.ViewModels;
using VRage.GameServices;
using VRage.Network;
using VRage.Steam;
namespace Torch.Server.Managers
{
public class MultiplayerManagerDedicated : MultiplayerManagerBase, IMultiplayerManagerServer
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
#pragma warning disable 649
[ReflectedGetter(Name = "m_members")]
private static Func<MyDedicatedServerBase, List<ulong>> _members;
[ReflectedGetter(Name = "m_waitingForGroup")]
private static Func<MyDedicatedServerBase, HashSet<ulong>> _waitingForGroup;
#pragma warning restore 649
/// <inheritdoc />
public IReadOnlyList<ulong> BannedPlayers => MySandboxGame.ConfigDedicated.Banned;
private Dictionary<ulong, ulong> _gameOwnerIds = new Dictionary<ulong, ulong>();
/// <inheritdoc />
public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) { }
/// <inheritdoc />
public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
/// <inheritdoc />
public void BanPlayer(ulong steamId, bool banned = true)
{
Torch.Invoke(() =>
{
MyMultiplayer.Static.BanClient(steamId, banned);
if (_gameOwnerIds.ContainsKey(steamId))
MyMultiplayer.Static.BanClient(_gameOwnerIds[steamId], banned);
});
}
/// <inheritdoc />
public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || MySandboxGame.ConfigDedicated.Banned.Contains(steamId);
/// <inheritdoc/>
public override void Attach()
{
base.Attach();
_gameServerValidateAuthTicketReplacer = _gameServerValidateAuthTicketFactory.Invoke();
_gameServerUserGroupStatusReplacer = _gameServerUserGroupStatusFactory.Invoke();
_gameServerValidateAuthTicketReplacer.Replace(new Action<ulong, JoinResult, ulong>(ValidateAuthTicketResponse), MyGameService.GameServer);
_gameServerUserGroupStatusReplacer.Replace(new Action<ulong, ulong, bool, bool>(UserGroupStatusResponse), MyGameService.GameServer);
_log.Info("Inserted steam authentication intercept");
}
/// <inheritdoc/>
public override void Detach()
{
if (_gameServerValidateAuthTicketReplacer != null && _gameServerValidateAuthTicketReplacer.Replaced)
_gameServerValidateAuthTicketReplacer.Restore(MyGameService.GameServer);
if (_gameServerUserGroupStatusReplacer != null && _gameServerUserGroupStatusReplacer.Replaced)
_gameServerUserGroupStatusReplacer.Restore(MyGameService.GameServer);
_log.Info("Removed steam authentication intercept");
base.Detach();
}
#pragma warning disable 649
[ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse), typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")]
private static Func<ReflectedEventReplacer> _gameServerValidateAuthTicketFactory;
[ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")]
private static Func<ReflectedEventReplacer> _gameServerUserGroupStatusFactory;
private ReflectedEventReplacer _gameServerValidateAuthTicketReplacer;
private ReflectedEventReplacer _gameServerUserGroupStatusReplacer;
#pragma warning restore 649
#region CustomAuth
#pragma warning disable 649
[ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase), Name = "ConvertSteamIDFrom64")]
private static Func<ulong, string> _convertSteamIDFrom64;
[ReflectedStaticMethod(Type = typeof(MyGameService), Name = "GetServerAccountType")]
private static Func<ulong, MyGameServiceAccountType> _getServerAccountType;
[ReflectedMethod(Name = "UserAccepted")]
private static Action<MyDedicatedServerBase, ulong> _userAcceptedImpl;
[ReflectedMethod(Name = "UserRejected")]
private static Action<MyDedicatedServerBase, ulong, JoinResult> _userRejected;
[ReflectedMethod(Name = "IsClientBanned")]
private static Func<MyMultiplayerBase, ulong, bool> _isClientBanned;
[ReflectedMethod(Name = "IsClientKicked")]
private static Func<MyMultiplayerBase, ulong, bool> _isClientKicked;
[ReflectedMethod(Name = "RaiseClientKicked")]
private static Action<MyMultiplayerBase, ulong> _raiseClientKicked;
#pragma warning restore 649
//Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner)
{
_log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}");
if (IsBanned(steamOwner))
{
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.BannedByAdmins);
_raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
else if (_isClientKicked.Invoke(MyMultiplayer.Static, steamOwner))
{
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.KickedRecently);
_raiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
if (response != JoinResult.OK)
{
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, response);
return;
}
if (MyMultiplayer.Static.MemberLimit > 0 && _members.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Count - 1 >= MyMultiplayer.Static.MemberLimit)
{
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.ServerFull);
return;
}
if (MySandboxGame.ConfigDedicated.GroupID == 0uL ||
MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) ||
MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(steamID)))
{
this.UserAccepted(steamID);
return;
}
if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan)
{
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.GroupIdInvalid);
return;
}
if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
{
_waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Add(steamID);
return;
}
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.SteamServersOffline);
}
private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer)
{
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Remove(userId))
{
if (member || officer)
UserAccepted(userId);
else
_userRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, userId, JoinResult.NotInGroup);
}
}
private void UserAccepted(ulong steamId)
{
_userAcceptedImpl.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamId);
base.RaiseClientJoined(steamId);
}
#endregion
}
}

View File

@@ -44,13 +44,16 @@ namespace Torch.Server
var binDir = Path.Combine(workingDir, "DedicatedServer64"); var binDir = Path.Combine(workingDir, "DedicatedServer64");
Directory.SetCurrentDirectory(workingDir); Directory.SetCurrentDirectory(workingDir);
if (!TorchLauncher.IsTorchWrapped())
{
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName,args, binDir);
return;
}
if (!Environment.UserInteractive) if (!Environment.UserInteractive)
{ {
using (var service = new TorchService()) using (var service = new TorchService())
using (new TorchAssemblyResolver(binDir))
{
ServiceBase.Run(service); ServiceBase.Run(service);
}
return; return;
} }

View File

@@ -182,6 +182,10 @@
<HintPath>..\GameBinaries\VRage.Scripting.dll</HintPath> <HintPath>..\GameBinaries\VRage.Scripting.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="VRage.Steam">
<HintPath>..\GameBinaries\VRage.Steam.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
<Reference Include="PresentationCore" /> <Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" /> <Reference Include="PresentationFramework" />
@@ -191,6 +195,8 @@
<Link>Properties\AssemblyVersion.cs</Link> <Link>Properties\AssemblyVersion.cs</Link>
</Compile> </Compile>
<Compile Include="ListBoxExtensions.cs" /> <Compile Include="ListBoxExtensions.cs" />
<Compile Include="Managers\EntityControlManager.cs" />
<Compile Include="Managers\MultiplayerManagerDedicated.cs" />
<Compile Include="Managers\InstanceManager.cs" /> <Compile Include="Managers\InstanceManager.cs" />
<Compile Include="NativeMethods.cs" /> <Compile Include="NativeMethods.cs" />
<Compile Include="Initializer.cs" /> <Compile Include="Initializer.cs" />
@@ -209,6 +215,13 @@
<Compile Include="ViewModels\Entities\Blocks\PropertyViewModel.cs" /> <Compile Include="ViewModels\Entities\Blocks\PropertyViewModel.cs" />
<Compile Include="ViewModels\Entities\CharacterViewModel.cs" /> <Compile Include="ViewModels\Entities\CharacterViewModel.cs" />
<Compile Include="ViewModels\ConfigDedicatedViewModel.cs" /> <Compile Include="ViewModels\ConfigDedicatedViewModel.cs" />
<Compile Include="ViewModels\Entities\EntityControlViewModel.cs" />
<Compile Include="Views\Entities\EntityControlHost.xaml.cs">
<DependentUpon>EntityControlHost.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Entities\EntityControlsView.xaml.cs">
<DependentUpon>EntityControlsView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\EntityTreeViewModel.cs" /> <Compile Include="ViewModels\EntityTreeViewModel.cs" />
<Compile Include="ViewModels\Entities\EntityViewModel.cs" /> <Compile Include="ViewModels\Entities\EntityViewModel.cs" />
<Compile Include="ViewModels\Entities\FloatingObjectViewModel.cs" /> <Compile Include="ViewModels\Entities\FloatingObjectViewModel.cs" />
@@ -300,6 +313,14 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Include="Views\Entities\EntityControlHost.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Entities\EntityControlsView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\AddWorkshopItemsDialog.xaml"> <Page Include="Views\AddWorkshopItemsDialog.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -328,6 +349,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\PluginsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Entities\VoxelMapView.xaml"> <Page Include="Views\Entities\VoxelMapView.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@@ -340,10 +365,6 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Views\PluginsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\TorchUI.xaml"> <Page Include="Views\TorchUI.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>

View File

@@ -3,20 +3,25 @@ using Sandbox.Engine.Utils;
using Sandbox.Game; using Sandbox.Game;
using Sandbox.Game.World; using Sandbox.Game.World;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Principal; using System.Security.Principal;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Xml.Serialization.GeneratedAssembly; using Microsoft.Xml.Serialization.GeneratedAssembly;
using NLog;
using Sandbox.Engine.Analytics; using Sandbox.Engine.Analytics;
using Sandbox.Game.Multiplayer; using Sandbox.Game.Multiplayer;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using SteamSDK; using SteamSDK;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers; using Torch.Managers;
using Torch.Server.Managers; using Torch.Server.Managers;
using Torch.Utils; using Torch.Utils;
@@ -38,11 +43,17 @@ namespace Torch.Server
public class TorchServer : TorchBase, ITorchServer public class TorchServer : TorchBase, ITorchServer
{ {
//public MyConfigDedicated<MyObjectBuilder_SessionSettings> DedicatedConfig { get; set; } //public MyConfigDedicated<MyObjectBuilder_SessionSettings> DedicatedConfig { get; set; }
/// <inheritdoc />
public float SimulationRatio { get => _simRatio; set { _simRatio = value; OnPropertyChanged(); } } public float SimulationRatio { get => _simRatio; set { _simRatio = value; OnPropertyChanged(); } }
/// <inheritdoc />
public TimeSpan ElapsedPlayTime { get => _elapsedPlayTime; set { _elapsedPlayTime = value; OnPropertyChanged(); } } public TimeSpan ElapsedPlayTime { get => _elapsedPlayTime; set { _elapsedPlayTime = value; OnPropertyChanged(); } }
/// <inheritdoc />
public Thread GameThread { get; private set; } public Thread GameThread { get; private set; }
/// <inheritdoc />
public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } } public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } }
/// <inheritdoc />
public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } } public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } }
/// <inheritdoc />
public InstanceManager DedicatedInstance { get; } public InstanceManager DedicatedInstance { get; }
/// <inheritdoc /> /// <inheritdoc />
public string InstanceName => Config?.InstanceName; public string InstanceName => Config?.InstanceName;
@@ -57,12 +68,16 @@ namespace Torch.Server
private Timer _watchdog; private Timer _watchdog;
private Stopwatch _uptime; private Stopwatch _uptime;
/// <inheritdoc />
public TorchServer(TorchConfig config = null) public TorchServer(TorchConfig config = null)
{ {
DedicatedInstance = new InstanceManager(this); DedicatedInstance = new InstanceManager(this);
AddManager(DedicatedInstance); AddManager(DedicatedInstance);
AddManager(new EntityControlManager(this));
Config = config ?? new TorchConfig(); Config = config ?? new TorchConfig();
MyFakes.ENABLE_INFINARIO = false;
var sessionManager = Managers.GetManager<ITorchSessionManager>();
sessionManager.AddFactory((x) => new MultiplayerManagerDedicated(this));
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -89,8 +104,17 @@ namespace Torch.Server
MyPlugins.Load(); MyPlugins.Load();
MyGlobalTypeMetadata.Static.Init(); MyGlobalTypeMetadata.Static.Init();
Managers.GetManager<ITorchSessionManager>().SessionStateChanged += OnSessionStateChanged;
GetManager<InstanceManager>().LoadInstance(Config.InstancePath); GetManager<InstanceManager>().LoadInstance(Config.InstancePath);
Plugins.LoadPlugins(); }
private void OnSessionStateChanged(ITorchSession session, TorchSessionState newState)
{
if (newState == TorchSessionState.Unloading || newState == TorchSessionState.Unloaded)
{
_watchdog?.Dispose();
_watchdog = null;
}
} }
private void InvokeBeforeRun() private void InvokeBeforeRun()
@@ -140,6 +164,7 @@ namespace Torch.Server
Log.Info("Starting server."); Log.Info("Starting server.");
MySandboxGame.IsDedicated = true; MySandboxGame.IsDedicated = true;
MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model;
Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString()); Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString());
VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); }; VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); };
@@ -190,14 +215,67 @@ namespace Torch.Server
((TorchServer)state).Invoke(() => mre.Set()); ((TorchServer)state).Invoke(() => mre.Set());
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
{ {
var mainThread = MySandboxGame.Static.UpdateThread; #if DEBUG
if (mainThread.IsAlive) Log.Error($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
mainThread.Suspend(); Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
var stackTrace = new StackTrace(mainThread, true); #else
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.\n{stackTrace}"); Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
#endif
}
else
{
Log.Debug("Server watchdog responded");
}
} }
Log.Debug("Server watchdog responded"); private static string DumpFrozenThread(Thread thread, int traces = 3, int pause = 5000)
{
var stacks = new List<string>(traces);
var totalSize = 0;
for (var i = 0; i < traces; i++)
{
string dump = DumpStack(thread).ToString();
totalSize += dump.Length;
stacks.Add(dump);
Thread.Sleep(pause);
}
string commonPrefix = StringUtils.CommonSuffix(stacks);
// Advance prefix to include the line terminator.
commonPrefix = commonPrefix.Substring(commonPrefix.IndexOf('\n') + 1);
var result = new StringBuilder(totalSize - (stacks.Count - 1) * commonPrefix.Length);
result.AppendLine($"Frozen thread dump {thread.Name}");
result.AppendLine("Common prefix:").AppendLine(commonPrefix);
for (var i = 0; i < stacks.Count; i++)
if (stacks[i].Length > commonPrefix.Length)
{
result.AppendLine($"Suffix {i}");
result.AppendLine(stacks[i].Substring(0, stacks[i].Length - commonPrefix.Length));
}
return result.ToString();
}
private static StackTrace DumpStack(Thread thread)
{
try
{
thread.Suspend();
}
catch
{
// ignored
}
var stack = new StackTrace(thread, true);
try
{
thread.Resume();
}
catch
{
// ignored
}
return stack;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -220,9 +298,11 @@ namespace Torch.Server
MySandboxGame.Static.Exit(); MySandboxGame.Static.Exit();
Log.Info("Server stopped."); Log.Info("Server stopped.");
LogManager.Flush();
_stopHandle.Set(); _stopHandle.Set();
State = ServerState.Stopped; State = ServerState.Stopped;
IsRunning = false; IsRunning = false;
Process.GetCurrentProcess().Kill();
} }
/// <summary> /// <summary>
@@ -232,8 +312,12 @@ namespace Torch.Server
{ {
var exe = Assembly.GetExecutingAssembly().Location; var exe = Assembly.GetExecutingAssembly().Location;
((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString(); ((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString();
Config.Autostart = true;
Process.Start(exe, Config.ToString()); Process.Start(exe, Config.ToString());
Environment.Exit(0); Save(0).Wait();
Stop();
LogManager.Flush();
Process.GetCurrentProcess().Kill();
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -249,27 +333,32 @@ namespace Torch.Server
/// <param name="callerId">Caller of the save operation</param> /// <param name="callerId">Caller of the save operation</param>
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0) private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
{ {
string response = null;
switch (statusCode) switch (statusCode)
{ {
case SaveGameStatus.Success: case SaveGameStatus.Success:
Log.Info("Save completed."); Log.Info("Save completed.");
Multiplayer.SendMessage("Saved game.", playerId: callerId); response = "Saved game.";
break; break;
case SaveGameStatus.SaveInProgress: case SaveGameStatus.SaveInProgress:
Log.Error("Save failed, a save is already in progress."); Log.Error("Save failed, a save is already in progress.");
Multiplayer.SendMessage("Save failed, a save is already in progress.", playerId: callerId, font: MyFontEnum.Red); response = "Save failed, a save is already in progress.";
break; break;
case SaveGameStatus.GameNotReady: case SaveGameStatus.GameNotReady:
Log.Error("Save failed, game was not ready."); Log.Error("Save failed, game was not ready.");
Multiplayer.SendMessage("Save failed, game was not ready.", playerId: callerId, font: MyFontEnum.Red); response = "Save failed, game was not ready.";
break; break;
case SaveGameStatus.TimedOut: case SaveGameStatus.TimedOut:
Log.Error("Save failed, save timed out."); Log.Error("Save failed, save timed out.");
Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red); response = "Save failed, save timed out.";
break; break;
default: default:
break; break;
} }
if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result))
{
Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther("Server", response, statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId);
}
} }
} }
} }

View File

@@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using Sandbox.Engine.Utils; using Sandbox.Engine.Utils;
using Torch.Collections;
using VRage.Game; using VRage.Game;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
@@ -25,6 +26,7 @@ namespace Torch.Server.ViewModels
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated) public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
{ {
_config = configDedicated; _config = configDedicated;
_config.IgnoreLastSession = true;
SessionSettings = new SessionSettingsViewModel(_config.SessionSettings); SessionSettings = new SessionSettingsViewModel(_config.SessionSettings);
Administrators = string.Join(Environment.NewLine, _config.Administrators); Administrators = string.Join(Environment.NewLine, _config.Administrators);
Banned = string.Join(Environment.NewLine, _config.Banned); Banned = string.Join(Environment.NewLine, _config.Banned);
@@ -52,13 +54,15 @@ namespace Torch.Server.ViewModels
Log.Warn($"'{mod}' is not a valid mod ID."); Log.Warn($"'{mod}' is not a valid mod ID.");
} }
// Never ever
_config.IgnoreLastSession = true;
_config.Save(path); _config.Save(path);
} }
private SessionSettingsViewModel _sessionSettings; private SessionSettingsViewModel _sessionSettings;
public SessionSettingsViewModel SessionSettings { get => _sessionSettings; set { _sessionSettings = value; OnPropertyChanged(); } } public SessionSettingsViewModel SessionSettings { get => _sessionSettings; set { _sessionSettings = value; OnPropertyChanged(); } }
public ObservableList<string> WorldPaths { get; } = new ObservableList<string>(); public MtObservableList<string> WorldPaths { get; } = new MtObservableList<string>();
private string _administrators; private string _administrators;
public string Administrators { get => _administrators; set { _administrators = value; OnPropertyChanged(); } } public string Administrators { get => _administrators; set { _administrators = value; OnPropertyChanged(); } }
private string _banned; private string _banned;
@@ -78,12 +82,6 @@ namespace Torch.Server.ViewModels
set { _config.GroupID = value; OnPropertyChanged(); } set { _config.GroupID = value; OnPropertyChanged(); }
} }
public bool IgnoreLastSession
{
get { return _config.IgnoreLastSession; }
set { _config.IgnoreLastSession = value; OnPropertyChanged(); }
}
public string IP public string IP
{ {
get { return _config.IP; } get { return _config.IP; }

View File

@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Sandbox.Game.Entities.Cube; using Sandbox.Game.Entities.Cube;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using Sandbox.ModAPI.Interfaces; using Sandbox.ModAPI.Interfaces;
using Torch.Collections;
using Torch.Server.ViewModels.Entities; using Torch.Server.ViewModels.Entities;
namespace Torch.Server.ViewModels.Blocks namespace Torch.Server.ViewModels.Blocks
@@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Blocks
public class BlockViewModel : EntityViewModel public class BlockViewModel : EntityViewModel
{ {
public IMyTerminalBlock Block { get; } public IMyTerminalBlock Block { get; }
public ObservableList<PropertyViewModel> Properties { get; } = new ObservableList<PropertyViewModel>(); public MtObservableList<PropertyViewModel> Properties { get; } = new MtObservableList<PropertyViewModel>();
public string FullName => $"{Block.CubeGrid.CustomName} - {Block.CustomName}"; public string FullName => $"{Block.CubeGrid.CustomName} - {Block.CustomName}";

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Torch.Server.ViewModels.Entities
{
public class EntityControlViewModel : ViewModel
{
internal const string SignalPropertyInvalidateControl =
"InvalidateControl-4124a476-704f-4762-8b5e-336a18e2f7e5";
internal void InvalidateControl()
{
// ReSharper disable once ExplicitCallerInfoArgument
OnPropertyChanged(SignalPropertyInvalidateControl);
}
private bool _hide;
/// <summary>
/// Should this element be forced into the <see cref="Visibility.Collapsed"/>
/// </summary>
public bool Hide
{
get => _hide;
protected set
{
if (_hide == value)
return;
_hide = value;
OnPropertyChanged();
}
}
}
}

View File

@@ -1,4 +1,8 @@
using VRage.Game.ModAPI; using System.Windows.Controls;
using Torch.API.Managers;
using Torch.Collections;
using Torch.Server.Managers;
using VRage.Game.ModAPI;
using VRage.ModAPI; using VRage.ModAPI;
using VRageMath; using VRageMath;
@@ -7,9 +11,25 @@ namespace Torch.Server.ViewModels.Entities
public class EntityViewModel : ViewModel public class EntityViewModel : ViewModel
{ {
protected EntityTreeViewModel Tree { get; } protected EntityTreeViewModel Tree { get; }
public IMyEntity Entity { get; }
private IMyEntity _backing;
public IMyEntity Entity
{
get => _backing;
protected set
{
_backing = value;
OnPropertyChanged();
EntityControls = TorchBase.Instance?.Managers.GetManager<EntityControlManager>()?.BoundModels(this);
// ReSharper disable once ExplicitCallerInfoArgument
OnPropertyChanged(nameof(EntityControls));
}
}
public long Id => Entity.EntityId; public long Id => Entity.EntityId;
public MtObservableList<EntityControlViewModel> EntityControls { get; private set; }
public virtual string Name public virtual string Name
{ {
get => Entity.DisplayName; get => Entity.DisplayName;

View File

@@ -2,6 +2,8 @@
using System.Linq; using System.Linq;
using Sandbox.Game.Entities; using Sandbox.Game.Entities;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using Torch.API.Managers;
using Torch.Collections;
using Torch.Server.ViewModels.Blocks; using Torch.Server.ViewModels.Blocks;
namespace Torch.Server.ViewModels.Entities namespace Torch.Server.ViewModels.Entities
@@ -9,7 +11,7 @@ namespace Torch.Server.ViewModels.Entities
public class GridViewModel : EntityViewModel, ILazyLoad public class GridViewModel : EntityViewModel, ILazyLoad
{ {
private MyCubeGrid Grid => (MyCubeGrid)Entity; private MyCubeGrid Grid => (MyCubeGrid)Entity;
public ObservableList<BlockViewModel> Blocks { get; } = new ObservableList<BlockViewModel>(); public MtObservableList<BlockViewModel> Blocks { get; } = new MtObservableList<BlockViewModel>();
/// <inheritdoc /> /// <inheritdoc />
public string DescriptiveName { get; } public string DescriptiveName { get; }
@@ -34,7 +36,7 @@ namespace Torch.Server.ViewModels.Entities
{ {
var block = obj.FatBlock as IMyTerminalBlock; var block = obj.FatBlock as IMyTerminalBlock;
if (block != null) if (block != null)
Blocks.Insert(new BlockViewModel(block, Tree), b => b.Name); Blocks.Add(new BlockViewModel(block, Tree));
OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Name));
} }

View File

@@ -4,6 +4,7 @@ using Sandbox.Game.Entities;
using VRage.Game.Entity; using VRage.Game.Entity;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
using System.Threading.Tasks; using System.Threading.Tasks;
using Torch.Collections;
namespace Torch.Server.ViewModels.Entities namespace Torch.Server.ViewModels.Entities
{ {
@@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Entities
public override bool CanStop => false; public override bool CanStop => false;
public ObservableList<GridViewModel> AttachedGrids { get; } = new ObservableList<GridViewModel>(); public MtObservableList<GridViewModel> AttachedGrids { get; } = new MtObservableList<GridViewModel>();
public async Task UpdateAttachedGrids() public async Task UpdateAttachedGrids()
{ {

View File

@@ -11,16 +11,19 @@ using VRage.Game.ModAPI;
using VRage.ModAPI; using VRage.ModAPI;
using System.Windows.Threading; using System.Windows.Threading;
using NLog; using NLog;
using Torch.Collections;
namespace Torch.Server.ViewModels namespace Torch.Server.ViewModels
{ {
public class EntityTreeViewModel : ViewModel public class EntityTreeViewModel : ViewModel
{ {
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
//TODO: these should be sorted sets for speed //TODO: these should be sorted sets for speed
public ObservableList<GridViewModel> Grids { get; set; } = new ObservableList<GridViewModel>(); public MtObservableList<GridViewModel> Grids { get; set; } = new MtObservableList<GridViewModel>();
public ObservableList<CharacterViewModel> Characters { get; set; } = new ObservableList<CharacterViewModel>(); public MtObservableList<CharacterViewModel> Characters { get; set; } = new MtObservableList<CharacterViewModel>();
public ObservableList<EntityViewModel> FloatingObjects { get; set; } = new ObservableList<EntityViewModel>(); public MtObservableList<EntityViewModel> FloatingObjects { get; set; } = new MtObservableList<EntityViewModel>();
public ObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new ObservableList<VoxelMapViewModel>(); public MtObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableList<VoxelMapViewModel>();
public Dispatcher ControlDispatcher => _control.Dispatcher; public Dispatcher ControlDispatcher => _control.Dispatcher;
private EntityViewModel _currentEntity; private EntityViewModel _currentEntity;
@@ -29,7 +32,7 @@ namespace Torch.Server.ViewModels
public EntityViewModel CurrentEntity public EntityViewModel CurrentEntity
{ {
get => _currentEntity; get => _currentEntity;
set { _currentEntity = value; OnPropertyChanged(); } set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); }
} }
public EntityTreeViewModel(UserControl control) public EntityTreeViewModel(UserControl control)
@@ -44,6 +47,8 @@ namespace Torch.Server.ViewModels
} }
private void MyEntities_OnEntityRemove(VRage.Game.Entity.MyEntity obj) private void MyEntities_OnEntityRemove(VRage.Game.Entity.MyEntity obj)
{
try
{ {
switch (obj) switch (obj)
{ {
@@ -61,24 +66,38 @@ namespace Torch.Server.ViewModels
break; break;
} }
} }
catch (Exception e)
{
_log.Error(e);
// ignore error "it's only UI"
}
}
private void MyEntities_OnEntityAdd(VRage.Game.Entity.MyEntity obj) private void MyEntities_OnEntityAdd(VRage.Game.Entity.MyEntity obj)
{
try
{ {
switch (obj) switch (obj)
{ {
case MyCubeGrid grid: case MyCubeGrid grid:
Grids.Insert(new GridViewModel(grid, this), g => g.Name); Grids.Add(new GridViewModel(grid, this));
break; break;
case MyCharacter character: case MyCharacter character:
Characters.Insert(new CharacterViewModel(character, this), c => c.Name); Characters.Add(new CharacterViewModel(character, this));
break; break;
case MyFloatingObject floating: case MyFloatingObject floating:
FloatingObjects.Insert(new FloatingObjectViewModel(floating, this), f => f.Name); FloatingObjects.Add(new FloatingObjectViewModel(floating, this));
break; break;
case MyVoxelBase voxel: case MyVoxelBase voxel:
VoxelMaps.Insert(new VoxelMapViewModel(voxel, this), v => v.Name); VoxelMaps.Add(new VoxelMapViewModel(voxel, this));
break; break;
} }
} }
catch (Exception e)
{
_log.Error(e);
// ignore error "it's only UI"
}
}
} }
} }

View File

@@ -6,18 +6,19 @@ using System.Threading.Tasks;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.API.Plugins; using Torch.API.Plugins;
using Torch.Collections;
namespace Torch.Server.ViewModels namespace Torch.Server.ViewModels
{ {
public class PluginManagerViewModel : ViewModel public class PluginManagerViewModel : ViewModel
{ {
public ObservableList<PluginViewModel> Plugins { get; } = new ObservableList<PluginViewModel>(); public MtObservableList<PluginViewModel> Plugins { get; } = new MtObservableList<PluginViewModel>();
private PluginViewModel _selectedPlugin; private PluginViewModel _selectedPlugin;
public PluginViewModel SelectedPlugin public PluginViewModel SelectedPlugin
{ {
get => _selectedPlugin; get => _selectedPlugin;
set { _selectedPlugin = value; OnPropertyChanged(); } set { _selectedPlugin = value; OnPropertyChanged(nameof(SelectedPlugin)); }
} }
public PluginManagerViewModel() { } public PluginManagerViewModel() { }
@@ -29,7 +30,7 @@ namespace Torch.Server.ViewModels
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded; pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
} }
private void PluginManager_PluginsLoaded(IList<ITorchPlugin> obj) private void PluginManager_PluginsLoaded(IReadOnlyCollection<ITorchPlugin> obj)
{ {
Plugins.Clear(); Plugins.Clear();
foreach (var plugin in obj) foreach (var plugin in obj)

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using SharpDX.Toolkit.Collections; using SharpDX.Toolkit.Collections;
using Torch.Collections;
using VRage.Game; using VRage.Game;
using VRage.Library.Utils; using VRage.Library.Utils;
@@ -35,7 +36,7 @@ namespace Torch.Server.ViewModels
BlockLimits.Add(new BlockLimitViewModel(this, limit.Key, limit.Value)); BlockLimits.Add(new BlockLimitViewModel(this, limit.Key, limit.Value));
} }
public ObservableList<BlockLimitViewModel> BlockLimits { get; } = new ObservableList<BlockLimitViewModel>(); public MtObservableList<BlockLimitViewModel> BlockLimits { get; } = new MtObservableList<BlockLimitViewModel>();
#region Multipliers #region Multipliers
@@ -362,6 +363,19 @@ namespace Torch.Server.ViewModels
get => _settings.WorldSizeKm; set { _settings.WorldSizeKm = value; OnPropertyChanged(); } get => _settings.WorldSizeKm; set { _settings.WorldSizeKm = value; OnPropertyChanged(); }
} }
/// <inheritdoc cref="MyObjectBuilder_SessionSettings.ProceduralDensity"/>
public float ProceduralDensity
{
get => _settings.ProceduralDensity; set { _settings.ProceduralDensity = value; OnPropertyChanged(); }
}
/// <inheritdoc cref="MyObjectBuilder_SessionSettings.ProceduralSeed"/>
public int ProceduralSeed
{
get => _settings.ProceduralSeed;
set { _settings.ProceduralSeed = value; OnPropertyChanged(); }
}
/// <summary /> /// <summary />
public static implicit operator MyObjectBuilder_SessionSettings(SessionSettingsViewModel viewModel) public static implicit operator MyObjectBuilder_SessionSettings(SessionSettingsViewModel viewModel)
{ {

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
@@ -20,7 +21,11 @@ using Sandbox.Engine.Multiplayer;
using Sandbox.Game.World; using Sandbox.Game.World;
using SteamSDK; using SteamSDK;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers; using Torch.Managers;
using Torch.Server.Managers;
using VRage.Game;
namespace Torch.Server namespace Torch.Server
{ {
@@ -30,7 +35,6 @@ namespace Torch.Server
public partial class ChatControl : UserControl public partial class ChatControl : UserControl
{ {
private TorchBase _server; private TorchBase _server;
private MultiplayerManager _multiplayer;
public ChatControl() public ChatControl()
{ {
@@ -40,34 +44,76 @@ namespace Torch.Server
public void BindServer(ITorchServer server) public void BindServer(ITorchServer server)
{ {
_server = (TorchBase)server; _server = (TorchBase)server;
_multiplayer = (MultiplayerManager)server.Multiplayer; Dispatcher.InvokeAsync(() =>
DataContext = _multiplayer; {
ChatItems.Inlines.Clear(); ChatItems.Inlines.Clear();
_multiplayer.ChatHistory.ForEach(InsertMessage); });
if (_multiplayer.ChatHistory is INotifyCollectionChanged ncc)
ncc.CollectionChanged += ChatHistory_CollectionChanged; var sessionManager = server.Managers.GetManager<ITorchSessionManager>();
ChatScroller.ScrollToBottom(); if (sessionManager != null)
sessionManager.SessionStateChanged += SessionStateChanged;
} }
private void ChatHistory_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void SessionStateChanged(ITorchSession session, TorchSessionState state)
{
switch (state)
{
case TorchSessionState.Loading:
Dispatcher.InvokeAsync(() => ChatItems.Inlines.Clear());
break;
case TorchSessionState.Loaded:
{
var chatMgr = session.Managers.GetManager<IChatManagerClient>();
if (chatMgr != null)
chatMgr.MessageRecieved += OnMessageRecieved;
}
break;
case TorchSessionState.Unloading:
{
var chatMgr = session.Managers.GetManager<IChatManagerClient>();
if (chatMgr != null)
chatMgr.MessageRecieved -= OnMessageRecieved;
}
break;
case TorchSessionState.Unloaded:
break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
private void OnMessageRecieved(TorchChatMessage msg, ref bool consumed)
{ {
foreach (IChatMessage msg in e.NewItems)
InsertMessage(msg); InsertMessage(msg);
} }
private void InsertMessage(IChatMessage msg) private static readonly Dictionary<string, Brush> _brushes = new Dictionary<string, Brush>();
private static Brush LookupBrush(string font)
{
if (_brushes.TryGetValue(font, out Brush result))
return result;
Brush brush = typeof(Brushes).GetField(font, BindingFlags.Static)?.GetValue(null) as Brush ?? Brushes.Blue;
_brushes.Add(font, brush);
return brush;
}
private void InsertMessage(TorchChatMessage msg)
{
if (Dispatcher.CheckAccess())
{ {
bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight; bool atBottom = ChatScroller.VerticalOffset + 8 > ChatScroller.ScrollableHeight;
var span = new Span(); var span = new Span();
span.Inlines.Add($"{msg.Timestamp} "); span.Inlines.Add($"{msg.Timestamp} ");
span.Inlines.Add(new Run(msg.Name) { Foreground = msg.Name == "Server" ? Brushes.DarkBlue : Brushes.Blue }); span.Inlines.Add(new Run(msg.Author) { Foreground = LookupBrush(msg.Font) });
span.Inlines.Add($": {msg.Message}"); span.Inlines.Add($": {msg.Message}");
span.Inlines.Add(new LineBreak()); span.Inlines.Add(new LineBreak());
ChatItems.Inlines.Add(span); ChatItems.Inlines.Add(span);
if (atBottom) if (atBottom)
ChatScroller.ScrollToBottom(); ChatScroller.ScrollToBottom();
} }
else
Dispatcher.InvokeAsync(() => InsertMessage(msg));
}
private void SendButton_Click(object sender, RoutedEventArgs e) private void SendButton_Click(object sender, RoutedEventArgs e)
{ {
@@ -87,27 +133,22 @@ namespace Torch.Server
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
return; return;
var commands = _server.Commands; var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
if (commands.IsCommand(text)) if (commands != null && commands.IsCommand(text))
{ {
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", text)); InsertMessage(new TorchChatMessage("Server", text, MyFontEnum.DarkBlue));
_server.Invoke(() => _server.Invoke(() =>
{ {
var response = commands.HandleCommandFromServer(text); string response = commands.HandleCommandFromServer(text);
Dispatcher.BeginInvoke(() => OnMessageEntered_Callback(response)); if (!string.IsNullOrWhiteSpace(response))
InsertMessage(new TorchChatMessage("Server", response, MyFontEnum.Blue));
}); });
} }
else else
{ {
_server.Multiplayer.SendMessage(text); _server.CurrentSession?.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf(text);
} }
Message.Text = ""; Message.Text = "";
} }
private void OnMessageEntered_Callback(string response)
{
if (!string.IsNullOrEmpty(response))
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
}
} }
} }

View File

@@ -45,7 +45,6 @@
<Label Content=":" Width="12" /> <Label Content=":" Width="12" />
<TextBox Text="{Binding Port}" Width="48" Height="20" /> <TextBox Text="{Binding Port}" Width="48" Height="20" />
</StackPanel> </StackPanel>
<CheckBox IsChecked="{Binding IgnoreLastSession}" Content="Ignore Last Session" Margin="3" />
<CheckBox IsChecked="{Binding PauseGameWhenEmpty}" Content="Pause When Empty" Margin="3" /> <CheckBox IsChecked="{Binding PauseGameWhenEmpty}" Content="Pause When Empty" Margin="3" />
</StackPanel> </StackPanel>
<StackPanel Margin="3"> <StackPanel Margin="3">
@@ -174,6 +173,14 @@
DockPanel.Dock="Left" /> DockPanel.Dock="Left" />
<Label Content="Environment Hostility" /> <Label Content="Environment Hostility" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding ProceduralDensity}" Margin="3" Width="70" />
<Label Content="Procedural Density" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding ProceduralSeed}" Margin="3" Width="70" />
<Label Content="Procedural Seed" />
</StackPanel>
</StackPanel> </StackPanel>
</Expander> </Expander>
<Expander Header="Players"> <Expander Header="Players">

View File

@@ -5,6 +5,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server.Views.Blocks" xmlns:local="clr-namespace:Torch.Server.Views.Blocks"
xmlns:blocks="clr-namespace:Torch.Server.ViewModels.Blocks" xmlns:blocks="clr-namespace:Torch.Server.ViewModels.Blocks"
xmlns:entities="clr-namespace:Torch.Server.Views.Entities"
xmlns:entities1="clr-namespace:Torch.Server.ViewModels.Entities"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.DataContext> <UserControl.DataContext>
<blocks:BlockViewModel /> <blocks:BlockViewModel />
@@ -12,6 +14,7 @@
<Grid Margin="3"> <Grid Margin="3">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition/> <RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
@@ -22,7 +25,8 @@
</StackPanel> </StackPanel>
<Label Content="Properties"/> <Label Content="Properties"/>
</StackPanel> </StackPanel>
<ListView Grid.Row="1" ItemsSource="{Binding Properties}" Margin="3" IsEnabled="True"> <Expander Grid.Row="1" Header="Block Properties">
<ListView ItemsSource="{Binding Properties}" Margin="3" IsEnabled="True">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate> <DataTemplate>
<local:PropertyView /> <local:PropertyView />
@@ -39,5 +43,9 @@
</Style> </Style>
</ListView.ItemContainerStyle> </ListView.ItemContainerStyle>
</ListView> </ListView>
</Expander>
<ScrollViewer Grid.Row="2" Margin="3" VerticalScrollBarVisibility="Auto">
<entities:EntityControlsView DataContext="{Binding}"/>
</ScrollViewer>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -0,0 +1,8 @@
<UserControl x:Class="Torch.Server.Views.Entities.EntityControlHost"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
</UserControl>

View File

@@ -0,0 +1,72 @@
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using Torch.Server.Managers;
using Torch.API.Managers;
using Torch.Server.ViewModels.Entities;
namespace Torch.Server.Views.Entities
{
/// <summary>
/// Interaction logic for EntityControlHost.xaml
/// </summary>
public partial class EntityControlHost : UserControl
{
public EntityControlHost()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is ViewModel vmo)
{
vmo.PropertyChanged -= DataContext_OnPropertyChanged;
}
if (e.NewValue is ViewModel vmn)
{
vmn.PropertyChanged += DataContext_OnPropertyChanged;
}
RefreshControl();
}
private void DataContext_OnPropertyChanged(object sender, PropertyChangedEventArgs pa)
{
if (pa.PropertyName.Equals(EntityControlViewModel.SignalPropertyInvalidateControl))
RefreshControl();
else if (pa.PropertyName.Equals(nameof(EntityControlViewModel.Hide)))
RefreshVisibility();
}
private Control _currentControl;
private void RefreshControl()
{
if (Dispatcher.Thread != Thread.CurrentThread)
{
Dispatcher.InvokeAsync(RefreshControl);
return;
}
_currentControl = DataContext is EntityControlViewModel ecvm
? TorchBase.Instance?.Managers.GetManager<EntityControlManager>()?.CreateControl(ecvm)
: null;
Content = _currentControl;
RefreshVisibility();
}
private void RefreshVisibility()
{
if (Dispatcher.Thread != Thread.CurrentThread)
{
Dispatcher.InvokeAsync(RefreshVisibility);
return;
}
Visibility = (DataContext is EntityControlViewModel ecvm) && !ecvm.Hide && _currentControl != null
? Visibility.Visible
: Visibility.Collapsed;
}
}
}

View File

@@ -0,0 +1,31 @@
<ItemsControl x:Class="Torch.Server.Views.Entities.EntityControlsView"
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:entities="clr-namespace:Torch.Server.Views.Entities"
xmlns:modelsEntities="clr-namespace:Torch.Server.ViewModels.Entities"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding EntityControls}">
<ItemsControl.DataContext>
<modelsEntities:EntityViewModel/>
</ItemsControl.DataContext>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<entities:EntityControlHost DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Control.VerticalContentAlignment" Value="Stretch"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

View File

@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace Torch.Server.Views.Entities
{
/// <summary>
/// Interaction logic for EntityControlsView.xaml
/// </summary>
public partial class EntityControlsView : ItemsControl
{
public EntityControlsView()
{
InitializeComponent();
}
}
}

View File

@@ -3,20 +3,28 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Torch.Server.Views.Entities"
xmlns:entities="clr-namespace:Torch.Server.ViewModels.Entities" xmlns:entities="clr-namespace:Torch.Server.ViewModels.Entities"
xmlns:local="clr-namespace:Torch.Server.Views.Entities"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.DataContext> <UserControl.DataContext>
<entities:GridViewModel /> <entities:GridViewModel />
</UserControl.DataContext> </UserControl.DataContext>
<StackPanel> <Grid>
<StackPanel Orientation="Horizontal"> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Label Content="Name" Width="100"/> <Label Content="Name" Width="100"/>
<TextBox Text="{Binding Name}" Margin="3"/> <TextBox Text="{Binding Name}" Margin="3"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Grid.Row="1" Orientation="Horizontal">
<Label Content="Position" Width="100"/> <Label Content="Position" Width="100"/>
<TextBox Text="{Binding Position}" Margin="3" /> <TextBox Text="{Binding Position}" Margin="3" />
</StackPanel> </StackPanel>
</StackPanel> <ScrollViewer Grid.Row="2" Margin="3" VerticalScrollBarVisibility="Auto">
<local:EntityControlsView DataContext="{Binding}"/>
</ScrollViewer>
</Grid>
</UserControl> </UserControl>

View File

@@ -9,8 +9,12 @@
<UserControl.DataContext> <UserControl.DataContext>
<entities:VoxelMapViewModel/> <entities:VoxelMapViewModel/>
</UserControl.DataContext> </UserControl.DataContext>
<StackPanel> <Grid>
<Label Content="Attached Grids"></Label> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Expander Grid.Row="0" Header="Attached Grids">
<ListView ItemsSource="{Binding AttachedGrids}" Margin="3"> <ListView ItemsSource="{Binding AttachedGrids}" Margin="3">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -18,5 +22,10 @@
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
</StackPanel> </Expander>
<ScrollViewer Grid.Row="1" Margin="3" VerticalScrollBarVisibility="Auto">
<local:EntityControlsView DataContext="{Binding}"/>
</ScrollViewer>
</Grid>
</UserControl> </UserControl>

View File

@@ -12,6 +12,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using NLog;
using Torch; using Torch;
using Sandbox; using Sandbox;
using Sandbox.Engine.Multiplayer; using Sandbox.Engine.Multiplayer;
@@ -20,7 +21,10 @@ using Sandbox.Game.World;
using Sandbox.ModAPI; using Sandbox.ModAPI;
using SteamSDK; using SteamSDK;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers; using Torch.Managers;
using Torch.Server.Managers;
using Torch.ViewModels; using Torch.ViewModels;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
@@ -31,6 +35,8 @@ namespace Torch.Server
/// </summary> /// </summary>
public partial class PlayerListControl : UserControl public partial class PlayerListControl : UserControl
{ {
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private ITorchServer _server; private ITorchServer _server;
public PlayerListControl() public PlayerListControl()
@@ -41,19 +47,48 @@ namespace Torch.Server
public void BindServer(ITorchServer server) public void BindServer(ITorchServer server)
{ {
_server = server; _server = server;
DataContext = (MultiplayerManager)_server.Multiplayer;
var sessionManager = server.Managers.GetManager<ITorchSessionManager>();
sessionManager.SessionStateChanged += SessionStateChanged;
}
private void SessionStateChanged(ITorchSession session, TorchSessionState newState)
{
switch (newState)
{
case TorchSessionState.Loaded:
Dispatcher.InvokeAsync(() => DataContext = _server?.CurrentSession?.Managers.GetManager<MultiplayerManagerDedicated>());
break;
case TorchSessionState.Unloading:
Dispatcher.InvokeAsync(() => DataContext = null);
break;
}
} }
private void KickButton_Click(object sender, RoutedEventArgs e) private void KickButton_Click(object sender, RoutedEventArgs e)
{ {
var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
_server.Multiplayer.KickPlayer(player.Key); try
{
_server.CurrentSession.Managers.GetManager<IMultiplayerManagerServer>().KickPlayer(player.Key);
}
catch (Exception ex)
{
_log.Warn(ex);
}
} }
private void BanButton_Click(object sender, RoutedEventArgs e) private void BanButton_Click(object sender, RoutedEventArgs e)
{ {
var player = (KeyValuePair<ulong, PlayerViewModel>) PlayerList.SelectedItem; var player = (KeyValuePair<ulong, PlayerViewModel>)PlayerList.SelectedItem;
_server.Multiplayer.BanPlayer(player.Key); try
{
_server.CurrentSession.Managers.GetManager<IMultiplayerManagerServer>().BanPlayer(player.Key);
}
catch (Exception ex)
{
_log.Warn(ex);
}
} }
} }
} }

View File

@@ -12,7 +12,7 @@
</UserControl.DataContext> </UserControl.DataContext>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/> <ColumnDefinition Width="200"/>
<ColumnDefinition/> <ColumnDefinition/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.Column="0">
@@ -27,7 +27,7 @@
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>
</ListView> </ListView>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" IsEnabled="false"/> <Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/>
</Grid> </Grid>
<Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/> <Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
</Grid> </Grid>

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -15,6 +16,8 @@ using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using NLog; using NLog;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using Torch.Server.ViewModels; using Torch.Server.ViewModels;
namespace Torch.Server.Views namespace Torch.Server.Views
@@ -24,6 +27,9 @@ namespace Torch.Server.Views
/// </summary> /// </summary>
public partial class PluginsControl : UserControl public partial class PluginsControl : UserControl
{ {
private ITorchServer _server;
private PluginManager _plugins;
public PluginsControl() public PluginsControl()
{ {
InitializeComponent(); InitializeComponent();
@@ -31,8 +37,15 @@ namespace Torch.Server.Views
public void BindServer(ITorchServer server) public void BindServer(ITorchServer server)
{ {
var pluginManager = new PluginManagerViewModel(server.Plugins); _server = server;
_plugins = _server.Managers.GetManager<PluginManager>();
var pluginManager = new PluginManagerViewModel(_plugins);
DataContext = pluginManager; DataContext = pluginManager;
} }
private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
{
Process.Start("explorer.exe", _plugins.PluginDir);
}
} }
} }

386
Torch.Tests/PatchTest.cs Normal file
View File

@@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using Torch.Managers.PatchManager;
using Torch.Managers.PatchManager.MSIL;
using Torch.Utils;
using Xunit;
// ReSharper disable UnusedMember.Local
namespace Torch.Tests
{
#pragma warning disable 414
public class PatchTest
{
#region TestRunner
private static readonly PatchManager _patchContext = new PatchManager(null);
[Theory]
[MemberData(nameof(Prefixes))]
public void TestPrefix(TestBootstrap runner)
{
runner.TestPrefix();
}
[Theory]
[MemberData(nameof(Transpilers))]
public void TestTranspile(TestBootstrap runner)
{
runner.TestTranspile();
}
[Theory]
[MemberData(nameof(Suffixes))]
public void TestSuffix(TestBootstrap runner)
{
runner.TestSuffix();
}
[Theory]
[MemberData(nameof(Combo))]
public void TestCombo(TestBootstrap runner)
{
runner.TestCombo();
}
public class TestBootstrap
{
public bool HasPrefix => _prefixMethod != null;
public bool HasTranspile => _transpileMethod != null;
public bool HasSuffix => _suffixMethod != null;
private readonly MethodInfo _prefixMethod, _prefixAssert;
private readonly MethodInfo _suffixMethod, _suffixAssert;
private readonly MethodInfo _transpileMethod, _transpileAssert;
private readonly MethodInfo _targetMethod, _targetAssert;
private readonly MethodInfo _resetMethod;
private readonly object _instance;
private readonly object[] _targetParams;
private readonly Type _type;
public TestBootstrap(Type t)
{
const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
_type = t;
_prefixMethod = t.GetMethod("Prefix", flags);
_prefixAssert = t.GetMethod("AssertPrefix", flags);
_suffixMethod = t.GetMethod("Suffix", flags);
_suffixAssert = t.GetMethod("AssertSuffix", flags);
_transpileMethod = t.GetMethod("Transpile", flags);
_transpileAssert = t.GetMethod("AssertTranspile", flags);
_targetMethod = t.GetMethod("Target", flags);
_targetAssert = t.GetMethod("AssertNormal", flags);
_resetMethod = t.GetMethod("Reset", flags);
if (_targetMethod == null)
throw new Exception($"{t.FullName} must have a method named Target");
if (_targetAssert == null)
throw new Exception($"{t.FullName} must have a method named AssertNormal");
_instance = !_targetMethod.IsStatic ? Activator.CreateInstance(t) : null;
_targetParams = (object[])t.GetField("_targetParams", flags)?.GetValue(null) ?? new object[0];
}
private void Invoke(MethodBase i, params object[] args)
{
if (i == null) return;
i.Invoke(i.IsStatic ? null : _instance, args);
}
private void Invoke()
{
_targetMethod.Invoke(_instance, _targetParams);
Invoke(_targetAssert);
}
public void TestPrefix()
{
Invoke(_resetMethod);
PatchContext context = _patchContext.AcquireContext();
context.GetPattern(_targetMethod).Prefixes.Add(_prefixMethod);
_patchContext.Commit();
Invoke();
Invoke(_prefixAssert);
_patchContext.FreeContext(context);
_patchContext.Commit();
}
public void TestSuffix()
{
Invoke(_resetMethod);
PatchContext context = _patchContext.AcquireContext();
context.GetPattern(_targetMethod).Suffixes.Add(_suffixMethod);
_patchContext.Commit();
Invoke();
Invoke(_suffixAssert);
_patchContext.FreeContext(context);
_patchContext.Commit();
}
public void TestTranspile()
{
Invoke(_resetMethod);
PatchContext context = _patchContext.AcquireContext();
context.GetPattern(_targetMethod).Transpilers.Add(_transpileMethod);
_patchContext.Commit();
Invoke();
Invoke(_transpileAssert);
_patchContext.FreeContext(context);
_patchContext.Commit();
}
public void TestCombo()
{
Invoke(_resetMethod);
PatchContext context = _patchContext.AcquireContext();
if (_prefixMethod != null)
context.GetPattern(_targetMethod).Prefixes.Add(_prefixMethod);
if (_transpileMethod != null)
context.GetPattern(_targetMethod).Transpilers.Add(_transpileMethod);
if (_suffixMethod != null)
context.GetPattern(_targetMethod).Suffixes.Add(_suffixMethod);
_patchContext.Commit();
Invoke();
Invoke(_prefixAssert);
Invoke(_transpileAssert);
Invoke(_suffixAssert);
_patchContext.FreeContext(context);
_patchContext.Commit();
}
public override string ToString()
{
return _type.Name;
}
}
private class PatchTestAttribute : Attribute
{
}
private static readonly List<TestBootstrap> _patchTest;
static PatchTest()
{
TestUtils.Init();
foreach (Type type in typeof(PatchManager).Assembly.GetTypes())
if (type.Namespace?.StartsWith(typeof(PatchManager).Namespace ?? "") ?? false)
ReflectedManager.Process(type);
_patchTest = new List<TestBootstrap>();
foreach (Type type in typeof(PatchTest).GetNestedTypes(BindingFlags.NonPublic))
if (type.GetCustomAttribute(typeof(PatchTestAttribute)) != null)
_patchTest.Add(new TestBootstrap(type));
}
public static IEnumerable<object[]> Prefixes => _patchTest.Where(x => x.HasPrefix).Select(x => new object[] { x });
public static IEnumerable<object[]> Transpilers => _patchTest.Where(x => x.HasTranspile).Select(x => new object[] { x });
public static IEnumerable<object[]> Suffixes => _patchTest.Where(x => x.HasSuffix).Select(x => new object[] { x });
public static IEnumerable<object[]> Combo => _patchTest.Where(x => x.HasPrefix || x.HasTranspile || x.HasSuffix).Select(x => new object[] { x });
#endregion
#region PatchTests
[PatchTest]
private class StaticNoRetNoParm
{
private static bool _prefixHit, _normalHit, _suffixHit, _transpileHit;
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Prefix()
{
_prefixHit = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Target()
{
_normalHit = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Suffix()
{
_suffixHit = true;
}
public static IEnumerable<MsilInstruction> Transpile(IEnumerable<MsilInstruction> instructions)
{
yield return new MsilInstruction(OpCodes.Ldnull);
yield return new MsilInstruction(OpCodes.Ldc_I4_1);
yield return new MsilInstruction(OpCodes.Stfld).InlineValue(typeof(StaticNoRetNoParm).GetField("_transpileHit", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public));
foreach (MsilInstruction i in instructions)
yield return i;
}
public static void Reset()
{
_prefixHit = _normalHit = _suffixHit = _transpileHit = false;
}
public static void AssertTranspile()
{
Assert.True(_transpileHit, "Failed to transpile");
}
public static void AssertSuffix()
{
Assert.True(_suffixHit, "Failed to suffix");
}
public static void AssertNormal()
{
Assert.True(_normalHit, "Failed to execute normally");
}
public static void AssertPrefix()
{
Assert.True(_prefixHit, "Failed to prefix");
}
}
[PatchTest]
private class StaticNoRetParam
{
private static bool _prefixHit, _normalHit, _suffixHit;
private static readonly object[] _targetParams = { "test", 1, new StringBuilder("test1") };
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Prefix(string str, int i, StringBuilder o)
{
Assert.Equal(_targetParams[0], str);
Assert.Equal(_targetParams[1], i);
Assert.Equal(_targetParams[2], o);
_prefixHit = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Target(string str, int i, StringBuilder o)
{
_normalHit = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Suffix(string str, int i, StringBuilder o)
{
Assert.Equal(_targetParams[0], str);
Assert.Equal(_targetParams[1], i);
Assert.Equal(_targetParams[2], o);
_suffixHit = true;
}
public static void Reset()
{
_prefixHit = _normalHit = _suffixHit = false;
}
public static void AssertSuffix()
{
Assert.True(_suffixHit, "Failed to suffix");
}
public static void AssertNormal()
{
Assert.True(_normalHit, "Failed to execute normally");
}
public static void AssertPrefix()
{
Assert.True(_prefixHit, "Failed to prefix");
}
}
[PatchTest]
private class StaticNoRetParamReplace
{
private static bool _prefixHit, _normalHit, _suffixHit;
private static readonly object[] _targetParams = { "test", 1, new StringBuilder("stest1") };
private static readonly object[] _replacedParams = { "test2", 2, new StringBuilder("stest2") };
private static object[] _calledParams;
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Prefix(ref string str, ref int i, ref StringBuilder o)
{
Assert.Equal(_targetParams[0], str);
Assert.Equal(_targetParams[1], i);
Assert.Equal(_targetParams[2], o);
str = (string)_replacedParams[0];
i = (int)_replacedParams[1];
o = (StringBuilder)_replacedParams[2];
_prefixHit = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Target(string str, int i, StringBuilder o)
{
_calledParams = new object[] { str, i, o };
_normalHit = true;
}
public static void Reset()
{
_prefixHit = _normalHit = _suffixHit = false;
}
public static void AssertNormal()
{
Assert.True(_normalHit, "Failed to execute normally");
}
public static void AssertPrefix()
{
Assert.True(_prefixHit, "Failed to prefix");
for (var i = 0; i < 3; i++)
Assert.Equal(_replacedParams[i], _calledParams[i]);
}
}
[PatchTest]
private class StaticCancelExec
{
private static bool _prefixHit, _normalHit, _suffixHit;
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool Prefix()
{
_prefixHit = true;
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Target()
{
_normalHit = true;
}
public static void Reset()
{
_prefixHit = _normalHit = _suffixHit = false;
}
public static void AssertNormal()
{
Assert.False(_normalHit, "Executed normally when canceled");
}
public static void AssertPrefix()
{
Assert.True(_prefixHit, "Failed to prefix");
}
}
#endregion
}
#pragma warning restore 414
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using Torch.Utils; using Torch.Utils;
using Xunit; using Xunit;
@@ -19,6 +20,10 @@ namespace Torch.Tests
public static IEnumerable<object[]> Invokers => _manager.Invokers; public static IEnumerable<object[]> Invokers => _manager.Invokers;
public static IEnumerable<object[]> MemberInfo => _manager.MemberInfo;
public static IEnumerable<object[]> Events => _manager.Events;
#region Binding #region Binding
[Theory] [Theory]
[MemberData(nameof(Getters))] [MemberData(nameof(Getters))]
@@ -52,6 +57,28 @@ namespace Torch.Tests
if (field.Field.IsStatic) if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null)); Assert.NotNull(field.Field.GetValue(null));
} }
[Theory]
[MemberData(nameof(MemberInfo))]
public void TestBindingMemberInfo(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion #endregion
#region Results #region Results
@@ -79,10 +106,51 @@ namespace Torch.Tests
{ {
return k >= 0; return k >= 0;
} }
public event Action Event1;
public ReflectionTestTarget()
{
Event1 += Callback1;
}
public bool Callback1Flag = false;
public void Callback1()
{
Callback1Flag = true;
}
public bool Callback2Flag = false;
public void Callback2()
{
Callback2Flag = true;
}
public void RaiseEvent()
{
Event1?.Invoke();
}
} }
private class ReflectionTestBinding private class ReflectionTestBinding
{ {
#region Instance
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestField")]
public static FieldInfo TestFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestProperty")]
public static PropertyInfo TestPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall")]
public static MethodInfo TestMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", Parameters = new[] { typeof(int) })]
public static MethodInfo TestMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", ReturnType = typeof(bool))]
public static MethodInfo TestMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestField")] [ReflectedGetter(Name = "TestField")]
public static Func<ReflectionTestTarget, int> TestFieldGetter; public static Func<ReflectionTestTarget, int> TestFieldGetter;
[ReflectedSetter(Name = "TestField")] [ReflectedSetter(Name = "TestField")]
@@ -96,7 +164,27 @@ namespace Torch.Tests
[ReflectedMethod] [ReflectedMethod]
public static Func<ReflectionTestTarget, int, bool> TestCall; public static Func<ReflectionTestTarget, int, bool> TestCall;
[ReflectedEventReplace(typeof(ReflectionTestTarget), "Event1", typeof(ReflectionTestTarget), "Callback1")]
public static Func<ReflectedEventReplacer> TestEventReplacer;
#endregion
#region Static
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestFieldStatic")]
public static FieldInfo TestStaticFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestPropertyStatic")]
public static PropertyInfo TestStaticPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic")]
public static MethodInfo TestStaticMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", Parameters = new[] { typeof(int) })]
public static MethodInfo TestStaticMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", ReturnType = typeof(bool))]
public static MethodInfo TestStaticMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))] [ReflectedGetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
public static Func<int> TestStaticFieldGetter; public static Func<int> TestStaticFieldGetter;
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))] [ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
@@ -109,6 +197,7 @@ namespace Torch.Tests
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))] [ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
public static Func<int, bool> TestCallStatic; public static Func<int, bool> TestCallStatic;
#endregion
} }
#endregion #endregion
@@ -215,6 +304,32 @@ namespace Torch.Tests
Assert.True(ReflectionTestBinding.TestCallStatic.Invoke(1)); Assert.True(ReflectionTestBinding.TestCallStatic.Invoke(1));
Assert.False(ReflectionTestBinding.TestCallStatic.Invoke(-1)); Assert.False(ReflectionTestBinding.TestCallStatic.Invoke(-1));
} }
[Fact]
public void TestInstanceEventReplace()
{
var target = new ReflectionTestTarget();
target.Callback1Flag = false;
target.RaiseEvent();
Assert.True(target.Callback1Flag, "Control test failed");
target.Callback1Flag = false;
target.Callback2Flag = false;
ReflectedEventReplacer binder = ReflectionTestBinding.TestEventReplacer.Invoke();
Assert.True(binder.Test(target), "Binder was unable to find the requested method");
binder.Replace(new Action(() => target.Callback2()), target);
target.RaiseEvent();
Assert.True(target.Callback2Flag, "Substitute callback wasn't called");
Assert.False(target.Callback1Flag, "Original callback wasn't removed");
target.Callback1Flag = false;
target.Callback2Flag = false;
binder.Restore(target);
target.RaiseEvent();
Assert.False(target.Callback2Flag, "Substitute callback wasn't removed");
Assert.True(target.Callback1Flag, "Original callback wasn't restored");
}
#endregion #endregion
#endregion #endregion
} }

View File

@@ -28,12 +28,16 @@ namespace Torch.Tests
private readonly HashSet<object[]> _getters = new HashSet<object[]>(); private readonly HashSet<object[]> _getters = new HashSet<object[]>();
private readonly HashSet<object[]> _setters = new HashSet<object[]>(); private readonly HashSet<object[]> _setters = new HashSet<object[]>();
private readonly HashSet<object[]> _invokers = new HashSet<object[]>(); private readonly HashSet<object[]> _invokers = new HashSet<object[]>();
private readonly HashSet<object[]> _memberInfo = new HashSet<object[]>();
private readonly HashSet<object[]> _events = new HashSet<object[]>();
public ReflectionTestManager() public ReflectionTestManager()
{ {
_getters.Add(new object[] { new FieldRef(null) }); _getters.Add(new object[] { new FieldRef(null) });
_setters.Add(new object[] { new FieldRef(null) }); _setters.Add(new object[] { new FieldRef(null) });
_invokers.Add(new object[] { new FieldRef(null) }); _invokers.Add(new object[] { new FieldRef(null) });
_memberInfo.Add(new object[] {new FieldRef(null)});
_events.Add(new object[] {new FieldRef(null)});
} }
public ReflectionTestManager Init(Assembly asm) public ReflectionTestManager Init(Assembly asm)
@@ -50,12 +54,36 @@ namespace Torch.Tests
BindingFlags.Public | BindingFlags.Public |
BindingFlags.NonPublic)) BindingFlags.NonPublic))
{ {
if (field.GetCustomAttribute<ReflectedMethodAttribute>() != null) var args = new object[] { new FieldRef(field) };
_invokers.Add(new object[] { new FieldRef(field) }); foreach (ReflectedMemberAttribute attr in field.GetCustomAttributes<ReflectedMemberAttribute>())
if (field.GetCustomAttribute<ReflectedGetterAttribute>() != null) {
_getters.Add(new object[] { new FieldRef(field) }); if (!field.IsStatic)
if (field.GetCustomAttribute<ReflectedSetterAttribute>() != null) throw new ArgumentException("Field must be static to be reflected");
_setters.Add(new object[] { new FieldRef(field) }); switch (attr)
{
case ReflectedMethodAttribute rma:
_invokers.Add(args);
break;
case ReflectedGetterAttribute rga:
_getters.Add(args);
break;
case ReflectedSetterAttribute rsa:
_setters.Add(args);
break;
case ReflectedFieldInfoAttribute rfia:
case ReflectedPropertyInfoAttribute rpia:
case ReflectedMethodInfoAttribute rmia:
_memberInfo.Add(args);
break;
}
}
var reflectedEventReplacer = field.GetCustomAttribute<ReflectedEventReplaceAttribute>();
if (reflectedEventReplacer != null)
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
_events.Add(args);
}
} }
return this; return this;
} }
@@ -66,6 +94,10 @@ namespace Torch.Tests
public IEnumerable<object[]> Invokers => _invokers; public IEnumerable<object[]> Invokers => _invokers;
public IEnumerable<object[]> MemberInfo => _memberInfo;
public IEnumerable<object[]> Events => _events;
#endregion #endregion
} }

View File

@@ -63,6 +63,7 @@
<Compile Include="..\Versioning\AssemblyVersion.cs"> <Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link> <Link>Properties\AssemblyVersion.cs</Link>
</Compile> </Compile>
<Compile Include="PatchTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReflectionTestManager.cs" /> <Compile Include="ReflectionTestManager.cs" />
<Compile Include="ReflectionSystemTest.cs" /> <Compile Include="ReflectionSystemTest.cs" />

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Torch.Utils; using Torch.Utils;
using Xunit; using Xunit;
@@ -27,6 +28,10 @@ namespace Torch.Tests
public static IEnumerable<object[]> Invokers => Manager().Invokers; public static IEnumerable<object[]> Invokers => Manager().Invokers;
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
public static IEnumerable<object[]> Events => Manager().Events;
#region Binding #region Binding
[Theory] [Theory]
[MemberData(nameof(Getters))] [MemberData(nameof(Getters))]
@@ -60,6 +65,28 @@ namespace Torch.Tests
if (field.Field.IsStatic) if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null)); Assert.NotNull(field.Field.GetValue(null));
} }
[Theory]
[MemberData(nameof(MemberInfo))]
public void TestBindingMemberInfo(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion #endregion
} }
} }

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Torch.API;
using VRage.Network;
namespace Torch
{
public class ChatMessage : IChatMessage
{
public DateTime Timestamp { get; }
public ulong SteamId { get; }
public string Name { get; }
public string Message { get; }
public ChatMessage(DateTime timestamp, ulong steamId, string name, string message)
{
Timestamp = timestamp;
SteamId = steamId;
Name = name;
Message = message;
}
public static ChatMessage FromChatMsg(ChatMsg msg, DateTime dt = default(DateTime))
{
return new ChatMessage(
dt == default(DateTime) ? DateTime.Now : dt,
msg.Author,
MyMultiplayer.Static.GetMemberName(msg.Author),
msg.Text);
}
}
}

View File

@@ -1,77 +1,428 @@
using System; using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Threading; using Torch.Utils;
namespace Torch namespace Torch.Collections
{ {
[Obsolete("Use ObservableList<T>.")] /// <summary>
public class MTObservableCollection<T> : ObservableCollection<T> /// Multithread safe, observable collection
/// </summary>
/// <typeparam name="TC">Collection type</typeparam>
/// <typeparam name="TV">Value type</typeparam>
public abstract class MtObservableCollection<TC, TV> : INotifyPropertyChanged, INotifyCollectionChanged,
IEnumerable<TV>, ICollection where TC : class, ICollection<TV>
{ {
public override event NotifyCollectionChangedEventHandler CollectionChanged; protected readonly ReaderWriterLockSlim Lock;
protected readonly TC Backing;
private int _version;
private readonly ThreadLocal<ThreadView> _threadViews;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) protected MtObservableCollection(TC backing)
{ {
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged; Backing = backing;
if (collectionChanged != null) // recursion so the events can read snapshots.
foreach (var del in collectionChanged.GetInvocationList()) Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
{ _version = 0;
var nh = (NotifyCollectionChangedEventHandler)del; _threadViews = new ThreadLocal<ThreadView>(() => new ThreadView(this));
var dispObj = nh.Target as DispatcherObject; _deferredSnapshot = new DeferredUpdateToken(this);
_flushEventQueue = new Timer(FlushCollectionEventQueue);
var dispatcher = dispObj?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
} }
nh.Invoke(this, e); ~MtObservableCollection()
{
_flushEventQueue.Dispose();
}
/// <summary>
/// Should this observable collection actually dispatch events.
/// </summary>
public bool NotificationsEnabled { get; protected set; } = true;
/// <summary>
/// Takes a snapshot of this collection. Note: This call is only done when a read lock is acquired.
/// </summary>
/// <param name="old">Collection to clear and reuse, or null if none</param>
/// <returns>The snapshot</returns>
protected abstract List<TV> Snapshot(List<TV> old);
/// <summary>
/// Marks all snapshots taken of this collection as dirty.
/// </summary>
protected void MarkSnapshotsDirty()
{
_version++;
}
#region ICollection
/// <inheritdoc/>
public void Add(TV item)
{
using (Lock.WriteUsing())
{
Backing.Add(item);
MarkSnapshotsDirty();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item,
Backing.Count - 1));
} }
} }
public void Insert<TKey>(T item, Func<T, TKey> selector, IComparer<TKey> comparer) /// <inheritdoc/>
public void Clear()
{ {
var key = selector(item); using (Lock.WriteUsing())
for (var i = 0; i < Count; i++)
{ {
var key2 = selector(Items[i]); Backing.Clear();
if (comparer.Compare(key, key2) < 1) MarkSnapshotsDirty();
continue; OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Insert(i + 1, item); /// <inheritdoc/>
public bool Contains(TV item)
{
using (Lock.ReadUsing())
return Backing.Contains(item);
}
/// <inheritdoc/>
public void CopyTo(TV[] array, int arrayIndex)
{
using (Lock.ReadUsing())
Backing.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public bool Remove(TV item)
{
using (Lock.UpgradableReadUsing())
{
int? oldIndex = (Backing as IList<TV>)?.IndexOf(item);
if (oldIndex == -1)
return false;
using (Lock.WriteUsing())
{
if (!Backing.Remove(item))
return false;
MarkSnapshotsDirty();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(oldIndex.HasValue
? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item,
oldIndex.Value)
: new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
return true;
}
}
}
/// <inheritdoc/>
public int Count
{
get
{
using (Lock.ReadUsing())
return Backing.Count;
}
}
/// <inheritdoc/>
public bool IsReadOnly => Backing.IsReadOnly;
#endregion
#region Event Wrappers
private readonly DeferredUpdateToken _deferredSnapshot;
/// <summary>
/// Disposable that stops update signals and signals a full refresh when disposed.
/// </summary>
public IDisposable DeferredUpdate()
{
using (Lock.WriteUsing())
{
_deferredSnapshot.Enter();
return _deferredSnapshot;
}
}
private struct DummyToken : IDisposable
{
public void Dispose()
{
}
}
private class DeferredUpdateToken : IDisposable
{
private readonly MtObservableCollection<TC, TV> _collection;
private int _depth;
internal DeferredUpdateToken(MtObservableCollection<TC, TV> c)
{
_collection = c;
}
internal void Enter()
{
if (Interlocked.Increment(ref _depth) == 1)
{
_collection.NotificationsEnabled = false;
}
}
public void Dispose()
{
if (Interlocked.Decrement(ref _depth) == 0)
using (_collection.Lock.WriteUsing())
{
_collection.NotificationsEnabled = true;
_collection.OnPropertyChanged(nameof(Count));
_collection.OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
}
protected void OnPropertyChanged(string propName)
{
if (!NotificationsEnabled)
return; return;
_propertyChangedEvent.Raise(this, new PropertyChangedEventArgs(propName));
} }
Add(item); protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
}
public void Sort<TKey>(Func<T, TKey> selector, IComparer<TKey> comparer = null)
{ {
List<T> sortedItems; if (!NotificationsEnabled)
if (comparer != null) return;
sortedItems = Items.OrderBy(selector, comparer).ToList(); _collectionEventQueue.Enqueue(e);
else // In half a second, flush the events
sortedItems = Items.OrderBy(selector).ToList(); _flushEventQueue.Change(500, -1);
Items.Clear();
foreach (var item in sortedItems)
Add(item);
} }
public void RemoveWhere(Func<T, bool> condition) private readonly Timer _flushEventQueue;
private readonly Queue<NotifyCollectionChangedEventArgs> _collectionEventQueue =
new Queue<NotifyCollectionChangedEventArgs>();
private void FlushCollectionEventQueue(object data)
{ {
for (var i = Items.Count - 1; i > 0; i--) bool reset = _collectionEventQueue.Count >= 2;
var itemsChanged = false;
while (_collectionEventQueue.TryDequeue(out NotifyCollectionChangedEventArgs e))
if (!reset)
{ {
if (condition(Items[i])) _collectionChangedEvent.Raise(this, e);
RemoveAt(i); itemsChanged = true;
}
if (reset)
{
_collectionChangedEvent.Raise(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
itemsChanged = true;
}
if (itemsChanged)
OnPropertyChanged("Item[]");
}
private readonly MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler> _propertyChangedEvent
=
new MtObservableEvent<PropertyChangedEventArgs, PropertyChangedEventHandler>();
/// <inheritdoc/>
public event PropertyChangedEventHandler PropertyChanged
{
add
{
_propertyChangedEvent.Add(value);
OnPropertyChanged(nameof(IsObserved));
}
remove
{
_propertyChangedEvent.Remove(value);
OnPropertyChanged(nameof(IsObserved));
} }
} }
private readonly MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>
_collectionChangedEvent =
new MtObservableEvent<NotifyCollectionChangedEventArgs, NotifyCollectionChangedEventHandler>();
/// <inheritdoc/>
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add
{
_collectionChangedEvent.Add(value);
OnPropertyChanged(nameof(IsObserved));
}
remove
{
_collectionChangedEvent.Remove(value);
OnPropertyChanged(nameof(IsObserved));
}
}
#endregion
/// <summary>
/// Is this collection observed by any listeners.
/// </summary>
public bool IsObserved => _collectionChangedEvent.IsObserved || _propertyChangedEvent.IsObserved;
#region Enumeration
/// <summary>
/// Manages a snapshot to a collection and dispatches enumerators from that snapshot.
/// </summary>
private sealed class ThreadView
{
private readonly MtObservableCollection<TC, TV> _owner;
private readonly WeakReference<List<TV>> _snapshot;
/// <summary>
/// The <see cref="MtObservableCollection{TC,TV}._version"/> of the <see cref="_snapshot"/>
/// </summary>
private int _snapshotVersion;
/// <summary>
/// Number of strong references to the value pointed to be <see cref="_snapshot"/>
/// </summary>
private int _snapshotRefCount;
internal ThreadView(MtObservableCollection<TC, TV> owner)
{
_owner = owner;
_snapshot = new WeakReference<List<TV>>(null);
_snapshotVersion = 0;
_snapshotRefCount = 0;
}
private List<TV> GetSnapshot()
{
// reading the version number + snapshots
using (_owner.Lock.ReadUsing())
{
if (!_snapshot.TryGetTarget(out List<TV> currentSnapshot) || _snapshotVersion != _owner._version)
{
// Update the snapshot, using the old one if it isn't referenced.
currentSnapshot = _owner.Snapshot(_snapshotRefCount == 0 ? currentSnapshot : null);
_snapshotVersion = _owner._version;
_snapshotRefCount = 0;
_snapshot.SetTarget(currentSnapshot);
}
return currentSnapshot;
}
}
/// <summary>
/// Borrows a snapshot from a <see cref="ThreadView"/> and provides an enumerator.
/// Once <see cref="Dispose"/> is called the read lock is released.
/// </summary>
internal sealed class Enumerator : IEnumerator<TV>
{
private readonly IEnumerator<TV> _backing;
private readonly ThreadView _owner;
private bool _disposed;
internal Enumerator(ThreadView owner)
{
_owner = owner;
// Lock required since destructors run MT
lock (_owner)
{
_owner._snapshotRefCount++;
_backing = owner.GetSnapshot().GetEnumerator();
}
_disposed = false;
}
~Enumerator()
{
// Lock required since destructors run MT
if (!_disposed && _owner != null)
lock (_owner)
Dispose();
}
public void Dispose()
{
// safe deref so finalizer can clean up
_backing?.Dispose();
_owner._snapshotRefCount--;
_disposed = true;
}
public bool MoveNext()
{
if (_disposed)
throw new ObjectDisposedException(nameof(Enumerator));
return _backing.MoveNext();
}
public void Reset()
{
if (_disposed)
throw new ObjectDisposedException(nameof(Enumerator));
_backing.Reset();
}
public TV Current
{
get
{
if (_disposed)
throw new ObjectDisposedException(nameof(Enumerator));
return _backing.Current;
}
}
object IEnumerator.Current => Current;
}
}
/// <inheritdoc/>
public IEnumerator<TV> GetEnumerator()
{
return new ThreadView.Enumerator(_threadViews.Value);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
using (Lock.ReadUsing())
{
int i = index;
foreach (TV value in Backing)
{
if (i >= array.Length)
break;
array.SetValue(value, i++);
}
}
}
/// <inheritdoc/>
object ICollection.SyncRoot => this;
/// <inheritdoc/>
bool ICollection.IsSynchronized => true;
} }
} }

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Torch.Utils;
namespace Torch.Collections
{
/// <summary>
/// Multithread safe observable dictionary
/// </summary>
/// <typeparam name="TK">Key type</typeparam>
/// <typeparam name="TV">Value type</typeparam>
public class MtObservableDictionary<TK, TV> : MtObservableCollection<IDictionary<TK, TV>, KeyValuePair<TK, TV>>, IDictionary<TK, TV>
{
/// <summary>
/// Creates an empty observable dictionary
/// </summary>
public MtObservableDictionary() : base(new Dictionary<TK, TV>())
{
ObservableKeys = new ProxyCollection<TK>(this, Backing.Keys, (x) => x.Key);
ObservableValues = new ProxyCollection<TV>(this, Backing.Values, (x) => x.Value);
}
protected override List<KeyValuePair<TK, TV>> Snapshot(List<KeyValuePair<TK, TV>> old)
{
if (old == null)
return new List<KeyValuePair<TK, TV>>(Backing);
old.Clear();
old.AddRange(Backing);
return old;
}
/// <inheritdoc/>
public bool ContainsKey(TK key)
{
using (Lock.ReadUsing())
return Backing.ContainsKey(key);
}
/// <inheritdoc/>
public void Add(TK key, TV value)
{
Add(new KeyValuePair<TK, TV>(key, value));
}
/// <inheritdoc/>
public bool Remove(TK key)
{
return TryGetValue(key, out TV result) && Remove(new KeyValuePair<TK, TV>(key, result));
}
/// <inheritdoc/>
public bool TryGetValue(TK key, out TV value)
{
using (Lock.ReadUsing())
return Backing.TryGetValue(key, out value);
}
/// <inheritdoc/>
public TV this[TK key]
{
get
{
using (Lock.ReadUsing())
return Backing[key];
}
set
{
using (Lock.WriteUsing())
{
var oldKv = new KeyValuePair<TK, TV>(key, Backing[key]);
var newKv = new KeyValuePair<TK, TV>(key, value);
Backing[key] = value;
MarkSnapshotsDirty();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newKv, oldKv));
}
}
}
/// <inheritdoc/>
public ICollection<TK> Keys => ObservableKeys;
/// <inheritdoc/>
public ICollection<TV> Values => ObservableValues;
// TODO when we rewrite this to use a sorted dictionary.
/// <inheritdoc cref="Keys"/>
private ProxyCollection<TK> ObservableKeys { get; }
/// <inheritdoc cref="Keys"/>
private ProxyCollection<TV> ObservableValues { get; }
/// <summary>
/// Proxy collection capable of raising notifications when the parent collection changes.
/// </summary>
/// <typeparam name="TP">Entry type</typeparam>
public class ProxyCollection<TP> : ICollection<TP>
{
private readonly MtObservableDictionary<TK, TV> _owner;
private readonly ICollection<TP> _backing;
private readonly Func<KeyValuePair<TK, TV>, TP> _selector;
internal ProxyCollection(MtObservableDictionary<TK, TV> owner, ICollection<TP> backing, Func<KeyValuePair<TK, TV>, TP> selector)
{
_owner = owner;
_backing = backing;
_selector = selector;
}
/// <inheritdoc/>
public IEnumerator<TP> GetEnumerator() => new TransformEnumerator<KeyValuePair<TK, TV>, TP>(_owner.GetEnumerator(), _selector);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
public void Add(TP item)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public void Clear()
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public bool Contains(TP item)
{
using (_owner.Lock.ReadUsing())
return _backing.Contains(item);
}
/// <inheritdoc/>
public void CopyTo(TP[] array, int arrayIndex)
{
using (_owner.Lock.ReadUsing())
_backing.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public bool Remove(TP item)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public int Count
{
get
{
using (_owner.Lock.ReadUsing())
return _backing.Count;
}
}
/// <inheritdoc/>
public bool IsReadOnly => _backing.IsReadOnly;
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Windows.Threading;
namespace Torch.Collections
{
/// <summary>
/// Event that invokes handlers registered by dispatchers on dispatchers.
/// </summary>
/// <typeparam name="TEvtArgs">Event argument type</typeparam>
/// <typeparam name="TEvtHandle">Event handler delegate type</typeparam>
public sealed class MtObservableEvent<TEvtArgs, TEvtHandle> where TEvtArgs : EventArgs
{
private delegate void DelInvokeHandler(TEvtHandle handler, object sender, TEvtArgs args);
private static readonly DelInvokeHandler _invokeDirectly;
static MtObservableEvent()
{
MethodInfo invoke = typeof(TEvtHandle).GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
Debug.Assert(invoke != null, "No invoke method on handler type");
_invokeDirectly = (DelInvokeHandler)Delegate.CreateDelegate(typeof(DelInvokeHandler), invoke);
}
private static Dispatcher CurrentDispatcher => Dispatcher.FromThread(Thread.CurrentThread);
private event EventHandler<TEvtArgs> Event;
private int _observerCount = 0;
/// <summary>
/// Determines if this event has an observers.
/// </summary>
public bool IsObserved => _observerCount > 0;
/// <summary>
/// Raises this event for the given sender, with the given args
/// </summary>
/// <param name="sender">sender</param>
/// <param name="args">args</param>
public void Raise(object sender, TEvtArgs args)
{
Event?.Invoke(sender, args);
}
/// <summary>
/// Adds the given event handler.
/// </summary>
/// <param name="evt"></param>
public void Add(TEvtHandle evt)
{
if (evt == null)
return;
_observerCount++;
Event += new DispatcherDelegate(evt).Invoke;
}
/// <summary>
/// Removes the given event handler
/// </summary>
/// <param name="evt"></param>
public void Remove(TEvtHandle evt)
{
if (Event == null || evt == null)
return;
Delegate[] invokeList = Event.GetInvocationList();
for (int i = invokeList.Length - 1; i >= 0; i--)
{
var wrapper = (DispatcherDelegate)invokeList[i].Target;
if (wrapper._delegate.Equals(evt))
{
Event -= wrapper.Invoke;
_observerCount--;
return;
}
}
}
private struct DispatcherDelegate
{
private readonly Dispatcher _dispatcher;
internal readonly TEvtHandle _delegate;
internal DispatcherDelegate(TEvtHandle del)
{
_dispatcher = CurrentDispatcher;
_delegate = del;
}
public void Invoke(object sender, TEvtArgs args)
{
if (_dispatcher == null || _dispatcher == CurrentDispatcher)
_invokeDirectly(_delegate, sender, args);
else
// (Delegate) (object) == dual cast so that the compiler likes it
_dispatcher.BeginInvoke((Delegate)(object)_delegate, DispatcherPriority.DataBind, sender, args);
}
}
}
}

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch.Utils;
namespace Torch.Collections
{
/// <summary>
/// Multithread safe, observable list
/// </summary>
/// <typeparam name="T">Value type</typeparam>
public class MtObservableList<T> : MtObservableCollection<IList<T>, T>, IList<T>, IList
{
/// <summary>
/// Initializes a new instance of the MtObservableList class that is empty and has the default initial capacity.
/// </summary>
public MtObservableList() : base(new List<T>())
{
}
/// <summary>
/// Initializes a new instance of the MtObservableList class that is empty and has the specified initial capacity.
/// </summary>
/// <param name="capacity"></param>
public MtObservableList(int capacity) : base(new List<T>(capacity))
{
}
protected override List<T> Snapshot(List<T> old)
{
if (old == null)
{
var list = new List<T>(Backing);
return list;
}
old.Clear();
old.AddRange(Backing);
return old;
}
/// <inheritdoc/>
public int IndexOf(T item)
{
using (Lock.ReadUsing())
return Backing.IndexOf(item);
}
/// <inheritdoc/>
public void Insert(int index, T item)
{
using (Lock.WriteUsing())
{
Backing.Insert(index, item);
MarkSnapshotsDirty();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}
}
/// <inheritdoc/>
public void RemoveAt(int index)
{
using (Lock.WriteUsing())
{
T old = Backing[index];
Backing.RemoveAt(index);
MarkSnapshotsDirty();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, index));
}
}
/// <inheritdoc/>
public T this[int index]
{
get
{
using (Lock.ReadUsing())
return Backing[index];
}
set
{
using (Lock.ReadUsing())
{
T old = Backing[index];
Backing[index] = value;
MarkSnapshotsDirty();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
value, old, index));
}
}
}
/// <inheritdoc/>
public void RemoveWhere(Func<T, bool> predicate)
{
for (int i = Count - 1; i >= 0; i--)
if (predicate(this[i]))
RemoveAt(i);
}
/// <summary>
/// Sorts the list using the given selector and comparer./>
/// </summary>
public void Sort<TKey>(Func<T, TKey> selector, IComparer<TKey> comparer = null)
{
using (DeferredUpdate())
using (Lock.WriteUsing())
{
comparer = comparer ?? Comparer<TKey>.Default;
if (Backing is List<T> lst)
lst.Sort(new TransformComparer<T, TKey>(selector, comparer));
else
{
List<T> sortedItems = Backing.OrderBy(selector, comparer).ToList();
Backing.Clear();
foreach (T v in sortedItems)
Backing.Add(v);
}
}
}
/// <inheritdoc/>
int IList.Add(object value)
{
if (value is T t)
using (Lock.WriteUsing())
{
int index = Backing.Count;
Backing.Add(t);
return index;
}
return -1;
}
bool IList.Contains(object value)
{
return value is T t && Contains(t);
}
int IList.IndexOf(object value)
{
return value is T t ? IndexOf(t) : -1;
}
/// <inheritdoc/>
void IList.Insert(int index, object value)
{
Insert(index, (T) value);
}
/// <inheritdoc/>
void IList.Remove(object value)
{
if (value is T t)
base.Remove(t);
}
/// <inheritdoc/>
object IList.this[int index]
{
get => this[index];
set => this[index] = (T) value;
}
/// <inheritdoc/>
bool IList.IsFixedSize => false;
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace Torch.Collections
{
[Serializable]
public class ObservableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
{
/// <inheritdoc />
public new void Add(TKey key, TValue value)
{
base.Add(key, value);
var kv = new KeyValuePair<TKey, TValue>(key, value);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, kv));
}
/// <inheritdoc />
public new bool Remove(TKey key)
{
if (!ContainsKey(key))
return false;
var kv = new KeyValuePair<TKey, TValue>(key, this[key]);
base.Remove(key);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, kv));
return true;
}
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
if (collectionChanged != null)
foreach (NotifyCollectionChangedEventHandler nh in collectionChanged.GetInvocationList())
{
var dispObj = nh.Target as DispatcherObject;
var dispatcher = dispObj?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
}
nh.Invoke(this, e);
}
}
/// <inheritdoc />
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged;
}
}

View File

@@ -1,186 +0,0 @@
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.Windows.Threading;
namespace Torch
{
/// <summary>
/// An observable version of <see cref="List{T}"/>.
/// </summary>
public class ObservableList<T> : IList<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
private List<T> _internalList = new List<T>();
/// <inheritdoc />
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc />
public event PropertyChangedEventHandler PropertyChanged;
/// <inheritdoc />
public void Clear()
{
_internalList.Clear();
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <inheritdoc />
public bool Contains(T item)
{
return _internalList.Contains(item);
}
/// <inheritdoc />
public void CopyTo(T[] array, int arrayIndex)
{
_internalList.CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public bool Remove(T item)
{
var oldIndex = _internalList.IndexOf(item);
if (!_internalList.Remove(item))
return false;
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, oldIndex));
return true;
}
/// <inheritdoc />
public int Count => _internalList.Count;
/// <inheritdoc />
public bool IsReadOnly => false;
/// <inheritdoc />
public void Add(T item)
{
_internalList.Add(item);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}
/// <inheritdoc />
public int IndexOf(T item) => _internalList.IndexOf(item);
/// <inheritdoc />
public void Insert(int index, T item)
{
_internalList.Insert(index, item);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
}
/// <summary>
/// Inserts an item in order based on the provided selector and comparer. This will only work properly on a pre-sorted list.
/// </summary>
public void Insert<TKey>(T item, Func<T, TKey> selector, IComparer<TKey> comparer = null)
{
comparer = comparer ?? Comparer<TKey>.Default;
var key1 = selector(item);
for (var i = 0; i < _internalList.Count; i++)
{
var key2 = selector(_internalList[i]);
if (comparer.Compare(key1, key2) < 1)
{
Insert(i, item);
return;
}
}
Add(item);
}
/// <inheritdoc />
public void RemoveAt(int index)
{
var old = this[index];
_internalList.RemoveAt(index);
OnPropertyChanged(nameof(Count));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, index));
}
public T this[int index]
{
get => _internalList[index];
set
{
var old = _internalList[index];
if (old.Equals(value))
return;
_internalList[index] = value;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old, index));
}
}
/// <summary>
/// Sorts the list using the given selector and comparer./>
/// </summary>
public void Sort<TKey>(Func<T, TKey> selector, IComparer<TKey> comparer = null)
{
comparer = comparer ?? Comparer<TKey>.Default;
var sortedItems = _internalList.OrderBy(selector, comparer).ToList();
_internalList = sortedItems;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
/// Removes all items that satisfy the given condition.
/// </summary>
public void RemoveWhere(Func<T, bool> condition)
{
for (var i = Count - 1; i > 0; i--)
{
if (condition?.Invoke(this[i]) ?? false)
RemoveAt(i);
}
}
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var collectionChanged = CollectionChanged;
if (collectionChanged != null)
foreach (var del in collectionChanged.GetInvocationList())
{
var nh = (NotifyCollectionChangedEventHandler)del;
var dispObj = nh.Target as DispatcherObject;
var dispatcher = dispObj?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(() => nh.Invoke(this, e), DispatcherPriority.DataBind);
continue;
}
nh.Invoke(this, e);
}
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <inheritdoc />
public IEnumerator<T> GetEnumerator()
{
return _internalList.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_internalList).GetEnumerator();
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Collections
{
/// <summary>
/// Comparer that uses a delegate to select the key to compare on.
/// </summary>
/// <typeparam name="TIn">Input to this comparer</typeparam>
/// <typeparam name="TCompare">Type of comparison key</typeparam>
public class TransformComparer<TIn, TCompare> : IComparer<TIn>
{
private readonly IComparer<TCompare> _comparer;
private readonly Func<TIn, TCompare> _selector;
/// <summary>
/// Creates a new transforming comparer that uses the given key selector, and the given key comparer.
/// </summary>
/// <param name="transform">Key selector</param>
/// <param name="comparer">Key comparer</param>
public TransformComparer(Func<TIn, TCompare> transform, IComparer<TCompare> comparer = null)
{
_selector = transform;
_comparer = comparer ?? Comparer<TCompare>.Default;
}
/// <inheritdoc/>
public int Compare(TIn x, TIn y)
{
return _comparer.Compare(_selector(x), _selector(y));
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Collections
{
/// <summary>
/// Enumerator that transforms from one enumeration into another.
/// </summary>
/// <typeparam name="TIn">Input type</typeparam>
/// <typeparam name="TOut">Output type</typeparam>
public class TransformEnumerator<TIn,TOut> : IEnumerator<TOut>
{
private readonly IEnumerator<TIn> _input;
private readonly Func<TIn, TOut> _transform;
/// <summary>
/// Creates a new transform enumerator with the given transform function
/// </summary>
/// <param name="input">Input to proxy enumerator</param>
/// <param name="transform">Transform function</param>
public TransformEnumerator(IEnumerator<TIn> input, Func<TIn, TOut> transform)
{
_input = input;
_transform = transform;
}
/// <inheritdoc/>
public void Dispose()
{
_input.Dispose();
}
/// <inheritdoc/>
public bool MoveNext()
{
return _input.MoveNext();
}
/// <inheritdoc/>
public void Reset()
{
_input.Reset();
}
/// <inheritdoc/>
public TOut Current => _transform(_input.Current);
/// <inheritdoc/>
object IEnumerator.Current => Current;
}
}

View File

@@ -117,7 +117,7 @@ namespace Torch.Commands
catch (Exception e) catch (Exception e)
{ {
context.Respond(e.Message, "Error", MyFontEnum.Red); context.Respond(e.Message, "Error", MyFontEnum.Red);
Log.Error($"Command '{SyntaxHelp}' from '{Plugin.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}"); Log.Error($"Command '{SyntaxHelp}' from '{Plugin?.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}");
Log.Error(e); Log.Error(e);
return true; return true;
} }

View File

@@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Torch.API; using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins; using Torch.API.Plugins;
using VRage.Game; using VRage.Game;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
@@ -47,7 +48,7 @@ namespace Torch.Commands
Response = message; Response = message;
if (Player != null) if (Player != null)
Torch.Multiplayer.SendMessage(message, sender, Player.IdentityId, font); Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
} }
} }
} }

View File

@@ -9,6 +9,7 @@ using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.API.Plugins; using Torch.API.Plugins;
using Torch.Managers; using Torch.Managers;
using VRage.Game;
using VRage.Game.ModAPI; using VRage.Game.ModAPI;
using VRage.Network; using VRage.Network;
@@ -21,7 +22,7 @@ namespace Torch.Commands
public CommandTree Commands { get; set; } = new CommandTree(); public CommandTree Commands { get; set; } = new CommandTree();
private Logger _log = LogManager.GetLogger(nameof(CommandManager)); private Logger _log = LogManager.GetLogger(nameof(CommandManager));
[Dependency] [Dependency]
private ChatManager _chatManager; private IChatManagerServer _chatManager;
public CommandManager(ITorchBase torch, char prefix = '!') : base(torch) public CommandManager(ITorchBase torch, char prefix = '!') : base(torch)
{ {
@@ -31,7 +32,7 @@ namespace Torch.Commands
public override void Attach() public override void Attach()
{ {
RegisterCommandModule(typeof(TorchCommands)); RegisterCommandModule(typeof(TorchCommands));
_chatManager.MessageRecieved += HandleCommand; _chatManager.MessageProcessing += HandleCommand;
} }
public bool HasPermission(ulong steamId, Command command) public bool HasPermission(ulong steamId, Command command)
@@ -65,6 +66,11 @@ namespace Torch.Commands
} }
} }
public void UnregisterPluginCommands(ITorchPlugin plugin)
{
// TODO
}
public void RegisterPluginCommands(ITorchPlugin plugin) public void RegisterPluginCommands(ITorchPlugin plugin)
{ {
var assembly = plugin.GetType().Assembly; var assembly = plugin.GetType().Assembly;
@@ -93,20 +99,21 @@ namespace Torch.Commands
return context.Response; return context.Response;
} }
public void HandleCommand(ChatMsg msg, ref bool sendToOthers) public void HandleCommand(TorchChatMessage msg, ref bool consumed)
{ {
HandleCommand(msg.Text, msg.Author, ref sendToOthers); if (msg.AuthorSteamId.HasValue)
HandleCommand(msg.Message, msg.AuthorSteamId.Value, ref consumed);
} }
public void HandleCommand(string message, ulong steamId, ref bool sendToOthers, bool serverConsole = false) public void HandleCommand(string message, ulong steamId, ref bool consumed, bool serverConsole = false)
{ {
if (message.Length < 1 || message[0] != Prefix) if (message.Length < 1 || message[0] != Prefix)
return; return;
sendToOthers = false; consumed = true;
var player = Torch.Multiplayer.GetPlayerBySteamId(steamId); var player = Torch.CurrentSession.Managers.GetManager<IMultiplayerManagerBase>().GetPlayerBySteamId(steamId);
if (player == null) if (player == null)
{ {
_log.Error($"Command {message} invoked by nonexistant player"); _log.Error($"Command {message} invoked by nonexistant player");
@@ -123,7 +130,7 @@ namespace Torch.Commands
if (!HasPermission(steamId, command)) if (!HasPermission(steamId, command))
{ {
_log.Info($"{player.DisplayName} tried to use command {cmdPath} without permission"); _log.Info($"{player.DisplayName} tried to use command {cmdPath} without permission");
Torch.Multiplayer.SendMessage($"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", playerId: player.IdentityId); _chatManager.SendMessageAsOther("Server", $"You need to be a {command.MinimumPromoteLevel} or higher to use that command.", MyFontEnum.Red, steamId);
return; return;
} }

View File

@@ -21,7 +21,12 @@ namespace Torch.Commands
[Permission(MyPromoteLevel.None)] [Permission(MyPromoteLevel.None)]
public void Help() public void Help()
{ {
var commandManager = ((TorchBase)Context.Torch).Commands; var commandManager = Context.Torch.CurrentSession?.Managers.GetManager<CommandManager>();
if (commandManager == null)
{
Context.Respond("Must have an attached session to list commands");
return;
}
commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node); commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node);
if (node != null) if (node != null)
@@ -51,7 +56,12 @@ namespace Torch.Commands
[Command("longhelp", "Get verbose help. Will send a long message, check the Comms tab.")] [Command("longhelp", "Get verbose help. Will send a long message, check the Comms tab.")]
public void LongHelp() public void LongHelp()
{ {
var commandManager = Context.Torch.Managers.GetManager<CommandManager>(); var commandManager = Context.Torch.CurrentSession?.Managers.GetManager<CommandManager>();
if (commandManager == null)
{
Context.Respond("Must have an attached session to list commands");
return;
}
commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node); commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node);
if (node != null) if (node != null)
@@ -96,7 +106,7 @@ namespace Torch.Commands
[Permission(MyPromoteLevel.None)] [Permission(MyPromoteLevel.None)]
public void Plugins() public void Plugins()
{ {
var plugins = Context.Torch.Plugins.Select(p => p.Name); var plugins = Context.Torch.Managers.GetManager<PluginManager>()?.Plugins.Select(p => p.Value.Name) ?? Enumerable.Empty<string>();
Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}"); Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}");
} }
@@ -128,20 +138,19 @@ namespace Torch.Commands
{ {
if (i >= 60 && i % 60 == 0) if (i >= 60 && i % 60 == 0)
{ {
Context.Torch.Multiplayer.SendMessage($"Restarting server in {i / 60} minute{Pluralize(i / 60)}."); Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf($"Restarting server in {i / 60} minute{Pluralize(i / 60)}.");
yield return null; yield return null;
} }
else if (i > 0) else if (i > 0)
{ {
if (i < 11) if (i < 11)
Context.Torch.Multiplayer.SendMessage($"Restarting server in {i} second{Pluralize(i)}."); Context.Torch.CurrentSession.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf($"Restarting server in {i} second{Pluralize(i)}.");
yield return null; yield return null;
} }
else else
{ {
Context.Torch.Invoke(() => Context.Torch.Invoke(() =>
{ {
Context.Torch.Save(0).Wait();
Context.Torch.Restart(); Context.Torch.Restart();
}); });
yield break; yield break;

147
Torch/Event/EventList.cs Normal file
View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Torch.API.Event;
namespace Torch.Event
{
/// <summary>
/// Represents an ordered list of callbacks.
/// </summary>
/// <typeparam name="T">Event type</typeparam>
public class EventList<T> : IEventList where T : IEvent
{
/// <summary>
/// Delegate type for this event list
/// </summary>
/// <param name="evt">Event</param>
public delegate void DelEventHandler(ref T evt);
private struct EventHandlerData
{
internal readonly DelEventHandler _event;
internal readonly EventHandlerAttribute _attribute;
internal EventHandlerData(MethodInfo method, object instance)
{
_event = (DelEventHandler)Delegate.CreateDelegate(typeof(DelEventHandler), instance, method, true);
_attribute = method.GetCustomAttribute<EventHandlerAttribute>();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Raise(ref T evt)
{
if (!_attribute.SkipCancelled || !evt.Cancelled)
_event(ref evt);
}
}
private bool _dispatchersDirty = false;
private readonly List<EventHandlerData> _dispatchers = new List<EventHandlerData>();
private int _bakedCount;
private EventHandlerData[] _bakedDispatcher;
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
/// <inheritdoc/>
public void AddHandler(MethodInfo method, IEventHandler instance)
{
try
{
_lock.EnterWriteLock();
_dispatchers.Add(new EventHandlerData(method, instance));
_dispatchersDirty = true;
}
finally
{
_lock.ExitWriteLock();
}
}
/// <inheritdoc/>
public int RemoveHandlers(IEventHandler instance)
{
try
{
_lock.EnterWriteLock();
var removeCount = 0;
for (var i = 0; i < _dispatchers.Count; i++)
if (_dispatchers[i]._event.Target == instance)
{
_dispatchers.RemoveAtFast(i);
removeCount++;
i--;
}
if (removeCount > 0)
{
_dispatchersDirty = true;
_dispatchers.RemoveRange(_dispatchers.Count - removeCount, removeCount);
}
return removeCount;
}
finally
{
_lock.ExitWriteLock();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Bake()
{
if (!_dispatchersDirty && _bakedDispatcher != null)
return;
if (_bakedDispatcher == null || _dispatchers.Count > _bakedDispatcher.Length
|| _bakedDispatcher.Length * 5 / 4 < _dispatchers.Count)
_bakedDispatcher = new EventHandlerData[_dispatchers.Count];
_bakedCount = _dispatchers.Count;
for (var i = 0; i < _dispatchers.Count; i++)
_bakedDispatcher[i] = _dispatchers[i];
Array.Sort(_bakedDispatcher, 0, _bakedCount, EventHandlerDataComparer.Instance);
}
/// <summary>
/// Raises this event for all event handlers, passing the reference to all of them
/// </summary>
/// <param name="evt">event to raise</param>
public void RaiseEvent(ref T evt)
{
try
{
_lock.EnterUpgradeableReadLock();
if (_dispatchersDirty)
try
{
_lock.EnterWriteLock();
Bake();
}
finally
{
_lock.ExitWriteLock();
}
for (var i = 0; i < _bakedCount; i++)
_bakedDispatcher[i].Raise(ref evt);
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
private class EventHandlerDataComparer : IComparer<EventHandlerData>
{
internal static readonly EventHandlerDataComparer Instance = new EventHandlerDataComparer();
/// <inheritdoc cref="IComparer{EventHandlerData}.Compare"/>
/// <remarks>
/// This sorts event handlers with ascending priority order.
/// </remarks>
public int Compare(EventHandlerData x, EventHandlerData y)
{
return x._attribute.Priority.CompareTo(y._attribute.Priority);
}
}
}
}

191
Torch/Event/EventManager.cs Normal file
View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using NLog;
using Torch.API;
using Torch.API.Event;
using Torch.Managers;
namespace Torch.Event
{
/// <summary>
/// Manager class responsible for managing registration and dispatching of events.
/// </summary>
public class EventManager : Manager, IEventManager
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static readonly Dictionary<Type, IEventList> _eventLists = new Dictionary<Type, IEventList>();
internal static void AddDispatchShims(Assembly asm)
{
foreach (Type type in asm.GetTypes())
if (type.HasAttribute<EventShimAttribute>())
AddDispatchShim(type);
}
private static readonly HashSet<Type> _dispatchShims = new HashSet<Type>();
private static void AddDispatchShim(Type type)
{
lock (_dispatchShims)
if (!_dispatchShims.Add(type))
return;
if (!type.IsSealed || !type.IsAbstract)
_log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it isn't declared singleton");
var listsFound = 0;
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(EventList<>))
{
Type eventType = field.FieldType.GenericTypeArguments[0];
if (_eventLists.ContainsKey(eventType))
_log.Error($"Ignore event dispatch list {type.FullName}#{field.Name}; we already have one.");
else
{
_eventLists.Add(eventType, (IEventList)field.GetValue(null));
listsFound++;
}
}
if (listsFound == 0)
_log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it has no event lists.");
}
/// <summary>
/// Gets all event handler methods declared by the given type and its base types.
/// </summary>
/// <param name="exploreType">Type to explore</param>
/// <returns>All event handler methods</returns>
private static IEnumerable<MethodInfo> EventHandlers(Type exploreType)
{
IEnumerable<MethodInfo> enumerable = exploreType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Where(x =>
{
var attr = x.GetCustomAttribute<EventHandlerAttribute>();
if (attr == null)
return false;
ParameterInfo[] ps = x.GetParameters();
if (ps.Length != 1)
return false;
return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType);
});
return exploreType.BaseType != null ? enumerable.Concat(EventHandlers(exploreType.BaseType)) : enumerable;
}
/// <inheritdoc/>
private static void RegisterHandlerInternal(IEventHandler instance)
{
foreach (MethodInfo handler in EventHandlers(instance.GetType()))
{
Type eventType = handler.GetParameters()[0].ParameterType;
if (eventType.IsInterface)
{
var foundList = false;
foreach (KeyValuePair<Type, IEventList> kv in _eventLists)
if (eventType.IsAssignableFrom(kv.Key))
{
kv.Value.AddHandler(handler, instance);
foundList = true;
}
if (foundList)
continue;
}
else if (_eventLists.TryGetValue(eventType, out IEventList list))
{
list.AddHandler(handler, instance);
continue;
}
_log.Error($"Unable to find event handler list for event type {eventType.FullName}");
}
}
/// <summary>
/// Unregisters all handlers owned by the given instance
/// </summary>
/// <param name="instance">Instance</param>
private static void UnregisterHandlerInternal(IEventHandler instance)
{
foreach (IEventList list in _eventLists.Values)
list.RemoveHandlers(instance);
}
private Dictionary<Assembly, HashSet<IEventHandler>> _registeredHandlers = new Dictionary<Assembly, HashSet<IEventHandler>>();
/// <inheritdoc/>
public EventManager(ITorchBase torchInstance) : base(torchInstance)
{
}
/// <summary>
/// Registers all event handler methods contained in the given instance
/// </summary>
/// <param name="handler">Instance to register</param>
/// <returns><b>true</b> if added, <b>false</b> otherwise</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool RegisterHandler(IEventHandler handler)
{
Assembly caller = Assembly.GetCallingAssembly();
lock (_registeredHandlers)
{
if (!_registeredHandlers.TryGetValue(caller, out HashSet<IEventHandler> handlers))
_registeredHandlers.Add(caller, handlers = new HashSet<IEventHandler>());
if (handlers.Add(handler))
{
RegisterHandlerInternal(handler);
return true;
}
return false;
}
}
/// <summary>
/// Unregisters all event handler methods contained in the given instance
/// </summary>
/// <param name="handler">Instance to unregister</param>
/// <returns><b>true</b> if removed, <b>false</b> otherwise</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool UnregisterHandler(IEventHandler handler)
{
Assembly caller = Assembly.GetCallingAssembly();
lock (_registeredHandlers)
{
if (!_registeredHandlers.TryGetValue(caller, out HashSet<IEventHandler> handlers))
return false;
if (handlers.Remove(handler))
{
UnregisterHandlerInternal(handler);
return true;
}
return false;
}
}
/// <summary>
/// Unregisters all handlers owned by the given assembly.
/// </summary>
/// <param name="asm">Assembly to unregister</param>
/// <param name="callback">Optional callback invoked before a handler is unregistered. Ignored if null</param>
/// <returns>the number of handlers that were unregistered</returns>
internal int UnregisterAllHandlers(Assembly asm, Action<IEventHandler> callback = null)
{
lock (_registeredHandlers)
{
if (!_registeredHandlers.TryGetValue(asm, out HashSet<IEventHandler> handlers))
return 0;
foreach (IEventHandler k in handlers)
{
callback?.Invoke(k);
UnregisterHandlerInternal(k);
}
int count = handlers.Count;
handlers.Clear();
_registeredHandlers.Remove(asm);
return count;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Event
{
/// <summary>
/// Tagging class used to indicate that the class should be treated as an event shim.
/// Only works for core assemblies loaded by Torch (non-plugins).
/// </summary>
/// <remarks>
/// Event shims should be singleton, and have one (or more) fields that are of type <see cref="EventList{T}"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
internal class EventShimAttribute : Attribute
{
}
}

25
Torch/Event/IEventList.cs Normal file
View File

@@ -0,0 +1,25 @@
using System.Reflection;
using Torch.API.Event;
namespace Torch.Event
{
/// <summary>
/// Represents the interface for adding and removing from an ordered list of callbacks.
/// </summary>
public interface IEventList
{
/// <summary>
/// Adds an event handler for the given method, on the given instance.
/// </summary>
/// <param name="method">Handler method</param>
/// <param name="instance">Instance to invoke the handler on</param>
void AddHandler(MethodInfo method, IEventHandler instance);
/// <summary>
/// Removes all event handlers invoked on the given instance.
/// </summary>
/// <param name="instance">Instance to remove event handlers for</param>
/// <returns>The number of event handlers removed</returns>
int RemoveHandlers(IEventHandler instance);
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Threading;
namespace Torch
{
public static class ICollectionExtensions
{
/// <summary>
/// Returns a read-only wrapped <see cref="ICollection{T}"/>
/// </summary>
public static IReadOnlyCollection<T> AsReadOnly<T>(this ICollection<T> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyCollection<T> ?? new ReadOnlyCollectionAdapter<T>(source);
}
/// <summary>
/// Returns a read-only wrapped <see cref="IList{T}"/>
/// </summary>
public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyList<T> ?? new ReadOnlyCollection<T>(source);
}
/// <summary>
/// Returns a read-only wrapped <see cref="IList{T}"/> and proxies its <see cref="INotifyPropertyChanged"/> and <see cref="INotifyCollectionChanged"/> events.
/// </summary>
public static IReadOnlyList<T> AsReadOnlyObservable<T>(this IList<T> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (source is INotifyPropertyChanged && source is INotifyCollectionChanged)
return new ObservableReadOnlyList<T>(source);
throw new InvalidOperationException("The given list is not observable.");
}
/// <summary>
/// Returns a read-only wrapped <see cref="IDictionary{TKey, TValue}"/>
/// </summary>
public static IReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
return source as IReadOnlyDictionary<TKey, TValue> ?? new ReadOnlyDictionary<TKey, TValue>(source);
}
/// <summary>
/// Returns a read-only wrapped <see cref="IDictionary{TKey,TValue}"/> and proxies its <see cref="INotifyPropertyChanged"/> and <see cref="INotifyCollectionChanged"/> events.
/// </summary>
public static IReadOnlyDictionary<TKey, TValue> AsReadOnlyObservable<TKey, TValue>(this IDictionary<TKey, TValue> source)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (source is INotifyPropertyChanged && source is INotifyCollectionChanged)
return new ObservableReadOnlyDictionary<TKey, TValue>(source);
throw new InvalidOperationException("The given dictionary is not observable.");
}
sealed class ObservableReadOnlyList<T> : ViewModel, IReadOnlyList<T>, IDisposable
{
private IList<T> _list;
public ObservableReadOnlyList(IList<T> list)
{
_list = list;
if (_list is INotifyPropertyChanged p)
p.PropertyChanged += OnPropertyChanged;
if (_list is INotifyCollectionChanged c)
c.CollectionChanged += OnCollectionChanged;
}
public void Dispose()
{
if (_list is INotifyPropertyChanged p)
p.PropertyChanged -= OnPropertyChanged;
if (_list is INotifyCollectionChanged c)
c.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnCollectionChanged(e);
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e.PropertyName);
}
/// <inheritdoc />
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_list).GetEnumerator();
/// <inheritdoc />
public int Count => _list.Count;
/// <inheritdoc />
public T this[int index] => _list[index];
}
sealed class ObservableReadOnlyDictionary<TKey, TValue> : ViewModel, IReadOnlyDictionary<TKey, TValue>, IDisposable
{
private readonly IDictionary<TKey, TValue> _dictionary;
public ObservableReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
{
_dictionary = dictionary;
if (_dictionary is INotifyPropertyChanged p)
p.PropertyChanged += OnPropertyChanged;
if (_dictionary is INotifyCollectionChanged c)
c.CollectionChanged += OnCollectionChanged;
}
public void Dispose()
{
if (_dictionary is INotifyPropertyChanged p)
p.PropertyChanged -= OnPropertyChanged;
if (_dictionary is INotifyCollectionChanged c)
c.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnCollectionChanged(e);
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged(e.PropertyName);
}
/// <inheritdoc />
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _dictionary.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator();
/// <inheritdoc />
public int Count => _dictionary.Count;
/// <inheritdoc />
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
/// <inheritdoc />
public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value);
/// <inheritdoc />
public TValue this[TKey key] => _dictionary[key];
/// <inheritdoc />
public IEnumerable<TKey> Keys => _dictionary.Keys;
/// <inheritdoc />
public IEnumerable<TValue> Values => _dictionary.Values;
}
sealed class ReadOnlyCollectionAdapter<T> : IReadOnlyCollection<T>
{
private readonly ICollection<T> _source;
public ReadOnlyCollectionAdapter(ICollection<T> source)
{
_source = source;
}
public int Count => _source.Count;
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -2,51 +2,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Torch namespace Torch
{ {
public static class StringExtensions public static class StringExtensions
{ {
public static string Truncate(this string s, int maxLength) /// <summary>
/// Try to extract a 3 component version from the string. Format: #.#.#
/// </summary>
public static bool TryExtractVersion(this string version, out Version result)
{ {
return s.Length <= maxLength ? s : s.Substring(0, maxLength); result = null;
} var match = Regex.Match(version, @"(\d+\.)?(\d+\.)?(\d+)");
return match.Success && Version.TryParse(match.Value, out result);
public static IEnumerable<string> ReadLines(this string s, int max, bool skipEmpty = false, char delim = '\n')
{
var lines = s.Split(delim);
for (var i = 0; i < lines.Length && i < max; i++)
{
var l = lines[i];
if (skipEmpty && string.IsNullOrWhiteSpace(l))
continue;
yield return l;
}
}
public static string Wrap(this string s, int lineLength)
{
if (s.Length <= lineLength)
return s;
var result = new StringBuilder();
for (var i = 0; i < s.Length;)
{
var next = i + lineLength;
if (s.Length - 1 < next)
{
result.AppendLine(s.Substring(i));
break;
}
result.AppendLine(s.Substring(i, next));
i = next;
}
return result.ToString();
} }
} }
} }

View File

@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Engine.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using VRage;
using VRage.Library.Collections;
using VRage.Network;
using VRage.Serialization;
using VRage.Utils;
namespace Torch.Managers
{
[Manager]
public class ChatManager : Manager
{
private static Logger _log = LogManager.GetLogger(nameof(ChatManager));
public delegate void MessageRecievedDel(ChatMsg msg, ref bool sendToOthers);
public event MessageRecievedDel MessageRecieved;
internal void RaiseMessageRecieved(ChatMsg msg, ref bool sendToOthers) =>
MessageRecieved?.Invoke(msg, ref sendToOthers);
[Dependency]
private INetworkManager _networkManager;
public ChatManager(ITorchBase torchInstance) : base(torchInstance)
{
}
public override void Attach()
{
try
{
_networkManager.RegisterNetworkHandler(new ChatIntercept(this));
}
catch
{
_log.Error("Failed to initialize network intercept, command hiding will not work! Falling back to another method.");
MyMultiplayer.Static.ChatMessageReceived += Static_ChatMessageReceived;
}
}
private void Static_ChatMessageReceived(ulong arg1, string arg2)
{
var msg = new ChatMsg {Author = arg1, Text = arg2};
var sendToOthers = true;
RaiseMessageRecieved(msg, ref sendToOthers);
}
internal class ChatIntercept : NetworkHandlerBase, INetworkHandler
{
private ChatManager _chatManager;
private bool? _unitTestResult;
public ChatIntercept(ChatManager chatManager)
{
_chatManager = chatManager;
}
public override bool CanHandle(CallSite site)
{
if (site.MethodInfo.Name != "OnChatMessageRecieved")
return false;
if (_unitTestResult.HasValue)
return _unitTestResult.Value;
var parameters = site.MethodInfo.GetParameters();
if (parameters.Length != 1)
{
_unitTestResult = false;
return false;
}
if (parameters[0].ParameterType != typeof(ChatMsg))
_unitTestResult = false;
_unitTestResult = true;
return _unitTestResult.Value;
}
public override bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet)
{
var msg = new ChatMsg();
Serialize(site.MethodInfo, stream, ref msg);
bool sendToOthers = true;
_chatManager.RaiseMessageRecieved(msg, ref sendToOthers);
return !sendToOthers;
}
}
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Game.Entities.Character;
using Sandbox.Game.Gui;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Sandbox.ModAPI;
using Torch.API;
using Torch.API.Managers;
using Torch.Utils;
using VRage.Game;
namespace Torch.Managers.ChatManager
{
public class ChatManagerClient : Manager, IChatManagerClient
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
/// <inheritdoc />
public ChatManagerClient(ITorchBase torchInstance) : base(torchInstance) { }
/// <inheritdoc />
public event MessageRecievedDel MessageRecieved;
/// <inheritdoc />
public event MessageSendingDel MessageSending;
/// <inheritdoc />
public void SendMessageAsSelf(string message)
{
if (MyMultiplayer.Static != null)
{
if (Sandbox.Engine.Platform.Game.IsDedicated)
{
var scripted = new ScriptedChatMsg()
{
Author = "Server",
Font = MyFontEnum.Red,
Text = message,
Target = 0
};
MyMultiplayerBase.SendScriptedChatMessage(ref scripted);
}
else
MyMultiplayer.Static.SendChatMessage(message);
}
else if (HasHud)
MyHud.Chat.ShowMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", message);
}
/// <inheritdoc />
public void DisplayMessageOnSelf(string author, string message, string font)
{
if (HasHud)
MyHud.Chat?.ShowMessage(author, message, font);
MySession.Static.GlobalChatHistory.GlobalChatHistory.Chat.Enqueue(new MyGlobalChatItem()
{
Author = author,
AuthorFont = font,
Text = message
});
}
/// <inheritdoc/>
public override void Attach()
{
base.Attach();
MyAPIUtilities.Static.MessageEntered += OnMessageEntered;
if (MyMultiplayer.Static != null)
{
_chatMessageRecievedReplacer = _chatMessageReceivedFactory.Invoke();
_scriptedChatMessageRecievedReplacer = _scriptedChatMessageReceivedFactory.Invoke();
_chatMessageRecievedReplacer.Replace(new Action<ulong, string>(Multiplayer_ChatMessageReceived),
MyMultiplayer.Static);
_scriptedChatMessageRecievedReplacer.Replace(
new Action<string, string, string>(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
}
else
{
MyAPIUtilities.Static.MessageEntered += OfflineMessageReciever;
}
}
/// <inheritdoc/>
public override void Detach()
{
MyAPIUtilities.Static.MessageEntered -= OnMessageEntered;
if (_chatMessageRecievedReplacer != null && _chatMessageRecievedReplacer.Replaced && HasHud)
_chatMessageRecievedReplacer.Restore(MyHud.Chat);
if (_scriptedChatMessageRecievedReplacer != null && _scriptedChatMessageRecievedReplacer.Replaced && HasHud)
_scriptedChatMessageRecievedReplacer.Restore(MyHud.Chat);
MyAPIUtilities.Static.MessageEntered -= OfflineMessageReciever;
base.Detach();
}
/// <summary>
/// Callback used to process offline messages.
/// </summary>
/// <param name="msg"></param>
/// <returns>true if the message was consumed</returns>
protected virtual bool OfflineMessageProcessor(TorchChatMessage msg)
{
return false;
}
private void OfflineMessageReciever(string messageText, ref bool sendToOthers)
{
if (!sendToOthers)
return;
var torchMsg = new TorchChatMessage(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player", Sync.MyId, messageText);
var consumed = false;
MessageRecieved?.Invoke(torchMsg, ref consumed);
if (!consumed)
consumed = OfflineMessageProcessor(torchMsg);
sendToOthers = !consumed;
}
private void OnMessageEntered(string messageText, ref bool sendToOthers)
{
if (!sendToOthers)
return;
var consumed = false;
MessageSending?.Invoke(messageText, ref consumed);
sendToOthers = !consumed;
}
private void Multiplayer_ChatMessageReceived(ulong steamUserId, string message)
{
var torchMsg = new TorchChatMessage(steamUserId, message,
(steamUserId == MyGameService.UserId) ? MyFontEnum.DarkBlue : MyFontEnum.Blue);
var consumed = false;
MessageRecieved?.Invoke(torchMsg, ref consumed);
if (!consumed && HasHud)
_hudChatMessageReceived.Invoke(MyHud.Chat, steamUserId, message);
}
private void Multiplayer_ScriptedChatMessageReceived(string message, string author, string font)
{
var torchMsg = new TorchChatMessage(author, message, font);
var consumed = false;
MessageRecieved?.Invoke(torchMsg, ref consumed);
if (!consumed && HasHud)
_hudChatScriptedMessageReceived.Invoke(MyHud.Chat, author, message, font);
}
private const string _hudChatMessageReceivedName = "Multiplayer_ChatMessageReceived";
private const string _hudChatScriptedMessageReceivedName = "multiplayer_ScriptedChatMessageReceived";
protected static bool HasHud => !Sandbox.Engine.Platform.Game.IsDedicated;
[ReflectedMethod(Name = _hudChatMessageReceivedName)]
private static Action<MyHudChat, ulong, string> _hudChatMessageReceived;
[ReflectedMethod(Name = _hudChatScriptedMessageReceivedName)]
private static Action<MyHudChat, string, string, string> _hudChatScriptedMessageReceived;
[ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ChatMessageReceived), typeof(MyHudChat), _hudChatMessageReceivedName)]
private static Func<ReflectedEventReplacer> _chatMessageReceivedFactory;
[ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ScriptedChatMessageReceived), typeof(MyHudChat), _hudChatScriptedMessageReceivedName)]
private static Func<ReflectedEventReplacer> _scriptedChatMessageReceivedFactory;
private ReflectedEventReplacer _chatMessageRecievedReplacer;
private ReflectedEventReplacer _scriptedChatMessageRecievedReplacer;
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Game.Gui;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
using Torch.Utils;
using VRage;
using VRage.Library.Collections;
using VRage.Network;
namespace Torch.Managers.ChatManager
{
public class ChatManagerServer : ChatManagerClient, IChatManagerServer
{
[Dependency(Optional = true)]
private INetworkManager _networkManager;
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly ChatIntercept _chatIntercept;
/// <inheritdoc />
public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
{
_chatIntercept = new ChatIntercept(this);
}
/// <inheritdoc />
public event MessageProcessingDel MessageProcessing;
/// <inheritdoc />
public void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0)
{
if (MyMultiplayer.Static == null)
{
if ((targetSteamId == MyGameService.UserId || targetSteamId == 0) && HasHud)
MyHud.Chat?.ShowMessage(authorId == MyGameService.UserId ?
(MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player") : $"user_{authorId}", message);
return;
}
if (MyMultiplayer.Static is MyDedicatedServerBase dedicated)
{
var msg = new ChatMsg() { Author = authorId, Text = message };
_dedicatedServerBaseSendChatMessage.Invoke(ref msg);
_dedicatedServerBaseOnChatMessage.Invoke(dedicated, new object[] { msg });
}
}
#pragma warning disable 649
private delegate void MultiplayerBaseSendChatMessageDel(ref ChatMsg arg);
[ReflectedStaticMethod(Name = "SendChatMessage", Type = typeof(MyMultiplayerBase))]
private static MultiplayerBaseSendChatMessageDel _dedicatedServerBaseSendChatMessage;
// [ReflectedMethod] doesn't play well with instance methods and refs.
[ReflectedMethodInfo(typeof(MyDedicatedServerBase), "OnChatMessage")]
private static MethodInfo _dedicatedServerBaseOnChatMessage;
#pragma warning restore 649
/// <inheritdoc />
public void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0)
{
if (MyMultiplayer.Static == null)
{
if ((targetSteamId == MyGameService.UserId || targetSteamId == 0) && HasHud)
MyHud.Chat?.ShowMessage(author, message, font);
return;
}
var scripted = new ScriptedChatMsg()
{
Author = author,
Text = message,
Font = font,
Target = Sync.Players.TryGetIdentityId(targetSteamId)
};
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)
{
if (MyMultiplayer.Static != null)
return false;
var consumed = false;
MessageProcessing?.Invoke(msg, ref consumed);
return consumed;
}
private void MpStaticChatMessageReceived(ulong a, string b)
{
var tmp = false;
RaiseMessageRecieved(new ChatMsg()
{
Author = a,
Text = b
}, ref tmp);
}
/// <inheritdoc/>
public override void Detach()
{
if (MyMultiplayer.Static != null)
MyMultiplayer.Static.ChatMessageReceived -= MpStaticChatMessageReceived;
_networkManager?.UnregisterNetworkHandler(_chatIntercept);
base.Detach();
}
internal void RaiseMessageRecieved(ChatMsg message, ref bool consumed)
{
var torchMsg =
new TorchChatMessage(MyMultiplayer.Static?.GetMemberName(message.Author) ?? $"user_{message.Author}",
message.Author, message.Text);
MessageProcessing?.Invoke(torchMsg, ref consumed);
}
internal class ChatIntercept : NetworkHandlerBase, INetworkHandler
{
private readonly ChatManagerServer _chatManager;
private bool? _unitTestResult;
public ChatIntercept(ChatManagerServer chatManager)
{
_chatManager = chatManager;
}
/// <inheritdoc/>
public override bool CanHandle(CallSite site)
{
if (site.MethodInfo.Name != "OnChatMessageRecieved")
return false;
if (_unitTestResult.HasValue)
return _unitTestResult.Value;
ParameterInfo[] parameters = site.MethodInfo.GetParameters();
if (parameters.Length != 1)
{
_unitTestResult = false;
return false;
}
if (parameters[0].ParameterType != typeof(ChatMsg))
_unitTestResult = false;
_unitTestResult = true;
return _unitTestResult.Value;
}
/// <inheritdoc/>
public override bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet)
{
var msg = new ChatMsg();
Serialize(site.MethodInfo, stream, ref msg);
var consumed = false;
_chatManager.RaiseMessageRecieved(msg, ref consumed);
return consumed;
}
}
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using Torch.API;
using Torch.Managers.PatchManager;
using Torch.Utils;
using VRage.Utils;
namespace Torch.Managers
{
[PatchShim]
internal static class KeenLogPatch
{
private static readonly Logger _log = LogManager.GetLogger("Keen");
#pragma warning disable 649
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.Log), Parameters = new[] { typeof(MyLogSeverity), typeof(StringBuilder) })]
private static MethodInfo _logStringBuilder;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.Log), Parameters = new[] { typeof(MyLogSeverity), typeof(string), typeof(object[]) })]
private static MethodInfo _logFormatted;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLine), Parameters = new[] { typeof(string) })]
private static MethodInfo _logWriteLine;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.AppendToClosedLog), Parameters = new[] { typeof(string) })]
private static MethodInfo _logAppendToClosedLog;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLine), Parameters = new[] { typeof(string), typeof(LoggingOptions) })]
private static MethodInfo _logWriteLineOptions;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLine), Parameters = new[] { typeof(Exception) })]
private static MethodInfo _logWriteLineException;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.AppendToClosedLog), Parameters = new[] { typeof(Exception) })]
private static MethodInfo _logAppendToClosedLogException;
[ReflectedMethodInfo(typeof(MyLog), nameof(MyLog.WriteLineAndConsole), Parameters = new[] { typeof(string) })]
private static MethodInfo _logWriteLineAndConsole;
#pragma warning restore 649
public static void Patch(PatchContext context)
{
context.GetPattern(_logStringBuilder).Prefixes.Add(Method(nameof(PrefixLogStringBuilder)));
context.GetPattern(_logFormatted).Prefixes.Add(Method(nameof(PrefixLogFormatted)));
context.GetPattern(_logWriteLine).Prefixes.Add(Method(nameof(PrefixWriteLine)));
context.GetPattern(_logAppendToClosedLog).Prefixes.Add(Method(nameof(PrefixAppendToClosedLog)));
context.GetPattern(_logWriteLineAndConsole).Prefixes.Add(Method(nameof(PrefixWriteLineConsole)));
context.GetPattern(_logWriteLineException).Prefixes.Add(Method(nameof(PrefixWriteLineException)));
context.GetPattern(_logAppendToClosedLogException).Prefixes.Add(Method(nameof(PrefixAppendToClosedLogException)));
context.GetPattern(_logWriteLineOptions).Prefixes.Add(Method(nameof(PrefixWriteLineOptions)));
}
private static MethodInfo Method(string name)
{
return typeof(KeenLogPatch).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
}
[ReflectedMethod(Name = "GetThreadId")]
private static Func<MyLog, int> _getThreadId;
[ReflectedMethod(Name = "GetIdentByThread")]
private static Func<MyLog, int, int> _getIndentByThread;
private static readonly ThreadLocal<StringBuilder> _tmpStringBuilder = new ThreadLocal<StringBuilder>(() => new StringBuilder(32));
private static StringBuilder PrepareLog(MyLog log)
{
return _tmpStringBuilder.Value.Clear().Append(' ', _getIndentByThread(log, _getThreadId(log)) * 3);
}
private static bool PrefixWriteLine(MyLog __instance, string msg)
{
_log.Debug(PrepareLog(__instance).Append(msg));
return false;
}
private static bool PrefixWriteLineConsole(MyLog __instance, string msg)
{
_log.Info(PrepareLog(__instance).Append(msg));
return false;
}
private static bool PrefixAppendToClosedLog(MyLog __instance, string text)
{
_log.Info(PrepareLog(__instance).Append(text));
return false;
}
private static bool PrefixWriteLineOptions(MyLog __instance, string message, LoggingOptions option)
{
if (__instance.LogFlag(option))
_log.Info(PrepareLog(__instance).Append(message));
return false;
}
private static bool PrefixAppendToClosedLogException(Exception e)
{
_log.Error(e);
return false;
}
private static bool PrefixWriteLineException(Exception ex)
{
_log.Error(ex);
return false;
}
private static bool PrefixLogFormatted(MyLog __instance, MyLogSeverity severity, string format, object[] args)
{
_log.Log(LogLevelFor(severity), PrepareLog(__instance).AppendFormat(format, args));
return false;
}
private static bool PrefixLogStringBuilder(MyLog __instance, MyLogSeverity severity, StringBuilder builder)
{
_log.Log(LogLevelFor(severity), PrepareLog(__instance).Append(builder));
return false;
}
private static LogLevel LogLevelFor(MyLogSeverity severity)
{
switch (severity)
{
case MyLogSeverity.Debug:
return LogLevel.Debug;
case MyLogSeverity.Info:
return LogLevel.Info;
case MyLogSeverity.Warning:
return LogLevel.Warn;
case MyLogSeverity.Error:
return LogLevel.Error;
case MyLogSeverity.Critical:
return LogLevel.Fatal;
default:
return LogLevel.Info;
}
}
}
}

View File

@@ -1,338 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using NLog;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Game.Entities.Character;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Sandbox.ModAPI;
using SteamSDK;
using Torch.API;
using Torch.API.Managers;
using Torch.Collections;
using Torch.Commands;
using Torch.Utils;
using Torch.ViewModels;
using VRage.Game;
using VRage.Game.ModAPI;
using VRage.GameServices;
using VRage.Library.Collections;
using VRage.Network;
using VRage.Steam;
using VRage.Utils;
namespace Torch.Managers
{
/// <inheritdoc />
public class MultiplayerManager : Manager, IMultiplayerManager
{
/// <inheritdoc />
public event Action<IPlayer> PlayerJoined;
/// <inheritdoc />
public event Action<IPlayer> PlayerLeft;
/// <inheritdoc />
public event MessageReceivedDel MessageReceived;
public IList<IChatMessage> ChatHistory { get; } = new ObservableList<IChatMessage>();
public ObservableDictionary<ulong, PlayerViewModel> Players { get; } = new ObservableDictionary<ulong, PlayerViewModel>();
public IMyPlayer LocalPlayer => MySession.Static.LocalHumanPlayer;
private static readonly Logger Log = LogManager.GetLogger(nameof(MultiplayerManager));
private static readonly Logger ChatLog = LogManager.GetLogger("Chat");
[ReflectedGetter(Name = "m_players")]
private static Func<MyPlayerCollection, Dictionary<MyPlayer.PlayerId, MyPlayer>> _onlinePlayers;
[Dependency]
private ChatManager _chatManager;
[Dependency]
private CommandManager _commandManager;
[Dependency]
private NetworkManager _networkManager;
internal MultiplayerManager(ITorchBase torch) : base(torch)
{
}
/// <inheritdoc />
public override void Attach()
{
Torch.SessionLoaded += OnSessionLoaded;
_chatManager.MessageRecieved += Instance_MessageRecieved;
}
private void Instance_MessageRecieved(ChatMsg msg, ref bool sendToOthers)
{
var message = ChatMessage.FromChatMsg(msg);
ChatHistory.Add(message);
ChatLog.Info($"{message.Name}: {message.Message}");
MessageReceived?.Invoke(message, ref sendToOthers);
}
/// <inheritdoc />
public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
/// <inheritdoc />
public void BanPlayer(ulong steamId, bool banned = true)
{
Torch.Invoke(() =>
{
MyMultiplayer.Static.BanClient(steamId, banned);
if (_gameOwnerIds.ContainsKey(steamId))
MyMultiplayer.Static.BanClient(_gameOwnerIds[steamId], banned);
});
}
/// <inheritdoc />
public IMyPlayer GetPlayerByName(string name)
{
return _onlinePlayers.Invoke(MySession.Static.Players).FirstOrDefault(x => x.Value.DisplayName == name).Value;
}
/// <inheritdoc />
public IMyPlayer GetPlayerBySteamId(ulong steamId)
{
_onlinePlayers.Invoke(MySession.Static.Players).TryGetValue(new MyPlayer.PlayerId(steamId), out MyPlayer p);
return p;
}
public ulong GetSteamId(long identityId)
{
foreach (var kv in _onlinePlayers.Invoke(MySession.Static.Players))
{
if (kv.Value.Identity.IdentityId == identityId)
return kv.Key.SteamId;
}
return 0;
}
/// <inheritdoc />
public string GetSteamUsername(ulong steamId)
{
return MyMultiplayer.Static.GetMemberName(steamId);
}
/// <inheritdoc />
public void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Red)
{
if (string.IsNullOrEmpty(message))
return;
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, author, message));
if (_commandManager.IsCommand(message))
{
var response = _commandManager.HandleCommandFromServer(message);
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, author, response));
}
else
{
var msg = new ScriptedChatMsg { Author = author, Font = font, Target = playerId, Text = message };
MyMultiplayerBase.SendScriptedChatMessage(ref msg);
var character = MySession.Static.Players.TryGetIdentity(playerId)?.Character;
var steamId = GetSteamId(playerId);
if (character == null)
return;
var addToGlobalHistoryMethod = typeof(MyCharacter).GetMethod("OnGlobalMessageSuccess", BindingFlags.Instance | BindingFlags.NonPublic);
_networkManager.RaiseEvent(addToGlobalHistoryMethod, character, steamId, steamId, message);
}
}
private void OnSessionLoaded()
{
Log.Info("Initializing Steam auth");
MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft;
//TODO: Move these with the methods?
if (!RemoveHandlers())
{
Log.Error("Steam auth failed to initialize");
return;
}
MyGameService.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse;
MyGameService.GameServer.UserGroupStatusResponse += UserGroupStatusResponse;
Log.Info("Steam auth initialized");
}
private void OnClientKicked(ulong steamId)
{
OnClientLeft(steamId, MyChatMemberStateChangeEnum.Kicked);
}
private void OnClientLeft(ulong steamId, MyChatMemberStateChangeEnum stateChange)
{
Players.TryGetValue(steamId, out PlayerViewModel vm);
if (vm == null)
vm = new PlayerViewModel(steamId);
Log.Info($"{vm.Name} ({vm.SteamId}) {(ConnectionState)stateChange}.");
PlayerLeft?.Invoke(vm);
Players.Remove(steamId);
}
//TODO: Split the following into a new file?
//These methods override some Keen code to allow us full control over client authentication.
//This lets us have a server set to private (admins only) or friends (friends of all listed admins)
[ReflectedGetter(Name = "m_members")]
private static Func<MyDedicatedServerBase, List<ulong>> _members;
[ReflectedGetter(Name = "m_waitingForGroup")]
private static Func<MyDedicatedServerBase, HashSet<ulong>> _waitingForGroup;
[ReflectedGetter(Name = "m_kickedClients")]
private static Func<MyMultiplayerBase, Dictionary<ulong, int>> _kickedClients;
//private HashSet<ulong> _waitingForFriends;
private Dictionary<ulong, ulong> _gameOwnerIds = new Dictionary<ulong, ulong>();
//private IMultiplayer _multiplayerImplementation;
/// <summary>
/// Removes Keen's hooks into some Steam events so we have full control over client authentication
/// </summary>
private static bool RemoveHandlers()
{
MethodInfo methodValidateAuthTicket = typeof(MyDedicatedServerBase).GetMethod("GameServer_ValidateAuthTicketResponse",
BindingFlags.NonPublic | BindingFlags.Instance);
if (methodValidateAuthTicket == null)
{
Log.Error("Unable to find the GameServer_ValidateAuthTicketResponse method to unhook");
return false;
}
var eventValidateAuthTicket = Reflection.GetInstanceEvent(MyGameService.GameServer, nameof(MyGameService.GameServer.ValidateAuthTicketResponse))
.FirstOrDefault(x => x.Method == methodValidateAuthTicket) as Action<ulong, JoinResult, ulong>;
if (eventValidateAuthTicket == null)
{
Log.Error(
"Unable to unhook the GameServer_ValidateAuthTicketResponse method from GameServer.ValidateAuthTicketResponse");
Log.Debug(" Want to unhook {0}", methodValidateAuthTicket);
Log.Debug(" Registered handlers: ");
foreach (Delegate method in Reflection.GetInstanceEvent(MyGameService.GameServer,
nameof(MyGameService.GameServer.ValidateAuthTicketResponse)))
Log.Debug(" - " + method.Method);
return false;
}
MethodInfo methodUserGroupStatus = typeof(MyDedicatedServerBase).GetMethod("GameServer_UserGroupStatus",
BindingFlags.NonPublic | BindingFlags.Instance);
if (methodUserGroupStatus == null)
{
Log.Error("Unable to find the GameServer_UserGroupStatus method to unhook");
return false;
}
var eventUserGroupStatus = Reflection.GetInstanceEvent(MyGameService.GameServer, nameof(MyGameService.GameServer.UserGroupStatusResponse))
.FirstOrDefault(x => x.Method == methodUserGroupStatus)
as Action<ulong, ulong, bool, bool>;
if (eventUserGroupStatus == null)
{
Log.Error("Unable to unhook the GameServer_UserGroupStatus method from GameServer.UserGroupStatus");
Log.Debug(" Want to unhook {0}", methodUserGroupStatus);
Log.Debug(" Registered handlers: ");
foreach (Delegate method in Reflection.GetInstanceEvent(MyGameService.GameServer, nameof(MyGameService.GameServer.UserGroupStatusResponse)))
Log.Debug(" - " + method.Method);
return false;
}
MyGameService.GameServer.ValidateAuthTicketResponse -=
eventValidateAuthTicket;
MyGameService.GameServer.UserGroupStatusResponse -=
eventUserGroupStatus;
return true;
}
//Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner)
{
Log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}");
if (IsClientBanned.Invoke(MyMultiplayer.Static, steamOwner) || MySandboxGame.ConfigDedicated.Banned.Contains(steamOwner))
{
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.BannedByAdmins);
RaiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
else if (IsClientKicked.Invoke(MyMultiplayer.Static, steamOwner))
{
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.KickedRecently);
RaiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
if (response != JoinResult.OK)
{
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, response);
return;
}
if (MyMultiplayer.Static.MemberLimit > 0 && _members.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Count - 1 >= MyMultiplayer.Static.MemberLimit)
{
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.ServerFull);
return;
}
if (MySandboxGame.ConfigDedicated.GroupID == 0uL ||
MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) ||
MySandboxGame.ConfigDedicated.Administrators.Contains(ConvertSteamIDFrom64(steamID)))
{
this.UserAccepted(steamID);
return;
}
if (GetServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan)
{
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.GroupIdInvalid);
return;
}
if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
{
_waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Add(steamID);
return;
}
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.SteamServersOffline);
}
private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer)
{
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Remove(userId))
{
if (member || officer)
UserAccepted(userId);
else
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, userId, JoinResult.NotInGroup);
}
}
private void UserAccepted(ulong steamId)
{
UserAcceptedImpl.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamId);
var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected };
Log.Info($"Player {vm.Name} joined ({vm.SteamId})");
Players.Add(steamId, vm);
PlayerJoined?.Invoke(vm);
}
[ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase))]
private static Func<ulong, string> ConvertSteamIDFrom64;
[ReflectedStaticMethod(Type = typeof(MyGameService))]
private static Func<ulong, MyGameServiceAccountType> GetServerAccountType;
[ReflectedMethod(Name = "UserAccepted")]
private static Action<MyDedicatedServerBase, ulong> UserAcceptedImpl;
[ReflectedMethod]
private static Action<MyDedicatedServerBase, ulong, JoinResult> UserRejected;
[ReflectedMethod]
private static Func<MyMultiplayerBase, ulong, bool> IsClientBanned;
[ReflectedMethod]
private static Func<MyMultiplayerBase, ulong, bool> IsClientKicked;
[ReflectedMethod]
private static Action<MyMultiplayerBase, ulong> RaiseClientKicked;
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using NLog;
using Torch;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Sandbox.Game.Entities.Character;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.World;
using Sandbox.ModAPI;
using SteamSDK;
using Torch.API;
using Torch.API.Managers;
using Torch.Collections;
using Torch.Commands;
using Torch.Utils;
using Torch.ViewModels;
using VRage.Game;
using VRage.Game.ModAPI;
using VRage.GameServices;
using VRage.Library.Collections;
using VRage.Network;
using VRage.Steam;
using VRage.Utils;
namespace Torch.Managers
{
/// <inheritdoc />
public abstract class MultiplayerManagerBase : Manager, IMultiplayerManagerBase
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
/// <inheritdoc />
public event Action<IPlayer> PlayerJoined;
/// <inheritdoc />
public event Action<IPlayer> PlayerLeft;
public MtObservableDictionary<ulong, PlayerViewModel> Players { get; } = new MtObservableDictionary<ulong, PlayerViewModel>();
#pragma warning disable 649
[ReflectedGetter(Name = "m_players")]
private static Func<MyPlayerCollection, Dictionary<MyPlayer.PlayerId, MyPlayer>> _onlinePlayers;
#pragma warning restore 649
protected MultiplayerManagerBase(ITorchBase torch) : base(torch)
{
}
/// <inheritdoc />
public override void Attach()
{
MyMultiplayer.Static.ClientLeft += OnClientLeft;
}
/// <inheritdoc />
public override void Detach()
{
if (MyMultiplayer.Static != null)
MyMultiplayer.Static.ClientLeft -= OnClientLeft;
}
/// <inheritdoc />
public IMyPlayer GetPlayerByName(string name)
{
return _onlinePlayers.Invoke(MySession.Static.Players).FirstOrDefault(x => x.Value.DisplayName == name).Value;
}
/// <inheritdoc />
public IMyPlayer GetPlayerBySteamId(ulong steamId)
{
_onlinePlayers.Invoke(MySession.Static.Players).TryGetValue(new MyPlayer.PlayerId(steamId), out MyPlayer p);
return p;
}
public ulong GetSteamId(long identityId)
{
foreach (KeyValuePair<MyPlayer.PlayerId, MyPlayer> kv in _onlinePlayers.Invoke(MySession.Static.Players))
{
if (kv.Value.Identity.IdentityId == identityId)
return kv.Key.SteamId;
}
return 0;
}
/// <inheritdoc />
public string GetSteamUsername(ulong steamId)
{
return MyMultiplayer.Static.GetMemberName(steamId);
}
private void OnClientLeft(ulong steamId, MyChatMemberStateChangeEnum stateChange)
{
Players.TryGetValue(steamId, out PlayerViewModel vm);
if (vm == null)
vm = new PlayerViewModel(steamId);
_log.Info($"{vm.Name} ({vm.SteamId}) {(ConnectionState)stateChange}.");
PlayerLeft?.Invoke(vm);
Players.Remove(steamId);
}
protected void RaiseClientJoined(ulong steamId)
{
var vm = new PlayerViewModel(steamId) { State = ConnectionState.Connected };
_log.Info($"Player {vm.Name} joined ({vm.SteamId}");
Players.Add(steamId, vm);
PlayerJoined?.Invoke(vm);
}
}
}

View File

@@ -20,9 +20,9 @@ namespace Torch.Managers
{ {
private static Logger _log = LogManager.GetLogger(nameof(NetworkManager)); private static Logger _log = LogManager.GetLogger(nameof(NetworkManager));
private const string MyTransportLayerField = "TransportLayer"; private const string _myTransportLayerField = "TransportLayer";
private const string TransportHandlersField = "m_handlers"; private const string _transportHandlersField = "m_handlers";
private HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>(); private readonly HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
private bool _init; private bool _init;
[ReflectedGetter(Name = "m_typeTable")] [ReflectedGetter(Name = "m_typeTable")]
@@ -40,14 +40,14 @@ namespace Torch.Managers
try try
{ {
var syncLayerType = typeof(MySyncLayer); var syncLayerType = typeof(MySyncLayer);
var transportLayerField = syncLayerType.GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance); var transportLayerField = syncLayerType.GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance);
if (transportLayerField == null) if (transportLayerField == null)
throw new TypeLoadException("Could not find internal type for TransportLayer"); throw new TypeLoadException("Could not find internal type for TransportLayer");
var transportLayerType = transportLayerField.FieldType; var transportLayerType = transportLayerField.FieldType;
if (!Reflection.HasField(transportLayerType, TransportHandlersField)) if (!Reflection.HasField(transportLayerType, _transportHandlersField))
throw new TypeLoadException("Could not find Handlers field"); throw new TypeLoadException("Could not find Handlers field");
return true; return true;
@@ -60,15 +60,9 @@ namespace Torch.Managers
throw; throw;
} }
} }
/// <summary>
/// Loads the network intercept system
/// </summary>
public override void Attach()
{
Torch.SessionLoaded += OnSessionLoaded;
}
private void OnSessionLoaded() /// <inheritdoc/>
public override void Attach()
{ {
if (_init) if (_init)
return; return;
@@ -79,9 +73,9 @@ namespace Torch.Managers
throw new InvalidOperationException("Reflection unit test failed."); throw new InvalidOperationException("Reflection unit test failed.");
//don't bother with nullchecks here, it was all handled in ReflectionUnitTest //don't bother with nullchecks here, it was all handled in ReflectionUnitTest
var transportType = typeof(MySyncLayer).GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType; 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 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 handlers = (IDictionary)transportType.GetField(_transportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
var handlerTypeField = handlers.GetType().GenericTypeArguments[0].GetField("messageId"); //Should be MyTransportLayer.HandlerId var handlerTypeField = handlers.GetType().GenericTypeArguments[0].GetField("messageId"); //Should be MyTransportLayer.HandlerId
object id = null; object id = null;
foreach (var key in handlers.Keys) foreach (var key in handlers.Keys)
@@ -105,6 +99,12 @@ namespace Torch.Managers
_log.Debug("Initialized network intercept"); _log.Debug("Initialized network intercept");
} }
/// <inheritdoc/>
public override void Detach()
{
// TODO reverse what was done in Attach
}
#region Network Intercept #region Network Intercept
/// <summary> /// <summary>
@@ -205,6 +205,8 @@ namespace Torch.Managers
} }
} }
/// <inheritdoc />
public void RegisterNetworkHandler(INetworkHandler handler) public void RegisterNetworkHandler(INetworkHandler handler)
{ {
var handlerType = handler.GetType().FullName; var handlerType = handler.GetType().FullName;
@@ -225,6 +227,12 @@ namespace Torch.Managers
_networkHandlers.Add(handler); _networkHandlers.Add(handler);
} }
/// <inheritdoc />
public bool UnregisterNetworkHandler(INetworkHandler handler)
{
return _networkHandlers.Remove(handler);
}
public void RegisterNetworkHandlers(params INetworkHandler[] handlers) public void RegisterNetworkHandlers(params INetworkHandler[] handlers)
{ {
foreach (var handler in handlers) foreach (var handler in handlers)

View File

@@ -0,0 +1,104 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Torch.Utils;
namespace Torch.Managers.PatchManager
{
internal class AssemblyMemory
{
#pragma warning disable 649
[ReflectedMethod(Name = "GetMethodDescriptor")]
private static Func<DynamicMethod, RuntimeMethodHandle> _getMethodHandle;
#pragma warning restore 649
/// <summary>
/// Gets the address, in RAM, where the body of a method starts.
/// </summary>
/// <param name="method">Method to find the start of</param>
/// <returns>Address of the method's start</returns>
public static long GetMethodBodyStart(MethodBase method)
{
RuntimeMethodHandle handle;
if (method is DynamicMethod dyn)
handle = _getMethodHandle.Invoke(dyn);
else
handle = method.MethodHandle;
RuntimeHelpers.PrepareMethod(handle);
return handle.GetFunctionPointer().ToInt64();
}
// x64 ISA format:
// [prefixes] [opcode] [mod-r/m]
// [mod-r/m] is bitfield:
// [7-6] = "mod" adressing mode
// [5-3] = register or opcode extension
// [2-0] = "r/m" extra addressing mode
// http://ref.x86asm.net/coder64.html
/// Direct register addressing mode. (Jump directly to register)
private const byte MODRM_MOD_DIRECT = 0b11;
/// Long-mode prefix (64-bit operand)
private const byte REX_W = 0x48;
/// Moves a 16/32/64 operand into register i when opcode is (MOV_R0+i)
private const byte MOV_R0 = 0xB8;
// Extra opcodes. Used with opcode extension.
private const byte EXT = 0xFF;
/// Opcode extension used with <see cref="EXT"/> for the JMP opcode.
private const byte OPCODE_EXTENSION_JMP = 4;
/// <summary>
/// Reads a byte array from a memory location
/// </summary>
/// <param name="memory">Address to read from</param>
/// <param name="bytes">Number of bytes to read</param>
/// <returns>The bytes that were read</returns>
public static byte[] ReadMemory(long memory, int bytes)
{
var data = new byte[bytes];
Marshal.Copy(new IntPtr(memory), data,0, bytes);
return data;
}
/// <summary>
/// Writes a byte array to a memory location.
/// </summary>
/// <param name="memory">Address to write to</param>
/// <param name="bytes">Data to write</param>
public static void WriteMemory(long memory, byte[] bytes)
{
Marshal.Copy(bytes,0, new IntPtr(memory), bytes.Length);
}
/// <summary>
/// Writes an x64 assembly jump instruction at the given address.
/// </summary>
/// <param name="memory">Address to write the instruction at</param>
/// <param name="jumpTarget">Target address of the jump</param>
/// <returns>The bytes that were overwritten</returns>
public static byte[] WriteJump(long memory, long jumpTarget)
{
byte[] result = ReadMemory(memory, 12);
unsafe
{
var ptr = (byte*)memory;
*ptr = REX_W;
*(ptr + 1) = MOV_R0;
*((long*)(ptr + 2)) = jumpTarget;
*(ptr + 10) = EXT;
*(ptr + 11) = (MODRM_MOD_DIRECT << 6) | (OPCODE_EXTENSION_JMP << 3) | 0;
}
return result;
}
}
}

View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NLog;
using Torch.Managers.PatchManager.MSIL;
using Torch.Managers.PatchManager.Transpile;
using Torch.Utils;
namespace Torch.Managers.PatchManager
{
internal class DecoratedMethod : MethodRewritePattern
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly MethodBase _method;
internal DecoratedMethod(MethodBase method) : base(null)
{
_method = method;
}
private long _revertAddress;
private byte[] _revertData = null;
private GCHandle? _pinnedPatch;
internal bool HasChanged()
{
return Prefixes.HasChanges() || Suffixes.HasChanges() || Transpilers.HasChanges() || PostTranspilers.HasChanges();
}
internal void Commit()
{
try
{
// non-greedy so they are all reset
if (!Prefixes.HasChanges(true) & !Suffixes.HasChanges(true) & !Transpilers.HasChanges(true) & !PostTranspilers.HasChanges(true))
return;
Revert();
if (Prefixes.Count == 0 && Suffixes.Count == 0 && Transpilers.Count == 0 && PostTranspilers.Count == 0)
return;
_log.Log(PrintMsil ? LogLevel.Info : LogLevel.Debug,
$"Begin patching {_method.DeclaringType?.FullName}#{_method.Name}({string.Join(", ", _method.GetParameters().Select(x => x.ParameterType.Name))})");
var patch = ComposePatchedMethod();
_revertAddress = AssemblyMemory.GetMethodBodyStart(_method);
var newAddress = AssemblyMemory.GetMethodBodyStart(patch);
_revertData = AssemblyMemory.WriteJump(_revertAddress, newAddress);
_pinnedPatch = GCHandle.Alloc(patch);
_log.Log(PrintMsil ? LogLevel.Info : LogLevel.Debug,
$"Done patching {_method.DeclaringType?.FullName}#{_method.Name}({string.Join(", ", _method.GetParameters().Select(x => x.ParameterType.Name))})");
}
catch (Exception exception)
{
_log.Fatal(exception, $"Error patching {_method.DeclaringType?.FullName}#{_method}");
throw;
}
}
internal void Revert()
{
if (_pinnedPatch.HasValue)
{
_log.Debug($"Revert {_method.DeclaringType?.FullName}#{_method.Name}({string.Join(", ", _method.GetParameters().Select(x => x.ParameterType.Name))})");
AssemblyMemory.WriteMemory(_revertAddress, _revertData);
_revertData = null;
_pinnedPatch.Value.Free();
_pinnedPatch = null;
}
}
#region Create
private int _patchSalt = 0;
private DynamicMethod AllocatePatchMethod()
{
Debug.Assert(_method.DeclaringType != null);
var methodName = _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) })
.Concat(parameters.Select(x => x.ParameterType)).ToArray();
var patchMethod = new DynamicMethod(methodName, MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
returnType, parameterTypes, _method.DeclaringType, true);
if (!_method.IsStatic)
patchMethod.DefineParameter(0, ParameterAttributes.None, INSTANCE_PARAMETER);
for (var i = 0; i < parameters.Length; i++)
patchMethod.DefineParameter((patchMethod.IsStatic ? 0 : 1) + i, parameters[i].Attributes, parameters[i].Name);
return patchMethod;
}
public const string INSTANCE_PARAMETER = "__instance";
public const string RESULT_PARAMETER = "__result";
public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped";
#pragma warning disable 649
[ReflectedStaticMethod(Type = typeof(RuntimeHelpers), Name = "_CompileMethod", OverrideTypeNames = new[] { "System.IRuntimeMethodInfo" })]
private static Action<object> _compileDynamicMethod;
[ReflectedMethod(Name = "GetMethodInfo")]
private static Func<RuntimeMethodHandle, object> _getMethodInfo;
[ReflectedMethod(Name = "GetMethodDescriptor")]
private static Func<DynamicMethod, RuntimeMethodHandle> _getMethodHandle;
#pragma warning restore 649
public DynamicMethod ComposePatchedMethod()
{
DynamicMethod method = AllocatePatchMethod();
var generator = new LoggingIlGenerator(method.GetILGenerator(), PrintMsil ? LogLevel.Info : LogLevel.Trace);
List<MsilInstruction> il = EmitPatched((type, pinned) => new MsilLocal(generator.DeclareLocal(type, pinned))).ToList();
if (PrintMsil)
{
lock (_log)
{
MethodTranspiler.IntegrityAnalysis(LogLevel.Info, il);
}
}
MethodTranspiler.EmitMethod(il, generator);
try
{
// Force it to compile
RuntimeMethodHandle handle = _getMethodHandle.Invoke(method);
object runtimeMethodInfo = _getMethodInfo.Invoke(handle);
_compileDynamicMethod.Invoke(runtimeMethodInfo);
}
catch
{
lock (_log)
{
var ctx = new MethodContext(method);
ctx.Read();
MethodTranspiler.IntegrityAnalysis(LogLevel.Warn, ctx.Instructions);
}
throw;
}
return method;
}
#endregion
#region Emit
private IEnumerable<MsilInstruction> EmitPatched(Func<Type, bool, MsilLocal> declareLocal)
{
var methodBody = _method.GetMethodBody();
Debug.Assert(methodBody != null, "Method body is null");
foreach (var localVar in methodBody.LocalVariables)
{
Debug.Assert(localVar.LocalType != null);
declareLocal(localVar.LocalType, localVar.IsPinned);
}
var instructions = new List<MsilInstruction>();
var specialVariables = new Dictionary<string, MsilLocal>();
var labelAfterOriginalContent = new MsilLabel();
var labelSkipMethodContent = new MsilLabel();
Type returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
MsilLocal resultVariable = null;
if (returnType != typeof(void))
{
if (Prefixes.Concat(Suffixes).SelectMany(x => x.GetParameters()).Any(x => x.Name == RESULT_PARAMETER)
|| Prefixes.Any(x => x.ReturnType == typeof(bool)))
resultVariable = declareLocal(returnType, false);
}
if (resultVariable != null)
instructions.AddRange(resultVariable.SetToDefault());
MsilLocal prefixSkippedVariable = null;
if (Prefixes.Count > 0 && Suffixes.Any(x => x.GetParameters()
.Any(y => y.Name.Equals(PREFIX_SKIPPED_PARAMETER))))
{
prefixSkippedVariable = declareLocal(typeof(bool), false);
specialVariables.Add(PREFIX_SKIPPED_PARAMETER, prefixSkippedVariable);
}
if (resultVariable != null)
specialVariables.Add(RESULT_PARAMETER, resultVariable);
foreach (MethodInfo prefix in Prefixes)
{
instructions.AddRange(EmitMonkeyCall(prefix, specialVariables));
if (prefix.ReturnType == typeof(bool))
instructions.Add(new MsilInstruction(OpCodes.Brfalse).InlineTarget(labelSkipMethodContent));
else if (prefix.ReturnType != typeof(void))
throw new Exception(
$"Prefixes must return void or bool. {prefix.DeclaringType?.FullName}.{prefix.Name} returns {prefix.ReturnType}");
}
instructions.AddRange(MethodTranspiler.Transpile(_method, (x) => declareLocal(x, false), Transpilers, labelAfterOriginalContent));
instructions.Add(new MsilInstruction(OpCodes.Nop).LabelWith(labelAfterOriginalContent));
if (resultVariable != null)
instructions.Add(new MsilInstruction(OpCodes.Stloc).InlineValue(resultVariable));
var notSkip = new MsilLabel();
instructions.Add(new MsilInstruction(OpCodes.Br).InlineTarget(notSkip));
instructions.Add(new MsilInstruction(OpCodes.Nop).LabelWith(labelSkipMethodContent));
if (prefixSkippedVariable != null)
{
instructions.Add(new MsilInstruction(OpCodes.Ldc_I4_1));
instructions.Add(new MsilInstruction(OpCodes.Stloc).InlineValue(prefixSkippedVariable));
}
instructions.Add(new MsilInstruction(OpCodes.Nop).LabelWith(notSkip));
foreach (MethodInfo suffix in Suffixes)
{
instructions.AddRange(EmitMonkeyCall(suffix, specialVariables));
if (suffix.ReturnType != typeof(void))
throw new Exception($"Suffixes must return void. {suffix.DeclaringType?.FullName}.{suffix.Name} returns {suffix.ReturnType}");
}
if (resultVariable != null)
instructions.Add(new MsilInstruction(OpCodes.Ldloc).InlineValue(resultVariable));
instructions.Add(new MsilInstruction(OpCodes.Ret));
var result = MethodTranspiler.Transpile(_method, instructions, (x) => declareLocal(x, false), PostTranspilers, null).ToList();
if (result.Last().OpCode != OpCodes.Ret)
result.Add(new MsilInstruction(OpCodes.Ret));
return result;
}
private IEnumerable<MsilInstruction> EmitMonkeyCall(MethodInfo patch,
IReadOnlyDictionary<string, MsilLocal> specialVariables)
{
foreach (var param in patch.GetParameters())
{
switch (param.Name)
{
case INSTANCE_PARAMETER:
if (_method.IsStatic)
throw new Exception("Can't use an instance parameter for a static method");
yield return new MsilInstruction(OpCodes.Ldarg_0);
break;
case PREFIX_SKIPPED_PARAMETER:
if (param.ParameterType != typeof(bool))
throw new Exception($"Prefix skipped parameter {param.ParameterType} must be of type bool");
if (param.ParameterType.IsByRef || param.IsOut)
throw new Exception($"Prefix skipped parameter {param.ParameterType} can't be a reference type");
if (specialVariables.TryGetValue(PREFIX_SKIPPED_PARAMETER, out MsilLocal prefixSkip))
yield return new MsilInstruction(OpCodes.Ldloc).InlineValue(prefixSkip);
else
yield return new MsilInstruction(OpCodes.Ldc_I4_0);
break;
case RESULT_PARAMETER:
Type retType = param.ParameterType.IsByRef
? param.ParameterType.GetElementType()
: param.ParameterType;
if (retType == null || !retType.IsAssignableFrom(specialVariables[RESULT_PARAMETER].Type))
throw new Exception($"Return type {specialVariables[RESULT_PARAMETER].Type} can't be assigned to result parameter type {retType}");
yield return new MsilInstruction(param.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc)
.InlineValue(specialVariables[RESULT_PARAMETER]);
break;
default:
ParameterInfo declParam = _method.GetParameters().FirstOrDefault(x => x.Name == param.Name);
if (declParam == null)
throw new Exception($"Parameter name {param.Name} not found");
int paramIdx = (_method.IsStatic ? 0 : 1) + declParam.Position;
bool patchByRef = param.IsOut || param.ParameterType.IsByRef;
bool declByRef = declParam.IsOut || declParam.ParameterType.IsByRef;
if (patchByRef == declByRef)
yield return new MsilInstruction(OpCodes.Ldarg).InlineValue(new MsilArgument(paramIdx));
else if (patchByRef)
yield return new MsilInstruction(OpCodes.Ldarga).InlineValue(new MsilArgument(paramIdx));
else
{
yield return new MsilInstruction(OpCodes.Ldarg).InlineValue(new MsilArgument(paramIdx));
yield return EmitExtensions.EmitDereference(declParam.ParameterType);
}
break;
}
}
yield return new MsilInstruction(OpCodes.Call).InlineValue(patch);
}
#endregion
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.MSIL;
using Torch.Managers.PatchManager.Transpile;
namespace Torch.Managers.PatchManager
{
internal static class EmitExtensions
{
/// <summary>
/// Sets the given local to its default value in the given IL generator.
/// </summary>
/// <param name="local">Local to set to default</param>
/// <returns>Instructions</returns>
public static IEnumerable<MsilInstruction> SetToDefault(this MsilLocal local)
{
Debug.Assert(local.Type != null);
if (local.Type.IsEnum || local.Type.IsPrimitive)
{
if (local.Type == typeof(float))
yield return new MsilInstruction(OpCodes.Ldc_R4).InlineValue(0f);
else if (local.Type == typeof(double))
yield return new MsilInstruction(OpCodes.Ldc_R8).InlineValue(0d);
else if (local.Type == typeof(long) || local.Type == typeof(ulong))
yield return new MsilInstruction(OpCodes.Ldc_I8).InlineValue(0L);
else
yield return new MsilInstruction(OpCodes.Ldc_I4).InlineValue(0);
yield return new MsilInstruction(OpCodes.Stloc).InlineValue(local);
}
else if (local.Type.IsValueType) // struct
{
yield return new MsilInstruction(OpCodes.Ldloca).InlineValue(local);
yield return new MsilInstruction(OpCodes.Initobj).InlineValue(local.Type);
}
else // class
{
yield return new MsilInstruction(OpCodes.Ldnull);
yield return new MsilInstruction(OpCodes.Stloc).InlineValue(local);
}
}
/// <summary>
/// Emits a dereference for the given type.
/// </summary>
/// <param name="type">Type to dereference</param>
/// <returns>Derference instruction</returns>
public static MsilInstruction EmitDereference(Type type)
{
if (type.IsByRef)
type = type.GetElementType();
Debug.Assert(type != null);
if (type == typeof(float))
return new MsilInstruction(OpCodes.Ldind_R4);
if (type == typeof(double))
return new MsilInstruction(OpCodes.Ldind_R8);
if (type == typeof(byte))
return new MsilInstruction(OpCodes.Ldind_U1);
if (type == typeof(ushort) || type == typeof(char))
return new MsilInstruction(OpCodes.Ldind_U2);
if (type == typeof(uint))
return new MsilInstruction(OpCodes.Ldind_U4);
if (type == typeof(sbyte))
return new MsilInstruction(OpCodes.Ldind_I1);
if (type == typeof(short))
return new MsilInstruction(OpCodes.Ldind_I2);
if (type == typeof(int) || type.IsEnum)
return new MsilInstruction(OpCodes.Ldind_I4);
if (type == typeof(long) || type == typeof(ulong))
return new MsilInstruction(OpCodes.Ldind_I8);
return new MsilInstruction(OpCodes.Ldind_Ref);
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace Torch.Managers.PatchManager.MSIL
{
//https://stackoverflow.com/questions/4148297/resolving-the-tokens-found-in-the-il-from-a-dynamic-method/35711376#35711376
internal interface ITokenResolver
{
MemberInfo ResolveMember(int token);
Type ResolveType(int token);
FieldInfo ResolveField(int token);
MethodBase ResolveMethod(int token);
byte[] ResolveSignature(int token);
string ResolveString(int token);
}
internal sealed class NormalTokenResolver : ITokenResolver
{
private readonly Type[] _genericTypeArgs, _genericMethArgs;
private readonly Module _module;
internal NormalTokenResolver(MethodBase method)
{
_module = method.Module;
_genericTypeArgs = method.DeclaringType?.GenericTypeArguments ?? new Type[0];
_genericMethArgs = (method is MethodInfo ? method.GetGenericArguments() : new Type[0]);
}
public MemberInfo ResolveMember(int token)
{
return _module.ResolveMember(token, _genericTypeArgs, _genericMethArgs);
}
public Type ResolveType(int token)
{
return _module.ResolveType(token, _genericTypeArgs, _genericMethArgs);
}
public FieldInfo ResolveField(int token)
{
return _module.ResolveField(token, _genericTypeArgs, _genericMethArgs);
}
public MethodBase ResolveMethod(int token)
{
return _module.ResolveMethod(token, _genericTypeArgs, _genericMethArgs);
}
public byte[] ResolveSignature(int token)
{
return _module.ResolveSignature(token);
}
public string ResolveString(int token)
{
return _module.ResolveString(token);
}
}
internal sealed class NullTokenResolver : ITokenResolver
{
internal static readonly NullTokenResolver Instance = new NullTokenResolver();
internal NullTokenResolver()
{
}
public MemberInfo ResolveMember(int token)
{
return null;
}
public Type ResolveType(int token)
{
return null;
}
public FieldInfo ResolveField(int token)
{
return null;
}
public MethodBase ResolveMethod(int token)
{
return null;
}
public byte[] ResolveSignature(int token)
{
return null;
}
public string ResolveString(int token)
{
return null;
}
}
internal sealed class DynamicMethodTokenResolver : ITokenResolver
{
private readonly MethodInfo _getFieldInfo;
private readonly MethodInfo _getMethodBase;
private readonly GetTypeFromHandleUnsafe _getTypeFromHandleUnsafe;
private readonly ConstructorInfo _runtimeFieldHandleStubCtor;
private readonly ConstructorInfo _runtimeMethodHandleInternalCtor;
private readonly SignatureResolver _signatureResolver;
private readonly StringResolver _stringResolver;
private readonly TokenResolver _tokenResolver;
public DynamicMethodTokenResolver(DynamicMethod dynamicMethod)
{
object resolver = typeof(DynamicMethod)
.GetField("m_resolver", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(dynamicMethod);
if (resolver == null) throw new ArgumentException("The dynamic method's IL has not been finalized.");
_tokenResolver = (TokenResolver) resolver.GetType()
.GetMethod("ResolveToken", BindingFlags.Instance | BindingFlags.NonPublic)
.CreateDelegate(typeof(TokenResolver), resolver);
_stringResolver = (StringResolver) resolver.GetType()
.GetMethod("GetStringLiteral", BindingFlags.Instance | BindingFlags.NonPublic)
.CreateDelegate(typeof(StringResolver), resolver);
_signatureResolver = (SignatureResolver) resolver.GetType()
.GetMethod("ResolveSignature", BindingFlags.Instance | BindingFlags.NonPublic)
.CreateDelegate(typeof(SignatureResolver), resolver);
_getTypeFromHandleUnsafe = (GetTypeFromHandleUnsafe) typeof(Type)
.GetMethod("GetTypeFromHandleUnsafe", BindingFlags.Static | BindingFlags.NonPublic, null,
new[] {typeof(IntPtr)}, null).CreateDelegate(typeof(GetTypeFromHandleUnsafe), null);
Type runtimeType = typeof(RuntimeTypeHandle).Assembly.GetType("System.RuntimeType");
Type runtimeMethodHandleInternal =
typeof(RuntimeTypeHandle).Assembly.GetType("System.RuntimeMethodHandleInternal");
_getMethodBase = runtimeType.GetMethod("GetMethodBase", BindingFlags.Static | BindingFlags.NonPublic, null,
new[] {runtimeType, runtimeMethodHandleInternal}, null);
_runtimeMethodHandleInternalCtor =
runtimeMethodHandleInternal.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null,
new[] {typeof(IntPtr)}, null);
Type runtimeFieldInfoStub = typeof(RuntimeTypeHandle).Assembly.GetType("System.RuntimeFieldInfoStub");
_runtimeFieldHandleStubCtor =
runtimeFieldInfoStub.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null,
new[] {typeof(IntPtr), typeof(object)}, null);
_getFieldInfo = runtimeType.GetMethod("GetFieldInfo", BindingFlags.Static | BindingFlags.NonPublic, null,
new[] {runtimeType, typeof(RuntimeTypeHandle).Assembly.GetType("System.IRuntimeFieldInfo")}, null);
}
public Type ResolveType(int token)
{
IntPtr typeHandle, methodHandle, fieldHandle;
_tokenResolver.Invoke(token, out typeHandle, out methodHandle, out fieldHandle);
return _getTypeFromHandleUnsafe.Invoke(typeHandle);
}
public MethodBase ResolveMethod(int token)
{
IntPtr typeHandle, methodHandle, fieldHandle;
_tokenResolver.Invoke(token, out typeHandle, out methodHandle, out fieldHandle);
return (MethodBase) _getMethodBase.Invoke(null, new[]
{
typeHandle == IntPtr.Zero ? null : _getTypeFromHandleUnsafe.Invoke(typeHandle),
_runtimeMethodHandleInternalCtor.Invoke(new object[] {methodHandle})
});
}
public FieldInfo ResolveField(int token)
{
IntPtr typeHandle, methodHandle, fieldHandle;
_tokenResolver.Invoke(token, out typeHandle, out methodHandle, out fieldHandle);
return (FieldInfo) _getFieldInfo.Invoke(null, new[]
{
typeHandle == IntPtr.Zero ? null : _getTypeFromHandleUnsafe.Invoke(typeHandle),
_runtimeFieldHandleStubCtor.Invoke(new object[] {fieldHandle, null})
});
}
public MemberInfo ResolveMember(int token)
{
IntPtr typeHandle, methodHandle, fieldHandle;
_tokenResolver.Invoke(token, out typeHandle, out methodHandle, out fieldHandle);
if (methodHandle != IntPtr.Zero)
return (MethodBase) _getMethodBase.Invoke(null, new[]
{
typeHandle == IntPtr.Zero ? null : _getTypeFromHandleUnsafe.Invoke(typeHandle),
_runtimeMethodHandleInternalCtor.Invoke(new object[] {methodHandle})
});
if (fieldHandle != IntPtr.Zero)
return (FieldInfo) _getFieldInfo.Invoke(null, new[]
{
typeHandle == IntPtr.Zero ? null : _getTypeFromHandleUnsafe.Invoke(typeHandle),
_runtimeFieldHandleStubCtor.Invoke(new object[] {fieldHandle, null})
});
if (typeHandle != IntPtr.Zero)
return _getTypeFromHandleUnsafe.Invoke(typeHandle);
throw new NotImplementedException(
"DynamicMethods are not able to reference members by token other than types, methods and fields.");
}
public byte[] ResolveSignature(int token)
{
return _signatureResolver.Invoke(token, 0);
}
public string ResolveString(int token)
{
return _stringResolver.Invoke(token);
}
private delegate void TokenResolver(int token, out IntPtr typeHandle, out IntPtr methodHandle,
out IntPtr fieldHandle);
private delegate string StringResolver(int token);
private delegate byte[] SignatureResolver(int token, int fromMethod);
private delegate Type GetTypeFromHandleUnsafe(IntPtr handle);
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents metadata about a method's parameter
/// </summary>
public class MsilArgument
{
/// <summary>
/// The positon of this argument. Note, if the method is static, index 0 is the instance.
/// </summary>
public int Position { get; }
/// <summary>
/// The type of this parameter, or null if unknown.
/// </summary>
public Type Type { get; }
/// <summary>
/// The name of this parameter, or null if unknown.
/// </summary>
public string Name { get; }
/// <summary>
/// Creates an argument from the given parameter info.
/// </summary>
/// <param name="local">parameter info to use</param>
public MsilArgument(ParameterInfo local)
{
bool isStatic;
if (local.Member is FieldInfo fi)
isStatic = fi.IsStatic;
else if (local.Member is MethodBase mb)
isStatic = mb.IsStatic;
else
throw new ArgumentException("ParameterInfo.Member must be MethodBase or FieldInfo", nameof(local));
Position = (isStatic ? 0 : 1) + local.Position;
Type = local.ParameterType;
Name = local.Name;
}
/// <summary>
/// Creates an empty argument reference with the given position.
/// </summary>
/// <param name="position">The argument's position</param>
public MsilArgument(int position)
{
Position = position;
Type = null;
Name = null;
}
/// <inheritdoc/>
public override string ToString()
{
return $"arg{Position:X4}({Type?.Name ?? "unknown"})";
}
}
}

View File

@@ -0,0 +1,216 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Torch.Managers.PatchManager.Transpile;
using Torch.Utils;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents a single MSIL instruction, and its operand
/// </summary>
public class MsilInstruction
{
/// <summary>
/// Creates a new instruction with the given opcode.
/// </summary>
/// <param name="opcode">Opcode</param>
public MsilInstruction(OpCode opcode)
{
OpCode = opcode;
switch (opcode.OperandType)
{
case OperandType.InlineNone:
Operand = null;
break;
case OperandType.ShortInlineBrTarget:
case OperandType.InlineBrTarget:
Operand = new MsilOperandBrTarget(this);
break;
case OperandType.InlineField:
Operand = new MsilOperandInline.MsilOperandReflected<FieldInfo>(this);
break;
case OperandType.ShortInlineI:
case OperandType.InlineI:
Operand = new MsilOperandInline.MsilOperandInt32(this);
break;
case OperandType.InlineI8:
Operand = new MsilOperandInline.MsilOperandInt64(this);
break;
case OperandType.InlineMethod:
Operand = new MsilOperandInline.MsilOperandReflected<MethodBase>(this);
break;
case OperandType.InlineR:
Operand = new MsilOperandInline.MsilOperandDouble(this);
break;
case OperandType.InlineSig:
Operand = new MsilOperandInline.MsilOperandSignature(this);
break;
case OperandType.InlineString:
Operand = new MsilOperandInline.MsilOperandString(this);
break;
case OperandType.InlineSwitch:
Operand = new MsilOperandSwitch(this);
break;
case OperandType.InlineTok:
Operand = new MsilOperandInline.MsilOperandReflected<MemberInfo>(this);
break;
case OperandType.InlineType:
Operand = new MsilOperandInline.MsilOperandReflected<Type>(this);
break;
case OperandType.ShortInlineVar:
case OperandType.InlineVar:
if (OpCode.IsLocalStore() || OpCode.IsLocalLoad() || OpCode.IsLocalLoadByRef())
Operand = new MsilOperandInline.MsilOperandLocal(this);
else
Operand = new MsilOperandInline.MsilOperandArgument(this);
break;
case OperandType.ShortInlineR:
Operand = new MsilOperandInline.MsilOperandSingle(this);
break;
#pragma warning disable 618
case OperandType.InlinePhi:
#pragma warning restore 618
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Opcode of this instruction
/// </summary>
public OpCode OpCode { get; }
/// <summary>
/// Raw memory offset of this instruction; optional.
/// </summary>
public int Offset { get; internal set; }
/// <summary>
/// The operand for this instruction, or null.
/// </summary>
public MsilOperand Operand { get; }
/// <summary>
/// Labels pointing to this instruction.
/// </summary>
public HashSet<MsilLabel> Labels { get; } = new HashSet<MsilLabel>();
/// <summary>
/// The try catch operation that is performed here.
/// </summary>
public MsilTryCatchOperation TryCatchOperation { get; set; } = null;
private static readonly ConcurrentDictionary<Type, PropertyInfo> _setterInfoForInlines = new ConcurrentDictionary<Type, PropertyInfo>();
/// <summary>
/// Sets the inline value for this instruction.
/// </summary>
/// <typeparam name="T">The type of the inline constraint</typeparam>
/// <param name="o">Value</param>
/// <returns>This instruction</returns>
public MsilInstruction InlineValue<T>(T o)
{
Type type = typeof(T);
while (type != null)
{
if (!_setterInfoForInlines.TryGetValue(type, out PropertyInfo target))
{
Type genType = typeof(MsilOperandInline<>).MakeGenericType(type);
target = genType.GetProperty(nameof(MsilOperandInline<int>.Value));
_setterInfoForInlines[type] = target;
}
Debug.Assert(target?.DeclaringType != null);
if (target.DeclaringType.IsInstanceOfType(Operand))
{
target.SetValue(Operand, o);
return this;
}
type = type.BaseType;
}
((MsilOperandInline<T>)Operand).Value = o;
return this;
}
/// <summary>
/// Makes a copy of the instruction with a new opcode.
/// </summary>
/// <param name="newOpcode">The new opcode</param>
/// <returns>The copy</returns>
public MsilInstruction CopyWith(OpCode newOpcode)
{
var result = new MsilInstruction(newOpcode);
Operand?.CopyTo(result.Operand);
foreach (MsilLabel x in Labels)
result.Labels.Add(x);
result.TryCatchOperation = TryCatchOperation;
return result;
}
/// <summary>
/// Adds the given label to this instruction
/// </summary>
/// <param name="label">Label to add</param>
/// <returns>this instruction</returns>
public MsilInstruction LabelWith(MsilLabel label)
{
Labels.Add(label);
return this;
}
/// <summary>
/// Sets the inline branch target for this instruction.
/// </summary>
/// <param name="label">Target to jump to</param>
/// <returns>This instruction</returns>
public MsilInstruction InlineTarget(MsilLabel label)
{
((MsilOperandBrTarget)Operand).Target = label;
return this;
}
/// <inheritdoc />
public override string ToString()
{
var sb = new StringBuilder();
foreach (MsilLabel label in Labels)
sb.Append(label).Append(": ");
sb.Append(OpCode.Name).Append("\t").Append(Operand);
return sb.ToString();
}
#pragma warning disable 169
[ReflectedMethod(Name = "StackChange")]
private static Func<OpCode, int> _stackChange;
#pragma warning restore 169
/// <summary>
/// Estimates the stack delta for this instruction.
/// </summary>
/// <returns>Stack delta</returns>
public int StackChange()
{
int num = _stackChange.Invoke(OpCode);
if ((OpCode == OpCodes.Call || OpCode == OpCodes.Callvirt || OpCode == OpCodes.Newobj) &&
Operand is MsilOperandInline<MethodBase> inline)
{
MethodBase op = inline.Value;
if (op == null)
return num;
if (op is MethodInfo mi && mi.ReturnType != typeof(void))
num++;
num -= op.GetParameters().Length;
if (!op.IsStatic && OpCode != OpCodes.Newobj)
num--;
}
return num;
}
}
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Various methods to make composing MSIL easier
/// </summary>
public static class MsilInstructionExtensions
{
#region Local Utils
/// <summary>
/// Is this instruction a local load-by-value instruction.
/// </summary>
public static bool IsLocalLoad(this MsilInstruction me)
{
return me.OpCode.IsLocalLoad();
}
/// <summary>
/// Is this instruction a local load-by-reference instruction.
/// </summary>
public static bool IsLocalLoadByRef(this MsilInstruction me)
{
return me.OpCode.IsLocalLoadByRef();
}
/// <summary>
/// Is this instruction a local store instruction.
/// </summary>
public static bool IsLocalStore(this MsilInstruction me)
{
return me.OpCode.IsLocalStore();
}
/// <summary>
/// Is this instruction a local load-by-value instruction.
/// </summary>
public static bool IsLocalLoad(this OpCode opcode)
{
return opcode == OpCodes.Ldloc || opcode == OpCodes.Ldloc_S || opcode == OpCodes.Ldloc_0 ||
opcode == OpCodes.Ldloc_1 || opcode == OpCodes.Ldloc_2 || opcode == OpCodes.Ldloc_3;
}
/// <summary>
/// Is this instruction a local load-by-reference instruction.
/// </summary>
public static bool IsLocalLoadByRef(this OpCode opcode)
{
return opcode == OpCodes.Ldloca || opcode == OpCodes.Ldloca_S;
}
/// <summary>
/// Is this instruction a local store instruction.
/// </summary>
public static bool IsLocalStore(this OpCode opcode)
{
return opcode == OpCodes.Stloc || opcode == OpCodes.Stloc_S || opcode == OpCodes.Stloc_0 ||
opcode == OpCodes.Stloc_1 || opcode == OpCodes.Stloc_2 || opcode == OpCodes.Stloc_3;
}
/// <summary>
/// For a local referencing opcode, get the local it is referencing.
/// </summary>
public static MsilLocal GetReferencedLocal(this MsilInstruction me)
{
if (me.Operand is MsilOperandInline.MsilOperandLocal mol)
return mol.Value;
if (me.OpCode == OpCodes.Stloc_0 || me.OpCode == OpCodes.Ldloc_0)
return new MsilLocal(0);
if (me.OpCode == OpCodes.Stloc_1 || me.OpCode == OpCodes.Ldloc_1)
return new MsilLocal(1);
if (me.OpCode == OpCodes.Stloc_2 || me.OpCode == OpCodes.Ldloc_2)
return new MsilLocal(2);
if (me.OpCode == OpCodes.Stloc_3 || me.OpCode == OpCodes.Ldloc_3)
return new MsilLocal(3);
throw new ArgumentException($"Can't get referenced local in instruction {me}");
}
/// <summary>
/// Gets an instruction representing a load-by-value from the given local.
/// </summary>
/// <param name="local">Local to load</param>
/// <returns>Loading instruction</returns>
public static MsilInstruction AsValueLoad(this MsilLocal local)
{
switch (local.Index)
{
case 0:
return new MsilInstruction(OpCodes.Ldloc_0);
case 1:
return new MsilInstruction(OpCodes.Ldloc_1);
case 2:
return new MsilInstruction(OpCodes.Ldloc_2);
case 3:
return new MsilInstruction(OpCodes.Ldloc_3);
default:
return new MsilInstruction(local.Index < 0xFF ? OpCodes.Ldloc_S : OpCodes.Ldloc).InlineValue(local);
}
}
/// <summary>
/// Gets an instruction representing a store-by-value to the given local.
/// </summary>
/// <param name="local">Local to write to</param>
/// <returns>Loading instruction</returns>
public static MsilInstruction AsValueStore(this MsilLocal local)
{
switch (local.Index)
{
case 0:
return new MsilInstruction(OpCodes.Stloc_0);
case 1:
return new MsilInstruction(OpCodes.Stloc_1);
case 2:
return new MsilInstruction(OpCodes.Stloc_2);
case 3:
return new MsilInstruction(OpCodes.Stloc_3);
default:
return new MsilInstruction(local.Index < 0xFF ? OpCodes.Stloc_S : OpCodes.Stloc).InlineValue(local);
}
}
/// <summary>
/// Gets an instruction representing a load-by-reference from the given local.
/// </summary>
/// <param name="local">Local to load</param>
/// <returns>Loading instruction</returns>
public static MsilInstruction AsReferenceLoad(this MsilLocal local)
{
return new MsilInstruction(local.Index < 0xFF ? OpCodes.Ldloca_S : OpCodes.Ldloca).InlineValue(local);
}
#endregion
#region Argument Utils
/// <summary>
/// Is this instruction an argument load-by-value instruction.
/// </summary>
public static bool IsArgumentLoad(this MsilInstruction me)
{
return me.OpCode == OpCodes.Ldarg || me.OpCode == OpCodes.Ldarg_S || me.OpCode == OpCodes.Ldarg_0 ||
me.OpCode == OpCodes.Ldarg_1 || me.OpCode == OpCodes.Ldarg_2 || me.OpCode == OpCodes.Ldarg_3;
}
/// <summary>
/// Is this instruction an argument load-by-reference instruction.
/// </summary>
public static bool IsArgumentLoadByRef(this MsilInstruction me)
{
return me.OpCode == OpCodes.Ldarga || me.OpCode == OpCodes.Ldarga_S;
}
/// <summary>
/// Is this instruction an argument store instruction.
/// </summary>
public static bool IsArgumentStore(this MsilInstruction me)
{
return me.OpCode == OpCodes.Starg || me.OpCode == OpCodes.Starg_S;
}
/// <summary>
/// For an argument referencing opcode, get the index of the local it is referencing.
/// </summary>
public static MsilArgument GetReferencedArgument(this MsilInstruction me)
{
if (me.Operand is MsilOperandInline.MsilOperandArgument mol)
return mol.Value;
if (me.OpCode == OpCodes.Ldarg_0)
return new MsilArgument(0);
if (me.OpCode == OpCodes.Ldarg_1)
return new MsilArgument(1);
if (me.OpCode == OpCodes.Ldarg_2)
return new MsilArgument(2);
if (me.OpCode == OpCodes.Ldarg_3)
return new MsilArgument(3);
throw new ArgumentException($"Can't get referenced argument in instruction {me}");
}
/// <summary>
/// Gets an instruction representing a load-by-value from the given argument.
/// </summary>
/// <param name="argument">argument to load</param>
/// <returns>Load instruction</returns>
public static MsilInstruction AsValueLoad(this MsilArgument argument)
{
switch (argument.Position)
{
case 0:
return new MsilInstruction(OpCodes.Ldarg_0);
case 1:
return new MsilInstruction(OpCodes.Ldarg_1);
case 2:
return new MsilInstruction(OpCodes.Ldarg_2);
case 3:
return new MsilInstruction(OpCodes.Ldarg_3);
default:
return new MsilInstruction(argument.Position < 0xFF ? OpCodes.Ldarg_S : OpCodes.Ldarg).InlineValue(argument);
}
}
/// <summary>
/// Gets an instruction representing a store-by-value to the given argument.
/// </summary>
/// <param name="argument">argument to write to</param>
/// <returns>Store instruction</returns>
public static MsilInstruction AsValueStore(this MsilArgument argument)
{
return new MsilInstruction(argument.Position < 0xFF ? OpCodes.Starg_S : OpCodes.Starg).InlineValue(argument);
}
/// <summary>
/// Gets an instruction representing a load-by-reference from the given argument.
/// </summary>
/// <param name="argument">argument to load</param>
/// <returns>Reference load instruction</returns>
public static MsilInstruction AsReferenceLoad(this MsilArgument argument)
{
return new MsilInstruction(argument.Position < 0xFF ? OpCodes.Ldarga_S : OpCodes.Ldarga).InlineValue(argument);
}
#endregion
#region Constant Utils
/// <summary>
/// Determines if this instruction is a constant int load instruction.
/// </summary>
/// <param name="m">Instruction</param>
/// <returns>True if this instruction pushes a constant int onto the stack</returns>
public static bool IsConstIntLoad(this MsilInstruction m)
{
if (m.OpCode == OpCodes.Ldc_I4_0)
return true;
if (m.OpCode == OpCodes.Ldc_I4_1)
return true;
if (m.OpCode == OpCodes.Ldc_I4_2)
return true;
if (m.OpCode == OpCodes.Ldc_I4_3)
return true;
if (m.OpCode == OpCodes.Ldc_I4_4)
return true;
if (m.OpCode == OpCodes.Ldc_I4_5)
return true;
if (m.OpCode == OpCodes.Ldc_I4_6)
return true;
if (m.OpCode == OpCodes.Ldc_I4_7)
return true;
if (m.OpCode == OpCodes.Ldc_I4_8)
return true;
if (m.OpCode == OpCodes.Ldc_I4_M1)
return true;
if (m.OpCode == OpCodes.Ldc_I4)
return true;
return m.OpCode == OpCodes.Ldc_I4_S;
}
/// <summary>
/// Gets the constant int this instruction pushes onto the stack.
/// </summary>
/// <param name="m">Instruction</param>
/// <returns>The constant int</returns>
public static int GetConstInt(this MsilInstruction m)
{
if (m.OpCode == OpCodes.Ldc_I4_0)
return 0;
if (m.OpCode == OpCodes.Ldc_I4_1)
return 1;
if (m.OpCode == OpCodes.Ldc_I4_2)
return 2;
if (m.OpCode == OpCodes.Ldc_I4_3)
return 3;
if (m.OpCode == OpCodes.Ldc_I4_4)
return 4;
if (m.OpCode == OpCodes.Ldc_I4_5)
return 5;
if (m.OpCode == OpCodes.Ldc_I4_6)
return 6;
if (m.OpCode == OpCodes.Ldc_I4_7)
return 7;
if (m.OpCode == OpCodes.Ldc_I4_8)
return 8;
if (m.OpCode == OpCodes.Ldc_I4_M1)
return -1;
if (m.OpCode == OpCodes.Ldc_I4)
return ((MsilOperandInline<int>) m.Operand).Value;
if (m.OpCode == OpCodes.Ldc_I4_S)
return ((MsilOperandInline<byte>)m.Operand).Value;
throw new ArgumentException($"Can't get constant int from instruction {m}");
}
#endregion
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.Transpile;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents an abstract label, identified by its reference.
/// </summary>
public class MsilLabel
{
private readonly List<KeyValuePair<WeakReference<LoggingIlGenerator>, Label>> _labelInstances =
new List<KeyValuePair<WeakReference<LoggingIlGenerator>, Label>>();
private readonly Label? _overrideLabel;
/// <summary>
/// Creates an empty label the allocates a new <see cref="Label" /> when requested.
/// </summary>
public MsilLabel()
{
_overrideLabel = null;
}
/// <summary>
/// Creates a label the always supplies the given <see cref="Label" />
/// </summary>
public MsilLabel(Label overrideLabel)
{
_overrideLabel = overrideLabel;
}
/// <summary>
/// Creates a label that supplies the given <see cref="Label" /> when a label for the given generator is requested,
/// otherwise it creates a new label.
/// </summary>
/// <param name="generator">Generator to register the label on</param>
/// <param name="label">Label to register</param>
public MsilLabel(LoggingIlGenerator generator, Label label)
{
_labelInstances.Add(
new KeyValuePair<WeakReference<LoggingIlGenerator>, Label>(
new WeakReference<LoggingIlGenerator>(generator), label));
}
internal Label LabelFor(LoggingIlGenerator gen)
{
if (_overrideLabel.HasValue)
return _overrideLabel.Value;
foreach (KeyValuePair<WeakReference<LoggingIlGenerator>, Label> kv in _labelInstances)
if (kv.Key.TryGetTarget(out LoggingIlGenerator gen2) && gen2 == gen)
return kv.Value;
Label label = gen.DefineLabel();
_labelInstances.Add(
new KeyValuePair<WeakReference<LoggingIlGenerator>, Label>(new WeakReference<LoggingIlGenerator>(gen),
label));
return label;
}
/// <inheritdoc />
public override string ToString()
{
return $"L{GetHashCode() & 0xFFFF:X4}";
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents metadata about a method's local
/// </summary>
public class MsilLocal
{
/// <summary>
/// The index of this local.
/// </summary>
public int Index { get; }
/// <summary>
/// The type of this local, or null if unknown.
/// </summary>
public Type Type { get; }
/// <summary>
/// The name of this local, or null if unknown.
/// </summary>
public string Name { get; }
internal MsilLocal(LocalBuilder local)
{
Index = local.LocalIndex;
Type = local.LocalType;
Name = null;
}
internal MsilLocal(LocalVariableInfo local)
{
Index = local.LocalIndex;
Type = local.LocalType;
Name = null;
}
/// <summary>
/// Creates an empty local reference with the given index.
/// </summary>
/// <param name="index">The local's index</param>
public MsilLocal(int index)
{
Index = index;
Type = null;
Name = null;
}
/// <inheritdoc/>
public override string ToString()
{
return $"lcl{Index:X4}({Type?.Name ?? "unknown"})";
}
}
}

View File

@@ -0,0 +1,27 @@
using System.IO;
using Torch.Managers.PatchManager.Transpile;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents an operand for a MSIL instruction
/// </summary>
public abstract class MsilOperand
{
protected MsilOperand(MsilInstruction instruction)
{
Instruction = instruction;
}
/// <summary>
/// Instruction this operand is associated with
/// </summary>
public MsilInstruction Instruction { get; }
internal abstract void CopyTo(MsilOperand operand);
internal abstract void Read(MethodContext context, BinaryReader reader);
internal abstract void Emit(LoggingIlGenerator generator);
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.Transpile;
namespace Torch.Managers.PatchManager.MSIL
{
/// <summary>
/// Represents a branch target operand.
/// </summary>
public class MsilOperandBrTarget : MsilOperand
{
internal MsilOperandBrTarget(MsilInstruction instruction) : base(instruction)
{
}
/// <summary>
/// Branch target
/// </summary>
public MsilLabel Target { get; set; }
internal override void Read(MethodContext context, BinaryReader reader)
{
long offset;
// ReSharper disable once SwitchStatementMissingSomeCases
switch (Instruction.OpCode.OperandType)
{
case OperandType.ShortInlineBrTarget:
offset = reader.ReadSByte();
break;
case OperandType.InlineBrTarget:
offset = reader.ReadInt32();
break;
default:
throw new InvalidBranchException(
$"OpCode {Instruction.OpCode}, operand type {Instruction.OpCode.OperandType} doesn't match {GetType().Name}");
}
Target = context.LabelAt((int)(reader.BaseStream.Position + offset));
}
internal override void Emit(LoggingIlGenerator generator)
{
// ReSharper disable once SwitchStatementMissingSomeCases
switch (Instruction.OpCode.OperandType)
{
case OperandType.ShortInlineBrTarget:
case OperandType.InlineBrTarget:
generator.Emit(Instruction.OpCode, Target.LabelFor(generator));
break;
default:
throw new InvalidBranchException(
$"OpCode {Instruction.OpCode}, operand type {Instruction.OpCode.OperandType} doesn't match {GetType().Name}");
}
}
internal override void CopyTo(MsilOperand operand)
{
var lt = operand as MsilOperandBrTarget;
if (lt == null)
throw new ArgumentException($"Target {operand?.GetType().Name} must be of same type {GetType().Name}", nameof(operand));
lt.Target = Target;
}
/// <inheritdoc />
public override string ToString()
{
return Target?.ToString() ?? "null";
}
}
}

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