mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48dbcf7fd4 | ||
|
|
686c47a193 | ||
|
|
ecfaa68ae6 | ||
|
|
fd27f315cb | ||
|
|
fe1648d290 | ||
|
|
ec0c667c33 | ||
|
|
75d0c29973 | ||
|
|
5d6dbc18e3 | ||
|
|
5e160e26ee | ||
|
|
0e54fa7329 | ||
|
|
2722448474 | ||
|
|
e7f75ab35d | ||
|
|
57361e8ffd | ||
|
|
8449015cf8 | ||
|
|
72d6a42c27 | ||
|
|
f509405022 | ||
|
|
7bb516f0bf | ||
|
|
521e7981bc | ||
|
|
4c87e6185f | ||
|
|
aaf5003fcf | ||
|
|
3bec89aaa5 | ||
|
|
0b93a1b7e2 | ||
|
|
51397ba319 | ||
|
|
446cf8c003 | ||
|
|
65b8d0cce2 | ||
|
|
397b441a17 | ||
|
|
40b10f0dcc | ||
|
|
5885549c78 | ||
|
|
89e16d5ba9 | ||
|
|
2afef1480e | ||
|
|
76189579c7 | ||
|
|
114c2bee62 | ||
|
|
ce96331ec4 | ||
|
|
14b17aff6d | ||
|
|
65ed19fa4e | ||
|
|
30cd9eb527 | ||
|
|
3f556814a5 | ||
|
|
17662baaf7 | ||
|
|
dc1464b462 | ||
|
|
48654ac424 | ||
|
|
d9ea1079f7 | ||
|
|
cb384b8242 | ||
|
|
21581df93d | ||
|
|
df98bca4bc | ||
|
|
02b64b7386 | ||
|
|
36e5f10511 | ||
|
|
93d14d55c7 | ||
|
|
c20343601d | ||
|
|
52d3376c9e | ||
|
|
a417a8fd99 | ||
|
|
627856e207 | ||
|
|
aa5cca4c7f | ||
|
|
5b06066fcb | ||
|
|
736e46cd82 | ||
|
|
539d0563b8 | ||
|
|
d9740e3a4f | ||
|
|
89b6bcd8e2 | ||
|
|
7b245260e3 | ||
|
|
68f8d00931 | ||
|
|
57ad191d02 | ||
|
|
8cecdcc8de | ||
|
|
de7cdec86e | ||
|
|
8ffa85c266 | ||
|
|
e5be11458e | ||
|
|
9464ccb500 | ||
|
|
af7f4ec8e7 | ||
|
|
de2dafe507 | ||
|
|
dd41a7ce44 | ||
|
|
c25f6c5e98 | ||
|
|
2799de33c5 | ||
|
|
1fea48fbf4 | ||
|
|
636e287fc5 | ||
|
|
d43c3f2caf | ||
|
|
a1dddf6af1 | ||
|
|
5f1327808d | ||
|
|
d78e3ce157 | ||
|
|
602d7833a1 |
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
24
MSBuild/MacOSAppBundle.targets
Normal file
24
MSBuild/MacOSAppBundle.targets
Normal 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 "$(RTAppBundleIcon)"</_RTMacOSAppBundle_icon>
|
||||
</PropertyGroup>
|
||||
<Exec Command="$(MSBuildThisFileDirectory)/../Tools/macos_make_appbundle.py $(_RTMacOSAppBundle_for_webview) --name "$(RTAppBundleName)" --directory "$(OutputPath)" --apphost "$(AssemblyName)" --identifier "$(RTAppBundleIdentifier)" $(_RTMacOSAppBundle_icon)" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
5758
RELEASE-NOTES.md
5758
RELEASE-NOTES.md
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
));
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace Robust.Client.Graphics
|
||||
[NotContentImplementable]
|
||||
public interface IClyde
|
||||
{
|
||||
internal bool IsInitialized { get; }
|
||||
|
||||
IClydeWindow MainWindow { get; }
|
||||
IRenderTarget MainWindowRenderTarget => MainWindow.RenderTarget;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
89
Robust.Client/Network/Transfer/ClientTransferManager.cs
Normal file
89
Robust.Client/Network/Transfer/ClientTransferManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
14
Robust.Client/Network/Transfer/ClientTransferTestManager.cs
Normal file
14
Robust.Client/Network/Transfer/ClientTransferTestManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
122
Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs
Normal file
122
Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
171
Robust.Server/Network/Transfer/ServerTransferManager.cs
Normal file
171
Robust.Server/Network/Transfer/ServerTransferManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
23
Robust.Server/Network/Transfer/ServerTransferTestManager.cs
Normal file
23
Robust.Server/Network/Transfer/ServerTransferTestManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ namespace Robust.Server.ViewVariables
|
||||
object Object { get; }
|
||||
uint SessionId { get; }
|
||||
Type ObjectType { get; }
|
||||
Action<object>? ObjectChangeDelegate { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
@echo off
|
||||
|
||||
cd /d "%~dp0"
|
||||
Robust.Server.exe
|
||||
pause
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
53
Robust.Shared.Maths/UnsafeFloat.cs
Normal file
53
Robust.Shared.Maths/UnsafeFloat.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
Robust.Shared/Console/Commands/DumpStringTableCommand.cs
Normal file
23
Robust.Shared/Console/Commands/DumpStringTableCommand.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user