Compare commits

...

124 Commits

Author SHA1 Message Date
Westin Miller
897f75c069 Merge branch 'new_init' 2017-12-02 12:16:30 -08:00
Westin Miller
8167e04383 Merge branch 'staging' 2017-12-02 12:16:23 -08:00
Westin Miller
72b6d0e7bb Tweaks to versioning scheme
Nuke staging
2017-12-02 12:14:48 -08:00
Westin Miller
039c5d9244 Fields on internal types can be reflected 2017-11-25 23:02:01 -08:00
Westin Miller
7ea982c903 Future methods for loading/unloading sessions.
Fix reflected manager to work with instance methods on internal types.
2017-11-25 17:44:50 -08:00
Westin Miller
f0adeddb66 Split game initialization and starting into a separate thread and file. 2017-11-25 17:25:06 -08:00
Westin Miller
e709b6c321 Tweaked initializer to keep the SpaceEngineersGame instance around. 2017-11-25 15:52:53 -08:00
Westin Miller
1b0dcc9808 Validate auth ticket event exposed to other parts of Torch and plugins 2017-11-24 20:46:41 -08:00
John Gross
fe5dfa0ea7 Another restart fix and invoke tweaks 2017-11-23 10:46:28 -08:00
John Gross
25e6f27854 Fixes for 2017-11-17 major 2017-11-17 12:11:11 -08:00
Westin Miller
c07a01a427 Patcher correctly translates catch clauses 2017-11-11 03:50:25 -08:00
Westin Miller
11bc7cb60c Trace level messages
Collection change events are all deferred, and replaced with reset if it makes sense.
2017-11-09 09:42:48 -08:00
Westin Miller
6e3b7e7a04 Merge branch 'fix-dependent-dll-loading' of https://github.com/susu/Torch into staging 2017-11-06 19:42:31 -08:00
Westin Miller
ff58cf5b19 Merge pull request #161 from TorchAPI/plugin-wpf-controls
Pluggable WPF Controls for the Entity Manager UI
2017-11-06 19:36:24 -08:00
Marton Suranyi
25a708a3d4 PluginManager: load dependent DLLs first
When there are dependencies among a plugin's DLLs,
those dependencies needs a custom resolving.
2017-11-06 20:30:48 +01:00
Marton Suranyi
004dcc19dc PluginManager: make zip plugins working
Let Torch to load the plugin's assembly.
2017-11-06 20:28:15 +01:00
Marton Suranyi
ac95f5f89c PluginLoader: load only files with DLL extension from directories and zips
I had several Xy.dll.xml files there
and it was tried to be loaded as an Assembly.
2017-11-06 20:24:09 +01:00
Westin Miller
0fc9b49fba Didn't work well 2017-11-04 10:19:33 -07:00
Westin Miller
600e73ad43 Post transpilers (happen after prefix and suffix)
More debugging options
More robust inline writing and reading
2017-11-04 10:09:22 -07:00
Westin Miller
0bc0b0dc77 Fix integrity analysis to work with loops 2017-11-04 04:18:47 -07:00
Westin Miller
86f62e1f37 Plugin controls are scroll viewers. 2017-11-03 21:16:49 -07:00
Westin Miller
7850b8368a Fix hard clean
Remove leftover logging
2017-11-03 20:15:11 -07:00
Westin Miller
496bde733f Kill repeated file 2017-11-03 19:30:38 -07:00
Westin Miller
8b98deafca Fixed patch manager to emit try-catch-finally blocks.
Solves issue with PBs not running
2017-11-02 22:49:17 -07:00
Westin Miller
b3ab0cbd74 Fix loading plugins from ZIP files 2017-11-01 19:50:02 -07:00
Westin Miller
462eb77e0d Try to load .exe files when looking for assemblies in test framework 2017-11-01 18:19:22 -07:00
Westin Miller
d5702d3065 Progress report when patching 2017-11-01 17:47:21 -07:00
Westin Miller
c8377b318e EntityControlManager partial rewrite to do lazy updating
Auto commit patches once SE instance has been created
2017-11-01 05:08:17 -07:00
Westin Miller
6814a833be Removed some duplicated code. 2017-11-01 01:03:10 -07:00
Westin Miller
98aae10126 Tools for debugging patch transpilers. 2017-10-31 23:42:25 -07:00
Westin Miller
0558675132 If plugins or Torch call the static ctor of MyEntities too early the game doesn't break.
Freeze isn't fatal when compiled with DEBUG.  (So you can use breakpoints)
2017-10-31 20:55:48 -07:00
Westin Miller
c8f42e8a48 Method to determine if a MtObservableCollection is observed by anyone. 2017-10-31 17:39:45 -07:00
Westin Miller
d30d16b855 Fixed issue with not being on dispatcher thread 2017-10-31 04:43:50 -07:00
Westin Miller
178957642c Revert NLog level back to Info 2017-10-31 04:22:10 -07:00
Westin Miller
cd77fe74d5 Pluggable system for providing WPF controls inside the Entity Manager 2017-10-31 04:18:27 -07:00
Westin Miller
90c91c3ebc Initial version of profiler
Moved reflected manager into separate files
Extracted MtObservableEvent
Added a patch to Keencode that lets us call the static cctor of MyEntities in the wrong spot
2017-10-30 13:48:04 -07:00
Westin Miller
11dbf83faf NLog works again 2017-10-30 13:43:35 -07:00
Westin Miller
b7fa57c9b7 Non-indexed remove causes a full reset (@Xero's issue)
Removed ignore last session option.  Implicitly true now.
Asteroid generation options (fixes #151)
2017-10-29 09:53:20 -07:00
Westin Miller
7a63527d8f Archive before test 2017-10-28 00:06:00 -07:00
Westin Miller
4b33bedccd Disable NLog based KeenLog.
NLog is being derpy
2017-10-27 23:28:15 -07:00
Westin Miller
473637ceaf Fixed issue where constructor overloads killed me 2017-10-27 20:10:19 -07:00
Westin Miller
6f5142393b MT observable collections
(UI sync fix)
2017-10-27 17:52:27 -07:00
Westin Miller
fdc20d4e9d Keen's logs now go to Logs/Keen-*
Various formatting options
2017-10-27 14:13:56 -07:00
Westin Miller
4d0dcede41 Log unhandled exceptions very verbosely 2017-10-26 12:41:03 -07:00
Westin Miller
4ed262a330 Report loader exceptions 2017-10-26 12:16:28 -07:00
Westin Miller
48f0f81f12 Hard clean v2 2017-10-26 10:54:19 -07:00
Westin Miller
7b9f2d680a Hard clean 2017-10-26 10:51:56 -07:00
Westin Miller
c69537b173 Warning guard around some unsafe stuff 2017-10-26 10:11:51 -07:00
Westin Miller
794a4a23d3 VRage.Steam snuck in 2017-10-26 09:55:49 -07:00
Westin Miller
998ff6a13a Keen removed infinario 2017-10-26 09:17:36 -07:00
Westin Miller
4ff4a60106 Merge branch 'event-mgr' into staging 2017-10-22 03:31:46 -07:00
Westin Miller
eaaca5b003 Register aux assemblies when they aren't loaded from a zip 2017-10-21 04:21:04 -07:00
Westin Miller
388b4731c7 Merge pull request #137 from TorchAPI/observable-collections
Refactor observable collections and add read-only proxies for them
2017-10-12 02:23:48 -07:00
Westin Miller
f285d67c87 Fixed synchronization in assembly resolver
Close #145
2017-10-10 03:58:21 -07:00
Westin Miller
bc1a612a20 Add server state to ITorchServer
Fixes #101
2017-10-10 03:54:23 -07:00
Westin Miller
b67879577d Remove example shim pending writing actual shims 2017-10-10 03:49:48 -07:00
Westin Miller
2b5b9d44e6 Fix load order issues with reflected manager and patches 2017-10-10 03:08:41 -07:00
Westin Miller
3e48638d8c Merge branch 'app_domains' into event-mgr 2017-10-10 03:01:18 -07:00
Westin Miller
3307d2d23d Patch manager supports communicating method cancel state
Game state patch split out into a patch shim
Special NS for events
Core and auxillary assembly concepts
Reflected manager stops working for mods/scripts
2017-10-10 02:47:45 -07:00
Westin Miller
62d73cbf96 Moved event stuff to a non-manager NS.
Core assembly concept: Assemblies that will never need to get unloaded (Torch.API, Torch, Torch.Server)
Event shims and patch shims on the core assemblies.
2017-10-09 20:52:22 -07:00
Westin Miller
2004f71290 Patch manager state is static, registration methods are non-static.
Patch manager tracks patch context ownership by assembly internally
2017-10-09 01:06:01 -07:00
Westin Miller
013dc43c2f Tagging interface for event handlers
Support events in the TorchAPI
Static registry, non-static registration methods.
Support unregister by assembly.
2017-10-09 00:42:21 -07:00
Westin Miller
716e6cbc04 Event dispatch and registration service 2017-10-08 05:32:16 -07:00
Westin Miller
9e81b6316f Better debugging for freezing 2017-10-05 12:56:16 -07:00
Westin Miller
d709bf68dd New system for resolving binaries 2017-10-04 21:40:42 -07:00
Westin Miller
c14b8ed23a Fixes #138 2017-10-04 18:57:47 -07:00
John Gross
7ba6fb5a2e Add observable read only list implementation 2017-10-03 11:44:35 -07:00
John Gross
4f1a03811a Refactor property changed implementations 2017-10-02 23:44:06 -07:00
John Gross
0946d5a138 Update README.md 2017-09-28 13:57:19 -07:00
Westin Miller
6f650c8bbd More MSIL utilities 2017-09-23 01:29:06 -07:00
John Gross
ad1502e998 Small tweaks 2017-09-22 20:08:47 -07:00
Westin Miller
bb42dd026c Some more misc Msil stuff 2017-09-22 14:45:07 -07:00
Westin Miller
95b6c9dfe5 Fix so plugins properly register commands. 2017-09-22 14:30:21 -07:00
Westin Miller
9b1754a431 Merge pull request #113 from TorchAPI/session-mgr-cmp
Managers for Clients
2017-09-22 00:40:14 -07:00
Westin Miller
0574d59e12 Merge branch 'staging' into session-mgr-cmp 2017-09-22 00:22:45 -07:00
Westin Miller
4f7c35dfcf Use the correct binaries 2017-09-21 23:41:29 -07:00
Westin Miller
967384ccfe Clean before build 2017-09-21 23:07:37 -07:00
John Gross
b906a32e23 Merge pull request #121 from TorchAPI/plugins
Plugin loader improvements
2017-09-21 22:58:02 -07:00
John Gross
9b9a4c5ee1 Merge branch 'staging' into plugins 2017-09-21 22:56:32 -07:00
Westin Miller
13f3e7ee11 Jenkins only releases from master, and builds in Release only on master.
Packages don't contain PDB files on master builds either.
2017-09-21 22:54:48 -07:00
John Gross
b7f2a62b3c Expose read-only collections in PluginManager instead of full collections 2017-09-21 22:30:48 -07:00
John Gross
1f4197ce67 Merge branch 'staging' into plugins 2017-09-21 20:26:32 -07:00
John Gross
f377d044d6 Observable type improvements 2017-09-21 20:15:18 -07:00
Westin Miller
205dd1a201 Merge pull request #120 from blaho/session-mgr-cmp
Exposed information about banned players
2017-09-20 08:24:28 -07:00
Tomas Blaho
9c505c4f5d banned player list made readonly, lobby fakes support 2017-09-20 17:16:15 +02:00
Tomas Blaho
eb7f7f4244 MultiplayerManagerLobby doesn't implement IMultiplayerManagerServer 2017-09-20 17:05:23 +02:00
Tomas Blaho
f1fc49d276 Merge branch 'staging' into session-mgr-cmp
# Conflicts:
#	Torch.API/Managers/IMultiplayerManager.cs
#	Torch/Managers/MultiplayerManager.cs
2017-09-20 16:13:38 +02:00
Tomas Blaho
d8e2072493 documentation added 2017-09-20 13:03:06 +02:00
Tomas Blaho
96f813a17b IMultiplayerManager: added list of banned steam ID's 2017-09-20 12:34:09 +02:00
John Gross
a97542e649 Add "Open Folder" functionality to plugins tab, crash-safe plugin updating 2017-09-14 21:09:20 -07:00
Westin Miller
5eceb21ec7 Comments 2017-09-12 01:44:13 -07:00
Westin Miller
a61b646295 ReflectedMethodInfo allows non-public type names.
MsilInstructionExtensions to make life easier
2017-09-11 22:32:37 -07:00
Westin Miller
373c476d2d Better guessing on the generic operand type 2017-09-11 20:26:33 -07:00
Westin Miller
b1145c8926 Utility method to invert common load and store instructions 2017-09-11 19:50:07 -07:00
Westin Miller
0810e76474 Once the game is created we can patch it with impunity. 2017-09-11 18:55:09 -07:00
Westin Miller
57acb274c6 Don't crash when modifying constructor
Tweak log level of assembly resolver.
Events relating to game initialization and shutdown.
Plugin manager loads plugins right before the dependency manager is attached.
2017-09-11 18:28:53 -07:00
Westin Miller
e57f885d3b Log errors in ReflectedManager 2017-09-11 05:19:02 -07:00
Westin Miller
0073e101dd Fixed issues with operand replacing, and branch instructions. 2017-09-11 05:18:07 -07:00
Westin Miller
b42d43c0e1 Redirect Keen console to Info, all else to trace. 2017-09-11 05:17:21 -07:00
Westin Miller
9d8988a2ec MyLog logs to the Torch logging system
Fixed issue with labels not being transferred correctly when transpiling.
2017-09-10 17:40:43 -07:00
Westin Miller
aa784c121b Null protection in multiplayer manager detach 2017-09-10 15:17:50 -07:00
Westin Miller
a36e8a4065 Merge branch 'staging' into session-mgr-cmp 2017-09-10 15:11:08 -07:00
John Gross
3fd7b66905 Rewrite plugin loader/updater to support loading from .zip and more complex versions. 2017-09-10 01:44:15 -07:00
John Gross
44ebcc5d25 Merge pull request #110 from TorchAPI/patching
Patch Manager
2017-09-10 04:05:50 -04:00
Westin Miller
9a68ed6bd0 Added patching tests
Fixed error when reverting patches
Made the LoggingILGenerator not break without a logger
2017-09-09 23:30:49 -07:00
Westin Miller
4f84cd8963 Normal logging for patch manager
Fix: RE treating constructors as normal methods
2017-09-09 22:15:42 -07:00
Westin Miller
837b56462f Use reflected manager for patch internals 2017-09-09 00:50:45 -07:00
Westin Miller
9c3a22c556 Method patching framework 2017-09-09 00:37:34 -07:00
Westin Miller
9471d83a45 Merge pull request #108 from TorchAPI/staging
Prepare Release v1.2.0
2017-09-07 19:56:23 -07:00
Westin Miller
52b225d944 Merge branch 'master' into staging 2017-09-07 15:39:04 -07:00
Westin Miller
c8f0a61209 Provide DS config path to Save
This keeps MyFileSystem from getting initialized too early.
2017-09-07 14:54:57 -07:00
John Gross
59c3e9eb54 Merge pull request #107 from TorchAPI/patch-96
Proper scrolling and formatting for the ChatPanel.
2017-09-07 13:39:15 -04:00
Westin Miller
d20d68b831 Proper scrolling and formatting for the ChatPanel.
- Users are blue, server is dark blue.
- Autoscroll when it's scrolled to the bottom, else leave it alone.
2017-09-07 02:54:10 -07:00
Westin Miller
cfda1f8eef Fixes issues with the assembly resolver getting disposed when launched in nogui mode.
This should solve #102, #104.
2017-09-07 02:03:48 -07:00
Westin Miller
2fb222125a Merge pull request #106 from TorchAPI/reflected-internals
Specify internal types by Assembly Qualified Name for reflection
2017-09-06 20:58:29 -07:00
John Gross
c7c3c00783 Merge branch 'staging' into reflected-internals 2017-09-06 23:57:34 -04:00
Westin Miller
dcc130e2cf Fixed bad exit code.
Fixed creds GUID
2017-09-06 20:33:37 -07:00
Westin Miller
491f3d3af4 Auto-release 2017-08-31 19:36:01 -07:00
Westin Miller
140000df55 Test-time reflected event checker
Server UI components work with new system
Events for loading and unloading TorchSessions
Commands report information again.
Catch, log, and rethrow errors that occur in Torch-handled SE events
2017-08-31 16:27:55 -07:00
Westin Miller
4b2fee7614 Offline mode fallbacks for the chat manager. 2017-08-31 16:27:54 -07:00
Westin Miller
2c7b522378 Proper delegate naming 2017-08-31 16:27:54 -07:00
Westin Miller
d9ef60d4e8 Client manager components
- ReflectedManager now supports MemberInfo and event replacement
- New chat manager interfaces for both client and server
- New multiplayer manager interfaces for both client and server
2017-08-31 16:27:54 -07:00
Westin Miller
178fcb8164 Specify internal types by Assembly Qualified Name for reflection 2017-08-31 16:22:38 -07:00
John Gross
64f123abe9 Specify PR base branch 2017-08-27 09:08:50 -07:00
158 changed files with 10835 additions and 2169 deletions

View File

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

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=$TRUE
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)
}

57
Jenkinsfile vendored
View File

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

View File

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

View File

@@ -3,19 +3,24 @@
# 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.
# Features
## Torch.Server
### Features
* WPF-based user interface
* Chat: interact with the game chat and run commands without having to join the game.
* Entity manager: realtime modification of ingame entities such as stopping grids and changing block settings without having to join the game
* Organized, easy to use configuration editor
* Extensible using the Torch plugin system
# Installation
### Installation
* Get the latest Torch release here: https://github.com/TorchAPI/Torch/releases
* Unzip the Torch release into its own directory and run the executable. It will automatically download the SE DS and generate the other necessary files.
- If you already have a DS installed you can unzip the Torch files into the folder that contains the DedicatedServer64 folder.
## Torch.Client
* An optional client-side version of Torch. More documentation to come.
# Building
To build Torch you must first have a complete SE Dedicated installation somewhere. Before you open the solution, run the Setup batch file and enter the path of that installation's DedicatedServer64 folder. The script will make a symlink to that folder so the Torch solution can find the DLL references it needs.

View File

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

11
Torch.API/Event/IEvent.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

386
Torch.Tests/PatchTest.cs Normal file
View File

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

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Torch.Utils;
using Xunit;
@@ -19,6 +20,10 @@ namespace Torch.Tests
public static IEnumerable<object[]> Invokers => _manager.Invokers;
public static IEnumerable<object[]> MemberInfo => _manager.MemberInfo;
public static IEnumerable<object[]> Events => _manager.Events;
#region Binding
[Theory]
[MemberData(nameof(Getters))]
@@ -52,6 +57,28 @@ namespace Torch.Tests
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(MemberInfo))]
public void TestBindingMemberInfo(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
Assert.NotNull(field.Field.GetValue(null));
}
[Theory]
[MemberData(nameof(Events))]
public void TestBindingEvents(ReflectionTestManager.FieldRef field)
{
if (field.Field == null)
return;
Assert.True(ReflectedManager.Process(field.Field));
if (field.Field.IsStatic)
((Func<ReflectedEventReplacer>)field.Field.GetValue(null)).Invoke();
}
#endregion
#region Results
@@ -79,10 +106,51 @@ namespace Torch.Tests
{
return k >= 0;
}
public event Action Event1;
public ReflectionTestTarget()
{
Event1 += Callback1;
}
public bool Callback1Flag = false;
public void Callback1()
{
Callback1Flag = true;
}
public bool Callback2Flag = false;
public void Callback2()
{
Callback2Flag = true;
}
public void RaiseEvent()
{
Event1?.Invoke();
}
}
private class ReflectionTestBinding
{
#region Instance
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestField")]
public static FieldInfo TestFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestProperty")]
public static PropertyInfo TestPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall")]
public static MethodInfo TestMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", Parameters = new[] { typeof(int) })]
public static MethodInfo TestMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCall", ReturnType = typeof(bool))]
public static MethodInfo TestMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestField")]
public static Func<ReflectionTestTarget, int> TestFieldGetter;
[ReflectedSetter(Name = "TestField")]
@@ -96,7 +164,27 @@ namespace Torch.Tests
[ReflectedMethod]
public static Func<ReflectionTestTarget, int, bool> TestCall;
[ReflectedEventReplace(typeof(ReflectionTestTarget), "Event1", typeof(ReflectionTestTarget), "Callback1")]
public static Func<ReflectedEventReplacer> TestEventReplacer;
#endregion
#region Static
#region MemberInfo
[ReflectedFieldInfo(typeof(ReflectionTestTarget), "TestFieldStatic")]
public static FieldInfo TestStaticFieldInfo;
[ReflectedPropertyInfo(typeof(ReflectionTestTarget), "TestPropertyStatic")]
public static PropertyInfo TestStaticPropertyInfo;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic")]
public static MethodInfo TestStaticMethodInfoGeneral;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", Parameters = new[] { typeof(int) })]
public static MethodInfo TestStaticMethodInfoExplicitArgs;
[ReflectedMethodInfo(typeof(ReflectionTestTarget), "TestCallStatic", ReturnType = typeof(bool))]
public static MethodInfo TestStaticMethodInfoExplicitReturn;
#endregion
[ReflectedGetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
public static Func<int> TestStaticFieldGetter;
[ReflectedSetter(Name = "TestFieldStatic", Type = typeof(ReflectionTestTarget))]
@@ -109,6 +197,7 @@ namespace Torch.Tests
[ReflectedStaticMethod(Type = typeof(ReflectionTestTarget))]
public static Func<int, bool> TestCallStatic;
#endregion
}
#endregion
@@ -215,7 +304,33 @@ namespace Torch.Tests
Assert.True(ReflectionTestBinding.TestCallStatic.Invoke(1));
Assert.False(ReflectionTestBinding.TestCallStatic.Invoke(-1));
}
[Fact]
public void TestInstanceEventReplace()
{
var target = new ReflectionTestTarget();
target.Callback1Flag = false;
target.RaiseEvent();
Assert.True(target.Callback1Flag, "Control test failed");
target.Callback1Flag = false;
target.Callback2Flag = false;
ReflectedEventReplacer binder = ReflectionTestBinding.TestEventReplacer.Invoke();
Assert.True(binder.Test(target), "Binder was unable to find the requested method");
binder.Replace(new Action(() => target.Callback2()), target);
target.RaiseEvent();
Assert.True(target.Callback2Flag, "Substitute callback wasn't called");
Assert.False(target.Callback1Flag, "Original callback wasn't removed");
target.Callback1Flag = false;
target.Callback2Flag = false;
binder.Restore(target);
target.RaiseEvent();
Assert.False(target.Callback2Flag, "Substitute callback wasn't removed");
Assert.True(target.Callback1Flag, "Original callback wasn't restored");
}
#endregion
#endregion
}
}
}

View File

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

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Torch.Utils;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -117,7 +117,7 @@ namespace Torch.Commands
catch (Exception e)
{
context.Respond(e.Message, "Error", MyFontEnum.Red);
Log.Error($"Command '{SyntaxHelp}' from '{Plugin.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}");
Log.Error($"Command '{SyntaxHelp}' from '{Plugin?.Name ?? "Torch"}' threw an exception. Args: {string.Join(", ", context.Args)}");
Log.Error(e);
return true;
}

View File

@@ -2,6 +2,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
using VRage.Game;
using VRage.Game.ModAPI;
@@ -47,7 +48,7 @@ namespace Torch.Commands
Response = message;
if (Player != null)
Torch.Multiplayer.SendMessage(message, sender, Player.IdentityId, font);
Torch.CurrentSession.Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther(sender, message, font, Player.SteamUserId);
}
}
}

View File

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

View File

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

147
Torch/Event/EventList.cs Normal file
View File

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

198
Torch/Event/EventManager.cs Normal file
View File

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

View File

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

25
Torch/Event/IEventList.cs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,9 @@ namespace Torch.Managers
{
private static Logger _log = LogManager.GetLogger(nameof(NetworkManager));
private const string MyTransportLayerField = "TransportLayer";
private const string TransportHandlersField = "m_handlers";
private HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
private const string _myTransportLayerField = "TransportLayer";
private const string _transportHandlersField = "m_handlers";
private readonly HashSet<INetworkHandler> _networkHandlers = new HashSet<INetworkHandler>();
private bool _init;
[ReflectedGetter(Name = "m_typeTable")]
@@ -40,14 +40,14 @@ namespace Torch.Managers
try
{
var syncLayerType = typeof(MySyncLayer);
var transportLayerField = syncLayerType.GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance);
var transportLayerField = syncLayerType.GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance);
if (transportLayerField == null)
throw new TypeLoadException("Could not find internal type for TransportLayer");
var transportLayerType = transportLayerField.FieldType;
if (!Reflection.HasField(transportLayerType, TransportHandlersField))
if (!Reflection.HasField(transportLayerType, _transportHandlersField))
throw new TypeLoadException("Could not find Handlers field");
return true;
@@ -60,15 +60,9 @@ namespace Torch.Managers
throw;
}
}
/// <summary>
/// Loads the network intercept system
/// </summary>
public override void Attach()
{
Torch.SessionLoaded += OnSessionLoaded;
}
private void OnSessionLoaded()
/// <inheritdoc/>
public override void Attach()
{
if (_init)
return;
@@ -79,9 +73,9 @@ namespace Torch.Managers
throw new InvalidOperationException("Reflection unit test failed.");
//don't bother with nullchecks here, it was all handled in ReflectionUnitTest
var transportType = typeof(MySyncLayer).GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType;
var transportInstance = typeof(MySyncLayer).GetField(MyTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.Static.SyncLayer);
var handlers = (IDictionary)transportType.GetField(TransportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
var transportType = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance).FieldType;
var transportInstance = typeof(MySyncLayer).GetField(_myTransportLayerField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(MyMultiplayer.Static.SyncLayer);
var handlers = (IDictionary)transportType.GetField(_transportHandlersField, BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(transportInstance);
var handlerTypeField = handlers.GetType().GenericTypeArguments[0].GetField("messageId"); //Should be MyTransportLayer.HandlerId
object id = null;
foreach (var key in handlers.Keys)
@@ -105,6 +99,12 @@ namespace Torch.Managers
_log.Debug("Initialized network intercept");
}
/// <inheritdoc/>
public override void Detach()
{
// TODO reverse what was done in Attach
}
#region Network Intercept
/// <summary>
@@ -205,6 +205,8 @@ namespace Torch.Managers
}
}
/// <inheritdoc />
public void RegisterNetworkHandler(INetworkHandler handler)
{
var handlerType = handler.GetType().FullName;
@@ -225,6 +227,12 @@ namespace Torch.Managers
_networkHandlers.Add(handler);
}
/// <inheritdoc />
public bool UnregisterNetworkHandler(INetworkHandler handler)
{
return _networkHandlers.Remove(handler);
}
public void RegisterNetworkHandlers(params INetworkHandler[] handlers)
{
foreach (var handler in handlers)

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