Compare commits

...

71 Commits

Author SHA1 Message Date
Westin Miller
bd9d549607 Auto-release 2017-08-31 19:01:10 -07:00
John Gross
3d6806b63a Merge pull request #88 from szczepix/patch-1
When path contains spaces
2017-08-31 18:01:00 -04:00
Szczepan Zaskalski
22e3019610 Fixed batch mklink command argument 2017-08-31 22:25:10 +02:00
Szczepan Zaskalski
7937cbd122 When path contains spaces 2017-08-31 22:18:46 +02:00
John Gross
2bef34ee5b Merge pull request #94 from TorchAPI/jenkins
Unified versions.
2017-08-31 01:37:24 -04:00
Westin Miller
efe7236d31 TorchVersion is read-only 2017-08-30 22:35:50 -07:00
Westin Miller
67dba9c820 PluginAttribute now lets you get the version from the loaded assembly
Documentation in PluginAttribute
2017-08-30 14:13:49 -07:00
Westin Miller
52c509aba0 Salt after revision. 2017-08-30 13:38:43 -07:00
Westin Miller
437c7d293e Make local development versions clearly local dev 2017-08-30 11:19:13 -07:00
Westin Miller
2b6ce4f25b Unified versions. 2017-08-29 19:54:06 -07:00
John Gross
599a98bceb Merge pull request #87 from TorchAPI/server-refactor
Server initialization refactor
2017-08-24 23:12:02 -04:00
John Gross
2cd1b8bd4e Merge branch 'staging' into server-refactor
# Conflicts:
#	Torch.Server/Torch.Server.csproj
#	Torch/TorchBase.cs
2017-08-24 17:44:43 -07:00
John Gross
c0be9c25da Refactor server initialization for service support 2017-08-24 17:30:38 -07:00
John Gross
5cea66374f Merge pull request #84 from TorchAPI/jenkins
Generate Assembly Versions on Build
2017-08-23 16:44:29 -04:00
Westin Miller
ee1c270c68 Merge branch 'staging' into jenkins 2017-08-23 03:02:26 -07:00
Westin Miller
4ab08e2faf Automatically create zip file 2017-08-23 01:17:19 -07:00
Westin Miller
dd094edb88 Using UTC time 2017-08-22 23:25:18 -07:00
Westin Miller
56e45236d8 Create empty generated files to MSBuild doesn't freak. 2017-08-22 23:03:44 -07:00
Westin Miller
be9a8c5839 Transform Templates on Build
Templates for all assemblies
2017-08-22 22:31:24 -07:00
John Gross
8c11baf3b9 Merge pull request #81 from TorchAPI/sessions-manager-71
Session Management System
2017-08-22 11:09:04 -04:00
Westin Miller
6ce679bd83 Proper delegate naming 2017-08-22 08:06:30 -07:00
Westin Miller
a4b1b9bb96 CurrentSession available directly from TorchBase
SteamService is now properly injected for clients, and reports errors
Obsolete tags on the manager properties in TorchBase
2017-08-22 08:06:30 -07:00
Westin Miller
91ad78e6a2 Private session field of proper type
Project Setup
2017-08-22 08:06:30 -07:00
Westin Miller
4a68d66ab0 Session management system
- Managers bound to the session instead of to Torch
- TorchSession automatic creation and destruction
- Automatic manager creation for sessions
2017-08-22 08:06:30 -07:00
John Gross
4cb50b556f Merge pull request #80 from TorchAPI/shutdown-crash-fix
Check if thread is running before suspending
2017-08-22 01:31:02 -04:00
John Gross
b5f73a99cc Use Thread.IsAlive property 2017-08-21 20:39:54 -07:00
John Gross
e9476a59e8 Fix ambiguous reference resolution 2017-08-21 20:15:16 -07:00
John Gross
f48f23c2eb Check if thread is running before suspending 2017-08-21 18:57:35 -07:00
John Michael Gross
ddf465d8c9 Merge pull request #79 from TorchAPI/reflection-manager
Testing Framework
2017-08-21 19:38:29 -04:00
Westin Miller
7149287b8e Let's pretend that typo didn't happen. 2017-08-21 15:44:15 -07:00
Westin Miller
cc709c6bb3 Torch.Client.Tests
Fixed jenkinsfile to create reports directory properly
Removed Any CPU configurations from Torch.sln
2017-08-21 15:38:17 -07:00
Westin Miller
8d101c4c11 Many more things use the new reflection system
Project for Torch.Server tests.
Refactoring some of the torch utility classes into a different namespace.
2017-08-20 23:46:19 -07:00
Westin Miller
64eef6cd8e Typo Number 3 2017-08-20 21:56:41 -07:00
Westin Miller
f8ae3c0dd1 Typo Number 2 2017-08-20 21:55:18 -07:00
Westin Miller
589205edc3 :( 2017-08-20 21:50:07 -07:00
Westin Miller
48b212faaf Reflection unit testing
Jenkins integration (we can only hope)
2017-08-20 21:48:42 -07:00
Westin Miller
0554dbc608 Complete documentation for the reflection manager. 2017-08-20 18:30:52 -07:00
Westin Miller
3564eb805c Reflection manager capable of handling field, property, and method resolution. 2017-08-20 17:56:42 -07:00
John Michael Gross
62a8064edd Update README.md 2017-08-20 17:11:49 -07:00
John Michael Gross
55ed45190b Update README.md 2017-08-20 17:09:58 -07:00
Westin Miller
a6ae96093f CI Archiving 2017-08-19 21:49:57 -07:00
Westin Miller
5b1afe6d50 Patch for uninitialized CI directories. 2017-08-19 21:46:29 -07:00
Westin Miller
60df71a74c Merge pull request #77 from TorchAPI/jenkins
Continuous Integration Support
2017-08-19 21:43:18 -07:00
Westin Miller
a6d5da861f Pipeline DSL 2017-08-19 21:36:42 -07:00
Westin Miller
afc10911f7 More descriptive stages 2017-08-19 21:17:58 -07:00
Westin Miller
0686e95c72 Delete existing link 2017-08-19 20:52:03 -07:00
Westin Miller
234754fd49 Initial continuous integration support 2017-08-19 20:31:08 -07:00
John Michael Gross
efb8d0f226 Merge pull request #76 from TorchAPI/dedi-issue-69
Stronger runtime checks in MultiplayerManager and Reflection
2017-08-19 20:13:27 -07:00
Westin Miller
3f881f7d67 Fixed tag parsing in UpdateManager 2017-08-19 18:22:26 -07:00
Westin Miller
64d38abc99 Typo 2017-08-19 18:20:50 -07:00
Westin Miller
4a39362702 Stronger runtime checks in MultiplayerManager and Reflection
MultiplayerManager now uses Keen's wrapper wrapper (wrapper?) of SteamSDK
Removed rogue file in the csproj
2017-08-19 04:56:41 -07:00
John Michael Gross
db2d3794ae Merge pull request #73 from TorchAPI/dedi-issue-69
Multiplayer Authentication Hook Removal
2017-08-19 01:28:38 -07:00
Westin Miller
526ff6fff0 Provide some semblance of compile time checking. 2017-08-18 22:31:03 -07:00
Westin Miller
eebc0e428e Fix that actually fixes the underlying problem with #69 2017-08-18 21:27:09 -07:00
John Michael Gross
80aca514b2 Merge pull request #72 from TorchAPI/dedi-issue-69
Remove player if already registered
2017-08-18 19:52:28 -07:00
Westin Miller
3e8068e82d Fixes a stupid problem with the DS multiplayer not checking if a
player is already registered.  (#69)
2017-08-18 19:34:41 -07:00
John Michael Gross
601fbcd176 Merge pull request #70 from Equinox-/master
Dependency Resolution and Sorting
2017-08-18 16:24:33 -07:00
Westin Miller
40eab15d69 More explanatory naming for Manager Init (Attach) and Dispose (Detach).
Torch.Client uses the registry to discover Steam's installation directory.
2017-08-18 16:19:59 -07:00
Westin Miller
ceb272c0b4 Moved extension classes to seperate files as Jimmacle requires. 2017-08-18 15:30:36 -07:00
Westin Miller
80d4f62694 Added load order restrictions for DependencyAttribute
Abstracted dependency manager away as an interface
2017-08-18 02:03:53 -07:00
Westin Miller
42f58a8649 Recursive dependency trees
- Now TorchBase and TorchSession can each have seperate managers
- Managers now have a Dispose function that's run on shutdown
2017-08-17 23:52:39 -07:00
Westin Miller
6b9af71967 Automatic dependency resolution
- All managers now use automatic dependency resolution
2017-08-17 18:14:23 -07:00
Westin Miller
dbd98a09c5 Early initialization for Torch Client.
- Assembly resolution
- SE installation directory locating
Dependency manager with automatic sorting and resolution
- Drop in replacement for the system currently in TorchBase
Shared binary directory for all Torch projects
2017-08-17 17:32:08 -07:00
John Michael Gross
c6a6363163 Merge pull request #67 from Mavy87/master
Fix to allow Torch to run as a service.
2017-08-17 09:29:04 -07:00
John Gross
82815f66e5 # Torch 1.1.229.265
* Features
    - Added more lenient version parsing for plugins (v#.# should work)
    - Added countdown option to restart command (!restart [seconds])
* Fixes
    - General fixes to work with the latest SE version
    - Fixed config changes not saving
    - Fixed crash on servers using the Windows Classic theme
2017-08-17 09:09:51 -07:00
Rene K
0a38eb770d Fix to allow Torch to run as a service. 2017-08-05 13:06:15 +02:00
John Gross
97da740e7e Catch errors in updater and fix loading error 2017-08-01 13:01:10 -07:00
John Gross
42bb24ca6a Hotfix for save issues 2017-08-01 12:31:49 -07:00
John Gross
2f3b6cdda7 Fix crashes and save issues 2017-07-31 13:12:01 -07:00
John Michael Gross
525b496774 Update README.md 2017-07-26 09:35:43 -07:00
John Michael Gross
562bb77dda Update README.md 2017-07-26 00:57:47 -07:00
85 changed files with 3668 additions and 997 deletions

3
.gitignore vendored
View File

@@ -252,3 +252,6 @@ ModelManifest.xml
# FAKE - F# Make
.fake/
GameBinaries
# Generated Files
**Gen.cs

View File

@@ -1,4 +1,13 @@
# Torch 1.1.205.478
# Torch 1.1.229.265
* Features
- Added more lenient version parsing for plugins (v#.# should work)
- Added countdown option to restart command (!restart [seconds])
* Fixes
- General fixes to work with the latest SE version
- Fixed config changes not saving
- (hopefully) Fixed issue causing crashes on servers using the Windows Classic theme
# Torch 1.1.207.7
* Notes
- This release makes significant changes to TorchConfig.xml. It has been renamed to Torch.cfg and has different options.
* Features

View File

@@ -0,0 +1,22 @@
pushd
$steamData = "C:/Steam/Data/"
$steamCMDPath = "C:/Steam/steamcmd/"
$steamCMDZip = "C:/Steam/steamcmd.zip"
Add-Type -AssemblyName System.IO.Compression.FileSystem
if (!(Test-Path $steamData)) {
mkdir "$steamData"
}
if (!(Test-Path $steamCMDPath)) {
if (!(Test-Path $steamCMDZip)) {
(New-Object System.Net.WebClient).DownloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", "$steamCMDZip");
}
[System.IO.Compression.ZipFile]::ExtractToDirectory($steamCMDZip, $steamCMDPath)
}
cd "$steamData"
& "$steamCMDPath/steamcmd.exe" "+login anonymous" "+force_install_dir $steamData" "+app_update 298740" "+quit"
popd

52
Jenkins/release.ps1 Normal file
View File

@@ -0,0 +1,52 @@
param([string] $ApiBase, [string]$tagName, [string]$authinfo, [string[]] $assetPaths)
Add-Type -AssemblyName "System.Web"
$headers = @{
Authorization = "Basic " + [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($authinfo))
Accept = "application/vnd.github.v3+json"
}
try
{
Write-Output("Checking if release with tag " + $tagName + " already exists...")
$release = Invoke-RestMethod -Uri ($ApiBase+"releases/tags/$tagName") -Method "GET" -Headers $headers
Write-Output(" Using existing release " + $release.id + " at " + $release.html_url)
} catch {
Write-Output(" Doesn't exist")
$rel_arg = @{
tag_name=$tagName
name="Generated $tagName"
body=""
draft=$FALSE
prerelease=$tagName.Contains("alpha") -or $tagName.Contains("beta")
}
Write-Output("Creating new release " + $tagName + "...")
$release = Invoke-RestMethod -Uri ($ApiBase+"releases") -Method "POST" -Headers $headers -Body (ConvertTo-Json($rel_arg))
Write-Output(" Created new release " + $tagName + " at " + $release.html_url)
}
$assetsApiBase = $release.assets_url
Write-Output("Checking for existing assets...")
$existingAssets = Invoke-RestMethod -Uri ($assetsApiBase) -Method "GET" -Headers $headers
$assetLabels = ($assetPaths | ForEach-Object {[System.IO.Path]::GetFileName($_)})
foreach ($asset in $existingAssets) {
if ($assetLabels -contains $asset.name) {
$uri = $asset.url
Write-Output(" Deleting old asset " + $asset.name + " (id " + $asset.id + "); URI=" + $uri)
$result = Invoke-RestMethod -Uri $uri -Method "DELETE" -Headers $headers
}
}
Write-Output("Uploading assets...")
$uploadUrl = $release.upload_url.Substring(0, $release.upload_url.LastIndexOf('{'))
foreach ($asset in $assetPaths) {
$assetName = [System.IO.Path]::GetFileName($asset)
$assetType = [System.Web.MimeMapping]::GetMimeMapping($asset)
$assetData = [System.IO.File]::ReadAllBytes($asset)
$headerExtra = $headers + @{
"Content-Type" = $assetType
Name = $assetName
}
$uri = $uploadUrl + "?name=" + $assetName
Write-Output(" Uploading " + $asset + " as " + $assetType + "; URI=" + $uri)
$result = Invoke-RestMethod -Uri $uri -Method "POST" -Headers $headerExtra -Body $assetData
Write-Output(" ID=" + $result.id + ", found at=" + $result.browser_download_url)
}

67
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,67 @@
node {
stage('Checkout') {
checkout scm
bat 'git pull --tags'
}
stage('Acquire SE') {
bat 'powershell -File Jenkins/jenkins-grab-se.ps1'
bat 'IF EXIST GameBinaries RMDIR GameBinaries'
bat 'mklink /J GameBinaries "C:/Steam/Data/DedicatedServer64/"'
}
stage('Acquire NuGet Packages') {
bat 'nuget restore Torch.sln'
}
stage('Build') {
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim()
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=Release /p:Platform=x64"
}
stage('Test') {
bat 'IF NOT EXIST reports MKDIR reports'
bat "\"packages/xunit.runner.console.2.2.0/tools/xunit.console.exe\" \"bin-test/x64/Release/Torch.Tests.dll\" \"bin-test/x64/Release/Torch.Server.Tests.dll\" \"bin-test/x64/Release/Torch.Client.Tests.dll\" -parallel none -xml \"reports/Torch.Tests.xml\""
step([
$class: 'XUnitBuilder',
thresholdMode: 1,
thresholds: [[$class: 'FailedThreshold', failureThreshold: '1']],
tools: [[
$class: 'XUnitDotNetTestType',
deleteOutputFiles: true,
failIfNotNew: true,
pattern: 'reports/*.xml',
skipNoTestFiles: false,
stopProcessingIfError: true
]]
])
}
stage('Archive') {
bat '''IF EXIST bin\\torch-server.zip DEL bin\\torch-server.zip
IF EXIST bin\\package-server RMDIR /S /Q bin\\package-server
xcopy bin\\x64\\Release bin\\package-server\\
del bin\\package-server\\Torch.Client*'''
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-server\\\", \\\"\$PWD\\bin\\torch-server.zip\\\")\""
archiveArtifacts artifacts: 'bin/torch-server.zip', caseSensitive: false, onlyIfSuccessful: true
bat '''IF EXIST bin\\torch-client.zip DEL bin\\torch-client.zip
IF EXIST bin\\package-client RMDIR /S /Q bin\\package-client
xcopy bin\\x64\\Release bin\\package-client\\
del bin\\package-client\\Torch.Server*'''
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-client\\\", \\\"\$PWD\\bin\\torch-client.zip\\\")\""
archiveArtifacts artifacts: 'bin/torch-client.zip', caseSensitive: false, onlyIfSuccessful: true
archiveArtifacts artifacts: 'bin/x64/Release/Torch*', caseSensitive: false, fingerprint: true, onlyIfSuccessful: true
}
gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim()
gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim()
if (gitVersion == gitSimpleVersion) {
stage('Release') {
withCredentials([usernamePassword(credentialsId: 'e771beac-b3ee-4bc9-82b7-40a6d426d508', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
powershell "./Jenkins/release.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")"
}
}
}
}

View File

@@ -1,4 +1,4 @@
Discord: [![Discord](https://discordapp.com/api/guilds/230191591640268800/widget.png)](https://discord.gg/8uHZykr)
[![Discord](https://discordapp.com/api/guilds/230191591640268800/widget.png)](https://discord.gg/8uHZykr) [![Build Status](http://server.torchapi.net:8080/job/Torch/job/Torch/job/master/badge/icon)](http://server.torchapi.net:8080/job/Torch/job/Torch/job/master/)
# 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.
@@ -10,17 +10,15 @@ Torch is the successor to SE Server Extender and gives server admins the tools t
* Organized, easy to use configuration editor
* Extensible using the Torch plugin system
# 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.
# 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.
# Installation Guide
### Automatic (recommended)
* Unzip Torch to its own folder, run Torch.Server.exe and enter 'y' in the prompt for automatic updates. Torch will automatically download the Space Engineers files and generate all of the configs/folders necessary.
### Manual (for hosting companies or the paranoid)
* Install the Space Engineers DS and then unzip the Torch files into the server's DedicatedServer64 directory. It will automatically detect the manual install and disable automatic updates.
In both cases you will need to set the InstancePath in TorchConfig.xml to an existing dedicated server instance as Torch can't fully generate it on its own yet.
# Official Plugins
@@ -28,4 +26,5 @@ Install plugins by unzipping them into the 'Plugins' folder which should be in t
* [Essentials](https://github.com/TorchAPI/Essentials): Adds a slew of chat commands and other tools to help manage your server.
* [Concealment](https://github.com/TorchAPI/Concealment): Adds game logic and physics optimizations that significantly improve sim speed.
If you have a more enjoyable server experience because of Torch, please consider supporting us on [Patreon](https://www.patreon.com/bePatron?u=847269)!
If you have a more enjoyable server experience because of Torch, please consider supporting us on Patreon.
[![Patreon](http://i.imgur.com/VzzIMgn.png)](https://www.patreon.com/bePatron?u=847269)!

View File

@@ -3,11 +3,11 @@
@echo off
set /p path="Please enter the folder location of your SpaceEngineersDedicated.exe: "
cd %~dp0
mklink /D GameBinaries %path%
mklink /J GameBinaries "%path%"
if errorlevel 1 goto Error
echo Done! You can now open the Torch solution without issue.
goto End
:Error
echo An error occured creating the symlink.
:End
pause
pause

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch.API.Managers;
using Torch.API.Session;
using VRage.Game.ModAPI;
namespace Torch.API
@@ -33,17 +34,27 @@ namespace Torch.API
/// </summary>
event Action SessionUnloaded;
/// <summary>
/// Gets the currently running session instance, or null if none exists.
/// </summary>
ITorchSession CurrentSession { get; }
/// <summary>
/// Configuration for the current instance.
/// </summary>
ITorchConfig Config { get; }
/// <inheritdoc cref="IMultiplayerManager"/>
[Obsolete]
IMultiplayerManager Multiplayer { get; }
/// <inheritdoc cref="IPluginManager"/>
[Obsolete]
IPluginManager Plugins { get; }
/// <inheritdoc cref="IDependencyManager"/>
IDependencyManager Managers { get; }
/// <summary>
/// The binary version of the current instance.
/// </summary>
@@ -90,12 +101,6 @@ namespace Torch.API
/// Initialize the Torch instance.
/// </summary>
void Init();
/// <summary>
/// Get an <see cref="IManager"/> that is part of the Torch instance.
/// </summary>
/// <typeparam name="T">Manager type</typeparam>
T GetManager<T>() where T : class, IManager;
}
/// <summary>

View File

@@ -0,0 +1,30 @@
using System;
namespace Torch.API.Managers
{
public static class DependencyManagerExtensions
{
/// <summary>
/// Removes a single manager from this dependency manager.
/// </summary>
/// <param name="managerType">The dependency type to remove</param>
/// <returns>The manager that was removed, or null if one wasn't removed</returns>
/// <exception cref="InvalidOperationException">When removing managers from an initialized dependency manager</exception>
public static IManager RemoveManager(this IDependencyManager depManager, Type managerType)
{
IManager mgr = depManager.GetManager(managerType);
return depManager.RemoveManager(mgr) ? mgr : null;
}
/// <summary>
/// Removes a single manager from this dependency manager.
/// </summary>
/// <typeparam name="T">The dependency type to remove</typeparam>
/// <returns>The manager that was removed, or null if one wasn't removed</returns>
/// <exception cref="InvalidOperationException">When removing managers from an initialized dependency manager</exception>
public static IManager RemoveManager<T>(this IDependencyManager depManager)
{
return depManager.RemoveManager(typeof(T));
}
}
}

View File

@@ -0,0 +1,15 @@
namespace Torch.API.Managers
{
public static class DependencyProviderExtensions
{
/// <summary>
/// Gets the manager that provides the given type. If there is no such manager, returns null.
/// </summary>
/// <typeparam name="T">Type of manager</typeparam>
/// <returns>manager, or null if none exists</returns>
public static T GetManager<T>(this IDependencyProvider depProvider) where T : class, IManager
{
return (T)depProvider.GetManager(typeof(T));
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Managers
{
/// <summary>
/// Manages a set of <see cref="IManager"/> and the dependencies between them.
/// </summary>
public interface IDependencyManager : IDependencyProvider
{
/// <summary>
/// Registers the given manager into the dependency system.
/// </summary>
/// <remarks>
/// This method only returns false when there is already a manager registered with a type derived from this given manager,
/// or when the given manager is derived from an already existing manager.
/// </remarks>
/// <param name="manager">Manager to register</param>
/// <exception cref="InvalidOperationException">When adding a new manager to an initialized dependency manager</exception>
/// <returns>true if added, false if not</returns>
bool AddManager(IManager manager);
/// <summary>
/// Clears all managers registered with this dependency manager
/// </summary>
/// <exception cref="InvalidOperationException">When removing managers from an initialized dependency manager</exception>
void ClearManagers();
/// <summary>
/// Removes a single manager from this dependency manager.
/// </summary>
/// <param name="manager">The manager to remove</param>
/// <returns>true if successful, false if the manager wasn't found</returns>
/// <exception cref="InvalidOperationException">When removing managers from an initialized dependency manager</exception>
bool RemoveManager(IManager manager);
/// <summary>
/// Sorts the dependency manager, then attaches all its registered managers in <see cref="AttachOrder" />
/// </summary>
void Attach();
/// <summary>
/// Detaches all registered managers in <see cref="DetachOrder"/>
/// </summary>
void Detach();
/// <summary>
/// The order that managers should be attached in. (Dependencies, then dependents)
/// </summary>
/// <exception cref="InvalidOperationException">When trying to determine load order before this dependency manager is initialized</exception>
IEnumerable<IManager> AttachOrder { get; }
/// <summary>
/// The order that managers should be detached in. (Dependents, then dependencies)
/// </summary>
/// <exception cref="InvalidOperationException">When trying to determine unload order before this dependency manager is initialized</exception>
IEnumerable<IManager> DetachOrder { get; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Managers
{
public interface IDependencyProvider
{
/// <summary>
/// Gets the manager that provides the given type. If there is no such manager, returns null.
/// </summary>
/// <param name="type">Type of manager</param>
/// <returns>manager, or null if none exists</returns>
IManager GetManager(Type type);
}
}

View File

@@ -12,8 +12,13 @@ namespace Torch.API.Managers
public interface IManager
{
/// <summary>
/// Initializes the manager. Called after Torch is initialized.
/// Attaches the manager to the session. Called once this manager's dependencies have been attached.
/// </summary>
void Init();
void Attach();
/// <summary>
/// Detaches the manager from the session. Called before this manager's dependencies are detached.
/// </summary>
void Detach();
}
}

View File

@@ -26,11 +26,6 @@ namespace Torch.API.Managers
/// </summary>
void UpdatePlugins();
/// <summary>
/// Disposes all loaded plugins.
/// </summary>
void DisposePlugins();
/// <summary>
/// Load plugins.
/// </summary>

View File

@@ -1,22 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Torch.API.Plugins
{
/// <summary>
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
/// <summary>
/// The display name of the plugin
/// </summary>
public string Name { get; }
/// <summary>
/// The version of the plugin
/// </summary>
public Version Version { get; }
/// <summary>
/// The GUID of the plugin
/// </summary>
public Guid Guid { get; }
/// <summary>
/// Creates a new plugin attribute with the given attributes
/// </summary>
/// <param name="name"></param>
/// <param name="version"></param>
/// <param name="guid"></param>
public PluginAttribute(string name, string version, string guid)
{
Name = name;
Version = Version.Parse(version);
Guid = Guid.Parse(guid);
}
/// <summary>
/// Creates a new plugin attribute with the given attributes. Version is computed as the version of the assembly containing the given type.
/// </summary>
/// <param name="name"></param>
/// <param name="versionSupplier">Version is this type's assembly's version</param>
/// <param name="guid"></param>
public PluginAttribute(string name, Type versionSupplier, string guid)
{
Name = name;
Version = versionSupplier.Assembly.GetName().Version;
Guid = Guid.Parse(guid);
}
}
}

View File

@@ -1,36 +1,17 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("TorchAPI")]
[assembly: AssemblyTitle("Torch API")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TorchAPI")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("fba5d932-6254-4a1e-baf4-e229fa94e3c2")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Game.World;
using Torch.API.Managers;
namespace Torch.API.Session
{
/// <summary>
/// Represents the Torch code working with a single game session
/// </summary>
public interface ITorchSession
{
/// <summary>
/// The Torch instance this session is bound to
/// </summary>
ITorchBase Torch { get; }
/// <summary>
/// The Space Engineers game session this session is bound to.
/// </summary>
MySession KeenSession { get; }
/// <inheritdoc cref="IDependencyManager"/>
IDependencyManager Managers { get; }
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch.API.Managers;
namespace Torch.API.Session
{
/// <summary>
/// Creates a manager for the given session if applicable.
/// </summary>
/// <remarks>
/// This is for creating managers that will live inside the session, not the manager that controls sesssions.
/// </remarks>
/// <param name="session">The session to construct a bound manager for</param>
/// <returns>The manager that will live in the session, or null if none.</returns>
public delegate IManager SessionManagerFactoryDel(ITorchSession session);
/// <summary>
/// Manages the creation and destruction of <see cref="ITorchSession"/> instances for each <see cref="Sandbox.Game.World.MySession"/> created by Space Engineers.
/// </summary>
public interface ITorchSessionManager : IManager
{
/// <summary>
/// The currently running session
/// </summary>
ITorchSession CurrentSession { get; }
/// <summary>
/// Adds the given factory as a supplier for session based managers
/// </summary>
/// <param name="factory">Session based manager supplier</param>
/// <returns>true if added, false if already present</returns>
/// <exception cref="ArgumentNullException">If the factory is null</exception>
bool AddFactory(SessionManagerFactoryDel factory);
/// <summary>
/// Remove the given factory from the suppliers for session based managers
/// </summary>
/// <param name="factory">Session based manager supplier</param>
/// <returns>true if removed, false if not present</returns>
/// <exception cref="ArgumentNullException">If the factory is null</exception>
bool RemoveFactory(SessionManagerFactoryDel factory);
}
}

View File

@@ -2,8 +2,6 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FBA5D932-6254-4A1E-BAF4-E229FA94E3C2}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -12,10 +10,12 @@
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
@@ -23,14 +23,14 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\x64\Release\Torch.API.xml</DocumentationFile>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.API.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="HavokWrapper, Version=1.0.6278.22649, Culture=neutral, processorArchitecture=AMD64">
@@ -38,8 +38,8 @@
<HintPath>..\GameBinaries\HavokWrapper.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="NLog">
<HintPath>..\packages\NLog.4.4.1\lib\net45\NLog.dll</HintPath>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="PresentationCore" />
@@ -156,9 +156,16 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<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="Managers\IDependencyManager.cs" />
<Compile Include="Managers\IDependencyProvider.cs" />
<Compile Include="Managers\IManager.cs" />
<Compile Include="Managers\IMultiplayerManager.cs" />
<Compile Include="IPlayer.cs" />
@@ -173,16 +180,15 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ServerState.cs" />
<Compile Include="ModAPI\TorchAPI.cs" />
<Compile Include="Session\ITorchSession.cs" />
<Compile Include="Session\ITorchSessionManager.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.4.1" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
</packages>

View File

@@ -1,12 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Torch Server")]
[assembly: AssemblyTitle("Torch Client Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<ProjectGuid>{632E78C0-0DAC-4B71-B411-2F1B333CC310}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Torch.Client.Tests</RootNamespace>
<AssemblyName>Torch.Client.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<NoWarn>1591,0649</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>$(SolutionDir)\bin-test\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>$(SolutionDir)\bin-test\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>$(SolutionDir)\bin-test\x64\Release\Torch.Client.Tests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.assert, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll</HintPath>
</Reference>
<Reference Include="xunit.core, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll</HintPath>
</Reference>
<Reference Include="xunit.execution.desktop, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.execution.2.2.0\lib\net452\xunit.execution.desktop.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TorchClientReflectionTest.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
</ProjectReference>
<ProjectReference Include="..\Torch.Client\Torch.Client.csproj">
<Project>{e36df745-260b-4956-a2e8-09f08b2e7161}</Project>
<Name>Torch.Client</Name>
</ProjectReference>
<ProjectReference Include="..\Torch.Tests\Torch.Tests.csproj">
<Project>{c3c8b671-6ad1-44aa-a8da-e0c0dc0fedf5}</Project>
<Name>Torch.Tests</Name>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using Torch.Client;
using Torch.Tests;
using Torch.Utils;
using Xunit;
namespace Torch.Client.Tests
{
public class TorchClientReflectionTest
{
static TorchClientReflectionTest()
{
TestUtils.Init();
}
private static ReflectionTestManager _manager;
private static ReflectionTestManager Manager()
{
if (_manager != null)
return _manager;
return _manager = new ReflectionTestManager().Init(typeof(TorchClient).Assembly);
}
public static IEnumerable<object[]> Getters => Manager().Getters;
public static IEnumerable<object[]> Setters => Manager().Setters;
public static IEnumerable<object[]> Invokers => Manager().Invokers;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
public void TestBindingGetter(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(Setters))]
public void TestBindingSetter(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(Invokers))]
public void TestBindingInvoker(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="xunit" version="2.2.0" targetFramework="net461" />
<package id="xunit.abstractions" version="2.0.1" targetFramework="net461" />
<package id="xunit.assert" version="2.2.0" targetFramework="net461" />
<package id="xunit.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.execution" version="2.2.0" targetFramework="net461" />
<package id="xunit.runner.console" version="2.2.0" targetFramework="net461" developmentDependency="true" />
</packages>

View File

@@ -1,17 +1,167 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Forms;
using NLog;
using Torch.Utils;
using MessageBox = System.Windows.MessageBox;
namespace Torch.Client
{
public static class Program
{
public const string SpaceEngineersBinaries = "Bin64";
private static string _spaceEngInstallAlias = null;
public static string SpaceEngineersInstallAlias
{
get
{
// ReSharper disable once ConvertIfStatementToNullCoalescingExpression
if (_spaceEngInstallAlias == null)
{
// ReSharper disable once AssignNullToNotNullAttribute
_spaceEngInstallAlias = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "SpaceEngineersAlias");
}
return _spaceEngInstallAlias;
}
}
private const string _steamSpaceEngineersDirectory = @"steamapps\common\SpaceEngineers\";
private const string _spaceEngineersVerifyFile = SpaceEngineersBinaries + @"\SpaceEngineers.exe";
public const string ConfigName = "Torch.cfg";
private static Logger _log = LogManager.GetLogger("Torch");
#if DEBUG
[DllImport("kernel32.dll")]
private static extern void AllocConsole();
[DllImport("kernel32.dll")]
private static extern void FreeConsole();
#endif
public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
#if DEBUG
try
{
AllocConsole();
#endif
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
// Early config: Resolve SE install directory.
if (!File.Exists(Path.Combine(SpaceEngineersInstallAlias, _spaceEngineersVerifyFile)))
SetupSpaceEngInstallAlias();
using (new TorchAssemblyResolver(Path.Combine(SpaceEngineersInstallAlias, SpaceEngineersBinaries)))
{
RunClient();
}
#if DEBUG
}
finally
{
FreeConsole();
}
#endif
}
private static void SetupSpaceEngInstallAlias()
{
string spaceEngineersDirectory = null;
// TODO look at Steam/config/Config.VDF? Has alternate directories.
var steamDir =
Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\SOFTWARE\\Valve\\Steam", "SteamPath", null) as string;
if (steamDir != null)
{
spaceEngineersDirectory = Path.Combine(steamDir, _steamSpaceEngineersDirectory);
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (File.Exists(Path.Combine(spaceEngineersDirectory, _spaceEngineersVerifyFile)))
_log.Debug("Found Space Engineers in {0}", spaceEngineersDirectory);
else
_log.Debug("Couldn't find Space Engineers in {0}", spaceEngineersDirectory);
}
if (spaceEngineersDirectory == null)
{
var dialog = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Please select the SpaceEngineers installation folder"
};
do
{
if (dialog.ShowDialog() != DialogResult.OK)
{
var ex = new FileNotFoundException("Unable to find the Space Engineers install directory, aborting");
_log.Fatal(ex);
LogManager.Flush();
throw ex;
}
spaceEngineersDirectory = dialog.SelectedPath;
if (File.Exists(Path.Combine(spaceEngineersDirectory, _spaceEngineersVerifyFile)))
break;
if (MessageBox.Show(
$"Unable to find {0} in {1}. Are you sure it's the Space Engineers install directory?",
"Invalid Space Engineers Directory", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
break;
} while (true); // Repeat until they confirm.
}
if (!JunctionLink(SpaceEngineersInstallAlias, spaceEngineersDirectory))
{
var ex = new IOException($"Failed to create junction link {SpaceEngineersInstallAlias} => {spaceEngineersDirectory}. Aborting.");
_log.Fatal(ex);
LogManager.Flush();
throw ex;
}
string junctionVerify = Path.Combine(SpaceEngineersInstallAlias, _spaceEngineersVerifyFile);
if (!File.Exists(junctionVerify))
{
var ex = new FileNotFoundException($"Junction link is not working. File {junctionVerify} does not exist");
_log.Fatal(ex);
LogManager.Flush();
throw ex;
}
}
private static bool JunctionLink(string linkName, string targetDir)
{
var junctionLinkProc = new ProcessStartInfo("cmd.exe", $"/c mklink /J \"{linkName}\" \"{targetDir}\"")
{
WorkingDirectory = Directory.GetCurrentDirectory(),
UseShellExecute = false,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.ASCII
};
Process cmd = Process.Start(junctionLinkProc);
// ReSharper disable once PossibleNullReferenceException
while (!cmd.HasExited)
{
string line = cmd.StandardOutput.ReadLine();
if (!string.IsNullOrWhiteSpace(line))
_log.Info(line);
Thread.Sleep(100);
}
if (cmd.ExitCode != 0)
_log.Error("Unable to create junction link {0} => {1}", linkName, targetDir);
return cmd.ExitCode == 0;
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = (Exception)e.ExceptionObject;
_log.Error(ex);
LogManager.Flush();
MessageBox.Show(ex.StackTrace, ex.Message);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RunClient()
{
var client = new TorchClient();
try
@@ -27,11 +177,5 @@ namespace Torch.Client
client.Start();
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = (Exception)e.ExceptionObject;
MessageBox.Show(ex.StackTrace, ex.Message);
}
}
}

View File

@@ -1,4 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyVersion("1.0.207.7")]
[assembly: AssemblyFileVersion("1.0.207.7")]
[assembly: AssemblyTitle("Torch Client")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -1,16 +0,0 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System.Reflection;
<# var dt = DateTime.Now;
int major = 1;
int minor = 0;
int build = dt.DayOfYear;
int rev = (int)dt.TimeOfDay.TotalMinutes / 2;
#>
[assembly: AssemblyVersion("<#= major #>.<#= minor #>.<#= build #>.<#= rev #>")]
[assembly: AssemblyFileVersion("<#= major #>.<#= minor #>.<#= build #>.<#= rev #>")]

View File

@@ -2,8 +2,6 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E36DF745-260B-4956-A2E8-09F08B2E7161}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -15,10 +13,12 @@
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
@@ -27,7 +27,7 @@
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
@@ -35,19 +35,20 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<DocumentationFile>bin\x64\Release\Torch.Client.xml</DocumentationFile>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Client.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>torchicon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.1\lib\net45\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Sandbox.Common.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Sandbox.Game">
<HintPath>..\GameBinaries\Sandbox.Game.dll</HintPath>
@@ -64,6 +65,7 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
@@ -81,6 +83,10 @@
<HintPath>..\GameBinaries\VRage.Game.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Game.XmlSerializers">
<HintPath>..\GameBinaries\VRage.Game.XmlSerializers.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Input, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\VRage.Input.dll</HintPath>
@@ -103,18 +109,21 @@
<HintPath>..\GameBinaries\VRage.Render11.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" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<DependentUpon>AssemblyInfo.tt</DependentUpon>
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo1.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" />
@@ -144,35 +153,24 @@
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
<Private>True</Private>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7E01635C-3B67-472E-BCD6-C5539564F214}</Project>
<Name>Torch</Name>
<Private>True</Private>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="torchicon.ico" />
</ItemGroup>
<ItemGroup>
<Content Include="Properties\AssemblyInfo.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>AssemblyInfo.cs</LastGenOutput>
</Content>
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup>
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)"</PostBuildEvent>
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)"
</PostBuildEvent>
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,19 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.Windows;
using Sandbox;
using Sandbox.Engine.Networking;
using Sandbox.Engine.Platform;
using Sandbox.Engine.Utils;
using Sandbox.Game;
using Sandbox.ModAPI;
using SpaceEngineers.Game;
using VRage.Steam;
using Torch.API;
using VRage;
using VRage.FileSystem;
using VRage.GameServices;
using VRageRender;
using VRageRender.ExternalApp;
namespace Torch.Client
{
@@ -22,50 +23,50 @@ namespace Torch.Client
private MyCommonProgramStartup _startup;
private IMyRender _renderer;
private const uint APP_ID = 244850;
private VRageGameServices _services;
public TorchClient()
{
Config = new TorchClientConfig();
}
public override void Init()
{
Directory.SetCurrentDirectory(Program.SpaceEngineersInstallAlias);
MyFileSystem.ExePath = Path.Combine(Program.SpaceEngineersInstallAlias, Program.SpaceEngineersBinaries);
Log.Info("Initializing Torch Client");
base.Init();
if (!File.Exists("steam_appid.txt"))
{
Directory.SetCurrentDirectory(Path.GetDirectoryName(typeof(VRage.FastResourceLock).Assembly.Location) + "\\..");
}
SpaceEngineersGame.SetupBasicGameInfo();
_startup = new MyCommonProgramStartup(RunArgs);
if (_startup.PerformReporting())
return;
throw new InvalidOperationException("Torch client won't launch when started in error reporting mode");
_startup.PerformAutoconnect();
if (!_startup.CheckSingleInstance())
return;
throw new InvalidOperationException("Only one instance of Space Engineers can be running at a time.");
var appDataPath = _startup.GetAppDataPath();
MyInitializer.InvokeBeforeRun(APP_ID, MyPerGameSettings.BasicGameInfo.ApplicationName, appDataPath);
MyInitializer.InitCheckSum();
_startup.InitSplashScreen();
if (!_startup.Check64Bit())
return;
throw new InvalidOperationException("Torch requires a 64bit operating system");
_startup.DetectSharpDxLeaksBeforeRun();
using (var mySteamService = new SteamService(Game.IsDedicated, APP_ID))
{
_renderer = null;
SpaceEngineersGame.SetupPerGameSettings();
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");
OverrideMenus();
InitializeRender();
_services = new VRageGameServices(mySteamService);
if (!Game.IsDedicated)
MyFileSystem.InitUserSpecific(mySteamService.UserId.ToString());
}
_startup.DetectSharpDxLeaksAfterRun();
MyInitializer.InvokeAfterRun();
if (!Game.IsDedicated)
MyFileSystem.InitUserSpecific(MyGameService.UserId.ToString());
}
private void OverrideMenus()
@@ -82,19 +83,46 @@ namespace Torch.Client
MyPerGameSettings.GUI.MainMenu = typeof(TorchMainMenuScreen);
}
public override void Start()
{
using (var spaceEngineersGame = new SpaceEngineersGame(_services, RunArgs))
using (var spaceEngineersGame = new SpaceEngineersGame(RunArgs))
{
Log.Info("Starting client");
OverrideMenus();
spaceEngineersGame.OnGameLoaded += SpaceEngineersGame_OnGameLoaded;
spaceEngineersGame.Run();
spaceEngineersGame.OnGameExit += Dispose;
spaceEngineersGame.Run(false, _startup.DisposeSplashScreen);
}
}
private void SetRenderWindowTitle(string title)
{
MyRenderThread renderThread = MySandboxGame.Static?.GameRenderComponent?.RenderThread;
if (renderThread == null)
return;
FieldInfo renderWindowField = typeof(MyRenderThread).GetField("m_renderWindow",
BindingFlags.Instance | BindingFlags.NonPublic);
if (renderWindowField == null)
return;
var window = renderWindowField.GetValue(MySandboxGame.Static.GameRenderComponent.RenderThread) as System.Windows.Forms.Form;
if (window != null)
renderThread.Invoke(() =>
{
window.Text = title;
});
}
private void SpaceEngineersGame_OnGameLoaded(object sender, EventArgs e)
{
SetRenderWindowTitle($"Space Engineers v{GameVersion} with Torch v{TorchVersion}");
}
public override void Dispose()
{
MyGameService.ShutDown();
_startup.DetectSharpDxLeaksAfterRun();
MyInitializer.InvokeAfterRun();
}
public override void Stop()

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Torch.Client
{
public class TorchClientConfig : ITorchConfig
{
// How do we want to handle client side config? It's radically different than the server.
public bool GetPluginUpdates { get; set; } = false;
public bool GetTorchUpdates { get; set; } = false;
public string InstanceName { get; set; } = "TorchClient";
public string InstancePath { get; set; }
public bool NoUpdate { get; set; } = true;
public List<string> Plugins { get; set; }
public bool ShouldUpdatePlugins { get; } = false;
public bool ShouldUpdateTorch { get; } = false;
public int TickTimeout { get; set; }
public bool Autostart { get; set; } = false;
public bool ForceUpdate { get; set; } = false;
public bool NoGui { get; set; } = false;
public bool RestartOnCrash { get; set; } = false;
public string WaitForPID { get; set; } = null;
public bool Save(string path = null)
{
return true;
}
}
}

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.4.1" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
</packages>

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Torch Server Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<ProjectGuid>{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Torch.Server.Tests</RootNamespace>
<AssemblyName>Torch.Server.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<NoWarn>1591,0649</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>$(SolutionDir)\bin-test\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>$(SolutionDir)\bin-test\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>$(SolutionDir)\bin-test\x64\Release\Torch.Server.Tests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.assert, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll</HintPath>
</Reference>
<Reference Include="xunit.core, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll</HintPath>
</Reference>
<Reference Include="xunit.execution.desktop, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.execution.2.2.0\lib\net452\xunit.execution.desktop.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TorchServerReflectionTest.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
</ProjectReference>
<ProjectReference Include="..\Torch.Server\Torch.Server.csproj">
<Project>{ca50886b-7b22-4cd8-93a0-c06f38d4f77d}</Project>
<Name>Torch.Server</Name>
</ProjectReference>
<ProjectReference Include="..\Torch.Tests\Torch.Tests.csproj">
<Project>{c3c8b671-6ad1-44aa-a8da-e0c0dc0fedf5}</Project>
<Name>Torch.Tests</Name>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Torch.Tests;
using Torch.Utils;
using Xunit;
namespace Torch.Server.Tests
{
public class TorchServerReflectionTest
{
static TorchServerReflectionTest()
{
TestUtils.Init();
}
private static ReflectionTestManager _manager;
private static ReflectionTestManager Manager()
{
if (_manager != null)
return _manager;
return _manager = new ReflectionTestManager().Init(typeof(TorchServer).Assembly);
}
public static IEnumerable<object[]> Getters => Manager().Getters;
public static IEnumerable<object[]> Setters => Manager().Setters;
public static IEnumerable<object[]> Invokers => Manager().Invokers;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
public void TestBindingGetter(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(Setters))]
public void TestBindingSetter(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(Invokers))]
public void TestBindingInvoker(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="xunit" version="2.2.0" targetFramework="net461" />
<package id="xunit.abstractions" version="2.0.1" targetFramework="net461" />
<package id="xunit.assert" version="2.2.0" targetFramework="net461" />
<package id="xunit.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.execution" version="2.2.0" targetFramework="net461" />
<package id="xunit.runner.console" version="2.2.0" targetFramework="net461" developmentDependency="true" />
</packages>

184
Torch.Server/Initializer.cs Normal file
View File

@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using Torch.Utils;
namespace Torch.Server
{
public class Initializer
{
private static readonly Logger Log = LogManager.GetLogger(nameof(Initializer));
private bool _init;
private const string STEAMCMD_DIR = "steamcmd";
private const string STEAMCMD_ZIP = "temp.zip";
private static readonly string STEAMCMD_PATH = $"{STEAMCMD_DIR}\\steamcmd.exe";
private static readonly string RUNSCRIPT_PATH = $"{STEAMCMD_DIR}\\runscript.txt";
private const string RUNSCRIPT = @"force_install_dir ../
login anonymous
app_update 298740
quit";
private TorchAssemblyResolver _resolver;
private TorchConfig _config;
private TorchServer _server;
private string _basePath;
public TorchConfig Config => _config;
public TorchServer Server => _server;
public Initializer(string basePath)
{
_basePath = basePath;
}
public bool Initialize(string[] args)
{
if (_init)
return false;
AppDomain.CurrentDomain.UnhandledException += HandleException;
if (!args.Contains("-noupdate"))
RunSteamCmd();
_resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64"));
_config = InitConfig();
if (!_config.Parse(args))
return false;
if (!string.IsNullOrEmpty(_config.WaitForPID))
{
try
{
var pid = int.Parse(_config.WaitForPID);
var waitProc = Process.GetProcessById(pid);
Log.Info("Continuing in 5 seconds.");
Thread.Sleep(5000);
if (!waitProc.HasExited)
{
Log.Warn($"Killing old process {pid}.");
waitProc.Kill();
}
}
catch
{
// ignored
}
}
_init = true;
return true;
}
public void Run()
{
_server = new TorchServer(_config);
_server.Init();
if (_config.NoGui || _config.Autostart)
{
new Thread(_server.Start).Start();
}
if (!_config.NoGui)
{
new TorchUI(_server).ShowDialog();
}
_resolver?.Dispose();
}
private TorchConfig InitConfig()
{
var configName = "Torch.cfg";
var configPath = Path.Combine(Directory.GetCurrentDirectory(), configName);
if (File.Exists(configName))
{
Log.Info($"Loading config {configPath}");
return TorchConfig.LoadFrom(configPath);
}
else
{
Log.Info($"Generating default config at {configPath}");
var config = new TorchConfig { InstancePath = Path.GetFullPath("Instance") };
config.Save(configPath);
return config;
}
}
private static void RunSteamCmd()
{
var log = LogManager.GetLogger("SteamCMD");
if (!Directory.Exists(STEAMCMD_DIR))
{
Directory.CreateDirectory(STEAMCMD_DIR);
}
if (!File.Exists(RUNSCRIPT_PATH))
File.WriteAllText(RUNSCRIPT_PATH, RUNSCRIPT);
if (!File.Exists(STEAMCMD_PATH))
{
try
{
log.Info("Downloading SteamCMD.");
using (var client = new WebClient())
client.DownloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", STEAMCMD_ZIP);
ZipFile.ExtractToDirectory(STEAMCMD_ZIP, STEAMCMD_DIR);
File.Delete(STEAMCMD_ZIP);
log.Info("SteamCMD downloaded successfully!");
}
catch
{
log.Error("Failed to download SteamCMD, unable to update the DS.");
return;
}
}
log.Info("Checking for DS updates.");
var steamCmdProc = new ProcessStartInfo(STEAMCMD_PATH, "+runscript runscript.txt")
{
WorkingDirectory = Path.Combine(Directory.GetCurrentDirectory(), STEAMCMD_DIR),
UseShellExecute = false,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.ASCII
};
var cmd = Process.Start(steamCmdProc);
// ReSharper disable once PossibleNullReferenceException
while (!cmd.HasExited)
{
log.Info(cmd.StandardOutput.ReadLine());
Thread.Sleep(100);
}
}
private void HandleException(object sender, UnhandledExceptionEventArgs e)
{
var ex = (Exception)e.ExceptionObject;
Log.Fatal(ex);
Console.WriteLine("Exiting in 5 seconds.");
Thread.Sleep(5000);
if (_config.RestartOnCrash)
{
var exe = typeof(Program).Assembly.Location;
_config.WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, _config.ToString());
}
//1627 = Function failed during execution.
Environment.Exit(1627);
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace Torch.Server
{
public static class ListBoxExtensions
{
//https://stackoverflow.com/questions/28689125/how-to-autoscroll-listbox-to-bottom-wpf-c
public static void ScrollToItem(this ListBox listBox, int index)
{
// Find a container
UIElement container = null;
for (int i = index; i > 0; i--)
{
container = listBox.ItemContainerGenerator.ContainerFromIndex(i) as UIElement;
if (container != null)
{
break;
}
}
if (container == null)
return;
// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for (Visual vis = container; vis != null && vis != listBox; vis = VisualTreeHelper.GetParent(vis) as Visual)
if ((presenter = vis as ScrollContentPresenter) != null)
break;
if (presenter == null)
return;
// Find the IScrollInfo
var scrollInfo =
!presenter.CanContentScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;
// Find the amount of items that is "Visible" in the ListBox
var height = (container as ListBoxItem).ActualHeight;
var lbHeight = listBox.ActualHeight;
var showCount = (int)Math.Floor(lbHeight / height) - 1;
//Set the scrollbar
if (scrollInfo.CanVerticallyScroll)
scrollInfo.SetVerticalOffset(index - showCount);
}
private static DependencyObject FirstVisualChild(Visual visual)
{
if (visual == null) return null;
if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}
}

View File

@@ -25,6 +25,8 @@ namespace Torch.Server.Managers
private const string CONFIG_NAME = "SpaceEngineers-Dedicated.cfg";
public ConfigDedicatedViewModel DedicatedConfig { get; set; }
private static readonly Logger Log = LogManager.GetLogger(nameof(InstanceManager));
[Dependency]
private FilesystemManager _filesystemManager;
public InstanceManager(ITorchBase torchInstance) : base(torchInstance)
{
@@ -32,9 +34,12 @@ namespace Torch.Server.Managers
}
/// <inheritdoc />
public override void Init()
public override void Attach()
{
LoadInstance(Torch.Config.InstancePath);
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)
@@ -43,8 +48,10 @@ namespace Torch.Server.Managers
ValidateInstance(path);
MyFileSystem.Reset();
MyFileSystem.ExePath = Path.Combine(Torch.GetManager<FilesystemManager>().TorchDirectory, "DedicatedServer64");
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);
var configPath = Path.Combine(path, CONFIG_NAME);
if (!File.Exists(configPath))
@@ -68,6 +75,8 @@ namespace Torch.Server.Managers
return;
}
ImportWorldConfig();
/*
if (string.IsNullOrEmpty(DedicatedConfig.LoadWorld))
{
@@ -79,13 +88,13 @@ namespace Torch.Server.Managers
public void SelectWorld(string worldPath, bool modsOnly = true)
{
DedicatedConfig.LoadWorld = worldPath;
LoadWorldMods(modsOnly);
ImportWorldConfig(modsOnly);
}
private void LoadWorldMods(bool modsOnly = true)
private void ImportWorldConfig(bool modsOnly = true)
{
if (DedicatedConfig.LoadWorld == null)
if (string.IsNullOrEmpty(DedicatedConfig.LoadWorld))
return;
var sandboxPath = Path.Combine(DedicatedConfig.LoadWorld, "Sandbox.sbc");
@@ -93,28 +102,36 @@ namespace Torch.Server.Managers
if (!File.Exists(sandboxPath))
return;
MyObjectBuilderSerializer.DeserializeXML(sandboxPath, out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes);
if (checkpoint == null)
try
{
Log.Error($"Failed to load {DedicatedConfig.LoadWorld}, checkpoint null ({sizeInBytes} bytes, instance {TorchBase.Instance.Config.InstancePath})");
return;
MyObjectBuilderSerializer.DeserializeXML(sandboxPath, out MyObjectBuilder_Checkpoint checkpoint, out ulong sizeInBytes);
if (checkpoint == null)
{
Log.Error($"Failed to load {DedicatedConfig.LoadWorld}, checkpoint null ({sizeInBytes} bytes, instance {TorchBase.Instance.Config.InstancePath})");
return;
}
var sb = new StringBuilder();
foreach (var mod in checkpoint.Mods)
sb.AppendLine(mod.PublishedFileId.ToString());
DedicatedConfig.Mods = sb.ToString();
Log.Debug("Loaded mod list from world");
if (!modsOnly)
DedicatedConfig.SessionSettings = new SessionSettingsViewModel(checkpoint.Settings);
}
catch (Exception e)
{
Log.Error($"Error loading mod list from world, verify that your mod list is accurate. '{DedicatedConfig.LoadWorld}'.");
Log.Error(e);
}
var sb = new StringBuilder();
foreach (var mod in checkpoint.Mods)
sb.AppendLine(mod.PublishedFileId.ToString());
DedicatedConfig.Mods = sb.ToString();
Log.Info("Loaded mod list from world");
if (!modsOnly)
DedicatedConfig.SessionSettings = new SessionSettingsViewModel(checkpoint.Settings);
}
public void SaveConfig()
{
DedicatedConfig.Model.Save();
DedicatedConfig.Save();
Log.Info("Saved dedicated config.");
try

View File

@@ -25,6 +25,7 @@ using System.IO.Compression;
using System.Net;
using System.Security.Policy;
using Torch.Server.Managers;
using Torch.Utils;
using VRage.FileSystem;
using VRageRender;
@@ -32,264 +33,32 @@ namespace Torch.Server
{
internal static class Program
{
private static ITorchServer _server;
private static Logger _log = LogManager.GetLogger("Torch");
private static bool _restartOnCrash;
private static TorchConfig _config;
private static bool _steamCmdDone;
/// <summary>
/// <remarks>
/// This method must *NOT* load any types/assemblies from the vanilla game, otherwise automatic updates will fail.
/// </summary>
/// </remarks>
[STAThread]
public static void Main(string[] args)
{
//Ensures that all the files are downloaded in the Torch directory.
Directory.SetCurrentDirectory(new FileInfo(typeof(Program).Assembly.Location).Directory.ToString());
foreach (var file in Directory.GetFiles(Directory.GetCurrentDirectory(), "*.old"))
File.Delete(file);
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
var workingDir = new FileInfo(typeof(Program).Assembly.Location).Directory.ToString();
var binDir = Path.Combine(workingDir, "DedicatedServer64");
Directory.SetCurrentDirectory(workingDir);
if (!Environment.UserInteractive)
{
using (var service = new TorchService())
using (new TorchAssemblyResolver(binDir))
{
ServiceBase.Run(service);
}
return;
}
//CommandLine reflection triggers assembly loading, so DS update must be completely separated.
if (!args.Contains("-noupdate"))
{
if (!Directory.Exists("DedicatedServer64"))
{
_log.Error("Game libraries not found. Press the Enter key to install the dedicated server.");
Console.ReadLine();
}
RunSteamCmd();
}
InitConfig();
if (!_config.Parse(args))
var initializer = new Initializer(workingDir);
if (!initializer.Initialize(args))
return;
if (!string.IsNullOrEmpty(_config.WaitForPID))
{
try
{
var pid = int.Parse(_config.WaitForPID);
var waitProc = Process.GetProcessById(pid);
_log.Warn($"Waiting for process {pid} to exit.");
waitProc.WaitForExit();
_log.Info("Continuing in 5 seconds.");
Thread.Sleep(5000);
}
catch
{
// ignored
}
}
_restartOnCrash = _config.RestartOnCrash;
RunServer(_config);
}
public static void InitConfig()
{
var configName = "Torch.cfg";
var configPath = Path.Combine(Directory.GetCurrentDirectory(), configName);
if (File.Exists(configName))
{
_log.Info($"Loading config {configPath}");
_config = TorchConfig.LoadFrom(configPath);
}
else
{
_log.Info($"Generating default config at {configPath}");
_config = new TorchConfig { InstancePath = Path.GetFullPath("Instance") };
_config.Save(configPath);
}
}
private const string STEAMCMD_DIR = "steamcmd";
private const string STEAMCMD_ZIP = "temp.zip";
private static readonly string STEAMCMD_PATH = $"{STEAMCMD_DIR}\\steamcmd.exe";
private static readonly string RUNSCRIPT_PATH = $"{STEAMCMD_DIR}\\runscript.txt";
private const string RUNSCRIPT = @"force_install_dir ../
login anonymous
app_update 298740
quit";
public static void RunSteamCmd()
{
if (_steamCmdDone)
return;
var log = LogManager.GetLogger("SteamCMD");
if (!Directory.Exists(STEAMCMD_DIR))
{
Directory.CreateDirectory(STEAMCMD_DIR);
}
if (!File.Exists(RUNSCRIPT_PATH))
File.WriteAllText(RUNSCRIPT_PATH, RUNSCRIPT);
if (!File.Exists(STEAMCMD_PATH))
{
try
{
log.Info("Downloading SteamCMD.");
using (var client = new WebClient())
client.DownloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", STEAMCMD_ZIP);
ZipFile.ExtractToDirectory(STEAMCMD_ZIP, STEAMCMD_DIR);
File.Delete(STEAMCMD_ZIP);
log.Info("SteamCMD downloaded successfully!");
}
catch
{
log.Error("Failed to download SteamCMD, unable to update the DS.");
return;
}
}
log.Info("Checking for DS updates.");
var steamCmdProc = new ProcessStartInfo(STEAMCMD_PATH, "+runscript runscript.txt")
{
WorkingDirectory = Path.Combine(Directory.GetCurrentDirectory(), STEAMCMD_DIR),
UseShellExecute = false,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.ASCII
};
var cmd = Process.Start(steamCmdProc);
// ReSharper disable once PossibleNullReferenceException
while (!cmd.HasExited)
{
log.Info(cmd.StandardOutput.ReadLine());
Thread.Sleep(100);
}
_steamCmdDone = true;
}
public static void RunServer(TorchConfig config)
{
/*
if (!parser.ParseArguments(args, config))
{
_log.Error($"Parsing arguments failed: {string.Join(" ", args)}");
return;
}
if (!string.IsNullOrEmpty(config.Config) && File.Exists(config.Config))
{
config = ServerConfig.LoadFrom(config.Config);
parser.ParseArguments(args, config);
}*/
//RestartOnCrash autostart autosave=15
//gamepath ="C:\Program Files\Space Engineers DS" instance="Hydro Survival" instancepath="C:\ProgramData\SpaceEngineersDedicated\Hydro Survival"
/*
if (config.InstallService)
{
var serviceName = $"\"Torch - {config.InstanceName}\"";
// Working on installing the service properly instead of with sc.exe
_log.Info($"Installing service '{serviceName}");
var exePath = $"\"{Assembly.GetExecutingAssembly().Location}\"";
var createInfo = new ServiceCreateInfo
{
Name = config.InstanceName,
BinaryPath = exePath,
};
_log.Info("Service Installed");
var runArgs = string.Join(" ", args.Skip(1));
_log.Info($"Installing Torch as a service with arguments '{runArgs}'");
var startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"create Torch binPath=\"{Assembly.GetExecutingAssembly().Location} {runArgs}\"",
CreateNoWindow = true,
UseShellExecute = true,
Verb = "runas"
};
Process.Start(startInfo).WaitForExit();
_log.Info("Torch service installed");
return;
}
if (config.UninstallService)
{
_log.Info("Uninstalling Torch service");
var startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = "delete Torch",
CreateNoWindow = true,
UseShellExecute = true,
Verb = "runas"
};
Process.Start(startInfo).WaitForExit();
_log.Info("Torch service uninstalled");
return;
}*/
_server = new TorchServer(config);
_server.Init();
if (config.NoGui || config.Autostart)
{
new Thread(() => _server.Start()).Start();
}
if (!config.NoGui)
{
var ui = new TorchUI((TorchServer)_server);
ui.ShowDialog();
}
}
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
var basePath = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "DedicatedServer64");
string asmPath = Path.Combine(basePath, new AssemblyName(args.Name).Name + ".dll");
if (File.Exists(asmPath))
return Assembly.LoadFrom(asmPath);
}
catch
{
// ignored
}
return null;
}
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var ex = (Exception)e.ExceptionObject;
_log.Fatal(ex);
Console.WriteLine("Exiting in 5 seconds.");
Thread.Sleep(5000);
if (_restartOnCrash)
{
var exe = typeof(Program).Assembly.Location;
_config.WaitForPID = Process.GetCurrentProcess().Id.ToString();
Process.Start(exe, _config.ToString());
}
//1627 = Function failed during execution.
Environment.Exit(1627);
initializer.Run();
}
}
}

View File

@@ -1,4 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyVersion("1.1.207.7")]
[assembly: AssemblyFileVersion("1.1.207.7")]
[assembly: AssemblyTitle("Torch Server")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -1,16 +0,0 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System.Reflection;
<# var dt = DateTime.Now;
int major = 1;
int minor = 1;
int build = dt.DayOfYear;
int rev = (int)dt.TimeOfDay.TotalMinutes / 2;
#>
[assembly: AssemblyVersion("<#= major #>.<#= minor #>.<#= build #>.<#= rev #>")]
[assembly: AssemblyFileVersion("<#= major #>.<#= minor #>.<#= build #>.<#= rev #>")]

View File

@@ -2,8 +2,6 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -15,10 +13,12 @@
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
@@ -27,7 +27,7 @@
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
@@ -35,7 +35,7 @@
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<DocumentationFile>bin\x64\Release\Torch.Server.xml</DocumentationFile>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Server.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Torch.Server.Program</StartupObject>
@@ -63,7 +63,8 @@
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.11\lib\net45\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Sandbox.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
@@ -124,6 +125,7 @@
<Reference Include="VRage.Audio, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\VRage.Audio.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VRage.Dedicated, Version=1.0.0.0, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
@@ -185,14 +187,14 @@
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="ListBoxExtensions.cs" />
<Compile Include="Managers\InstanceManager.cs" />
<Compile Include="NativeMethods.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>AssemblyInfo.tt</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo1.cs" />
<Compile Include="Initializer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ServerStatistics.cs" />
<Compile Include="TorchConfig.cs" />
<Compile Include="TorchService.cs">
@@ -289,12 +291,12 @@
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
<Private>True</Private>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
<Private>True</Private>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
@@ -357,24 +359,10 @@
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<ItemGroup>
<Content Include="Properties\AssemblyInfo.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>AssemblyInfo.cs</LastGenOutput>
</Content>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup>
<PostBuildEvent>cd "$(TargetDir)"
copy "$(SolutionDir)NLog.config" "$(TargetDir)"
"Torch Server Release.bat"</PostBuildEvent>
<PostBuildEvent>copy "$(SolutionDir)NLog.config" "$(TargetDir)"</PostBuildEvent>
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -8,6 +8,7 @@ using NLog;
namespace Torch.Server
{
// TODO: redesign this gerbage
public class TorchConfig : CommandLine, ITorchConfig
{
private static Logger _log = LogManager.GetLogger("Config");

View File

@@ -19,6 +19,7 @@ using SteamSDK;
using Torch.API;
using Torch.Managers;
using Torch.Server.Managers;
using Torch.Utils;
using VRage.Dedicated;
using VRage.FileSystem;
using VRage.Game;
@@ -29,6 +30,7 @@ using VRage.Library;
using VRage.ObjectBuilders;
using VRage.Plugins;
using VRage.Utils;
#pragma warning disable 618
namespace Torch.Server
@@ -77,10 +79,9 @@ namespace Torch.Server
MySessionComponentExtDebug.ForceDisable = true;
MyPerServerSettings.AppId = 244850;
MyFinalBuildConstants.APP_VERSION = MyPerGameSettings.BasicGameInfo.GameVersion;
MyObjectBuilderSerializer.RegisterFromAssembly(typeof(MyObjectBuilder_CheckpointSerializer).Assembly);
InvokeBeforeRun();
//MyObjectBuilderSerializer.RegisterFromAssembly(typeof(MyObjectBuilder_CheckpointSerializer).Assembly);
MyPlugins.RegisterGameAssemblyFile(MyPerGameSettings.GameModAssembly);
MyPlugins.RegisterGameObjectBuildersAssemblyFile(MyPerGameSettings.GameModObjBuildersAssembly);
MyPlugins.RegisterSandboxAssemblyFile(MyPerGameSettings.SandboxAssembly);
@@ -88,6 +89,7 @@ namespace Torch.Server
MyPlugins.Load();
MyGlobalTypeMetadata.Static.Init();
GetManager<InstanceManager>().LoadInstance(Config.InstancePath);
Plugins.LoadPlugins();
}
@@ -121,6 +123,9 @@ namespace Torch.Server
MySandboxGame.Config.Load();
}
[ReflectedStaticMethod(Type = typeof(DedicatedServer), Name = "RunInternal")]
private static Action _dsRunInternal;
/// <inheritdoc />
public override void Start()
{
@@ -131,19 +136,26 @@ namespace Torch.Server
_uptime = Stopwatch.StartNew();
IsRunning = true;
GameThread = Thread.CurrentThread;
Config.Save();
State = ServerState.Starting;
Log.Info("Starting server.");
var runInternal = typeof(DedicatedServer).GetMethod("RunInternal", BindingFlags.Static | BindingFlags.NonPublic);
MySandboxGame.IsDedicated = true;
Environment.SetEnvironmentVariable("SteamAppId", MyPerServerSettings.AppId.ToString());
VRage.Service.ExitListenerSTA.OnExit += delegate { MySandboxGame.Static?.Exit(); };
base.Start();
runInternal.Invoke(null, null);
// 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;
@@ -179,7 +191,8 @@ namespace Torch.Server
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
{
var mainThread = MySandboxGame.Static.UpdateThread;
mainThread.Suspend();
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}");
}

View File

@@ -14,13 +14,15 @@ namespace Torch.Server
{
public const string Name = "Torch (SEDS)";
private TorchServer _server;
private static Logger _log = LogManager.GetLogger("Torch");
private Initializer _initializer;
public TorchService()
{
ServiceName = Name;
var workingDir = new FileInfo(typeof(TorchService).Assembly.Location).Directory.ToString();
Directory.SetCurrentDirectory(workingDir);
_initializer = new Initializer(workingDir);
CanHandlePowerEvent = true;
ServiceName = Name;
CanHandleSessionChangeEvent = false;
CanPauseAndContinue = false;
CanStop = true;
@@ -31,17 +33,8 @@ namespace Torch.Server
{
base.OnStart(args);
string configName = args.Length > 0 ? args[0] : "TorchConfig.xml";
var options = new TorchConfig("Torch");
if (File.Exists(configName))
options = TorchConfig.LoadFrom(configName);
else
options.Save(configName);
_server = new TorchServer(options);
_server.Init();
_server.RunArgs = args;
Task.Run(() => _server.Start());
_initializer.Initialize(args);
_initializer.Run();
}
/// <inheritdoc />
@@ -50,17 +43,5 @@ namespace Torch.Server
_server.Stop();
base.OnStop();
}
/// <inheritdoc />
protected override void OnShutdown()
{
base.OnShutdown();
}
/// <inheritdoc />
protected override bool OnPowerEvent(PowerBroadcastStatus powerStatus)
{
return base.OnPowerEvent(powerStatus);
}
}
}

View File

@@ -74,6 +74,12 @@ namespace Torch.Server.ViewModels
{
get => _settings.HackSpeedMultiplier; set { _settings.HackSpeedMultiplier = value; OnPropertyChanged(); }
}
/// <inheritdoc cref="MyObjectBuilder_SessionSettings.WelderSpeedMultiplier"/>
public float WelderSpeedMultiplier
{
get => _settings.WelderSpeedMultiplier; set { _settings.WelderSpeedMultiplier = value; OnPropertyChanged(); }
}
#endregion
#region NPCs

View File

@@ -49,12 +49,15 @@ namespace Torch.Server
private void ChatHistory_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ChatItems.ScrollToItem(ChatItems.Items.Count - 1);
/*
if (VisualTreeHelper.GetChildrenCount(ChatItems) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(ChatItems, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}*/
}
private void SendButton_Click(object sender, RoutedEventArgs e)

View File

@@ -104,6 +104,10 @@
<TextBox Text="{Binding AssemblerSpeedMultiplier}" Margin="3" Width="70" />
<Label Content="Assembler Speed" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding WelderSpeedMultiplier}" Margin="3" Width="70" />
<Label Content="Welder Speed" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding GrinderSpeedMultiplier}" Margin="3" Width="70" />
<Label Content="Grinder Speed" />

View File

@@ -1,34 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Xml.Serialization;
using NLog;
using Sandbox;
using Sandbox.Engine.Networking;
using Sandbox.Engine.Utils;
using Torch.API.Managers;
using Torch.Server.Managers;
using Torch.Server.ViewModels;
using Torch.Views;
using VRage;
using VRage.Dedicated;
using VRage.Game;
using VRage.ObjectBuilders;
using Path = System.IO.Path;
namespace Torch.Server.Views
{
@@ -42,7 +16,7 @@ namespace Torch.Server.Views
public ConfigControl()
{
InitializeComponent();
_instanceManager = TorchBase.Instance.GetManager<InstanceManager>();
_instanceManager = TorchBase.Instance.Managers.GetManager<InstanceManager>();
DataContext = _instanceManager.DedicatedConfig;
}

View File

@@ -39,7 +39,7 @@
</StackPanel>
<TabControl Grid.Row="1" Height="Auto" x:Name="TabControl" Margin="5,0,5,5">
<TabItem Header="Configuration">
<Grid>
<Grid IsEnabled="{Binding IsRunning, Converter={StaticResource InverseBool}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>

View File

@@ -65,7 +65,7 @@ namespace Torch.Server
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
_config.Save();
_server.GetManager<InstanceManager>().SaveConfig();
new Thread(_server.Start).Start();
}
@@ -80,7 +80,6 @@ namespace Torch.Server
_config.WindowSize = newSize;
var newPos = new Point((int)Left, (int)Top);
_config.WindowPosition = newPos;
_config.Save();
if (_server?.State == ServerState.Running)
_server.Stop();

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
<package id="NLog" version="4.4.11" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
</packages>

View File

@@ -1,12 +1,17 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Torch Client")]
[assembly: AssemblyTitle("Torch Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: ComVisible(false)]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using Torch.Utils;
using Xunit;
namespace Torch.Tests
{
public class ReflectionSystemTest
{
static ReflectionSystemTest()
{
TestUtils.Init();
}
private static ReflectionTestManager _manager = new ReflectionTestManager().Init(typeof(ReflectionTestBinding));
public static IEnumerable<object[]> Getters => _manager.Getters;
public static IEnumerable<object[]> Setters => _manager.Setters;
public static IEnumerable<object[]> Invokers => _manager.Invokers;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
public void TestBindingGetter(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(Setters))]
public void TestBindingSetter(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(Invokers))]
public void TestBindingInvoker(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
#endregion
#region Results
#region Dummy
private class ReflectionTestTarget
{
public int TestField;
public int TestProperty { get; set; }
/// <summary>
/// Return true when greater or equal than 0
/// </summary>
public bool TestCall(int k)
{
return k >= 0;
}
public static int TestFieldStatic;
public static int TestPropertyStatic { get; set; }
/// <summary>
/// Return true when greater or equal than 0
/// </summary>
public static bool TestCallStatic(int k)
{
return k >= 0;
}
}
private class ReflectionTestBinding
{
[ReflectedGetter(Name = "TestField")]
public static Func<ReflectionTestTarget, int> TestFieldGetter;
[ReflectedSetter(Name = "TestField")]
public static Action<ReflectionTestTarget, int> TestFieldSetter;
[ReflectedGetter(Name = "TestProperty")]
public static Func<ReflectionTestTarget, int> TestPropertyGetter;
[ReflectedSetter(Name = "TestProperty")]
public static Action<ReflectionTestTarget, int> TestPropertySetter;
[ReflectedMethod]
public static Func<ReflectionTestTarget, int, bool> TestCall;
[ReflectedGetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
public static Func<int> TestStaticFieldGetter;
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
public static Action<int> TestStaticFieldSetter;
[ReflectedGetter(Name = "TestPropertyStatic", Type = typeof(ReflectionTestTarget))]
public static Func<int> TestStaticPropertyGetter;
[ReflectedSetter(Name = "TestPropertyStatic", Type = typeof(ReflectionTestTarget))]
public static Action<int> TestStaticPropertySetter;
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
public static Func<int, bool> TestCallStatic;
}
#endregion
private readonly Random _rand = new Random();
private int AcquireRandomNum()
{
return _rand.Next();
}
#region Instance
[Fact]
public void TestInstanceFieldGet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
var target = new ReflectionTestTarget
{
TestField = testNumber
};
Assert.Equal(testNumber, ReflectionTestBinding.TestFieldGetter.Invoke(target));
}
[Fact]
public void TestInstanceFieldSet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
var target = new ReflectionTestTarget();
ReflectionTestBinding.TestFieldSetter.Invoke(target, testNumber);
Assert.Equal(testNumber, target.TestField);
}
[Fact]
public void TestInstancePropertyGet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
var target = new ReflectionTestTarget
{
TestProperty = testNumber
};
Assert.Equal(testNumber, ReflectionTestBinding.TestPropertyGetter.Invoke(target));
}
[Fact]
public void TestInstancePropertySet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
var target = new ReflectionTestTarget();
ReflectionTestBinding.TestPropertySetter.Invoke(target, testNumber);
Assert.Equal(testNumber, target.TestProperty);
}
[Fact]
public void TestInstanceInvoke()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
var target = new ReflectionTestTarget();
Assert.True(ReflectionTestBinding.TestCall.Invoke(target, 1));
Assert.False(ReflectionTestBinding.TestCall.Invoke(target, -1));
}
#endregion
#region Static
[Fact]
public void TestStaticFieldGet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
ReflectionTestTarget.TestFieldStatic = testNumber;
Assert.Equal(testNumber, ReflectionTestBinding.TestStaticFieldGetter.Invoke());
}
[Fact]
public void TestStaticFieldSet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
ReflectionTestBinding.TestStaticFieldSetter.Invoke(testNumber);
Assert.Equal(testNumber, ReflectionTestTarget.TestFieldStatic);
}
[Fact]
public void TestStaticPropertyGet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
ReflectionTestTarget.TestPropertyStatic = testNumber;
Assert.Equal(testNumber, ReflectionTestBinding.TestStaticPropertyGetter.Invoke());
}
[Fact]
public void TestStaticPropertySet()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
int testNumber = AcquireRandomNum();
ReflectionTestBinding.TestStaticPropertySetter.Invoke(testNumber);
Assert.Equal(testNumber, ReflectionTestTarget.TestPropertyStatic);
}
[Fact]
public void TestStaticInvoke()
{
ReflectedManager.Process(typeof(ReflectionTestBinding));
Assert.True(ReflectionTestBinding.TestCallStatic.Invoke(1));
Assert.False(ReflectionTestBinding.TestCallStatic.Invoke(-1));
}
#endregion
#endregion
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Torch.Utils;
namespace Torch.Tests
{
public class ReflectionTestManager
{
#region FieldProvider
public struct FieldRef
{
public FieldInfo Field;
public FieldRef(FieldInfo f)
{
Field = f;
}
public override string ToString()
{
if (Field == null)
return "Ignored";
return Field.DeclaringType?.FullName + "." + Field.Name;
}
}
private readonly HashSet<object[]> _getters = new HashSet<object[]>();
private readonly HashSet<object[]> _setters = new HashSet<object[]>();
private readonly HashSet<object[]> _invokers = 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) });
}
public ReflectionTestManager Init(Assembly asm)
{
foreach (Type type in asm.GetTypes())
Init(type);
return this;
}
public ReflectionTestManager Init(Type type)
{
foreach (FieldInfo field in type.GetFields(BindingFlags.Static |
BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic))
{
if (field.GetCustomAttribute<ReflectedMethodAttribute>() != null)
_invokers.Add(new object[] { new FieldRef(field) });
if (field.GetCustomAttribute<ReflectedGetterAttribute>() != null)
_getters.Add(new object[] { new FieldRef(field) });
if (field.GetCustomAttribute<ReflectedSetterAttribute>() != null)
_setters.Add(new object[] { new FieldRef(field) });
}
return this;
}
public IEnumerable<object[]> Getters => _getters;
public IEnumerable<object[]> Setters => _setters;
public IEnumerable<object[]> Invokers => _invokers;
#endregion
}
}

35
Torch.Tests/TestUtils.cs Normal file
View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Torch.Utils;
namespace Torch.Tests
{
public sealed class TestUtils
{
public static void Init()
{
if (_torchResolver == null)
_torchResolver = new TorchAssemblyResolver(GetGameBinaries());
}
private static string GetGameBinaries()
{
string dir = Environment.CurrentDirectory;
while (!string.IsNullOrWhiteSpace(dir))
{
string gameBin = Path.Combine(dir, "GameBinaries");
if (Directory.Exists(gameBin))
return gameBin;
dir = Path.GetDirectoryName(dir);
}
throw new Exception("GetGameBinaries failed to find a folder named GameBinaries in the directory tree");
}
private static TorchAssemblyResolver _torchResolver;
}
}

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<ProjectGuid>{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Torch.Tests</RootNamespace>
<AssemblyName>Torch.Tests</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<NoWarn>1591,0649</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>$(SolutionDir)\bin-test\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>$(SolutionDir)\bin-test\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>$(SolutionDir)\bin-test\x64\Release\Torch.Tests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.assert, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.assert.2.2.0\lib\netstandard1.1\xunit.assert.dll</HintPath>
</Reference>
<Reference Include="xunit.core, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.core.2.2.0\lib\netstandard1.1\xunit.core.dll</HintPath>
</Reference>
<Reference Include="xunit.execution.desktop, Version=2.2.0.3545, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\packages\xunit.extensibility.execution.2.2.0\lib\net452\xunit.execution.desktop.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReflectionTestManager.cs" />
<Compile Include="ReflectionSystemTest.cs" />
<Compile Include="TestUtils.cs" />
<Compile Include="TorchReflectionTest.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
</ProjectReference>
<ProjectReference Include="..\Torch\Torch.csproj">
<Project>{7e01635c-3b67-472e-bcd6-c5539564f214}</Project>
<Name>Torch</Name>
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -0,0 +1,65 @@
using System.Collections.Generic;
using Torch.Utils;
using Xunit;
namespace Torch.Tests
{
public class TorchReflectionTest
{
static TorchReflectionTest()
{
TestUtils.Init();
}
private static ReflectionTestManager _manager;
private static ReflectionTestManager Manager()
{
if (_manager != null)
return _manager;
return _manager = new ReflectionTestManager().Init(typeof(TorchBase).Assembly);
}
public static IEnumerable<object[]> Getters => Manager().Getters;
public static IEnumerable<object[]> Setters => Manager().Setters;
public static IEnumerable<object[]> Invokers => Manager().Invokers;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
public void TestBindingGetter(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(Setters))]
public void TestBindingSetter(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(Invokers))]
public void TestBindingInvoker(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="xunit" version="2.2.0" targetFramework="net461" />
<package id="xunit.abstractions" version="2.0.1" targetFramework="net461" />
<package id="xunit.assert" version="2.2.0" targetFramework="net461" />
<package id="xunit.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.core" version="2.2.0" targetFramework="net461" />
<package id="xunit.extensibility.execution" version="2.2.0" targetFramework="net461" />
<package id="xunit.runner.console" version="2.2.0" targetFramework="net461" developmentDependency="true" />
</packages>

View File

@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.14
VisualStudioVersion = 15.0.26730.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch", "Torch\Torch.csproj", "{7E01635C-3B67-472E-BCD6-C5539564F214}"
EndProject
@@ -16,6 +16,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
NLog.config = NLog.config
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Tests", "Torch.Tests\Torch.Tests.csproj", "{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Server.Tests", "Torch.Server.Tests\Torch.Server.Tests.csproj", "{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Torch.Client.Tests", "Torch.Client.Tests\Torch.Client.Tests.csproj", "{632E78C0-0DAC-4B71-B411-2F1B333CC310}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Versioning", "Versioning", "{762F6A0D-55EF-4173-8CDE-309D183F40C4}"
ProjectSection(SolutionItems) = preProject
Versioning\AssemblyVersion.cs = Versioning\AssemblyVersion.cs
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -38,8 +49,26 @@ Global
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Debug|x64.Build.0 = Debug|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.ActiveCfg = Release|x64
{CA50886B-7B22-4CD8-93A0-C06F38D4F77D}.Release|x64.Build.0 = Release|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|x64.ActiveCfg = Debug|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Debug|x64.Build.0 = Debug|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|x64.ActiveCfg = Release|x64
{C3C8B671-6AD1-44AA-A8DA-E0C0DC0FEDF5}.Release|x64.Build.0 = Release|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|x64.ActiveCfg = Debug|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Debug|x64.Build.0 = Debug|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.ActiveCfg = Release|x64
{9EFD1D91-2FA2-47ED-B537-D8BC3B0E543E}.Release|x64.Build.0 = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.ActiveCfg = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Debug|x64.Build.0 = Debug|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.ActiveCfg = Release|x64
{632E78C0-0DAC-4B71-B411-2F1B333CC310}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{762F6A0D-55EF-4173-8CDE-309D183F40C4} = {7AD02A71-1D4C-48F9-A8C1-789A5512424F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB51D91F-958D-4B63-A897-3C40642ACD3E}
EndGlobalSection
EndGlobal

View File

@@ -62,7 +62,7 @@ namespace Torch.Commands
_parameters = commandMethod.GetParameters();
var sb = new StringBuilder();
sb.Append($"/{string.Join(" ", Path)} ");
sb.Append($"!{string.Join(" ", Path)} ");
for (var i = 0; i < _parameters.Length; i++)
{
var param = _parameters[i];

View File

@@ -20,16 +20,18 @@ namespace Torch.Commands
public CommandTree Commands { get; set; } = new CommandTree();
private Logger _log = LogManager.GetLogger(nameof(CommandManager));
[Dependency]
private ChatManager _chatManager;
public CommandManager(ITorchBase torch, char prefix = '!') : base(torch)
{
Prefix = prefix;
}
public override void Init()
public override void Attach()
{
RegisterCommandModule(typeof(TorchCommands));
Torch.GetManager<ChatManager>().MessageRecieved += HandleCommand;
_chatManager.MessageRecieved += HandleCommand;
}
public bool HasPermission(ulong steamId, Command command)
@@ -40,7 +42,7 @@ namespace Torch.Commands
public bool IsCommand(string command)
{
return command.Length > 1 && command[0] == Prefix;
return !string.IsNullOrEmpty(command) && command[0] == Prefix;
}
public void RegisterCommandModule(Type moduleType, ITorchPlugin plugin = null)

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Sandbox.ModAPI;
using Torch;
using Torch.API.Managers;
using Torch.Commands.Permissions;
using Torch.Managers;
using VRage.Game.ModAPI;
@@ -47,7 +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.GetManager<CommandManager>();
var commandManager = Context.Torch.Managers.GetManager<CommandManager>();
commandManager.Commands.GetNode(Context.Args, out CommandTree.CommandNode node);
if (node != null)
@@ -106,12 +110,48 @@ namespace Torch.Commands
}
[Command("restart", "Restarts the server.")]
public void Restart(bool save = true)
public void Restart(int countdownSeconds = 10, bool save = true)
{
Context.Respond("Restarting server.");
if (save)
Context.Torch.Save(Context.Player?.IdentityId ?? 0).Wait();
Context.Torch.Restart();
Task.Run(() =>
{
var countdown = RestartCountdown(countdownSeconds).GetEnumerator();
while (countdown.MoveNext())
{
Thread.Sleep(1000);
}
});
}
private IEnumerable RestartCountdown(int countdown)
{
for (var i = countdown; i >= 0; i--)
{
if (i >= 60 && i % 60 == 0)
{
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.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;
}
}
}
private string Pluralize(int num)
{
return num == 1 ? "" : "s";
}
/// <summary>

View File

@@ -27,16 +27,19 @@ namespace Torch.Managers
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 Init()
public override void Attach()
{
try
{
Torch.GetManager<INetworkManager>().RegisterNetworkHandler(new ChatIntercept(this));
_networkManager.RegisterNetworkHandler(new ChatIntercept(this));
}
catch
{
@@ -45,7 +48,7 @@ namespace Torch.Managers
}
}
private void Static_ChatMessageReceived(ulong arg1, string arg2, SteamSDK.ChatEntryTypeEnum arg3)
private void Static_ChatMessageReceived(ulong arg1, string arg2)
{
var msg = new ChatMsg {Author = arg1, Text = arg2};
var sendToOthers = true;

View File

@@ -0,0 +1,309 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NLog;
using Torch.API.Managers;
namespace Torch.Managers
{
public sealed class DependencyManager : IDependencyManager
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private class DependencyInfo
{
private readonly Manager.DependencyAttribute _attribute;
internal Type DependencyType => Field.FieldType;
internal FieldInfo Field { get; private set; }
internal bool Optional => _attribute.Optional;
internal bool Ordered => _attribute.Ordered;
public DependencyInfo(FieldInfo field)
{
Field = field;
_attribute = field.GetCustomAttribute<Manager.DependencyAttribute>();
}
}
/// <summary>
/// Represents a registered instance of a manager.
/// </summary>
private class ManagerInstance
{
public IManager Instance { get; private set; }
internal readonly List<DependencyInfo> Dependencies;
internal readonly HashSet<Type> SuppliedManagers;
internal readonly HashSet<ManagerInstance> Dependents;
public ManagerInstance(IManager manager)
{
Instance = manager;
SuppliedManagers = new HashSet<Type>();
Dependencies = new List<DependencyInfo>();
Dependents = new HashSet<ManagerInstance>();
var openBases = new Queue<Type>();
openBases.Enqueue(manager.GetType());
while (openBases.TryDequeue(out Type type))
{
if (!SuppliedManagers.Add(type))
continue;
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
if (field.HasAttribute<Manager.DependencyAttribute>())
Dependencies.Add(new DependencyInfo(field));
foreach (Type parent in type.GetInterfaces())
openBases.Enqueue(parent);
if (type.BaseType != null)
openBases.Enqueue(type.BaseType);
}
}
/// <summary>
/// Used by <see cref="DependencyManager"/> internally to topologically sort the dependency list.
/// </summary>
public int UnsolvedDependencies { get; set; }
}
private readonly Dictionary<Type, ManagerInstance> _dependencySatisfaction;
private readonly List<ManagerInstance> _registeredManagers;
private readonly List<ManagerInstance> _orderedManagers;
private readonly IDependencyProvider[] _parentProviders;
public DependencyManager(params IDependencyProvider[] parents)
{
_dependencySatisfaction = new Dictionary<Type, ManagerInstance>();
_registeredManagers = new List<ManagerInstance>();
_orderedManagers = new List<ManagerInstance>();
_parentProviders = parents.Distinct().ToArray();
}
private void AddDependencySatisfaction(ManagerInstance instance)
{
foreach (Type supplied in instance.SuppliedManagers)
if (_dependencySatisfaction.ContainsKey(supplied))
// When we already have a manager supplying this component we have to unregister it.
_dependencySatisfaction[supplied] = null;
else
_dependencySatisfaction.Add(supplied, instance);
}
private void RebuildDependencySatisfaction()
{
_dependencySatisfaction.Clear();
foreach (ManagerInstance manager in _registeredManagers)
AddDependencySatisfaction(manager);
}
/// <inheritdoc/>
public bool AddManager(IManager manager)
{
if (_initialized)
throw new InvalidOperationException("Can't add new managers to an initialized dependency manager");
// Protect against adding a manager derived from an existing manager
if (_registeredManagers.Any(x => x.Instance.GetType().IsInstanceOfType(manager)))
return false;
// Protect against adding a manager when an existing manager derives from it.
// ReSharper disable once ConvertIfStatementToReturnStatement
if (_registeredManagers.Any(x => manager.GetType().IsInstanceOfType(x.Instance)))
return false;
ManagerInstance instance = new ManagerInstance(manager);
_registeredManagers.Add(instance);
AddDependencySatisfaction(instance);
return true;
}
/// <inheritdoc/>
public void ClearManagers()
{
if (_initialized)
throw new InvalidOperationException("Can't remove managers from an initialized dependency manager");
_registeredManagers.Clear();
_dependencySatisfaction.Clear();
}
/// <inheritdoc/>
public bool RemoveManager(IManager manager)
{
if (_initialized)
throw new InvalidOperationException("Can't remove managers from an initialized dependency manager");
if (manager == null)
return false;
for (int i = 0; i < _registeredManagers.Count; i++)
if (_registeredManagers[i].Instance == manager)
{
_registeredManagers.RemoveAtFast(i);
RebuildDependencySatisfaction();
return true;
}
return false;
}
private void Sort()
{
// Resets the previous sort results
#region Reset
_orderedManagers.Clear();
foreach (ManagerInstance manager in _registeredManagers)
manager.Dependents.Clear();
#endregion
// Creates the dependency graph
#region Prepare
var dagQueue = new List<ManagerInstance>();
foreach (ManagerInstance manager in _registeredManagers)
{
var inFactor = 0;
foreach (DependencyInfo dependency in manager.Dependencies)
{
if (_dependencySatisfaction.TryGetValue(dependency.DependencyType, out var dependencyInstance))
{
if (dependency.Ordered)
{
inFactor++;
dependencyInstance.Dependents.Add(manager);
}
}
else if (!dependency.Optional && _parentProviders.All(x => x.GetManager(dependency.DependencyType) == null))
_log.Warn("Unable to satisfy dependency {0} ({1}) of {2}.", dependency.DependencyType.Name,
dependency.Field.Name, manager.Instance.GetType().Name);
}
manager.UnsolvedDependencies = inFactor;
dagQueue.Add(manager);
}
#endregion
// Sorts the dependency graph into _orderedManagers
#region Sort
var tmpQueue = new List<ManagerInstance>();
while (dagQueue.Any())
{
tmpQueue.Clear();
for (var i = 0; i < dagQueue.Count; i++)
{
if (dagQueue[i].UnsolvedDependencies == 0)
tmpQueue.Add(dagQueue[i]);
else
dagQueue[i - tmpQueue.Count] = dagQueue[i];
}
dagQueue.RemoveRange(dagQueue.Count - tmpQueue.Count, tmpQueue.Count);
if (tmpQueue.Count == 0)
{
_log.Fatal("Dependency loop detected in the following managers:");
foreach (ManagerInstance manager in dagQueue)
{
_log.Fatal(" + {0} has {1} unsolved dependencies.", manager.Instance.GetType().FullName, manager.UnsolvedDependencies);
_log.Fatal(" - Dependencies: {0}",
string.Join(", ", manager.Dependencies.Select(x => x.DependencyType.Name + (x.Optional ? " (Optional)" : ""))));
_log.Fatal(" - Dependents: {0}",
string.Join(", ", manager.Dependents.Select(x => x.Instance.GetType().Name)));
}
throw new InvalidOperationException("Unable to satisfy all required manager dependencies");
}
// Update the number of unsolved dependencies
foreach (ManagerInstance manager in tmpQueue)
foreach (ManagerInstance dependent in manager.Dependents)
dependent.UnsolvedDependencies--;
// tmpQueue.Sort(); If we have priorities this is where to sort them.
_orderedManagers.AddRange(tmpQueue);
}
_log.Debug("Dependency tree satisfied. Load order is:");
foreach (ManagerInstance manager in _orderedManagers)
{
_log.Debug(" - {0}", manager.Instance.GetType().FullName);
_log.Debug(" - Dependencies: {0}",
string.Join(", ", manager.Dependencies.Select(x => x.DependencyType.Name + (x.Optional ? " (Optional)" : ""))));
_log.Debug(" - Dependents: {0}",
string.Join(", ", manager.Dependents.Select(x => x.Instance.GetType().Name)));
}
#endregion
// Updates the dependency fields with the correct manager instances
#region Satisfy
foreach (ManagerInstance manager in _registeredManagers)
{
manager.Dependents.Clear();
foreach (DependencyInfo dependency in manager.Dependencies)
dependency.Field.SetValue(manager.Instance, GetManager(dependency.DependencyType));
}
#endregion
}
private bool _initialized = false;
/// <summary>
/// Initializes the dependency manager, and all its registered managers.
/// </summary>
public void Attach()
{
if (_initialized)
throw new InvalidOperationException("Can't start the dependency manager more than once");
_initialized = true;
Sort();
foreach (ManagerInstance manager in _orderedManagers)
manager.Instance.Attach();
}
/// <summary>
/// Disposes the dependency manager, and all its registered managers.
/// </summary>
public void Detach()
{
if (!_initialized)
throw new InvalidOperationException("Can't dispose an uninitialized dependency manager");
for (int i = _orderedManagers.Count - 1; i >= 0; i--)
{
_orderedManagers[i].Instance.Detach();
foreach (DependencyInfo field in _orderedManagers[i].Dependencies)
field.Field.SetValue(_orderedManagers[i].Instance, null);
}
_initialized = false;
}
/// <inheritdoc/>
public IEnumerable<IManager> AttachOrder
{
get
{
if (!_initialized)
throw new InvalidOperationException("Can't determine dependency load order when uninitialized");
foreach (ManagerInstance k in _orderedManagers)
yield return k.Instance;
}
}
/// <inheritdoc/>
public IEnumerable<IManager> DetachOrder
{
get
{
if (!_initialized)
throw new InvalidOperationException("Can't determine dependency load order when uninitialized");
for (int i = _orderedManagers.Count - 1; i >= 0; i--)
yield return _orderedManagers[i].Instance;
}
}
/// <inheritdoc/>
public IManager GetManager(Type type)
{
// ReSharper disable once ConvertIfStatementToReturnStatement
if (_dependencySatisfaction.TryGetValue(type, out ManagerInstance mgr))
return mgr.Instance;
foreach (IDependencyProvider provider in _parentProviders)
{
IManager entry = provider.GetManager(type);
if (entry != null)
return entry;
}
return null;
}
}
}

View File

@@ -16,6 +16,46 @@ namespace Torch.Managers
public abstract class Manager : IManager
{
/// <summary>
/// Indicates a field is a dependency of this parent manager.
/// </summary>
/// <example>
/// <code>
/// public class NetworkManager : Manager { }
/// public class ChatManager : Manager {
/// [Dependency(Optional = false)]
/// private NetworkManager _network;
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Field)]
public class DependencyAttribute : Attribute
{
/// <summary>
/// If this dependency isn't required.
/// </summary>
/// <remarks>
/// The tagged field can be null if, and only if, this is true.
/// </remarks>
public bool Optional { get; set; } = false;
/// <summary>
/// Dependency must be loaded before and unloaded after the containing manager.
/// </summary>
/// <example>
/// <code>
/// public class NetworkManager : Manager { }
/// public class ChatManager : Manager {
/// [Dependency(Ordered = true)]
/// private NetworkManager _network;
/// }
/// </code>
/// Load order will be NetworkManager, then ChatManager.
/// Unload order will be ChatManager, then NetworkManager
/// </example>
public bool Ordered { get; set; } = true;
}
protected ITorchBase Torch { get; }
protected Manager(ITorchBase torchInstance)
@@ -23,7 +63,12 @@ namespace Torch.Managers
Torch = torchInstance;
}
public virtual void Init()
public virtual void Attach()
{
}
public virtual void Detach()
{
}

View File

@@ -16,6 +16,7 @@ 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;
@@ -25,11 +26,14 @@ 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
@@ -49,18 +53,27 @@ namespace Torch.Managers
public IMyPlayer LocalPlayer => MySession.Static.LocalHumanPlayer;
private static readonly Logger Log = LogManager.GetLogger(nameof(MultiplayerManager));
private static readonly Logger ChatLog = LogManager.GetLogger("Chat");
private Dictionary<MyPlayer.PlayerId, MyPlayer> _onlinePlayers;
[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 Init()
public override void Attach()
{
Torch.SessionLoaded += OnSessionLoaded;
Torch.GetManager<ChatManager>().MessageRecieved += Instance_MessageRecieved;
_chatManager.MessageRecieved += Instance_MessageRecieved;
}
private void Instance_MessageRecieved(ChatMsg msg, ref bool sendToOthers)
@@ -88,21 +101,19 @@ namespace Torch.Managers
/// <inheritdoc />
public IMyPlayer GetPlayerByName(string name)
{
ValidateOnlinePlayersList();
return _onlinePlayers.FirstOrDefault(x => x.Value.DisplayName == name).Value;
return _onlinePlayers.Invoke(MySession.Static.Players).FirstOrDefault(x => x.Value.DisplayName == name).Value;
}
/// <inheritdoc />
public IMyPlayer GetPlayerBySteamId(ulong steamId)
{
ValidateOnlinePlayersList();
_onlinePlayers.TryGetValue(new MyPlayer.PlayerId(steamId), out MyPlayer p);
_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)
foreach (var kv in _onlinePlayers.Invoke(MySession.Static.Players))
{
if (kv.Value.Identity.IdentityId == identityId)
return kv.Key.SteamId;
@@ -120,11 +131,13 @@ namespace Torch.Managers
/// <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));
var commands = Torch.GetManager<CommandManager>();
if (commands.IsCommand(message))
if (_commandManager.IsCommand(message))
{
var response = commands.HandleCommandFromServer(message);
var response = _commandManager.HandleCommandFromServer(message);
ChatHistory.Add(new ChatMessage(DateTime.Now, 0, author, response));
}
else
@@ -137,39 +150,33 @@ namespace Torch.Managers
return;
var addToGlobalHistoryMethod = typeof(MyCharacter).GetMethod("OnGlobalMessageSuccess", BindingFlags.Instance | BindingFlags.NonPublic);
Torch.GetManager<NetworkManager>().RaiseEvent(addToGlobalHistoryMethod, character, steamId, steamId, message);
_networkManager.RaiseEvent(addToGlobalHistoryMethod, character, steamId, steamId, message);
}
}
private void ValidateOnlinePlayersList()
{
if (_onlinePlayers == null)
_onlinePlayers = MySession.Static.Players.GetPrivateField<Dictionary<MyPlayer.PlayerId, MyPlayer>>("m_players");
}
private void OnSessionLoaded()
{
Log.Info("Initializing Steam auth");
MyMultiplayer.Static.ClientKicked += OnClientKicked;
MyMultiplayer.Static.ClientLeft += OnClientLeft;
ValidateOnlinePlayersList();
//TODO: Move these with the methods?
RemoveHandlers();
SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse += ValidateAuthTicketResponse;
SteamServerAPI.Instance.GameServer.UserGroupStatus += UserGroupStatus;
_members = (List<ulong>)typeof(MyDedicatedServerBase).GetField("m_members", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
_waitingForGroup = (HashSet<ulong>)typeof(MyDedicatedServerBase).GetField("m_waitingForGroup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(MyMultiplayer.Static);
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, ChatMemberStateChangeEnum.Kicked);
OnClientLeft(steamId, MyChatMemberStateChangeEnum.Kicked);
}
private void OnClientLeft(ulong steamId, ChatMemberStateChangeEnum stateChange)
private void OnClientLeft(ulong steamId, MyChatMemberStateChangeEnum stateChange)
{
Players.TryGetValue(steamId, out PlayerViewModel vm);
if (vm == null)
@@ -182,8 +189,12 @@ namespace Torch.Managers
//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)
private List<ulong> _members;
private HashSet<ulong> _waitingForGroup;
[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;
@@ -191,152 +202,137 @@ namespace Torch.Managers
/// <summary>
/// Removes Keen's hooks into some Steam events so we have full control over client authentication
/// </summary>
private static void RemoveHandlers()
private static bool RemoveHandlers()
{
var eventField = typeof(GameServer).GetField("<backing_store>ValidateAuthTicketResponse", BindingFlags.NonPublic | BindingFlags.Instance);
if (eventField?.GetValue(SteamServerAPI.Instance.GameServer) is MulticastDelegate eventDel)
MethodInfo methodValidateAuthTicket = typeof(MyDedicatedServerBase).GetMethod("GameServer_ValidateAuthTicketResponse",
BindingFlags.NonPublic | BindingFlags.Instance);
if (methodValidateAuthTicket == null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_ValidateAuthTicketResponse")
{
SteamServerAPI.Instance.GameServer.ValidateAuthTicketResponse -= handle as ValidateAuthTicketResponse;
Log.Debug("Removed GameServer_ValidateAuthTicketResponse");
}
}
Log.Error("Unable to find the GameServer_ValidateAuthTicketResponse method to unhook");
return false;
}
eventField = typeof(GameServer).GetField("<backing_store>UserGroupStatus", BindingFlags.NonPublic | BindingFlags.Instance);
eventDel = eventField?.GetValue(SteamServerAPI.Instance.GameServer) as MulticastDelegate;
if (eventDel != null)
var eventValidateAuthTicket = Reflection.GetInstanceEvent(MyGameService.GameServer, nameof(MyGameService.GameServer.ValidateAuthTicketResponse))
.FirstOrDefault(x => x.Method == methodValidateAuthTicket) as Action<ulong, JoinResult, ulong>;
if (eventValidateAuthTicket == null)
{
foreach (var handle in eventDel.GetInvocationList())
{
if (handle.Method.Name == "GameServer_UserGroupStatus")
{
SteamServerAPI.Instance.GameServer.UserGroupStatus -= handle as UserGroupStatus;
Log.Debug("Removed GameServer_UserGroupStatus");
}
}
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, AuthSessionResponseEnum response, ulong ownerSteamID)
private void ValidateAuthTicketResponse(ulong steamID, JoinResult response, ulong steamOwner)
{
Log.Info($"Server ValidateAuthTicketResponse ({response}), owner: {ownerSteamID}");
if (steamID != ownerSteamID)
Log.Debug($"ValidateAuthTicketResponse(user={steamID}, response={response}, owner={steamOwner}");
if (IsClientBanned.Invoke(MyMultiplayer.Static, steamOwner) || MySandboxGame.ConfigDedicated.Banned.Contains(steamOwner))
{
Log.Info($"User {steamID} is using a game owned by {ownerSteamID}. Tracking...");
_gameOwnerIds[steamID] = ownerSteamID;
if (MySandboxGame.ConfigDedicated.Banned.Contains(ownerSteamID))
{
Log.Info($"Game owner {ownerSteamID} is banned. Banning and rejecting client {steamID}...");
UserRejected(steamID, JoinResult.BannedByAdmins);
BanPlayer(steamID);
}
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.BannedByAdmins);
RaiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
if (response == AuthSessionResponseEnum.OK)
else if (IsClientKicked.Invoke(MyMultiplayer.Static, steamOwner))
{
if (MySession.Static.MaxPlayers > 0 && _members.Count - 1 >= MySession.Static.MaxPlayers)
{
UserRejected(steamID, JoinResult.ServerFull);
}
else if (MySandboxGame.ConfigDedicated.Administrators.Contains(steamID.ToString()) /*|| MySandboxGame.ConfigDedicated.Administrators.Contains(MyDedicatedServerBase.ConvertSteamIDFrom64(steamID))*/)
{
UserAccepted(steamID);
}
else if (MySandboxGame.ConfigDedicated.GroupID == 0)
{
switch (MySession.Static.OnlineMode)
{
case MyOnlineModeEnum.PUBLIC:
UserAccepted(steamID);
break;
case MyOnlineModeEnum.PRIVATE:
UserRejected(steamID, JoinResult.NotInGroup);
break;
case MyOnlineModeEnum.FRIENDS:
//TODO: actually verify friendship
UserRejected(steamID, JoinResult.NotInGroup);
break;
}
}
else if (SteamServerAPI.Instance.GetAccountType(MySandboxGame.ConfigDedicated.GroupID) != AccountType.Clan)
{
UserRejected(steamID, JoinResult.GroupIdInvalid);
}
else if (SteamServerAPI.Instance.GameServer.RequestGroupStatus(steamID, MySandboxGame.ConfigDedicated.GroupID))
{
// Returns false when there's no connection to Steam
_waitingForGroup.Add(steamID);
}
else
{
UserRejected(steamID, JoinResult.SteamServersOffline);
}
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID, JoinResult.KickedRecently);
RaiseClientKicked.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, steamID);
}
else
if (response != JoinResult.OK)
{
JoinResult joinResult = JoinResult.TicketInvalid;
switch (response)
{
case AuthSessionResponseEnum.AuthTicketCanceled:
joinResult = JoinResult.TicketCanceled;
break;
case AuthSessionResponseEnum.AuthTicketInvalidAlreadyUsed:
joinResult = JoinResult.TicketAlreadyUsed;
break;
case AuthSessionResponseEnum.LoggedInElseWhere:
joinResult = JoinResult.LoggedInElseWhere;
break;
case AuthSessionResponseEnum.NoLicenseOrExpired:
joinResult = JoinResult.NoLicenseOrExpired;
break;
case AuthSessionResponseEnum.UserNotConnectedToSteam:
joinResult = JoinResult.UserNotConnected;
break;
case AuthSessionResponseEnum.VACBanned:
joinResult = JoinResult.VACBanned;
break;
case AuthSessionResponseEnum.VACCheckTimedOut:
joinResult = JoinResult.VACCheckTimedOut;
break;
}
UserRejected(steamID, joinResult);
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 UserGroupStatus(ulong userId, ulong groupId, bool member, bool officer)
private void UserGroupStatusResponse(ulong userId, ulong groupId, bool member, bool officer)
{
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Remove(userId))
if (groupId == MySandboxGame.ConfigDedicated.GroupID && _waitingForGroup.Invoke((MyDedicatedServerBase)MyMultiplayer.Static).Remove(userId))
{
if (member || officer)
{
UserAccepted(userId);
}
else
{
UserRejected(userId, JoinResult.NotInGroup);
}
UserRejected.Invoke((MyDedicatedServerBase)MyMultiplayer.Static, userId, JoinResult.NotInGroup);
}
}
private void UserAccepted(ulong steamId)
{
typeof(MyDedicatedServerBase).GetMethod("UserAccepted", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId});
var vm = new PlayerViewModel(steamId) {State = ConnectionState.Connected};
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);
}
private void UserRejected(ulong steamId, JoinResult reason)
{
typeof(MyDedicatedServerBase).GetMethod("UserRejected", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(MyMultiplayer.Static, new object[] {steamId, reason});
}
[ReflectedStaticMethod(Type = typeof(MyDedicatedServerBase))]
private static Func<ulong, string> ConvertSteamIDFrom64;
[ReflectedStaticMethod(Type = typeof(MyGameService))]
private static Func<ulong, MyGameServiceAccountType> GetServerAccountType;
[ReflectedMethod(Name = "UserAccepted")]
private static Action<MyDedicatedServerBase, ulong> UserAcceptedImpl;
[ReflectedMethod]
private static Action<MyDedicatedServerBase, ulong, JoinResult> UserRejected;
[ReflectedMethod]
private static Func<MyMultiplayerBase, ulong, bool> IsClientBanned;
[ReflectedMethod]
private static Func<MyMultiplayerBase, ulong, bool> IsClientKicked;
[ReflectedMethod]
private static Action<MyMultiplayerBase, ulong> RaiseClientKicked;
}
}

View File

@@ -9,6 +9,7 @@ using Sandbox.Engine.Multiplayer;
using Sandbox.Game.Multiplayer;
using Torch.API;
using Torch.API.Managers;
using Torch.Utils;
using VRage;
using VRage.Library.Collections;
using VRage.Network;
@@ -20,12 +21,15 @@ namespace Torch.Managers
private static Logger _log = LogManager.GetLogger(nameof(NetworkManager));
private const string MyTransportLayerField = "TransportLayer";
private const string TypeTableField = "m_typeTable";
private const string TransportHandlersField = "m_handlers";
private MyTypeTable m_typeTable = new MyTypeTable();
private HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
private bool _init;
[ReflectedGetter(Name = "m_typeTable")]
private static Func<MyReplicationLayerBase, MyTypeTable> _typeTableGetter;
[ReflectedGetter(Name = "m_methodInfoLookup")]
private static Func<MyEventTable, Dictionary<MethodInfo, CallSite>> _methodInfoLookupGetter;
public NetworkManager(ITorchBase torchInstance) : base(torchInstance)
{
@@ -43,10 +47,6 @@ namespace Torch.Managers
var transportLayerType = transportLayerField.FieldType;
var replicationLayerType = typeof(MyReplicationLayerBase);
if (!Reflection.HasField(replicationLayerType, TypeTableField))
throw new TypeLoadException("Could not find TypeTable field");
if (!Reflection.HasField(transportLayerType, TransportHandlersField))
throw new TypeLoadException("Could not find Handlers field");
@@ -63,7 +63,7 @@ namespace Torch.Managers
/// <summary>
/// Loads the network intercept system
/// </summary>
public override void Init()
public override void Attach()
{
Torch.SessionLoaded += OnSessionLoaded;
}
@@ -78,7 +78,6 @@ namespace Torch.Managers
if (!ReflectionUnitTest())
throw new InvalidOperationException("Reflection unit test failed.");
m_typeTable = typeof(MyReplicationLayerBase).GetField(TypeTableField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.ReplicationLayer) as MyTypeTable;
//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);
@@ -107,7 +106,7 @@ namespace Torch.Managers
}
#region Network Intercept
/// <summary>
/// This is the main body of the network intercept system. When messages come in from clients, they are processed here
/// before being passed on to the game server.
@@ -126,14 +125,13 @@ namespace Torch.Managers
}
catch (Exception ex)
{
_log.Fatal(ex);
_log.Fatal(ex, "~Error processing event!");
_log.Error(ex);
//crash after logging, bad things could happen if we continue on with bad data
throw;
}
return;
}
var stream = new BitStream();
stream.ResetRead(packet);
@@ -147,7 +145,7 @@ namespace Torch.Managers
object obj;
if (networkId.IsInvalid) // Static event
{
site = m_typeTable.StaticEventTable.Get(eventId);
site = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).StaticEventTable.Get(eventId);
obj = null;
}
else // Instance event
@@ -157,7 +155,7 @@ namespace Torch.Managers
{
return;
}
var typeInfo = m_typeTable.Get(sendAs.GetType());
var typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(sendAs.GetType());
var eventCount = typeInfo.EventTable.Count;
if (eventId < eventCount) // Directly
{
@@ -167,7 +165,7 @@ namespace Torch.Managers
else // Through proxy
{
obj = ((IMyProxyTarget)sendAs).Target;
typeInfo = m_typeTable.Get(obj.GetType());
typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(obj.GetType());
site = typeInfo.EventTable.Get(eventId - (uint)eventCount); // Subtract max id of Proxy
}
}
@@ -200,8 +198,8 @@ namespace Torch.Managers
}
catch (Exception ex)
{
_log.Fatal(ex);
_log.Fatal(ex, "Error when returning control to game server!");
_log.Error(ex, "Error processing network event!");
_log.Error(ex);
//crash after logging, bad things could happen if we continue on with bad data
throw;
}
@@ -237,18 +235,18 @@ namespace Torch.Managers
#region Network Injection
/// <summary>
/// Broadcasts an event to all connected clients
/// </summary>
/// <param name="method"></param>
/// <param name="obj"></param>
/// <param name="args"></param>
public void RaiseEvent(MethodInfo method, object obj, params object[] args)
public void RaiseEvent(MethodInfo method, object obj, params object[] args)
{
//default(EndpointId) tells the network to broadcast the message
RaiseEvent(method, obj, default(EndpointId), args);
}
}
/// <summary>
/// Sends an event to one client by SteamId
@@ -258,9 +256,9 @@ namespace Torch.Managers
/// <param name="steamId"></param>
/// <param name="args"></param>
public void RaiseEvent(MethodInfo method, object obj, ulong steamId, params object[] args)
{
RaiseEvent(method, obj, new EndpointId(steamId), args);
}
{
RaiseEvent(method, obj, new EndpointId(steamId), args);
}
/// <summary>
/// Sends an event to one client
@@ -281,7 +279,7 @@ namespace Torch.Managers
if (obj != null && owner == null)
throw new InvalidCastException("Provided event target is not of type IMyEventOwner!");
if(!method.HasAttribute<EventAttribute>())
if (!method.HasAttribute<EventAttribute>())
throw new CustomAttributeFormatException("Provided event target does not have the Event attribute! Replication will not succeed!");
//array to hold arguments to pass into DispatchEvent
@@ -328,10 +326,10 @@ namespace Torch.Managers
/// <param name="method"></param>
/// <param name="args"></param>
public void RaiseStaticEvent(MethodInfo method, params object[] args)
{
{
//default(EndpointId) tells the network to broadcast the message
RaiseStaticEvent(method, default(EndpointId), args);
}
}
/// <summary>
/// Sends a static event to one client by SteamId
@@ -340,9 +338,9 @@ namespace Torch.Managers
/// <param name="steamId"></param>
/// <param name="args"></param>
public void RaiseStaticEvent(MethodInfo method, ulong steamId, params object[] args)
{
RaiseEvent(method, null, new EndpointId(steamId), args);
}
{
RaiseEvent(method, null, new EndpointId(steamId), args);
}
/// <summary>
/// Sends a static event to one client
@@ -356,18 +354,17 @@ namespace Torch.Managers
}
private CallSite TryGetStaticCallSite(MethodInfo method)
{
var methodLookup = (Dictionary<MethodInfo, CallSite>)typeof(MyEventTable).GetField("m_methodInfoLookup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(m_typeTable.StaticEventTable);
if (!methodLookup.TryGetValue(method, out CallSite result))
{
MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer);
if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result))
throw new MissingMemberException("Provided event target not found!");
return result;
}
}
private CallSite TryGetCallSite(MethodInfo method, object arg)
{
var typeInfo = m_typeTable.Get(arg.GetType());
var methodLookup = (Dictionary<MethodInfo, CallSite>)typeof(MyEventTable).GetField("m_methodInfoLookup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(typeInfo.EventTable);
if (!methodLookup.TryGetValue(method, out CallSite result))
private CallSite TryGetCallSite(MethodInfo method, object arg)
{
MySynchronizedTypeInfo typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(arg.GetType());
if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result))
throw new MissingMemberException("Provided event target not found!");
return result;
}

View File

@@ -20,7 +20,10 @@ namespace Torch.Managers
{
private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
[Dependency]
private UpdateManager _updateManager;
[Dependency]
private CommandManager _commandManager;
/// <inheritdoc />
public IList<ITorchPlugin> Plugins { get; } = new ObservableList<ITorchPlugin>();
@@ -45,7 +48,7 @@ namespace Torch.Managers
/// <summary>
/// Unloads all plugins.
/// </summary>
public void DisposePlugins()
public override void Detach()
{
foreach (var plugin in Plugins)
plugin.Dispose();
@@ -87,9 +90,6 @@ namespace Torch.Managers
/// <inheritdoc />
public void LoadPlugins()
{
_updateManager = Torch.GetManager<UpdateManager>();
var commands = Torch.GetManager<CommandManager>();
if (Torch.Config.ShouldUpdatePlugins)
DownloadPlugins();
else
@@ -119,11 +119,12 @@ namespace Torch.Managers
plugin.StoragePath = Torch.Config.InstancePath;
Plugins.Add(plugin);
commands.RegisterPluginCommands(plugin);
_commandManager.RegisterPluginCommands(plugin);
}
catch
catch (Exception e)
{
_log.Error($"Error loading plugin '{type.FullName}'");
_log.Error(e);
throw;
}
}

View File

@@ -16,7 +16,7 @@ namespace Torch.Managers
{
private MyScriptWhitelist _whitelist;
public void Init()
public void Attach()
{
_whitelist = MyScriptCompiler.Static.Whitelist;
MyScriptCompiler.Static.AddConditionalCompilationSymbols("TORCH");
@@ -41,6 +41,11 @@ namespace Torch.Managers
Log.Info(whitelist);*/
}
public void Detach()
{
// TODO unregister whitelist patches
}
public void UnwhitelistType(Type t)
{
throw new NotImplementedException();

View File

@@ -6,6 +6,7 @@ using System.IO.Packaging;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using NLog;
@@ -18,12 +19,13 @@ namespace Torch.Managers
/// <summary>
/// Handles updating of the DS and Torch plugins.
/// </summary>
public class UpdateManager : Manager, IDisposable
public class UpdateManager : Manager
{
private Timer _updatePollTimer;
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
private Logger _log = LogManager.GetLogger(nameof(UpdateManager));
[Dependency]
private FilesystemManager _fsManager;
public UpdateManager(ITorchBase torchInstance) : base(torchInstance)
@@ -32,9 +34,8 @@ namespace Torch.Managers
}
/// <inheritdoc />
public override void Init()
public override void Attach()
{
_fsManager = Torch.GetManager<FilesystemManager>();
CheckAndUpdateTorch();
}
@@ -52,7 +53,13 @@ namespace Torch.Managers
return new Tuple<Version, string>(new Version(), null);
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
return new Tuple<Version, string>(new Version(latest.TagName ?? "0"), zip?.BrowserDownloadUrl);
var versionName = Regex.Match(latest.TagName, "(\\d+\\.)+\\d+").ToString();
if (string.IsNullOrWhiteSpace(versionName))
{
_log.Warn("Unable to parse tag {0} for {1}/{2}", latest.TagName, owner, name);
versionName = "0.0";
}
return new Tuple<Version, string>(new Version(versionName), zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
@@ -67,47 +74,67 @@ namespace Torch.Managers
if (!Torch.Config.GetPluginUpdates)
return;
var name = manifest.Repository.Split('/');
if (name.Length != 2)
try
{
_log.Error($"'{manifest.Repository}' is not a valid GitHub repository.");
return;
}
var name = manifest.Repository.Split('/');
if (name.Length != 2)
{
_log.Error($"'{manifest.Repository}' is not a valid GitHub repository.");
return;
}
var currentVersion = new Version(manifest.Version);
var releaseInfo = await GetLatestRelease(name[0], name[1]).ConfigureAwait(false);
if (releaseInfo.Item1 > currentVersion)
{
_log.Warn($"Updating {manifest.Repository} from version {currentVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, $"{name[0]}_{name[1]}.zip");
var updatePath = Path.Combine(_torchDir, "Plugins");
await new WebClient().DownloadFileTaskAsync(new Uri(releaseInfo.Item2), updateName).ConfigureAwait(false);
UpdateFromZip(updateName, updatePath);
File.Delete(updateName);
var currentVersion = new Version(manifest.Version);
var releaseInfo = await GetLatestRelease(name[0], name[1]).ConfigureAwait(false);
if (releaseInfo.Item1 > currentVersion)
{
_log.Warn($"Updating {manifest.Repository} from version {currentVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, $"{name[0]}_{name[1]}.zip");
var updatePath = Path.Combine(_torchDir, "Plugins");
await new WebClient().DownloadFileTaskAsync(new Uri(releaseInfo.Item2), updateName).ConfigureAwait(false);
UpdateFromZip(updateName, updatePath);
File.Delete(updateName);
}
else
{
_log.Info($"{manifest.Repository} is up to date. ({currentVersion})");
}
}
else
catch (Exception e)
{
_log.Info($"{manifest.Repository} is up to date. ({currentVersion})");
_log.Error($"An error occured downloading the plugin update for {manifest.Repository}.");
_log.Error(e);
}
}
private async void CheckAndUpdateTorch()
{
// Doesn't work properly or reliably, TODO update when Jenkins is fully configured
return;
if (!Torch.Config.GetTorchUpdates)
return;
var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
if (releaseInfo.Item1 > Torch.TorchVersion)
try
{
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip");
new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
UpdateFromZip(updateName, _torchDir);
File.Delete(updateName);
var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
if (releaseInfo.Item1 > Torch.TorchVersion)
{
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip");
new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName);
UpdateFromZip(updateName, _torchDir);
File.Delete(updateName);
_log.Warn($"Torch version {releaseInfo.Item1} has been installed, please restart Torch to finish the process.");
}
else
{
_log.Info("Torch is up to date.");
}
}
else
catch (Exception e)
{
_log.Info("Torch is up to date.");
_log.Error("An error occured downloading the Torch update.");
_log.Error(e);
}
}
@@ -127,7 +154,7 @@ namespace Torch.Managers
}
/// <inheritdoc />
public void Dispose()
public override void Detach()
{
_updatePollTimer?.Dispose();
}

View File

@@ -1,36 +1,17 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Piston")]
[assembly: AssemblyTitle("Torch")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Piston")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyProduct("Torch")]
[assembly: AssemblyCopyright("Copyright © Torch API 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("7e01635c-3b67-472e-bcd6-c5539564f214")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyConfiguration("Release")]
#endif

View File

@@ -1,131 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
namespace Torch
{
public static class Reflection
{
private static readonly Logger Log = LogManager.GetLogger("Reflection");
public static bool HasMethod(Type type, string methodName, Type[] argTypes = null)
{
try
{
if (string.IsNullOrEmpty(methodName))
return false;
if (argTypes == null)
{
var methodInfo = type.GetMethod(methodName);
if (methodInfo == null)
methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (methodInfo == null && type.BaseType != null)
methodInfo = type.BaseType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (methodInfo == null)
{
Log.Error( "Failed to find method '" + methodName + "' in type '" + type.FullName + "'" );
return false;
}
}
else
{
MethodInfo method = type.GetMethod(methodName, argTypes);
if (method == null)
method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy, Type.DefaultBinder, argTypes, null);
if (method == null && type.BaseType != null)
method = type.BaseType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy, Type.DefaultBinder, argTypes, null);
if (method == null)
{
Log.Error( "Failed to find method '" + methodName + "' in type '" + type.FullName + "'" );
return false;
}
}
return true;
}
catch (AmbiguousMatchException)
{
return true;
}
catch (Exception ex)
{
Log.Error( "Failed to find method '" + methodName + "' in type '" + type.FullName + "': " + ex.Message );
Log.Error( ex );
return false;
}
}
public static bool HasField(Type type, string fieldName)
{
try
{
if (string.IsNullOrEmpty(fieldName))
return false;
var field = type.GetField(fieldName);
if (field == null)
field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (field == null)
field = type.BaseType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (field == null)
{
Log.Error("Failed to find field '{0}' in type '{1}'", fieldName, type.FullName);
return false;
}
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to find field '{0}' in type '{1}'", fieldName, type.FullName);
return false;
}
}
public static bool HasProperty(Type type, string propertyName)
{
try
{
if (string.IsNullOrEmpty(propertyName))
return false;
var prop = type.GetProperty(propertyName);
if (prop == null)
prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (prop == null)
prop = type.BaseType?.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (prop == null)
{
Log.Error("Failed to find property '{0}' in type '{1}'", propertyName, type.FullName);
return false;
}
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to find property '{0}' in type '{1}'", propertyName, type.FullName);
return false;
}
}
public static object InvokeStaticMethod(Type type, string methodName, params object[] args)
{
var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method == null)
{
Log.Error($"Method {methodName} not found in static class {type.FullName}");
return null;
}
return method.Invoke(null, args);
}
public static T GetPrivateField<T>(this object obj, string fieldName)
{
var type = obj.GetType();
return (T)type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj);
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers;
namespace Torch.Session
{
public class TorchSession : ITorchSession
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
/// <summary>
/// The Torch instance this session is bound to
/// </summary>
public ITorchBase Torch { get; }
/// <summary>
/// The Space Engineers game session this session is bound to.
/// </summary>
public MySession KeenSession { get; }
/// <inheritdoc cref="IDependencyManager"/>
public IDependencyManager Managers { get; }
public TorchSession(ITorchBase torch, MySession keenSession)
{
Torch = torch;
KeenSession = keenSession;
Managers = new DependencyManager(torch.Managers);
}
internal void Attach()
{
Managers.Attach();
}
internal void Detach()
{
Managers.Detach();
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using Sandbox.Game.World;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Session;
using Torch.Managers;
using Torch.Session;
namespace Torch.Session
{
/// <summary>
/// Manages the creation and destruction of <see cref="TorchSession"/> instances for each <see cref="MySession"/> created by Space Engineers.
/// </summary>
public class TorchSessionManager : Manager, ITorchSessionManager
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private TorchSession _currentSession;
/// <inheritdoc/>
public ITorchSession CurrentSession => _currentSession;
private readonly HashSet<SessionManagerFactoryDel> _factories = new HashSet<SessionManagerFactoryDel>();
public TorchSessionManager(ITorchBase torchInstance) : base(torchInstance)
{
}
/// <inheritdoc/>
public bool AddFactory(SessionManagerFactoryDel factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory), "Factory must be non-null");
return _factories.Add(factory);
}
/// <inheritdoc/>
public bool RemoveFactory(SessionManagerFactoryDel factory)
{
if (factory == null)
throw new ArgumentNullException(nameof(factory), "Factory must be non-null");
return _factories.Remove(factory);
}
private void SessionLoaded()
{
if (_currentSession != null)
{
_log.Warn($"Override old torch session {_currentSession.KeenSession.Name}");
_currentSession.Detach();
}
_log.Info($"Starting new torch session for {MySession.Static.Name}");
_currentSession = new TorchSession(Torch, MySession.Static);
foreach (SessionManagerFactoryDel factory in _factories)
{
IManager manager = factory(CurrentSession);
if (manager != null)
CurrentSession.Managers.AddManager(manager);
}
(CurrentSession as TorchSession)?.Attach();
}
private void SessionUnloaded()
{
if (_currentSession == null)
return;
_log.Info($"Unloading torch session for {_currentSession.KeenSession.Name}");
_currentSession.Detach();
_currentSession = null;
}
/// <inheritdoc/>
public override void Attach()
{
MySession.AfterLoading += SessionLoaded;
MySession.OnUnloaded += SessionUnloaded;
}
/// <inheritdoc/>
public override void Detach()
{
_currentSession?.Detach();
_currentSession = null;
MySession.AfterLoading -= SessionLoaded;
MySession.OnUnloaded -= SessionUnloaded;
}
}
}

View File

@@ -3,8 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NLog;
using SteamSDK;
using VRage.Steam;
using Sandbox;
using Sandbox.Engine.Networking;
using Torch.Utils;
using VRage.GameServices;
namespace Torch
{
@@ -16,34 +21,75 @@ namespace Torch
/// </summary>
public class SteamService : MySteamService
{
public SteamService(bool isDedicated, uint appId) : base(true, appId)
{
// TODO: Add protection for this mess... somewhere
SteamServerAPI.Instance.Dispose();
var steam = typeof(MySteamService);
steam.GetField("SteamServerAPI").SetValue(this, null);
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
#pragma warning disable 649
[ReflectedSetter(Name = nameof(SteamServerAPI))]
private static Action<MySteamService, SteamServerAPI> _steamServerAPISetter;
[ReflectedSetter(Name = "m_gameServer")]
private static Action<MySteamService, MySteamGameServer> _steamGameServerSetter;
[ReflectedSetter(Name = nameof(AppId))]
private static Action<MySteamService, uint> _steamAppIdSetter;
[ReflectedSetter(Name = nameof(API))]
private static Action<MySteamService, SteamAPI> _steamApiSetter;
[ReflectedSetter(Name = nameof(IsActive))]
private static Action<MySteamService, bool> _steamIsActiveSetter;
[ReflectedSetter(Name = nameof(UserId))]
private static Action<MySteamService, ulong> _steamUserIdSetter;
[ReflectedSetter(Name = nameof(UserName))]
private static Action<MySteamService, string> _steamUserNameSetter;
[ReflectedSetter(Name = nameof(OwnsGame))]
private static Action<MySteamService, bool> _steamOwnsGameSetter;
[ReflectedSetter(Name = nameof(UserUniverse))]
private static Action<MySteamService, MyGameServiceUniverse> _steamUserUniverseSetter;
[ReflectedSetter(Name = nameof(BranchName))]
private static Action<MySteamService, string> _steamBranchNameSetter;
[ReflectedSetter(Name = nameof(InventoryAPI))]
private static Action<MySteamService, MySteamInventory> _steamInventoryAPISetter;
[ReflectedMethod]
private static Action<MySteamService> RegisterCallbacks;
[ReflectedSetter(Name = nameof(Peer2Peer))]
private static Action<MySteamService, IMyPeer2Peer> _steamPeer2PeerSetter;
#pragma warning restore 649
public SteamService(bool isDedicated, uint appId)
: base(true, appId)
{
SteamServerAPI.Instance.Dispose();
_steamServerAPISetter.Invoke(this, null);
_steamGameServerSetter.Invoke(this, null);
_steamAppIdSetter.Invoke(this, appId);
steam.GetProperty("AppId").GetSetMethod(true).Invoke(this, new object[] { appId });
if (isDedicated)
{
steam.GetField("SteamServerAPI").SetValue(this, SteamServerAPI.Instance);
_steamServerAPISetter.Invoke(this, null);
_steamGameServerSetter.Invoke(this, new MySteamGameServer());
}
else
{
var steamApi = SteamAPI.Instance;
steam.GetField("SteamAPI").SetValue(this, SteamAPI.Instance);
steam.GetProperty("IsActive").GetSetMethod(true).Invoke(this, new object[] { SteamAPI.Instance != null });
SteamAPI steamApi = SteamAPI.Instance;
_steamApiSetter.Invoke(this, steamApi);
bool initResult = steamApi.Init();
if (!initResult)
_log.Warn("Failed to initialize SteamService");
_steamIsActiveSetter.Invoke(this, initResult);
if (steamApi != null)
if (IsActive)
{
steam.GetProperty("UserId").GetSetMethod(true).Invoke(this, new object[] { steamApi.GetSteamUserId() });
steam.GetProperty("UserName").GetSetMethod(true).Invoke(this, new object[] { steamApi.GetSteamName() });
steam.GetProperty("OwnsGame").GetSetMethod(true).Invoke(this, new object[] { steamApi.HasGame() });
steam.GetProperty("UserUniverse").GetSetMethod(true).Invoke(this, new object[] { steamApi.GetSteamUserUniverse() });
steam.GetProperty("BranchName").GetSetMethod(true).Invoke(this, new object[] { steamApi.GetBranchName() });
_steamUserIdSetter.Invoke(this, steamApi.GetSteamUserId());
_steamUserNameSetter.Invoke(this, steamApi.GetSteamName());
_steamOwnsGameSetter.Invoke(this, steamApi.HasGame());
_steamUserUniverseSetter.Invoke(this, (MyGameServiceUniverse)steamApi.GetSteamUserUniverse());
_steamBranchNameSetter.Invoke(this, steamApi.GetBranchName());
steamApi.LoadStats();
}
_steamInventoryAPISetter.Invoke(this, new MySteamInventory());
RegisterCallbacks(this);
} else
_log.Warn("SteamService isn't initialized; Torch Client won't start");
}
_steamPeer2PeerSetter.Invoke(this, new MySteamPeer2Peer());
}
}
}

View File

@@ -2,8 +2,6 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{7E01635C-3B67-472E-BCD6-C5539564F214}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -12,10 +10,12 @@
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
@@ -23,14 +23,14 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<OutputPath>$(SolutionDir)\bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\x64\Release\Torch.xml</DocumentationFile>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="HavokWrapper, Version=1.0.6278.22649, Culture=neutral, processorArchitecture=AMD64">
@@ -43,7 +43,7 @@
<HintPath>..\GameBinaries\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.1\lib\net45\NLog.dll</HintPath>
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.24.0.0, Culture=neutral, processorArchitecture=MSIL">
@@ -142,12 +142,21 @@
<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" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Versioning\AssemblyVersion.cs">
<Link>Properties\AssemblyVersion.cs</Link>
</Compile>
<Compile Include="ChatMessage.cs" />
<Compile Include="Collections\ObservableList.cs" />
<Compile Include="DispatcherExtensions.cs" />
<Compile Include="Managers\DependencyManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SaveGameStatus.cs" />
<Compile Include="Collections\KeyTree.cs" />
<Compile Include="Collections\ObservableDictionary.cs" />
@@ -173,11 +182,15 @@
<Compile Include="Managers\UpdateManager.cs" />
<Compile Include="Persistent.cs" />
<Compile Include="PluginManifest.cs" />
<Compile Include="Reflection.cs" />
<Compile Include="Utils\Reflection.cs" />
<Compile Include="Managers\ScriptingManager.cs" />
<Compile Include="Utils\TorchAssemblyResolver.cs" />
<Compile Include="Utils\ReflectedManager.cs" />
<Compile Include="Session\TorchSessionManager.cs" />
<Compile Include="TorchBase.cs" />
<Compile Include="SteamService.cs" />
<Compile Include="TorchPluginBase.cs" />
<Compile Include="Session\TorchSession.cs" />
<Compile Include="ViewModels\ModViewModel.cs" />
<Compile Include="Collections\MTObservableCollection.cs" />
<Compile Include="Extensions\MyPlayerCollectionExtensions.cs" />
@@ -186,7 +199,6 @@
<Compile Include="ViewModels\PlayerViewModel.cs" />
<Compile Include="ViewModels\ViewModel.cs" />
<Compile Include="Managers\PluginManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewModels\PluginViewModel.cs" />
<Compile Include="Views\CollectionEditor.xaml.cs">
<DependentUpon>CollectionEditor.xaml</DependentUpon>
@@ -196,7 +208,7 @@
<ProjectReference Include="..\Torch.API\Torch.API.csproj">
<Project>{fba5d932-6254-4a1e-baf4-e229fa94e3c2}</Project>
<Name>Torch.API</Name>
<Private>True</Private>
<Private>False</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
@@ -208,12 +220,9 @@
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
</Project>

View File

@@ -19,8 +19,11 @@ using SpaceEngineers.Game;
using Torch.API;
using Torch.API.Managers;
using Torch.API.ModAPI;
using Torch.API.Session;
using Torch.Commands;
using Torch.Managers;
using Torch.Utils;
using Torch.Session;
using VRage.Collections;
using VRage.FileSystem;
using VRage.Game.ObjectBuilder;
@@ -36,6 +39,12 @@ namespace Torch
/// </summary>
public abstract class TorchBase : ViewModel, ITorchBase, IPlugin
{
static TorchBase()
{
// We can safely never detach this since we don't reload assemblies.
new ReflectedManager().Attach();
}
/// <summary>
/// Hack because *keen*.
/// Use only if necessary, prefer dependency injection.
@@ -44,21 +53,36 @@ namespace Torch
/// <inheritdoc />
public ITorchConfig Config { get; protected set; }
/// <inheritdoc />
public Version TorchVersion { get; protected set; }
public Version TorchVersion { get; }
/// <summary>
/// The version of Torch used, with extra data.
/// </summary>
public string TorchVersionVerbose { get; }
/// <inheritdoc />
public Version GameVersion { get; private set; }
/// <inheritdoc />
public string[] RunArgs { get; set; }
/// <inheritdoc />
[Obsolete("Use GetManager<T>() or the [Dependency] attribute.")]
public IPluginManager Plugins { get; protected set; }
/// <inheritdoc />
[Obsolete("Use GetManager<T>() or the [Dependency] attribute.")]
public IMultiplayerManager Multiplayer { get; protected set; }
/// <inheritdoc />
[Obsolete("Use GetManager<T>() or the [Dependency] attribute.")]
public EntityManager Entities { get; protected set; }
/// <inheritdoc />
[Obsolete("Use GetManager<T>() or the [Dependency] attribute.")]
public INetworkManager Network { get; protected set; }
/// <inheritdoc />
[Obsolete("Use GetManager<T>() or the [Dependency] attribute.")]
public CommandManager Commands { get; protected set; }
/// <inheritdoc />
public ITorchSession CurrentSession => Managers?.GetManager<ITorchSessionManager>()?.CurrentSession;
/// <inheritdoc />
public event Action SessionLoading;
/// <inheritdoc />
@@ -72,7 +96,10 @@ namespace Torch
/// Common log for the Torch instance.
/// </summary>
protected static Logger Log { get; } = LogManager.GetLogger("Torch");
private readonly List<IManager> _managers;
/// <inheritdoc/>
public IDependencyManager Managers { get; }
private bool _init;
/// <summary>
@@ -86,41 +113,41 @@ namespace Torch
Instance = this;
TorchVersion = Assembly.GetExecutingAssembly().GetName().Version;
TorchVersion = Assembly.GetExecutingAssembly().GetName().Version;
TorchVersionVerbose = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? TorchVersion.ToString();
RunArgs = new string[0];
Managers = new DependencyManager();
Plugins = new PluginManager(this);
Multiplayer = new MultiplayerManager(this);
Entities = new EntityManager(this);
Network = new NetworkManager(this);
Commands = new CommandManager(this);
_managers = new List<IManager> { new FilesystemManager(this), new UpdateManager(this), Network, Commands, Plugins, Multiplayer, Entities, new ChatManager(this), };
Managers.AddManager(new TorchSessionManager(this));
Managers.AddManager(new FilesystemManager(this));
Managers.AddManager(new UpdateManager(this));
Managers.AddManager(Network);
Managers.AddManager(Commands);
Managers.AddManager(Plugins);
Managers.AddManager(Multiplayer);
Managers.AddManager(Entities);
Managers.AddManager(new ChatManager(this));
TorchAPI.Instance = this;
}
/// <inheritdoc />
public ListReader<IManager> GetManagers()
{
return new ListReader<IManager>(_managers);
}
/// <inheritdoc />
public T GetManager<T>() where T : class, IManager
{
return _managers.FirstOrDefault(m => m is T) as T;
return Managers.GetManager<T>();
}
/// <inheritdoc />
public bool AddManager<T>(T manager) where T : class, IManager
{
if (_managers.Any(x => x is T))
return false;
_managers.Add(manager);
return true;
return Managers.AddManager(manager);
}
public bool IsOnGameThread()
@@ -136,7 +163,7 @@ namespace Torch
{
callback?.Invoke(SaveGameStatus.GameNotReady);
}
else if(MyAsyncSaving.InProgress)
else if (MyAsyncSaving.InProgress)
{
callback?.Invoke(SaveGameStatus.SaveInProgress);
}
@@ -222,20 +249,24 @@ namespace Torch
public virtual void Init()
{
Debug.Assert(!_init, "Torch instance is already initialized.");
SpaceEngineersGame.SetupBasicGameInfo();
SpaceEngineersGame.SetupPerGameSettings();
TorchVersion = Assembly.GetEntryAssembly().GetName().Version;
Debug.Assert(MyPerGameSettings.BasicGameInfo.GameVersion != null, "MyPerGameSettings.BasicGameInfo.GameVersion != null");
GameVersion = new Version(new MyVersion(MyPerGameSettings.BasicGameInfo.GameVersion.Value).FormattedText.ToString().Replace("_", "."));
var verInfo = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}";
Console.Title = verInfo;
try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; }
catch
{
///Running as service
}
#if DEBUG
Log.Info("DEBUG");
#else
Log.Info("RELEASE");
#endif
Log.Info(verInfo);
Log.Info($"Torch Version: {TorchVersionVerbose}");
Log.Info($"Game Version: {GameVersion}");
Log.Info($"Executing assembly: {Assembly.GetEntryAssembly().FullName}");
Log.Info($"Executing directory: {AppDomain.CurrentDomain.BaseDirectory}");
@@ -244,8 +275,7 @@ namespace Torch
MySession.OnUnloading += OnSessionUnloading;
MySession.OnUnloaded += OnSessionUnloaded;
RegisterVRagePlugin();
foreach (var manager in _managers)
manager.Init();
Managers.Attach();
_init = true;
}
@@ -295,25 +325,25 @@ namespace Torch
/// <inheritdoc/>
public virtual void Start()
{
}
/// <inheritdoc />
public virtual void Stop()
{
}
/// <inheritdoc />
public virtual void Restart()
{
}
/// <inheritdoc />
public virtual void Dispose()
{
Plugins.DisposePlugins();
Managers.Detach();
}
/// <inheritdoc />
@@ -325,7 +355,7 @@ namespace Torch
/// <inheritdoc />
public virtual void Update()
{
Plugins.UpdatePlugins();
GetManager<IPluginManager>().UpdatePlugins();
}
}
}

View File

@@ -0,0 +1,338 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Sandbox.Engine.Multiplayer;
using Torch.API;
namespace Torch.Utils
{
public abstract class ReflectedMemberAttribute : Attribute
{
/// <summary>
/// Name of the member to access. If null, the tagged field's name.
/// </summary>
public string Name { get; set; } = null;
/// <summary>
/// Declaring type of the member to access. If null, inferred from the instance argument type.
/// </summary>
public Type Type { get; set; } = null;
}
/// <summary>
/// Indicates that this field should contain a delegate capable of retrieving the value of a field.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// [ReflectedGetterAttribute(Name="_instanceField")]
/// private static Func<Example, int> _instanceGetter;
///
/// [ReflectedGetterAttribute(Name="_staticField", Type=typeof(Example))]
/// private static Func<int> _staticGetter;
///
/// private class Example {
/// private int _instanceField;
/// private static int _staticField;
/// }
/// ]]>
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Field)]
public class ReflectedGetterAttribute : ReflectedMemberAttribute
{
}
/// <summary>
/// Indicates that this field should contain a delegate capable of setting the value of a field.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// [ReflectedSetterAttribute(Name="_instanceField")]
/// private static Action<Example, int> _instanceSetter;
///
/// [ReflectedSetterAttribute(Name="_staticField", Type=typeof(Example))]
/// private static Action<int> _staticSetter;
///
/// private class Example {
/// private int _instanceField;
/// private static int _staticField;
/// }
/// ]]>
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Field)]
public class ReflectedSetterAttribute : ReflectedMemberAttribute
{
}
/// <summary>
/// Indicates that this field should contain a delegate capable of invoking an instance method.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// [ReflectedMethodAttribute]
/// private static Func<Example, int, float, string> ExampleInstance;
///
/// private class Example {
/// private int ExampleInstance(int a, float b) {
/// return a + ", " + b;
/// }
/// }
/// ]]>
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Field)]
public class ReflectedMethodAttribute : ReflectedMemberAttribute
{
}
/// <summary>
/// Indicates that this field should contain a delegate capable of invoking a static method.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// [ReflectedMethodAttribute(Type = typeof(Example)]
/// private static Func<int, float, string> ExampleStatic;
///
/// private class Example {
/// private static int ExampleStatic(int a, float b) {
/// return a + ", " + b;
/// }
/// }
/// ]]>
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Field)]
public class ReflectedStaticMethodAttribute : ReflectedMethodAttribute
{
}
/// <summary>
/// Automatically calls <see cref="ReflectedManager.Process(Assembly)"/> for every assembly already loaded, and every assembly that is loaded in the future.
/// </summary>
public class ReflectedManager
{
private static readonly string[] _namespaceBlacklist = new[] {
"System", "VRage", "Sandbox", "SpaceEngineers"
};
/// <summary>
/// Registers the assembly load event and loads every already existing assembly.
/// </summary>
public void Attach()
{
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
Process(asm);
AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;
}
/// <summary>
/// Deregisters the assembly load event
/// </summary>
public void Detach()
{
AppDomain.CurrentDomain.AssemblyLoad -= CurrentDomain_AssemblyLoad;
}
private void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
{
Process(args.LoadedAssembly);
}
private static readonly HashSet<Type> _processedTypes = new HashSet<Type>();
/// <summary>
/// Ensures all reflected fields and methods contained in the given type are initialized
/// </summary>
/// <param name="t">Type to process</param>
public static void Process(Type t)
{
if (_processedTypes.Add(t))
{
foreach (string ns in _namespaceBlacklist)
if (t.FullName.StartsWith(ns))
return;
foreach (FieldInfo field in t.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
Process(field);
}
}
/// <summary>
/// Ensures all types in the given assembly are initialized using <see cref="Process(Type)"/>
/// </summary>
/// <param name="asm">Assembly to process</param>
public static void Process(Assembly asm)
{
foreach (Type type in asm.GetTypes())
Process(type);
}
/// <summary>
/// Processes the given field, determines if it's reflected, and initializes it if it is.
/// </summary>
/// <param name="field">Field to process</param>
/// <returns>true if it was reflected, false if it wasn't reflectable</returns>
/// <exception cref="ArgumentException">If the field failed to process</exception>
public static bool Process(FieldInfo field)
{
var attr = field.GetCustomAttribute<ReflectedMethodAttribute>();
if (attr != null)
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
ProcessReflectedMethod(field, attr);
return true;
}
var attr2 = field.GetCustomAttribute<ReflectedGetterAttribute>();
if (attr2 != null)
{
if (!field.IsStatic)
throw new ArgumentException("Field must be static to be reflected");
ProcessReflectedField(field, attr2);
return true;
}
var attr3 = field.GetCustomAttribute<ReflectedSetterAttribute>();
if (attr3 != null)
{
if (!field.IsStatic)
{
throw new ArgumentException("Field must be static to be reflected");
}
ProcessReflectedField(field, attr3);
return true;
}
return false;
}
private static void ProcessReflectedMethod(FieldInfo field, ReflectedMethodAttribute attr)
{
MethodInfo delegateMethod = field.FieldType.GetMethod("Invoke");
ParameterInfo[] parameters = delegateMethod.GetParameters();
Type trueType = attr.Type;
Type[] trueParameterTypes;
if (attr is ReflectedStaticMethodAttribute)
{
trueParameterTypes = parameters.Select(x => x.ParameterType).ToArray();
}
else
{
trueType = trueType ?? parameters[0].ParameterType;
trueParameterTypes = parameters.Skip(1).Select(x => x.ParameterType).ToArray();
}
MethodInfo methodInstance = trueType.GetMethod(attr.Name ?? field.Name,
(attr is ReflectedStaticMethodAttribute ? BindingFlags.Static : BindingFlags.Instance) |
BindingFlags.Public |
BindingFlags.NonPublic, null, CallingConventions.Any, trueParameterTypes, null);
if (methodInstance == null)
{
string methodType = attr is ReflectedStaticMethodAttribute ? "static" : "instance";
string methodParams = string.Join(", ",
trueParameterTypes.Select(x => x.Name));
throw new NoNullAllowedException(
$"Unable to find {methodType} method {attr.Name ?? field.Name} in type {trueType.FullName} with parameters {methodParams}");
}
if (attr is ReflectedStaticMethodAttribute)
field.SetValue(null, Delegate.CreateDelegate(field.FieldType, methodInstance));
else
{
ParameterExpression[] paramExp = parameters.Select(x => Expression.Parameter(x.ParameterType)).ToArray();
field.SetValue(null,
Expression.Lambda(Expression.Call(paramExp[0], methodInstance, paramExp.Skip(1)), paramExp)
.Compile());
}
}
private static T GetFieldPropRecursive<T>(Type baseType, string name, BindingFlags flags, Func<Type, string, BindingFlags, T> getter) where T : class
{
while (baseType != null)
{
T result = getter.Invoke(baseType, name, flags);
if (result != null)
return result;
baseType = baseType.BaseType;
}
return null;
}
private static void ProcessReflectedField(FieldInfo field, ReflectedMemberAttribute attr)
{
MethodInfo delegateMethod = field.FieldType.GetMethod("Invoke");
ParameterInfo[] parameters = delegateMethod.GetParameters();
string trueName = attr.Name ?? field.Name;
Type trueType = attr.Type;
bool isStatic;
if (attr is ReflectedSetterAttribute)
{
if (delegateMethod.ReturnType != typeof(void) || (parameters.Length != 1 && parameters.Length != 2))
throw new ArgumentOutOfRangeException(nameof(field),
"Delegate for setter must be an action with one or two arguments");
isStatic = parameters.Length == 1;
if (trueType == null && isStatic)
throw new ArgumentException("Static field setters need their type defined", nameof(field));
if (!isStatic)
trueType = parameters[0].ParameterType;
}
else if (attr is ReflectedGetterAttribute)
{
if (delegateMethod.ReturnType == typeof(void) || (parameters.Length != 0 && parameters.Length != 1))
throw new ArgumentOutOfRangeException(nameof(field),
"Delegate for getter must be an function with one or no arguments");
isStatic = parameters.Length == 0;
if (trueType == null && isStatic)
throw new ArgumentException("Static field getters need their type defined", nameof(field));
if (!isStatic)
trueType = parameters[0].ParameterType;
}
else
throw new ArgumentException($"Field attribute type {attr.GetType().FullName} is invalid", nameof(field));
BindingFlags bindingFlags = (isStatic ? BindingFlags.Static : BindingFlags.Instance) |
BindingFlags.NonPublic |
BindingFlags.Public;
FieldInfo sourceField = GetFieldPropRecursive(trueType, trueName, bindingFlags,
(a, b, c) => a.GetField(b, c));
PropertyInfo sourceProperty =
GetFieldPropRecursive(trueType, trueName, bindingFlags, (a, b, c) => a.GetProperty(b, c));
if (sourceField == null && sourceProperty == null)
throw new ArgumentException(
$"Unable to find field or property for {trueName} in {trueType.FullName} or its base types", nameof(field));
ParameterExpression[] paramExp = parameters.Select(x => Expression.Parameter(x.ParameterType)).ToArray();
MemberExpression fieldExp = sourceField != null
? Expression.Field(isStatic ? null : paramExp[0], sourceField)
: Expression.Property(isStatic ? null : paramExp[0], sourceProperty);
Expression impl;
if (attr is ReflectedSetterAttribute)
{
impl = Expression.Block(Expression.Assign(fieldExp, paramExp[isStatic ? 0 : 1]), Expression.Default(typeof(void)));
}
else
{
impl = fieldExp;
}
field.SetValue(null, Expression.Lambda(impl, paramExp).Compile());
}
}
}

227
Torch/Utils/Reflection.cs Normal file
View File

@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
using SteamSDK;
namespace Torch.Utils
{
public static class Reflection
{
private static readonly Logger Log = LogManager.GetLogger("Reflection");
public static bool HasMethod(Type type, string methodName, Type[] argTypes = null)
{
try
{
if (string.IsNullOrEmpty(methodName))
return false;
if (argTypes == null)
{
var methodInfo = type.GetMethod(methodName);
if (methodInfo == null)
methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (methodInfo == null && type.BaseType != null)
methodInfo = type.BaseType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (methodInfo == null)
{
Log.Error("Failed to find method '" + methodName + "' in type '" + type.FullName + "'");
return false;
}
}
else
{
MethodInfo method = type.GetMethod(methodName, argTypes);
if (method == null)
method = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy, Type.DefaultBinder, argTypes, null);
if (method == null && type.BaseType != null)
method = type.BaseType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy, Type.DefaultBinder, argTypes, null);
if (method == null)
{
Log.Error("Failed to find method '" + methodName + "' in type '" + type.FullName + "'");
return false;
}
}
return true;
}
catch (AmbiguousMatchException)
{
return true;
}
catch (Exception ex)
{
Log.Error("Failed to find method '" + methodName + "' in type '" + type.FullName + "': " + ex.Message);
Log.Error(ex);
return false;
}
}
public static bool HasField(Type type, string fieldName)
{
try
{
if (string.IsNullOrEmpty(fieldName))
return false;
var field = type.GetField(fieldName);
if (field == null)
field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (field == null)
field = type.BaseType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (field == null)
{
Log.Error("Failed to find field '{0}' in type '{1}'", fieldName, type.FullName);
return false;
}
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to find field '{0}' in type '{1}'", fieldName, type.FullName);
return false;
}
}
public static bool HasProperty(Type type, string propertyName)
{
try
{
if (string.IsNullOrEmpty(propertyName))
return false;
var prop = type.GetProperty(propertyName);
if (prop == null)
prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (prop == null)
prop = type.BaseType?.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (prop == null)
{
Log.Error("Failed to find property '{0}' in type '{1}'", propertyName, type.FullName);
return false;
}
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to find property '{0}' in type '{1}'", propertyName, type.FullName);
return false;
}
}
/// <summary>
/// Invokes the static method of the given type, with the given arguments.
/// </summary>
/// <param name="type">Type the method is contained in</param>
/// <param name="methodName">Method name</param>
/// <param name="args">Arguments to the method</param>
/// <returns>return value of the invoked method, or null if it failed</returns>
public static object InvokeStaticMethod(Type type, string methodName, params object[] args)
{
var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method == null)
{
Log.Error($"Method {methodName} not found in static class {type.FullName}");
return null;
}
return method.Invoke(null, args);
}
/// <summary>
/// Invokes the private method with the given arguments on the instance. Includes base types of instance.
/// </summary>
/// <param name="instance"></param>
/// <param name="methodName"></param>
/// <param name="args"></param>
/// <returns>the return value of the method, or null if it failed</returns>
public static object InvokePrivateMethod(object instance, string methodName, params object[] args)
{
Type type = instance.GetType();
while (type != null)
{
MethodInfo method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
return method.Invoke(instance, args);
type = type.BaseType;
}
Log.Error($"Method {methodName} not found in type {instance.GetType().FullName} or its parents");
return null;
}
/// <summary>
/// Gets the value of a private field in an instance.
/// </summary>
/// <typeparam name="T">The type of the private field</typeparam>
/// <param name="obj">The instance</param>
/// <param name="fieldName">Field name</param>
/// <param name="recurse">Should the base types be examined</param>
/// <returns></returns>
public static T GetPrivateField<T>(this object obj, string fieldName, bool recurse = false)
{
var type = obj.GetType();
while (type != null)
{
FieldInfo field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field != null)
return (T)field
.GetValue(obj);
if (!recurse)
break;
type = type.BaseType;
}
Log.Error($"Field {fieldName} not found in type {obj.GetType().FullName}" + (recurse ? " or its parents" : ""));
return default(T);
}
/// <summary>
/// Gets the list of all delegates registered in the named static event
/// </summary>
/// <param name="type">The type (or child type) that contains the event</param>
/// <param name="eventName">Name of the event</param>
/// <returns>All delegates registered with the event</returns>
public static IEnumerable<Delegate> GetStaticEvent(Type type, string eventName)
{
return GetEventsInternal(null, eventName, type);
}
/// <summary>
/// Gets the list of all delegates registered in the named event
/// </summary>
/// <param name="instance">Instance to retrieve the event list for</param>
/// <param name="eventName">Name of the event</param>
/// <returns>All delegates registered with the event</returns>
public static IEnumerable<Delegate> GetInstanceEvent(object instance, string eventName)
{
return GetEventsInternal(instance, eventName);
}
private static readonly string[] _backingFieldForEvent = { "{0}", "<backing_store>{0}" };
private static IEnumerable<Delegate> GetEventsInternal(object instance, string eventName, Type baseType = null)
{
BindingFlags bindingFlags = BindingFlags.NonPublic |
(instance == null ? BindingFlags.Static : BindingFlags.Instance);
FieldInfo eventField = null;
baseType = baseType ?? instance?.GetType();
Type type = baseType;
while (type != null && eventField == null)
{
for (var i = 0; i < _backingFieldForEvent.Length && eventField == null; i++)
eventField = type.GetField(string.Format(_backingFieldForEvent[i], eventName), bindingFlags);
type = type.BaseType;
}
if (eventField?.GetValue(instance) is MulticastDelegate eventDel)
{
foreach (Delegate handle in eventDel.GetInvocationList())
yield return handle;
}
else
Log.Error($"{eventName} doesn't have a backing store in {baseType} or its parents.");
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NLog;
namespace Torch.Utils
{
/// <summary>
/// Adds and removes an additional library path
/// </summary>
public class TorchAssemblyResolver : IDisposable
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
private readonly Dictionary<string, Assembly> _assemblies = new Dictionary<string, Assembly>();
private readonly string[] _paths;
private readonly string _removablePathPrefix;
/// <summary>
/// Initializes an assembly resolver that looks at the given paths for assemblies
/// </summary>
/// <param name="paths"></param>
public TorchAssemblyResolver(params string[] paths)
{
string location = Assembly.GetEntryAssembly()?.Location ?? GetType().Assembly.Location;
location = location != null ? Path.GetDirectoryName(location) + Path.DirectorySeparatorChar : null;
_removablePathPrefix = location ?? "";
_paths = paths;
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
}
private string SimplifyPath(string path)
{
return path.StartsWith(_removablePathPrefix) ? path.Substring(_removablePathPrefix.Length) : path;
}
private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
{
string assemblyName = new AssemblyName(args.Name).Name;
lock (_assemblies)
{
if (_assemblies.TryGetValue(assemblyName, out Assembly asm))
return asm;
}
lock (AppDomain.CurrentDomain)
{
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
if (asm.GetName().Name.Equals(assemblyName))
{
lock (this)
{
_assemblies.Add(assemblyName, asm);
return asm;
}
}
}
lock (this)
{
foreach (string path in _paths)
{
try
{
string assemblyPath = Path.Combine(path, assemblyName + ".dll");
if (!File.Exists(assemblyPath))
continue;
_log.Debug("Loading {0} from {1}", assemblyName, SimplifyPath(assemblyPath));
LogManager.Flush();
Assembly asm = Assembly.LoadFrom(assemblyPath);
_assemblies.Add(assemblyName, asm);
// Recursively load SE dependencies since they don't trigger AssemblyResolve.
// This trades some performance on load for actually working code.
foreach (AssemblyName dependency in asm.GetReferencedAssemblies())
CurrentDomainOnAssemblyResolve(sender, new ResolveEventArgs(dependency.Name, asm));
return asm;
}
catch
{
// Ignored
}
}
}
return null;
}
/// <summary>
/// Unregisters the assembly resolver
/// </summary>
public void Dispose()
{
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomainOnAssemblyResolve;
_assemblies.Clear();
}
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.4.1" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="Octokit" version="0.24.0" targetFramework="net461" />
</packages>

9
TransformOnBuild.targets Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="TransformOnBuild">
<ItemGroup>
<TemplatePaths Include="@(None)" Condition="'%(None.Generator)' == 'TextTemplatingFileGenerator'" />
</ItemGroup>
<Exec Command="&quot;$(SolutionDir)\packages\Mono.TextTransform.1.0.0\tools\TextTransform.exe&quot; &quot;%(TemplatePaths.FullPath)&quot;" Condition="'%(TemplatePaths.Identity)' != ''"/>
</Target>
</Project>

View File

@@ -0,0 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("0.0.0.0")]
[assembly: AssemblyInformationalVersion("v0.0.0-dev")]

20
Versioning/version.ps1 Normal file
View File

@@ -0,0 +1,20 @@
$gitVersion = git describe --tags
$gitSimpleVersion = git describe --tags --abbrev=0
if ($gitSimpleVersion.Equals($gitVersion)) {
$buildSalt = 0
} else {
$gitLatestCommit = git rev-parse --short HEAD
$buildSalt = [System.Numerics.BigInteger]::Abs([System.Numerics.BigInteger]::Parse($gitLatestCommit, [System.Globalization.NumberStyles]::HexNumber) % 16383) + 1
}
$dotNetVersion = echo $gitSimpleVersion | Select-String -Pattern "([0-9]+)\.([0-9]+)\.([0-9]+)" | % {$_.Matches} | %{$_.Groups[1].Value+"."+$_.Groups[2].Value+"."+$_.Groups[3].Value+".$buildSalt"}
$fileContent = @"
using System.Reflection;
[assembly: AssemblyVersion("$dotNetVersion")]
[assembly: AssemblyInformationalVersion("$gitVersion")]
"@
echo $fileContent | Set-Content "$PSScriptRoot/AssemblyVersion.cs"
echo "$gitVersion / $dotNetVersion"