diff --git a/NLog.config b/NLog.config
index 984f095..270fe34 100644
--- a/NLog.config
+++ b/NLog.config
@@ -6,10 +6,12 @@
+
+
\ No newline at end of file
diff --git a/Torch.API/IChatMessage.cs b/Torch.API/IChatMessage.cs
deleted file mode 100644
index e9dbc11..0000000
--- a/Torch.API/IChatMessage.cs
+++ /dev/null
@@ -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
- {
- ///
- /// The time the message was created.
- ///
- DateTime Timestamp { get; }
-
- ///
- /// The SteamID of the message author.
- ///
- ulong SteamId { get; }
-
- ///
- /// The name of the message author.
- ///
- string Name { get; }
-
- ///
- /// The content of the message.
- ///
- string Message { get; }
- }
-}
diff --git a/Torch.API/ITorchBase.cs b/Torch.API/ITorchBase.cs
index b7e15d0..c319818 100644
--- a/Torch.API/ITorchBase.cs
+++ b/Torch.API/ITorchBase.cs
@@ -44,10 +44,6 @@ namespace Torch.API
///
ITorchConfig Config { get; }
- ///
- [Obsolete]
- IMultiplayerManager Multiplayer { get; }
-
///
[Obsolete]
IPluginManager Plugins { get; }
@@ -55,6 +51,12 @@ namespace Torch.API
///
IDependencyManager Managers { get; }
+ [Obsolete("Prefer using Managers.GetManager for global managers")]
+ T GetManager() where T : class, IManager;
+
+ [Obsolete("Prefer using Managers.AddManager for global managers")]
+ bool AddManager(T manager) where T : class, IManager;
+
///
/// The binary version of the current instance.
///
@@ -101,6 +103,16 @@ namespace Torch.API
/// Initialize the Torch instance.
///
void Init();
+
+ ///
+ /// The current state of the game this instance of torch is controlling.
+ ///
+ TorchGameState GameState { get; }
+
+ ///
+ /// Event raised when changes.
+ ///
+ event TorchGameStateChangedDel GameStateChanged;
}
///
@@ -119,6 +131,6 @@ namespace Torch.API
///
public interface ITorchClient : ITorchBase
{
-
+
}
}
diff --git a/Torch.API/Managers/IChatManagerClient.cs b/Torch.API/Managers/IChatManagerClient.cs
new file mode 100644
index 0000000..d9a0bba
--- /dev/null
+++ b/Torch.API/Managers/IChatManagerClient.cs
@@ -0,0 +1,108 @@
+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.Network;
+
+namespace Torch.API.Managers
+{
+ ///
+ /// Represents a scripted or user chat message.
+ ///
+ public struct TorchChatMessage
+ {
+ ///
+ /// Creates a new torch chat message with the given author and message.
+ ///
+ /// Author's name
+ /// Message
+ public TorchChatMessage(string author, string message)
+ {
+ Timestamp = DateTime.Now;
+ AuthorSteamId = null;
+ Author = author;
+ Message = message;
+ Font = "Blue";
+ }
+
+ ///
+ /// Creates a new torch chat message with the given author and message.
+ ///
+ /// Author's steam ID
+ /// Message
+ public TorchChatMessage(ulong authorSteamId, string message)
+ {
+ Timestamp = DateTime.Now;
+ AuthorSteamId = authorSteamId;
+ Author = MyMultiplayer.Static?.GetMemberName(authorSteamId) ?? "Player";
+ Message = message;
+ Font = "Blue";
+ }
+
+ ///
+ /// This message's timestamp.
+ ///
+ public DateTime Timestamp;
+ ///
+ /// The author's steam ID, if available. Else, null.
+ ///
+ public ulong? AuthorSteamId;
+ ///
+ /// The author's name, if available. Else, null.
+ ///
+ public string Author;
+ ///
+ /// The message contents.
+ ///
+ public string Message;
+ ///
+ /// The font, or null if default.
+ ///
+ public string Font;
+ }
+
+ ///
+ /// Callback used to indicate that a messaage has been recieved.
+ ///
+ ///
+ /// If true, this event has been consumed and should be ignored
+ public delegate void MessageRecievedDel(TorchChatMessage msg, ref bool consumed);
+
+ ///
+ /// Callback used to indicate the user is attempting to send a message locally.
+ ///
+ /// Message the user is attempting to send
+ /// If true, this event has been consumed and should be ignored
+ public delegate void MessageSendingDel(string msg, ref bool consumed);
+
+ public interface IChatManagerClient : IManager
+ {
+ ///
+ /// Event that is raised when a message addressed to us is recieved.
+ ///
+ event MessageRecievedDel MessageRecieved;
+
+ ///
+ /// Event that is raised when we are attempting to send a message.
+ ///
+ event MessageSendingDel MessageSending;
+
+ ///
+ /// Triggers the event,
+ /// typically raised by the user entering text into the chat window.
+ ///
+ /// The message to send
+ void SendMessageAsSelf(string message);
+
+ ///
+ /// Displays a message on the UI given an author name and a message.
+ ///
+ /// Author name
+ /// Message content
+ /// font to use
+ void DisplayMessageOnSelf(string author, string message, string font = "Blue" );
+ }
+}
diff --git a/Torch.API/Managers/IChatManagerServer.cs b/Torch.API/Managers/IChatManagerServer.cs
new file mode 100644
index 0000000..52bd75f
--- /dev/null
+++ b/Torch.API/Managers/IChatManagerServer.cs
@@ -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
+{
+
+ ///
+ /// Callback used to indicate the server has recieved a message to process and forward on to others.
+ ///
+ /// Steam ID of the user sending a message
+ /// Message the user is attempting to send
+ /// If true, this event has been consumed and should be ignored
+ public delegate void MessageProcessingDel(TorchChatMessage msg, ref bool consumed);
+
+ public interface IChatManagerServer : IChatManagerClient
+ {
+ ///
+ /// Event triggered when the server has recieved a message and should process it.
+ ///
+ event MessageProcessingDel MessageProcessing;
+
+
+ ///
+ /// Sends a message with the given author and message to the given player, or all players by default.
+ ///
+ /// Author's steam ID
+ /// The message to send
+ /// Player to send the message to, or everyone by default
+ void SendMessageAsOther(ulong authorId, string message, ulong targetSteamId = 0);
+
+
+ ///
+ /// Sends a scripted message with the given author and message to the given player, or all players by default.
+ ///
+ /// Author name
+ /// The message to send
+ /// Font to use
+ /// Player to send the message to, or everyone by default
+ void SendMessageAsOther(string author, string message, string font, ulong targetSteamId = 0);
+ }
+}
diff --git a/Torch.API/Managers/IMultiplayerManager.cs b/Torch.API/Managers/IMultiplayerManager.cs
deleted file mode 100644
index 509360d..0000000
--- a/Torch.API/Managers/IMultiplayerManager.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Delegate for received messages.
- ///
- /// Message data.
- /// Flag to broadcast message to other players.
- public delegate void MessageReceivedDel(IChatMessage message, ref bool sendToOthers);
-
- ///
- /// API for multiplayer related functions.
- ///
- public interface IMultiplayerManager : IManager
- {
- ///
- /// Fired when a player joins.
- ///
- event Action PlayerJoined;
-
- ///
- /// Fired when a player disconnects.
- ///
- event Action PlayerLeft;
-
- ///
- /// Fired when a chat message is received.
- ///
- event MessageReceivedDel MessageReceived;
-
- ///
- /// Send a chat message to all or one specific player.
- ///
- void SendMessage(string message, string author = "Server", long playerId = 0, string font = MyFontEnum.Blue);
-
- ///
- /// Kicks the player from the game.
- ///
- void KickPlayer(ulong steamId);
-
- ///
- /// Bans or unbans a player from the game.
- ///
- void BanPlayer(ulong steamId, bool banned = true);
-
- ///
- /// Gets a player by their Steam64 ID or returns null if the player isn't found.
- ///
- IMyPlayer GetPlayerBySteamId(ulong id);
-
- ///
- /// Gets a player by their display name or returns null if the player isn't found.
- ///
- IMyPlayer GetPlayerByName(string name);
- }
-}
\ No newline at end of file
diff --git a/Torch.API/Managers/IMultiplayerManagerBase.cs b/Torch.API/Managers/IMultiplayerManagerBase.cs
new file mode 100644
index 0000000..09b76c9
--- /dev/null
+++ b/Torch.API/Managers/IMultiplayerManagerBase.cs
@@ -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
+{
+ ///
+ /// API for multiplayer related functions common to servers and clients.
+ ///
+ public interface IMultiplayerManagerBase : IManager
+ {
+ ///
+ /// Fired when a player joins.
+ ///
+ event Action PlayerJoined;
+
+ ///
+ /// Fired when a player disconnects.
+ ///
+ event Action PlayerLeft;
+
+ ///
+ /// Gets a player by their Steam64 ID or returns null if the player isn't found.
+ ///
+ IMyPlayer GetPlayerBySteamId(ulong id);
+
+ ///
+ /// Gets a player by their display name or returns null if the player isn't found.
+ ///
+ IMyPlayer GetPlayerByName(string name);
+
+ ///
+ /// Gets the steam username of a member's steam ID
+ ///
+ /// steam ID
+ /// steam username
+ string GetSteamUsername(ulong steamId);
+ }
+}
\ No newline at end of file
diff --git a/Torch.API/Managers/IMultiplayerManagerClient.cs b/Torch.API/Managers/IMultiplayerManagerClient.cs
new file mode 100644
index 0000000..f4e6a32
--- /dev/null
+++ b/Torch.API/Managers/IMultiplayerManagerClient.cs
@@ -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
+ {
+ }
+}
diff --git a/Torch.API/Managers/IMultiplayerManagerServer.cs b/Torch.API/Managers/IMultiplayerManagerServer.cs
new file mode 100644
index 0000000..31dadcd
--- /dev/null
+++ b/Torch.API/Managers/IMultiplayerManagerServer.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Torch.API.Managers
+{
+ ///
+ /// API for multiplayer functions that exist on servers and lobbies
+ ///
+ public interface IMultiplayerManagerServer : IMultiplayerManagerBase
+ {
+ ///
+ /// Kicks the player from the game.
+ ///
+ void KickPlayer(ulong steamId);
+
+ ///
+ /// Bans or unbans a player from the game.
+ ///
+ void BanPlayer(ulong steamId, bool banned = true);
+
+ ///
+ /// List of the banned SteamID's
+ ///
+ IReadOnlyList BannedPlayers { get; }
+
+ ///
+ /// Checks if the player with the given SteamID is banned.
+ ///
+ /// The SteamID of the player.
+ /// True if the player is banned; otherwise false.
+ bool IsBanned(ulong steamId);
+ }
+}
diff --git a/Torch.API/Managers/INetworkManager.cs b/Torch.API/Managers/INetworkManager.cs
index 548ec10..acbea3b 100644
--- a/Torch.API/Managers/INetworkManager.cs
+++ b/Torch.API/Managers/INetworkManager.cs
@@ -18,6 +18,12 @@ namespace Torch.API.Managers
/// Register a network handler.
///
void RegisterNetworkHandler(INetworkHandler handler);
+
+ ///
+ /// Unregister a network handler.
+ ///
+ /// true if the handler was unregistered, false if it wasn't registered to begin with
+ bool UnregisterNetworkHandler(INetworkHandler handler);
}
///
@@ -33,6 +39,7 @@ namespace Torch.API.Managers
///
/// Processes a network message.
///
+ /// true if the message should be discarded
bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet);
}
}
diff --git a/Torch.API/Session/ITorchSession.cs b/Torch.API/Session/ITorchSession.cs
index 710c9dd..59add06 100644
--- a/Torch.API/Session/ITorchSession.cs
+++ b/Torch.API/Session/ITorchSession.cs
@@ -25,5 +25,15 @@ namespace Torch.API.Session
///
IDependencyManager Managers { get; }
+
+ ///
+ /// The current state of the session
+ ///
+ TorchSessionState State { get; }
+
+ ///
+ /// Event raised when the changes.
+ ///
+ event TorchSessionStateChangedDel StateChanged;
}
}
diff --git a/Torch.API/Session/ITorchSessionManager.cs b/Torch.API/Session/ITorchSessionManager.cs
index 08fef9e..bfa3b88 100644
--- a/Torch.API/Session/ITorchSessionManager.cs
+++ b/Torch.API/Session/ITorchSessionManager.cs
@@ -27,6 +27,11 @@ namespace Torch.API.Session
///
ITorchSession CurrentSession { get; }
+ ///
+ /// Raised when any changes.
+ ///
+ event TorchSessionStateChangedDel SessionStateChanged;
+
///
/// Adds the given factory as a supplier for session based managers
///
diff --git a/Torch.API/Session/TorchSessionState.cs b/Torch.API/Session/TorchSessionState.cs
new file mode 100644
index 0000000..6d02da3
--- /dev/null
+++ b/Torch.API/Session/TorchSessionState.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Torch.API.Session
+{
+ ///
+ /// Represents the state of a
+ ///
+ public enum TorchSessionState
+ {
+ ///
+ /// The session has been created, and is now loading.
+ ///
+ Loading,
+ ///
+ /// The session has loaded, and is now running.
+ ///
+ Loaded,
+ ///
+ /// The session was running, and is now unloading.
+ ///
+ Unloading,
+ ///
+ /// The session was unloading, and is now unloaded and stopped.
+ ///
+ Unloaded
+ }
+
+ ///
+ /// Callback raised when a session's state changes
+ ///
+ /// The session who had a state change
+ /// The session's new state
+ public delegate void TorchSessionStateChangedDel(ITorchSession session, TorchSessionState newState);
+}
diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj
index 99b4836..bbf2e3b 100644
--- a/Torch.API/Torch.API.csproj
+++ b/Torch.API/Torch.API.csproj
@@ -160,15 +160,18 @@
Properties\AssemblyVersion.cs
-
+
+
-
+
+
+
@@ -182,6 +185,8 @@
+
+
diff --git a/Torch.API/TorchGameState.cs b/Torch.API/TorchGameState.cs
new file mode 100644
index 0000000..5c81393
--- /dev/null
+++ b/Torch.API/TorchGameState.cs
@@ -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
+{
+ ///
+ /// Represents the state of a
+ ///
+ public enum TorchGameState
+ {
+ ///
+ /// The game is currently being created.
+ ///
+ Creating,
+ ///
+ /// The game has been created and is ready to begin loading.
+ ///
+ Created,
+ ///
+ /// The game is currently loading.
+ ///
+ Loading,
+ ///
+ /// The game is fully loaded and ready to start sessions
+ ///
+ Loaded,
+ ///
+ /// The game is beginning the unload sequence
+ ///
+ Unloading,
+ ///
+ /// The game has been shutdown and is no longer active
+ ///
+ Unloaded
+ }
+
+ ///
+ /// Callback raised when a game's state changes
+ ///
+ /// The game who had a state change
+ /// The game's new state
+ public delegate void TorchGameStateChangedDel(MySandboxGame game, TorchGameState newState);
+}
diff --git a/Torch.Client.Tests/TorchClientReflectionTest.cs b/Torch.Client.Tests/TorchClientReflectionTest.cs
index 756b144..80cb47f 100644
--- a/Torch.Client.Tests/TorchClientReflectionTest.cs
+++ b/Torch.Client.Tests/TorchClientReflectionTest.cs
@@ -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 Invokers => Manager().Invokers;
+ public static IEnumerable MemberInfo => Manager().MemberInfo;
+
+ public static IEnumerable 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)field.Field.GetValue(null)).Invoke();
+ }
#endregion
}
}
diff --git a/Torch.Client/Manager/MultiplayerManagerClient.cs b/Torch.Client/Manager/MultiplayerManagerClient.cs
new file mode 100644
index 0000000..3cfc519
--- /dev/null
+++ b/Torch.Client/Manager/MultiplayerManagerClient.cs
@@ -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
+ {
+ ///
+ public MultiplayerManagerClient(ITorchBase torch) : base(torch) { }
+
+ ///
+ public override void Attach()
+ {
+ base.Attach();
+ MyMultiplayer.Static.ClientJoined += RaiseClientJoined;
+ }
+
+ ///
+ public override void Detach()
+ {
+ MyMultiplayer.Static.ClientJoined -= RaiseClientJoined;
+ base.Detach();
+ }
+ }
+}
diff --git a/Torch.Client/Manager/MultiplayerManagerLobby.cs b/Torch.Client/Manager/MultiplayerManagerLobby.cs
new file mode 100644
index 0000000..1ef3e3f
--- /dev/null
+++ b/Torch.Client/Manager/MultiplayerManagerLobby.cs
@@ -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
+ {
+ ///
+ public IReadOnlyList BannedPlayers => new List();
+
+ ///
+ public MultiplayerManagerLobby(ITorchBase torch) : base(torch) { }
+
+ ///
+ public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
+
+ ///
+ public void BanPlayer(ulong steamId, bool banned = true) => Torch.Invoke(() => MyMultiplayer.Static.BanClient(steamId, banned));
+
+ ///
+ public bool IsBanned(ulong steamId) => false;
+
+ ///
+ public override void Attach()
+ {
+ base.Attach();
+ MyMultiplayer.Static.ClientJoined += RaiseClientJoined;
+ }
+
+ ///
+ public override void Detach()
+ {
+ MyMultiplayer.Static.ClientJoined -= RaiseClientJoined;
+ base.Detach();
+ }
+ }
+}
diff --git a/Torch.Client/Torch.Client.csproj b/Torch.Client/Torch.Client.csproj
index abe6a5f..9ed8a34 100644
--- a/Torch.Client/Torch.Client.csproj
+++ b/Torch.Client/Torch.Client.csproj
@@ -121,6 +121,8 @@
Properties\AssemblyVersion.cs
+
+
@@ -167,6 +169,7 @@
+
diff --git a/Torch.Client/TorchClient.cs b/Torch.Client/TorchClient.cs
index 384ad3b..839c40a 100644
--- a/Torch.Client/TorchClient.cs
+++ b/Torch.Client/TorchClient.cs
@@ -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();
+ 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()
diff --git a/Torch.Client/TorchMainMenuScreen.cs b/Torch.Client/TorchMainMenuScreen.cs
index c0078e4..0bd9e5c 100644
--- a/Torch.Client/TorchMainMenuScreen.cs
+++ b/Torch.Client/TorchMainMenuScreen.cs
@@ -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)
+ {
+ }
///
public override void RecreateControls(bool constructor)
{
diff --git a/Torch.Server.Tests/TorchServerReflectionTest.cs b/Torch.Server.Tests/TorchServerReflectionTest.cs
index e4ec2de..b9c7904 100644
--- a/Torch.Server.Tests/TorchServerReflectionTest.cs
+++ b/Torch.Server.Tests/TorchServerReflectionTest.cs
@@ -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 Invokers => Manager().Invokers;
+ public static IEnumerable MemberInfo => Manager().MemberInfo;
+
+ public static IEnumerable 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)field.Field.GetValue(null)).Invoke();
+ }
#endregion
}
}
diff --git a/Torch.Server/Managers/MultiplayerManagerDedicated.cs b/Torch.Server/Managers/MultiplayerManagerDedicated.cs
new file mode 100644
index 0000000..d8427fc
--- /dev/null
+++ b/Torch.Server/Managers/MultiplayerManagerDedicated.cs
@@ -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> _members;
+ [ReflectedGetter(Name = "m_waitingForGroup")]
+ private static Func> _waitingForGroup;
+#pragma warning restore 649
+
+ ///
+ public IReadOnlyList BannedPlayers => MySandboxGame.ConfigDedicated.Banned;
+
+ private Dictionary _gameOwnerIds = new Dictionary();
+
+ ///
+ public MultiplayerManagerDedicated(ITorchBase torch) : base(torch) { }
+
+ ///
+ public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
+
+ ///
+ 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);
+ });
+ }
+
+ ///
+ public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) || MySandboxGame.ConfigDedicated.Banned.Contains(steamId);
+
+ ///
+ public override void Attach()
+ {
+ base.Attach();
+ _gameServerValidateAuthTicketReplacer = _gameServerValidateAuthTicketFactory.Invoke();
+ _gameServerUserGroupStatusReplacer = _gameServerUserGroupStatusFactory.Invoke();
+ _gameServerValidateAuthTicketReplacer.Replace(new Action(ValidateAuthTicketResponse), MyGameService.GameServer);
+ _gameServerUserGroupStatusReplacer.Replace(new Action(UserGroupStatusResponse), MyGameService.GameServer);
+ _log.Info("Inserted steam authentication intercept");
+ }
+
+ ///
+ 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 _gameServerValidateAuthTicketFactory;
+ [ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse), typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")]
+ private static Func _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 _convertSteamIDFrom64;
+
+ [ReflectedStaticMethod(Type = typeof(MyGameService), Name = "GetServerAccountType")]
+ private static Func _getServerAccountType;
+
+ [ReflectedMethod(Name = "UserAccepted")]
+ private static Action _userAcceptedImpl;
+
+ [ReflectedMethod(Name = "UserRejected")]
+ private static Action _userRejected;
+ [ReflectedMethod(Name = "IsClientBanned")]
+ private static Func _isClientBanned;
+ [ReflectedMethod(Name = "IsClientKicked")]
+ private static Func _isClientKicked;
+ [ReflectedMethod(Name = "RaiseClientKicked")]
+ private static Action _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
+ }
+}
diff --git a/Torch.Server/Torch.Server.csproj b/Torch.Server/Torch.Server.csproj
index fbfab75..ff2f432 100644
--- a/Torch.Server/Torch.Server.csproj
+++ b/Torch.Server/Torch.Server.csproj
@@ -182,6 +182,9 @@
..\GameBinaries\VRage.Scripting.dll
False
+
+ ..\GameBinaries\VRage.Steam.dll
+
@@ -191,6 +194,7 @@
Properties\AssemblyVersion.cs
+
diff --git a/Torch.Server/TorchServer.cs b/Torch.Server/TorchServer.cs
index 987aad9..afd6bfe 100644
--- a/Torch.Server/TorchServer.cs
+++ b/Torch.Server/TorchServer.cs
@@ -17,6 +17,8 @@ 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;
@@ -63,6 +65,9 @@ namespace Torch.Server
AddManager(DedicatedInstance);
Config = config ?? new TorchConfig();
MyFakes.ENABLE_INFINARIO = false;
+
+ var sessionManager = Managers.GetManager();
+ sessionManager.AddFactory((x) => new MultiplayerManagerDedicated(this));
}
///
@@ -90,7 +95,6 @@ namespace Torch.Server
MyGlobalTypeMetadata.Static.Init();
GetManager().LoadInstance(Config.InstancePath);
- Plugins.LoadPlugins();
}
private void InvokeBeforeRun()
@@ -253,19 +257,20 @@ namespace Torch.Server
{
case SaveGameStatus.Success:
Log.Info("Save completed.");
- Multiplayer.SendMessage("Saved game.", playerId: callerId);
+ // TODO
+// Multiplayer.SendMessage("Saved game.", playerId: callerId);
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);
+// Multiplayer.SendMessage("Save failed, a save is already in progress.", playerId: callerId, font: MyFontEnum.Red);
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);
+// Multiplayer.SendMessage("Save failed, game was not ready.", playerId: callerId, font: MyFontEnum.Red);
break;
case SaveGameStatus.TimedOut:
Log.Error("Save failed, save timed out.");
- Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red);
+// Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red);
break;
default:
break;
diff --git a/Torch.Server/Views/ChatControl.xaml.cs b/Torch.Server/Views/ChatControl.xaml.cs
index 6078aeb..59334b2 100644
--- a/Torch.Server/Views/ChatControl.xaml.cs
+++ b/Torch.Server/Views/ChatControl.xaml.cs
@@ -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,33 +44,75 @@ namespace Torch.Server
public void BindServer(ITorchServer server)
{
_server = (TorchBase)server;
- _multiplayer = (MultiplayerManager)server.Multiplayer;
- DataContext = _multiplayer;
+ Dispatcher.Invoke(() =>
+ {
+ ChatItems.Inlines.Clear();
+ });
- ChatItems.Inlines.Clear();
- _multiplayer.ChatHistory.ForEach(InsertMessage);
- if (_multiplayer.ChatHistory is INotifyCollectionChanged ncc)
- ncc.CollectionChanged += ChatHistory_CollectionChanged;
- ChatScroller.ScrollToBottom();
+ var sessionManager = server.Managers.GetManager();
+ if (sessionManager != null)
+ sessionManager.SessionStateChanged += SessionStateChanged;
}
- private void ChatHistory_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void SessionStateChanged(ITorchSession session, TorchSessionState state)
{
- foreach (IChatMessage msg in e.NewItems)
- InsertMessage(msg);
+ switch (state)
+ {
+ case TorchSessionState.Loading:
+ Dispatcher.Invoke(() => ChatItems.Inlines.Clear());
+ break;
+ case TorchSessionState.Loaded:
+ {
+ var chatMgr = session.Managers.GetManager();
+ if (chatMgr != null)
+ chatMgr.MessageRecieved += OnMessageRecieved;
+ }
+ break;
+ case TorchSessionState.Unloading:
+ {
+ var chatMgr = session.Managers.GetManager();
+ if (chatMgr != null)
+ chatMgr.MessageRecieved -= OnMessageRecieved;
+ }
+ break;
+ case TorchSessionState.Unloaded:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(state), state, null);
+ }
}
- private void InsertMessage(IChatMessage msg)
+ private void OnMessageRecieved(TorchChatMessage msg, ref bool consumed)
{
- 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($": {msg.Message}");
- span.Inlines.Add(new LineBreak());
- ChatItems.Inlines.Add(span);
- if (atBottom)
- ChatScroller.ScrollToBottom();
+ InsertMessage(msg);
+ }
+
+ private static readonly Dictionary _brushes = new Dictionary();
+ 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.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.Invoke(() => 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();
+ if (commands != null && commands.IsCommand(text))
{
- _multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", text));
+ InsertMessage(new TorchChatMessage("Server", text) { Font = 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) { Font = MyFontEnum.Blue });
});
}
else
{
- _server.Multiplayer.SendMessage(text);
+ _server.CurrentSession?.Managers.GetManager().SendMessageAsSelf(text);
}
Message.Text = "";
}
-
- private void OnMessageEntered_Callback(string response)
- {
- if (!string.IsNullOrEmpty(response))
- _multiplayer.ChatHistory.Add(new ChatMessage(DateTime.Now, 0, "Server", response));
- }
}
}
diff --git a/Torch.Server/Views/PlayerListControl.xaml.cs b/Torch.Server/Views/PlayerListControl.xaml.cs
index a54a306..c9e9676 100644
--- a/Torch.Server/Views/PlayerListControl.xaml.cs
+++ b/Torch.Server/Views/PlayerListControl.xaml.cs
@@ -20,7 +20,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;
@@ -41,19 +44,34 @@ namespace Torch.Server
public void BindServer(ITorchServer server)
{
_server = server;
- DataContext = (MultiplayerManager)_server.Multiplayer;
+
+ var sessionManager = server.Managers.GetManager();
+ sessionManager.SessionStateChanged += SessionStateChanged;
+ }
+
+ private void SessionStateChanged(ITorchSession session, TorchSessionState newState)
+ {
+ switch (newState)
+ {
+ case TorchSessionState.Loaded:
+ Dispatcher.Invoke(() => DataContext = _server?.CurrentSession?.Managers.GetManager());
+ break;
+ case TorchSessionState.Unloading:
+ Dispatcher.Invoke(() => DataContext = null);
+ break;
+ }
}
private void KickButton_Click(object sender, RoutedEventArgs e)
{
var player = (KeyValuePair)PlayerList.SelectedItem;
- _server.Multiplayer.KickPlayer(player.Key);
+ _server.CurrentSession?.Managers.GetManager()?.KickPlayer(player.Key);
}
private void BanButton_Click(object sender, RoutedEventArgs e)
{
- var player = (KeyValuePair) PlayerList.SelectedItem;
- _server.Multiplayer.BanPlayer(player.Key);
+ var player = (KeyValuePair)PlayerList.SelectedItem;
+ _server.CurrentSession?.Managers.GetManager()?.BanPlayer(player.Key);
}
}
}
diff --git a/Torch.Tests/ReflectionSystemTest.cs b/Torch.Tests/ReflectionSystemTest.cs
index 0e6fc92..c586a19 100644
--- a/Torch.Tests/ReflectionSystemTest.cs
+++ b/Torch.Tests/ReflectionSystemTest.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Reflection;
using Torch.Utils;
using Xunit;
@@ -11,7 +12,7 @@ namespace Torch.Tests
{
TestUtils.Init();
}
-
+
private static ReflectionTestManager _manager = new ReflectionTestManager().Init(typeof(ReflectionTestBinding));
public static IEnumerable Getters => _manager.Getters;
@@ -19,6 +20,10 @@ namespace Torch.Tests
public static IEnumerable Invokers => _manager.Invokers;
+ public static IEnumerable MemberInfo => _manager.MemberInfo;
+
+ public static IEnumerable 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)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 TestFieldGetter;
[ReflectedSetter(Name = "TestField")]
@@ -96,7 +164,27 @@ namespace Torch.Tests
[ReflectedMethod]
public static Func TestCall;
+ [ReflectedEventReplace(typeof(ReflectionTestTarget), "Event1", typeof(ReflectionTestTarget), "Callback1")]
+ public static Func 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 TestStaticFieldGetter;
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
@@ -109,6 +197,7 @@ namespace Torch.Tests
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
public static Func TestCallStatic;
+ #endregion
}
#endregion
@@ -215,7 +304,33 @@ 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
- }
+ }
}
diff --git a/Torch.Tests/ReflectionTestManager.cs b/Torch.Tests/ReflectionTestManager.cs
index 425024e..87c3757 100644
--- a/Torch.Tests/ReflectionTestManager.cs
+++ b/Torch.Tests/ReflectionTestManager.cs
@@ -28,12 +28,16 @@ namespace Torch.Tests
private readonly HashSet _getters = new HashSet();
private readonly HashSet _setters = new HashSet();
private readonly HashSet _invokers = new HashSet();
+ private readonly HashSet _memberInfo = new HashSet();
+ private readonly HashSet _events = new HashSet();
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() != null)
- _invokers.Add(new object[] { new FieldRef(field) });
- if (field.GetCustomAttribute() != null)
- _getters.Add(new object[] { new FieldRef(field) });
- if (field.GetCustomAttribute() != null)
- _setters.Add(new object[] { new FieldRef(field) });
+ var args = new object[] { new FieldRef(field) };
+ foreach (ReflectedMemberAttribute attr in field.GetCustomAttributes())
+ {
+ 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();
+ 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 Invokers => _invokers;
+ public IEnumerable MemberInfo => _memberInfo;
+
+ public IEnumerable Events => _events;
+
#endregion
}
diff --git a/Torch.Tests/TorchReflectionTest.cs b/Torch.Tests/TorchReflectionTest.cs
index bb495d8..775d100 100644
--- a/Torch.Tests/TorchReflectionTest.cs
+++ b/Torch.Tests/TorchReflectionTest.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 Invokers => Manager().Invokers;
+ public static IEnumerable MemberInfo => Manager().MemberInfo;
+
+ public static IEnumerable 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)field.Field.GetValue(null)).Invoke();
+ }
#endregion
}
}
diff --git a/Torch/ChatMessage.cs b/Torch/ChatMessage.cs
deleted file mode 100644
index f2307ab..0000000
--- a/Torch/ChatMessage.cs
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/Torch/Commands/Command.cs b/Torch/Commands/Command.cs
index 7aedcab..cbd08e2 100644
--- a/Torch/Commands/Command.cs
+++ b/Torch/Commands/Command.cs
@@ -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;
}
diff --git a/Torch/Commands/CommandContext.cs b/Torch/Commands/CommandContext.cs
index dd54918..30700a7 100644
--- a/Torch/Commands/CommandContext.cs
+++ b/Torch/Commands/CommandContext.cs
@@ -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()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
}
}
}
\ No newline at end of file
diff --git a/Torch/Commands/CommandManager.cs b/Torch/Commands/CommandManager.cs
index 20b2101..ff2d9a0 100644
--- a/Torch/Commands/CommandManager.cs
+++ b/Torch/Commands/CommandManager.cs
@@ -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)
@@ -93,20 +94,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().GetPlayerBySteamId(steamId);
if (player == null)
{
_log.Error($"Command {message} invoked by nonexistant player");
@@ -123,7 +125,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;
}
diff --git a/Torch/Commands/TorchCommands.cs b/Torch/Commands/TorchCommands.cs
index 0f604f0..cdc7fd8 100644
--- a/Torch/Commands/TorchCommands.cs
+++ b/Torch/Commands/TorchCommands.cs
@@ -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();
+ 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();
+ var commandManager = Context.Torch.CurrentSession?.Managers.GetManager();
+ 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()?.Plugins.Select(p => p.Value.Name) ?? Enumerable.Empty();
Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}");
}
@@ -128,13 +138,13 @@ 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().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().SendMessageAsSelf($"Restarting server in {i} second{Pluralize(i)}.");
yield return null;
}
else
@@ -153,7 +163,7 @@ namespace Torch.Commands
{
return num == 1 ? "" : "s";
}
-
+
///
/// Initializes a save of the game.
/// Caller id defaults to 0 in the case of triggering the chat command from server.
diff --git a/Torch/Managers/ChatManager.cs b/Torch/Managers/ChatManager.cs
deleted file mode 100644
index 2d9216a..0000000
--- a/Torch/Managers/ChatManager.cs
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/Torch/Managers/ChatManager/ChatManagerClient.cs b/Torch/Managers/ChatManager/ChatManagerClient.cs
new file mode 100644
index 0000000..f18a494
--- /dev/null
+++ b/Torch/Managers/ChatManager/ChatManagerClient.cs
@@ -0,0 +1,188 @@
+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();
+
+ ///
+ public ChatManagerClient(ITorchBase torchInstance) : base(torchInstance) { }
+
+ ///
+ public event MessageRecievedDel MessageRecieved;
+
+ ///
+ public event MessageSendingDel MessageSending;
+
+ ///
+ 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);
+ }
+
+ ///
+ 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
+ });
+ }
+
+ ///
+ public override void Attach()
+ {
+ base.Attach();
+ MyAPIUtilities.Static.MessageEntered += OnMessageEntered;
+ if (MyMultiplayer.Static != null)
+ {
+ _chatMessageRecievedReplacer = _chatMessageReceivedFactory.Invoke();
+ _scriptedChatMessageRecievedReplacer = _scriptedChatMessageReceivedFactory.Invoke();
+ _chatMessageRecievedReplacer.Replace(new Action(Multiplayer_ChatMessageReceived),
+ MyMultiplayer.Static);
+ _scriptedChatMessageRecievedReplacer.Replace(
+ new Action(Multiplayer_ScriptedChatMessageReceived), MyMultiplayer.Static);
+ }
+ else
+ {
+ MyAPIUtilities.Static.MessageEntered += OfflineMessageReciever;
+ }
+ }
+
+ ///
+ 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();
+ }
+
+ ///
+ /// Callback used to process offline messages.
+ ///
+ ///
+ /// true if the message was consumed
+ protected virtual bool OfflineMessageProcessor(TorchChatMessage msg)
+ {
+ return false;
+ }
+
+ private void OfflineMessageReciever(string messageText, ref bool sendToOthers)
+ {
+ if (!sendToOthers)
+ return;
+ var torchMsg = new TorchChatMessage()
+ {
+ AuthorSteamId = Sync.MyId,
+ Author = MySession.Static.LocalHumanPlayer?.DisplayName ?? "Player",
+ Message = 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()
+ {
+ AuthorSteamId = steamUserId,
+ Author = Torch.CurrentSession.Managers.GetManager()
+ ?.GetSteamUsername(steamUserId),
+ Font = (steamUserId == MyGameService.UserId) ? "DarkBlue" : "Blue",
+ Message = message
+ };
+ 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()
+ {
+ AuthorSteamId = null,
+ Author = author,
+ Font = font,
+ Message = message
+ };
+ 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 _hudChatMessageReceived;
+ [ReflectedMethod(Name = _hudChatScriptedMessageReceivedName)]
+ private static Action _hudChatScriptedMessageReceived;
+
+ [ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ChatMessageReceived), typeof(MyHudChat), _hudChatMessageReceivedName)]
+ private static Func _chatMessageReceivedFactory;
+ [ReflectedEventReplace(typeof(MyMultiplayerBase), nameof(MyMultiplayerBase.ScriptedChatMessageReceived), typeof(MyHudChat), _hudChatScriptedMessageReceivedName)]
+ private static Func _scriptedChatMessageReceivedFactory;
+
+ private ReflectedEventReplacer _chatMessageRecievedReplacer;
+ private ReflectedEventReplacer _scriptedChatMessageRecievedReplacer;
+ }
+}
diff --git a/Torch/Managers/ChatManager/ChatManagerServer.cs b/Torch/Managers/ChatManager/ChatManagerServer.cs
new file mode 100644
index 0000000..f71900c
--- /dev/null
+++ b/Torch/Managers/ChatManager/ChatManagerServer.cs
@@ -0,0 +1,206 @@
+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;
+
+ ///
+ public ChatManagerServer(ITorchBase torchInstance) : base(torchInstance)
+ {
+ _chatIntercept = new ChatIntercept(this);
+ }
+
+ ///
+ public event MessageProcessingDel MessageProcessing;
+
+ ///
+ 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
+
+ ///
+ 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);
+ }
+
+
+ ///
+ public override void Attach()
+ {
+ base.Attach();
+ if (_networkManager != null)
+ try
+ {
+ _networkManager.RegisterNetworkHandler(_chatIntercept);
+ _log.Debug("Initialized network intercept for chat messages");
+ return;
+ }
+ catch
+ {
+ // Discard exception and use second method
+ }
+
+ if (MyMultiplayer.Static != null)
+ {
+ MyMultiplayer.Static.ChatMessageReceived += MpStaticChatMessageReceived;
+ _log.Warn(
+ "Failed to initialize network intercept, we can't discard chat messages! Falling back to another method.");
+ }
+ else
+ {
+ _log.Debug("Using offline message processor");
+ }
+ }
+
+ ///
+ protected override bool OfflineMessageProcessor(TorchChatMessage msg)
+ {
+ 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);
+ }
+
+ ///
+ 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()
+ {
+ AuthorSteamId = message.Author,
+ Author = MyMultiplayer.Static?.GetMemberName(message.Author) ?? $"user_{message.Author}",
+ Message = 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;
+ }
+
+ ///
+ public override bool CanHandle(CallSite site)
+ {
+ if (site.MethodInfo.Name != "OnChatMessageRecieved")
+ return false;
+
+ if (_unitTestResult.HasValue)
+ return _unitTestResult.Value;
+
+ ParameterInfo[] parameters = site.MethodInfo.GetParameters();
+ if (parameters.Length != 1)
+ {
+ _unitTestResult = false;
+ return false;
+ }
+
+ if (parameters[0].ParameterType != typeof(ChatMsg))
+ _unitTestResult = false;
+
+ _unitTestResult = true;
+
+ return _unitTestResult.Value;
+ }
+
+ ///
+ public override bool Handle(ulong remoteUserId, CallSite site, BitStream stream, object obj, MyPacket packet)
+ {
+ var msg = new ChatMsg();
+ Serialize(site.MethodInfo, stream, ref msg);
+
+ var consumed = false;
+ _chatManager.RaiseMessageRecieved(msg, ref consumed);
+
+ return consumed;
+ }
+ }
+ }
+}
diff --git a/Torch/Managers/KeenLogManager.cs b/Torch/Managers/KeenLogManager.cs
new file mode 100644
index 0000000..591c63b
--- /dev/null
+++ b/Torch/Managers/KeenLogManager.cs
@@ -0,0 +1,164 @@
+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
+{
+ public class KeenLogManager : Manager
+ {
+ private static readonly Logger _log = LogManager.GetLogger("Keen");
+
+#pragma warning disable 649
+ [Dependency]
+ private PatchManager.PatchManager _patchManager;
+
+ [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
+
+ private PatchContext _context;
+
+ public KeenLogManager(ITorchBase torchInstance) : base(torchInstance)
+ {
+ }
+
+ public override void Attach()
+ {
+ _context = _patchManager.AcquireContext();
+
+ _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)));
+
+ _patchManager.Commit();
+ }
+
+ public override void Detach()
+ {
+ _patchManager.FreeContext(_context);
+ }
+
+ private static MethodInfo Method(string name)
+ {
+ return typeof(KeenLogManager).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ }
+
+ [ReflectedMethod(Name = "GetThreadId")]
+ private static Func _getThreadId;
+
+ [ReflectedMethod(Name = "GetIdentByThread")]
+ private static Func _getIndentByThread;
+
+ private static readonly ThreadLocal _tmpStringBuilder = new ThreadLocal(() => 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.Trace(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.Info(e);
+ return false;
+ }
+
+ private static bool PrefixWriteLineException(Exception ex)
+ {
+ _log.Info(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;
+ }
+ }
+ }
+}
diff --git a/Torch/Managers/MultiplayerManager.cs b/Torch/Managers/MultiplayerManager.cs
deleted file mode 100644
index ac1f09f..0000000
--- a/Torch/Managers/MultiplayerManager.cs
+++ /dev/null
@@ -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
-{
- ///
- public class MultiplayerManager : Manager, IMultiplayerManager
- {
- ///
- public event Action PlayerJoined;
- ///
- public event Action PlayerLeft;
- ///
- public event MessageReceivedDel MessageReceived;
-
- public IList ChatHistory { get; } = new ObservableList();
- public ObservableDictionary Players { get; } = new ObservableDictionary();
- 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> _onlinePlayers;
-
- [Dependency]
- private ChatManager _chatManager;
- [Dependency]
- private CommandManager _commandManager;
- [Dependency]
- private NetworkManager _networkManager;
-
- internal MultiplayerManager(ITorchBase torch) : base(torch)
- {
-
- }
-
- ///
- 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);
- }
-
- ///
- public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
-
- ///
- 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);
- });
- }
-
- ///
- public IMyPlayer GetPlayerByName(string name)
- {
- return _onlinePlayers.Invoke(MySession.Static.Players).FirstOrDefault(x => x.Value.DisplayName == name).Value;
- }
-
- ///
- 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;
- }
-
- ///
- public string GetSteamUsername(ulong steamId)
- {
- return MyMultiplayer.Static.GetMemberName(steamId);
- }
-
- ///
- 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> _members;
- [ReflectedGetter(Name = "m_waitingForGroup")]
- private static Func> _waitingForGroup;
- [ReflectedGetter(Name = "m_kickedClients")]
- private static Func> _kickedClients;
- //private HashSet _waitingForFriends;
- private Dictionary _gameOwnerIds = new Dictionary();
- //private IMultiplayer _multiplayerImplementation;
-
- ///
- /// Removes Keen's hooks into some Steam events so we have full control over client authentication
- ///
- 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;
- 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;
- 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 ConvertSteamIDFrom64;
-
- [ReflectedStaticMethod(Type = typeof(MyGameService))]
- private static Func GetServerAccountType;
-
- [ReflectedMethod(Name = "UserAccepted")]
- private static Action UserAcceptedImpl;
-
- [ReflectedMethod]
- private static Action UserRejected;
- [ReflectedMethod]
- private static Func IsClientBanned;
- [ReflectedMethod]
- private static Func IsClientKicked;
- [ReflectedMethod]
- private static Action RaiseClientKicked;
- }
-}
diff --git a/Torch/Managers/MultiplayerManagerBase.cs b/Torch/Managers/MultiplayerManagerBase.cs
new file mode 100644
index 0000000..4825ac2
--- /dev/null
+++ b/Torch/Managers/MultiplayerManagerBase.cs
@@ -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
+{
+ ///
+ public abstract class MultiplayerManagerBase : Manager, IMultiplayerManagerBase
+ {
+ private static readonly Logger _log = LogManager.GetCurrentClassLogger();
+
+ ///
+ public event Action PlayerJoined;
+ ///
+ public event Action PlayerLeft;
+
+ public ObservableDictionary Players { get; } = new ObservableDictionary();
+
+#pragma warning disable 649
+ [ReflectedGetter(Name = "m_players")]
+ private static Func> _onlinePlayers;
+#pragma warning restore 649
+
+ protected MultiplayerManagerBase(ITorchBase torch) : base(torch)
+ {
+
+ }
+
+ ///
+ public override void Attach()
+ {
+ MyMultiplayer.Static.ClientLeft += OnClientLeft;
+ }
+
+ ///
+ public override void Detach()
+ {
+ if (MyMultiplayer.Static != null)
+ MyMultiplayer.Static.ClientLeft -= OnClientLeft;
+ }
+
+ ///
+ public IMyPlayer GetPlayerByName(string name)
+ {
+ return _onlinePlayers.Invoke(MySession.Static.Players).FirstOrDefault(x => x.Value.DisplayName == name).Value;
+ }
+
+ ///
+ 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 kv in _onlinePlayers.Invoke(MySession.Static.Players))
+ {
+ if (kv.Value.Identity.IdentityId == identityId)
+ return kv.Key.SteamId;
+ }
+
+ return 0;
+ }
+
+ ///
+ 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);
+ }
+ }
+}
diff --git a/Torch/Managers/NetworkManager/NetworkManager.cs b/Torch/Managers/NetworkManager/NetworkManager.cs
index 91dff87..948ef80 100644
--- a/Torch/Managers/NetworkManager/NetworkManager.cs
+++ b/Torch/Managers/NetworkManager/NetworkManager.cs
@@ -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 _networkHandlers = new HashSet();
+ private const string _myTransportLayerField = "TransportLayer";
+ private const string _transportHandlersField = "m_handlers";
+ private readonly HashSet _networkHandlers = new HashSet();
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;
}
}
- ///
- /// Loads the network intercept system
- ///
+
+ ///
public override void Attach()
- {
- Torch.SessionLoaded += OnSessionLoaded;
- }
-
- private void OnSessionLoaded()
{
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");
}
+ ///
+ public override void Detach()
+ {
+ // TODO reverse what was done in Attach
+ }
+
#region Network Intercept
///
@@ -205,6 +205,8 @@ namespace Torch.Managers
}
}
+
+ ///
public void RegisterNetworkHandler(INetworkHandler handler)
{
var handlerType = handler.GetType().FullName;
@@ -225,6 +227,12 @@ namespace Torch.Managers
_networkHandlers.Add(handler);
}
+ ///
+ public bool UnregisterNetworkHandler(INetworkHandler handler)
+ {
+ return _networkHandlers.Remove(handler);
+ }
+
public void RegisterNetworkHandlers(params INetworkHandler[] handlers)
{
foreach (var handler in handlers)
diff --git a/Torch/Managers/PatchManager/DecoratedMethod.cs b/Torch/Managers/PatchManager/DecoratedMethod.cs
index 025b61a..8ca6a47 100644
--- a/Torch/Managers/PatchManager/DecoratedMethod.cs
+++ b/Torch/Managers/PatchManager/DecoratedMethod.cs
@@ -7,6 +7,7 @@ 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;
@@ -34,18 +35,21 @@ namespace Torch.Managers.PatchManager
if (Prefixes.Count == 0 && Suffixes.Count == 0 && Transpilers.Count == 0)
return;
+ _log.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.Debug($"Done patching {_method.DeclaringType?.FullName}#{_method.Name}({string.Join(", ", _method.GetParameters().Select(x => x.ParameterType.Name))})");
}
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();
@@ -113,29 +117,30 @@ namespace Torch.Managers.PatchManager
var specialVariables = new Dictionary();
- Label? labelAfterOriginalContent = Suffixes.Count > 0 ? target.DefineLabel() : (Label?)null;
- Label? labelAfterOriginalReturn = Prefixes.Any(x => x.ReturnType == typeof(bool)) ? target.DefineLabel() : (Label?)null;
+ Label labelAfterOriginalContent = target.DefineLabel();
+ Label labelAfterOriginalReturn = target.DefineLabel();
- var returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
- var resultVariable = returnType != typeof(void) && (labelAfterOriginalReturn.HasValue || // If we jump past main content we need local to store return val
- Prefixes.Concat(Suffixes).SelectMany(x => x.GetParameters()).Any(x => x.Name == RESULT_PARAMETER))
- ? target.DeclareLocal(returnType)
- : null;
+ Type returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
+ LocalBuilder resultVariable = null;
+ if (returnType != typeof(void))
+ {
+ if (Prefixes.Concat(Suffixes).SelectMany(x => x.GetParameters()).Any(x => x.Name == RESULT_PARAMETER))
+ resultVariable = target.DeclareLocal(returnType);
+ else if (Prefixes.Any(x => x.ReturnType == typeof(bool)))
+ resultVariable = target.DeclareLocal(returnType);
+ }
resultVariable?.SetToDefault(target);
if (resultVariable != null)
specialVariables.Add(RESULT_PARAMETER, resultVariable);
target.EmitComment("Prefixes Begin");
- foreach (var prefix in Prefixes)
+ foreach (MethodInfo prefix in Prefixes)
{
EmitMonkeyCall(target, prefix, specialVariables);
if (prefix.ReturnType == typeof(bool))
- {
- Debug.Assert(labelAfterOriginalReturn.HasValue);
- target.Emit(OpCodes.Brfalse, labelAfterOriginalReturn.Value);
- }
+ target.Emit(OpCodes.Brfalse, labelAfterOriginalReturn);
else if (prefix.ReturnType != typeof(void))
throw new Exception(
$"Prefixes must return void or bool. {prefix.DeclaringType?.FullName}.{prefix.Name} returns {prefix.ReturnType}");
@@ -143,32 +148,25 @@ namespace Torch.Managers.PatchManager
target.EmitComment("Prefixes End");
target.EmitComment("Original Begin");
- MethodTranspiler.Transpile(_method, Transpilers, target, labelAfterOriginalContent);
+ MethodTranspiler.Transpile(_method, (type) => new MsilLocal(target.DeclareLocal(type)), Transpilers, target, labelAfterOriginalContent);
target.EmitComment("Original End");
- if (labelAfterOriginalContent.HasValue)
- {
- target.MarkLabel(labelAfterOriginalContent.Value);
- if (resultVariable != null)
- target.Emit(OpCodes.Stloc, resultVariable);
- }
- if (labelAfterOriginalReturn.HasValue)
- target.MarkLabel(labelAfterOriginalReturn.Value);
+
+ target.MarkLabel(labelAfterOriginalContent);
+ if (resultVariable != null)
+ target.Emit(OpCodes.Stloc, resultVariable);
+ target.MarkLabel(labelAfterOriginalReturn);
target.EmitComment("Suffixes Begin");
- foreach (var suffix in Suffixes)
+ foreach (MethodInfo suffix in Suffixes)
{
EmitMonkeyCall(target, suffix, specialVariables);
if (suffix.ReturnType != typeof(void))
throw new Exception($"Suffixes must return void. {suffix.DeclaringType?.FullName}.{suffix.Name} returns {suffix.ReturnType}");
}
target.EmitComment("Suffixes End");
-
- if (labelAfterOriginalContent.HasValue || labelAfterOriginalReturn.HasValue)
- {
- if (resultVariable != null)
- target.Emit(OpCodes.Ldloc, resultVariable);
- target.Emit(OpCodes.Ret);
- }
+ if (resultVariable != null)
+ target.Emit(OpCodes.Ldloc, resultVariable);
+ target.Emit(OpCodes.Ret);
}
private void EmitMonkeyCall(LoggingIlGenerator target, MethodInfo patch,
diff --git a/Torch/Managers/PatchManager/MSIL/ITokenResolver.cs b/Torch/Managers/PatchManager/MSIL/ITokenResolver.cs
index ffaac0a..5b85c93 100644
--- a/Torch/Managers/PatchManager/MSIL/ITokenResolver.cs
+++ b/Torch/Managers/PatchManager/MSIL/ITokenResolver.cs
@@ -24,7 +24,7 @@ namespace Torch.Managers.PatchManager.MSIL
{
_module = method.Module;
_genericTypeArgs = method.DeclaringType?.GenericTypeArguments ?? new Type[0];
- _genericMethArgs = method.GetGenericArguments();
+ _genericMethArgs = (method is MethodInfo ? method.GetGenericArguments() : new Type[0]);
}
public MemberInfo ResolveMember(int token)
diff --git a/Torch/Managers/PatchManager/MSIL/MsilArgument.cs b/Torch/Managers/PatchManager/MSIL/MsilArgument.cs
new file mode 100644
index 0000000..1246546
--- /dev/null
+++ b/Torch/Managers/PatchManager/MSIL/MsilArgument.cs
@@ -0,0 +1,55 @@
+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
+{
+ ///
+ /// Represents metadata about a method's parameter
+ ///
+ public class MsilArgument
+ {
+ ///
+ /// The positon of this argument. Note, if the method is static, index 0 is the instance.
+ ///
+ public int Position { get; }
+
+ ///
+ /// The type of this parameter, or null if unknown.
+ ///
+ public Type Type { get; }
+
+ ///
+ /// The name of this parameter, or null if unknown.
+ ///
+ public string Name { get; }
+
+ internal MsilArgument(ParameterInfo local)
+ {
+ Position = (((MethodBase)local.Member).IsStatic ? 0 : 1) + local.Position;
+ Type = local.ParameterType;
+ Name = local.Name;
+ }
+
+ ///
+ /// Creates an empty argument reference with the given position.
+ ///
+ /// The argument's position
+ public MsilArgument(int position)
+ {
+ Position = position;
+ Type = null;
+ Name = null;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"arg{Position:X4}({Type?.Name ?? "unknown"})";
+ }
+ }
+}
diff --git a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs
index 9ac0359..54ad06e 100644
--- a/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs
+++ b/Torch/Managers/PatchManager/MSIL/MsilInstruction.cs
@@ -1,9 +1,13 @@
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;
+using Label = System.Windows.Controls.Label;
namespace Torch.Managers.PatchManager.MSIL
{
@@ -12,8 +16,6 @@ namespace Torch.Managers.PatchManager.MSIL
///
public class MsilInstruction
{
- private MsilOperand _operandBacking;
-
///
/// Creates a new instruction with the given opcode.
///
@@ -65,11 +67,11 @@ namespace Torch.Managers.PatchManager.MSIL
if (OpCode.Name.IndexOf("loc", StringComparison.OrdinalIgnoreCase) != -1)
Operand = new MsilOperandInline.MsilOperandLocal(this);
else
- Operand = new MsilOperandInline.MsilOperandParameter(this);
+ Operand = new MsilOperandInline.MsilOperandArgument(this);
break;
case OperandType.ShortInlineI:
Operand = OpCode == OpCodes.Ldc_I4_S
- ? (MsilOperand) new MsilOperandInline.MsilOperandInt8(this)
+ ? (MsilOperand)new MsilOperandInline.MsilOperandInt8(this)
: new MsilOperandInline.MsilOperandUInt8(this);
break;
case OperandType.ShortInlineR:
@@ -96,22 +98,16 @@ namespace Torch.Managers.PatchManager.MSIL
///
/// The operand for this instruction, or null.
///
- public MsilOperand Operand
- {
- get => _operandBacking;
- set
- {
- if (_operandBacking != null && value.GetType() != _operandBacking.GetType())
- throw new ArgumentException($"Operand for {OpCode.Name} must be {_operandBacking.GetType().Name}");
- _operandBacking = value;
- }
- }
+ public MsilOperand Operand { get; }
///
/// Labels pointing to this instruction.
///
public HashSet Labels { get; } = new HashSet();
+
+ private static readonly ConcurrentDictionary _setterInfoForInlines = new ConcurrentDictionary();
+
///
/// Sets the inline value for this instruction.
///
@@ -120,10 +116,41 @@ namespace Torch.Managers.PatchManager.MSIL
/// This instruction
public MsilInstruction InlineValue(T o)
{
- ((MsilOperandInline) Operand).Value = 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.Value));
+ _setterInfoForInlines[type] = target;
+ }
+ Debug.Assert(target?.DeclaringType != null);
+ if (target.DeclaringType.IsInstanceOfType(Operand))
+ {
+ target.SetValue(Operand, o);
+ return this;
+ }
+ type = type.BaseType;
+ }
+ ((MsilOperandInline)Operand).Value = o;
return this;
}
+ ///
+ /// Makes a copy of the instruction with a new opcode.
+ ///
+ /// The new opcode
+ /// The copy
+ public MsilInstruction CopyWith(OpCode newOpcode)
+ {
+ var result = new MsilInstruction(newOpcode);
+ Operand?.CopyTo(result.Operand);
+ foreach (MsilLabel x in Labels)
+ result.Labels.Add(x);
+ return result;
+ }
+
///
/// Sets the inline branch target for this instruction.
///
@@ -131,7 +158,7 @@ namespace Torch.Managers.PatchManager.MSIL
/// This instruction
public MsilInstruction InlineTarget(MsilLabel label)
{
- ((MsilOperandBrTarget) Operand).Target = label;
+ ((MsilOperandBrTarget)Operand).Target = label;
return this;
}
@@ -158,5 +185,32 @@ namespace Torch.Managers.PatchManager.MSIL
sb.Append(OpCode.Name).Append("\t").Append(Operand);
return sb.ToString();
}
+
+
+
+#pragma warning disable 169
+ [ReflectedMethod(Name = "StackChange")]
+ private static Func _stackChange;
+#pragma warning restore 169
+
+ ///
+ /// Estimates the stack delta for this instruction.
+ ///
+ /// Stack delta
+ public int StackChange()
+ {
+ int num = _stackChange.Invoke(OpCode);
+ if ((OpCode == OpCodes.Call || OpCode == OpCodes.Callvirt || OpCode == OpCodes.Newobj) &&
+ Operand is MsilOperandInline inline)
+ {
+ MethodBase op = inline.Value;
+ if (op is MethodInfo mi && mi.ReturnType != typeof(void))
+ num++;
+ num -= op.GetParameters().Length;
+ if (!op.IsStatic && OpCode != OpCodes.Newobj)
+ num--;
+ }
+ return num;
+ }
}
}
\ No newline at end of file
diff --git a/Torch/Managers/PatchManager/MSIL/MsilInstructionExtensions.cs b/Torch/Managers/PatchManager/MSIL/MsilInstructionExtensions.cs
new file mode 100644
index 0000000..4629f7f
--- /dev/null
+++ b/Torch/Managers/PatchManager/MSIL/MsilInstructionExtensions.cs
@@ -0,0 +1,202 @@
+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
+{
+ ///
+ /// Various methods to make composing MSIL easier
+ ///
+ public static class MsilInstructionExtensions
+ {
+ #region Local Utils
+ ///
+ /// Is this instruction a local load-by-value instruction.
+ ///
+ public static bool IsLocalLoad(this MsilInstruction me)
+ {
+ return me.OpCode == OpCodes.Ldloc || me.OpCode == OpCodes.Ldloc_S || me.OpCode == OpCodes.Ldloc_0 ||
+ me.OpCode == OpCodes.Ldloc_1 || me.OpCode == OpCodes.Ldloc_2 || me.OpCode == OpCodes.Ldloc_3;
+ }
+
+ ///
+ /// Is this instruction a local load-by-reference instruction.
+ ///
+ public static bool IsLocalLoadByRef(this MsilInstruction me)
+ {
+ return me.OpCode == OpCodes.Ldloca || me.OpCode == OpCodes.Ldloca_S;
+ }
+
+ ///
+ /// Is this instruction a local store instruction.
+ ///
+ public static bool IsLocalStore(this MsilInstruction me)
+ {
+ return me.OpCode == OpCodes.Stloc || me.OpCode == OpCodes.Stloc_S || me.OpCode == OpCodes.Stloc_0 ||
+ me.OpCode == OpCodes.Stloc_1 || me.OpCode == OpCodes.Stloc_2 || me.OpCode == OpCodes.Stloc_3;
+ }
+
+ ///
+ /// For a local referencing opcode, get the local it is referencing.
+ ///
+ 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}");
+ }
+ ///
+ /// Gets an instruction representing a load-by-value from the given local.
+ ///
+ /// Local to load
+ /// Loading instruction
+ 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);
+ }
+ }
+
+ ///
+ /// Gets an instruction representing a store-by-value to the given local.
+ ///
+ /// Local to write to
+ /// Loading instruction
+ 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);
+ }
+ }
+
+ ///
+ /// Gets an instruction representing a load-by-reference from the given local.
+ ///
+ /// Local to load
+ /// Loading instruction
+ public static MsilInstruction AsReferenceLoad(this MsilLocal local)
+ {
+ return new MsilInstruction(local.Index < 0xFF ? OpCodes.Ldloca_S : OpCodes.Ldloca).InlineValue(local);
+ }
+ #endregion
+
+ #region Argument Utils
+ ///
+ /// Is this instruction an argument load-by-value instruction.
+ ///
+ 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;
+ }
+
+ ///
+ /// Is this instruction an argument load-by-reference instruction.
+ ///
+ public static bool IsArgumentLoadByRef(this MsilInstruction me)
+ {
+ return me.OpCode == OpCodes.Ldarga || me.OpCode == OpCodes.Ldarga_S;
+ }
+
+ ///
+ /// Is this instruction an argument store instruction.
+ ///
+ public static bool IsArgumentStore(this MsilInstruction me)
+ {
+ return me.OpCode == OpCodes.Starg || me.OpCode == OpCodes.Starg_S;
+ }
+
+ ///
+ /// For an argument referencing opcode, get the index of the local it is referencing.
+ ///
+ 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}");
+ }
+
+ ///
+ /// Gets an instruction representing a load-by-value from the given argument.
+ ///
+ /// argument to load
+ /// Load instruction
+ 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);
+ }
+ }
+
+ ///
+ /// Gets an instruction representing a store-by-value to the given argument.
+ ///
+ /// argument to write to
+ /// Store instruction
+ public static MsilInstruction AsValueStore(this MsilArgument argument)
+ {
+ return new MsilInstruction(argument.Position < 0xFF ? OpCodes.Starg_S : OpCodes.Starg).InlineValue(argument);
+ }
+
+ ///
+ /// Gets an instruction representing a load-by-reference from the given argument.
+ ///
+ /// argument to load
+ /// Reference load instruction
+ public static MsilInstruction AsReferenceLoad(this MsilArgument argument)
+ {
+ return new MsilInstruction(argument.Position < 0xFF ? OpCodes.Ldarga_S : OpCodes.Ldarga).InlineValue(argument);
+ }
+ #endregion
+ }
+}
diff --git a/Torch/Managers/PatchManager/MSIL/MsilLocal.cs b/Torch/Managers/PatchManager/MSIL/MsilLocal.cs
new file mode 100644
index 0000000..1db7eb1
--- /dev/null
+++ b/Torch/Managers/PatchManager/MSIL/MsilLocal.cs
@@ -0,0 +1,62 @@
+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
+{
+ ///
+ /// Represents metadata about a method's local
+ ///
+ public class MsilLocal
+ {
+ ///
+ /// The index of this local.
+ ///
+ public int Index { get; }
+
+ ///
+ /// The type of this local, or null if unknown.
+ ///
+ public Type Type { get; }
+
+ ///
+ /// The name of this local, or null if unknown.
+ ///
+ 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;
+ }
+
+ ///
+ /// Creates an empty local reference with the given index.
+ ///
+ /// The local's index
+ public MsilLocal(int index)
+ {
+ Index = index;
+ Type = null;
+ Name = null;
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"lcl{Index:X4}({Type?.Name ?? "unknown"})";
+ }
+ }
+}
diff --git a/Torch/Managers/PatchManager/MSIL/MsilOperand.cs b/Torch/Managers/PatchManager/MSIL/MsilOperand.cs
index d9dbf2f..0df66c4 100644
--- a/Torch/Managers/PatchManager/MSIL/MsilOperand.cs
+++ b/Torch/Managers/PatchManager/MSIL/MsilOperand.cs
@@ -18,6 +18,8 @@ namespace Torch.Managers.PatchManager.MSIL
///
public MsilInstruction Instruction { get; }
+ internal abstract void CopyTo(MsilOperand operand);
+
internal abstract void Read(MethodContext context, BinaryReader reader);
internal abstract void Emit(LoggingIlGenerator generator);
diff --git a/Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs b/Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs
index 01e0913..ea93ce4 100644
--- a/Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs
+++ b/Torch/Managers/PatchManager/MSIL/MsilOperandBrTarget.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.Transpile;
@@ -22,8 +23,8 @@ namespace Torch.Managers.PatchManager.MSIL
{
int val = Instruction.OpCode.OperandType == OperandType.InlineBrTarget
? reader.ReadInt32()
- : reader.ReadByte();
- Target = context.LabelAt((int) reader.BaseStream.Position + val);
+ : reader.ReadSByte();
+ Target = context.LabelAt((int)reader.BaseStream.Position + val);
}
internal override void Emit(LoggingIlGenerator generator)
@@ -31,6 +32,14 @@ namespace Torch.Managers.PatchManager.MSIL
generator.Emit(Instruction.OpCode, Target.LabelFor(generator));
}
+ 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;
+ }
+
///
public override string ToString()
{
diff --git a/Torch/Managers/PatchManager/MSIL/MsilOperandInline.cs b/Torch/Managers/PatchManager/MSIL/MsilOperandInline.cs
index e327054..2442fd9 100644
--- a/Torch/Managers/PatchManager/MSIL/MsilOperandInline.cs
+++ b/Torch/Managers/PatchManager/MSIL/MsilOperandInline.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.Transpile;
@@ -22,6 +23,15 @@ namespace Torch.Managers.PatchManager.MSIL
///
public T Value { get; set; }
+ internal override void CopyTo(MsilOperand operand)
+ {
+ var lt = operand as MsilOperandInline;
+ if (lt == null)
+ throw new ArgumentException($"Target {operand?.GetType().Name} must be of same type {GetType().Name}", nameof(operand));
+ lt.Value = Value;
+ ;
+ }
+
///
public override string ToString()
{
@@ -66,7 +76,7 @@ namespace Torch.Managers.PatchManager.MSIL
internal override void Read(MethodContext context, BinaryReader reader)
{
Value =
- (sbyte) reader.ReadByte();
+ (sbyte)reader.ReadByte();
}
internal override void Emit(LoggingIlGenerator generator)
@@ -199,21 +209,23 @@ namespace Torch.Managers.PatchManager.MSIL
}
///
- /// Inline parameter reference
+ /// Inline argument reference
///
- public class MsilOperandParameter : MsilOperandInline
+ public class MsilOperandArgument : MsilOperandInline
{
- internal MsilOperandParameter(MsilInstruction instruction) : base(instruction)
+ internal MsilOperandArgument(MsilInstruction instruction) : base(instruction)
{
}
internal override void Read(MethodContext context, BinaryReader reader)
{
- Value =
- context.Method.GetParameters()[
- Instruction.OpCode.OperandType == OperandType.ShortInlineVar
- ? reader.ReadByte()
- : reader.ReadUInt16()];
+ int paramID =
+ Instruction.OpCode.OperandType == OperandType.ShortInlineVar
+ ? reader.ReadByte()
+ : reader.ReadUInt16();
+ if (paramID == 0 && !context.Method.IsStatic)
+ throw new ArgumentException("Haven't figured out how to ldarg with the \"this\" argument");
+ Value = new MsilArgument(context.Method.GetParameters()[paramID - (context.Method.IsStatic ? 0 : 1)]);
}
internal override void Emit(LoggingIlGenerator generator)
@@ -225,7 +237,7 @@ namespace Torch.Managers.PatchManager.MSIL
///
/// Inline local variable reference
///
- public class MsilOperandLocal : MsilOperandInline
+ public class MsilOperandLocal : MsilOperandInline
{
internal MsilOperandLocal(MsilInstruction instruction) : base(instruction)
{
@@ -234,15 +246,15 @@ namespace Torch.Managers.PatchManager.MSIL
internal override void Read(MethodContext context, BinaryReader reader)
{
Value =
- context.Method.GetMethodBody().LocalVariables[
+ new MsilLocal(context.Method.GetMethodBody().LocalVariables[
Instruction.OpCode.OperandType == OperandType.ShortInlineVar
? reader.ReadByte()
- : reader.ReadUInt16()];
+ : reader.ReadUInt16()]);
}
internal override void Emit(LoggingIlGenerator generator)
{
- generator.Emit(Instruction.OpCode, Value.LocalIndex);
+ generator.Emit(Instruction.OpCode, Value.Index);
}
}
diff --git a/Torch/Managers/PatchManager/MSIL/MsilOperandSwitch.cs b/Torch/Managers/PatchManager/MSIL/MsilOperandSwitch.cs
index 66e3b2d..2f28297 100644
--- a/Torch/Managers/PatchManager/MSIL/MsilOperandSwitch.cs
+++ b/Torch/Managers/PatchManager/MSIL/MsilOperandSwitch.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using System.Linq;
using System.Reflection.Emit;
using Torch.Managers.PatchManager.Transpile;
@@ -19,6 +20,20 @@ namespace Torch.Managers.PatchManager.MSIL
///
public MsilLabel[] Labels { get; set; }
+
+ internal override void CopyTo(MsilOperand operand)
+ {
+ var lt = operand as MsilOperandSwitch;
+ if (lt == null)
+ throw new ArgumentException($"Target {operand?.GetType().Name} must be of same type {GetType().Name}", nameof(operand));
+ if (Labels == null)
+ lt.Labels = null;
+ else
+ {
+ lt.Labels = new MsilLabel[Labels.Length];
+ Array.Copy(Labels, lt.Labels, Labels.Length);
+ }
+ }
internal override void Read(MethodContext context, BinaryReader reader)
{
int length = reader.ReadInt32();
diff --git a/Torch/Managers/PatchManager/Transpile/LoggingILGenerator.cs b/Torch/Managers/PatchManager/Transpile/LoggingILGenerator.cs
index e18eb30..1835ac2 100644
--- a/Torch/Managers/PatchManager/Transpile/LoggingILGenerator.cs
+++ b/Torch/Managers/PatchManager/Transpile/LoggingILGenerator.cs
@@ -52,7 +52,7 @@ namespace Torch.Managers.PatchManager.Transpile
///
public void Emit(OpCode op, LocalBuilder arg)
{
- _log?.Trace($"Emit\t{op,_opcodePadding} L:{arg.LocalIndex} {arg.LocalType}");
+ _log?.Trace($"Emit\t{op,_opcodePadding} Local:{arg.LocalIndex}/{arg.LocalType}");
Backing.Emit(op, arg);
}
diff --git a/Torch/Managers/PatchManager/Transpile/MethodContext.cs b/Torch/Managers/PatchManager/Transpile/MethodContext.cs
index 8c5be0d..7d316f7 100644
--- a/Torch/Managers/PatchManager/Transpile/MethodContext.cs
+++ b/Torch/Managers/PatchManager/Transpile/MethodContext.cs
@@ -7,6 +7,7 @@ using System.Reflection;
using System.Reflection.Emit;
using NLog;
using Torch.Managers.PatchManager.MSIL;
+using Torch.Utils;
namespace Torch.Managers.PatchManager.Transpile
{
@@ -34,8 +35,8 @@ namespace Torch.Managers.PatchManager.Transpile
public MethodContext(MethodBase method)
{
Method = method;
- _msilBytes = Method.GetMethodBody().GetILAsByteArray();
- TokenResolver = new NormalTokenResolver(method);
+ _msilBytes = Method.GetMethodBody().GetILAsByteArray();
+ TokenResolver = new NormalTokenResolver(method);
}
public void Read()
@@ -52,20 +53,19 @@ namespace Torch.Managers.PatchManager.Transpile
using (var reader = new BinaryReader(memory))
while (memory.Length > memory.Position)
{
- var count = 1;
+ var opcodeOffset = (int) memory.Position;
var instructionValue = (short)memory.ReadByte();
if (Prefixes.Contains(instructionValue))
{
- instructionValue = (short) ((instructionValue << 8) | memory.ReadByte());
- count++;
+ instructionValue = (short)((instructionValue << 8) | memory.ReadByte());
}
if (!OpCodeLookup.TryGetValue(instructionValue, out OpCode opcode))
throw new Exception($"Unknown opcode {instructionValue:X}");
- if (opcode.Size != count)
- throw new Exception($"Opcode said it was {opcode.Size} but we read {count}");
+ if (opcode.Size != memory.Position - opcodeOffset)
+ throw new Exception($"Opcode said it was {opcode.Size} but we read {memory.Position - opcodeOffset}");
var instruction = new MsilInstruction(opcode)
{
- Offset = (int) memory.Position
+ Offset = opcodeOffset
};
_instructions.Add(instruction);
instruction.Operand?.Read(this, reader);
@@ -76,22 +76,74 @@ namespace Torch.Managers.PatchManager.Transpile
{
foreach (var label in Labels)
{
- int min = 0, max = _instructions.Count - 1;
- while (min <= max)
+ int min = 0, max = _instructions.Count;
+ while (min != max)
{
- var mid = min + ((max - min) / 2);
- if (label.Key < _instructions[mid].Offset)
- max = mid - 1;
- else
+ int mid = (min + max) / 2;
+ if (_instructions[mid].Offset < label.Key)
min = mid + 1;
+ else
+ max = mid;
}
+#if DEBUG
+ if (min >= _instructions.Count || min < 0)
+ {
+ _log.Trace(
+ $"Want offset {label.Key} for {label.Value}, instruction offsets at\n {string.Join("\n", _instructions.Select(x => $"IL_{x.Offset:X4} {x}"))}");
+ }
+ MsilInstruction prevInsn = min > 0 ? _instructions[min - 1] : null;
+ if ((prevInsn == null || prevInsn.Offset >= label.Key) ||
+ _instructions[min].Offset < label.Key)
+ _log.Error($"Label {label.Value} wanted {label.Key} but instruction is at {_instructions[min].Offset}. Previous instruction is at {prevInsn?.Offset ?? -1}");
+#endif
_instructions[min]?.Labels?.Add(label.Value);
}
}
+
+ [Conditional("DEBUG")]
+ public void CheckIntegrity()
+ {
+ var entryStackCount = new Dictionary>();
+ var currentStackSize = 0;
+ foreach (MsilInstruction insn in _instructions)
+ {
+ // I don't want to deal with this, so I won't
+ if (insn.OpCode == OpCodes.Br || insn.OpCode == OpCodes.Br_S || insn.OpCode == OpCodes.Jmp ||
+ insn.OpCode == OpCodes.Leave || insn.OpCode == OpCodes.Leave_S)
+ break;
+ foreach (MsilLabel label in insn.Labels)
+ if (entryStackCount.TryGetValue(label, out Dictionary dict))
+ dict.Add(insn, currentStackSize);
+ else
+ (entryStackCount[label] = new Dictionary()).Add(insn, currentStackSize);
+
+ currentStackSize += insn.StackChange();
+
+ if (insn.Operand is MsilOperandBrTarget br)
+ if (entryStackCount.TryGetValue(br.Target, out Dictionary dict))
+ dict.Add(insn, currentStackSize);
+ else
+ (entryStackCount[br.Target] = new Dictionary()).Add(insn, currentStackSize);
+ }
+ foreach (KeyValuePair> label in entryStackCount)
+ {
+ if (label.Value.Values.Aggregate(new HashSet(), (a, b) =>
+ {
+ a.Add(b);
+ return a;
+ }).Count > 1)
+ {
+ _log.Warn($"Label {label.Key} has multiple entry stack counts");
+ foreach (KeyValuePair kv in label.Value)
+ _log.Warn($"{kv.Key.Offset:X4} {kv.Key} => {kv.Value}");
+ }
+ }
+ }
+
public string ToHumanMsil()
{
- return string.Join("\n", _instructions.Select(x => $"IL_{x.Offset:X4}: {x}"));
+ return string.Join("\n", _instructions.Select(x => $"IL_{x.Offset:X4}: {x.StackChange():+0;-#} {x}"));
}
private static readonly Dictionary OpCodeLookup;
@@ -106,9 +158,9 @@ namespace Torch.Managers.PatchManager.Transpile
var opcode = (OpCode)field.GetValue(null);
if (opcode.OpCodeType != OpCodeType.Nternal)
OpCodeLookup.Add(opcode.Value, opcode);
- if ((ushort) opcode.Value > 0xFF)
+ if ((ushort)opcode.Value > 0xFF)
{
- Prefixes.Add((short) ((ushort) opcode.Value >> 8));
+ Prefixes.Add((short)((ushort)opcode.Value >> 8));
}
}
}
diff --git a/Torch/Managers/PatchManager/Transpile/MethodTranspiler.cs b/Torch/Managers/PatchManager/Transpile/MethodTranspiler.cs
index d9a37eb..e2db491 100644
--- a/Torch/Managers/PatchManager/Transpile/MethodTranspiler.cs
+++ b/Torch/Managers/PatchManager/Transpile/MethodTranspiler.cs
@@ -9,18 +9,34 @@ namespace Torch.Managers.PatchManager.Transpile
{
internal class MethodTranspiler
{
- private static readonly Logger _log = LogManager.GetCurrentClassLogger();
+ public static readonly Logger _log = LogManager.GetCurrentClassLogger();
- internal static void Transpile(MethodBase baseMethod, IEnumerable transpilers, LoggingIlGenerator output, Label? retLabel)
+ internal static void Transpile(MethodBase baseMethod, Func localCreator, IEnumerable transpilers, LoggingIlGenerator output, Label? retLabel)
{
var context = new MethodContext(baseMethod);
context.Read();
- _log.Trace("Input Method:");
- _log.Trace(context.ToHumanMsil);
+ context.CheckIntegrity();
+ // _log.Trace("Input Method:");
+ // _log.Trace(context.ToHumanMsil);
- var methodContent = (IEnumerable) context.Instructions;
- foreach (var transpiler in transpilers)
- methodContent = (IEnumerable)transpiler.Invoke(null, new object[] { methodContent });
+ var methodContent = (IEnumerable)context.Instructions;
+ foreach (MethodInfo transpiler in transpilers)
+ {
+ var paramList = new List();
+ foreach (var parameter in transpiler.GetParameters())
+ {
+ if (parameter.Name.Equals("__methodBody"))
+ paramList.Add(baseMethod.GetMethodBody());
+ else if (parameter.Name.Equals("__localCreator"))
+ paramList.Add(localCreator);
+ else if (parameter.ParameterType == typeof(IEnumerable))
+ paramList.Add(methodContent);
+ else
+ throw new ArgumentException(
+ $"Bad transpiler parameter type {parameter.ParameterType.FullName} {parameter.Name}");
+ }
+ methodContent = (IEnumerable)transpiler.Invoke(null, paramList.ToArray());
+ }
methodContent = FixBranchAndReturn(methodContent, retLabel);
foreach (var k in methodContent)
k.Emit(output);
@@ -28,21 +44,24 @@ namespace Torch.Managers.PatchManager.Transpile
private static IEnumerable FixBranchAndReturn(IEnumerable insn, Label? retTarget)
{
- foreach (var i in insn)
+ foreach (MsilInstruction i in insn)
{
if (retTarget.HasValue && i.OpCode == OpCodes.Ret)
{
- var j = new MsilInstruction(OpCodes.Br);
- ((MsilOperandBrTarget)j.Operand).Target = new MsilLabel(retTarget.Value);
+ MsilInstruction j = new MsilInstruction(OpCodes.Br).InlineTarget(new MsilLabel(retTarget.Value));
+ foreach (MsilLabel l in i.Labels)
+ j.Labels.Add(l);
+ _log.Trace($"Replacing {i} with {j}");
yield return j;
- continue;
}
- if (_opcodeReplaceRule.TryGetValue(i.OpCode, out OpCode replaceOpcode))
+ else if (_opcodeReplaceRule.TryGetValue(i.OpCode, out OpCode replaceOpcode))
{
- yield return new MsilInstruction(replaceOpcode) { Operand = i.Operand };
- continue;
+ var result = i.CopyWith(replaceOpcode);
+ _log.Trace($"Replacing {i} with {result}");
+ yield return result;
}
- yield return i;
+ else
+ yield return i;
}
}
@@ -56,12 +75,13 @@ namespace Torch.Managers.PatchManager.Transpile
if (opcode.OperandType == OperandType.ShortInlineBrTarget &&
opcode.Name.EndsWith(".s", StringComparison.OrdinalIgnoreCase))
{
- var other = (OpCode?) typeof(OpCodes).GetField(field.Name.Substring(0, field.Name.Length - 2),
+ var other = (OpCode?)typeof(OpCodes).GetField(field.Name.Substring(0, field.Name.Length - 2),
BindingFlags.Static | BindingFlags.Public)?.GetValue(null);
if (other.HasValue && other.Value.OperandType == OperandType.InlineBrTarget)
_opcodeReplaceRule.Add(opcode, other.Value);
}
}
+ _opcodeReplaceRule[OpCodes.Leave_S] = OpCodes.Leave;
}
}
}
diff --git a/Torch/Managers/PluginManager.cs b/Torch/Managers/PluginManager.cs
new file mode 100644
index 0000000..92112f2
--- /dev/null
+++ b/Torch/Managers/PluginManager.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using NLog;
+using Torch.API;
+using Torch.API.Managers;
+using Torch.API.Plugins;
+using Torch.API.Session;
+using Torch.Commands;
+using VRage.Collections;
+
+namespace Torch.Managers
+{
+ ///
+ public class PluginManager : Manager, IPluginManager
+ {
+ private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
+ public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
+ [Dependency]
+ private UpdateManager _updateManager;
+ [Dependency(Optional = true)]
+ private ITorchSessionManager _sessionManager;
+
+ ///
+ public IList Plugins { get; } = new ObservableList();
+
+ public event Action PluginLoaded;
+ public event Action> PluginsLoaded;
+
+ public PluginManager(ITorchBase torchInstance) : base(torchInstance)
+ {
+ if (!Directory.Exists(PluginDir))
+ Directory.CreateDirectory(PluginDir);
+ }
+
+ ///
+ /// Updates loaded plugins in parallel.
+ ///
+ public void UpdatePlugins()
+ {
+ foreach (var plugin in Plugins)
+ plugin.Update();
+ }
+
+ private Action _attachCommandsHandler = null;
+
+ private void SessionStateChanged(ITorchSession session, TorchSessionState newState)
+ {
+ var cmdManager = session.Managers.GetManager();
+ if (cmdManager == null)
+ return;
+ switch (newState)
+ {
+ case TorchSessionState.Loaded:
+ if (_attachCommandsHandler != null)
+ PluginLoaded -= _attachCommandsHandler;
+ _attachCommandsHandler = (x) => cmdManager.RegisterPluginCommands(x);
+ PluginLoaded += _attachCommandsHandler;
+ foreach (ITorchPlugin plugin in Plugins)
+ cmdManager.RegisterPluginCommands(plugin);
+ break;
+ case TorchSessionState.Unloading:
+ if (_attachCommandsHandler != null)
+ {
+ PluginLoaded -= _attachCommandsHandler;
+ _attachCommandsHandler = null;
+ }
+ foreach (ITorchPlugin plugin in Plugins)
+ {
+ // cmdMgr?.UnregisterPluginCommands(plugin);
+ }
+ break;
+ case TorchSessionState.Loading:
+ case TorchSessionState.Unloaded:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(newState), newState, null);
+ }
+ }
+
+ ///
+ /// Prepares the plugin manager for loading.
+ ///
+ public override void Attach()
+ {
+ if (_sessionManager != null)
+ _sessionManager.SessionStateChanged += SessionStateChanged;
+ }
+
+ ///
+ /// Unloads all plugins.
+ ///
+ public override void Detach()
+ {
+ if (_sessionManager != null)
+ _sessionManager.SessionStateChanged -= SessionStateChanged;
+ foreach (var plugin in Plugins)
+ plugin.Dispose();
+
+ Plugins.Clear();
+ }
+
+ private void DownloadPlugins()
+ {
+ var folders = Directory.GetDirectories(PluginDir);
+ var taskList = new List();
+
+ //Copy list because we don't want to modify the config.
+ var toDownload = Torch.Config.Plugins.ToList();
+
+ foreach (var folder in folders)
+ {
+ var manifestPath = Path.Combine(folder, "manifest.xml");
+ if (!File.Exists(manifestPath))
+ {
+ _log.Debug($"No manifest in {folder}, skipping");
+ continue;
+ }
+
+ var manifest = PluginManifest.Load(manifestPath);
+ toDownload.RemoveAll(x => string.Compare(manifest.Repository, x, StringComparison.InvariantCultureIgnoreCase) == 0);
+ taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
+ }
+
+ foreach (var repository in toDownload)
+ {
+ var manifest = new PluginManifest { Repository = repository, Version = "0.0" };
+ taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
+ }
+
+ Task.WaitAll(taskList.ToArray());
+ }
+
+ ///
+ public void LoadPlugins()
+ {
+ if (Torch.Config.ShouldUpdatePlugins)
+ DownloadPlugins();
+ else
+ _log.Warn("Automatic plugin updates are disabled.");
+
+ _log.Info("Loading plugins");
+ var dlls = Directory.GetFiles(PluginDir, "*.dll", SearchOption.AllDirectories);
+ foreach (var dllPath in dlls)
+ {
+ _log.Info($"Loading plugin {dllPath}");
+ var asm = Assembly.UnsafeLoadFrom(dllPath);
+
+ foreach (var type in asm.GetExportedTypes())
+ {
+ if (type.GetInterfaces().Contains(typeof(ITorchPlugin)))
+ {
+ if (type.GetCustomAttribute() == null)
+ continue;
+
+ try
+ {
+ var plugin = (TorchPluginBase)Activator.CreateInstance(type);
+ if (plugin.Id == default(Guid))
+ throw new TypeLoadException($"Plugin '{type.FullName}' is missing a {nameof(PluginAttribute)}");
+
+ _log.Info($"Loading plugin {plugin.Name} ({plugin.Version})");
+ plugin.StoragePath = Torch.Config.InstancePath;
+ Plugins.Add(plugin);
+ PluginLoaded?.Invoke(plugin);
+ }
+ catch (Exception e)
+ {
+ _log.Error($"Error loading plugin '{type.FullName}'");
+ _log.Error(e);
+ throw;
+ }
+ }
+ }
+ }
+
+ Plugins.ForEach(p => p.Init(Torch));
+ PluginsLoaded?.Invoke(Plugins.ToList());
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return Plugins.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/Torch/Session/TorchSession.cs b/Torch/Session/TorchSession.cs
index 6ec4a2d..2120ef1 100644
--- a/Torch/Session/TorchSession.cs
+++ b/Torch/Session/TorchSession.cs
@@ -45,5 +45,20 @@ namespace Torch.Session
{
Managers.Detach();
}
+
+ private TorchSessionState _state = TorchSessionState.Loading;
+ ///
+ public TorchSessionState State
+ {
+ get => _state;
+ internal set
+ {
+ _state = value;
+ StateChanged?.Invoke(this, _state);
+ }
+ }
+
+ ///
+ public event TorchSessionStateChangedDel StateChanged;
}
}
diff --git a/Torch/Session/TorchSessionManager.cs b/Torch/Session/TorchSessionManager.cs
index 3e4b1a6..426b320 100644
--- a/Torch/Session/TorchSessionManager.cs
+++ b/Torch/Session/TorchSessionManager.cs
@@ -21,6 +21,9 @@ namespace Torch.Session
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private TorchSession _currentSession;
+ ///
+ public event TorchSessionStateChangedDel SessionStateChanged;
+
///
public ITorchSession CurrentSession => _currentSession;
@@ -46,48 +49,130 @@ namespace Torch.Session
return _factories.Remove(factory);
}
+ #region Session events
+
+ private void SetState(TorchSessionState state)
+ {
+ if (_currentSession == null)
+ return;
+ _currentSession.State = state;
+ SessionStateChanged?.Invoke(_currentSession, _currentSession.State);
+ }
+
+ private void SessionLoading()
+ {
+ try
+ {
+ if (_currentSession != null)
+ {
+ _log.Warn($"Override old torch session {_currentSession.KeenSession.Name}");
+ _currentSession.Detach();
+ }
+
+ _log.Info($"Starting new torch session for {MySession.Static.Name}");
+
+ _currentSession = new TorchSession(Torch, MySession.Static);
+ SetState(TorchSessionState.Loading);
+ }
+ catch (Exception e)
+ {
+ _log.Error(e);
+ throw;
+ }
+ }
+
private void SessionLoaded()
{
- if (_currentSession != null)
+ try
{
- _log.Warn($"Override old torch session {_currentSession.KeenSession.Name}");
- _currentSession.Detach();
+ if (_currentSession == null)
+ {
+ _log.Warn("Session loaded event occurred when we don't have a session.");
+ return;
+ }
+ foreach (SessionManagerFactoryDel factory in _factories)
+ {
+ IManager manager = factory(CurrentSession);
+ if (manager != null)
+ CurrentSession.Managers.AddManager(manager);
+ }
+ (CurrentSession as TorchSession)?.Attach();
+ SetState(TorchSessionState.Loaded);
}
+ catch (Exception e)
+ {
+ _log.Error(e);
+ throw;
+ }
+ }
- _log.Info($"Starting new torch session for {MySession.Static.Name}");
- _currentSession = new TorchSession(Torch, MySession.Static);
- foreach (SessionManagerFactoryDel factory in _factories)
+ private void SessionUnloading()
+ {
+ try
{
- IManager manager = factory(CurrentSession);
- if (manager != null)
- CurrentSession.Managers.AddManager(manager);
+ if (_currentSession == null)
+ {
+ _log.Warn("Session unloading event occurred when we don't have a session.");
+ return;
+ }
+ SetState(TorchSessionState.Unloading);
+ }
+ catch (Exception e)
+ {
+ _log.Error(e);
+ throw;
}
- (CurrentSession as TorchSession)?.Attach();
}
private void SessionUnloaded()
{
- if (_currentSession == null)
- return;
- _log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}");
- _currentSession.Detach();
- _currentSession = null;
+ try
+ {
+ if (_currentSession == null)
+ {
+ _log.Warn("Session unloading event occurred when we don't have a session.");
+ return;
+ }
+ _log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}");
+ SetState(TorchSessionState.Unloaded);
+ _currentSession.Detach();
+ _currentSession = null;
+ }
+ catch (Exception e)
+ {
+ _log.Error(e);
+ throw;
+ }
}
+ #endregion
///
public override void Attach()
{
+ MySession.OnLoading += SessionLoading;
MySession.AfterLoading += SessionLoaded;
+ MySession.OnUnloading += SessionUnloading;
MySession.OnUnloaded += SessionUnloaded;
}
+
///
public override void Detach()
{
- _currentSession?.Detach();
- _currentSession = null;
+ MySession.OnLoading -= SessionLoading;
MySession.AfterLoading -= SessionLoaded;
+ MySession.OnUnloading -= SessionUnloading;
MySession.OnUnloaded -= SessionUnloaded;
+
+ if (_currentSession != null)
+ {
+ if (_currentSession.State == TorchSessionState.Loaded)
+ SetState(TorchSessionState.Unloading);
+ if (_currentSession.State == TorchSessionState.Unloading)
+ SetState(TorchSessionState.Unloaded);
+ _currentSession.Detach();
+ _currentSession = null;
+ }
}
}
}
diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj
index bab46f8..9d94c77 100644
--- a/Torch/Torch.csproj
+++ b/Torch/Torch.csproj
@@ -154,17 +154,22 @@
Properties\AssemblyVersion.cs
-
+
+
+
+
+
+
@@ -192,13 +197,12 @@
-
-
+
diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs
index 58d9f8f..75d5612 100644
--- a/Torch/TorchBase.cs
+++ b/Torch/TorchBase.cs
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using NLog;
using Sandbox;
+using Sandbox.Engine.Multiplayer;
using Sandbox.Game;
using Sandbox.Game.Multiplayer;
using Sandbox.Game.Screens.Helpers;
@@ -22,10 +23,13 @@ using Torch.API.ModAPI;
using Torch.API.Session;
using Torch.Commands;
using Torch.Managers;
+using Torch.Managers.ChatManager;
+using Torch.Managers.PatchManager;
using Torch.Utils;
using Torch.Session;
using VRage.Collections;
using VRage.FileSystem;
+using VRage.Game;
using VRage.Game.ObjectBuilder;
using VRage.ObjectBuilders;
using VRage.Plugins;
@@ -67,18 +71,6 @@ namespace Torch
///
[Obsolete("Use GetManager() or the [Dependency] attribute.")]
public IPluginManager Plugins { get; protected set; }
- ///
- [Obsolete("Use GetManager() or the [Dependency] attribute.")]
- public IMultiplayerManager Multiplayer { get; protected set; }
- ///
- [Obsolete("Use GetManager() or the [Dependency] attribute.")]
- public EntityManager Entities { get; protected set; }
- ///
- [Obsolete("Use GetManager() or the [Dependency] attribute.")]
- public INetworkManager Network { get; protected set; }
- ///
- [Obsolete("Use GetManager() or the [Dependency] attribute.")]
- public CommandManager Commands { get; protected set; }
///
public ITorchSession CurrentSession => Managers?.GetManager()?.CurrentSession;
@@ -120,31 +112,38 @@ namespace Torch
Managers = new DependencyManager();
Plugins = new PluginManager(this);
- Multiplayer = new MultiplayerManager(this);
- Entities = new EntityManager(this);
- Network = new NetworkManager(this);
- Commands = new CommandManager(this);
- Managers.AddManager(new TorchSessionManager(this));
+ var sessionManager = new TorchSessionManager(this);
+ sessionManager.AddFactory((x) => MyMultiplayer.Static?.SyncLayer != null ? new NetworkManager(this) : null);
+ sessionManager.AddFactory((x) => Sync.IsServer ? new ChatManagerServer(this) : new ChatManagerClient(this));
+ sessionManager.AddFactory((x) => Sync.IsServer ? new CommandManager(this) : null);
+ sessionManager.AddFactory((x) => new EntityManager(this));
+
+ Managers.AddManager(sessionManager);
+ var patcher = new PatchManager(this);
+ GameStateInjector.Inject(patcher.AcquireContext());
+ Managers.AddManager(patcher);
+ // Managers.AddManager(new KeenLogManager(this));
Managers.AddManager(new FilesystemManager(this));
Managers.AddManager(new UpdateManager(this));
- Managers.AddManager(Network);
- Managers.AddManager(Commands);
Managers.AddManager(Plugins);
- Managers.AddManager(Multiplayer);
- Managers.AddManager(Entities);
- Managers.AddManager(new ChatManager(this));
-
+ GameStateChanged += (game, state) =>
+ {
+ if (state != TorchGameState.Created)
+ return;
+ // At this point flush the patches; it's safe.
+ patcher.Commit();
+ };
TorchAPI.Instance = this;
}
- ///
+ [Obsolete("Prefer using Managers.GetManager for global managers")]
public T GetManager() where T : class, IManager
{
return Managers.GetManager();
}
- ///
+ [Obsolete("Prefer using Managers.AddManager for global managers")]
public bool AddManager(T manager) where T : class, IManager
{
return Managers.AddManager(manager);
@@ -254,10 +253,13 @@ namespace Torch
Debug.Assert(MyPerGameSettings.BasicGameInfo.GameVersion != null, "MyPerGameSettings.BasicGameInfo.GameVersion != null");
GameVersion = new Version(new MyVersion(MyPerGameSettings.BasicGameInfo.GameVersion.Value).FormattedText.ToString().Replace("_", "."));
- try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; }
+ try
+ {
+ Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}";
+ }
catch
{
- //Running as service
+ // Running as service
}
#if DEBUG
@@ -275,6 +277,7 @@ namespace Torch
MySession.OnUnloading += OnSessionUnloading;
MySession.OnUnloaded += OnSessionUnloaded;
RegisterVRagePlugin();
+ Managers.GetManager().LoadPlugins();
Managers.Attach();
_init = true;
}
@@ -282,25 +285,57 @@ namespace Torch
private void OnSessionLoading()
{
Log.Debug("Session loading");
- SessionLoading?.Invoke();
+ try
+ {
+ SessionLoading?.Invoke();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e);
+ throw;
+ }
}
private void OnSessionLoaded()
{
Log.Debug("Session loaded");
- SessionLoaded?.Invoke();
+ try
+ {
+ SessionLoaded?.Invoke();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e);
+ throw;
+ }
}
private void OnSessionUnloading()
{
Log.Debug("Session unloading");
- SessionUnloading?.Invoke();
+ try
+ {
+ SessionUnloading?.Invoke();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e);
+ throw;
+ }
}
private void OnSessionUnloaded()
{
Log.Debug("Session unloaded");
- SessionUnloaded?.Invoke();
+ try
+ {
+ SessionUnloaded?.Invoke();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e);
+ throw;
+ }
}
///
@@ -357,5 +392,89 @@ namespace Torch
{
GetManager().UpdatePlugins();
}
+
+
+ private TorchGameState _gameState = TorchGameState.Unloaded;
+
+ ///
+ public TorchGameState GameState
+ {
+ get => _gameState;
+ private set
+ {
+ _gameState = value;
+ GameStateChanged?.Invoke(MySandboxGame.Static, _gameState);
+ }
+ }
+
+ ///
+ public event TorchGameStateChangedDel GameStateChanged;
+
+ #region GameStateInjecting
+ private static class GameStateInjector
+ {
+#pragma warning disable 649
+ [ReflectedMethodInfo(typeof(MySandboxGame), nameof(MySandboxGame.Dispose))]
+ private static MethodInfo _sandboxGameDispose;
+ [ReflectedMethodInfo(typeof(MySandboxGame), "Initialize")]
+ private static MethodInfo _sandboxGameInit;
+#pragma warning restore 649
+
+ internal static void Inject(PatchContext target)
+ {
+ ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] {typeof(string[])});
+ if (ctor == null)
+ throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
+ target.GetPattern(ctor).Prefixes.Add(MethodRef(nameof(PrefixConstructor)));
+ target.GetPattern(ctor).Suffixes.Add(MethodRef(nameof(SuffixConstructor)));
+ target.GetPattern(_sandboxGameInit).Prefixes.Add(MethodRef(nameof(PrefixInit)));
+ target.GetPattern(_sandboxGameInit).Suffixes.Add(MethodRef(nameof(SuffixInit)));
+ target.GetPattern(_sandboxGameDispose).Prefixes.Add(MethodRef(nameof(PrefixDispose)));
+ target.GetPattern(_sandboxGameDispose).Suffixes.Add(MethodRef(nameof(SuffixDispose)));
+ }
+
+ private static MethodInfo MethodRef(string name)
+ {
+ return typeof(GameStateInjector).GetMethod(name,
+ BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ }
+
+ private static void PrefixConstructor()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Creating;
+ }
+
+ private static void SuffixConstructor()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Created;
+ }
+
+ private static void PrefixInit()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Loading;
+ }
+
+ private static void SuffixInit()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Loaded;
+ }
+
+ private static void PrefixDispose()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Unloading;
+ }
+
+ private static void SuffixDispose()
+ {
+ if (Instance is TorchBase tb)
+ tb.GameState = TorchGameState.Unloaded;
+ }
+ }
+ #endregion
}
}
diff --git a/Torch/Utils/ReflectedManager.cs b/Torch/Utils/ReflectedManager.cs
index 6a6f342..9687cf1 100644
--- a/Torch/Utils/ReflectedManager.cs
+++ b/Torch/Utils/ReflectedManager.cs
@@ -7,6 +7,7 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
+using NLog;
using Sandbox.Engine.Multiplayer;
using Torch.API;
@@ -34,6 +35,81 @@ namespace Torch.Utils
}
}
+ #region MemberInfoAttributes
+ ///
+ /// Indicates that this field should contain the instance for the given field.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class ReflectedFieldInfoAttribute : ReflectedMemberAttribute
+ {
+ ///
+ /// Creates a reflected field info attribute using the given type and name.
+ ///
+ /// Type that contains the member
+ /// Name of the member
+ public ReflectedFieldInfoAttribute(Type type, string name)
+ {
+ Type = type;
+ Name = name;
+ }
+ }
+
+ ///
+ /// Indicates that this field should contain the instance for the given method.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class ReflectedMethodInfoAttribute : ReflectedMemberAttribute
+ {
+ ///
+ /// Creates a reflected method info attribute using the given type and name.
+ ///
+ /// Type that contains the member
+ /// Name of the member
+ public ReflectedMethodInfoAttribute(Type type, string name)
+ {
+ Type = type;
+ Name = name;
+ }
+ ///
+ /// Expected parameters of this method, or null if any parameters are accepted.
+ ///
+ public Type[] Parameters { get; set; } = null;
+
+ ///
+ /// Assembly qualified names of
+ ///
+ public string[] ParameterNames
+ {
+ get => Parameters.Select(x => x.AssemblyQualifiedName).ToArray();
+ set => Parameters = value?.Select(x => x == null ? null : Type.GetType(x)).ToArray();
+ }
+
+ ///
+ /// Expected return type of this method, or null if any return type is accepted.
+ ///
+ public Type ReturnType { get; set; } = null;
+ }
+
+ ///
+ /// Indicates that this field should contain the instance for the given property.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class ReflectedPropertyInfoAttribute : ReflectedMemberAttribute
+ {
+ ///
+ /// Creates a reflected property info attribute using the given type and name.
+ ///
+ /// Type that contains the member
+ /// Name of the member
+ public ReflectedPropertyInfoAttribute(Type type, string name)
+ {
+ Type = type;
+ Name = name;
+ }
+ }
+ #endregion
+
+ #region FieldPropGetSet
///
/// Indicates that this field should contain a delegate capable of retrieving the value of a field.
///
@@ -81,7 +157,9 @@ namespace Torch.Utils
public class ReflectedSetterAttribute : ReflectedMemberAttribute
{
}
+ #endregion
+ #region Invoker
///
/// Indicates that this field should contain a delegate capable of invoking an instance method.
///
@@ -138,14 +216,193 @@ namespace Torch.Utils
public class ReflectedStaticMethodAttribute : ReflectedMethodAttribute
{
}
+ #endregion
+
+ #region EventReplacer
+ ///
+ /// Instance of statefully replacing and restoring the callbacks of an event.
+ ///
+ public class ReflectedEventReplacer
+ {
+ private const BindingFlags BindFlagAll = BindingFlags.Static |
+ BindingFlags.Instance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic;
+
+ private object _instance;
+ private Func> _backingStoreReader;
+ private Action _callbackAdder;
+ private Action _callbackRemover;
+ private readonly ReflectedEventReplaceAttribute _attributes;
+ private readonly HashSet _registeredCallbacks = new HashSet();
+ private readonly MethodInfo _targetMethodInfo;
+
+ internal ReflectedEventReplacer(ReflectedEventReplaceAttribute attr)
+ {
+ _attributes = attr;
+ FieldInfo backingStore = GetEventBackingField(attr.EventName, attr.EventDeclaringType);
+ if (backingStore == null)
+ throw new ArgumentException($"Unable to find backing field for event {attr.EventDeclaringType.FullName}#{attr.EventName}");
+ EventInfo evtInfo = ReflectedManager.GetFieldPropRecursive(attr.EventDeclaringType, attr.EventName, BindFlagAll, (a, b, c) => a.GetEvent(b, c));
+ if (evtInfo == null)
+ throw new ArgumentException($"Unable to find event info for event {attr.EventDeclaringType.FullName}#{attr.EventName}");
+ _backingStoreReader = () => GetEventsInternal(_instance, backingStore);
+ _callbackAdder = (x) => evtInfo.AddEventHandler(_instance, x);
+ _callbackRemover = (x) => evtInfo.RemoveEventHandler(_instance, x);
+ if (attr.TargetParameters == null)
+ {
+ _targetMethodInfo = attr.TargetDeclaringType.GetMethod(attr.TargetName, BindFlagAll);
+ if (_targetMethodInfo == null)
+ throw new ArgumentException($"Unable to find method {attr.TargetDeclaringType.FullName}#{attr.TargetName} to replace");
+ }
+ else
+ {
+ _targetMethodInfo =
+ attr.TargetDeclaringType.GetMethod(attr.TargetName, BindFlagAll, null, attr.TargetParameters, null);
+ if (_targetMethodInfo == null)
+ throw new ArgumentException($"Unable to find method {attr.TargetDeclaringType.FullName}#{attr.TargetName}){string.Join(", ", attr.TargetParameters.Select(x => x.FullName))}) to replace");
+ }
+ }
+
+ ///
+ /// Test that this replacement can be performed.
+ ///
+ /// The instance to operate on, or null if static
+ /// true if possible, false if unsuccessful
+ public bool Test(object instance)
+ {
+ _instance = instance;
+ _registeredCallbacks.Clear();
+ foreach (Delegate callback in _backingStoreReader.Invoke())
+ if (callback.Method == _targetMethodInfo)
+ _registeredCallbacks.Add(callback);
+
+ return _registeredCallbacks.Count > 0;
+ }
+
+ private Delegate _newCallback;
+
+ ///
+ /// Removes the target callback defined in the attribute and replaces it with the provided callback.
+ ///
+ /// The new event callback
+ /// The instance to operate on, or null if static
+ public void Replace(Delegate newCallback, object instance)
+ {
+ _instance = instance;
+ if (_newCallback != null)
+ throw new Exception("Reflected event replacer is in invalid state: Replace when already replaced");
+ _newCallback = newCallback;
+ Test(instance);
+ if (_registeredCallbacks.Count == 0)
+ throw new Exception("Reflected event replacer is in invalid state: Nothing to replace");
+ foreach (Delegate callback in _registeredCallbacks)
+ _callbackRemover.Invoke(callback);
+ _callbackAdder.Invoke(_newCallback);
+ }
+
+ ///
+ /// Checks if the callback is currently replaced
+ ///
+ public bool Replaced => _newCallback != null;
+
+ ///
+ /// Removes the callback added by and puts the original callback back.
+ ///
+ /// The instance to operate on, or null if static
+ public void Restore(object instance)
+ {
+ _instance = instance;
+ if (_newCallback == null)
+ throw new Exception("Reflected event replacer is in invalid state: Restore when not replaced");
+ _callbackRemover.Invoke(_newCallback);
+ foreach (Delegate callback in _registeredCallbacks)
+ _callbackAdder.Invoke(callback);
+ _newCallback = null;
+ }
+
+
+ private static readonly string[] _backingFieldForEvent = { "{0}", "{0}" };
+
+ private static FieldInfo GetEventBackingField(string eventName, Type baseType)
+ {
+ FieldInfo eventField = null;
+ Type type = baseType;
+ while (type != null && eventField == null)
+ {
+ for (var i = 0; i < _backingFieldForEvent.Length && eventField == null; i++)
+ eventField = type.GetField(string.Format(_backingFieldForEvent[i], eventName), BindFlagAll);
+ type = type.BaseType;
+ }
+ return eventField;
+ }
+
+ private static IEnumerable GetEventsInternal(object instance, FieldInfo eventField)
+ {
+ if (eventField.GetValue(instance) is MulticastDelegate eventDel)
+ {
+ foreach (Delegate handle in eventDel.GetInvocationList())
+ yield return handle;
+ }
+ }
+ }
+
+ ///
+ /// Attribute used to indicate that the the given field, of type ]]>, should be filled with
+ /// a function used to create a new event replacer.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class ReflectedEventReplaceAttribute : Attribute
+ {
+ ///
+ /// Type that the event is declared in
+ ///
+ public Type EventDeclaringType { get; set; }
+ ///
+ /// Name of the event
+ ///
+ public string EventName { get; set; }
+
+ ///
+ /// Type that the method to replace is declared in
+ ///
+ public Type TargetDeclaringType { get; set; }
+ ///
+ /// Name of the method to replace
+ ///
+ public string TargetName { get; set; }
+ ///
+ /// Optional parameters of the method to replace. Null to ignore.
+ ///
+ public Type[] TargetParameters { get; set; } = null;
+
+ ///
+ /// Creates a reflected event replacer attribute to, for the event defined as eventName in eventDeclaringType,
+ /// replace the method defined as targetName in targetDeclaringType with a custom callback.
+ ///
+ /// Type the event is declared in
+ /// Name of the event
+ /// Type the method to remove is declared in
+ /// Name of the method to remove
+ public ReflectedEventReplaceAttribute(Type eventDeclaringType, string eventName, Type targetDeclaringType,
+ string targetName)
+ {
+ EventDeclaringType = eventDeclaringType;
+ EventName = eventName;
+ TargetDeclaringType = targetDeclaringType;
+ TargetName = targetName;
+ }
+ }
+ #endregion
///
/// Automatically calls for every assembly already loaded, and every assembly that is loaded in the future.
///
public class ReflectedManager
{
+ private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private static readonly string[] _namespaceBlacklist = new[] {
- "System", "VRage", "Sandbox", "SpaceEngineers"
+ "System", "VRage", "Sandbox", "SpaceEngineers", "Microsoft"
};
///
@@ -181,11 +438,28 @@ namespace Torch.Utils
{
if (_processedTypes.Add(t))
{
+ if (string.IsNullOrWhiteSpace(t.Namespace))
+ return;
foreach (string ns in _namespaceBlacklist)
if (t.FullName.StartsWith(ns))
return;
- foreach (FieldInfo field in t.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
- Process(field);
+ foreach (FieldInfo field in t.GetFields(BindingFlags.Static | BindingFlags.Instance |
+ BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ try
+ {
+#if DEBUG
+ if (Process(field))
+ _log?.Trace($"Field {field.DeclaringType?.FullName}#{field.Name} = {field.GetValue(null) ?? "null"}");
+#else
+ Process(field);
+#endif
+ }
+ catch (Exception e)
+ {
+ _log?.Error(e.InnerException ?? e, $"Unable to fill {field.DeclaringType?.FullName}#{field.Name}. {(e.InnerException ?? e).Message}");
+ }
+ }
}
}
@@ -207,37 +481,95 @@ namespace Torch.Utils
/// If the field failed to process
public static bool Process(FieldInfo field)
{
- var attr = field.GetCustomAttribute();
- if (attr != null)
+ foreach (ReflectedMemberAttribute attr in field.GetCustomAttributes())
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
- ProcessReflectedMethod(field, attr);
- return true;
- }
- var attr2 = field.GetCustomAttribute();
- if (attr2 != null)
- {
- if (!field.IsStatic)
- throw new ArgumentException("Field must be static to be reflected");
- ProcessReflectedField(field, attr2);
- return true;
- }
- var attr3 = field.GetCustomAttribute();
- if (attr3 != null)
- {
- if (!field.IsStatic)
+ switch (attr)
{
- throw new ArgumentException("Field must be static to be reflected");
+ case ReflectedMethodAttribute rma:
+ ProcessReflectedMethod(field, rma);
+ return true;
+ case ReflectedGetterAttribute rga:
+ ProcessReflectedField(field, rga);
+ return true;
+ case ReflectedSetterAttribute rsa:
+ ProcessReflectedField(field, rsa);
+ return true;
+ case ReflectedFieldInfoAttribute rfia:
+ ProcessReflectedMemberInfo(field, rfia);
+ return true;
+ case ReflectedPropertyInfoAttribute rpia:
+ ProcessReflectedMemberInfo(field, rpia);
+ return true;
+ case ReflectedMethodInfoAttribute rmia:
+ ProcessReflectedMemberInfo(field, rmia);
+ return true;
}
-
- ProcessReflectedField(field, attr3);
- return true;
}
+ var reflectedEventReplacer = field.GetCustomAttribute();
+ if (reflectedEventReplacer != null)
+ {
+ if (!field.IsStatic)
+ throw new ArgumentException("Field must be static to be reflected");
+ field.SetValue(null,
+ new Func(() => new ReflectedEventReplacer(reflectedEventReplacer)));
+ return true;
+ }
return false;
}
+ private static void ProcessReflectedMemberInfo(FieldInfo field, ReflectedMemberAttribute attr)
+ {
+ MemberInfo info = null;
+ if (attr.Type == null)
+ throw new ArgumentException("Reflected member info attributes require Type to be defined");
+ if (attr.Name == null)
+ throw new ArgumentException("Reflected member info attributes require Name to be defined");
+ switch (attr)
+ {
+ case ReflectedFieldInfoAttribute rfia:
+ info = GetFieldPropRecursive(rfia.Type, rfia.Name,
+ BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
+ (type, name, bindingFlags) => type.GetField(name, bindingFlags));
+ if (info == null)
+ throw new ArgumentException($"Unable to find field {rfia.Type.FullName}#{rfia.Name}");
+ break;
+ case ReflectedPropertyInfoAttribute rpia:
+ info = GetFieldPropRecursive(rpia.Type, rpia.Name,
+ BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
+ (type, name, bindingFlags) => type.GetProperty(name, bindingFlags));
+ if (info == null)
+ throw new ArgumentException($"Unable to find property {rpia.Type.FullName}#{rpia.Name}");
+ break;
+ case ReflectedMethodInfoAttribute rmia:
+ if (rmia.Parameters != null)
+ {
+ info = rmia.Type.GetMethod(rmia.Name,
+ BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ null, CallingConventions.Any, rmia.Parameters, null);
+ if (info == null)
+ throw new ArgumentException(
+ $"Unable to find method {rmia.Type.FullName}#{rmia.Name}({string.Join(", ", rmia.Parameters.Select(x => x.FullName))})");
+ }
+ else
+ {
+ info = rmia.Type.GetMethod(rmia.Name,
+ BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (info == null)
+ throw new ArgumentException(
+ $"Unable to find method {rmia.Type.FullName}#{rmia.Name}");
+ }
+ if (rmia.ReturnType != null && !rmia.ReturnType.IsAssignableFrom(((MethodInfo)info).ReturnType))
+ throw new ArgumentException($"Method {rmia.Type.FullName}#{rmia.Name} has return type {((MethodInfo)info).ReturnType.FullName}, expected {rmia.ReturnType.FullName}");
+ break;
+ }
+ if (info == null)
+ throw new ArgumentException($"Unable to find member info for {attr.GetType().Name}[{attr.Type.FullName}#{attr.Name}");
+ field.SetValue(null, info);
+ }
+
private static void ProcessReflectedMethod(FieldInfo field, ReflectedMethodAttribute attr)
{
MethodInfo delegateMethod = field.FieldType.GetMethod("Invoke");
@@ -307,7 +639,7 @@ namespace Torch.Utils
}
}
- private static T GetFieldPropRecursive(Type baseType, string name, BindingFlags flags, Func getter) where T : class
+ internal static T GetFieldPropRecursive(Type baseType, string name, BindingFlags flags, Func getter) where T : class
{
while (baseType != null)
{
@@ -372,7 +704,7 @@ namespace Torch.Utils
Expression instanceExpr = null;
if (!isStatic)
{
- instanceExpr = trueType == paramExp[0].Type ? (Expression) paramExp[0] : Expression.Convert(paramExp[0], trueType);
+ instanceExpr = trueType == paramExp[0].Type ? (Expression)paramExp[0] : Expression.Convert(paramExp[0], trueType);
}
MemberExpression fieldExp = sourceField != null
diff --git a/Torch/Utils/TorchAssemblyResolver.cs b/Torch/Utils/TorchAssemblyResolver.cs
index ca1f6bb..7b558bd 100644
--- a/Torch/Utils/TorchAssemblyResolver.cs
+++ b/Torch/Utils/TorchAssemblyResolver.cs
@@ -67,7 +67,7 @@ namespace Torch.Utils
string assemblyPath = Path.Combine(path, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
continue;
- _log.Debug("Loading {0} from {1}", assemblyName, SimplifyPath(assemblyPath));
+ _log.Trace("Loading {0} from {1}", assemblyName, SimplifyPath(assemblyPath));
LogManager.Flush();
Assembly asm = Assembly.LoadFrom(assemblyPath);
_assemblies.Add(assemblyName, asm);