Compare commits

..

1 Commits

Author SHA1 Message Date
Westin Miller
bd9d549607 Auto-release 2017-08-31 19:01:10 -07:00
158 changed files with 2182 additions and 10786 deletions

View File

@@ -1,9 +1,9 @@
# Making a Pull Request
* Fork this repository and make sure your local **staging** branch is up to date with the main repository.
* Create a new branch from the **staging** branch for your addition with an appropriate name, e.g. **add-restart-command**
* Fork this repository and make sure your local **master** branch is up to date with the main repository.
* Create a new branch for your addition with an appropriate name, e.g. **add-restart-command**
* PRs work by submitting the *entire* branch, so this allows you to continue work without locking up your whole repository.
* Commit your changes to that branch, making sure that you **follow the code guidelines below**.
* Submit your branch as a PR to be reviewed, with Torch's **staging** branch as the base.
* Submit your branch as a PR to be reviewed.
## Naming Conventions
* Types: **PascalCase**

View File

@@ -16,7 +16,7 @@ try
tag_name=$tagName
name="Generated $tagName"
body=""
draft=$TRUE
draft=$FALSE
prerelease=$tagName.Contains("alpha") -or $tagName.Contains("beta")
}
Write-Output("Creating new release " + $tagName + "...")

67
Jenkinsfile vendored
View File

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

View File

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

View File

@@ -3,24 +3,19 @@
# What is Torch?
Torch is the successor to SE Server Extender and gives server admins the tools they need to keep their Space Engineers servers running smoothly. It features a user interface with live management tools and a plugin system so you can run your server exactly how you'd like. Torch is still in early development so there may be bugs and incomplete features.
## Torch.Server
### Features
# Features
* WPF-based user interface
* Chat: interact with the game chat and run commands without having to join the game.
* Entity manager: realtime modification of ingame entities such as stopping grids and changing block settings without having to join the game
* Organized, easy to use configuration editor
* Extensible using the Torch plugin system
### Installation
# Installation
* Get the latest Torch release here: https://github.com/TorchAPI/Torch/releases
* Unzip the Torch release into its own directory and run the executable. It will automatically download the SE DS and generate the other necessary files.
- If you already have a DS installed you can unzip the Torch files into the folder that contains the DedicatedServer64 folder.
## Torch.Client
* An optional client-side version of Torch. More documentation to come.
# Building
To build Torch you must first have a complete SE Dedicated installation somewhere. Before you open the solution, run the Setup batch file and enter the path of that installation's DedicatedServer64 folder. The script will make a symlink to that folder so the Torch solution can find the DLL references it needs.

View File

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

View File

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

View File

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

View File

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

31
Torch.API/IChatMessage.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -121,12 +121,11 @@
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="Manager\MultiplayerManagerClient.cs" />
<Compile Include="Manager\MultiplayerManagerLobby.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TorchClient.cs" />
<Compile Include="TorchClientConfig.cs" />
<Compile Include="TorchConsoleScreen.cs" />
<Compile Include="TorchMainMenuScreen.cs" />
<Compile Include="TorchSettingsScreen.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\Resources.Designer.cs">
@@ -139,9 +138,6 @@
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="UI\TorchMainMenuScreen.cs" />
<Compile Include="UI\TorchNavScreen.cs" />
<Compile Include="UI\TorchSettingsScreen.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
@@ -171,7 +167,6 @@
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup>

View File

@@ -4,18 +4,12 @@ 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.Client.UI;
using Torch.Session;
using VRage;
using VRage.FileSystem;
using VRage.GameServices;
@@ -28,20 +22,11 @@ namespace Torch.Client
{
private MyCommonProgramStartup _startup;
private IMyRender _renderer;
protected override uint SteamAppId => 244850;
protected override string SteamAppName => "Space Engineers";
private const uint APP_ID = 244850;
public TorchClient()
{
Config = new TorchClientConfig();
var sessionManager = Managers.GetManager<ITorchSessionManager>();
sessionManager.AddFactory((x) => MyMultiplayer.Static is MyMultiplayerLobby
? new MultiplayerManagerLobby(this)
: null);
sessionManager.AddFactory((x) => MyMultiplayer.Static is MyMultiplayerClientBase
? new MultiplayerManagerClient(this)
: null);
}
public override void Init()
@@ -49,9 +34,10 @@ namespace Torch.Client
Directory.SetCurrentDirectory(Program.SpaceEngineersInstallAlias);
MyFileSystem.ExePath = Path.Combine(Program.SpaceEngineersInstallAlias, Program.SpaceEngineersBinaries);
Log.Info("Initializing Torch Client");
_startup = new MyCommonProgramStartup(RunArgs);
base.Init();
SpaceEngineersGame.SetupBasicGameInfo();
SpaceEngineersGame.SetupPerGameSettings();
_startup = new MyCommonProgramStartup(RunArgs);
if (_startup.PerformReporting())
throw new InvalidOperationException("Torch client won't launch when started in error reporting mode");
@@ -60,16 +46,27 @@ namespace Torch.Client
throw new InvalidOperationException("Only one instance of Space Engineers can be running at a time.");
var appDataPath = _startup.GetAppDataPath();
Config.InstancePath = appDataPath;
base.Init();
OverrideMenus();
SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}");
}
MyInitializer.InvokeBeforeRun(APP_ID, MyPerGameSettings.BasicGameInfo.ApplicationName, appDataPath);
MyInitializer.InitCheckSum();
_startup.InitSplashScreen();
if (!_startup.Check64Bit())
throw new InvalidOperationException("Torch requires a 64bit operating system");
public override void Dispose()
{
base.Dispose();
_startup.DetectSharpDxLeaksAfterRun();
_startup.DetectSharpDxLeaksBeforeRun();
var steamService = new SteamService(Game.IsDedicated, APP_ID);
MyServiceManager.Instance.AddService<IMyGameService>(steamService);
_renderer = null;
SpaceEngineersGame.SetupPerGameSettings();
// I'm sorry, but it's what Keen does in SpaceEngineers.MyProgram
#pragma warning disable 612
SpaceEngineersGame.SetupRender();
#pragma warning restore 612
InitializeRender();
if (!_startup.CheckSteamRunning())
throw new InvalidOperationException("Space Engineers requires steam to be running");
if (!Game.IsDedicated)
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
}
private void OverrideMenus()
@@ -86,6 +83,18 @@ namespace Torch.Client
MyPerGameSettings.GUI.MainMenu = typeof(TorchMainMenuScreen);
}
public override void Start()
{
using (var spaceEngineersGame = new SpaceEngineersGame(RunArgs))
{
Log.Info("Starting client");
OverrideMenus();
spaceEngineersGame.OnGameLoaded += SpaceEngineersGame_OnGameLoaded;
spaceEngineersGame.OnGameExit += Dispose;
spaceEngineersGame.Run(false, _startup.DisposeSplashScreen);
}
}
private void SetRenderWindowTitle(string title)
{
@@ -104,9 +113,58 @@ namespace Torch.Client
});
}
public override void Restart()
private void SpaceEngineersGame_OnGameLoaded(object sender, EventArgs e)
{
throw new NotImplementedException();
SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}");
}
public override void Dispose()
{
MyGameService.ShutDown();
_startup.DetectSharpDxLeaksAfterRun();
MyInitializer.InvokeAfterRun();
}
public override void Stop()
{
MySandboxGame.ExitThreadSafe();
}
private void InitializeRender()
{
try
{
if (Game.IsDedicated)
{
_renderer = new MyNullRender();
}
else
{
var graphicsRenderer = MySandboxGame.Config.GraphicsRenderer;
if (graphicsRenderer == MySandboxGame.DirectX11RendererKey)
{
_renderer = new MyDX11Render();
if (!_renderer.IsSupported)
{
MySandboxGame.Log.WriteLine("DirectX 11 renderer not supported. No renderer to revert back to.");
_renderer = null;
}
}
if (_renderer == null)
throw new MyRenderException("The current version of the game requires a Dx11 card. \\n For more information please see : http://blog.marekrosa.org/2016/02/space-engineers-news-full-source-code_26.html", MyRenderExceptionEnum.GpuNotSupported);
MySandboxGame.Config.GraphicsRenderer = graphicsRenderer;
}
MyRenderProxy.Initialize(_renderer);
MyRenderProxy.GetRenderProfiler().SetAutocommit(false);
MyRenderProxy.GetRenderProfiler().InitMemoryHack("MainEntryPoint");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Render Initialization Failed");
Environment.Exit(-1);
}
}
}
}

View File

@@ -0,0 +1,35 @@
#pragma warning disable 618
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Graphics;
using Sandbox.Graphics.GUI;
using Sandbox.Gui;
using SpaceEngineers.Game.GUI;
using VRage.Game;
using VRage.Utils;
using VRageMath;
namespace Torch.Client
{
public class TorchMainMenuScreen : MyGuiScreenMainMenu
{
/// <inheritdoc />
public override void RecreateControls(bool constructor)
{
base.RecreateControls(constructor);
var buttonSize = MyGuiControlButton.GetVisualStyle(MyGuiControlButtonStyleEnum.Default).NormalTexture.MinSizeGui;
Vector2 leftButtonPositionOrigin = MyGuiManager.ComputeFullscreenGuiCoordinate(MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM) + new Vector2(buttonSize.X / 2f, 0f);
var btn = MakeButton(leftButtonPositionOrigin - 9 * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA, MyStringId.GetOrCompute("Torch"), TorchButtonClicked);
Controls.Add(btn);
}
private void TorchButtonClicked(MyGuiControlButton obj)
{
MyGuiSandbox.AddScreen(new TorchSettingsScreen());
}
}
}

View File

@@ -1,56 +0,0 @@
#pragma warning disable 618
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Networking;
using Sandbox.Engine.Utils;
using Sandbox.Game;
using Sandbox.Game.Gui;
using Sandbox.Graphics;
using Sandbox.Graphics.GUI;
using Sandbox.Gui;
using SpaceEngineers.Game.GUI;
using Torch.Utils;
using VRage.Game;
using VRage.Utils;
using VRageMath;
namespace Torch.Client.UI
{
public class TorchMainMenuScreen : MyGuiScreenMainMenu
{
#pragma warning disable 169
[ReflectedGetter(Name = "m_elementGroup")]
private static Func<MyGuiScreenMainMenu, MyGuiControlElementGroup> _elementsGroup;
#pragma warning restore 169
public TorchMainMenuScreen() : this(false)
{
}
public TorchMainMenuScreen(bool pauseGame)
: base(pauseGame)
{
}
/// <inheritdoc />
public override void RecreateControls(bool constructor)
{
base.RecreateControls(constructor);
Vector2 minSizeGui = MyGuiControlButton.GetVisualStyle(MyGuiControlButtonStyleEnum.Default).NormalTexture.MinSizeGui;
Vector2 value = MyGuiManager.ComputeFullscreenGuiCoordinate(MyGuiDrawAlignEnum.HORISONTAL_LEFT_AND_VERTICAL_BOTTOM, 54, 54) + new Vector2(minSizeGui.X / 2f, 0f) + new Vector2(15f, 0f) / MyGuiConstants.GUI_OPTIMAL_SIZE;
MyGuiControlButton myGuiControlButton = MakeButton(value - 9 * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA,
MyStringId.GetOrCompute("Torch"), TorchButtonClicked, MyCommonTexts.ToolTipExitToWindows);
Controls.Add(myGuiControlButton);
_elementsGroup.Invoke(this).Add(myGuiControlButton);
}
private void TorchButtonClicked(MyGuiControlButton obj)
{
MyGuiSandbox.AddScreen(MyGuiSandbox.CreateScreen<TorchNavScreen>());
}
}
}

View File

@@ -1,49 +0,0 @@
using Sandbox;
using Sandbox.Game.Gui;
using Sandbox.Graphics.GUI;
using VRage;
using VRage.Game;
using VRage.Utils;
using VRageMath;
namespace Torch.Client.UI
{
public class TorchNavScreen : MyGuiScreenBase
{
private MyGuiControlElementGroup _elementGroup;
public TorchNavScreen() : base(new Vector2(0.5f, 0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR, new Vector2(0.35875f, 0.558333337f))
{
EnabledBackgroundFade = true;
RecreateControls(true);
}
public override void RecreateControls(bool constructor)
{
base.RecreateControls(constructor);
_elementGroup = new MyGuiControlElementGroup();
_elementGroup.HighlightChanged += ElementGroupHighlightChanged;
AddCaption(MyCommonTexts.ScreenCaptionOptions, null, null);
var value = new Vector2(0f, -m_size.Value.Y / 2f + 0.146f);
var num = 0;
var myGuiControlButton = new MyGuiControlButton(value + num++ * MyGuiConstants.MENU_BUTTONS_POSITION_DELTA, MyGuiControlButtonStyleEnum.Default, null, null, MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER, null, MyTexts.Get(MyCommonTexts.ScreenOptionsButtonGame), 0.8f, MyGuiDrawAlignEnum.HORISONTAL_CENTER_AND_VERTICAL_CENTER, MyGuiControlHighlightType.WHEN_ACTIVE, delegate(MyGuiControlButton sender)
{
MyGuiSandbox.AddScreen(MyGuiSandbox.CreateScreen<TorchSettingsScreen>());
}, GuiSounds.MouseClick, 1f, null);
Controls.Add(myGuiControlButton);
_elementGroup.Add(myGuiControlButton);
CloseButtonEnabled = true;
}
private void ElementGroupHighlightChanged(MyGuiControlElementGroup obj)
{
foreach (MyGuiControlBase current in _elementGroup)
if (current.HasFocus && obj.SelectedElement != current)
FocusedControl = obj.SelectedElement;
}
public override string GetFriendlyName() => "Torch";
public void OnBackClick(MyGuiControlButton sender) => CloseScreen();
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Graphics.GUI;
using VRageMath;
namespace Torch.Client.UI
{
public class TorchSettingsScreen : MyGuiScreenBase
{
public TorchSettingsScreen() : base(new Vector2(0.5f, 0.5f), MyGuiConstants.SCREEN_BACKGROUND_COLOR,
new Vector2(0.35875f, 0.558333337f))
{
EnabledBackgroundFade = true;
RecreateControls(true);
}
/// <inheritdoc />
public override string GetFriendlyName() => "Torch Settings";
public void OnBackClick(MyGuiControlButton sender) => CloseScreen();
}
}

View File

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

View File

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

View File

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

View File

@@ -32,13 +32,23 @@ namespace Torch.Server.Managers
{
}
/// <inheritdoc />
public override void Attach()
{
MyFileSystem.ExePath = Path.Combine(_filesystemManager.TorchDirectory, "DedicatedServer64");
MyFileSystem.Init("Content", Torch.Config.InstancePath);
//Initializes saves path. Why this isn't in Init() we may never know.
MyFileSystem.InitUserSpecific(null);
}
public void LoadInstance(string path, bool validate = true)
{
if (validate)
ValidateInstance(path);
MyFileSystem.Reset();
MyFileSystem.ExePath = Path.Combine(_filesystemManager.TorchDirectory, "DedicatedServer64");
MyFileSystem.Init("Content", path);
//Initializes saves path. Why this isn't in Init() we may never know.
MyFileSystem.InitUserSpecific(null);
@@ -121,7 +131,7 @@ namespace Torch.Server.Managers
public void SaveConfig()
{
DedicatedConfig.Save(Path.Combine(Torch.Config.InstancePath, CONFIG_NAME));
DedicatedConfig.Save();
Log.Info("Saved dedicated config.");
try

View File

@@ -1,234 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
using NLog.Fluent;
using Sandbox;
using Sandbox.Engine.Multiplayer;
using Sandbox.Engine.Networking;
using Torch.API;
using Torch.API.Managers;
using Torch.Managers;
using Torch.Utils;
using Torch.ViewModels;
using VRage.GameServices;
using VRage.Network;
using VRage.Steam;
namespace Torch.Server.Managers
{
public class MultiplayerManagerDedicated : MultiplayerManagerBase, IMultiplayerManagerServer
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
#pragma warning disable 649
[ReflectedGetter(Name = "m_members")] private static Func<MyDedicatedServerBase, List<ulong>> _members;
[ReflectedGetter(Name = "m_waitingForGroup")]
private static Func<MyDedicatedServerBase, HashSet<ulong>> _waitingForGroup;
#pragma warning restore 649
/// <inheritdoc />
public IReadOnlyList<ulong> BannedPlayers => MySandboxGame.ConfigDedicated.Banned;
private Dictionary<ulong, ulong> _gameOwnerIds = new Dictionary<ulong, ulong>();
/// <inheritdoc />
public MultiplayerManagerDedicated(ITorchBase torch) : base(torch)
{
}
/// <inheritdoc />
public void KickPlayer(ulong steamId) => Torch.Invoke(() => MyMultiplayer.Static.KickClient(steamId));
/// <inheritdoc />
public void BanPlayer(ulong steamId, bool banned = true)
{
Torch.Invoke(() =>
{
MyMultiplayer.Static.BanClient(steamId, banned);
if (_gameOwnerIds.ContainsKey(steamId))
MyMultiplayer.Static.BanClient(_gameOwnerIds[steamId], banned);
});
}
/// <inheritdoc />
public bool IsBanned(ulong steamId) => _isClientBanned.Invoke(MyMultiplayer.Static, steamId) ||
MySandboxGame.ConfigDedicated.Banned.Contains(steamId);
/// <inheritdoc/>
public override void Attach()
{
base.Attach();
_gameServerValidateAuthTicketReplacer = _gameServerValidateAuthTicketFactory.Invoke();
_gameServerUserGroupStatusReplacer = _gameServerUserGroupStatusFactory.Invoke();
_gameServerValidateAuthTicketReplacer.Replace(
new Action<ulong, JoinResult, ulong>(ValidateAuthTicketResponse), MyGameService.GameServer);
_gameServerUserGroupStatusReplacer.Replace(new Action<ulong, ulong, bool, bool>(UserGroupStatusResponse),
MyGameService.GameServer);
_log.Info("Inserted steam authentication intercept");
}
/// <inheritdoc/>
public override void Detach()
{
if (_gameServerValidateAuthTicketReplacer != null && _gameServerValidateAuthTicketReplacer.Replaced)
_gameServerValidateAuthTicketReplacer.Restore(MyGameService.GameServer);
if (_gameServerUserGroupStatusReplacer != null && _gameServerUserGroupStatusReplacer.Replaced)
_gameServerUserGroupStatusReplacer.Restore(MyGameService.GameServer);
_log.Info("Removed steam authentication intercept");
base.Detach();
}
#pragma warning disable 649
[ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.ValidateAuthTicketResponse),
typeof(MyDedicatedServerBase), "GameServer_ValidateAuthTicketResponse")]
private static Func<ReflectedEventReplacer> _gameServerValidateAuthTicketFactory;
[ReflectedEventReplace(typeof(MySteamGameServer), nameof(MySteamGameServer.UserGroupStatusResponse),
typeof(MyDedicatedServerBase), "GameServer_UserGroupStatus")]
private static Func<ReflectedEventReplacer> _gameServerUserGroupStatusFactory;
private ReflectedEventReplacer _gameServerValidateAuthTicketReplacer;
private ReflectedEventReplacer _gameServerUserGroupStatusReplacer;
#pragma warning restore 649
#region CustomAuth
#pragma warning disable 649
[ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase), Name = "ConvertSteamIDFrom64")]
private static Func<ulong, string> _convertSteamIDFrom64;
[ReflectedStaticMethod(Type = typeof(MyGameService), Name = "GetServerAccountType")]
private static Func<ulong, MyGameServiceAccountType> _getServerAccountType;
[ReflectedMethod(Name = "UserAccepted")] private static Action<MyDedicatedServerBase, ulong> _userAcceptedImpl;
[ReflectedMethod(Name = "UserRejected")]
private static Action<MyDedicatedServerBase, ulong, JoinResult> _userRejected;
[ReflectedMethod(Name = "IsClientBanned")] private static Func<MyMultiplayerBase, ulong, bool> _isClientBanned;
[ReflectedMethod(Name = "IsClientKicked")] private static Func<MyMultiplayerBase, ulong, bool> _isClientKicked;
[ReflectedMethod(Name = "RaiseClientKicked")]
private static Action<MyMultiplayerBase, ulong> _raiseClientKicked;
#pragma warning restore 649
private const int _waitListSize = 32;
private readonly List<WaitingForGroup> _waitingForGroupLocal = new List<WaitingForGroup>(_waitListSize);
private struct WaitingForGroup
{
public readonly ulong SteamId;
public readonly JoinResult Response;
public readonly ulong SteamOwner;
public WaitingForGroup(ulong id, JoinResult response, ulong owner)
{
SteamId = id;
Response = response;
SteamOwner = owner;
}
}
//Largely copied from SE
private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner)
{
_log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}");
if (MySandboxGame.ConfigDedicated.GroupID == 0uL)
RunEvent(new ValidateAuthTicketEvent(steamID, steamOwner, response, 0, true, false));
else if (_getServerAccountType(MySandboxGame.ConfigDedicated.GroupID) != MyGameServiceAccountType.Clan)
UserRejected(steamID, JoinResult.GroupIdInvalid);
else if (MyGameService.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
lock (_waitingForGroupLocal)
{
if (_waitingForGroupLocal.Count >= _waitListSize)
_waitingForGroupLocal.RemoveAt(0);
_waitingForGroupLocal.Add(new WaitingForGroup(steamID, response, steamOwner));
}
else
UserRejected(steamID, JoinResult.SteamServersOffline);
}
private void RunEvent(ValidateAuthTicketEvent info)
{
MultiplayerManagerDedicatedEventShim.RaiseValidateAuthTicket(ref info);
if (info.FutureVerdict == null)
{
if (IsBanned(info.SteamOwner) || IsBanned(info.SteamID))
CommitVerdict(info.SteamID, JoinResult.BannedByAdmins);
else if (_isClientKicked(MyMultiplayer.Static, info.SteamID) ||
_isClientKicked(MyMultiplayer.Static, info.SteamOwner))
CommitVerdict(info.SteamID, JoinResult.KickedRecently);
else if (info.SteamResponse != JoinResult.OK)
CommitVerdict(info.SteamID, info.SteamResponse);
else if (MyMultiplayer.Static.MemberLimit > 0 &&
MyMultiplayer.Static.MemberCount + 1 > MyMultiplayer.Static.MemberLimit)
CommitVerdict(info.SteamID, JoinResult.ServerFull);
else if (MySandboxGame.ConfigDedicated.GroupID == 0uL ||
MySandboxGame.ConfigDedicated.Administrators.Contains(info.SteamID.ToString()) ||
MySandboxGame.ConfigDedicated.Administrators.Contains(_convertSteamIDFrom64(info.SteamID)))
CommitVerdict(info.SteamID, JoinResult.OK);
else if (MySandboxGame.ConfigDedicated.GroupID == info.Group && (info.Member || info.Officer))
CommitVerdict(info.SteamID, JoinResult.OK);
else
CommitVerdict(info.SteamID, JoinResult.NotInGroup);
return;
}
info.FutureVerdict.ContinueWith((task) =>
{
JoinResult verdict;
if (task.IsFaulted)
{
_log.Error(task.Exception, $"Future validation verdict faulted");
verdict = JoinResult.TicketCanceled;
}
else
verdict = task.Result;
Torch.Invoke(() => { CommitVerdict(info.SteamID, verdict); });
});
}
private void CommitVerdict(ulong steamId, JoinResult verdict)
{
if (verdict == JoinResult.OK)
UserAccepted(steamId);
else
UserRejected(steamId, verdict);
}
private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer)
{
lock (_waitingForGroupLocal)
for (var j = 0; j < _waitingForGroupLocal.Count; j++)
{
var wait = _waitingForGroupLocal[j];
if (wait.SteamId == userId)
{
RunEvent(new ValidateAuthTicketEvent(wait.SteamId, wait.SteamOwner, wait.Response, groupId,
member, officer));
_waitingForGroupLocal.RemoveAt(j);
break;
}
}
}
private void UserRejected(ulong steamId, JoinResult reason)
{
_userRejected.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId, reason);
}
private void UserAccepted(ulong steamId)
{
_userAcceptedImpl.Invoke((MyDedicatedServerBase) MyMultiplayer.Static, steamId);
base.RaiseClientJoined(steamId);
}
#endregion
}
}

View File

@@ -1,84 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox;
using Torch.API.Event;
using Torch.Event;
using VRage.Network;
namespace Torch.Server.Managers
{
[EventShim]
internal static class MultiplayerManagerDedicatedEventShim
{
private static readonly EventList<ValidateAuthTicketEvent> _eventValidateAuthTicket =
new EventList<ValidateAuthTicketEvent>();
internal static void RaiseValidateAuthTicket(ref ValidateAuthTicketEvent info)
{
_eventValidateAuthTicket?.RaiseEvent(ref info);
}
}
/// <summary>
/// Event that occurs when a player tries to connect to a dedicated server.
/// Use these values to choose a <see cref="ValidateAuthTicketEvent.FutureVerdict"/>,
/// or leave it unset to allow the default logic to handle the request.
/// </summary>
public struct ValidateAuthTicketEvent : IEvent
{
/// <summary>
/// SteamID of the player
/// </summary>
public readonly ulong SteamID;
/// <summary>
/// SteamID of the game owner
/// </summary>
public readonly ulong SteamOwner;
/// <summary>
/// The response from steam
/// </summary>
public readonly JoinResult SteamResponse;
/// <summary>
/// ID of the queried group, or <c>0</c> if no group.
/// </summary>
public readonly ulong Group;
/// <summary>
/// Is this person a member of <see cref="Group"/>. If no group this is true.
/// </summary>
public readonly bool Member;
/// <summary>
/// Is this person an officer of <see cref="Group"/>. If no group this is false.
/// </summary>
public readonly bool Officer;
/// <summary>
/// A future verdict on this authorization request. If null, let the default logic choose. If not async use <see cref="Task.FromResult{TResult}(TResult)"/>
/// </summary>
public Task<JoinResult> FutureVerdict;
internal ValidateAuthTicketEvent(ulong steamId, ulong steamOwner, JoinResult steamResponse,
ulong serverGroup, bool member, bool officer)
{
SteamID = steamId;
SteamOwner = steamOwner;
SteamResponse = steamResponse;
Group = serverGroup;
Member = member;
Officer = officer;
FutureVerdict = null;
}
/// <inheritdoc/>
public bool Cancelled => FutureVerdict != null;
}
}

View File

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

View File

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

View File

@@ -3,25 +3,20 @@ using Sandbox.Engine.Utils;
using Sandbox.Game;
using Sandbox.Game.World;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xml.Serialization.GeneratedAssembly;
using NLog;
using Sandbox.Engine.Analytics;
using Sandbox.Game.Multiplayer;
using Sandbox.ModAPI;
using SteamSDK;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers;
using Torch.Server.Managers;
using Torch.Utils;
@@ -43,59 +38,14 @@ namespace Torch.Server
public class TorchServer : TorchBase, ITorchServer
{
//public MyConfigDedicated<MyObjectBuilder_SessionSettings> DedicatedConfig { get; set; }
/// <inheritdoc />
public float SimulationRatio
{
get => _simRatio;
set
{
_simRatio = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public TimeSpan ElapsedPlayTime
{
get => _elapsedPlayTime;
set
{
_elapsedPlayTime = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public float SimulationRatio { get => _simRatio; set { _simRatio = value; OnPropertyChanged(); } }
public TimeSpan ElapsedPlayTime { get => _elapsedPlayTime; set { _elapsedPlayTime = value; OnPropertyChanged(); } }
public Thread GameThread { get; private set; }
/// <inheritdoc />
public ServerState State
{
get => _state;
private set
{
_state = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public bool IsRunning
{
get => _isRunning;
set
{
_isRunning = value;
OnPropertyChanged();
}
}
/// <inheritdoc />
public ServerState State { get => _state; private set { _state = value; OnPropertyChanged(); } }
public bool IsRunning { get => _isRunning; set { _isRunning = value; OnPropertyChanged(); } }
public InstanceManager DedicatedInstance { get; }
/// <inheritdoc />
public string InstanceName => Config?.InstanceName;
/// <inheritdoc />
public string InstancePath => Config?.InstancePath;
@@ -103,107 +53,120 @@ namespace Torch.Server
private ServerState _state;
private TimeSpan _elapsedPlayTime;
private float _simRatio;
private readonly AutoResetEvent _stopHandle = new AutoResetEvent(false);
private Timer _watchdog;
private Stopwatch _uptime;
/// <inheritdoc />
public TorchServer(TorchConfig config = null)
{
DedicatedInstance = new InstanceManager(this);
AddManager(DedicatedInstance);
AddManager(new EntityControlManager(this));
Config = config ?? new TorchConfig();
var sessionManager = Managers.GetManager<ITorchSessionManager>();
sessionManager.AddFactory((x) => new MultiplayerManagerDedicated(this));
MyFakes.ENABLE_INFINARIO = false;
}
/// <inheritdoc/>
protected override uint SteamAppId => 244850;
/// <inheritdoc/>
protected override string SteamAppName => "SpaceEngineersDedicated";
/// <inheritdoc />
public override void Init()
{
Log.Info($"Init server '{Config.InstanceName}' at '{Config.InstancePath}'");
Sandbox.Engine.Platform.Game.IsDedicated = true;
base.Init();
Managers.GetManager<ITorchSessionManager>().SessionStateChanged += OnSessionStateChanged;
MyPerGameSettings.SendLogToKeen = false;
MyPerServerSettings.GameName = MyPerGameSettings.GameName;
MyPerServerSettings.GameNameSafe = MyPerGameSettings.GameNameSafe;
MyPerServerSettings.GameDSName = MyPerServerSettings.GameNameSafe + "Dedicated";
MyPerServerSettings.GameDSDescription = "Your place for space engineering, destruction and exploring.";
MySessionComponentExtDebug.ForceDisable = true;
MyPerServerSettings.AppId = 244850;
MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion;
InvokeBeforeRun();
//MyObjectBuilderSerializer.RegisterFromAssembly(typeof(MyObjectBuilder_CheckpointSerializer).Assembly);
MyPlugins.RegisterGameAssemblyFile(MyPerGameSettings.GameModAssembly);
MyPlugins.RegisterGameObjectBuildersAssemblyFile(MyPerGameSettings.GameModObjBuildersAssembly);
MyPlugins.RegisterSandboxAssemblyFile(MyPerGameSettings.SandboxAssembly);
MyPlugins.RegisterSandboxGameAssemblyFile(MyPerGameSettings.SandboxGameAssembly);
MyPlugins.Load();
MyGlobalTypeMetadata.Static.Init();
GetManager<InstanceManager>().LoadInstance(Config.InstancePath);
Plugins.LoadPlugins();
}
private void OnSessionStateChanged(ITorchSession session, TorchSessionState newState)
private void InvokeBeforeRun()
{
if (newState == TorchSessionState.Unloading || newState == TorchSessionState.Unloaded)
{
_watchdog?.Dispose();
_watchdog = null;
}
MySandboxGame.Log.Init("SpaceEngineers-Dedicated.log", MyFinalBuildConstants.APP_VERSION_STRING);
MySandboxGame.Log.WriteLine("Steam build: Always true");
MySandboxGame.Log.WriteLine("Environment.ProcessorCount: " + MyEnvironment.ProcessorCount);
//MySandboxGame.Log.WriteLine("Environment.OSVersion: " + GetOsName());
MySandboxGame.Log.WriteLine("Environment.CommandLine: " + Environment.CommandLine);
MySandboxGame.Log.WriteLine("Environment.Is64BitProcess: " + MyEnvironment.Is64BitProcess);
MySandboxGame.Log.WriteLine("Environment.Is64BitOperatingSystem: " + Environment.Is64BitOperatingSystem);
//MySandboxGame.Log.WriteLine("Environment.Version: " + GetNETFromRegistry());
MySandboxGame.Log.WriteLine("Environment.CurrentDirectory: " + Environment.CurrentDirectory);
MySandboxGame.Log.WriteLine("MainAssembly.ProcessorArchitecture: " + Assembly.GetExecutingAssembly().GetArchitecture());
MySandboxGame.Log.WriteLine("ExecutingAssembly.ProcessorArchitecture: " + MyFileSystem.MainAssembly.GetArchitecture());
MySandboxGame.Log.WriteLine("IntPtr.Size: " + IntPtr.Size);
MySandboxGame.Log.WriteLine("Default Culture: " + CultureInfo.CurrentCulture.Name);
MySandboxGame.Log.WriteLine("Default UI Culture: " + CultureInfo.CurrentUICulture.Name);
MySandboxGame.Log.WriteLine("IsAdmin: " + new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator));
MyLog.Default = MySandboxGame.Log;
Thread.CurrentThread.Name = "Main thread";
//Because we want exceptions from users to be in english
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
MySandboxGame.Config = new MyConfig("SpaceEngineers-Dedicated.cfg");
MySandboxGame.Config.Load();
}
[ReflectedStaticMethod(Type = typeof(DedicatedServer), Name = "RunInternal")]
private static Action _dsRunInternal;
/// <inheritdoc />
public override void Start()
{
if (State != ServerState.Stopped)
return;
State = ServerState.Starting;
IsRunning = true;
Log.Info("Starting server.");
MySandboxGame.ConfigDedicated = DedicatedInstance.DedicatedConfig.Model;
DedicatedInstance.SaveConfig();
_uptime = Stopwatch.StartNew();
IsRunning = true;
GameThread = Thread.CurrentThread;
State = ServerState.Starting;
Log.Info("Starting server.");
MySandboxGame.IsDedicated = true;
Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString());
VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); };
base.Start();
}
/// <inheritdoc />
public override void Stop()
{
if (State == ServerState.Stopped)
Log.Error("Server is already stopped");
Log.Info("Stopping server.");
base.Stop();
Log.Info("Server stopped.");
// Stops RunInternal from calling MyFileSystem.InitUserSpecific(null), we call it in InstanceManager.
MySandboxGame.IsReloading = true;
try
{
_dsRunInternal.Invoke();
}
catch (TargetInvocationException e)
{
// Makes log formatting a little nicer.
throw e.InnerException ?? e;
}
MySandboxGame.Log.Close();
State = ServerState.Stopped;
IsRunning = false;
}
/// <summary>
/// Restart the program.
/// </summary>
public override void Restart()
{
Save(0).Wait();
Stop();
LogManager.Flush();
var exe = Assembly.GetExecutingAssembly().Location;
((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString();
Config.Autostart = true;
Process.Start(exe, Config.ToString());
Process.GetCurrentProcess().Kill();
}
/// <inheritdoc />
public override void Init(object gameInstance)
{
base.Init(gameInstance);
var game = gameInstance as MySandboxGame;
if (game != null && MySession.Static != null)
{
State = ServerState.Running;
// SteamServerAPI.Instance.GameServer.SetKeyValue("SM", "Torch");
}
else
{
State = ServerState.Stopped;
}
State = ServerState.Running;
SteamServerAPI.Instance.GameServer.SetKeyValue("SM", "Torch");
}
/// <inheritdoc />
@@ -217,85 +180,62 @@ namespace Torch.Server
if (_watchdog == null && Config.TickTimeout > 0)
{
Log.Info("Starting server watchdog.");
_watchdog = new Timer(CheckServerResponding, this, TimeSpan.Zero,
TimeSpan.FromSeconds(Config.TickTimeout));
_watchdog = new Timer(CheckServerResponding, this, TimeSpan.Zero, TimeSpan.FromSeconds(Config.TickTimeout));
}
}
#region Freeze Detection
private static void CheckServerResponding(object state)
{
var mre = new ManualResetEvent(false);
((TorchServer) state).Invoke(() => mre.Set());
((TorchServer)state).Invoke(() => mre.Set());
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
{
#if DEBUG
Log.Error(
$"Server watchdog detected that the server was frozen for at least {((TorchServer) state).Config.TickTimeout} seconds.");
Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
#else
Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
#endif
}
else
{
Log.Debug("Server watchdog responded");
var mainThread = MySandboxGame.Static.UpdateThread;
if (mainThread.IsAlive)
mainThread.Suspend();
var stackTrace = new StackTrace(mainThread, true);
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.\n{stackTrace}");
}
Log.Debug("Server watchdog responded");
}
private static string DumpFrozenThread(Thread thread, int traces = 3, int pause = 5000)
/// <inheritdoc />
public override void Stop()
{
var stacks = new List<string>(traces);
var totalSize = 0;
for (var i = 0; i < traces; i++)
{
string dump = DumpStack(thread).ToString();
totalSize += dump.Length;
stacks.Add(dump);
Thread.Sleep(pause);
}
string commonPrefix = StringUtils.CommonSuffix(stacks);
// Advance prefix to include the line terminator.
commonPrefix = commonPrefix.Substring(commonPrefix.IndexOf('\n') + 1);
if (State == ServerState.Stopped)
Log.Error("Server is already stopped");
var result = new StringBuilder(totalSize - (stacks.Count - 1) * commonPrefix.Length);
result.AppendLine($"Frozen thread dump {thread.Name}");
result.AppendLine("Common prefix:").AppendLine(commonPrefix);
for (var i = 0; i < stacks.Count; i++)
if (stacks[i].Length > commonPrefix.Length)
{
result.AppendLine($"Suffix {i}");
result.AppendLine(stacks[i].Substring(0, stacks[i].Length - commonPrefix.Length));
}
return result.ToString();
if (Thread.CurrentThread != MySandboxGame.Static.UpdateThread)
{
Log.Debug("Invoking server stop on game thread.");
Invoke(Stop);
return;
}
Log.Info("Stopping server.");
//Unload all the static junk.
//TODO: Finish unloading all server data so it's in a completely clean state.
MySandboxGame.Static.Exit();
Log.Info("Server stopped.");
_stopHandle.Set();
State = ServerState.Stopped;
IsRunning = false;
}
private static StackTrace DumpStack(Thread thread)
/// <summary>
/// Restart the program. DOES NOT SAVE!
/// </summary>
public override void Restart()
{
try
{
thread.Suspend();
}
catch
{
// ignored
}
var stack = new StackTrace(thread, true);
try
{
thread.Resume();
}
catch
{
// ignored
}
return stack;
var exe = Assembly.GetExecutingAssembly().Location;
((TorchConfig)Config).WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, Config.ToString());
Environment.Exit(0);
}
#endregion
/// <inheritdoc/>
public override Task Save(long callerId)
{
@@ -309,33 +249,27 @@ namespace Torch.Server
/// <param name="callerId">Caller of the save operation</param>
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
{
string response = null;
switch (statusCode)
{
case SaveGameStatus.Success:
Log.Info("Save completed.");
response = "Saved game.";
Multiplayer.SendMessage("Saved game.", playerId: callerId);
break;
case SaveGameStatus.SaveInProgress:
Log.Error("Save failed, a save is already in progress.");
response = "Save failed, a save is already in progress.";
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.");
response = "Save failed, game was not ready.";
Multiplayer.SendMessage("Save failed, game was not ready.", playerId: callerId, font: MyFontEnum.Red);
break;
case SaveGameStatus.TimedOut:
Log.Error("Save failed, save timed out.");
response = "Save failed, save timed out.";
Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red);
break;
default:
break;
}
if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result))
{
Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther("Server", response,
statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId);
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,20 @@
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer x:Name="ChatScroller" Grid.Row="0" Margin="5,5,5,5" HorizontalScrollBarVisibility="Disabled">
<TextBlock x:Name="ChatItems" />
</ScrollViewer>
<ListView Grid.Row="0" x:Name="ChatItems" ItemsSource="{Binding ChatHistory}" Margin="5,5,5,5">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"/>
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Timestamp}"/>
<TextBlock Text=" "/>
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text=": "/>
<TextBlock Text="{Binding Message}"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition/>

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
<UserControl x:Class="Torch.Server.Views.Entities.EntityControlHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
</UserControl>

View File

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

View File

@@ -1,31 +0,0 @@
<ItemsControl x:Class="Torch.Server.Views.Entities.EntityControlsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:entities="clr-namespace:Torch.Server.Views.Entities"
xmlns:modelsEntities="clr-namespace:Torch.Server.ViewModels.Entities"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding EntityControls}">
<ItemsControl.DataContext>
<modelsEntities:EntityViewModel/>
</ItemsControl.DataContext>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<entities:EntityControlHost DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Control.HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Control.VerticalContentAlignment" Value="Stretch"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ namespace Torch.Server
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
_server.GetManager<InstanceManager>().SaveConfig();
_server.Start();
new Thread(_server.Start).Start();
}
private void BtnStop_Click(object sender, RoutedEventArgs e)

View File

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

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Torch.Utils;
using Xunit;
@@ -12,7 +11,7 @@ namespace Torch.Tests
{
TestUtils.Init();
}
private static ReflectionTestManager _manager = new ReflectionTestManager().Init(typeof(ReflectionTestBinding));
public static IEnumerable<object[]> Getters => _manager.Getters;
@@ -20,10 +19,6 @@ namespace Torch.Tests
public static IEnumerable<object[]> Invokers => _manager.Invokers;
public static IEnumerable<object[]> MemberInfo => _manager.MemberInfo;
public static IEnumerable<object[]> Events => _manager.Events;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
@@ -57,28 +52,6 @@ namespace Torch.Tests
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(MemberInfo))]
public void TestBindingMemberInfo(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion
#region Results
@@ -106,51 +79,10 @@ namespace Torch.Tests
{
return k >= 0;
}
public event Action Event1;
public ReflectionTestTarget()
{
Event1 += Callback1;
}
public bool Callback1Flag = false;
public void Callback1()
{
Callback1Flag = true;
}
public bool Callback2Flag = false;
public void Callback2()
{
Callback2Flag = true;
}
public void RaiseEvent()
{
Event1?.Invoke();
}
}
private class ReflectionTestBinding
{
#region Instance
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestField")]
public static FieldInfo TestFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestProperty")]
public static PropertyInfo TestPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall")]
public static MethodInfo TestMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", Parameters = new[] { typeof(int) })]
public static MethodInfo TestMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", ReturnType = typeof(bool))]
public static MethodInfo TestMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestField")]
public static Func<ReflectionTestTarget, int> TestFieldGetter;
[ReflectedSetter(Name = "TestField")]
@@ -164,27 +96,7 @@ namespace Torch.Tests
[ReflectedMethod]
public static Func<ReflectionTestTarget, int, bool> TestCall;
[ReflectedEventReplace(typeof(ReflectionTestTarget), "Event1", typeof(ReflectionTestTarget), "Callback1")]
public static Func<ReflectedEventReplacer> TestEventReplacer;
#endregion
#region Static
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestFieldStatic")]
public static FieldInfo TestStaticFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestPropertyStatic")]
public static PropertyInfo TestStaticPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic")]
public static MethodInfo TestStaticMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", Parameters = new[] { typeof(int) })]
public static MethodInfo TestStaticMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", ReturnType = typeof(bool))]
public static MethodInfo TestStaticMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
public static Func<int> TestStaticFieldGetter;
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
@@ -197,7 +109,6 @@ namespace Torch.Tests
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
public static Func<int, bool> TestCallStatic;
#endregion
}
#endregion
@@ -304,33 +215,7 @@ 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
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using Torch.Utils;
@@ -29,29 +28,18 @@ namespace Torch.Tests
private readonly HashSet<object[]> _getters = new HashSet<object[]>();
private readonly HashSet<object[]> _setters = new HashSet<object[]>();
private readonly HashSet<object[]> _invokers = new HashSet<object[]>();
private readonly HashSet<object[]> _memberInfo = new HashSet<object[]>();
private readonly HashSet<object[]> _events = new HashSet<object[]>();
public ReflectionTestManager()
{
_getters.Add(new object[] { new FieldRef(null) });
_setters.Add(new object[] { new FieldRef(null) });
_invokers.Add(new object[] { new FieldRef(null) });
_memberInfo.Add(new object[] {new FieldRef(null)});
_events.Add(new object[] {new FieldRef(null)});
}
public ReflectionTestManager Init(Assembly asm)
{
try
{
foreach (Type type in asm.GetTypes())
Init(type);
}
catch (ReflectionTypeLoadException e)
{
throw e.LoaderExceptions[0];
}
foreach (Type type in asm.GetTypes())
Init(type);
return this;
}
@@ -62,36 +50,12 @@ namespace Torch.Tests
BindingFlags.Public |
BindingFlags.NonPublic))
{
var args = new object[] { new FieldRef(field) };
foreach (ReflectedMemberAttribute attr in field.GetCustomAttributes<ReflectedMemberAttribute>())
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
switch (attr)
{
case ReflectedMethodAttribute rma:
_invokers.Add(args);
break;
case ReflectedGetterAttribute rga:
_getters.Add(args);
break;
case ReflectedSetterAttribute rsa:
_setters.Add(args);
break;
case ReflectedFieldInfoAttribute rfia:
case ReflectedPropertyInfoAttribute rpia:
case ReflectedMethodInfoAttribute rmia:
_memberInfo.Add(args);
break;
}
}
var reflectedEventReplacer = field.GetCustomAttribute<ReflectedEventReplaceAttribute>();
if (reflectedEventReplacer != null)
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
_events.Add(args);
}
if (field.GetCustomAttribute<ReflectedMethodAttribute>() != null)
_invokers.Add(new object[] { new FieldRef(field) });
if (field.GetCustomAttribute<ReflectedGetterAttribute>() != null)
_getters.Add(new object[] { new FieldRef(field) });
if (field.GetCustomAttribute<ReflectedSetterAttribute>() != null)
_setters.Add(new object[] { new FieldRef(field) });
}
return this;
}
@@ -102,10 +66,6 @@ namespace Torch.Tests
public IEnumerable<object[]> Invokers => _invokers;
public IEnumerable<object[]> MemberInfo => _memberInfo;
public IEnumerable<object[]> Events => _events;
#endregion
}

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Torch.Utils;
@@ -34,4 +32,4 @@ namespace Torch.Tests
private static TorchAssemblyResolver _torchResolver;
}
}
}

View File

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

View File

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

37
Torch/ChatMessage.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -117,7 +117,7 @@ namespace Torch.Commands
catch (Exception e)
{
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;
}

View File

@@ -2,7 +2,6 @@
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;
@@ -48,7 +47,7 @@ namespace Torch.Commands
Response = message;
if (Player != null)
Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
Torch.Multiplayer.SendMessage(message, sender, Player.IdentityId, font);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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