Compare commits
1 Commits
v1.3.0
...
v1.0.0-alp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd9d549607 |
@@ -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**
|
||||
|
@@ -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
67
Jenkinsfile
vendored
@@ -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\")"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
NLog.config
14
NLog.config
@@ -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>
|
@@ -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.
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
@@ -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
31
Torch.API/IChatMessage.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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" );
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
61
Torch.API/Managers/IMultiplayerManager.cs
Normal file
61
Torch.API/Managers/IMultiplayerManager.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
@@ -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" />
|
||||
|
@@ -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);
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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()
|
||||
@@ -87,6 +84,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)
|
||||
{
|
||||
MyRenderThread renderThread = MySandboxGame.Static?.GameRenderComponent?.RenderThread;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
Torch.Client/TorchMainMenuScreen.cs
Normal file
35
Torch.Client/TorchMainMenuScreen.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -33,12 +33,22 @@ 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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
|
@@ -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}";
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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()
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
@@ -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/>
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Torch.Utils;
|
||||
using Xunit;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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" />
|
||||
|
@@ -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
37
Torch/ChatMessage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
63
Torch/Collections/ObservableDictionary.cs
Normal file
63
Torch/Collections/ObservableDictionary.cs
Normal 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;
|
||||
}
|
||||
}
|
186
Torch/Collections/ObservableList.cs
Normal file
186
Torch/Collections/ObservableList.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -117,7 +117,7 @@ namespace Torch.Commands
|
||||
catch (Exception e)
|
||||
{
|
||||
context.Respond(e.Message, "Error", MyFontEnum.Red);
|
||||
Log.Error($"Command '{SyntaxHelp}' from '{Plugin?.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}");
|
||||
Log.Error($"Command '{SyntaxHelp}' from '{Plugin.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}");
|
||||
Log.Error(e);
|
||||
return true;
|
||||
}
|
||||
|
@@ -2,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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
104
Torch/Managers/ChatManager.cs
Normal file
104
Torch/Managers/ChatManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
338
Torch/Managers/MultiplayerManager.cs
Normal file
338
Torch/Managers/MultiplayerManager.cs
Normal 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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
Reference in New Issue
Block a user