Merge branch 'staging'
This commit is contained in:
63
Jenkinsfile
vendored
63
Jenkinsfile
vendored
@@ -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 {
|
||||
stage('Checkout') {
|
||||
checkout scm
|
||||
@@ -16,12 +34,29 @@ node {
|
||||
|
||||
stage('Build') {
|
||||
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') {
|
||||
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([
|
||||
$class: 'XUnitBuilder',
|
||||
thresholdMode: 1,
|
||||
@@ -37,30 +72,14 @@ node {
|
||||
])
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
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
|
||||
}
|
||||
|
||||
if (env.BRANCH_NAME == "master") {
|
||||
gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim()
|
||||
gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim()
|
||||
if (gitVersion == gitSimpleVersion) {
|
||||
stage('Release') {
|
||||
stage('${buildMode}') {
|
||||
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\")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
NLog.config
14
NLog.config
@@ -1,15 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<variable name="logStamp" value="${time} ${pad:padding=-8:inner=[${level:uppercase=true}]}" />
|
||||
<variable name="logContent" value="${message:withException=true}"/>
|
||||
|
||||
<targets>
|
||||
<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="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>
|
||||
|
||||
<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="Chat" minlevel="Info" writeTo="chat" />
|
||||
<!--<logger name="Torch.Managers.PatchManager.*" minlevel="Trace" writeTo="patch"/>-->
|
||||
</rules>
|
||||
</nlog>
|
28
Torch.API/Event/EventHandlerAttribute.cs
Normal file
28
Torch.API/Event/EventHandlerAttribute.cs
Normal 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
11
Torch.API/Event/IEvent.cs
Normal 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; }
|
||||
}
|
||||
}
|
9
Torch.API/Event/IEventHandler.cs
Normal file
9
Torch.API/Event/IEventHandler.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
27
Torch.API/Event/IEventManager.cs
Normal file
27
Torch.API/Event/IEventManager.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Torch.API.Managers;
|
||||
@@ -44,10 +45,6 @@ namespace Torch.API
|
||||
/// </summary>
|
||||
ITorchConfig Config { get; }
|
||||
|
||||
/// <inheritdoc cref="IMultiplayerManager"/>
|
||||
[Obsolete]
|
||||
IMultiplayerManager Multiplayer { get; }
|
||||
|
||||
/// <inheritdoc cref="IPluginManager"/>
|
||||
[Obsolete]
|
||||
IPluginManager Plugins { get; }
|
||||
@@ -55,6 +52,12 @@ namespace Torch.API
|
||||
/// <inheritdoc cref="IDependencyManager"/>
|
||||
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>
|
||||
/// The binary version of the current instance.
|
||||
/// </summary>
|
||||
@@ -63,18 +66,18 @@ namespace Torch.API
|
||||
/// <summary>
|
||||
/// Invoke an action on the game thread.
|
||||
/// </summary>
|
||||
void Invoke(Action action);
|
||||
void Invoke(Action action, [CallerMemberName] string caller = "");
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
void InvokeBlocking(Action action);
|
||||
void InvokeBlocking(Action action, [CallerMemberName] string caller = "");
|
||||
|
||||
/// <summary>
|
||||
/// Invoke an action on the game thread asynchronously.
|
||||
/// </summary>
|
||||
Task InvokeAsync(Action action);
|
||||
Task InvokeAsync(Action action, [CallerMemberName] string caller = "");
|
||||
|
||||
/// <summary>
|
||||
/// Start the Torch instance.
|
||||
@@ -101,6 +104,16 @@ namespace Torch.API
|
||||
/// Initialize the Torch instance.
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -108,6 +121,11 @@ namespace Torch.API
|
||||
/// </summary>
|
||||
public interface ITorchServer : ITorchBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The current <see cref="ServerState"/>
|
||||
/// </summary>
|
||||
ServerState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Path of the dedicated instance folder.
|
||||
/// </summary>
|
||||
|
128
Torch.API/Managers/IChatManagerClient.cs
Normal file
128
Torch.API/Managers/IChatManagerClient.cs
Normal 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" );
|
||||
}
|
||||
}
|
45
Torch.API/Managers/IChatManagerServer.cs
Normal file
45
Torch.API/Managers/IChatManagerServer.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
41
Torch.API/Managers/IMultiplayerManagerBase.cs
Normal file
41
Torch.API/Managers/IMultiplayerManagerBase.cs
Normal 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);
|
||||
}
|
||||
}
|
12
Torch.API/Managers/IMultiplayerManagerClient.cs
Normal file
12
Torch.API/Managers/IMultiplayerManagerClient.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
36
Torch.API/Managers/IMultiplayerManagerServer.cs
Normal file
36
Torch.API/Managers/IMultiplayerManagerServer.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -18,6 +18,12 @@ namespace Torch.API.Managers
|
||||
/// Register a network handler.
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -33,6 +39,7 @@ namespace Torch.API.Managers
|
||||
/// <summary>
|
||||
/// Processes a network message.
|
||||
/// </summary>
|
||||
/// <returns>true if the message should be discarded</returns>
|
||||
bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
|
||||
}
|
||||
}
|
||||
|
@@ -14,12 +14,12 @@ namespace Torch.API.Managers
|
||||
/// <summary>
|
||||
/// Fired when plugins are loaded.
|
||||
/// </summary>
|
||||
event Action<IList<ITorchPlugin>> PluginsLoaded;
|
||||
event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of loaded plugins.
|
||||
/// </summary>
|
||||
IList<ITorchPlugin> Plugins { get; }
|
||||
IReadOnlyDictionary<Guid, ITorchPlugin> Plugins { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates all loaded plugins.
|
||||
|
@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
|
||||
/// <summary>
|
||||
/// The version of the plugin.
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the plugin.
|
||||
|
@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
|
||||
/// <summary>
|
||||
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
|
||||
/// </summary>
|
||||
[Obsolete("All plugin meta-information is now defined in the manifest.xml.")]
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginAttribute : Attribute
|
||||
{
|
||||
|
@@ -25,5 +25,15 @@ namespace Torch.API.Session
|
||||
|
||||
/// <inheritdoc cref="IDependencyManager"/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,11 @@ namespace Torch.API.Session
|
||||
/// </summary>
|
||||
ITorchSession CurrentSession { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when any <see cref="ITorchSession"/> <see cref="ITorchSession.State"/> changes.
|
||||
/// </summary>
|
||||
event TorchSessionStateChangedDel SessionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given factory as a supplier for session based managers
|
||||
/// </summary>
|
||||
|
38
Torch.API/Session/TorchSessionState.cs
Normal file
38
Torch.API/Session/TorchSessionState.cs
Normal 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);
|
||||
}
|
@@ -160,15 +160,22 @@
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="ConnectionState.cs" />
|
||||
<Compile Include="IChatMessage.cs" />
|
||||
<Compile Include="ITorchConfig.cs" />
|
||||
<Compile Include="Managers\DependencyManagerExtensions.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\IDependencyProvider.cs" />
|
||||
<Compile Include="Event\IEventManager.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="Managers\IMultiplayerManagerServer.cs" />
|
||||
<Compile Include="Managers\INetworkManager.cs" />
|
||||
<Compile Include="Managers\IPluginManager.cs" />
|
||||
<Compile Include="Plugins\ITorchPlugin.cs" />
|
||||
@@ -182,6 +189,8 @@
|
||||
<Compile Include="ModAPI\TorchAPI.cs" />
|
||||
<Compile Include="Session\ITorchSession.cs" />
|
||||
<Compile Include="Session\ITorchSessionManager.cs" />
|
||||
<Compile Include="Session\TorchSessionState.cs" />
|
||||
<Compile Include="TorchGameState.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
47
Torch.API/TorchGameState.cs
Normal file
47
Torch.API/TorchGameState.cs
Normal 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);
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Torch.Client;
|
||||
using Torch.Tests;
|
||||
using Torch.Utils;
|
||||
@@ -29,6 +30,10 @@ namespace Torch.Client.Tests
|
||||
|
||||
public static IEnumerable<object[]> Invokers => Manager().Invokers;
|
||||
|
||||
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
|
||||
|
||||
public static IEnumerable<object[]> Events => Manager().Events;
|
||||
|
||||
#region Binding
|
||||
[Theory]
|
||||
[MemberData(nameof(Getters))]
|
||||
@@ -62,6 +67,28 @@ namespace Torch.Client.Tests
|
||||
if (field.Field.IsStatic)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
32
Torch.Client/Manager/MultiplayerManagerClient.cs
Normal file
32
Torch.Client/Manager/MultiplayerManagerClient.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
44
Torch.Client/Manager/MultiplayerManagerLobby.cs
Normal file
44
Torch.Client/Manager/MultiplayerManagerLobby.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -121,6 +121,8 @@
|
||||
<Compile Include="..\Versioning\AssemblyVersion.cs">
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Manager\MultiplayerManagerClient.cs" />
|
||||
<Compile Include="Manager\MultiplayerManagerLobby.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TorchClient.cs" />
|
||||
<Compile Include="TorchClientConfig.cs" />
|
||||
@@ -167,6 +169,7 @@
|
||||
<ItemGroup>
|
||||
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
|
||||
<PropertyGroup>
|
||||
|
@@ -4,12 +4,17 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using Sandbox;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Engine.Networking;
|
||||
using Sandbox.Engine.Platform;
|
||||
using Sandbox.Game;
|
||||
using SpaceEngineers.Game;
|
||||
using VRage.Steam;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Client.Manager;
|
||||
using Torch.Session;
|
||||
using VRage;
|
||||
using VRage.FileSystem;
|
||||
using VRage.GameServices;
|
||||
@@ -27,6 +32,13 @@ namespace Torch.Client
|
||||
public TorchClient()
|
||||
{
|
||||
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()
|
||||
|
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Sandbox.Game.Gui;
|
||||
using Sandbox.Graphics;
|
||||
using Sandbox.Graphics.GUI;
|
||||
using Sandbox.Gui;
|
||||
@@ -16,6 +17,15 @@ namespace Torch.Client
|
||||
{
|
||||
public class TorchMainMenuScreen : MyGuiScreenMainMenu
|
||||
{
|
||||
public TorchMainMenuScreen()
|
||||
: this(false)
|
||||
{
|
||||
}
|
||||
|
||||
public TorchMainMenuScreen(bool pauseGame)
|
||||
: base(pauseGame)
|
||||
{
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override void RecreateControls(bool constructor)
|
||||
{
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Torch.Tests;
|
||||
using Torch.Utils;
|
||||
using Xunit;
|
||||
@@ -28,6 +29,10 @@ namespace Torch.Server.Tests
|
||||
|
||||
public static IEnumerable<object[]> Invokers => Manager().Invokers;
|
||||
|
||||
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
|
||||
|
||||
public static IEnumerable<object[]> Events => Manager().Events;
|
||||
|
||||
#region Binding
|
||||
[Theory]
|
||||
[MemberData(nameof(Getters))]
|
||||
@@ -61,6 +66,17 @@ namespace Torch.Server.Tests
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -27,7 +28,6 @@ login anonymous
|
||||
app_update 298740
|
||||
quit";
|
||||
|
||||
private TorchAssemblyResolver _resolver;
|
||||
private TorchConfig _config;
|
||||
private TorchServer _server;
|
||||
private string _basePath;
|
||||
@@ -45,12 +45,13 @@ quit";
|
||||
if (_init)
|
||||
return false;
|
||||
|
||||
#if !DEBUG
|
||||
AppDomain.CurrentDomain.UnhandledException += HandleException;
|
||||
#endif
|
||||
|
||||
if (!args.Contains("-noupdate"))
|
||||
RunSteamCmd();
|
||||
|
||||
_resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64"));
|
||||
_config = InitConfig();
|
||||
if (!_config.Parse(args))
|
||||
return false;
|
||||
@@ -62,11 +63,11 @@ quit";
|
||||
var pid = int.Parse(_config.WaitForPID);
|
||||
var waitProc = Process.GetProcessById(pid);
|
||||
Log.Info("Continuing in 5 seconds.");
|
||||
Thread.Sleep(5000);
|
||||
if (!waitProc.HasExited)
|
||||
Log.Warn($"Waiting for process {pid} to close");
|
||||
while (!waitProc.HasExited)
|
||||
{
|
||||
Log.Warn($"Killing old process {pid}.");
|
||||
waitProc.Kill();
|
||||
Console.Write(".");
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,8 +95,6 @@ quit";
|
||||
}
|
||||
else
|
||||
_server.Start();
|
||||
|
||||
_resolver?.Dispose();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var ex = (Exception)e.ExceptionObject;
|
||||
Log.Fatal(ex);
|
||||
LogException(ex);
|
||||
Console.WriteLine("Exiting in 5 seconds.");
|
||||
Thread.Sleep(5000);
|
||||
LogManager.Flush();
|
||||
if (_config.RestartOnCrash)
|
||||
{
|
||||
var exe = typeof(Program).Assembly.Location;
|
||||
_config.WaitForPID = Process.GetCurrentProcess().Id.ToString();
|
||||
Process.Start(exe, _config.ToString());
|
||||
}
|
||||
//1627 = Function failed during execution.
|
||||
Environment.Exit(1627);
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
265
Torch.Server/Managers/EntityControlManager.cs
Normal file
265
Torch.Server/Managers/EntityControlManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
Torch.Server/Managers/MultiplayerManagerDedicated.cs
Normal file
173
Torch.Server/Managers/MultiplayerManagerDedicated.cs
Normal 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
|
||||
}
|
||||
}
|
@@ -44,13 +44,16 @@ namespace Torch.Server
|
||||
var binDir = Path.Combine(workingDir, "DedicatedServer64");
|
||||
Directory.SetCurrentDirectory(workingDir);
|
||||
|
||||
if (!TorchLauncher.IsTorchWrapped())
|
||||
{
|
||||
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName,args, binDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Environment.UserInteractive)
|
||||
{
|
||||
using (var service = new TorchService())
|
||||
using (new TorchAssemblyResolver(binDir))
|
||||
{
|
||||
ServiceBase.Run(service);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -182,6 +182,10 @@
|
||||
<HintPath>..\GameBinaries\VRage.Scripting.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="VRage.Steam">
|
||||
<HintPath>..\GameBinaries\VRage.Steam.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
@@ -191,6 +195,8 @@
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="ListBoxExtensions.cs" />
|
||||
<Compile Include="Managers\EntityControlManager.cs" />
|
||||
<Compile Include="Managers\MultiplayerManagerDedicated.cs" />
|
||||
<Compile Include="Managers\InstanceManager.cs" />
|
||||
<Compile Include="NativeMethods.cs" />
|
||||
<Compile Include="Initializer.cs" />
|
||||
@@ -209,6 +215,13 @@
|
||||
<Compile Include="ViewModels\Entities\Blocks\PropertyViewModel.cs" />
|
||||
<Compile Include="ViewModels\Entities\CharacterViewModel.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\Entities\EntityViewModel.cs" />
|
||||
<Compile Include="ViewModels\Entities\FloatingObjectViewModel.cs" />
|
||||
@@ -300,6 +313,14 @@
|
||||
</ProjectReference>
|
||||
</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">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -328,6 +349,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\PluginsControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Entities\VoxelMapView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -340,10 +365,6 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\PluginsControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\TorchUI.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
|
@@ -3,20 +3,25 @@ using Sandbox.Engine.Utils;
|
||||
using Sandbox.Game;
|
||||
using Sandbox.Game.World;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Xml.Serialization.GeneratedAssembly;
|
||||
using NLog;
|
||||
using Sandbox.Engine.Analytics;
|
||||
using Sandbox.Game.Multiplayer;
|
||||
using Sandbox.ModAPI;
|
||||
using SteamSDK;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Managers;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.Utils;
|
||||
@@ -38,11 +43,17 @@ namespace Torch.Server
|
||||
public class TorchServer : TorchBase, ITorchServer
|
||||
{
|
||||
//public MyConfigDedicated<MyObjectBuilder_SessionSettings> DedicatedConfig { get; set; }
|
||||
/// <inheritdoc />
|
||||
public float SimulationRatio { get => _simRatio; set { _simRatio = value; OnPropertyChanged(); } }
|
||||
/// <inheritdoc />
|
||||
public TimeSpan ElapsedPlayTime { get => _elapsedPlayTime; set { _elapsedPlayTime = value; OnPropertyChanged(); } }
|
||||
/// <inheritdoc />
|
||||
public Thread GameThread { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } }
|
||||
/// <inheritdoc />
|
||||
public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } }
|
||||
/// <inheritdoc />
|
||||
public InstanceManager DedicatedInstance { get; }
|
||||
/// <inheritdoc />
|
||||
public string InstanceName => Config?.InstanceName;
|
||||
@@ -57,12 +68,16 @@ namespace Torch.Server
|
||||
private Timer _watchdog;
|
||||
private Stopwatch _uptime;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TorchServer(TorchConfig config = null)
|
||||
{
|
||||
DedicatedInstance = new InstanceManager(this);
|
||||
AddManager(DedicatedInstance);
|
||||
AddManager(new EntityControlManager(this));
|
||||
Config = config ?? new TorchConfig();
|
||||
MyFakes.ENABLE_INFINARIO = false;
|
||||
|
||||
var sessionManager = Managers.GetManager<ITorchSessionManager>();
|
||||
sessionManager.AddFactory((x) => new MultiplayerManagerDedicated(this));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -89,8 +104,17 @@ namespace Torch.Server
|
||||
MyPlugins.Load();
|
||||
MyGlobalTypeMetadata.Static.Init();
|
||||
|
||||
Managers.GetManager<ITorchSessionManager>().SessionStateChanged += OnSessionStateChanged;
|
||||
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()
|
||||
@@ -140,6 +164,7 @@ namespace Torch.Server
|
||||
Log.Info("Starting server.");
|
||||
|
||||
MySandboxGame.IsDedicated = true;
|
||||
MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model;
|
||||
Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString());
|
||||
|
||||
VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); };
|
||||
@@ -190,14 +215,67 @@ namespace Torch.Server
|
||||
((TorchServer)state).Invoke(() => mre.Set());
|
||||
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
|
||||
{
|
||||
var mainThread = MySandboxGame.Static.UpdateThread;
|
||||
if (mainThread.IsAlive)
|
||||
mainThread.Suspend();
|
||||
var stackTrace = new StackTrace(mainThread, true);
|
||||
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.\n{stackTrace}");
|
||||
#if DEBUG
|
||||
Log.Error($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
|
||||
Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
|
||||
#else
|
||||
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 />
|
||||
@@ -220,9 +298,11 @@ namespace Torch.Server
|
||||
MySandboxGame.Static.Exit();
|
||||
|
||||
Log.Info("Server stopped.");
|
||||
LogManager.Flush();
|
||||
_stopHandle.Set();
|
||||
State = ServerState.Stopped;
|
||||
IsRunning = false;
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -232,8 +312,12 @@ namespace Torch.Server
|
||||
{
|
||||
var exe = Assembly.GetExecutingAssembly().Location;
|
||||
((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString();
|
||||
Config.Autostart = true;
|
||||
Process.Start(exe, Config.ToString());
|
||||
Environment.Exit(0);
|
||||
Save(0).Wait();
|
||||
Stop();
|
||||
LogManager.Flush();
|
||||
Process.GetCurrentProcess().Kill();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -249,27 +333,32 @@ namespace Torch.Server
|
||||
/// <param name="callerId">Caller of the save operation</param>
|
||||
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
|
||||
{
|
||||
string response = null;
|
||||
switch (statusCode)
|
||||
{
|
||||
case SaveGameStatus.Success:
|
||||
Log.Info("Save completed.");
|
||||
Multiplayer.SendMessage("Saved game.", playerId: callerId);
|
||||
response = "Saved game.";
|
||||
break;
|
||||
case SaveGameStatus.SaveInProgress:
|
||||
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;
|
||||
case SaveGameStatus.GameNotReady:
|
||||
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;
|
||||
case SaveGameStatus.TimedOut:
|
||||
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;
|
||||
default:
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using Sandbox.Engine.Utils;
|
||||
using Torch.Collections;
|
||||
using VRage.Game;
|
||||
using VRage.Game.ModAPI;
|
||||
|
||||
@@ -25,6 +26,7 @@ namespace Torch.Server.ViewModels
|
||||
public ConfigDedicatedViewModel(MyConfigDedicated<MyObjectBuilder_SessionSettings> configDedicated)
|
||||
{
|
||||
_config = configDedicated;
|
||||
_config.IgnoreLastSession = true;
|
||||
SessionSettings = new SessionSettingsViewModel(_config.SessionSettings);
|
||||
Administrators = string.Join(Environment.NewLine, _config.Administrators);
|
||||
Banned = string.Join(Environment.NewLine, _config.Banned);
|
||||
@@ -52,13 +54,15 @@ namespace Torch.Server.ViewModels
|
||||
Log.Warn($"'{mod}' is not a valid mod ID.");
|
||||
}
|
||||
|
||||
// Never ever
|
||||
_config.IgnoreLastSession = true;
|
||||
_config.Save(path);
|
||||
}
|
||||
|
||||
private SessionSettingsViewModel _sessionSettings;
|
||||
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;
|
||||
public string Administrators { get => _administrators; set { _administrators = value; OnPropertyChanged(); } }
|
||||
private string _banned;
|
||||
@@ -78,12 +82,6 @@ namespace Torch.Server.ViewModels
|
||||
set { _config.GroupID = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IgnoreLastSession
|
||||
{
|
||||
get { return _config.IgnoreLastSession; }
|
||||
set { _config.IgnoreLastSession = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string IP
|
||||
{
|
||||
get { return _config.IP; }
|
||||
|
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using Sandbox.Game.Entities.Cube;
|
||||
using Sandbox.ModAPI;
|
||||
using Sandbox.ModAPI.Interfaces;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.ViewModels.Entities;
|
||||
|
||||
namespace Torch.Server.ViewModels.Blocks
|
||||
@@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Blocks
|
||||
public class BlockViewModel : EntityViewModel
|
||||
{
|
||||
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}";
|
||||
|
||||
|
38
Torch.Server/ViewModels/Entities/EntityControlViewModel.cs
Normal file
38
Torch.Server/ViewModels/Entities/EntityControlViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 VRageMath;
|
||||
|
||||
@@ -7,9 +11,25 @@ namespace Torch.Server.ViewModels.Entities
|
||||
public class EntityViewModel : ViewModel
|
||||
{
|
||||
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 MtObservableList<EntityControlViewModel> EntityControls { get; private set; }
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get => Entity.DisplayName;
|
||||
|
@@ -2,6 +2,8 @@
|
||||
using System.Linq;
|
||||
using Sandbox.Game.Entities;
|
||||
using Sandbox.ModAPI;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Collections;
|
||||
using Torch.Server.ViewModels.Blocks;
|
||||
|
||||
namespace Torch.Server.ViewModels.Entities
|
||||
@@ -9,7 +11,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
public class GridViewModel : EntityViewModel, ILazyLoad
|
||||
{
|
||||
private MyCubeGrid Grid => (MyCubeGrid)Entity;
|
||||
public ObservableList<BlockViewModel> Blocks { get; } = new ObservableList<BlockViewModel>();
|
||||
public MtObservableList<BlockViewModel> Blocks { get; } = new MtObservableList<BlockViewModel>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DescriptiveName { get; }
|
||||
@@ -34,7 +36,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
var block = obj.FatBlock as IMyTerminalBlock;
|
||||
if (block != null)
|
||||
Blocks.Insert(new BlockViewModel(block, Tree), b => b.Name);
|
||||
Blocks.Add(new BlockViewModel(block, Tree));
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ using Sandbox.Game.Entities;
|
||||
using VRage.Game.Entity;
|
||||
using VRage.Game.ModAPI;
|
||||
using System.Threading.Tasks;
|
||||
using Torch.Collections;
|
||||
|
||||
namespace Torch.Server.ViewModels.Entities
|
||||
{
|
||||
@@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Entities
|
||||
|
||||
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()
|
||||
{
|
||||
|
@@ -11,16 +11,19 @@ using VRage.Game.ModAPI;
|
||||
using VRage.ModAPI;
|
||||
using System.Windows.Threading;
|
||||
using NLog;
|
||||
using Torch.Collections;
|
||||
|
||||
namespace Torch.Server.ViewModels
|
||||
{
|
||||
public class EntityTreeViewModel : ViewModel
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
//TODO: these should be sorted sets for speed
|
||||
public ObservableList<GridViewModel> Grids { get; set; } = new ObservableList<GridViewModel>();
|
||||
public ObservableList<CharacterViewModel> Characters { get; set; } = new ObservableList<CharacterViewModel>();
|
||||
public ObservableList<EntityViewModel> FloatingObjects { get; set; } = new ObservableList<EntityViewModel>();
|
||||
public ObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new ObservableList<VoxelMapViewModel>();
|
||||
public MtObservableList<GridViewModel> Grids { get; set; } = new MtObservableList<GridViewModel>();
|
||||
public MtObservableList<CharacterViewModel> Characters { get; set; } = new MtObservableList<CharacterViewModel>();
|
||||
public MtObservableList<EntityViewModel> FloatingObjects { get; set; } = new MtObservableList<EntityViewModel>();
|
||||
public MtObservableList<VoxelMapViewModel> VoxelMaps { get; set; } = new MtObservableList<VoxelMapViewModel>();
|
||||
public Dispatcher ControlDispatcher => _control.Dispatcher;
|
||||
|
||||
private EntityViewModel _currentEntity;
|
||||
@@ -29,7 +32,7 @@ namespace Torch.Server.ViewModels
|
||||
public EntityViewModel CurrentEntity
|
||||
{
|
||||
get => _currentEntity;
|
||||
set { _currentEntity = value; OnPropertyChanged(); }
|
||||
set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); }
|
||||
}
|
||||
|
||||
public EntityTreeViewModel(UserControl control)
|
||||
@@ -44,6 +47,8 @@ namespace Torch.Server.ViewModels
|
||||
}
|
||||
|
||||
private void MyEntities_OnEntityRemove(VRage.Game.Entity.MyEntity obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
@@ -61,24 +66,38 @@ namespace Torch.Server.ViewModels
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Error(e);
|
||||
// ignore error "it's only UI"
|
||||
}
|
||||
}
|
||||
|
||||
private void MyEntities_OnEntityAdd(VRage.Game.Entity.MyEntity obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case MyCubeGrid grid:
|
||||
Grids.Insert(new GridViewModel(grid, this), g => g.Name);
|
||||
Grids.Add(new GridViewModel(grid, this));
|
||||
break;
|
||||
case MyCharacter character:
|
||||
Characters.Insert(new CharacterViewModel(character, this), c => c.Name);
|
||||
Characters.Add(new CharacterViewModel(character, this));
|
||||
break;
|
||||
case MyFloatingObject floating:
|
||||
FloatingObjects.Insert(new FloatingObjectViewModel(floating, this), f => f.Name);
|
||||
FloatingObjects.Add(new FloatingObjectViewModel(floating, this));
|
||||
break;
|
||||
case MyVoxelBase voxel:
|
||||
VoxelMaps.Insert(new VoxelMapViewModel(voxel, this), v => v.Name);
|
||||
VoxelMaps.Add(new VoxelMapViewModel(voxel, this));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Error(e);
|
||||
// ignore error "it's only UI"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,18 +6,19 @@ using System.Threading.Tasks;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Plugins;
|
||||
using Torch.Collections;
|
||||
|
||||
namespace Torch.Server.ViewModels
|
||||
{
|
||||
public class PluginManagerViewModel : ViewModel
|
||||
{
|
||||
public ObservableList<PluginViewModel> Plugins { get; } = new ObservableList<PluginViewModel>();
|
||||
public MtObservableList<PluginViewModel> Plugins { get; } = new MtObservableList<PluginViewModel>();
|
||||
|
||||
private PluginViewModel _selectedPlugin;
|
||||
public PluginViewModel SelectedPlugin
|
||||
{
|
||||
get => _selectedPlugin;
|
||||
set { _selectedPlugin = value; OnPropertyChanged(); }
|
||||
set { _selectedPlugin = value; OnPropertyChanged(nameof(SelectedPlugin)); }
|
||||
}
|
||||
|
||||
public PluginManagerViewModel() { }
|
||||
@@ -29,7 +30,7 @@ namespace Torch.Server.ViewModels
|
||||
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
|
||||
}
|
||||
|
||||
private void PluginManager_PluginsLoaded(IList<ITorchPlugin> obj)
|
||||
private void PluginManager_PluginsLoaded(IReadOnlyCollection<ITorchPlugin> obj)
|
||||
{
|
||||
Plugins.Clear();
|
||||
foreach (var plugin in obj)
|
||||
|
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpDX.Toolkit.Collections;
|
||||
using Torch.Collections;
|
||||
using VRage.Game;
|
||||
using VRage.Library.Utils;
|
||||
|
||||
@@ -35,7 +36,7 @@ namespace Torch.Server.ViewModels
|
||||
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
|
||||
|
||||
@@ -362,6 +363,19 @@ namespace Torch.Server.ViewModels
|
||||
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 />
|
||||
public static implicit operator MyObjectBuilder_SessionSettings(SessionSettingsViewModel viewModel)
|
||||
{
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -20,7 +21,11 @@ using Sandbox.Engine.Multiplayer;
|
||||
using Sandbox.Game.World;
|
||||
using SteamSDK;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Managers;
|
||||
using Torch.Server.Managers;
|
||||
using VRage.Game;
|
||||
|
||||
namespace Torch.Server
|
||||
{
|
||||
@@ -30,7 +35,6 @@ namespace Torch.Server
|
||||
public partial class ChatControl : UserControl
|
||||
{
|
||||
private TorchBase _server;
|
||||
private MultiplayerManager _multiplayer;
|
||||
|
||||
public ChatControl()
|
||||
{
|
||||
@@ -40,34 +44,76 @@ namespace Torch.Server
|
||||
public void BindServer(ITorchServer server)
|
||||
{
|
||||
_server = (TorchBase)server;
|
||||
_multiplayer = (MultiplayerManager)server.Multiplayer;
|
||||
DataContext = _multiplayer;
|
||||
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
ChatItems.Inlines.Clear();
|
||||
_multiplayer.ChatHistory.ForEach(InsertMessage);
|
||||
if (_multiplayer.ChatHistory is INotifyCollectionChanged ncc)
|
||||
ncc.CollectionChanged += ChatHistory_CollectionChanged;
|
||||
ChatScroller.ScrollToBottom();
|
||||
});
|
||||
|
||||
var sessionManager = server.Managers.GetManager<ITorchSessionManager>();
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
var span = new Span();
|
||||
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(new LineBreak());
|
||||
ChatItems.Inlines.Add(span);
|
||||
if (atBottom)
|
||||
ChatScroller.ScrollToBottom();
|
||||
}
|
||||
else
|
||||
Dispatcher.InvokeAsync(() => InsertMessage(msg));
|
||||
}
|
||||
|
||||
private void SendButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -87,27 +133,22 @@ namespace Torch.Server
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
var commands = _server.Commands;
|
||||
if (commands.IsCommand(text))
|
||||
var commands = _server.CurrentSession?.Managers.GetManager<Torch.Commands.CommandManager>();
|
||||
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(() =>
|
||||
{
|
||||
var response = commands.HandleCommandFromServer(text);
|
||||
Dispatcher.BeginInvoke(() => OnMessageEntered_Callback(response));
|
||||
string response = commands.HandleCommandFromServer(text);
|
||||
if (!string.IsNullOrWhiteSpace(response))
|
||||
InsertMessage(new TorchChatMessage("Server", response, MyFontEnum.Blue));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_server.Multiplayer.SendMessage(text);
|
||||
_server.CurrentSession?.Managers.GetManager<IChatManagerClient>().SendMessageAsSelf(text);
|
||||
}
|
||||
Message.Text = "";
|
||||
}
|
||||
|
||||
private void OnMessageEntered_Callback(string response)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
_multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -45,7 +45,6 @@
|
||||
<Label Content=":" Width="12" />
|
||||
<TextBox Text="{Binding Port}" Width="48" Height="20" />
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding IgnoreLastSession}" Content="Ignore Last Session" Margin="3" />
|
||||
<CheckBox IsChecked="{Binding PauseGameWhenEmpty}" Content="Pause When Empty" Margin="3" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="3">
|
||||
@@ -174,6 +173,14 @@
|
||||
DockPanel.Dock="Left" />
|
||||
<Label Content="Environment Hostility" />
|
||||
</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>
|
||||
</Expander>
|
||||
<Expander Header="Players">
|
||||
|
@@ -5,6 +5,8 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Torch.Server.Views.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">
|
||||
<UserControl.DataContext>
|
||||
<blocks:BlockViewModel />
|
||||
@@ -12,6 +14,7 @@
|
||||
<Grid Margin="3">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0">
|
||||
@@ -22,7 +25,8 @@
|
||||
</StackPanel>
|
||||
<Label Content="Properties"/>
|
||||
</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>
|
||||
<DataTemplate>
|
||||
<local:PropertyView />
|
||||
@@ -39,5 +43,9 @@
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
</ListView>
|
||||
</Expander>
|
||||
<ScrollViewer Grid.Row="2" Margin="3" VerticalScrollBarVisibility="Auto">
|
||||
<entities:EntityControlsView DataContext="{Binding}"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
8
Torch.Server/Views/Entities/EntityControlHost.xaml
Normal file
8
Torch.Server/Views/Entities/EntityControlHost.xaml
Normal 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>
|
72
Torch.Server/Views/Entities/EntityControlHost.xaml.cs
Normal file
72
Torch.Server/Views/Entities/EntityControlHost.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
31
Torch.Server/Views/Entities/EntityControlsView.xaml
Normal file
31
Torch.Server/Views/Entities/EntityControlsView.xaml
Normal 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>
|
15
Torch.Server/Views/Entities/EntityControlsView.xaml.cs
Normal file
15
Torch.Server/Views/Entities/EntityControlsView.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,20 +3,28 @@
|
||||
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:local="clr-namespace:Torch.Server.Views.Entities"
|
||||
xmlns:entities="clr-namespace:Torch.Server.ViewModels.Entities"
|
||||
xmlns:local="clr-namespace:Torch.Server.Views.Entities"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.DataContext>
|
||||
<entities:GridViewModel />
|
||||
</UserControl.DataContext>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal">
|
||||
<Label Content="Name" Width="100"/>
|
||||
<TextBox Text="{Binding Name}" Margin="3"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<Label Content="Position" Width="100"/>
|
||||
<TextBox Text="{Binding Position}" Margin="3" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="2" Margin="3" VerticalScrollBarVisibility="Auto">
|
||||
<local:EntityControlsView DataContext="{Binding}"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
@@ -9,8 +9,12 @@
|
||||
<UserControl.DataContext>
|
||||
<entities:VoxelMapViewModel/>
|
||||
</UserControl.DataContext>
|
||||
<StackPanel>
|
||||
<Label Content="Attached Grids"></Label>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Expander Grid.Row="0" Header="Attached Grids">
|
||||
<ListView ItemsSource="{Binding AttachedGrids}" Margin="3">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
@@ -18,5 +22,10 @@
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Expander>
|
||||
|
||||
<ScrollViewer Grid.Row="1" Margin="3" VerticalScrollBarVisibility="Auto">
|
||||
<local:EntityControlsView DataContext="{Binding}"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
@@ -12,6 +12,7 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using NLog;
|
||||
using Torch;
|
||||
using Sandbox;
|
||||
using Sandbox.Engine.Multiplayer;
|
||||
@@ -20,7 +21,10 @@ using Sandbox.Game.World;
|
||||
using Sandbox.ModAPI;
|
||||
using SteamSDK;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Session;
|
||||
using Torch.Managers;
|
||||
using Torch.Server.Managers;
|
||||
using Torch.ViewModels;
|
||||
using VRage.Game.ModAPI;
|
||||
|
||||
@@ -31,6 +35,8 @@ namespace Torch.Server
|
||||
/// </summary>
|
||||
public partial class PlayerListControl : UserControl
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private ITorchServer _server;
|
||||
|
||||
public PlayerListControl()
|
||||
@@ -41,19 +47,48 @@ namespace Torch.Server
|
||||
public void BindServer(ITorchServer 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
</UserControl.DataContext>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="150"/>
|
||||
<ColumnDefinition Width="200"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
@@ -27,7 +27,7 @@
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</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>
|
||||
<Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
|
||||
</Grid>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,6 +16,8 @@ using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.Managers;
|
||||
using Torch.Server.ViewModels;
|
||||
|
||||
namespace Torch.Server.Views
|
||||
@@ -24,6 +27,9 @@ namespace Torch.Server.Views
|
||||
/// </summary>
|
||||
public partial class PluginsControl : UserControl
|
||||
{
|
||||
private ITorchServer _server;
|
||||
private PluginManager _plugins;
|
||||
|
||||
public PluginsControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -31,8 +37,15 @@ namespace Torch.Server.Views
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Process.Start("explorer.exe", _plugins.PluginDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
386
Torch.Tests/PatchTest.cs
Normal file
386
Torch.Tests/PatchTest.cs
Normal 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
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Torch.Utils;
|
||||
using Xunit;
|
||||
|
||||
@@ -19,6 +20,10 @@ namespace Torch.Tests
|
||||
|
||||
public static IEnumerable<object[]> Invokers => _manager.Invokers;
|
||||
|
||||
public static IEnumerable<object[]> MemberInfo => _manager.MemberInfo;
|
||||
|
||||
public static IEnumerable<object[]> Events => _manager.Events;
|
||||
|
||||
#region Binding
|
||||
[Theory]
|
||||
[MemberData(nameof(Getters))]
|
||||
@@ -52,6 +57,28 @@ namespace Torch.Tests
|
||||
if (field.Field.IsStatic)
|
||||
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
|
||||
|
||||
#region Results
|
||||
@@ -79,10 +106,51 @@ namespace Torch.Tests
|
||||
{
|
||||
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
|
||||
{
|
||||
#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")]
|
||||
public static Func<ReflectionTestTarget, int> TestFieldGetter;
|
||||
[ReflectedSetter(Name = "TestField")]
|
||||
@@ -96,7 +164,27 @@ namespace Torch.Tests
|
||||
[ReflectedMethod]
|
||||
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))]
|
||||
public static Func<int> TestStaticFieldGetter;
|
||||
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
|
||||
@@ -109,6 +197,7 @@ namespace Torch.Tests
|
||||
|
||||
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
|
||||
public static Func<int, bool> TestCallStatic;
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -215,6 +304,32 @@ namespace Torch.Tests
|
||||
Assert.True(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
|
||||
}
|
||||
|
@@ -28,12 +28,16 @@ namespace Torch.Tests
|
||||
private readonly HashSet<object[]> _getters = new HashSet<object[]>();
|
||||
private readonly HashSet<object[]> _setters = 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()
|
||||
{
|
||||
_getters.Add(new object[] { new FieldRef(null) });
|
||||
_setters.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)
|
||||
@@ -50,12 +54,36 @@ namespace Torch.Tests
|
||||
BindingFlags.Public |
|
||||
BindingFlags.NonPublic))
|
||||
{
|
||||
if (field.GetCustomAttribute<ReflectedMethodAttribute>() != null)
|
||||
_invokers.Add(new object[] { new FieldRef(field) });
|
||||
if (field.GetCustomAttribute<ReflectedGetterAttribute>() != null)
|
||||
_getters.Add(new object[] { new FieldRef(field) });
|
||||
if (field.GetCustomAttribute<ReflectedSetterAttribute>() != null)
|
||||
_setters.Add(new object[] { new FieldRef(field) });
|
||||
var args = new object[] { new FieldRef(field) };
|
||||
foreach (ReflectedMemberAttribute attr in field.GetCustomAttributes<ReflectedMemberAttribute>())
|
||||
{
|
||||
if (!field.IsStatic)
|
||||
throw new ArgumentException("Field must be static to be reflected");
|
||||
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;
|
||||
}
|
||||
@@ -66,6 +94,10 @@ namespace Torch.Tests
|
||||
|
||||
public IEnumerable<object[]> Invokers => _invokers;
|
||||
|
||||
public IEnumerable<object[]> MemberInfo => _memberInfo;
|
||||
|
||||
public IEnumerable<object[]> Events => _events;
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
@@ -63,6 +63,7 @@
|
||||
<Compile Include="..\Versioning\AssemblyVersion.cs">
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="PatchTest.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ReflectionTestManager.cs" />
|
||||
<Compile Include="ReflectionSystemTest.cs" />
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Torch.Utils;
|
||||
using Xunit;
|
||||
|
||||
@@ -27,6 +28,10 @@ namespace Torch.Tests
|
||||
|
||||
public static IEnumerable<object[]> Invokers => Manager().Invokers;
|
||||
|
||||
public static IEnumerable<object[]> MemberInfo => Manager().MemberInfo;
|
||||
|
||||
public static IEnumerable<object[]> Events => Manager().Events;
|
||||
|
||||
#region Binding
|
||||
[Theory]
|
||||
[MemberData(nameof(Getters))]
|
||||
@@ -60,6 +65,28 @@ namespace Torch.Tests
|
||||
if (field.Field.IsStatic)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,77 +1,428 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch
|
||||
namespace Torch.Collections
|
||||
{
|
||||
[Obsolete("Use ObservableList<T>.")]
|
||||
public class MTObservableCollection<T> : ObservableCollection<T>
|
||||
/// <summary>
|
||||
/// 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;
|
||||
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(
|
||||
(Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
|
||||
DispatcherPriority.DataBind);
|
||||
continue;
|
||||
Backing = backing;
|
||||
// recursion so the events can read snapshots.
|
||||
Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
_version = 0;
|
||||
_threadViews = new ThreadLocal<ThreadView>(() => new ThreadView(this));
|
||||
_deferredSnapshot = new DeferredUpdateToken(this);
|
||||
_flushEventQueue = new Timer(FlushCollectionEventQueue);
|
||||
}
|
||||
|
||||
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);
|
||||
for (var i = 0; i < Count; i++)
|
||||
using (Lock.WriteUsing())
|
||||
{
|
||||
var key2 = selector(Items[i]);
|
||||
if (comparer.Compare(key, key2) < 1)
|
||||
continue;
|
||||
Backing.Clear();
|
||||
MarkSnapshotsDirty();
|
||||
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;
|
||||
_propertyChangedEvent.Raise(this, new PropertyChangedEventArgs(propName));
|
||||
}
|
||||
|
||||
Add(item);
|
||||
}
|
||||
|
||||
public void Sort<TKey>(Func<T, TKey> selector, IComparer<TKey> comparer = null)
|
||||
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
List<T> sortedItems;
|
||||
if (comparer != null)
|
||||
sortedItems = Items.OrderBy(selector, comparer).ToList();
|
||||
else
|
||||
sortedItems = Items.OrderBy(selector).ToList();
|
||||
|
||||
Items.Clear();
|
||||
foreach (var item in sortedItems)
|
||||
Add(item);
|
||||
if (!NotificationsEnabled)
|
||||
return;
|
||||
_collectionEventQueue.Enqueue(e);
|
||||
// In half a second, flush the events
|
||||
_flushEventQueue.Change(500, -1);
|
||||
}
|
||||
|
||||
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]))
|
||||
RemoveAt(i);
|
||||
_collectionChangedEvent.Raise(this, e);
|
||||
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;
|
||||
}
|
||||
}
|
163
Torch/Collections/MtObservableDictionary.cs
Normal file
163
Torch/Collections/MtObservableDictionary.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
102
Torch/Collections/MtObservableEvent.cs
Normal file
102
Torch/Collections/MtObservableEvent.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
175
Torch/Collections/MtObservableList.cs
Normal file
175
Torch/Collections/MtObservableList.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
36
Torch/Collections/TransformComparer.cs
Normal file
36
Torch/Collections/TransformComparer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
55
Torch/Collections/TransformEnumerator.cs
Normal file
55
Torch/Collections/TransformEnumerator.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -117,7 +117,7 @@ namespace Torch.Commands
|
||||
catch (Exception e)
|
||||
{
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Plugins;
|
||||
using VRage.Game;
|
||||
using VRage.Game.ModAPI;
|
||||
@@ -47,7 +48,7 @@ namespace Torch.Commands
|
||||
Response = message;
|
||||
|
||||
if (Player != null)
|
||||
Torch.Multiplayer.SendMessage(message, sender, Player.IdentityId, font);
|
||||
Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ using Torch.API;
|
||||
using Torch.API.Managers;
|
||||
using Torch.API.Plugins;
|
||||
using Torch.Managers;
|
||||
using VRage.Game;
|
||||
using VRage.Game.ModAPI;
|
||||
using VRage.Network;
|
||||
|
||||
@@ -21,7 +22,7 @@ namespace Torch.Commands
|
||||
public CommandTree Commands { get; set; } = new CommandTree();
|
||||
private Logger _log = LogManager.GetLogger(nameof(CommandManager));
|
||||
[Dependency]
|
||||
private ChatManager _chatManager;
|
||||
private IChatManagerServer _chatManager;
|
||||
|
||||
public CommandManager(ITorchBase torch, char prefix = '!') : base(torch)
|
||||
{
|
||||
@@ -31,7 +32,7 @@ namespace Torch.Commands
|
||||
public override void Attach()
|
||||
{
|
||||
RegisterCommandModule(typeof(TorchCommands));
|
||||
_chatManager.MessageRecieved += HandleCommand;
|
||||
_chatManager.MessageProcessing += HandleCommand;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var assembly = plugin.GetType().Assembly;
|
||||
@@ -93,20 +99,21 @@ namespace Torch.Commands
|
||||
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)
|
||||
return;
|
||||
|
||||
sendToOthers = false;
|
||||
consumed = true;
|
||||
|
||||
var player = Torch.Multiplayer.GetPlayerBySteamId(steamId);
|
||||
var player = Torch.CurrentSession.Managers.GetManager<IMultiplayerManagerBase>().GetPlayerBySteamId(steamId);
|
||||
if (player == null)
|
||||
{
|
||||
_log.Error($"Command {message} invoked by nonexistant player");
|
||||
@@ -123,7 +130,7 @@ namespace Torch.Commands
|
||||
if (!HasPermission(steamId, command))
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,12 @@ namespace Torch.Commands
|
||||
[Permission(MyPromoteLevel.None)]
|
||||
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);
|
||||
|
||||
if (node != null)
|
||||
@@ -51,7 +56,12 @@ namespace Torch.Commands
|
||||
[Command("longhelp", "Get verbose help. Will send a long message, check the Comms tab.")]
|
||||
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);
|
||||
|
||||
if (node != null)
|
||||
@@ -96,7 +106,7 @@ namespace Torch.Commands
|
||||
[Permission(MyPromoteLevel.None)]
|
||||
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)}");
|
||||
}
|
||||
|
||||
@@ -128,20 +138,19 @@ namespace Torch.Commands
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (i > 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
Context.Torch.Invoke(() =>
|
||||
{
|
||||
Context.Torch.Save(0).Wait();
|
||||
Context.Torch.Restart();
|
||||
});
|
||||
yield break;
|
||||
|
147
Torch/Event/EventList.cs
Normal file
147
Torch/Event/EventList.cs
Normal 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
191
Torch/Event/EventManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
Torch/Event/EventShimAttribute.cs
Normal file
20
Torch/Event/EventShimAttribute.cs
Normal 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
25
Torch/Event/IEventList.cs
Normal 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);
|
||||
}
|
||||
}
|
191
Torch/Extensions/ICollectionExtensions.cs
Normal file
191
Torch/Extensions/ICollectionExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,51 +2,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Torch
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
result = null;
|
||||
var match = Regex.Match(version, @"(\d+\.)?(\d+\.)?(\d+)");
|
||||
return match.Success && Version.TryParse(match.Value, out result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
171
Torch/Managers/ChatManager/ChatManagerClient.cs
Normal file
171
Torch/Managers/ChatManager/ChatManagerClient.cs
Normal 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;
|
||||
}
|
||||
}
|
203
Torch/Managers/ChatManager/ChatManagerServer.cs
Normal file
203
Torch/Managers/ChatManager/ChatManagerServer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
Torch/Managers/KeenLogPatch.cs
Normal file
149
Torch/Managers/KeenLogPatch.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
124
Torch/Managers/MultiplayerManagerBase.cs
Normal file
124
Torch/Managers/MultiplayerManagerBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,9 +20,9 @@ namespace Torch.Managers
|
||||
{
|
||||
private static Logger _log = LogManager.GetLogger(nameof(NetworkManager));
|
||||
|
||||
private const string MyTransportLayerField = "TransportLayer";
|
||||
private const string TransportHandlersField = "m_handlers";
|
||||
private HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
|
||||
private const string _myTransportLayerField = "TransportLayer";
|
||||
private const string _transportHandlersField = "m_handlers";
|
||||
private readonly HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
|
||||
private bool _init;
|
||||
|
||||
[ReflectedGetter(Name = "m_typeTable")]
|
||||
@@ -40,14 +40,14 @@ namespace Torch.Managers
|
||||
try
|
||||
{
|
||||
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)
|
||||
throw new TypeLoadException("Could not find internal type for TransportLayer");
|
||||
|
||||
var transportLayerType = transportLayerField.FieldType;
|
||||
|
||||
if (!Reflection.HasField(transportLayerType, TransportHandlersField))
|
||||
if (!Reflection.HasField(transportLayerType, _transportHandlersField))
|
||||
throw new TypeLoadException("Could not find Handlers field");
|
||||
|
||||
return true;
|
||||
@@ -60,15 +60,9 @@ namespace Torch.Managers
|
||||
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)
|
||||
return;
|
||||
@@ -79,9 +73,9 @@ namespace Torch.Managers
|
||||
throw new InvalidOperationException("Reflection unit test failed.");
|
||||
|
||||
//don't bother with nullchecks here, it was all handled in ReflectionUnitTest
|
||||
var transportType = typeof(MySyncLayer).GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType;
|
||||
var transportInstance = typeof(MySyncLayer).GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.Static.SyncLayer);
|
||||
var handlers = (IDictionary)transportType.GetField(TransportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
|
||||
var transportType = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType;
|
||||
var transportInstance = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.Static.SyncLayer);
|
||||
var handlers = (IDictionary)transportType.GetField(_transportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
|
||||
var handlerTypeField = handlers.GetType().GenericTypeArguments[0].GetField("messageId"); //Should be MyTransportLayer.HandlerId
|
||||
object id = null;
|
||||
foreach (var key in handlers.Keys)
|
||||
@@ -105,6 +99,12 @@ namespace Torch.Managers
|
||||
_log.Debug("Initialized network intercept");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Detach()
|
||||
{
|
||||
// TODO reverse what was done in Attach
|
||||
}
|
||||
|
||||
#region Network Intercept
|
||||
|
||||
/// <summary>
|
||||
@@ -205,6 +205,8 @@ namespace Torch.Managers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterNetworkHandler(INetworkHandler handler)
|
||||
{
|
||||
var handlerType = handler.GetType().FullName;
|
||||
@@ -225,6 +227,12 @@ namespace Torch.Managers
|
||||
_networkHandlers.Add(handler);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UnregisterNetworkHandler(INetworkHandler handler)
|
||||
{
|
||||
return _networkHandlers.Remove(handler);
|
||||
}
|
||||
|
||||
public void RegisterNetworkHandlers(params INetworkHandler[] handlers)
|
||||
{
|
||||
foreach (var handler in handlers)
|
||||
|
104
Torch/Managers/PatchManager/AssemblyMemory.cs
Normal file
104
Torch/Managers/PatchManager/AssemblyMemory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
279
Torch/Managers/PatchManager/DecoratedMethod.cs
Normal file
279
Torch/Managers/PatchManager/DecoratedMethod.cs
Normal 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
|
||||
}
|
||||
}
|
76
Torch/Managers/PatchManager/EmitExtensions.cs
Normal file
76
Torch/Managers/PatchManager/EmitExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
226
Torch/Managers/PatchManager/MSIL/ITokenResolver.cs
Normal file
226
Torch/Managers/PatchManager/MSIL/ITokenResolver.cs
Normal 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);
|
||||
}
|
||||
}
|
66
Torch/Managers/PatchManager/MSIL/MsilArgument.cs
Normal file
66
Torch/Managers/PatchManager/MSIL/MsilArgument.cs
Normal 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"})";
|
||||
}
|
||||
}
|
||||
}
|
216
Torch/Managers/PatchManager/MSIL/MsilInstruction.cs
Normal file
216
Torch/Managers/PatchManager/MSIL/MsilInstruction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
294
Torch/Managers/PatchManager/MSIL/MsilInstructionExtensions.cs
Normal file
294
Torch/Managers/PatchManager/MSIL/MsilInstructionExtensions.cs
Normal 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
|
||||
}
|
||||
}
|
67
Torch/Managers/PatchManager/MSIL/MsilLabel.cs
Normal file
67
Torch/Managers/PatchManager/MSIL/MsilLabel.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
63
Torch/Managers/PatchManager/MSIL/MsilLocal.cs
Normal file
63
Torch/Managers/PatchManager/MSIL/MsilLocal.cs
Normal 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"})";
|
||||
}
|
||||
}
|
||||
}
|
27
Torch/Managers/PatchManager/MSIL/MsilOperand.cs
Normal file
27
Torch/Managers/PatchManager/MSIL/MsilOperand.cs
Normal 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);
|
||||
}
|
||||
}
|
74
Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs
Normal file
74
Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs
Normal 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
Reference in New Issue
Block a user