Compare commits

..

77 Commits

Author SHA1 Message Date
Moony
48dbcf7fd4 Hotpatch for map loading test fail. 2026-02-08 15:33:30 +01:00
PJB3005
686c47a193 Release notes for ecfaa68ae6 2026-02-08 14:11:50 +01:00
Moony
ecfaa68ae6 Deprecate System.Random leakage 2026-02-08 14:09:48 +01:00
PJB3005
fd27f315cb Add more MapLoaderSystem TextReader/TextWriter overloads
Co-authored-by: kaylie <moony@hellomouse.net>
2026-02-08 14:04:09 +01:00
DrSmugleaf
fe1648d290 Make EntitySystemManager.DependencyCollection inject EntityQuery, make BUIs inject systems and entity queries (#6394)
* Make EntitySystemManager.DependencyCollection inject EntityQuery

* Make BUIs inject systems and entity queries

* Fix import

* We parallelize those

* RIDER I BEG YOU

* Mocked unit tests are my passion

* Perhaps we do not care about fractional milliseconds

* Forgor to make it debug only

* Use Parallel.For instead of ForEach

* Rider I am going to become the joker

* Fix EntMan resolve

* Now with lazy resolve technology

* Use GetOrAdd
2026-02-05 21:35:52 +01:00
PJB3005
ec0c667c33 Add System.StringComparer to sandbox
Fixes #6081
2026-02-05 21:32:59 +01:00
PJB3005
75d0c29973 Add OrderedDictionary to sandbox whitelist
Fixes #6411
2026-02-05 16:38:01 +01:00
PJB3005
5d6dbc18e3 Remove swnfd from ClientDllMap 2026-01-31 22:21:36 +01:00
Princess Cheeseballs
5e160e26ee Prevent a potential EnsureComp exception. (#6405)
Fix potential EnsureComponent collision

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2026-01-31 09:59:39 -05:00
deltanedas
0e54fa7329 add debug info to static protoid in generic class error (#6395)
* add debug info to static protoid in generic class error

* lets see

* goida

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
2026-01-31 11:59:23 +01:00
PJB3005
2722448474 Don't compile new sandbox code outside TOOLS
Fix build
2026-01-31 09:11:24 +01:00
PJB3005
e7f75ab35d Fix size of OSWindows on macOS (& probably Wayland)
The way SDL handles window coordinates passes through the native platform API's behavior instead of trying to make a consistent API, so the way sizes are handled on macOS is different.
2026-01-31 08:13:19 +01:00
Cookie Cakes
57361e8ffd Update PlacementManager::HandleRectRemoveReq (#6403)
Update PlacementManager.cs

* Fixed AGhost observers and player torsos getting deleted with rect delete.
2026-01-30 13:50:20 +01:00
PJB3005
8449015cf8 Locate references to bad compiler generated methods in sandbox
Should help pinpoint issues from params arrays and similar.
2026-01-30 10:19:57 +01:00
PJB3005
72d6a42c27 Fix release notes for previous version 2026-01-29 01:52:25 +01:00
PJB3005
f509405022 Version: 272.0.0 2026-01-29 01:49:02 +01:00
PJB3005
7bb516f0bf Update release notes 2026-01-29 01:49:01 +01:00
PJB3005
521e7981bc Fix ValidateMemberAnalyzer performance
The analyzer was built to go off syntax nodes. This (AFAICT) meant that the SemanticModel had to be recalculated for every single invocation.

If you don't know what the above means: it basically means the compiler has to re-analyze the entire file.

Fix this by moving it to an operation analyzer so the compiler can properly cache the semantic model.
2026-01-29 01:39:06 +01:00
DrSmugleaf
4c87e6185f Add ProfManager.Value guard, write first command argument as a ProfManager value in ExecuteInShell (#6400)
* Add ProfManager.Value guard, write first command argument as a ProfManager value in ExecuteInShell

* Make EntitySystemManager use the new Value method
2026-01-28 21:26:01 +01:00
DrSmugleaf
aaf5003fcf Initialize ProfManager on the server (#6401) 2026-01-28 16:01:58 +01:00
PJB3005
3bec89aaa5 Fix MapCoordinate spawns using grid-relative rotation
This was an undocumented breaking change introduced by https://github.com/space-wizards/RobustToolbox/pull/5915. The behavior does not make much sense: you're specifying coordinates relative to the map, so the rotation should be relative to the map too.
2026-01-26 19:20:12 +01:00
PJB3005
0b93a1b7e2 Version: 271.2.0 2026-01-25 19:11:54 +01:00
PJB3005
51397ba319 Release notes 2026-01-25 19:11:39 +01:00
B_Kirill
446cf8c003 Cleanup warnings: CS0414 (#6396) 2026-01-24 23:41:10 -05:00
PJB3005
65b8d0cce2 Add network serialization float NaN sanitization
Apparently cheat clients have figured out that none of SS14's code does validation against NaN inputs. Uh oh.

IRobustSerializer can now be configured to remove NaN values when reading. This is intended to be set on the server to completely block the issue.

Added "Unsafe" float types that can be used to bypass the new configurable behavior, in case somebody *really* needs NaNs.

An alternative option was to make a "SafeFloat" type, and only apply the sanitization to that. The problem is that would require updating hundreds if not thousands of messages in SS14, and probably significantly confuse contributors on "when use what." Blocking NaNs by default is likely to cause little issues while ensuring the entire exploit is guaranteed impossible.
2026-01-25 03:45:50 +01:00
deltanedas
397b441a17 improve contact debug asserts (#6397)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2026-01-24 08:44:49 -05:00
PJB3005
40b10f0dcc Version: 271.1.0 2026-01-20 19:45:58 +01:00
PJB3005
5885549c78 Release notes 2026-01-20 19:45:51 +01:00
PJB3005
89e16d5ba9 Fix Reset() not getting called if client channel disconnects unprompted.
Fixes https://github.com/space-wizards/RobustToolbox/issues/6390
Fixes https://github.com/space-wizards/RobustToolbox/issues/6388
2026-01-20 19:03:32 +01:00
PJB3005
2afef1480e Add debug code to slow down transfer connections 2026-01-20 19:01:56 +01:00
PJB3005
76189579c7 Remove dead code from transfer 2026-01-20 19:01:39 +01:00
PJB3005
114c2bee62 Fix transfer being entirely nonfunctional
Oops
2026-01-20 19:01:32 +01:00
PJB3005
ce96331ec4 Add completions to launchauth command 2026-01-20 01:14:56 +01:00
Ataman
14b17aff6d Add events on animation starts (#6382)
* added AnimationStartedEvent

* added AnimationStarted event to Control

* reordered Control.AnimationStarted above Control.AnimationCompleted

* fixed comment

* switched to internal constructor and proxy method
2026-01-19 23:29:22 +01:00
PJB3005
65ed19fa4e Version: 271.0.0 2026-01-19 21:07:22 +01:00
PJB3005
30cd9eb527 Update release notes 2026-01-19 21:06:58 +01:00
PJB3005
3f556814a5 Cull old release notes
This file was hundreds of kilobytes and genuinely made my Rider lag to edit
2026-01-19 21:06:25 +01:00
ThereDrD
17662baaf7 Fixed looped audio playback position calculation (#6325)
* fix: looped audio position calculation

* refactor: tiny clean up

* move comment

* fix: review changes

* remove `

* rerun tests

* Remove unecessary formatting changes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2026-01-19 21:05:44 +01:00
Pieter-Jan Briers
dc1464b462 High-bandwidth transfer system (#6373)
* WebSocket-based data transfer system

* Move resource downloads/uploads to the new transfer system

Should drastically increase the permitted practical size

* Transfer impl for Lidgren

* Async impl for receive stream

* Use unbounded channel for Lidgren

* Add metrics

* More comments

* Add serverside stream limit to avoid being a DoS vector

* Fix tests

* Oops forgot to actually implement sequence channels in NetMessage

* Doc comment for NetMessage.SequenceChannel

* Release notes
2026-01-19 20:44:44 +01:00
DrSmugleaf
48654ac424 Fix component tree system crapping out taking half the game with it (#6366)
* Fix component tree system crapping out taking half the game with it

* Fix build
2026-01-19 20:43:50 +01:00
ArtisticRoomba
d9ea1079f7 Fix Robust.Benchmarks failing to compile (#6365) 2026-01-19 20:42:03 +01:00
DrSmugleaf
cb384b8242 Make VV work with structs in components (#6377)
* Make VV work with structs in components

* Fix missing imports
2026-01-19 20:41:24 +01:00
Pieter-Jan Briers
21581df93d Revert "make SharedAudioSystem.Stop not return early when the current tick has already been predicted" (#6375)
Revert "make SharedAudioSystem.Stop not return early when the current tick ha…"

This reverts commit c41d63be27.
2026-01-19 20:40:31 +01:00
Princess Cheeseballs
df98bca4bc Fix Erronius entity deletion on grid deletion from SetTiles() (#6367)
Don't delete the grid *before* we raise the event???

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-19 20:39:49 +01:00
PJB3005
02b64b7386 Show task bar progress bar for loading progress
Using the new API in SDL 3.4.0
2026-01-18 23:30:15 +01:00
PJB3005
36e5f10511 Update SDL3 to 3.4.0 2026-01-18 23:30:15 +01:00
Richard Van Tassel
93d14d55c7 Let viewport clear when eye is missing (#6379)
* Adds option to viewport to allow render target clearing if eye is missing

* on->when and add to IClydeViewport

* add to ClydeHeadless
2026-01-18 21:17:31 +01:00
pathetic meowmeow
c20343601d Make tabs wrap to available size (#6387) 2026-01-18 21:16:44 +01:00
deltanedas
52d3376c9e make "failed to set destination" debug assert not useless (#6383)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2026-01-18 09:27:50 -05:00
PJB3005
a417a8fd99 Mark DebugTools.AssertNotNull as [NotNull]
This means C# nullable analysis actually recognizes it.
2026-01-18 02:42:23 +01:00
Aiden
627856e207 Update error message for direct project references. (#6380)
Update error message for direct project references
2026-01-16 14:55:51 +01:00
Dinner
aa5cca4c7f Fix Layer constructor in SpriteComponent not properly copying unshaded sprite layers (#6368)
* remove Layer() null check for Unshaded and ShaderPrototype variables

* remove comments
2026-01-11 04:03:05 +01:00
eoineoineoin
5b06066fcb Make some Control properties animatable (#6376) 2026-01-10 23:35:08 +01:00
deltanedas
736e46cd82 fix MarkupNode.ToString mangling tags (#6374)
Co-authored-by: deltanedas <@deltanedas:kde.org>
2026-01-09 20:17:46 +01:00
PJB3005
539d0563b8 "Add vorbis" she said. "It'll be easy"
Yeah so this adds libogg, libvorbis, and libopus to the Rust library on the client. This is intended for use by the client soon-ish to replace .NET ogg vorbis implementations and add opus support.

This turns out to be a huge pain thanks to https://github.com/rust-lang/rfcs/issues/2771 . To solve this I ended up compiling the projects as staticlib and creating a build.py script to invoke the linker. This sucks a *lot* and I have yet to write the linker invocations for Linux/Windows, but it's probably the best option we have.
2026-01-04 04:16:20 +01:00
PJB3005
d9740e3a4f Discover the magic of +whole-archive to avoid relying on hacks to load the objective-C++ code in webview native 2026-01-04 04:16:20 +01:00
slarticodefast
89b6bcd8e2 fix console warning spam (#6363)
fix console spam
2026-01-02 20:30:39 -05:00
TGRCDev
7b245260e3 SpriteView updates its size whenever the entity is set (#6362)
* SpriteView updates its size whenever the entity is set

* SpriteView recalculates its measure when the entity changes
2026-01-02 16:41:20 +01:00
PJB3005
68f8d00931 Oops, typo. Actual version is 270.1.0 2026-01-01 12:37:20 +01:00
PJB3005
57ad191d02 Version: 170.1.0 2026-01-01 12:35:52 +01:00
PJB3005
8cecdcc8de Oops one last one 2026-01-01 12:35:42 +01:00
PJB3005
de7cdec86e Update release notes 2026-01-01 12:34:53 +01:00
PJB3005
8ffa85c266 Move WebView appbundle stuff to imports folder 2026-01-01 12:31:08 +01:00
DrSmugleaf
e5be11458e Fix EntProtoId<T>.TryGet throwing an error on invalid prototype ids (#6349) 2026-01-01 00:53:14 +01:00
āda
9464ccb500 Get hard collision API method (#6354)
commit

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-12-31 23:20:14 +01:00
PJB3005
af7f4ec8e7 Make RichTextLabel.Text allow all tags again
Fixes #6360

I debated doing this with a special case in Robust.Xaml instead, but decided against it. This is mostly used for static contents anyways.
2025-12-31 23:15:06 +01:00
slarticodefast
de2dafe507 increase Loc.GetString log level to warning (#6361)
* increase loglevel to warning

* add release notes

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-12-31 19:49:05 +01:00
PJB3005
dd41a7ce44 Use hard links for macOS app bundles
Avoid needing to get executable path from MainModule, which broke the game when run with the dotnet command instead of the bin's apphost. Fixes tests.
2025-12-31 18:46:46 +01:00
PJB3005
c25f6c5e98 Merge branch '25-11-01-cef-update' 2025-12-29 18:19:03 +01:00
Axionyx
2799de33c5 Make run_server.bat cd to the current directory (#6356)
Add a cd to the current directory 

Will literally only affect people doing a "Run as administrator" as far as I'm aware, but still good to have I suppose
2025-12-28 20:50:15 +01:00
PJB3005
1fea48fbf4 Don't disable GPU compositing in CEF
Workaround no longer needed with message pump fixed. I think.
2025-12-28 02:16:23 +01:00
PJB3005
636e287fc5 Add new msbuild files to slnx 2025-12-27 22:29:15 +01:00
PJB3005
d43c3f2caf Merge remote-tracking branch 'origin/master' into 25-11-01-cef-update 2025-12-27 01:50:23 +01:00
PJB3005
a1dddf6af1 More macOS fixes, introduce RUST code 2025-12-27 01:44:57 +01:00
PJB3005
5f1327808d Mark IRobustRandom.GetRandom() as obsolete
This API should've never existed.
2025-12-24 16:03:06 +01:00
Princess Cheeseballs
d78e3ce157 Extend AddMessage capabilites in RobustToolbox. (#6350)
push or w/e

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-12-24 15:42:40 +01:00
PJB3005
602d7833a1 Initial macOS WebView support
This is a gigantic kerfuffle because Chromium expects a very specific directory & app bundle layout. Have to change a bunch of resource loading code to account for content development being launched from an app bundle, and also had to make automatic MSBuild tooling & a python script to generate such an app bundle
2025-11-09 00:06:16 +01:00
176 changed files with 5261 additions and 6115 deletions

View File

@@ -59,7 +59,7 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.2.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
<PackageVersion Include="SpaceWizards.Sdl" Version="1.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.3.0" />
<PackageVersion Include="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />

View File

@@ -1,4 +1,13 @@
<Project>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- If you are using Robust.Client.WebView, import this to depend on it. -->
<Import Condition="'$(_RTMacOSAppBundle_targets_imported)' != 'True'"
Project="$(MSBuildThisFileDirectory)\..\MSBuild\MacOSAppBundle.targets" />
<PropertyGroup>
<_RTMacOSAppBundle_for_webview>--webview</_RTMacOSAppBundle_for_webview>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.WebView\Robust.Client.WebView.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,24 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
Depend on this in your client project (e.g. Content.Client) to generate a development app bundle for macOS.
This is required for WebView.
-->
<PropertyGroup>
<RTMakeAppBundle Condition="'$(TargetOS)' == 'MacOS' And '$(RTMakeAppBundle)' == '' And '$(FullRelease)' != 'True'">True</RTMakeAppBundle>
<RTAppBundleName Condition="'$(RTAppBundleName)' == ''">RobustToolbox Project</RTAppBundleName>
<RTAppBundleIdentifier Condition="'$(RTAppBundleIdentifier)' == ''">org.robusttoolbox.project</RTAppBundleIdentifier>
<!-- RTAppBundleIcon controls icon -->
</PropertyGroup>
<PropertyGroup>
<_RTMacOSAppBundle_targets_imported>True</_RTMacOSAppBundle_targets_imported>
</PropertyGroup>
<Target Name="RTMakeAppBundleAfterBuild" Condition="'$(RTMakeAppBundle)' == 'True'" AfterTargets="AfterBuild">
<PropertyGroup>
<_RTMacOSAppBundle_icon Condition="'$(RTAppBundleIcon)' != ''">--icon &quot;$(RTAppBundleIcon)&quot;</_RTMacOSAppBundle_icon>
</PropertyGroup>
<Exec Command="$(MSBuildThisFileDirectory)/../Tools/macos_make_appbundle.py $(_RTMacOSAppBundle_for_webview) --name &quot;$(RTAppBundleName)&quot; --directory &quot;$(OutputPath)&quot; --apphost &quot;$(AssemblyName)&quot; --identifier &quot;$(RTAppBundleIdentifier)&quot; $(_RTMacOSAppBundle_icon)" />
</Target>
</Project>

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -11,7 +11,7 @@
<Target Name="_RTCheckForDirectReferences" BeforeTargets="BeforeResolveReferences"
Condition="'$(AllowDirectRobustReferences)' != 'true'">
<Error File="%(ProjectReference.DefiningProjectFullPath)"
Text="Content may not reference %(Filename) directly"
Text="Direct reference to %(Filename) is not allowed. Use RobustToolbox/Imports/*.props instead (e.g., Shared.props, Client.props, Server.props)"
Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Identity)', 'RobustToolbox')) and !$([System.Text.RegularExpressions.Regex]::IsMatch('%(DefiningProjectFullPath)', '([Mm]icrosoft|RobustToolbox)'))" />
</Target>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,9 @@ cmd-replay-pause-help = replay_pause
cmd-replay-toggle-desc = Resume or pause replay playback.
cmd-replay-toggle-help = replay_toggle
cmd-replay-toggle-screenshot-mode-desc = Toggles screenshot mode for replays, hiding the replay control widget.
cmd-replay-toggle-screenshot-mode-help = replay_toggle_screenshot_mode
cmd-replay-stop-desc = Stop and unload a replay.
cmd-replay-stop-help = replay_stop

View File

@@ -1,8 +1,6 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
@@ -29,16 +27,15 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterSyntaxNodeAction(AnalyzeExpression, SyntaxKind.InvocationExpression);
context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation);
}
private void AnalyzeExpression(SyntaxNodeAnalysisContext context)
private void AnalyzeOperation(OperationAnalysisContext context)
{
if (context.Node is not InvocationExpressionSyntax node)
if (context.Operation is not IInvocationOperation node)
return;
if (context.SemanticModel.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol methodSymbol)
return;
var methodSymbol = node.TargetMethod;
// We need at least one type argument for context
if (methodSymbol.TypeArguments.Length < 1)
@@ -48,16 +45,12 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer
if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol targetType)
return;
// We defer building this set until we need it later, so we don't have to build it for every single method invocation!
ImmutableHashSet<ISymbol>? members = null;
// Check each parameter of the method
foreach (var parameterContext in node.ArgumentList.Arguments)
foreach (var op in node.Arguments)
{
// Get the symbol for this parameter
if (context.SemanticModel.GetOperation(parameterContext) is not IArgumentOperation op || op.Parameter is null)
if (op.Parameter is null)
continue;
var parameterSymbol = op.Parameter.OriginalDefinition;
// Make sure the parameter has the ValidateMember attribute
@@ -66,15 +59,12 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer
// Find the value passed for this parameter.
// We use GetConstantValue to resolve compile-time values - i.e. the result of nameof()
if (context.SemanticModel.GetConstantValue(parameterContext.Expression).Value is not string fieldName)
if (op.Value.ConstantValue is not { HasValue: true, Value: string fieldName})
continue;
// Get a set containing all the members of the target type and its ancestors
members ??= targetType.GetBaseTypesAndThis().SelectMany(n => n.GetMembers()).ToImmutableHashSet(SymbolEqualityComparer.Default);
// Check each member of the target type to see if it matches our passed in value
var found = false;
foreach (var member in members)
foreach (var member in targetType.GetMembers())
{
if (member.Name == fieldName)
{
@@ -84,12 +74,14 @@ public sealed class ValidateMemberAnalyzer : DiagnosticAnalyzer
}
// If we didn't find it, report the violation
if (!found)
{
context.ReportDiagnostic(Diagnostic.Create(
ValidateMemberDescriptor,
parameterContext.GetLocation(),
op.Syntax.GetLocation(),
fieldName,
targetType.Name
));
));
}
}
}
}

View File

@@ -16,7 +16,11 @@
<ProjectReference Include="..\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<!-- BenchmarkDotNet autogenerates files that attempt to reference BenchmarkDotNet through Robust.Benchmarks.
By default the RT project privates these files, so we have to explicitly state that these files should be made available
to the BenchmarkDotNet project so it can build the runner that executes the benchmark. -->
<PackageReference Include="BenchmarkDotNet" PrivateAssets="none" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="prometheus-net" />

View File

@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.ContentPack;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
@@ -20,6 +22,20 @@ namespace Robust.Client.WebView.Cef
argv[0] = "-";
}
#if MACOS
NativeLibrary.SetDllImportResolver(typeof(CefSettings).Assembly,
(name, assembly, path) =>
{
if (name == "libcef")
{
var libPath = PathHelpers.ExecutableRelativeFile("../../../../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework");
return NativeLibrary.Load(libPath, assembly, path);
}
return 0;
});
#endif
var mainArgs = new CefMainArgs(argv);
StartWatchThread();

View File

@@ -41,7 +41,7 @@ namespace Robust.Client.WebView.Cef
// commandLine.AppendSwitch("--single-process");
//commandLine.AppendSwitch("--disable-gpu");
commandLine.AppendSwitch("--disable-gpu-compositing");
//commandLine.AppendSwitch("--disable-gpu-compositing");
//commandLine.AppendSwitch("--in-process-gpu");
commandLine.AppendSwitch("--off-screen-rendering-enabled");
@@ -54,6 +54,7 @@ namespace Robust.Client.WebView.Cef
protected override void OnRegisterCustomSchemes(CefSchemeRegistrar registrar)
{
// NOTE: KEEP IN SYNC WITH RUST CODE!
registrar.AddCustomScheme("res", CefSchemeOptions.Secure | CefSchemeOptions.Standard);
registrar.AddCustomScheme("usr", CefSchemeOptions.Secure | CefSchemeOptions.Standard);
}

View File

@@ -17,11 +17,13 @@ namespace Robust.Client.WebView.Cef
{
internal partial class WebViewManagerCef
{
private readonly List<ControlImpl> _activeControls = new();
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
{
var shader = _prototypeManager.Index<ShaderPrototype>("bgra");
var shaderInstance = shader.Instance();
var impl = new ControlImpl(owner, shaderInstance);
var impl = new ControlImpl(this, owner, shaderInstance);
_dependencyCollection.InjectDependencies(impl);
return impl;
}
@@ -133,11 +135,13 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IInputManager _inputMgr = default!;
private readonly WebViewManagerCef _manager;
public readonly WebViewControl Owner;
private readonly ShaderInstance _shaderInstance;
public ControlImpl(WebViewControl owner, ShaderInstance shaderInstance)
public ControlImpl(WebViewManagerCef manager, WebViewControl owner, ShaderInstance shaderInstance)
{
_manager = manager;
Owner = owner;
_shaderInstance = shaderInstance;
}
@@ -194,6 +198,7 @@ namespace Robust.Client.WebView.Cef
var texture = _clyde.CreateBlankTexture<Rgba32>(Vector2i.One);
_data = new LiveData(texture, client, browser, renderer);
_manager._activeControls.Add(this);
}
public void CloseBrowser()
@@ -203,6 +208,8 @@ namespace Robust.Client.WebView.Cef
_data!.Texture.Dispose();
_data.Browser.GetHost().CloseBrowser(true);
_data = null;
_manager._activeControls.Remove(this);
}
public void MouseMove(GUIMouseMoveEventArgs args)
@@ -279,6 +286,7 @@ namespace Robust.Client.WebView.Cef
// Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");
#if !MACOS
var lParam = 0;
lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
if (guiRawEvent.Action != RawKeyAction.Down)
@@ -286,7 +294,9 @@ namespace Robust.Client.WebView.Cef
if (guiRawEvent.Action == RawKeyAction.Up)
lParam |= 1 << 31;
#else
var lParam = guiRawEvent.RawCode;
#endif
var modifiers = CalcModifiers(guiRawEvent.Key);
host.SendKeyEvent(new CefKeyEvent
@@ -307,7 +317,7 @@ namespace Robust.Client.WebView.Cef
host.SendKeyEvent(new CefKeyEvent
{
EventType = CefKeyEventType.Char,
WindowsKeyCode = '\r',
WindowsKeyCode = '\b',
NativeKeyCode = lParam,
Modifiers = modifiers
});

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -44,10 +45,11 @@ namespace Robust.Client.WebView.Cef
_localization.GetString("cmd-flushcookies-help"),
(_, _, _) => CefCookieManager.GetGlobal(null).FlushStore(null));
#if !MACOS
string subProcessName;
if (OperatingSystem.IsWindows())
subProcessName = "Robust.Client.WebView.exe";
else if (OperatingSystem.IsLinux())
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
subProcessName = "Robust.Client.WebView";
else
throw new NotSupportedException("Unsupported platform for CEF!");
@@ -60,19 +62,39 @@ namespace Robust.Client.WebView.Cef
if (cefResourcesPath == null)
throw new InvalidOperationException("Unable to locate cef_resources directory!");
#endif
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
var cachePath = FindAndLockCacheDirectory();
#if MACOS
NativeLibrary.SetDllImportResolver(typeof(CefSettings).Assembly,
(name, assembly, path) =>
{
if (name == "libcef")
{
var libPath = PathHelpers.ExecutableRelativeFile("../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework");
return NativeLibrary.Load(libPath, assembly, path);
}
return 0;
});
// Needed to implement CefAppProtocol on our NSApplication.
NativeLibrary.Load("robust_native_webview", typeof(WebViewManagerCef).Assembly, null);
#endif
var settings = new CefSettings()
{
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
ExternalMessagePump = true,
NoSandbox = true, // Not disabling the sandbox crashes CEF.
#if !MACOS
BrowserSubprocessPath = subProcessPath,
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
ResourcesDirPath = cefResourcesPath,
#endif
RemoteDebuggingPort = remoteDebugPort,
CookieableSchemesList = "usr,res",
CachePath = cachePath,
@@ -113,7 +135,6 @@ namespace Robust.Client.WebView.Cef
if (ProbeDir(BasePath, out var path))
return path;
foreach (var searchDir in NativeDllSearchDirectories())
{
if (ProbeDir(searchDir, out path))
@@ -147,6 +168,16 @@ namespace Robust.Client.WebView.Cef
public void Shutdown()
{
foreach (var control in _activeControls.ToArray())
{
control.CloseBrowser();
}
foreach (var window in _browserWindows.ToArray())
{
window.Dispose();
}
CefRuntime.Shutdown();
}

View File

@@ -8,10 +8,10 @@ internal sealed class TestBrowseWindow : DefaultWindow
{
protected override Vector2 ContentsMinimumSize => new Vector2(640, 480);
public TestBrowseWindow()
public TestBrowseWindow(string url)
{
var wv = new WebViewControl();
wv.Url = "https://spacestation14.io";
wv.Url = url;
Contents.AddChild(wv);
}
@@ -23,6 +23,15 @@ internal sealed class TestBrowseWindowCommand : LocalizedCommands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
new TestBrowseWindow().Open();
var url = args.Length > 0 ? args[0] : "https://spacestation14.com";
new TestBrowseWindow(url).Open();
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHint("<url>");
return CompletionResult.Empty;
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using JetBrains.Annotations;
using OpenTK.Audio.OpenAL;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -17,7 +16,6 @@ using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
@@ -182,14 +180,16 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
// If playback position changed then update it.
var position = (float) ((entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart).TotalSeconds;
var totalLen = GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds;
var position = CalculateAudioPosition(entity, (float) totalLen);
var currentPosition = entity.Comp.Source.PlaybackPosition;
var diff = Math.Abs(position - currentPosition);
// Don't try to set the audio too far ahead.
if (!string.IsNullOrEmpty(entity.Comp.FileName))
{
if (position > GetAudioLengthImpl(entity.Comp.FileName).TotalSeconds - _audioEndBuffer)
if (position > totalLen - _audioEndBuffer)
{
entity.Comp.StopPlaying();
return;
@@ -251,7 +251,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
length ??= GetAudioLength(component.FileName);
// If audio came into range then start playback at the correct position.
var offset = ((entity.Comp.PauseTime ?? Timing.CurTime) - component.AudioStart).TotalSeconds;
var offset = CalculateAudioPosition(entity, (float) length.Value.TotalSeconds);
if (TryAudioLimit(component.FileName))
{
@@ -289,7 +289,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (offset > 0)
{
component.PlaybackPosition = (float) offset;
component.PlaybackPosition = offset;
}
}
@@ -755,10 +755,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
var comp = entity.Comp;
var source = comp.Source;
// TODO clamp the offset inside of SetPlaybackPosition() itself.
var offset = audioP.PlayOffsetSeconds;
var maxOffset = Math.Max((float) stream.Length.TotalSeconds - 0.01f, 0f);
offset = Math.Clamp(offset, 0f, maxOffset);
var offset = CalculateAudioPosition(entity, (float)stream.Length.TotalSeconds, audioP.PlayOffsetSeconds);
source.PlaybackPosition = offset;
source.StartPlaying();

View File

@@ -13,6 +13,7 @@ using Robust.Client.HWId;
using Robust.Client.Input;
using Robust.Client.Localization;
using Robust.Client.Map;
using Robust.Client.Network.Transfer;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.Profiling;
@@ -41,6 +42,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -174,6 +176,8 @@ namespace Robust.Client
deps.Register<IXamlProxyHelper, XamlProxyHelper>();
deps.Register<MarkupTagManager>();
deps.Register<IHWId, BasicHWId>();
deps.Register<ITransferManager, ClientTransferManager>();
deps.Register<ClientTransferTestManager>();
}
}
}

View File

@@ -1,6 +1,9 @@
#if TOOLS
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Robust.Client.Utility;
using Robust.Shared.Console;
@@ -20,15 +23,7 @@ namespace Robust.Client.Console.Commands
{
var wantName = args.Length > 0 ? args[0] : null;
var basePath = UserDataDir.GetRootUserDataDir(_gameController);
var launcherDirName = Environment.GetEnvironmentVariable("SS14_LAUNCHER_APPDATA_NAME") ?? "launcher";
var dbPath = Path.Combine(basePath, launcherDirName, "settings.db");
#if USE_SYSTEM_SQLITE
SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
#endif
using var con = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
con.Open();
using var con = GetDb();
using var cmd = con.CreateCommand();
cmd.CommandText = "SELECT UserId, UserName, Token FROM Login WHERE Expires > datetime('NOW')";
@@ -57,6 +52,51 @@ namespace Robust.Client.Console.Commands
shell.WriteLine($"Logged into account {userName}");
}
public override async ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
string argStr,
CancellationToken cancel)
{
if (args.Length != 1)
return CompletionResult.Empty;
return await Task.Run(() =>
{
using var con = GetDb();
using var cmd = con.CreateCommand();
cmd.CommandText = "SELECT UserName FROM Login WHERE Expires > datetime('NOW')";
var options = new List<CompletionOption>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
var name = reader.GetString(0);
options.Add(new CompletionOption(name));
}
return CompletionResult.FromOptions(options);
},
cancel);
}
private SqliteConnection GetDb()
{
var basePath = UserDataDir.GetRootUserDataDir(_gameController);
var launcherDirName = Environment.GetEnvironmentVariable("SS14_LAUNCHER_APPDATA_NAME") ?? "launcher";
var dbPath = Path.Combine(basePath, launcherDirName, "settings.db");
#if USE_SYSTEM_SQLITE
SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
#endif
var con = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
con.Open();
return con;
}
}
}

View File

@@ -11,6 +11,7 @@ using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Network.Transfer;
using Robust.Client.Placement;
using Robust.Client.Replays.Loading;
using Robust.Client.Replays.Playback;
@@ -35,6 +36,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
@@ -98,6 +100,8 @@ namespace Robust.Client
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
[Dependency] private readonly LoadingScreenManager _loadscr = default!;
[Dependency] private readonly ITransferManager _transfer = default!;
[Dependency] private readonly ClientTransferTestManager _transferTest = default!;
private IWebViewManagerHook? _webViewHook;
@@ -107,6 +111,7 @@ namespace Robust.Client
private IMainArgs? _loaderArgs;
public bool ContentStart { get; set; } = false;
public StartType StartTypeValue { get; private set; }
public GameControllerOptions Options { get; private set; } = new();
public InitialLaunchState LaunchState { get; private set; } = default!;
@@ -200,7 +205,14 @@ namespace Robust.Client
_logManager.GetSawmill("res"));
_loadscr.LoadingStep(_resourceCache.PreloadTextures, "Texture preload");
_loadscr.LoadingStep(() => { _networkManager.Initialize(false); }, _networkManager);
_loadscr.LoadingStep(() =>
{
_networkManager.Initialize(false);
_transfer.Initialize();
_transferTest.Initialize();
},
_networkManager);
_loadscr.LoadingStep(_configurationManager.SetupNetworking, _configurationManager);
_loadscr.LoadingStep(_serializer.Initialize, _serializer);
_loadscr.LoadingStep(_inputManager.Initialize, _inputManager);
@@ -424,9 +436,18 @@ namespace Robust.Client
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
StartTypeValue = ContentStart ? StartType.Content : StartType.Engine;
#if FULL_RELEASE
if (_loaderArgs != null || Options.ResourceMountDisabled)
StartTypeValue = StartType.Loader;
#else
if (StartTypeValue == StartType.Content && Path.GetFileName(PathHelpers.GetExecutableDirectory()) == "MacOS")
StartTypeValue = StartType.ContentAppBundle;
#endif
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
Options.LoadContentResources, StartTypeValue);
if (_loaderArgs != null)
{
@@ -675,6 +696,11 @@ namespace Robust.Client
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
}
using (_prof.Group("Transfer"))
{
_transfer.FrameUpdate();
}
_audio.FlushALDisposeQueues();
}

View File

@@ -1336,11 +1336,10 @@ namespace Robust.Client.GameObjects
public Layer(Layer toClone, SpriteComponent parent) : this(parent)
{
if (toClone.Shader != null)
{
Shader = toClone.Shader.Mutable ? toClone.Shader.Duplicate() : toClone.Shader;
UnShaded = toClone.UnShaded;
ShaderPrototype = toClone.ShaderPrototype;
}
UnShaded = toClone.UnShaded;
ShaderPrototype = toClone.ShaderPrototype;
Texture = toClone.Texture;
RSI = toClone.RSI;
State = toClone.State;

View File

@@ -14,9 +14,9 @@ namespace Robust.Client.GameObjects
private EntityQuery<AnimationPlayerComponent> _playerQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
#pragma warning disable CS0414
#if DEBUG
[Dependency] private readonly IComponentFactory _compFact = default!;
#pragma warning restore CS0414
#endif
public override void Initialize()
{
@@ -154,6 +154,9 @@ namespace Robust.Client.GameObjects
}
ent.Comp.PlayingAnimations.Add(key, playback);
var startedEvent = new AnimationStartedEvent(ent.Owner, ent.Comp, key);
RaiseLocalEvent(ent.Owner, startedEvent, true);
}
public bool HasRunningAnimation(EntityUid uid, string key)
@@ -199,6 +202,34 @@ namespace Robust.Client.GameObjects
}
}
/// <summary>
/// Raised whenever an animation started playing.
/// </summary>
public sealed class AnimationStartedEvent : EntityEventArgs
{
/// <summary>
/// The entity associated with the event.
/// </summary>
public EntityUid Uid { get; init; }
/// <summary>
/// The animation player component associated with the entity this event was raised on.
/// </summary>
public AnimationPlayerComponent AnimationPlayer { get; init; }
/// <summary>
/// The key associated with the animation that was started.
/// </summary>
public string Key { get; init; } = string.Empty;
internal AnimationStartedEvent(EntityUid uid, AnimationPlayerComponent animationPlayer, string key)
{
Uid = uid;
AnimationPlayer = animationPlayer;
Key = key;
}
}
/// <summary>
/// Raised whenever an animation stops, either due to running its course or being stopped manually.
/// </summary>

View File

@@ -493,6 +493,9 @@ namespace Robust.Client.Graphics.Clyde
{
if (viewport.Eye == null || viewport.Eye.Position.MapId == MapId.Nullspace)
{
if (viewport.ClearWhenMissingEye)
RenderInRenderTarget(viewport.RenderTarget, () => { }, viewport.ClearColor);
return;
}

View File

@@ -138,6 +138,7 @@ namespace Robust.Client.Graphics.Clyde
public Vector2i Size { get; set; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public bool ClearWhenMissingEye { get; set; }
public Vector2 RenderScale { get; set; } = Vector2.One;
public bool AutomaticRender { get; set; }
public long Id { get; }

View File

@@ -601,6 +601,11 @@ namespace Robust.Client.Graphics.Clyde
_clyde._windowing!.TextInputStop(Reg);
}
public void SetWindowProgress(WindowProgressState state, float value)
{
_clyde._windowing!.WindowSetProgress(Reg, state, value);
}
public nint? WindowsHWnd => _clyde._windowing!.WindowGetWin32Window(Reg);
}

View File

@@ -91,6 +91,8 @@ namespace Robust.Client.Graphics.Clyde
private bool _earlyGLInit;
private bool _threadWindowApi;
public bool IsInitialized { get; private set; }
public Clyde()
{
_currentBoundRenderTarget = default!;
@@ -183,6 +185,8 @@ namespace Robust.Client.Graphics.Clyde
if (!InitMainWindowAndRenderer())
return false;
IsInitialized = true;
return true;
}

View File

@@ -27,6 +27,7 @@ namespace Robust.Client.Graphics.Clyde
internal sealed class ClydeHeadless : IClydeInternal
{
// Would it make sense to report a fake resolution like 720p here so code doesn't break? idk.
public bool IsInitialized { get; private set; }
public IClydeWindow MainWindow { get; }
public Vector2i ScreenSize => (1280, 720);
public IEnumerable<IClydeWindow> AllWindows => _windows;
@@ -172,6 +173,7 @@ namespace Robust.Client.Graphics.Clyde
public bool InitializePostWindowing()
{
IsInitialized = true;
return true;
}
@@ -524,6 +526,7 @@ namespace Robust.Client.Graphics.Clyde
public Vector2i Size { get; }
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
public Color? ClearColor { get; set; } = Color.Black;
public bool ClearWhenMissingEye { get; set; }
public Vector2 RenderScale { get; set; }
public bool AutomaticRender { get; set; }
@@ -582,6 +585,11 @@ namespace Robust.Client.Graphics.Clyde
public event Action<WindowDestroyedEventArgs>? Destroyed;
public event Action<WindowResizedEventArgs>? Resized { add { } remove { } }
public void SetWindowProgress(WindowProgressState state, float value)
{
// Nop.
}
public void TextInputSetRect(UIBox2i rect, int cursor)
{
// Nop.

View File

@@ -42,6 +42,7 @@ namespace Robust.Client.Graphics.Clyde
void WindowSetSize(WindowReg window, Vector2i size);
void WindowSetVisible(WindowReg window, bool visible);
void WindowRequestAttention(WindowReg window);
void WindowSetProgress(WindowReg reg, WindowProgressState state, float value);
void WindowSwapBuffers(WindowReg window);
uint? WindowGetX11Id(WindowReg window);
nint? WindowGetX11Display(WindowReg window);

View File

@@ -38,7 +38,7 @@ internal partial class Clyde
fixed (Rgba32* pixPtr = img.GetPixelSpan())
{
var surface = SDL.SDL_CreateSurfaceFrom(
var surface = (nint) SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,

View File

@@ -163,7 +163,7 @@ internal partial class Clyde
var button = ConvertSdl3Button(ev.Button);
var key = Mouse.MouseButtonToKey(button);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0, 0);
}
private void ProcessEventMouseMotion(EventMouseMotion ev)
@@ -227,10 +227,10 @@ internal partial class Clyde
private void ProcessEventKey(EventKey ev)
{
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode, ev.Raw);
}
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode, ushort rawCode)
{
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
@@ -241,7 +241,8 @@ internal partial class Clyde
key,
repeat,
alt, control, shift, system,
(int)scancode);
(int)scancode,
rawCode);
switch (type)
{

View File

@@ -140,6 +140,7 @@ internal partial class Clyde
{
WindowId = ev.windowID,
Scancode = ev.scancode,
Raw = ev.raw,
Type = ev.type,
Repeat = ev.repeat,
Mods = ev.mod,
@@ -194,6 +195,7 @@ internal partial class Clyde
{
public uint WindowId;
public SDL.SDL_Scancode Scancode;
public ushort Raw;
public ET Type;
public bool Repeat;
public SDL.SDL_Keymod Mods;

View File

@@ -21,6 +21,7 @@ internal partial class Clyde
private sealed partial class Sdl3WindowingImpl
{
private int _nextWindowId = 1;
private bool _progressUnavailable;
public (WindowReg?, string? error) WindowCreate(
GLContextSpec? spec,
@@ -438,7 +439,8 @@ internal partial class Clyde
private static void WinThreadWinSetSize(CmdWinSetSize cmd)
{
SDL.SDL_SetWindowSize(cmd.Window, cmd.W, cmd.H);
var density = SDL.SDL_GetWindowPixelDensity(cmd.Window);
SDL.SDL_SetWindowSize(cmd.Window, (int)(cmd.W / density), (int)(cmd.H / density));
}
private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
@@ -461,6 +463,42 @@ internal partial class Clyde
_sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
}
public void WindowSetProgress(WindowReg window, WindowProgressState state, float value)
{
SendCmd(new CmdWinSetProgress
{
Window = WinPtr(window),
State = (SDL.SDL_ProgressState)state,
Value = value
});
}
private void WinThreadWinSetProgress(CmdWinSetProgress cmd)
{
if (_progressUnavailable)
return;
try
{
var res = SDL.SDL_SetWindowProgressState(cmd.Window, cmd.State);
if (!res)
{
_sawmill.Error("Failed to set window progress state: {error}", SDL.SDL_GetError());
return;
}
res = SDL.SDL_SetWindowProgressValue(cmd.Window, cmd.Value);
if (!res)
_sawmill.Error("Failed to set window progress value: {error}", SDL.SDL_GetError());
}
catch (EntryPointNotFoundException)
{
// Allowing it to fail means I don't have to update the launcher immediately :)
_progressUnavailable = true;
_sawmill.Debug("SDL3 progress APIs unavailable");
}
}
public unsafe void WindowSwapBuffers(WindowReg window)
{
var reg = (Sdl3WindowReg)window;

View File

@@ -75,7 +75,7 @@ internal partial class Clyde
IntPtr surface;
fixed (byte* ptr = copied)
{
surface = SDL.SDL_CreateSurfaceFrom(
surface = (nint) SDL.SDL_CreateSurfaceFrom(
img.Width,
img.Height,
SDL.SDL_PixelFormat.SDL_PIXELFORMAT_ABGR8888,

View File

@@ -93,6 +93,10 @@ internal partial class Clyde
WinThreadWinRequestAttention(cmd);
break;
case CmdWinSetProgress cmd:
WinThreadWinSetProgress(cmd);
break;
case CmdWinSetSize cmd:
WinThreadWinSetSize(cmd);
break;
@@ -261,6 +265,13 @@ internal partial class Clyde
public nint Window;
}
private sealed class CmdWinSetProgress : CmdBase
{
public nint Window;
public SDL.SDL_ProgressState State;
public float Value;
}
private sealed class CmdWinSetSize : CmdBase
{
public nint Window;

View File

@@ -18,6 +18,8 @@ namespace Robust.Client.Graphics
[NotContentImplementable]
public interface IClyde
{
internal bool IsInitialized { get; }
IClydeWindow MainWindow { get; }
IRenderTarget MainWindowRenderTarget => MainWindow.RenderTarget;

View File

@@ -43,6 +43,11 @@ namespace Robust.Client.Graphics
/// </summary>
Color? ClearColor { get; set; }
/// <summary>
/// On frames where Eye is null or in nullspace, whether the viewport may clear.
/// </summary>
bool ClearWhenMissingEye { get; set; }
/// <summary>
/// This is, effectively, a multiplier to the eye's zoom.
/// </summary>

View File

@@ -2,6 +2,7 @@
using System.Numerics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using SDL3;
namespace Robust.Client.Graphics
{
@@ -43,6 +44,8 @@ namespace Robust.Client.Graphics
/// </summary>
event Action<WindowResizedEventArgs> Resized;
internal void SetWindowProgress(WindowProgressState state, float value);
/// <summary>
/// Set the active text input area in window pixel coordinates.
/// </summary>
@@ -68,6 +71,15 @@ namespace Robust.Client.Graphics
void TextInputStop();
}
internal enum WindowProgressState : byte
{
None = SDL.SDL_ProgressState.SDL_PROGRESS_STATE_NONE,
Indeterminate = SDL.SDL_ProgressState.SDL_PROGRESS_STATE_INDETERMINATE,
Normal = SDL.SDL_ProgressState.SDL_PROGRESS_STATE_NORMAL,
Paused = SDL.SDL_ProgressState.SDL_PROGRESS_STATE_PAUSED,
Error = SDL.SDL_ProgressState.SDL_PROGRESS_STATE_ERROR
}
[NotContentImplementable]
internal interface IClydeWindowInternal : IClydeWindow
{

View File

@@ -118,6 +118,13 @@ internal sealed class LoadingScreenManager : ILoadingScreenManager
_currentSectionName = sectionName;
if (_clyde.IsInitialized)
{
_clyde.MainWindow.SetWindowProgress(
WindowProgressState.Normal,
_currentSection / (float)_numberOfLoadingSections);
}
if (!dontRender)
{
// This ensures that if the screen was resized or something the new size is properly updated to clyde.
@@ -176,6 +183,8 @@ internal sealed class LoadingScreenManager : ILoadingScreenManager
if (_currentSection != _numberOfLoadingSections)
_sawmill.Error($"The number of seen loading sections isn't equal to the total number of loading sections! Seen: {_currentSection}, Total: {_numberOfLoadingSections}");
_clyde.MainWindow.SetWindowProgress(WindowProgressState.None, 1);
_finished = true;
}

View File

@@ -1,5 +1,6 @@
using System;
using Robust.Client.Input;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Timing;
@@ -9,6 +10,7 @@ namespace Robust.Client
{
GameControllerOptions Options { get; }
bool ContentStart { get; set; }
StartType StartTypeValue { get; }
void SetCommandLineArgs(CommandLineArgs args);
void Run(GameController.DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null);
void KeyDown(KeyEventArgs keyEvent);

View File

@@ -106,17 +106,20 @@ namespace Robust.Client.Input
public bool IsRepeat { get; }
public int ScanCode { get; }
internal ushort RawCode { get; }
public KeyEventArgs(
Keyboard.Key key,
bool repeat,
bool alt, bool control, bool shift, bool system,
int scanCode)
int scanCode,
ushort rawCode=0)
: base(alt, control, shift, system)
{
Key = key;
IsRepeat = repeat;
ScanCode = scanCode;
RawCode = rawCode;
}
}

View File

@@ -306,7 +306,8 @@ namespace Robust.Client.Input
args.Key,
args.ScanCode,
action,
(Vector2i) (mousePos ?? Vector2.Zero));
(Vector2i) (mousePos ?? Vector2.Zero),
args.RawCode);
var block = rawInput.RawKeyEvent(keyEvent);
return block;

View File

@@ -0,0 +1,48 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
namespace Robust.Client.Network.Transfer;
internal sealed class ClientTransferImplWebSocket : TransferImplWebSocket
{
private readonly (string EndpointUrl, byte[] Key) _info;
private readonly bool _slow;
public ClientTransferImplWebSocket(
(string EndpointUrl, byte[] Key) info,
ISawmill sawmill,
BaseTransferManager parent,
INetChannel channel,
bool slow)
: base(sawmill, parent, channel)
{
_info = info;
_slow = slow;
}
public override async Task ClientInit(CancellationToken cancel)
{
var clientWs = new ClientWebSocket();
clientWs.Options.SetRequestHeader(KeyHeaderName, Convert.ToBase64String(_info.Key));
clientWs.Options.SetRequestHeader(UserIdHeaderName, Channel.UserId.ToString());
if (_slow)
await Task.Delay(2000, cancel);
await clientWs.ConnectAsync(new Uri(_info.EndpointUrl), cancel);
WebSocket = clientWs;
ReadThread();
}
public override Task ServerInit()
{
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages.Transfer;
using Robust.Shared.Network.Transfer;
namespace Robust.Client.Network.Transfer;
internal sealed class ClientTransferManager : BaseTransferManager, ITransferManager
{
private readonly IClientNetManager _netManager;
private readonly IConfigurationManager _cfg;
private BaseTransferImpl? _transferImpl;
public event Action? ClientHandshakeComplete;
internal ClientTransferManager(
IClientNetManager netManager,
ILogManager logManager,
ITaskManager taskManager,
IConfigurationManager cfg)
: base(logManager, NetMessageAccept.Client, taskManager)
{
_netManager = netManager;
_cfg = cfg;
}
public Stream StartTransfer(INetChannel channel, TransferStartInfo startInfo)
{
if (_transferImpl == null)
throw new InvalidOperationException("Not connected yet!");
if (channel != _netManager.ServerChannel)
throw new InvalidOperationException("Invalid channel!");
return _transferImpl.StartTransfer(startInfo);
}
public void Initialize()
{
_netManager.RegisterNetMessage<MsgTransferInit>(RxTransferInit, NetMessageAccept.Client | NetMessageAccept.Handshake);
_netManager.RegisterNetMessage<MsgTransferAckInit>();
_netManager.RegisterNetMessage<MsgTransferData>(RxTransferData, NetMessageAccept.Client | NetMessageAccept.Handshake);
}
private async void RxTransferInit(MsgTransferInit message)
{
BaseTransferImpl impl;
if (message.HttpInfo is { } httpInfo)
{
impl = new ClientTransferImplWebSocket(
httpInfo,
Sawmill,
this,
message.MsgChannel,
_cfg.GetCVar(CVars.TransferArtificialDelay));
}
else
{
impl = new TransferImplLidgren(Sawmill, message.MsgChannel, this, _netManager);
}
_transferImpl = impl;
await _transferImpl.ClientInit(default);
ClientHandshakeComplete?.Invoke();
}
private void RxTransferData(MsgTransferData message)
{
if (_transferImpl is not TransferImplLidgren lidgren)
{
message.MsgChannel.Disconnect("Not lidgren");
return;
}
lidgren.ReceiveData(message);
}
public Task ServerHandshake(INetChannel channel)
{
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
namespace Robust.Client.Network.Transfer;
internal sealed class ClientTransferTestManager(ITransferManager manager, ILogManager logManager)
: TransferTestManager(manager, logManager)
{
protected override bool PermissionCheck(INetChannel channel)
{
return true;
}
}

View File

@@ -13,9 +13,9 @@ namespace Robust.Client.Prototypes
public sealed class ClientPrototypeManager : PrototypeManager
{
[Dependency] private readonly INetManager _netManager = default!;
#pragma warning disable CS0414
#if TOOLS
[Dependency] private readonly IClientGameTiming _timing = default!;
#pragma warning restore CS0414
#endif
[Dependency] private readonly IGameControllerInternal _controller = default!;
[Dependency] private readonly IReloadManager _reload = default!;

View File

@@ -279,10 +279,7 @@ public sealed partial class ReplayLoadManager
var path = resUpload.RelativePath.Clean().ToRelativePath();
if (uploadedFiles.Add(path) && !_netResMan.FileExists(path))
{
_netMan.DispatchLocalNetMessage(new NetworkResourceUploadMessage
{
RelativePath = path, Data = resUpload.Data
});
_netResMan.StoreFile(path, resUpload.Data);
message.Messages.RemoveSwap(i);
break;
}

View File

@@ -21,7 +21,6 @@ public sealed partial class ReplayLoadManager : IReplayLoadManager
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly EntityManager _entMan = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientNetManager _netMan = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly ILocalizationManager _locMan = default!;

View File

@@ -4,7 +4,6 @@ using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
@@ -14,7 +13,7 @@ public sealed class UploadFileCommand : IConsoleCommand
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IFileDialogManager _dialog = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly NetworkResourceManager _netRes = default!;
public string Command => "uploadfile";
public string Description => "Uploads a resource to the server.";
@@ -55,12 +54,6 @@ public sealed class UploadFileCommand : IConsoleCommand
var data = file.CopyToArray();
var msg = new NetworkResourceUploadMessage
{
RelativePath = path,
Data = data
};
_netManager.ClientSendMessage(msg);
_netRes.UploadResources([(path, data)]);
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using Robust.Shared;
using Robust.Shared.Configuration;
@@ -6,7 +6,6 @@ using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
@@ -14,9 +13,9 @@ namespace Robust.Client.Upload.Commands;
public sealed class UploadFolderCommand : IConsoleCommand
{
[Dependency] private IResourceManager _resourceManager = default!;
[Dependency] private IConfigurationManager _configManager = default!;
[Dependency] private INetManager _netMan = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly NetworkResourceManager _netRes = default!;
public string Command => "uploadfolder";
public string Description => Loc.GetString("uploadfolder-command-description");
@@ -50,6 +49,7 @@ public sealed class UploadFolderCommand : IConsoleCommand
}
//Grab all files in specified folder and upload them
var files = new List<(ResPath Relative, byte[] Data)>();
foreach (var filepath in _resourceManager.UserData.Find($"{folderPath.ToRelativePath()}/").files )
{
await using var filestream = _resourceManager.UserData.Open(filepath, FileMode.Open);
@@ -63,17 +63,14 @@ public sealed class UploadFolderCommand : IConsoleCommand
var data = filestream.CopyToArray();
var msg = new NetworkResourceUploadMessage
{
RelativePath = filepath.RelativeTo(BaseUploadFolderPath),
Data = data
};
files.Add((filepath.RelativeTo(BaseUploadFolderPath), data));
_netMan.ClientSendMessage(msg);
fileCount++;
}
}
_netRes.UploadResources(files);
shell.WriteLine( Loc.GetString("uploadfolder-command-success",("fileCount",fileCount)));
}
}

View File

@@ -1,5 +1,11 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
namespace Robust.Client.Upload;
@@ -7,10 +13,44 @@ public sealed class NetworkResourceManager : SharedNetworkResourceManager
{
[Dependency] private readonly IBaseClient _client = default!;
public override void Initialize()
internal override void Initialize()
{
base.Initialize();
_client.RunLevelChanged += OnLevelChanged;
TransferManager.RegisterTransferMessage(TransferKeyNetworkUpload);
TransferManager.RegisterTransferMessage(TransferKeyNetworkDownload, ReceiveDownload);
NetManager.RegisterNetMessage<NetworkResourceAckMessage>();
}
private async void ReceiveDownload(TransferReceivedEvent transfer)
{
Sawmill.Debug("Receiving file download transfer!");
await using var stream = transfer.DataStream;
try
{
var ackKeyBytes = new byte[4];
await stream.ReadExactlyAsync(ackKeyBytes);
var ackKey = BinaryPrimitives.ReadInt32LittleEndian(ackKeyBytes);
await IngestFileStream(stream);
if (ackKey != 0)
{
NetManager.ClientSendMessage(new NetworkResourceAckMessage
{
Key = ackKey
});
}
}
catch (Exception e)
{
Sawmill.Error($"Error while downloading transfer resources: {e}");
}
}
private void OnLevelChanged(object? sender, RunLevelChangedEventArgs e)
@@ -27,4 +67,20 @@ public sealed class NetworkResourceManager : SharedNetworkResourceManager
{
ContentRoot.Clear();
}
internal async void UploadResources(List<(ResPath Relative, byte[] Data)> files)
{
var clientNet = (IClientNetManager)NetManager;
if (clientNet.ServerChannel is not { } channel)
throw new InvalidOperationException("Not connected to server!");
await using var transfer = TransferManager.StartTransfer(
channel,
new TransferStartInfo
{
MessageKey = TransferKeyNetworkUpload,
});
await WriteFileStream(transfer, files);
}
}

View File

@@ -12,6 +12,7 @@ namespace Robust.Client.UserInterface
{
private Dictionary<string, AnimationPlayback>? _playingAnimations;
public Action<string>? AnimationStarted;
public Action<string>? AnimationCompleted;
/// <summary>
@@ -27,6 +28,7 @@ namespace Robust.Client.UserInterface
_playingAnimations ??= new Dictionary<string, AnimationPlayback>();
_playingAnimations.Add(key, playback);
AnimationStarted?.Invoke(key);
}
public bool HasRunningAnimation(string key)

View File

@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Animations;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
@@ -355,6 +356,7 @@ namespace Robust.Client.UserInterface
/// </remarks>
/// <seealso cref="MinWidth"/>
/// <seealso cref="MinHeight"/>
[Animatable]
public Vector2 MinSize
{
get => new(_minWidth, _minHeight);
@@ -378,6 +380,7 @@ namespace Robust.Client.UserInterface
/// </remarks>
/// <seealso cref="SetWidth"/>
/// <seealso cref="SetHeight"/>
[Animatable]
public Vector2 SetSize
{
get => new(_setWidth, _setHeight);
@@ -434,6 +437,7 @@ namespace Robust.Client.UserInterface
/// Width component of <see cref="SetSize"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public float SetWidth
{
get => _setWidth;
@@ -449,6 +453,7 @@ namespace Robust.Client.UserInterface
/// Height component of <see cref="SetSize"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public float SetHeight
{
get => _setHeight;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
@@ -44,6 +44,7 @@ namespace Robust.Client.UserInterface.Controls
/// Thrown if <see cref="TextMemory"/> was set directly and there is no backing string instance to fetch.
/// </exception>
[ViewVariables]
[Animatable]
public string? Text
{
get => _text ?? (_textMemory.Length > 0 ? throw new InvalidOperationException("Label uses TextMemory, cannot fetch string text.") : null);

View File

@@ -144,7 +144,7 @@ namespace Robust.Client.UserInterface.Controls
SetPositionFirst();
// Resize the window by our UIScale
ClydeWindow.Size = new((int)(ClydeWindow.Size.X * UIScale), (int)(ClydeWindow.Size.Y * UIScale));
ClydeWindow.Size = new((int)(parameters.Width * UIScale), (int)(parameters.Height * UIScale));
return ClydeWindow;
}

View File

@@ -136,9 +136,14 @@ namespace Robust.Client.UserInterface.Controls
AddMessage(msg);
}
public void AddMessage(FormattedMessage message)
public void AddMessage(FormattedMessage message, Color? defaultColor = null)
{
var entry = new RichTextEntry(message, this, _tagManager);
AddMessage(message, RichTextEntry.DefaultTags, defaultColor);
}
public void AddMessage(FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null)
{
var entry = new RichTextEntry(message, this, _tagManager, tagsAllowed, defaultColor);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);

View File

@@ -39,6 +39,20 @@ namespace Robust.Client.UserInterface.Controls
}
}
/// <summary>
/// Gets or sets the markup string displayed by this control.
/// </summary>
/// <remarks>
/// <para>
/// This method converts the given string with <see cref="FormattedMessage.FromMarkupPermissive(string)"/>.
/// The original markup string is not kept,
/// so setting and then getting the function may provide a different result.
/// </para>
/// <para>
/// Unlike <see cref="M:SetMessage(FormattedMessage,Color?)"/>,
/// no tag whitelist will be set on the rendered message. Do not pass untrusted user input to this!
/// </para>
/// </remarks>
public string? Text
{
get => _entry?.Message.ToMarkup();
@@ -47,7 +61,7 @@ namespace Robust.Client.UserInterface.Controls
if (value == null)
Clear();
else
SetMessage(FormattedMessage.FromMarkupPermissive(value));
SetMessage(FormattedMessage.FromMarkupPermissive(value), tagsAllowed: null);
}
}
@@ -67,11 +81,32 @@ namespace Robust.Client.UserInterface.Controls
VerticalAlignment = VAlignment.Center;
}
/// <summary>
/// Sets the formatted message displayed by this control.
/// </summary>
/// <param name="message">The message to display.</param>
/// <param name="defaultColor">If provided, the default color to use for this message rendering.</param>
/// <remarks>
/// This method sets the set of allowed tags to only include a small amount of safe formatting tags.
/// Use <see cref="M:SetMessage(FormattedMessage,Type[],Color?)"/> if this is not desired.
/// </remarks>
public void SetMessage(FormattedMessage message, Color? defaultColor = null)
{
SetMessage(message, RichTextEntry.DefaultTags, defaultColor);
}
/// <summary>
/// Sets the formatted message displayed by this control.
/// </summary>
/// <param name="message">The message to display.</param>
/// <param name="tagsAllowed">
/// The set of allowed markup tags that will be displayed.
/// If <c>null</c>, all tags are allowed.</param>
/// <param name="defaultColor">If provided, the default color to use for this message rendering.</param>
/// <remarks>
/// This method sets the set of allowed tags to only include a small amount of safe formatting tags.
/// Use <see cref="M:SetMessage(FormattedMessage,Type[],Color?)"/> if this is not desired.
/// </remarks>
public void SetMessage(FormattedMessage message, Type[]? tagsAllowed, Color? defaultColor = null)
{
_entry?.RemoveControls();

View File

@@ -170,6 +170,7 @@ namespace Robust.Client.UserInterface.Controls
Entity = new(uid.Value, sprite, xform);
NetEnt = EntMan.GetNetEntity(uid);
InvalidateMeasure();
}
protected override Vector2 MeasureOverride(Vector2 availableSize)

View File

@@ -22,8 +22,10 @@ namespace Robust.Client.UserInterface.Controls
private int _currentTab;
private bool _tabsVisible = true;
// The right-most coordinate of each tab header
private List<float> _tabRight = new();
// the laid out tabs
private List<TabBox> _tabBoxes = new();
private float _enclosingTabHeight;
public int CurrentTab
{
@@ -146,41 +148,66 @@ namespace Robust.Client.UserInterface.Controls
base.Draw(handle);
// First, draw panel.
var headerSize = _getHeaderSize();
var headerSize = _enclosingTabHeight;
var panel = _getPanel();
var panelBox = new UIBox2(0, headerSize, PixelWidth, PixelHeight);
panel?.Draw(handle, panelBox, UIScale);
var font = _getFont();
var boxActive = _getTabBoxActive();
var boxInactive = _getTabBoxInactive();
var fontColorActive = _getTabFontColorActive();
var fontColorInactive = _getTabFontColorInactive();
var headerOffset = 0f;
// Then draw the tabs
foreach (var tabBox in _tabBoxes)
{
if (tabBox.Box is { } styleBox)
{
styleBox.Draw(handle, tabBox.Bounding, UIScale);
}
_tabRight.Clear();
var baseLine = new Vector2(0, font.GetAscent(UIScale)) + tabBox.Content.TopLeft;
foreach (var rune in tabBox.Title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
continue;
font.DrawChar(handle, rune, baseLine, UIScale, tabBox.Index == _currentTab ? fontColorActive : fontColorInactive);
baseLine += new Vector2(metrics.Advance, 0);
}
}
}
private readonly record struct TabBox(UIBox2 Bounding, UIBox2 Content, StyleBox? Box, string Title, int Index);
private void CalculateTabBoxes(Vector2 availableSize)
{
availableSize *= UIScale;
var tabLeft = 0f;
var tabTop = 0f;
var tabHeight = 0f;
var font = _getFont();
var boxActive = _getTabBoxActive();
var boxInactive = _getTabBoxInactive();
_tabBoxes.Clear();
if (!_tabsVisible)
return;
// Then, draw the tabs.
for (var i = 0; i < ChildCount; i++)
{
if (!GetTabVisible(i))
{
_tabRight.Add(headerOffset);
continue;
}
var title = GetActualTabTitle(i);
var titleLength = 0;
// Get string length.
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
titleLength += metrics.Advance;
}
@@ -188,50 +215,57 @@ namespace Robust.Client.UserInterface.Controls
var active = _currentTab == i;
var box = active ? boxActive : boxInactive;
UIBox2 contentBox;
var topLeft = new Vector2(headerOffset, 0);
var topLeft = new Vector2(tabLeft, tabTop);
var size = new Vector2(titleLength, font.GetHeight(UIScale));
float boxAdvance;
if (box != null)
{
var drawBox = box.GetEnvelopBox(topLeft, size, UIScale);
boxAdvance = drawBox.Width;
box.Draw(handle, drawBox, UIScale);
contentBox = box.GetContentBox(drawBox, UIScale);
size = box.GetEnvelopBox(topLeft, size, UIScale).Size;
}
if (tabLeft + size.X > availableSize.X)
{
tabLeft = 0;
tabTop += tabHeight;
tabHeight = 0;
}
topLeft = new(tabLeft, tabTop);
size = new(titleLength, font.GetHeight(UIScale));
UIBox2 boundingBox;
UIBox2 contentBox;
if (box != null)
{
boundingBox = box.GetEnvelopBox(topLeft, size, UIScale);
contentBox = box.GetContentBox(boundingBox, UIScale);
}
else
{
boxAdvance = size.X;
contentBox = UIBox2.FromDimensions(topLeft, size);
boundingBox = contentBox;
}
var baseLine = new Vector2(0, font.GetAscent(UIScale)) + contentBox.TopLeft;
foreach (var rune in title.EnumerateRunes())
{
if (!font.TryGetCharMetrics(rune, UIScale, out var metrics))
{
continue;
}
font.DrawChar(handle, rune, baseLine, UIScale, active ? fontColorActive : fontColorInactive);
baseLine += new Vector2(metrics.Advance, 0);
}
headerOffset += boxAdvance;
// Remember the right-most point of this tab, for testing clicked areas
_tabRight.Add(headerOffset);
tabLeft += boundingBox.Size.X;
tabHeight = Math.Max(tabHeight, boundingBox.Size.Y);
_tabBoxes.Add(new(boundingBox, contentBox, box, title, i));
}
if (Math.Abs(_enclosingTabHeight - (tabTop + tabHeight)) >= 0.1)
{
InvalidateArrange();
}
_enclosingTabHeight = tabTop + tabHeight;
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
CalculateTabBoxes(availableSize);
var headerSize = Vector2.Zero;
if (TabsVisible)
{
headerSize = new Vector2(0, _getHeaderSize() / UIScale);
headerSize = new Vector2(0, _enclosingTabHeight / UIScale);
}
var panel = _getPanel();
@@ -254,12 +288,13 @@ namespace Robust.Client.UserInterface.Controls
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
CalculateTabBoxes(finalSize);
if (ChildCount == 0 || _currentTab >= ChildCount)
{
return finalSize;
}
var headerSize = _getHeaderSize();
var headerSize = (int)_enclosingTabHeight;
var panel = _getPanel();
var contentBox = new UIBox2i(0, headerSize, (int) (finalSize.X * UIScale), (int) (finalSize.Y * UIScale));
if (panel != null)
@@ -283,50 +318,23 @@ namespace Robust.Client.UserInterface.Controls
}
// Outside of header size, ignore.
if (args.RelativePixelPosition.Y < 0 || args.RelativePixelPosition.Y > _getHeaderSize())
if (args.RelativePixelPosition.Y < 0 || args.RelativePixelPosition.Y > _enclosingTabHeight)
{
return;
}
args.Handle();
var relX = args.RelativePixelPosition.X;
float tabLeft = 0;
for (var i = 0; i < ChildCount; i++)
foreach (var box in _tabBoxes)
{
if (relX > tabLeft && relX <= _tabRight[i])
if (box.Bounding.Contains(args.RelativePixelPosition))
{
CurrentTab = i;
CurrentTab = box.Index;
return;
}
// Next tab starts here
tabLeft = _tabRight[i];
}
}
// Returns the size of the header, in real pixels
[System.Diagnostics.Contracts.Pure]
private int _getHeaderSize()
{
var headerSize = 0;
if (TabsVisible)
{
var active = _getTabBoxActive();
var inactive = _getTabBoxInactive();
var font = _getFont();
var activeSize = (active?.MinimumSize ?? Vector2.Zero) * UIScale;
var inactiveSize = (inactive?.MinimumSize ?? Vector2.Zero) * UIScale;
headerSize = (int) MathF.Max(activeSize.Y, inactiveSize.Y);
headerSize += font.GetHeight(UIScale);
}
return headerSize;
}
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getTabBoxActive()
{

View File

@@ -35,13 +35,15 @@ namespace Robust.Client.UserInterface
public readonly int ScanCode;
public readonly RawKeyAction Action;
public readonly Vector2i MouseRelative;
public readonly ushort RawCode;
public GuiRawKeyEvent(Keyboard.Key key, int scanCode, RawKeyAction action, Vector2i mouseRelative)
public GuiRawKeyEvent(Keyboard.Key key, int scanCode, RawKeyAction action, Vector2i mouseRelative, ushort rawCode)
{
Key = key;
ScanCode = scanCode;
Action = action;
MouseRelative = mouseRelative;
RawCode = rawCode;
}
}

View File

@@ -14,15 +14,6 @@ namespace Robust.Client.Utility
{
NativeLibrary.SetDllImportResolver(typeof(ClientDllMap).Assembly, (name, assembly, path) =>
{
if (name == "swnfd.dll")
{
#if LINUX || FREEBSD
return NativeLibrary.Load("libswnfd.so", assembly, path);
#elif MACOS
return NativeLibrary.Load("libswnfd.dylib", assembly, path);
#endif
}
if (name == "libEGL.dll")
{
#if LINUX || FREEBSD

View File

@@ -20,9 +20,9 @@ internal sealed class ReloadManager : IReloadManager
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IResourceManagerInternal _res = default!;
#pragma warning disable CS0414
#if TOOLS
[Dependency] private readonly ITaskManager _tasks = default!;
#pragma warning restore CS0414
#endif
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
private CancellationTokenSource _reloadToken = new();

View File

@@ -56,7 +56,7 @@ internal sealed class ViewVariableControlFactory : IViewVariableControlFactory
RegisterForType<TimeSpan>(_ => new VVPropEditorTimeSpan());
RegisterWithCondition(
type => type != typeof(ViewVariablesBlobMembers.ServerValueTypeToken) && !type.IsValueType,
type => type != typeof(ViewVariablesBlobMembers.ServerValueTypeToken),
_ => new VVPropEditorReference()
);
RegisterWithCondition(

View File

@@ -9,11 +9,13 @@ using Robust.Server.Debugging;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Localization;
using Robust.Server.Network.Transfer;
using Robust.Server.Physics;
using Robust.Server.Player;
using Robust.Server.Prototypes;
using Robust.Server.Reflection;
using Robust.Server.Replays;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
@@ -28,6 +30,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Components;
@@ -198,6 +201,9 @@ namespace Robust.UnitTesting.Server
container.Register<HttpClientHolder>();
container.Register<IHttpClientHolder, HttpClientHolder>();
container.Register<IHWId, DummyHWId>();
container.Register<IServerNetManager, NetManager>();
container.Register<IStatusHost, StatusHost>();
container.Register<ITransferManager, ServerTransferManager>();
var realReflection = new ServerReflectionManager();
realReflection.LoadAssemblies(new List<Assembly>(2)
@@ -262,7 +268,6 @@ namespace Robust.UnitTesting.Server
// I just wanted to load pvs system
container.Register<IServerEntityManager, ServerEntityManager>();
container.Register<IServerNetManager, NetManager>();
// god help you if you actually need to test pvs functions
container.RegisterInstance<IPlayerManager>(new Mock<IPlayerManager>().Object);
container.RegisterInstance<ISharedPlayerManager>(new Mock<ISharedPlayerManager>().Object);

View File

@@ -9,6 +9,7 @@ using Robust.Server.DataMetrics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Log;
using Robust.Server.Network.Transfer;
using Robust.Server.Placement;
using Robust.Server.Player;
using Robust.Server.Scripting;
@@ -29,6 +30,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
@@ -107,6 +109,8 @@ namespace Robust.Server
[Dependency] private readonly UploadedContentManager _uploadedContMan = default!;
[Dependency] private readonly NetworkResourceManager _netResMan = default!;
[Dependency] private readonly IReflectionManager _refMan = default!;
[Dependency] private readonly ITransferManager _transfer = default!;
[Dependency] private readonly ServerTransferTestManager _transferTest = default!;
private readonly Stopwatch _uptimeStopwatch = new();
@@ -275,6 +279,7 @@ namespace Robust.Server
// Load metrics really early so that we can profile startup times in the future maybe.
_metricsManager.Initialize();
_prof.Initialize();
try
{
@@ -293,6 +298,9 @@ namespace Robust.Server
return true;
}
_transfer.Initialize();
_transferTest.Initialize();
var dataDir = Options.LoadConfigAndUserData
? _commandLineArgs?.DataDir ?? PathHelpers.ExecutableRelativeFile("data")
: null;
@@ -303,8 +311,14 @@ namespace Robust.Server
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
var startType = ContentStart ? StartType.Content : StartType.Engine;
#if FULL_RELEASE
if (Options.ResourceMountDisabled)
startType = StartType.Loader;
#endif
ProgramShared.DoMounts(_resources, mountOptions, Options.ContentBuildDirectory, Options.AssemblyDirectory,
Options.LoadContentResources, Options.ResourceMountDisabled, ContentStart);
Options.LoadContentResources, startType);
// When the game is ran with the startup executable being content,
// we have to disable the separate load context.
@@ -767,6 +781,8 @@ namespace Robust.Server
_modLoader.BroadcastUpdate(ModUpdateLevel.FramePostEngine, frameEventArgs);
_transfer.FrameUpdate();
_metricsManager.FrameUpdate();
}

View File

@@ -9,6 +9,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Toolshed;
using Robust.Shared.Utility;
@@ -22,6 +23,7 @@ namespace Robust.Server.Console
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly ISystemConsoleManager _systemConsole = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
[Dependency] private readonly ProfManager _prof = default!;
public ServerConsoleHost() : base(isServer: true) {}
@@ -108,7 +110,8 @@ namespace Robust.Server.Console
if (args.Count == 0)
return;
string? cmdName = args[0];
var cmdName = args[0];
using var _ = _prof.Group(cmdName);
if (RegisteredCommands.TryGetValue(cmdName, out var conCmd)) // command registered
{

View File

@@ -0,0 +1,122 @@
using System;
using System.Net;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages.Transfer;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Utility;
namespace Robust.Server.Network.Transfer;
internal sealed class ServerTransferImplWebSocket : TransferImplWebSocket
{
private readonly IConfigurationManager _cfg;
private readonly INetManager _netManager;
private readonly SemaphoreSlim _apiLock = new(1, 1);
private readonly TaskCompletionSource _connectTcs = new();
// To authenticate the client doing the HTTP request,
// we ask that they provide a key we gave them via Lidgren traffic.
public byte[]? Key;
public ServerTransferImplWebSocket(
ISawmill sawmill,
BaseTransferManager parent,
IConfigurationManager cfg,
INetManager netManager,
INetChannel channel)
: base(sawmill, parent, channel)
{
_cfg = cfg;
_netManager = netManager;
}
public override Task ServerInit()
{
Key = RandomNumberGenerator.GetBytes(RandomKeyBytes);
var uriBuilder = new UriBuilder(string.Concat(
_cfg.GetCVar(CVars.TransferHttpEndpoint).TrimEnd("/"),
ServerTransferManager.TransferApiUrl));
uriBuilder.Scheme = uriBuilder.Scheme switch
{
"http" => "ws",
"https" => "wss",
_ => throw new InvalidOperationException($"Invalid API endpoint scheme: {uriBuilder.Scheme}")
};
var url = uriBuilder.ToString();
Sawmill.Verbose($"Transfer API URL is '{url}'");
var initMsg = new MsgTransferInit();
initMsg.HttpInfo = (url, Key);
_netManager.ServerSendMessage(initMsg, Channel);
return _connectTcs.Task;
}
public override Task ClientInit(CancellationToken cancel)
{
throw new NotSupportedException();
}
public async Task HandleApiRequest(NetUserId userId, IStatusHandlerContext context)
{
using var _ = await _apiLock.WaitGuardAsync();
if (Key == null)
{
Sawmill.Warning($"HTTP request failed: UserID '{userId}' tried to connect twice");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return;
}
if (!context.RequestHeaders.TryGetValue(KeyHeaderName, out var keyValue) || keyValue is not [{ } keyValueStr])
{
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return;
}
var buf = new byte[RandomKeyBytes];
if (!Convert.TryFromBase64String(keyValueStr, buf, out var written) || written != RandomKeyBytes)
{
Sawmill.Verbose("HTTP request failed: key is not valid base64 or wrong length");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return;
}
if (!CryptographicOperations.FixedTimeEquals(buf, Key))
{
Sawmill.Warning("HTTP request failed: key is wrong");
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
return;
}
Sawmill.Debug("Client connect to transfer WS channel: {UserId}", userId);
WebSocket = await context.AcceptWebSocketAsync();
// We've connected.
// Clear key so this can't be reconnected to.
Key = null;
_connectTcs.TrySetResult();
ReadThread();
}
public override void Dispose()
{
_connectTcs.TrySetCanceled();
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Robust.Server.ServerStatus;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages.Transfer;
using Robust.Shared.Network.Transfer;
namespace Robust.Server.Network.Transfer;
internal sealed class ServerTransferManager : BaseTransferManager, ITransferManager
{
internal const string TransferApiUrl = "/rt_transfer_init";
private readonly IConfigurationManager _cfg;
private readonly IStatusHost _statusHost;
private readonly IServerNetManager _netManager;
private readonly Dictionary<NetUserId, Player> _onlinePlayers = new();
internal ServerTransferManager(IConfigurationManager cfg, IStatusHost statusHost, IServerNetManager netManager, ILogManager logManager, ITaskManager taskManager)
: base(logManager, NetMessageAccept.Server, taskManager)
{
_cfg = cfg;
_statusHost = statusHost;
_netManager = netManager;
}
public void Initialize()
{
_netManager.RegisterNetMessage<MsgTransferInit>();
_netManager.RegisterNetMessage<MsgTransferData>(RxTransferData, NetMessageAccept.Server | NetMessageAccept.Handshake);
_netManager.RegisterNetMessage<MsgTransferAckInit>(RxTransferAckInit, NetMessageAccept.Server | NetMessageAccept.Handshake);
_statusHost.AddHandler(HandleRequest);
_netManager.Disconnect += NetManagerOnDisconnect;
}
private void RxTransferData(MsgTransferData message)
{
if (!_onlinePlayers.TryGetValue(message.MsgChannel.UserId, out var player)
|| player.Impl is not TransferImplLidgren lidgren)
{
message.MsgChannel.Disconnect("Not lidgren");
return;
}
lidgren.ReceiveData(message);
}
private void RxTransferAckInit(MsgTransferAckInit message)
{
if (!_onlinePlayers.TryGetValue(message.MsgChannel.UserId, out var player)
|| player.Impl is not TransferImplLidgren lidgren)
{
message.MsgChannel.Disconnect("Not lidgren");
return;
}
lidgren.ReceiveInitAck();
}
public Stream StartTransfer(INetChannel channel, TransferStartInfo startInfo)
{
if (!_onlinePlayers.TryGetValue(channel.UserId, out var player))
throw new InvalidOperationException("Player is not connected yet!");
return player.Impl.StartTransfer(startInfo);
}
private async Task<bool> HandleRequest(IStatusHandlerContext context)
{
if (context.Url.AbsolutePath != TransferApiUrl)
return false;
if (!context.IsWebSocketRequest)
{
Sawmill.Verbose("HTTP request failed: not a websocket request");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return true;
}
if (!context.RequestHeaders.TryGetValue(TransferImplWebSocket.UserIdHeaderName, out var userIdValue)
|| userIdValue.Count != 1)
{
Sawmill.Verbose("HTTP request failed: missing RT-UserId");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return true;
}
if (!Guid.TryParse(userIdValue[0], out var userId))
{
Sawmill.Verbose($"HTTP request failed: UserID '{userId}' invalid");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return true;
}
if (!_onlinePlayers.TryGetValue(new NetUserId(userId), out var player))
{
Sawmill.Warning($"HTTP request failed: UserID '{userId}' not online");
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
return true;
}
if (player.Impl is not ServerTransferImplWebSocket serverWs)
{
Sawmill.Warning($"HTTP request failed: UserID '{userId}' is not using websocket transfer");
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
return true;
}
await serverWs.HandleApiRequest(new NetUserId(userId), context);
return true;
}
public async Task ServerHandshake(INetChannel channel)
{
if (_onlinePlayers.ContainsKey(channel.UserId))
throw new InvalidOperationException("We already have data for this user??");
var transferHttpEnabled = _cfg.GetCVar(CVars.TransferHttp);
BaseTransferImpl impl;
if (transferHttpEnabled)
{
impl = new ServerTransferImplWebSocket(Sawmill, this, _cfg, _netManager, channel);
}
else
{
impl = new TransferImplLidgren(Sawmill, channel, this, _netManager);
}
impl.MaxChannelCount = _cfg.GetCVar(CVars.TransferStreamLimit);
var datum = new Player
{
Impl = impl,
};
_onlinePlayers.Add(channel.UserId, datum);
await impl.ServerInit();
}
public event Action ClientHandshakeComplete
{
add { }
remove { }
}
private void NetManagerOnDisconnect(object? sender, NetDisconnectedArgs e)
{
if (!_onlinePlayers.Remove(e.Channel.UserId, out var player))
return;
Sawmill.Debug("Cleaning up connection for channel {Player} that disconnected", e.Channel);
player.Impl.Dispose();
}
private sealed class Player
{
public required BaseTransferImpl Impl;
}
}

View File

@@ -0,0 +1,23 @@
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
namespace Robust.Server.Network.Transfer;
internal sealed class ServerTransferTestManager(
ITransferManager manager,
ILogManager logManager,
IConGroupController controller,
IPlayerManager playerManager)
: TransferTestManager(manager, logManager)
{
protected override bool PermissionCheck(INetChannel channel)
{
if (!playerManager.TryGetSessionByChannel(channel, out var session))
return false;
return controller.CanCommand(session, TransferTestCommand.CommandKey);
}
}

View File

@@ -217,6 +217,10 @@ namespace Robust.Server.Placement
}
}
/// <summary>
/// Deletes any existing entity.
/// </summary>
/// <param name="msg"></param>
private void HandleEntRemoveReq(MsgPlacement msg)
{
//TODO: Some form of admin check
@@ -225,26 +229,61 @@ namespace Robust.Server.Placement
if (!_entityManager.EntityExists(entity))
return;
var placementEraseEvent = new PlacementEntityEvent(entity, _entityManager.GetComponent<TransformComponent>(entity).Coordinates, PlacementEventAction.Erase, msg.MsgChannel.UserId);
var placementEraseEvent = new PlacementEntityEvent(entity,
_entityManager.GetComponent<TransformComponent>(entity).Coordinates,
PlacementEventAction.Erase,
msg.MsgChannel.UserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
_entityManager.DeleteEntity(entity);
}
/// <summary>
/// Deletes almost any existing entity within a selection box.
/// </summary>
/// <param name="msg"></param>
private void HandleRectRemoveReq(MsgPlacement msg)
{
EntityCoordinates start = _entityManager.GetCoordinates(msg.NetCoordinates);
Vector2 rectSize = msg.RectSize;
foreach (var entity in _lookup.GetEntitiesIntersecting(_xformSystem.GetMapId(start),
new Box2(start.Position, start.Position + rectSize)))
var start = _entityManager.GetCoordinates(msg.NetCoordinates);
var rectSize = msg.RectSize;
foreach (var entity in _lookup.GetEntitiesIntersecting(_xformSystem.GetMapId(start), new Box2(start.Position, start.Position + rectSize)))
{
if (_entityManager.Deleted(entity) ||
_entityManager.HasComponent<MapGridComponent>(entity) ||
_entityManager.HasComponent<ActorComponent>(entity))
{
if (_entityManager.Deleted(entity)
|| _entityManager.HasComponent<MapGridComponent>(entity)
|| _entityManager.HasComponent<ActorComponent>(entity))
continue;
var xform = _entityManager.GetComponent<TransformComponent>(entity);
var parent = xform.ParentUid;
var isChildOfActor = false;
while (parent.IsValid())
{
if (_entityManager.HasComponent<ActorComponent>(parent))
{
isChildOfActor = true;
break;
}
if (_entityManager.TryGetComponent<TransformComponent>(parent, out var parentXform))
{
parent = parentXform.ParentUid;
}
else
{
break;
}
}
var placementEraseEvent = new PlacementEntityEvent(entity, _entityManager.GetComponent<TransformComponent>(entity).Coordinates, PlacementEventAction.Erase, msg.MsgChannel.UserId);
if (isChildOfActor)
continue;
var placementEraseEvent = new PlacementEntityEvent(entity,
_entityManager.GetComponent<TransformComponent>(entity).Coordinates,
PlacementEventAction.Erase,
msg.MsgChannel.UserId);
_entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent);
_entityManager.DeleteEntity(entity);
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Input;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Robust.Server.Player;
@@ -10,4 +11,6 @@ namespace Robust.Server.Player;
public interface IPlayerManager : ISharedPlayerManager
{
BoundKeyMap KeyMap { get; }
internal void MarkPlayerResourcesSent(INetChannel channel);
}

View File

@@ -120,13 +120,34 @@ namespace Robust.Server.Player
private void HandlePlayerListReq(MsgPlayerListReq message)
{
var channel = message.MsgChannel;
var session = (CommonSession) GetSessionByChannel(channel);
session.InitialPlayerListReqDone = true;
if (!session.InitialResourcesDone)
return;
SendPlayerList(channel, session);
}
public void MarkPlayerResourcesSent(INetChannel channel)
{
var session = (CommonSession) GetSessionByChannel(channel);
session.InitialResourcesDone = true;
if (!session.InitialPlayerListReqDone)
return;
SendPlayerList(channel, session);
}
private void SendPlayerList(INetChannel channel, CommonSession session)
{
var players = Sessions;
var netMsg = new MsgPlayerList();
// client session is complete, set their status accordingly.
// This is done before the packet is built, so that the client
// can see themselves Connected.
var session = GetSessionByChannel(channel);
session.ConnectedTime = DateTime.UtcNow;
SetStatus(session, SessionStatus.Connected);

View File

@@ -13,10 +13,10 @@ namespace Robust.Server.Prototypes
{
public sealed class ServerPrototypeManager : PrototypeManager
{
#pragma warning disable CS0414
#if TOOLS
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConGroupController _conGroups = default!;
#pragma warning restore CS0414
#endif
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IBaseServerInternal _server = default!;

View File

@@ -5,6 +5,7 @@ using Robust.Server.DataMetrics;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Localization;
using Robust.Server.Network.Transfer;
using Robust.Server.Placement;
using Robust.Server.Player;
using Robust.Server.Prototypes;
@@ -25,6 +26,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
@@ -102,6 +104,8 @@ namespace Robust.Server
deps.Register<IHWId, DummyHWId>();
deps.Register<ILocalizationManager, ServerLocalizationManager>();
deps.Register<ILocalizationManagerInternal, ServerLocalizationManager>();
deps.Register<ITransferManager, ServerTransferManager>();
deps.Register<ServerTransferTestManager>();
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
@@ -25,6 +26,8 @@ namespace Robust.Server.ServerStatus
IDictionary<string, string> ResponseHeaders { get; }
bool KeepAlive { get; set; }
bool IsWebSocketRequest { get; }
Task<T?> RequestBodyJsonAsync<T>();
Task RespondNoContentAsync();
@@ -54,5 +57,7 @@ namespace Robust.Server.ServerStatus
Task RespondJsonAsync(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
Task<Stream> RespondStreamAsync(HttpStatusCode code = HttpStatusCode.OK);
Task<WebSocket> AcceptWebSocketAsync();
}
}

View File

@@ -14,6 +14,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -242,6 +243,7 @@ namespace Robust.Server.ServerStatus
public Uri Url => _context.Request.Url!;
public bool IsGetLike => RequestMethod == HttpMethod.Head || RequestMethod == HttpMethod.Get;
public IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
public bool IsWebSocketRequest => _context.Request.IsWebSocketRequest;
public bool KeepAlive
{
@@ -353,6 +355,12 @@ namespace Robust.Server.ServerStatus
return Task.FromResult(_context.Response.OutputStream);
}
public async Task<WebSocket> AcceptWebSocketAsync()
{
var context = await _context.AcceptWebSocketAsync(null);
return context.WebSocket;
}
private void RespondShared()
{
foreach (var (header, value) in _responseHeaders)

View File

@@ -1,77 +1,153 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Player;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Server.Upload;
public sealed class NetworkResourcesUploadedEvent
{
public ICommonSession Session { get; }
public ImmutableArray<(ResPath Relative, byte[] Data)> Files { get; }
internal NetworkResourcesUploadedEvent(ICommonSession session, ImmutableArray<(ResPath, byte[])> files)
{
Session = session;
Files = files;
}
}
public sealed class NetworkResourceManager : SharedNetworkResourceManager
{
internal const int AckInitial = 1;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IServerNetManager _serverNetManager = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IConGroupController _controller = default!;
[Obsolete("Use ResourcesUploaded instead")]
public event Action<ICommonSession, NetworkResourceUploadMessage>? OnResourceUploaded;
public event Action<NetworkResourcesUploadedEvent>? ResourcesUploaded;
[ViewVariables] public bool Enabled { get; private set; } = true;
[ViewVariables] public float SizeLimit { get; private set; }
public override void Initialize()
internal event Action<INetChannel, int>? AckReceived;
internal override void Initialize()
{
base.Initialize();
TransferManager.RegisterTransferMessage(TransferKeyNetworkDownload);
TransferManager.RegisterTransferMessage(TransferKeyNetworkUpload, ReceiveUpload);
_cfgManager.OnValueChanged(CVars.ResourceUploadingEnabled, value => Enabled = value, true);
_cfgManager.OnValueChanged(CVars.ResourceUploadingLimitMb, value => SizeLimit = value, true);
_serverNetManager.RegisterNetMessage<NetworkResourceAckMessage>(RxAck);
}
/// <summary>
/// Callback for when a client attempts to upload a resource.
/// </summary>
/// <param name="msg"></param>
/// <exception cref="NotImplementedException"></exception>
protected override void ResourceUploadMsg(NetworkResourceUploadMessage msg)
private void RxAck(NetworkResourceAckMessage message)
{
AckReceived?.Invoke(message.MsgChannel, message.Key);
}
private async void ReceiveUpload(TransferReceivedEvent transfer)
{
// Do not allow uploading any new resources if it has been disabled.
// Note: Any resources uploaded before being disabled will still be kept and sent.
if (!Enabled)
{
transfer.Channel.Disconnect("Resource upload not enabled.");
return;
}
if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session))
if (!_playerManager.TryGetSessionByChannel(transfer.Channel, out var session))
{
transfer.Channel.Disconnect("Not in-game");
return;
}
if (!_controller.CanCommand(session, "uploadfile"))
{
transfer.Channel.Disconnect("Not authorized");
return;
}
// Ensure the data is under the current size limit, if it's currently enabled.
if (SizeLimit > 0f && msg.Data.Length * BytesToMegabytes > SizeLimit)
return;
Sawmill.Verbose("Ingesting file uploads from {Session}", session);
base.ResourceUploadMsg(msg);
List<(ResPath Relative, byte[] Data)> ingested;
await using (var stream = transfer.DataStream)
{
ingested = await IngestFileStream(stream);
}
Sawmill.Verbose("Ingesting file uploads complete, distributing...");
// Now we broadcast the message!
foreach (var channel in _serverNetManager.Channels)
{
channel.SendMessage(msg);
SendToPlayer(channel, ingested);
}
OnResourceUploaded?.Invoke(session, msg);
#pragma warning disable CS0618 // Type or member is obsolete
if (OnResourceUploaded != null)
{
foreach (var (relative, data) in ingested)
{
OnResourceUploaded?.Invoke(session, new NetworkResourceUploadMessage
{
MsgChannel = session.Channel,
Data = data,
RelativePath = relative
});
}
}
#pragma warning restore CS0618 // Type or member is obsolete
ResourcesUploaded?.Invoke(new NetworkResourcesUploadedEvent(session, [..ingested]));
}
internal void SendToNewUser(INetChannel channel)
protected override void ValidateUpload(uint size)
{
foreach (var (path, data) in ContentRoot.GetAllFiles())
{
var msg = new NetworkResourceUploadMessage();
msg.RelativePath = path;
msg.Data = data;
channel.SendMessage(msg);
}
if (SizeLimit > 0f && size * BytesToMegabytes > SizeLimit)
throw new Exception("File upload too large!");
}
internal bool SendToNewUser(INetChannel channel)
{
var allFiles = ContentRoot.GetAllFiles().ToList();
if (allFiles.Count == 0)
return false;
SendToPlayer(channel, allFiles, AckInitial);
return true;
}
private async void SendToPlayer(INetChannel channel, List<(ResPath Relative, byte[] Data)> files, int ack = 0)
{
await using var stream = TransferManager.StartTransfer(channel,
new TransferStartInfo
{
MessageKey = TransferKeyNetworkDownload
});
var ackBytes = new byte[4];
BinaryPrimitives.WriteInt32LittleEndian(ackBytes, ack);
await stream.WriteAsync(ackBytes);
await WriteFileStream(stream, files);
}
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.IoC;
using Robust.Server.Player;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.Server.Upload;
@@ -9,20 +10,36 @@ namespace Robust.Server.Upload;
internal sealed class UploadedContentManager
{
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GamePrototypeLoadManager _prototypeLoadManager = default!;
[Dependency] private readonly NetworkResourceManager _networkResourceManager = default!;
public void Initialize()
{
_netManager.Connected += NetManagerOnConnected;
_networkResourceManager.AckReceived += OnAckReceived;
}
private void OnAckReceived(INetChannel channel, int ack)
{
if (ack != NetworkResourceManager.AckInitial)
return;
ResourcesReady(channel);
}
private void NetManagerOnConnected(object? sender, NetChannelArgs e)
{
// This just shells out to the other managers, ensuring they are ordered properly.
// Resources must be done before prototypes.
// Note: both net messages sent here are on the same group and are therefore ordered.
_networkResourceManager.SendToNewUser(e.Channel);
_prototypeLoadManager.SendToNewUser(e.Channel);
var sentAny = _networkResourceManager.SendToNewUser(e.Channel);
if (!sentAny)
ResourcesReady(e.Channel);
}
private void ResourcesReady(INetChannel channel)
{
_prototypeLoadManager.SendToNewUser(channel);
_playerManager.MarkPlayerResourcesSent(channel);
}
}

View File

@@ -12,5 +12,6 @@ namespace Robust.Server.ViewVariables
object Object { get; }
uint SessionId { get; }
Type ObjectType { get; }
Action<object>? ObjectChangeDelegate { get; }
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -138,6 +137,7 @@ namespace Robust.Server.ViewVariables
}
object theObject;
Action<object>? objectChangeDelegate = null;
switch (message.Selector)
{
@@ -200,13 +200,14 @@ namespace Robust.Server.ViewVariables
return;
}
if (value == null || value.GetType().IsValueType)
if (value == null)
{
Deny(ViewVariablesResponseCode.NoObject);
return;
}
theObject = value;
objectChangeDelegate = obj => relSession.Modify(sessionRelativeSelector.PropertyIndex, obj);
break;
}
case ViewVariablesIoCSelector ioCSelector:
@@ -250,7 +251,7 @@ namespace Robust.Server.ViewVariables
}
var sessionId = _nextSessionId++;
var session = new ViewVariablesSession(message.MsgChannel.UserId, theObject, sessionId, this,
var session = new ViewVariablesSession(message.MsgChannel.UserId, theObject, objectChangeDelegate, sessionId, this,
_robustSerializer, _entityManager, Sawmill);
_sessions.Add(sessionId, session);

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
@@ -198,6 +197,8 @@ namespace Robust.Server.ViewVariables.Traits
try
{
field.SetValue(Session.Object, value);
Session.ObjectChangeDelegate?.Invoke(Session.Object);
return true;
}
catch (Exception e)

View File

@@ -22,6 +22,7 @@ namespace Robust.Server.ViewVariables
public object Object { get; }
public uint SessionId { get; }
public Type ObjectType { get; }
public Action<object>? ObjectChangeDelegate { get; }
/// <param name="playerUser">The session ID of the player who opened this session.</param>
/// <param name="o">The object we represent.</param>
@@ -29,13 +30,14 @@ namespace Robust.Server.ViewVariables
/// The session ID for this session. This is what the server and client use to talk about this session.
/// </param>
/// <param name="host">The view variables host owning this session.</param>
public ViewVariablesSession(NetUserId playerUser, object o, uint sessionId, IServerViewVariablesInternal host,
public ViewVariablesSession(NetUserId playerUser, object o, Action<object>? objectChangeDelegate, uint sessionId, IServerViewVariablesInternal host,
IRobustSerializer robustSerializer, IEntityManager entMan, ISawmill logger)
{
PlayerUser = playerUser;
Object = o;
SessionId = sessionId;
ObjectType = o.GetType();
ObjectChangeDelegate = objectChangeDelegate;
Host = host;
RobustSerializer = robustSerializer;
EntityManager = entMan;

View File

@@ -1,2 +1,5 @@
@echo off
cd /d "%~dp0"
Robust.Server.exe
pause

View File

@@ -2,9 +2,14 @@ using System;
using System.IO;
using Moq;
using NUnit.Framework;
using Robust.Server;
using Robust.Server.Configuration;
using Robust.Server.Network.Transfer;
using Robust.Server.Player;
using Robust.Server.Reflection;
using Robust.Server.Serialization;
using Robust.Server.ServerStatus;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -13,6 +18,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Network.Transfer;
using Robust.Shared.Profiling;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
@@ -43,6 +49,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
container.Register<IAuthManager, AuthManager>();
container.Register<IGameTiming, GameTiming>();
container.Register<ProfManager, ProfManager>();
container.RegisterInstance<ITransferManager>(Mock.Of<ITransferManager>());
container.Register<HttpClientHolder>();
container.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);
container.BuildGraph();

View File

@@ -1,6 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using Robust.Server.Configuration;
@@ -88,7 +85,6 @@ namespace Robust.UnitTesting.Shared.GameObjects
deps.Register<IDynamicTypeFactoryInternal, DynamicTypeFactory>();
deps.RegisterInstance<IModLoader>(new Mock<IModLoader>().Object);
deps.Register<IEntitySystemManager, EntitySystemManager>();
deps.RegisterInstance<IEntityManager>(new Mock<IEntityManager>().Object);
// WHEN WILL THE SUFFERING END
deps.RegisterInstance<IReplayRecordingManager>(new Mock<IReplayRecordingManager>().Object);
@@ -104,6 +100,15 @@ namespace Robust.UnitTesting.Shared.GameObjects
deps.RegisterInstance<IReflectionManager>(reflectionMock.Object);
// Never
var componentFactoryMock = new Mock<IComponentFactory>();
componentFactoryMock.Setup(p => p.AllRegisteredTypes).Returns(Enumerable.Empty<Type>());
deps.RegisterInstance<IComponentFactory>(componentFactoryMock.Object);
var entityManagerMock = new Mock<IEntityManager>();
entityManagerMock.Setup(p => p.ComponentFactory).Returns(componentFactoryMock.Object);
deps.RegisterInstance<IEntityManager>(entityManagerMock.Object);
deps.BuildGraph();
IoCManager.InitThread(deps, true);

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.Analyzers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
using Robust.Shared.Physics.Components;
namespace Robust.UnitTesting.Shared.GameObjects
{
@@ -39,6 +38,14 @@ namespace Robust.UnitTesting.Shared.GameObjects
[Dependency] public readonly ESystemDepA ESystemDepA = default!;
}
internal sealed class ESystemDepAll : EntitySystem
{
[Dependency] public readonly ESystemDepA ESystemDepA = default!;
[Dependency] public readonly IConfigurationManager Config = default!;
[Dependency] public readonly EntityQuery<TransformComponent> TransformQuery = default!;
[Dependency] public readonly EntityQuery<PhysicsComponent> PhysicsQuery = default!;
}
/*
ESystemBase (Abstract)
- ESystemA
@@ -58,6 +65,7 @@ namespace Robust.UnitTesting.Shared.GameObjects
syssy.LoadExtraSystemType<ESystemC>();
syssy.LoadExtraSystemType<ESystemDepA>();
syssy.LoadExtraSystemType<ESystemDepB>();
syssy.LoadExtraSystemType<ESystemDepAll>();
syssy.Initialize(false);
}
@@ -103,5 +111,16 @@ namespace Robust.UnitTesting.Shared.GameObjects
Assert.That(sysB.ESystemDepA, Is.EqualTo(sysA));
}
[Test]
public void DependencyInjectionTest()
{
var esm = IoCManager.Resolve<IEntitySystemManager>();
var sys = esm.GetEntitySystem<ESystemDepAll>();
Assert.That(sys.ESystemDepA, Is.Not.Null);
Assert.That(sys.Config, Is.Not.Null);
Assert.That(sys.TransformQuery, Is.Not.Default);
Assert.That(sys.PhysicsQuery, Is.Not.Default);
}
}
}

View File

@@ -0,0 +1,203 @@
using JetBrains.Annotations;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.UnitTesting.Shared;
namespace Robust.Shared.IntegrationTests.Serialization;
[Serializable, NetSerializable]
[UsedImplicitly(Reason = "Needed so RobustSerializer is guaranteed to pick up on the unsafe types.")]
internal sealed class MakeTheseSerializable
{
public UnsafeFloat Single;
public UnsafeDouble Double;
public UnsafeHalf Half;
public Half SafeHalf;
}
/// <summary>
/// Tests the serialization behavior of float types when <see cref="IRobustSerializer"/> is *not* set to do anything special.
/// Tests both primitives and Robust's "Unsafe" variants.
/// </summary>
[TestFixture, TestOf(typeof(RobustSerializer)), TestOf(typeof(NetUnsafeFloatSerializer))]
internal sealed class NetSerializerDefaultFloatTest : OurRobustUnitTest
{
private IRobustSerializer _serializer = null!;
[OneTimeSetUp]
public void Setup()
{
_serializer = IoCManager.Resolve<IRobustSerializer>();
_serializer.Initialize();
}
internal static readonly TestCaseData[] PassThroughFloatTests =
[
new TestCaseData(0.0).Returns(0.0),
new TestCaseData(1.0).Returns(1.0),
new TestCaseData(double.NaN).Returns(double.NaN),
new TestCaseData(double.PositiveInfinity).Returns(double.PositiveInfinity),
];
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestSingle(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (float)input);
ms.Position = 0;
return _serializer.Deserialize<float>(ms);
}
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestUnsafeSingle(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeFloat)input);
ms.Position = 0;
return _serializer.Deserialize<UnsafeFloat>(ms);
}
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestDouble(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, input);
ms.Position = 0;
return _serializer.Deserialize<double>(ms);
}
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestUnsafeDouble(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeDouble)input);
ms.Position = 0;
return _serializer.Deserialize<UnsafeDouble>(ms);
}
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestHalf(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (Half)input);
ms.Position = 0;
return (double)_serializer.Deserialize<Half>(ms);
}
[TestCaseSource(nameof(PassThroughFloatTests))]
public double TestUnsafeHalf(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeHalf)(Half)input);
ms.Position = 0;
return (double)(Half)_serializer.Deserialize<UnsafeHalf>(ms);
}
}
/// <summary>
/// Tests the serialization behavior of float types when <see cref="IRobustSerializer"/> is set to remove NaNs on read.
/// Tests both primitives and Robust's "Unsafe" variants.
/// </summary>
[TestFixture]
[TestOf(typeof(RobustSerializer)), TestOf(typeof(NetUnsafeFloatSerializer)), TestOf(typeof(NetSafeFloatSerializer))]
internal sealed class NetSerializerSafeFloatTest : OurRobustUnitTest
{
private IRobustSerializer _serializer = default!;
[OneTimeSetUp]
public void Setup()
{
_serializer = IoCManager.Resolve<IRobustSerializer>();
_serializer.FloatFlags = SerializerFloatFlags.RemoveReadNan;
_serializer.Initialize();
}
internal static readonly TestCaseData[] SafeFloatTests =
[
new TestCaseData(0.0).Returns(0.0),
new TestCaseData(1.0).Returns(1.0),
new TestCaseData(double.NaN).Returns(0.0),
new TestCaseData(double.PositiveInfinity).Returns(double.PositiveInfinity),
];
[TestCaseSource(nameof(SafeFloatTests))]
public double TestSingle(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (float)input);
ms.Position = 0;
return _serializer.Deserialize<float>(ms);
}
[TestCaseSource(typeof(NetSerializerDefaultFloatTest), nameof(NetSerializerDefaultFloatTest.PassThroughFloatTests))]
public double TestUnsafeSingle(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeFloat)input);
ms.Position = 0;
return _serializer.Deserialize<UnsafeFloat>(ms);
}
[TestCaseSource(nameof(SafeFloatTests))]
public double TestDouble(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, input);
ms.Position = 0;
return _serializer.Deserialize<double>(ms);
}
[TestCaseSource(typeof(NetSerializerDefaultFloatTest), nameof(NetSerializerDefaultFloatTest.PassThroughFloatTests))]
public double TestUnsafeDouble(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeDouble)input);
ms.Position = 0;
return _serializer.Deserialize<UnsafeDouble>(ms);
}
[TestCaseSource(nameof(SafeFloatTests))]
public double TestHalf(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (Half)input);
ms.Position = 0;
return (double)_serializer.Deserialize<Half>(ms);
}
[TestCaseSource(typeof(NetSerializerDefaultFloatTest), nameof(NetSerializerDefaultFloatTest.PassThroughFloatTests))]
public double TestUnsafeHalf(double input)
{
var ms = new MemoryStream();
_serializer.Serialize(ms, (UnsafeHalf)(Half)input);
ms.Position = 0;
return (double)(Half)_serializer.Deserialize<UnsafeHalf>(ms);
}
}

View File

@@ -0,0 +1,53 @@
using System;
namespace Robust.Shared.Maths;
/// <summary>
/// Marker type to indicate floating point values that should preserve NaNs across the network.
/// </summary>
/// <remarks>
/// Robust's network serializer may be configured to flush NaN float values to 0,
/// to avoid exploits from lacking input validation. Even if this feature is enabled,
/// NaN values passed in this type are still untouched.
/// </remarks>
/// <param name="Value">The actual inner floating point value</param>
/// <seealso cref="System.Half"/>
public readonly record struct UnsafeHalf(Half Value)
{
public static implicit operator Half(UnsafeHalf f) => f.Value;
public static implicit operator UnsafeHalf(Half f) => new(f);
}
/// <summary>
/// Marker type to indicate floating point values that should preserve NaNs across the network.
/// </summary>
/// <remarks>
/// Robust's network serializer may be configured to flush NaN float values to 0,
/// to avoid exploits from lacking input validation. Even if this feature is enabled,
/// NaN values passed in this type are still untouched.
/// </remarks>
/// <param name="Value">The actual inner floating point value</param>
/// <seealso cref="System.Single"/>
public readonly record struct UnsafeFloat(float Value)
{
public static implicit operator float(UnsafeFloat f) => f.Value;
public static implicit operator UnsafeFloat(float f) => new(f);
}
/// <summary>
/// Marker type to indicate floating point values that should preserve NaNs across the network.
/// </summary>
/// <remarks>
/// Robust's network serializer may be configured to flush NaN float values to 0,
/// to avoid exploits from lacking input validation. Even if this feature is enabled,
/// NaN values passed in this type are still untouched.
/// </remarks>
/// <param name="Value">The actual inner floating point value</param>
/// <seealso cref="System.Double"/>
public readonly record struct UnsafeDouble(double Value)
{
public static implicit operator double(UnsafeDouble f) => f.Value;
public static implicit operator UnsafeDouble(double f) => new(f);
public static implicit operator UnsafeDouble(float f) => new(f);
public static implicit operator UnsafeDouble(UnsafeFloat f) => new(f);
}

View File

@@ -75,6 +75,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
return;
var audioLength = GetAudioLength(entity.Comp.FileName);
position = CalculateAudioPosition(entity!, (float)audioLength.TotalSeconds, position);
if (audioLength.TotalSeconds < position)
{
@@ -86,12 +87,6 @@ public abstract partial class SharedAudioSystem : EntitySystem
return;
}
if (position < 0f)
{
Log.Error($"Tried to set playback position for {ToPrettyString(entity.Owner)} / {entity.Comp.FileName} outside of bounds");
return;
}
// If we're paused then the current position is <pause time - start time>, else it's <cur time - start time>
var currentPos = (entity.Comp.PauseTime ?? Timing.CurTime) - entity.Comp.AudioStart;
var timeOffset = TimeSpan.FromSeconds(position - currentPos.TotalSeconds);
@@ -315,6 +310,36 @@ public abstract partial class SharedAudioSystem : EntitySystem
return GetAudioPath(resolved);
}
/// <summary>
/// Calculates the current playback position of an audio entity
/// and clamps it to the range from 0 to (length - 0.01f).
/// </summary>
/// <param name="ent">The audio entity.</param>
/// <param name="length">
/// The total length of the audio file.
/// If null, the method retrieves the length using <see cref="GetAudioLength"/>.
/// </param>
/// <param name="position">
/// A precomputed playback position.
/// If provided, it will be added to the calculation.
/// </param>
/// <returns>The playback position as a float.</returns>
protected float CalculateAudioPosition(Entity<AudioComponent> ent, float? length = null, float? position = null)
{
position ??= (float) ((ent.Comp.PauseTime ?? Timing.CurTime) - ent.Comp.AudioStart).TotalSeconds;
length ??= (float) GetAudioLength(ent.Comp.FileName).TotalSeconds;
// Looped audio has no conceptual start or end.
if (ent.Comp.Params.Loop)
position %= length;
// TODO clamp the offset inside of AudioSource.SetPlaybackPosition() itself.
var maxOffset = Math.Max((float) length - 0.01f, 0f);
position = Math.Clamp(position.Value, 0f, maxOffset);
return position.Value;
}
#region AudioParams
[return: NotNullIfNotNull(nameof(specifier))]
@@ -455,7 +480,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
if (uid == null || !Resolve(uid.Value, ref component, false))
return null;
if (_netManager.IsClient && !IsClientSide(uid.Value))
if (!Timing.IsFirstTimePredicted || (_netManager.IsClient && !IsClientSide(uid.Value)))
return null;
QueueDel(uid);

View File

@@ -406,6 +406,46 @@ namespace Robust.Shared
public static readonly CVarDef<bool> NetHWId =
CVarDef.Create("net.hwid", true, CVar.SERVERONLY);
/**
* TRANSFER
*/
/// <summary>
/// If true, enable the WebSocket-based high bandwidth transfer channel.
/// </summary>
/// <remarks>
/// <para>
/// If set, <see cref="TransferHttpEndpoint"/> must be set to the API address of the server,
/// and you must ensure your reverse proxy (if you have one) is configured to allow WebSocket connections.
/// </para>
/// <para>
/// The transfer channel has no additional encryption layer. Unless your API is exposed behind HTTPS,
/// traffic over the channel will not be encrypted, and you are discouraged from enabling it.
/// </para>
/// </remarks>
public static readonly CVarDef<bool> TransferHttp =
CVarDef.Create("transfer.http", false, CVar.SERVERONLY);
/// <summary>
/// The base HTTP URL of the game server, used for the high-bandwidth transfer channel.
/// </summary>
public static readonly CVarDef<string> TransferHttpEndpoint =
CVarDef.Create("transfer.http_endpoint", "http://localhost:1212/", CVar.SERVERONLY);
/// <summary>
/// Amount of concurrent client->server transfer streams allowed.
/// </summary>
/// <remarks>
/// Clients will be disconnected if they exceed this limit.
/// </remarks>
public static readonly CVarDef<int> TransferStreamLimit =
CVarDef.Create("transfer.stream_limit", 10, CVar.SERVERONLY);
/// <summary>
/// Artificially delay transfer operations to simulate slow network. Debug option.
/// </summary>
internal static readonly CVarDef<bool> TransferArtificialDelay =
CVarDef.Create("transfer.artificial_delay", false);
/**
* SUS

View File

@@ -205,60 +205,67 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
/// </summary>
public void UpdateTreePositions()
{
if (!CheckEnabled())
return;
if (_updateQueue.Count == 0)
return;
var trees = GetEntityQuery<TTreeComp>();
while (_updateQueue.TryDequeue(out var entry))
try
{
var (comp, xform) = entry;
if (!CheckEnabled())
return;
// Was this entity queued multiple times?
DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?");
if (_updateQueue.Count == 0)
return;
comp.TreeUpdateQueued = false;
if (!comp.Running)
continue;
var trees = GetEntityQuery<TTreeComp>();
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
while (_updateQueue.TryDequeue(out var entry))
{
var (comp, xform) = entry;
// Was this entity queued multiple times?
DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?");
comp.TreeUpdateQueued = false;
if (!comp.Running)
continue;
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
{
RemoveFromTree(comp);
continue;
}
var newTree = xform.GridUid ?? xform.MapUid;
if (!trees.TryGetComponent(newTree, out var newTreeComp) && comp.TreeUid == null)
continue;
Vector2 pos;
Angle rot;
if (comp.TreeUid == newTree)
{
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value);
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
continue;
}
RemoveFromTree(comp);
continue;
}
var newTree = xform.GridUid ?? xform.MapUid;
if (!trees.TryGetComponent(newTree, out var newTreeComp) && comp.TreeUid == null)
continue;
if (newTreeComp == null)
return;
comp.TreeUid = newTree;
comp.Tree = newTreeComp.Tree;
Vector2 pos;
Angle rot;
if (comp.TreeUid == newTree)
{
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value);
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
continue;
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
}
RemoveFromTree(comp);
if (newTreeComp == null)
return;
comp.TreeUid = newTree;
comp.Tree = newTreeComp.Tree;
(pos, rot) = XformSystem.GetRelativePositionRotation(
entry.Transform,
newTree!.Value);
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
}
finally
{
_updateQueue.Clear();
}
}

View File

@@ -0,0 +1,23 @@
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Robust.Shared.Console.Commands;
internal sealed class DumpStringTableCommand : IConsoleCommand
{
[Dependency] private readonly INetManager _netManager = default!;
public string Command => "net_dumpstringtable";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var netMgr = (NetManager)_netManager;
foreach (var (k, v) in netMgr.StringTable.Strings.OrderBy(x => x.Key))
{
shell.WriteLine($"{k}: {v}");
}
}
}

View File

@@ -101,7 +101,7 @@ public abstract partial class SharedContainerSystem
RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true);
RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, container), false);
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), "failed to set destination");
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), $"Failed to set coordinates of {ToPrettyString(toRemove, meta)} to be inside {ToPrettyString(container.Owner)} container '{container.ID}'");
Dirty(container.Owner, container.Manager);
return true;

View File

@@ -14,12 +14,9 @@ namespace Robust.Shared.ContentPack;
internal sealed partial class AssemblyTypeChecker
{
// This part of the code tries to find the originator of bad sandbox references.
private void ReportBadReferences(PEReader peReader, MetadataReader reader, IEnumerable<EntityHandle> reference)
private IEnumerable<(EntityHandle Referenced, MethodDefinitionHandle SourceMethod, int InstructionOffset)> FindReference(PEReader peReader, MetadataReader reader, params IEnumerable<EntityHandle> handles)
{
_sawmill.Info("Started search for originator of bad references...");
var refs = reference.ToHashSet();
var refs = handles.ToHashSet();
ExpandReferences(reader, refs);
foreach (var methodDefHandle in reader.MethodDefinitions)
@@ -28,8 +25,6 @@ internal sealed partial class AssemblyTypeChecker
if (methodDef.RelativeVirtualAddress == 0)
continue;
var methodName = reader.GetString(methodDef.Name);
var body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress);
var bytes = body.GetILBytes()!;
@@ -41,9 +36,7 @@ internal sealed partial class AssemblyTypeChecker
{
if (refs.Overlaps(ExpandHandle(reader, handle)))
{
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
$"Found reference to {DisplayHandle(reader, handle)} in method {type}.{methodName} at IL 0x{prefPosition:X4}");
yield return (handle, methodDefHandle, prefPosition);
}
}
@@ -52,6 +45,19 @@ internal sealed partial class AssemblyTypeChecker
}
}
private void ReportBadReferences(PEReader peReader, MetadataReader reader, IEnumerable<EntityHandle> reference)
{
foreach (var (referenced, method, ilOffset) in FindReference(peReader, reader, reference))
{
var methodDef = reader.GetMethodDefinition(method);
var methodName = reader.GetString(methodDef.Name);
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
$"Found reference to {DisplayHandle(reader, referenced)} in method {type}.{methodName} at IL 0x{ilOffset:X4}");
}
}
private static string DisplayHandle(MetadataReader reader, EntityHandle handle)
{
switch (handle.Kind)

View File

@@ -227,6 +227,8 @@ namespace Robust.Shared.ContentPack
#if TOOLS
if (!badRefs.IsEmpty)
{
_sawmill.Info("Started search for originator of bad references...");
ReportBadReferences(peReader, reader, badRefs);
}
#endif
@@ -298,6 +300,9 @@ namespace Robust.Shared.ContentPack
verifyErrors = true;
_sawmill.Error(msg);
if (!res.Method.IsNil)
PrintCompilerGeneratedMethodUsage(peReader, reader, res.Method);
}
_sawmill.Debug($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms");
@@ -310,6 +315,24 @@ namespace Robust.Shared.ContentPack
return true;
}
private void PrintCompilerGeneratedMethodUsage(
PEReader peReader,
MetadataReader reader,
MethodDefinitionHandle method)
{
#if TOOLS
var methodDef = reader.GetMethodDefinition(method);
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
if (!type.Name.Contains('<'))
return;
_sawmill.Error("Hint: method is compiler-generated. Check for params collections and/or collection expressions:");
ReportBadReferences(peReader, reader, [method]);
#endif
}
private static string FormatMethodName(MetadataReader reader, MethodDefinition method)
{
var methodSig = method.DecodeSignature(new TypeProvider(), 0);

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
@@ -16,8 +15,7 @@ namespace Robust.Shared.ContentPack
/// </summary>
internal static string GetExecutableDirectory()
{
// TODO: remove this shitty hack, either through making it less hardcoded into shared,
// or by making our file structure less spaghetti somehow.
// Fallback in case the above doesn't work ig?
var assembly = typeof(PathHelpers).Assembly;
var location = assembly.Location;
if (location == string.Empty)

View File

@@ -444,6 +444,7 @@ Types:
LinkedList`1: { All: True }
LinkedListNode`1: { All: True }
List`1: { All: True }
OrderedDictionary`2: { All: True }
Queue`1: { All: True }
ReferenceEqualityComparer: { All: True }
SortedDictionary`2: { All: True }
@@ -1462,6 +1463,7 @@ Types:
- "void .ctor(char[], int, int)"
- "void .ctor(System.ReadOnlySpan`1<char>)"
- "void CopyTo(int, char[], int, int)"
StringComparer: { All: True }
StringComparison: { } # Enum
StringSplitOptions: { } # Enum
TimeOnly: { All: True }

View File

@@ -19,8 +19,8 @@ namespace Robust.Shared.EntitySerialization.Systems;
public sealed partial class MapLoaderSystem
{
/// <summary>
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// Tries to load entities from a YAML file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// </summary>
public bool TryLoadGeneric(
ResPath file,
@@ -30,6 +30,7 @@ public sealed partial class MapLoaderSystem
{
grids = null;
maps = null;
if (!TryLoadGeneric(file, out var data, options))
return false;
@@ -39,33 +40,29 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Tries to load entities from a YAML file, taking in a raw byte stream.
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// </summary>
/// <param name="file">The file contents to load from.</param>
/// <param name="fileName">
/// The name of the file being loaded. This is used purely for logging/informational purposes.
/// </param>
/// <param name="result">The result of the load operation.</param>
/// <param name="options">Options for the load operation.</param>
/// <returns>True if the load succeeded, false otherwise.</returns>
/// <seealso cref="M:Robust.Shared.EntitySerialization.Systems.MapLoaderSystem.TryLoadGeneric(Robust.Shared.Utility.ResPath,Robust.Shared.EntitySerialization.LoadResult@,System.Nullable{Robust.Shared.EntitySerialization.MapLoadOptions})"/>
public bool TryLoadGeneric(
Stream file,
string fileName,
[NotNullWhen(true)] out LoadResult? result,
TextReader reader,
string source,
[NotNullWhen(true)] out HashSet<Entity<MapComponent>>? maps,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
MapLoadOptions? options = null)
{
result = null;
if (!TryReadFile(new StreamReader(file), out var data))
grids = null;
maps = null;
if (!TryLoadGeneric(reader, source, out var data, options))
return false;
return TryLoadGeneric(data, fileName, out result, options);
maps = data.Maps;
grids = data.Grids;
return true;
}
/// <summary>
/// Tries to load entities from a yaml file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// Tries to load entities from a YAML file. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// </summary>
/// <param name="file">The file to load.</param>
/// <param name="result">Data class containing information about the loaded entities</param>
@@ -74,15 +71,51 @@ public sealed partial class MapLoaderSystem
{
result = null;
if (!TryReadFile(file, out var data))
if (!TryReadFile(file.ToRootedPath(), out var data))
return false;
return TryLoadGeneric(data, file.ToString(), out result, options);
}
private bool TryLoadGeneric(
/// <summary>
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// </summary>
/// <param name="reader">The text to load.</param>
/// <param name="source">The name of the source, if any. This should be your file path (for example)</param>
/// <param name="result">Data class containing information about the loaded entities</param>
/// <param name="options">Optional Options for configuring loading behaviour.</param>
public bool TryLoadGeneric(TextReader reader, string source, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
{
result = null;
if (!TryReadFile(reader, out var data))
return false;
return TryLoadGeneric(data, source, out result, options);
}
/// <summary>
/// Tries to load entities from a YAML text stream. Whenever possible, you should try to use <see cref="TryLoadMap"/>,
/// <see cref="TryLoadGrid"/>, or <see cref="TryLoadEntity"/> instead.
/// </summary>
/// <param name="stream">The stream containing the text to load.</param>
/// <param name="source">The name of the source, if any. This should be your file path (for example)</param>
/// <param name="result">Data class containing information about the loaded entities</param>
/// <param name="options">Optional Options for configuring loading behaviour.</param>
public bool TryLoadGeneric(Stream stream, string source, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
{
result = null;
if (!TryReadFile(new StreamReader(stream, leaveOpen: true), out var data))
return false;
return TryLoadGeneric(data, source, out result, options);
}
public bool TryLoadGeneric(
MappingDataNode data,
string fileName,
string source,
[NotNullWhen(true)] out LoadResult? result,
MapLoadOptions? options = null)
{
@@ -118,7 +151,7 @@ public sealed partial class MapLoaderSystem
if (!deserializer.TryProcessData())
{
Log.Debug($"Failed to process entity data in {fileName}");
Log.Debug($"Failed to process entity data in {source}");
return false;
}
@@ -128,7 +161,7 @@ public sealed partial class MapLoaderSystem
&& deserializer.Result.Category != FileCategory.Unknown)
{
// Did someone try to load a map file as a grid or vice versa?
Log.Error($"Map {fileName} does not contain the expected data. Expected {expected} but got {deserializer.Result.Category}");
Log.Error($"Map {source} does not contain the expected data. Expected {expected} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}
@@ -139,7 +172,7 @@ public sealed partial class MapLoaderSystem
}
catch (Exception e)
{
Log.Error($"Caught exception while creating entities for map {fileName}: {e}");
Log.Error($"Caught exception while creating entities for map {source}: {e}");
Delete(deserializer.Result);
throw;
}
@@ -149,7 +182,7 @@ public sealed partial class MapLoaderSystem
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
{
// Did someone try to load a map file as a grid or vice versa?
Log.Error($"Map {fileName} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Log.Error($"Map {source} does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}
@@ -184,12 +217,33 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Tries to load a regular (non-map, non-grid) entity from a file.
/// The loaded entity will initially be in null-space.
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
/// Tries to load a regular (non-map, non-grid) entity from a YAML file.
/// The loaded entity will initially be in null-space.
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadEntity(
ResPath path,
ResPath file,
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
DeserializationOptions? options = null)
{
entity = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryLoadEntity(reader, file.ToString(), out entity, options);
}
}
/// <summary>
/// Tries to load a regular (non-map, non-grid) entity from a YAML text stream.
/// The loaded entity will initially be in null-space.
/// If the file does not contain exactly one orphaned entity, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadEntity(
TextReader reader,
string source,
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
DeserializationOptions? options = null)
{
@@ -200,7 +254,7 @@ public sealed partial class MapLoaderSystem
};
entity = null;
if (!TryLoadGeneric(path, out var result, opts))
if (!TryLoadGeneric(reader, source, out var result, opts))
return false;
if (result.Orphans.Count == 1)
@@ -215,12 +269,35 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Tries to load a grid entity from a file and parent it to the given map.
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
/// Tries to load a grid entity from a YAML file and parent it to the given map.
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadGrid(
MapId map,
ResPath path,
ResPath file,
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
grid = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryLoadGrid(map, reader, file.ToString(), out grid, options, offset, rot);
}
}
/// <summary>
/// Tries to load a grid entity from a YAML text stream and parent it to the given map.
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadGrid(
MapId map,
TextReader reader,
string source,
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
DeserializationOptions? options = null,
Vector2 offset = default,
@@ -236,7 +313,7 @@ public sealed partial class MapLoaderSystem
};
grid = null;
if (!TryLoadGeneric(path, out var result, opts))
if (!TryLoadGeneric(reader, source, out var result, opts))
return false;
if (result.Grids.Count == 1)
@@ -250,11 +327,35 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Tries to load a grid entity from a file and parent it to a newly created map.
/// Tries to load a grid entity from a YAML file and parent it to a newly created map.
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadGrid(
ResPath path,
ResPath file,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
grid = null;
map = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryLoadGrid(reader, file.ToString(), out map, out grid, options, offset, rot);
}
}
/// <summary>
/// Tries to load a grid entity from a YAML text stream and parent it to a newly created map.
/// If the file does not contain exactly one grid, this will return false and delete loaded entities.
/// </summary>
public bool TryLoadGrid(
TextReader reader,
string source,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
DeserializationOptions? options = null,
@@ -267,7 +368,7 @@ public sealed partial class MapLoaderSystem
if (opts.PauseMaps)
_mapSystem.SetPaused(mapUid, true);
if (!TryLoadGrid(mapId, path, out grid, options, offset, rot))
if (!TryLoadGrid(mapId, reader, source, out grid, options, offset, rot))
{
Del(mapUid);
map = null;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using Robust.Shared.GameObjects;
@@ -15,14 +16,41 @@ namespace Robust.Shared.EntitySerialization.Systems;
public sealed partial class MapLoaderSystem
{
/// <summary>
/// Attempts to load a file containing a single map.
/// If the file does not contain exactly one map, this will return false and delete all loaded entities.
/// Attempts to load a YAML file containing a single map.
/// If the file does not contain exactly one map, this will return false and delete all loaded entities.
/// </summary>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMap(
ResPath path,
ResPath file,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
map = null;
grids = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryLoadMap(reader, file.ToString(), out map, out grids, options, offset, rot);
}
}
/// <summary>
/// Attempts to load a YAML stream containing a single map.
/// If the file does not contain exactly one map, this will return false and delete all loaded entities.
/// </summary>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMap(
TextReader reader,
string source,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
@@ -39,7 +67,7 @@ public sealed partial class MapLoaderSystem
map = null;
grids = null;
if (!TryLoadGeneric(path, out var result, opts))
if (!TryLoadGeneric(reader, source, out var result, opts))
return false;
if (result.Maps.Count == 1)
@@ -54,17 +82,47 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Attempts to load a file containing a single map, assign it the given map id.
/// Attempts to load a YAML file containing a single map, assign it the given map id.
/// </summary>
/// <remarks>
/// If possible, it is better to use <see cref="TryLoadMap"/> which automatically assigns a <see cref="MapId"/>.
/// If possible, it is better to use <see cref="TryLoadMap"/> which automatically assigns a <see cref="MapId"/>.
/// </remarks>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMapWithId(
MapId mapId,
ResPath path,
ResPath file,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
map = null;
grids = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryLoadMapWithId(mapId, reader, file.ToString(), out map, out grids, options, offset, rot);
}
}
/// <summary>
/// Attempts to load a YAML text stream containing a single map, assign it the given map id.
/// </summary>
/// <remarks>
/// If possible, it is better to use <see cref="TryLoadMap"/> which automatically assigns a <see cref="MapId"/>.
/// </remarks>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMapWithId(
MapId mapId,
TextReader reader,
string source,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
@@ -86,7 +144,7 @@ public sealed partial class MapLoaderSystem
throw new Exception($"Target map already exists");
opts.ForceMapId = mapId;
if (!TryLoadGeneric(path, out var result, opts))
if (!TryLoadGeneric(reader, source, out var result, opts))
return false;
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))
@@ -98,12 +156,35 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Attempts to load a file containing a single map, and merge its children onto another map. After which the
/// loaded map gets deleted.
/// Attempts to load a YAML text stream containing a single map, and merge its children onto another map. After which
/// the loaded map gets deleted.
/// </summary>
public bool TryMergeMap(
MapId mapId,
ResPath path,
ResPath file,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
grids = null;
if (!TryGetReader(file.ToRootedPath(), out var reader))
return false;
using (reader)
{
return TryMergeMap(mapId, reader, file.ToString(), out grids, options, offset, rot);
}
}
/// <summary>
/// Attempts to load a YAML file containing a single map, and merge its children onto another map. After which
/// the loaded map gets deleted.
/// </summary>
public bool TryMergeMap(
MapId mapId,
TextReader reader,
string source,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
@@ -123,7 +204,7 @@ public sealed partial class MapLoaderSystem
throw new Exception($"Target map {mapId} does not exist");
opts.MergeMap = mapId;
if (!TryLoadGeneric(path, out var result, opts))
if (!TryLoadGeneric(reader, source, out var result, opts))
return false;
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -59,10 +60,19 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Serialize a standard (non-grid, non-map) entity and all of its children and write the result to a
/// yaml file.
/// Serialize a standard (non-grid, non-map) entity and all of its children and write the result to a YAML file.
/// </summary>
public bool TrySaveEntity(EntityUid entity, ResPath path, SerializationOptions? options = null)
public bool TrySaveEntity(EntityUid entity, ResPath target, SerializationOptions? options = null)
{
using var writer = GetWriterForPath(target);
return TrySaveEntity(entity, writer, options);
}
/// <summary>
/// Serialize a standard (non-grid, non-map) entity and all of its children and write the result to a YAML text
/// stream.
/// </summary>
public bool TrySaveEntity(EntityUid entity, TextWriter target, SerializationOptions? options = null)
{
if (_mapQuery.HasComp(entity))
{
@@ -97,12 +107,12 @@ public sealed partial class MapLoaderSystem
return false;
}
Write(path, data);
Write(target, data);
return true;
}
/// <summary>
/// Serialize a map and all of its children and write the result to a yaml file.
/// Serialize a map and all of its children and write the result to a YAML file.
/// </summary>
public bool TrySaveMap(MapId mapId, ResPath path, SerializationOptions? options = null)
{
@@ -114,9 +124,18 @@ public sealed partial class MapLoaderSystem
}
/// <summary>
/// Serialize a map and all of its children and write the result to a yaml file.
/// Serialize a map and all of its children and write the result to a YAML file.
/// </summary>
public bool TrySaveMap(EntityUid map, ResPath path, SerializationOptions? options = null)
public bool TrySaveMap(EntityUid map, ResPath target, SerializationOptions? options = null)
{
using var writer = GetWriterForPath(target);
return TrySaveMap(map, writer, options);
}
/// <summary>
/// Serialize a map and all of its children and write the result to a YAML text stream.
/// </summary>
public bool TrySaveMap(EntityUid map, TextWriter target, SerializationOptions? options = null)
{
if (!_mapQuery.HasComp(map))
{
@@ -145,14 +164,23 @@ public sealed partial class MapLoaderSystem
return false;
}
Write(path, data);
Write(target, data);
return true;
}
/// <summary>
/// Serialize a grid and all of its children and write the result to a yaml file.
/// Serialize a grid and all of its children and write the result to a YAML file.
/// </summary>
public bool TrySaveGrid(EntityUid grid, ResPath path, SerializationOptions? options = null)
public bool TrySaveGrid(EntityUid map, ResPath target, SerializationOptions? options = null)
{
using var writer = GetWriterForPath(target);
return TrySaveGrid(map, writer, options);
}
/// <summary>
/// Serialize a grid and all of its children and write the result to a YAML text stream.
/// </summary>
public bool TrySaveGrid(EntityUid grid, TextWriter target, SerializationOptions? options = null)
{
if (!_gridQuery.HasComp(grid))
{
@@ -187,32 +215,62 @@ public sealed partial class MapLoaderSystem
return false;
}
Write(path, data);
Write(target, data);
return true;
}
/// <summary>
/// Serialize an entities and all of their children to a yaml file.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// Serialize an entity and all of their children to a YAML file.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// </summary>
public bool TrySaveGeneric(
EntityUid uid,
ResPath path,
ResPath target,
out FileCategory category,
SerializationOptions? options = null)
{
return TrySaveGeneric([uid], path, out category, options);
using var writer = GetWriterForPath(target);
return TrySaveGeneric(uid, writer, out category, options);
}
/// <summary>
/// Serialize one or more entities and all of their children to a yaml file.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// Serialize an entity and all of their children to a YAML text stream.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// </summary>
public bool TrySaveGeneric(
EntityUid uid,
TextWriter target,
out FileCategory category,
SerializationOptions? options = null)
{
return TrySaveGeneric([uid], target, out category, options);
}
/// <summary>
/// Serialize one or more entities and all of their children to a YAML file.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// </summary>
public bool TrySaveGeneric(
HashSet<EntityUid> uid,
ResPath target,
out FileCategory category,
SerializationOptions? options = null)
{
using var writer = GetWriterForPath(target);
return TrySaveGeneric(uid, writer, out category, options);
}
/// <summary>
/// Serialize one or more entities and all of their children to a YAML text stream.
/// This makes no assumptions about the expected entity or resulting file category.
/// If possible, use the map/grid specific variants instead.
/// </summary>
public bool TrySaveGeneric(
HashSet<EntityUid> entities,
ResPath path,
TextWriter target,
out FileCategory category,
SerializationOptions? options = null)
{
@@ -233,10 +291,21 @@ public sealed partial class MapLoaderSystem
return false;
}
Write(path, data);
Write(target, data);
return true;
}
/// <inheritdoc cref="TrySerializeAllEntities(out MappingDataNode, SerializationOptions?)"/>
public bool TrySaveAllEntities(TextWriter target, SerializationOptions? options = null)
{
if (!TrySerializeAllEntities(out var data, options))
return false;
Write(target, data);
return true;
}
/// <inheritdoc cref="TrySerializeAllEntities(out MappingDataNode, SerializationOptions?)"/>
public bool TrySaveAllEntities(ResPath path, SerializationOptions? options = null)
{

View File

@@ -42,17 +42,29 @@ public sealed partial class MapLoaderSystem : EntitySystem
_gridQuery = GetEntityQuery<MapGridComponent>();
}
private void Write(TextWriter target, MappingDataNode data)
{
var document = new YamlDocument(data.ToYaml());
var stream = new YamlStream {document};
stream.Save(new YamlMappingFix(new Emitter(target)), false);
}
private StreamWriter GetWriterForPath(ResPath path)
{
Log.Info($"Saving serialized results to {path}");
path = path.ToRootedPath();
_resourceManager.UserData.CreateDir(path.Directory);
return _resourceManager.UserData.OpenWriteText(path);
}
private void Write(ResPath path, MappingDataNode data)
{
Log.Info($"Saving serialized results to {path}");
path = path.ToRootedPath();
var document = new YamlDocument(data.ToYaml());
_resourceManager.UserData.CreateDir(path.Directory);
using var writer = _resourceManager.UserData.OpenWriteText(path);
{
var stream = new YamlStream {document};
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}
Write(writer, data);
}
public bool TryReadFile(ResPath file, [NotNullWhen(true)] out MappingDataNode? data)

View File

@@ -32,7 +32,8 @@ namespace Robust.Shared.GameObjects
protected BoundUserInterface(EntityUid owner, Enum uiKey)
{
IoCManager.InjectDependencies(this);
IoCManager.Resolve(ref EntMan);
EntMan.EntitySysManager.DependencyCollection.InjectDependencies(this);
UiSystem = EntMan.System<SharedUserInterfaceSystem>();
Owner = owner;

View File

@@ -838,18 +838,16 @@ namespace Robust.Shared.GameObjects
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool EnsureComponent<T>(ref Entity<T?> entity) where T : IComponent, new()
{
if (entity.Comp != null)
{
// Check for deferred component removal.
if (entity.Comp.LifeStage <= ComponentLifeStage.Running)
{
DebugTools.AssertOwner(entity, entity.Comp);
return true;
}
if (entity.Comp == null)
return EnsureComponent<T>(entity.Owner, out entity.Comp);
RemoveComponent(entity, entity.Comp);
}
DebugTools.AssertOwner(entity, entity.Comp);
// Check for deferred component removal.
if (entity.Comp.LifeStage <= ComponentLifeStage.Running)
return true;
RemoveComponent(entity, entity.Comp);
entity.Comp = AddComponent<T>(entity);
return false;
}

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