Compare commits

...

16 Commits

Author SHA1 Message Date
PJB3005
dc46dba04d Version: 264.0.2 2025-09-19 09:17:25 +02:00
Skye
f2c4254e0e Fix resource loading on non-Windows platforms (#6201)
(cherry picked from commit 51bbc5dc45)
2025-09-19 09:17:24 +02:00
PJB3005
24bd679949 Version: 264.0.1 2025-09-14 14:55:49 +02:00
PJB3005
b561441803 Squashed commit of the following:
commit d4f265c314
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sun Sep 14 14:32:44 2025 +0200

    Fix incorrect path combine in DirLoader and WritableDirProvider

    This (and the other couple past commits) reported by Elelzedel.

commit 7654d38612
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 22:50:51 2025 +0200

    Move CEF cache out of data directory

    Don't want content messing with this...

commit cdcc255123
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:11:16 2025 +0200

    Make Robust.Client.WebView.Cef.Program internal.

commit 2f56a6a110
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:10:46 2025 +0200

    Update SpaceWizards.NFluidSynth to 0.2.2

commit 16fc48cef2
Author: PJB3005 <pieterjan.briers+git@gmail.com>
Date:   Sat Sep 13 19:09:43 2025 +0200

    Hide IWritableDirProvider.RootDir on client

    This shouldn't be exposed.

(cherry picked from commit 2f07159336bc640e41fbbccfdec4133a68c13bdb)
(cherry picked from commit d6c3212c74373ed2420cc4be2cf10fcd899c2106)
(cherry picked from commit bfa70d7e2ca6758901b680547fcfa9b24e0610b7)
(cherry picked from commit 06e52f5d58efc1491915822c2650f922673c82c6)
2025-09-14 14:55:49 +02:00
PJB3005
56eda3ea92 Version: 264.0.0 2025-06-27 22:03:33 +02:00
Tayrtahn
9dffd36319 Use non-generic TryComp to get MetaDataComponent in DebugAnchoringSystem (#6051) 2025-06-27 20:38:10 +02:00
Tayrtahn
a45b72a1c5 IRobustCloneable and generator support (#5692)
* Add IRobustCloneable and check for it in compnet generator.

* Redo compnetgenerator support; add test

* Disconnect client at end of test

* Actually test for client entities

* Cleanup

* Cleanup 2
2025-06-27 20:37:43 +02:00
Perry Fraser
bd0579ed6d fix: apply scale when calculating sprite bounding box (#6046)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
2025-06-26 23:23:57 +02:00
Pieter-Jan Briers
c73b54862e Add analyzers to detect some prototype misuse (#6048)
* Add analyzers to detect some prototype misuse

Detects people marking prototype as NetSerializable.

Detects people creating new prototype instances themselves.

* Update Robust.Analyzers/PrototypeNetSerializableAnalyzer.cs

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
2025-06-26 22:24:23 +02:00
PJB3005
6436ff8040 Fix prototype manager Index exceptions
Index<T> was documented to throw KeyNotFoundException, but actually threw UnknownPrototypeException. Index(Type type, string id) threw KeyNotFoundException.

This has now been made consistent to be UnknownPrototypeException everywhere.
2025-06-26 16:58:35 +02:00
PJB3005
98313ae369 Update NetSerializer submodule
Makes it report where broken serialization types come from.
2025-06-26 16:58:21 +02:00
PJB3005
0e63391203 Add PrototypeManagerExt.Index that takes nullable ProtoId<T> 2025-06-26 16:52:18 +02:00
wixoa
261bfaeeb8 Add AlwaysActive to WebViewControl (#6047) 2025-06-25 21:50:07 +02:00
Tayrtahn
4017e1f57e Make some PlacementManager dependency fields public (#6044)
* Make some PlacementManager dependency fields public

* Revert "Make some PlacementManager dependency fields public"

This reverts commit 99fe37b502.

* Now part of IPlacementManager
2025-06-23 22:48:25 +02:00
lzk
e170bf1ad2 genetive case (#6045)
* dative

* slipped it

* slipped it twice

* 1

* Update _engine_lib.ftl
2025-06-23 22:47:50 +02:00
PJB3005
da0abd2535 Make functions static to avoid delegate allocations in DataDefinitionAnalyzer. 2025-06-22 13:48:50 +02:00
44 changed files with 727 additions and 64 deletions

View File

@@ -57,7 +57,7 @@
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />

View File

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

View File

@@ -54,6 +54,44 @@ END TEMPLATE-->
*None yet*
## 264.0.2
## 264.0.1
## 264.0.0
### Breaking changes
* `IPrototypeManager.Index(Type kind, string id)` now throws `UnknownPrototypeException` instead of `KeyNotFoundException`, for consistency with `IPrototypeManager.Index<T>`.
### New features
* Types can now implement the new interface `IRobustCloneable<T>` to be cloned by the component state source generator.
* Added extra Roslyn Analyzers to detect some misuse of prototypes:
* Network serializing prototypes (tagging them with `[Serializable, NetSerializable]`).
* Constructing new instances of prototypes directly.
* Add `PrototypeManagerExt.Index` helper function that takes a nullable `ProtoId<T>`, returning null if the ID is null.
* Added an `AlwaysActive` field to `WebViewControl` to make a browser window active even when not in the UI tree.
* Made some common dependencies accessible through `IPlacementManager`.
* Added a new `GENITIVE()` localization helper function, which is useful for certain languages.
### Bugfixes
* Sprite scale is now correctly applied to sprite boundaries in `SpriteSystem.GetLocalBounds`.
* Fixed documentation for `IPrototypeManager.Index<T>` stating that `KeyNotFoundException` gets thrown, when in actuality `UnknownPrototypeException` gets thrown.
### Other
* More tiny optimizations to `DataDefinitionAnalyzer`.
* NetSerializer has been updated. On debug, it will now report *where* a type that can't be serialized is referenced from.
### Internal
* Minor internal code cleanup.
## 263.0.0
### Breaking changes

View File

@@ -21,7 +21,8 @@ zzzz-object-pronoun = { GENDER($ent) ->
}
# Used internally by the DAT-OBJ() function.
# Not used in en-US. Created for supporting other languages.
# Not used in en-US. Created to support other languages.
# (e.g., "to him," "for her")
zzzz-dat-object = { GENDER($ent) ->
[male] him
[female] her
@@ -29,6 +30,16 @@ zzzz-dat-object = { GENDER($ent) ->
*[neuter] it
}
# Used internally by the GENITIVE() function.
# Not used in en-US. Created to support other languages.
# e.g., "у него" (Russian), "seines Vaters" (German).
zzzz-genitive = { GENDER($ent) ->
[male] his
[female] her
[epicene] their
*[neuter] its
}
# Used internally by the POSS-PRONOUN() function.
zzzz-possessive-pronoun = { GENDER($ent) ->
[male] his

View File

@@ -0,0 +1,64 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeInstantiationAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(PrototypeInstantiationAnalyzer))]
public sealed class PrototypeInstantiationAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new RTAnalyzerTest<PrototypeInstantiationAnalyzer>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Prototypes.Attributes.cs",
"Robust.Shared.Prototypes.IPrototype.cs",
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
[Prototype]
public sealed class FooPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
}
public static class Bad
{
public static FooPrototype Real()
{
return new FooPrototype();
}
}
""";
await Verifier(code,
// /0/Test0.cs(15,16): warning RA0039: Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.
VerifyCS.Diagnostic().WithSpan(15, 16, 15, 34));
}
}

View File

@@ -0,0 +1,61 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
using VerifyCS =
Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Robust.Analyzers.PrototypeNetSerializableAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace Robust.Analyzers.Tests;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture]
[TestOf(typeof(PrototypeNetSerializableAnalyzer))]
public sealed class PrototypeNetSerializableAnalyzerTest
{
private static Task Verifier(string code, params DiagnosticResult[] expected)
{
var test = new RTAnalyzerTest<PrototypeNetSerializableAnalyzer>()
{
TestState =
{
Sources = { code }
},
};
TestHelper.AddEmbeddedSources(
test.TestState,
"Robust.Shared.Serialization.NetSerializableAttribute.cs",
"Robust.Shared.Prototypes.Attributes.cs",
"Robust.Shared.Prototypes.IPrototype.cs",
"Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs"
);
// ExpectedDiagnostics cannot be set, so we need to AddRange here...
test.TestState.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
[Test]
public async Task Test()
{
const string code = """
using System;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
[Prototype]
[Serializable, NetSerializable]
public sealed class FooPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
}
""";
await Verifier(code,
// /0/Test0.cs(7,21): warning RA0037: Type FooPrototype is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleNetSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"),
// /0/Test0.cs(7,21): warning RA0038: Type FooPrototype is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.
VerifyCS.Diagnostic(PrototypeNetSerializableAnalyzer.RuleSerializable).WithSpan(7, 21, 7, 33).WithArguments("FooPrototype"));
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
namespace Robust.Analyzers.Tests;
public sealed class RTAnalyzerTest<TAnalyzer> : CSharpAnalyzerTest<TAnalyzer, DefaultVerifier>
where TAnalyzer : DiagnosticAnalyzer, new()
{
protected override ParseOptions CreateParseOptions()
{
var baseOptions = (CSharpParseOptions) base.CreateParseOptions();
return baseOptions.WithPreprocessorSymbols("ROBUST_ANALYZERS_TEST");
}
}

View File

@@ -17,6 +17,10 @@
<EmbeddedResource Include="..\Robust.Shared\Analyzers\ObsoleteInheritanceAttribute.cs" LogicalName="Robust.Shared.Analyzers.ObsoleteInheritanceAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\IoC\DependencyAttribute.cs" LogicalName="Robust.Shared.IoC.DependencyAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\GameObjects\EventBusAttributes.cs" LogicalName="Robust.Shared.GameObjects.EventBusAttributes.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Serialization\NetSerializableAttribute.cs" LogicalName="Robust.Shared.Serialization.NetSerializableAttribute.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Prototypes\Attributes.cs" LogicalName="Robust.Shared.Prototypes.Attributes.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Prototypes\IPrototype.cs" LogicalName="Robust.Shared.Prototypes.IPrototype.cs" LinkBase="Implementations" />
<EmbeddedResource Include="..\Robust.Shared\Serialization\Manager\Attributes\DataFieldAttribute.cs" LogicalName="Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute.cs" LinkBase="Implementations" />
</ItemGroup>
<PropertyGroup>

View File

@@ -121,7 +121,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}, SymbolKind.NamedType);
}
private void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataDefinition(SyntaxNodeAnalysisContext context)
{
if (context.Node is not TypeDeclarationSyntax declaration)
return;
@@ -147,7 +147,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}
}
private void AnalyzeDataField(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataField(SyntaxNodeAnalysisContext context)
{
if (context.Node is not FieldDeclarationSyntax field)
return;
@@ -198,7 +198,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
}
}
private void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context)
{
if (context.Node is not PropertyDeclarationSyntax property)
return;

View File

@@ -0,0 +1,48 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PrototypeInstantiationAnalyzer : DiagnosticAnalyzer
{
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
public static readonly DiagnosticDescriptor Rule = new(
Diagnostics.IdPrototypeInstantiation,
"Do not instantiate prototypes directly",
"Do not instantiate prototypes directly. Prototypes should always be instantiated by the prototype manager.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(static ctx =>
{
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
if (prototypeInterface == null)
return;
ctx.RegisterOperationAction(symContext => Check(prototypeInterface, symContext), OperationKind.ObjectCreation);
});
}
private static void Check(INamedTypeSymbol prototypeInterface, OperationAnalysisContext ctx)
{
if (ctx.Operation is not IObjectCreationOperation { Type: { } resultType } creationOp)
return;
if (!TypeSymbolHelper.ImplementsInterface(resultType, prototypeInterface))
return;
ctx.ReportDiagnostic(Diagnostic.Create(Rule, creationOp.Syntax.GetLocation()));
}
}

View File

@@ -0,0 +1,76 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PrototypeNetSerializableAnalyzer : DiagnosticAnalyzer
{
private const string PrototypeInterfaceType = "Robust.Shared.Prototypes.IPrototype";
private const string NetSerializableAttributeType = "Robust.Shared.Serialization.NetSerializableAttribute";
public static readonly DiagnosticDescriptor RuleNetSerializable = new(
Diagnostics.IdPrototypeNetSerializable,
"Prototypes should not be [NetSerializable]",
"Type {0} is a prototype and marked as [NetSerializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
"Usage",
DiagnosticSeverity.Warning,
true);
public static readonly DiagnosticDescriptor RuleSerializable = new(
Diagnostics.IdPrototypeSerializable,
"Prototypes should not be [Serializable]",
"Type {0} is a prototype and marked as [Serializable]. Prototypes should not be directly sent over the network, send their IDs instead.",
"Usage",
DiagnosticSeverity.Warning,
true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [
RuleNetSerializable,
RuleSerializable
];
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(static ctx =>
{
var prototypeInterface = ctx.Compilation.GetTypeByMetadataName(PrototypeInterfaceType);
var netSerializableAttribute = ctx.Compilation.GetTypeByMetadataName(NetSerializableAttributeType);
if (prototypeInterface == null || netSerializableAttribute == null)
return;
ctx.RegisterSymbolAction(symbolContext => CheckClass(prototypeInterface, netSerializableAttribute, symbolContext), SymbolKind.NamedType);
});
}
private static void CheckClass(
INamedTypeSymbol prototypeInterface,
INamedTypeSymbol netSerializableAttribute,
SymbolAnalysisContext symbolContext)
{
if (symbolContext.Symbol is not INamedTypeSymbol symbol)
return;
if (!TypeSymbolHelper.ImplementsInterface(symbol, prototypeInterface))
return;
if (AttributeHelper.HasAttribute(symbol, netSerializableAttribute, out _))
{
symbolContext.ReportDiagnostic(
Diagnostic.Create(RuleNetSerializable, symbol.Locations[0], symbol.ToDisplayString()));
}
if (symbol.IsSerializable)
{
symbolContext.ReportDiagnostic(
Diagnostic.Create(RuleSerializable, symbol.Locations[0], symbol.ToDisplayString()));
}
}
}

View File

@@ -6,7 +6,7 @@ using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
{
public static class Program
internal static class Program
{
// This was supposed to be the main entry for the subprocess program... It doesn't work.
public static int Main(string[] args)

View File

@@ -162,9 +162,10 @@ namespace Robust.Client.WebView.Cef
}
}
public bool IsOpen => _data != null;
public bool IsLoading => _data?.Browser.IsLoading ?? false;
public void EnteredTree()
public void StartBrowser()
{
DebugTools.AssertNull(_data);
@@ -195,7 +196,7 @@ namespace Robust.Client.WebView.Cef
_data = new LiveData(texture, client, browser, renderer);
}
public void ExitedTree()
public void CloseBrowser()
{
DebugTools.AssertNotNull(_data);

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -24,6 +25,7 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameControllerInternal _gameController = default!;
[Dependency] private readonly IResourceManagerInternal _resourceManager = default!;
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -61,7 +63,10 @@ namespace Robust.Client.WebView.Cef
var cachePath = "";
if (_resourceManager.UserData is WritableDirProvider userData)
cachePath = userData.GetFullPath(new ResPath("/cef_cache"));
{
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
cachePath = Path.Combine(rootDir, "cef_cache", "0");
}
var settings = new CefSettings()
{

View File

@@ -81,11 +81,13 @@ namespace Robust.Client.WebView.Headless
private sealed class WebViewControlImplDummy : DummyBase, IWebViewControlImpl
{
public void EnteredTree()
public bool IsOpen => false;
public void StartBrowser()
{
}
public void ExitedTree()
public void CloseBrowser()
{
}

View File

@@ -9,8 +9,10 @@ namespace Robust.Client.WebView
/// </summary>
internal interface IWebViewControlImpl : IWebViewControl
{
void EnteredTree();
void ExitedTree();
public bool IsOpen { get; }
void StartBrowser();
void CloseBrowser();
void MouseMove(GUIMouseMoveEventArgs args);
void MouseExited();
void MouseWheel(GUIMouseWheelEventArgs args);

View File

@@ -14,6 +14,7 @@ namespace Robust.Client.WebView
[Dependency] private readonly IWebViewManagerInternal _webViewManager = default!;
private readonly IWebViewControlImpl _controlImpl;
private bool _alwaysActive;
[ViewVariables(VVAccess.ReadWrite)]
public string Url
@@ -22,6 +23,21 @@ namespace Robust.Client.WebView
set => _controlImpl.Url = value;
}
[ViewVariables(VVAccess.ReadWrite)]
public bool AlwaysActive
{
get => _alwaysActive;
set
{
_alwaysActive = value;
if (_alwaysActive && !_controlImpl.IsOpen)
_controlImpl.StartBrowser();
else if (!_alwaysActive && _controlImpl.IsOpen && !IsInsideTree)
_controlImpl.CloseBrowser();
}
}
[ViewVariables] public bool IsLoading => _controlImpl.IsLoading;
public WebViewControl()
@@ -39,14 +55,16 @@ namespace Robust.Client.WebView
{
base.EnteredTree();
_controlImpl.EnteredTree();
if (!_controlImpl.IsOpen)
_controlImpl.StartBrowser();
}
protected override void ExitedTree()
{
base.ExitedTree();
_controlImpl.ExitedTree();
if (!_alwaysActive)
_controlImpl.CloseBrowser();
}
protected internal override void MouseMove(GUIMouseMoveEventArgs args)

View File

@@ -82,7 +82,7 @@ namespace Robust.Client.Debugging
foreach (var ent in _mapSystem.GetAnchoredEntities(gridUid, grid, spot))
{
if (TryComp<MetaDataComponent>(ent, out var meta))
if (TryComp(ent, out MetaDataComponent? meta))
{
text.AppendLine($"uid: {ent}, {meta.EntityName}");
}

View File

@@ -387,7 +387,7 @@ namespace Robust.Client
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null, hideUserDataDir: true);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)

View File

@@ -32,7 +32,7 @@ public sealed partial class SpriteSystem
bounds = bounds.Union(GetLocalBounds(layer));
}
sprite.Comp._bounds = bounds;
sprite.Comp._bounds = bounds.Scale(sprite.Comp.Scale);
sprite.Comp.BoundsDirty = false;
return sprite.Comp._bounds;
}

View File

@@ -1,5 +1,8 @@
using System;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -18,6 +21,10 @@ namespace Robust.Client.Placement
PlacementMode? CurrentMode { get; set; }
PlacementInformation? CurrentPermission { get; set; }
IEntityManager EntityManager { get; }
IEyeManager EyeManager { get; }
IMapManager MapManager { get; }
/// <summary>
/// The direction to spawn the entity in (presently exposed for EntitySpawnWindow UI)
/// </summary>

View File

@@ -32,17 +32,21 @@ namespace Robust.Client.Placement
[Dependency] internal readonly IPlayerManager PlayerManager = default!;
[Dependency] internal readonly IResourceCache ResourceCache = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] internal readonly IMapManager MapManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] internal readonly IEyeManager EyeManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] internal readonly IInputManager InputManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] internal readonly IEntityManager EntityManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] internal readonly IClyde Clyde = default!;
public IEntityManager EntityManager => _entityManager;
public IEyeManager EyeManager => _eyeManager;
public IMapManager MapManager => _mapManager;
private ISawmill _sawmill = default!;
private SharedMapSystem Maps => EntityManager.System<SharedMapSystem>();

View File

@@ -43,4 +43,22 @@ public static class AttributeHelper
return defaultValue;
}
public static bool HasAttribute(
INamedTypeSymbol symbol,
INamedTypeSymbol attribute,
[NotNullWhen(true)] out AttributeData? matchedAttribute)
{
matchedAttribute = null;
foreach (var typeAttribute in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(typeAttribute.AttributeClass, attribute))
{
matchedAttribute = typeAttribute;
return true;
}
}
return false;
}
}

View File

@@ -40,6 +40,9 @@ public static class Diagnostics
public const string IdObsoleteInheritance = "RA0034";
public const string IdObsoleteInheritanceWithMessage = "RA0035";
public const string IdDataFieldYamlSerializable = "RA0036";
public const string IdPrototypeNetSerializable = "RA0037";
public const string IdPrototypeSerializable = "RA0038";
public const string IdPrototypeInstantiation = "RA0039";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");

View File

@@ -6,7 +6,7 @@ namespace Robust.Roslyn.Shared;
public static class TypeSymbolHelper
{
public static bool ShittyTypeMatch(INamedTypeSymbol type, string attributeMetadataName)
public static bool ShittyTypeMatch(ITypeSymbol type, string attributeMetadataName)
{
// Doing it like this only allocates when the type actually matches, which is good enough for me right now.
if (!attributeMetadataName.EndsWith(type.Name))
@@ -15,7 +15,7 @@ public static class TypeSymbolHelper
return type.ToDisplayString() == attributeMetadataName;
}
public static bool ImplementsInterface(INamedTypeSymbol type, string interfaceTypeName)
public static bool ImplementsInterface(ITypeSymbol type, string interfaceTypeName)
{
foreach (var interfaceType in type.AllInterfaces)
{
@@ -25,4 +25,15 @@ public static class TypeSymbolHelper
return false;
}
public static bool ImplementsInterface(ITypeSymbol type, INamedTypeSymbol interfaceType)
{
foreach (var @interface in type.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(@interface, interfaceType))
return true;
}
return false;
}
}

View File

@@ -297,7 +297,7 @@ namespace Robust.Server
: null;
// Set up the VFS
_resources.Initialize(dataDir);
_resources.Initialize(dataDir, hideUserDataDir: false);
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;

View File

@@ -35,6 +35,7 @@ namespace Robust.Shared.CompNetworkGenerator
private const string GlobalDictionaryName = "global::System.Collections.Generic.Dictionary<TKey, TValue>";
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet<T>";
private const string GlobalListName = "global::System.Collections.Generic.List<T>";
private const string GlobalIRobustCloneableName = "global::Robust.Shared.Serialization.IRobustCloneable";
private static readonly SymbolDisplayFormat FullNullableFormat =
FullyQualifiedFormat.WithMiscellaneousOptions(IncludeNullableReferenceTypeModifier);
@@ -375,7 +376,39 @@ namespace Robust.Shared.CompNetworkGenerator
stateFields.Append($@"
public {networkedType} {name} = default!;");
if (IsCloneType(type))
if (ImplementsInterface(type, GlobalIRobustCloneableName))
{
getField = $"component.{name}";
cast = $"({castString})";
var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString;
if (nullable)
{
handleStateSetters.Append($@"
component.{name} = state.{name} == null ? null! : state.{name}.Clone();");
deltaHandleFields.Append($@"
var {name}Value = {cast} {fieldHandleValue};
if ({name}Value == null)
component.{name} = null!;
else
component.{name} = {nullCast}({name}Value.Clone());");
shallowClone.Append($@"
{name} = this.{name},");
deltaApply.Add($"fullState.{name} = {name} == null ? null! : {name}.Clone();");
}
else
{
handleStateSetters.Append($@"
component.{name} = state.{name}.Clone();");
deltaHandleFields.Append($@"
component.{name} = {cast}({fieldHandleValue}.Clone());");
shallowClone.Append($@"
{name} = this.{name},");
deltaApply.Add($"fullState.{name} = {name}.Clone();");
}
}
else if (IsCloneType(type))
{
getField = $"component.{name}";
cast = $"({castString})";
@@ -758,5 +791,19 @@ public partial class {componentName}{deltaInterface}
_ => false
};
}
private static bool ImplementsInterface(ITypeSymbol type, string interfaceName)
{
foreach (var interfaceType in type.AllInterfaces)
{
if (interfaceType.ToDisplayString(FullyQualifiedFormat).Contains(interfaceName)
|| interfaceType.ConstructedFrom.ToDisplayString(FullyQualifiedFormat).Contains(interfaceName))
{
return true;
}
}
return false;
}
}
}

View File

@@ -60,9 +60,7 @@ namespace Robust.Shared.ContentPack
internal string GetPath(ResPath relPath)
{
return Path.GetFullPath(Path.Combine(_directory.FullName, relPath.ToRelativeSystemPath()))
// Sanitise platform-specific path and standardize it for engine use.
.Replace(Path.DirectorySeparatorChar, '/');
return PathHelpers.SafeGetResourcePath(_directory.FullName, relPath);
}
/// <inheritdoc />

View File

@@ -14,7 +14,11 @@ namespace Robust.Shared.ContentPack
/// The directory to use for user data.
/// If null, a virtual temporary file system is used instead.
/// </param>
void Initialize(string? userData);
/// <param name="hideUserDataDir">
/// If true, <see cref="IWritableDirProvider.RootDir"/> will be hidden on
/// <see cref="IResourceManager.UserData"/>.
/// </param>
void Initialize(string? userData, bool hideUserDataDir);
/// <summary>
/// Mounts a single stream as a content file. Useful for unit testing.

View File

@@ -13,7 +13,7 @@ namespace Robust.Shared.ContentPack
{
/// <summary>
/// The root path of this provider.
/// Can be null if it's a virtual provider.
/// Can be null if it's a virtual provider or the path is protected (e.g. on the client).
/// </summary>
string? RootDir { get; }

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
{
@@ -63,5 +64,27 @@ namespace Robust.Shared.ContentPack
!OperatingSystem.IsWindows()
&& !OperatingSystem.IsMacOS();
internal static string SafeGetResourcePath(string baseDir, ResPath path)
{
var relSysPath = path.ToRelativeSystemPath();
if (relSysPath.Contains("\\..") || relSysPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
var retPath = Path.GetFullPath(Path.Join(baseDir, relSysPath));
// better safe than sorry check
if (!retPath.StartsWith(baseDir))
{
// Allow path to match if it's just missing the directory separator at the end.
if (retPath != baseDir.TrimEnd(Path.DirectorySeparatorChar))
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return retPath;
}
}
}

View File

@@ -41,13 +41,13 @@ namespace Robust.Shared.ContentPack
public IWritableDirProvider UserData { get; private set; } = default!;
/// <inheritdoc />
public virtual void Initialize(string? userData)
public virtual void Initialize(string? userData, bool hideRootDir)
{
Sawmill = _logManager.GetSawmill("res");
if (userData != null)
{
UserData = new WritableDirProvider(Directory.CreateDirectory(userData));
UserData = new WritableDirProvider(Directory.CreateDirectory(userData), hideRootDir);
}
else
{
@@ -379,6 +379,10 @@ namespace Robust.Shared.ContentPack
{
var rootDir = loader.GetPath(new ResPath(@"/"));
// TODO: GET RID OF THIS.
// This code shouldn't be passing OS disk paths through ResPath.
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
yield return new ResPath(rootDir);
}
}

View File

@@ -10,17 +10,22 @@ namespace Robust.Shared.ContentPack
/// <inheritdoc />
internal sealed class WritableDirProvider : IWritableDirProvider
{
/// <inheritdoc />
private readonly bool _hideRootDir;
public string RootDir { get; }
string? IWritableDirProvider.RootDir => _hideRootDir ? null : RootDir;
/// <summary>
/// Constructs an instance of <see cref="WritableDirProvider"/>.
/// </summary>
/// <param name="rootDir">Root file system directory to allow writing.</param>
public WritableDirProvider(DirectoryInfo rootDir)
/// <param name="hideRootDir">If true, <see cref="IWritableDirProvider.RootDir"/> is reported as null.</param>
public WritableDirProvider(DirectoryInfo rootDir, bool hideRootDir)
{
// FullName does not have a trailing separator, and we MUST have a separator.
RootDir = rootDir.FullName + Path.DirectorySeparatorChar.ToString();
_hideRootDir = hideRootDir;
}
#region File Access
@@ -119,7 +124,7 @@ namespace Robust.Shared.ContentPack
throw new FileNotFoundException();
var dirInfo = new DirectoryInfo(GetFullPath(path));
return new WritableDirProvider(dirInfo);
return new WritableDirProvider(dirInfo, _hideRootDir);
}
/// <inheritdoc />
@@ -180,20 +185,7 @@ namespace Robust.Shared.ContentPack
path = path.Clean();
return GetFullPath(RootDir, path);
}
private static string GetFullPath(string root, ResPath path)
{
var relPath = path.ToRelativeSystemPath();
if (relPath.Contains("\\..") || relPath.Contains("/.."))
{
// Hard cap on any exploit smuggling a .. in there.
// Since that could allow leaving sandbox.
throw new InvalidOperationException($"This branch should never be reached. Path: {path}");
}
return Path.GetFullPath(Path.Combine(root, relPath));
return PathHelpers.SafeGetResourcePath(RootDir, path);
}
}
}

View File

@@ -23,6 +23,7 @@ namespace Robust.Shared.Localization
AddCtxFunction(bundle, "SUBJECT", FuncSubject);
AddCtxFunction(bundle, "OBJECT", FuncObject);
AddCtxFunction(bundle, "DAT-OBJ", FuncDatObj);
AddCtxFunction(bundle, "GENITIVE", FuncGenitive);
AddCtxFunction(bundle, "POSS-ADJ", FuncPossAdj);
AddCtxFunction(bundle, "POSS-PRONOUN", FuncPossPronoun);
AddCtxFunction(bundle, "REFLEXIVE", FuncReflexive);
@@ -212,6 +213,15 @@ namespace Robust.Shared.Localization
return new LocValueString(GetString("zzzz-dat-object", ("ent", args.Args[0])));
}
/// <summary>
/// Returns the respective genitive form (pronoun or possessive adjective) for the entity's gender.
/// This is used in languages with a genitive case to indicate possession or related relationships,
/// e.g., "у него" (Russian), "seines Vaters" (German).
private ILocValue FuncGenitive(LocArgs args)
{
return new LocValueString(GetString("zzzz-genitive", ("ent", args.Args[0])));
}
/// <summary>
/// Returns the respective possessive adjective (his, her, their, its) for the entity's gender.
/// </summary>

View File

@@ -1,6 +1,8 @@
using System;
#if !ROBUST_ANALYZERS_TEST
using JetBrains.Annotations;
using Robust.Shared.Serialization.Manager.Attributes;
#endif
namespace Robust.Shared.Prototypes;
@@ -9,10 +11,12 @@ namespace Robust.Shared.Prototypes;
/// To prevent needing to instantiate it because interfaces can't declare statics.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
#if !ROBUST_ANALYZERS_TEST
[BaseTypeRequired(typeof(IPrototype))]
[MeansImplicitUse]
[MeansDataDefinition]
[Virtual]
#endif
public class PrototypeAttribute : Attribute
{
/// <summary>
@@ -35,10 +39,12 @@ public class PrototypeAttribute : Attribute
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
#if !ROBUST_ANALYZERS_TEST
[BaseTypeRequired(typeof(IPrototype))]
[MeansImplicitUse]
[MeansDataDefinition]
[MeansDataRecord]
#endif
public sealed class PrototypeRecordAttribute : PrototypeAttribute
{
public PrototypeRecordAttribute(string type, int loadPriority = 1) : base(type, loadPriority)

View File

@@ -1,11 +1,8 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
#if !ROBUST_ANALYZERS_TEST
using Robust.Shared.ViewVariables;
using YamlDotNet.Core.Tokens;
using YamlDotNet.RepresentationModel;
#endif
namespace Robust.Shared.Prototypes
{
@@ -22,7 +19,10 @@ namespace Robust.Shared.Prototypes
/// An ID for this prototype instance.
/// If this is a duplicate, an error will be thrown.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)] string ID { get; }
#if !ROBUST_ANALYZERS_TEST
[ViewVariables(VVAccess.ReadOnly)]
#endif
string ID { get; }
}
public interface IInheritingPrototype

View File

@@ -91,7 +91,7 @@ public interface IPrototypeManager
/// <summary>
/// Index for a <see cref="IPrototype"/> by ID.
/// </summary>
/// <exception cref="KeyNotFoundException">
/// <exception cref="UnknownPrototypeException">
/// Thrown if the type of prototype is not registered.
/// </exception>
T Index<T>(string id) where T : class, IPrototype;
@@ -105,7 +105,7 @@ public interface IPrototypeManager
/// <summary>
/// Index for a <see cref="IPrototype"/> by ID.
/// </summary>
/// <exception cref="KeyNotFoundException">
/// <exception cref="UnknownPrototypeException">
/// Thrown if the ID does not exist or the kind of prototype is not registered.
/// </exception>
IPrototype Index(Type kind, string id);

View File

@@ -279,7 +279,14 @@ namespace Robust.Shared.Prototypes
throw new InvalidOperationException("No prototypes have been loaded yet.");
}
return _kinds[kind].Instances[id];
try
{
return _kinds[kind].Instances[id];
}
catch (KeyNotFoundException)
{
throw new UnknownPrototypeException(id, kind);
}
}
/// <inheritdoc />

View File

@@ -0,0 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
namespace Robust.Shared.Prototypes;
/// <summary>
/// Extension methods for working with <see cref="IPrototypeManager"/>.
/// </summary>
[PublicAPI]
public static class PrototypeManagerExt
{
/// <summary>
/// Index for a <see cref="IPrototype"/> by ID, returning null if the ID is null.
/// </summary>
/// <param name="prototypeManager"></param>
/// <param name="protoId">The prototype ID to look up.</param>
/// <typeparam name="T">The kind of prototype to look up.</typeparam>
/// <returns>The prototype, or null if <paramref name="protoId"/> is <see langword="null"/>.</returns>
/// <exception cref="UnknownPrototypeException">
/// Thrown if the prototype ID given is invalid.
/// </exception>
[return: NotNullIfNotNull(nameof(protoId))]
public static T? Index<T>(this IPrototypeManager prototypeManager, ProtoId<T>? protoId) where T : class, IPrototype
{
return protoId is null ? null : prototypeManager.Index(protoId.Value);
}
}

View File

@@ -0,0 +1,19 @@
namespace Robust.Shared.Serialization;
/// <summary>
/// Implementers of this interface will have their <see cref="Clone"/> method
/// called when generating component states. This can be useful for reference types
/// used as datafields to make copies of values instead of references.
/// </summary>
/// <typeparam name="T">
/// Type returned by the <see cref="Clone"/> method.
/// This should probably be the same Type as the implementer.
/// </typeparam>
public interface IRobustCloneable<T>
{
/// <summary>
/// Returns a new instance of <typeparamref name="T"/> with the same values as this instance.
/// </summary>
/// <returns>A new instance of <typeparamref name="T"/> with the same values as this instance.</returns>
T Clone();
}

View File

@@ -1,12 +1,16 @@
using System;
#if !ROBUST_ANALYZERS_TEST
using JetBrains.Annotations;
#endif
namespace Robust.Shared.Serialization.Manager.Attributes
{
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
#if !ROBUST_ANALYZERS_TEST
[MeansImplicitAssignment]
[MeansImplicitUse(ImplicitUseKindFlags.Assign)]
[Virtual]
#endif
public class DataFieldAttribute : DataFieldBaseAttribute
{
/// <summary>

View File

@@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class RobustCloneableTestClass : IRobustCloneable<RobustCloneableTestClass>
{
[DataField]
public int IntValue;
public RobustCloneableTestClass Clone()
{
return new RobustCloneableTestClass
{
IntValue = IntValue
};
}
}
[Serializable, NetSerializable]
[DataDefinition]
public partial struct RobustCloneableTestStruct : IRobustCloneable<RobustCloneableTestStruct>
{
[DataField]
public int IntValue;
public RobustCloneableTestStruct Clone()
{
return new RobustCloneableTestStruct
{
IntValue = IntValue
};
}
}
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class RobustCloneableTestComponent : Component
{
[DataField, AutoNetworkedField]
public RobustCloneableTestClass TestClass = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct TestStruct = new();
[DataField, AutoNetworkedField]
public RobustCloneableTestStruct? NullableTestStruct;
}
public sealed class RobustCloneableTest() : RobustIntegrationTest
{
[Test]
public async Task TestClone()
{
var server = StartServer();
var client = StartClient();
await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync());
var sEntMan = server.EntMan;
var sPlayerMan = server.ResolveDependency<ISharedPlayerManager>();
var cEntMan = client.EntMan;
var cNetMan = client.ResolveDependency<IClientNetManager>();
MapId mapId = default;
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out mapId);
var coords = new MapCoordinates(0, 0, mapId);
var uid = sEntMan.SpawnEntity(null, coords);
var comp = sEntMan.EnsureComponent<RobustCloneableTestComponent>(uid);
comp.TestClass.IntValue = 50;
comp.TestStruct.IntValue = 60;
comp.NullableTestStruct = new() { IntValue = 70 };
});
// Connect client.
Assert.DoesNotThrow(() => client.SetConnectTarget(server));
await client.WaitPost(() => cNetMan.ClientConnect(null!, 0, null!));
async Task RunTicks()
{
for (int i = 0; i < 10; i++)
{
await server.WaitRunTicks(1);
await client.WaitRunTicks(1);
}
}
await RunTicks();
EntityUid player = default;
await server.WaitPost(() =>
{
var coords = new MapCoordinates(0, 0, mapId);
player = sEntMan.SpawnEntity(null, coords);
var session = sPlayerMan.Sessions.First();
server.PlayerMan.SetAttachedEntity(session, player);
sPlayerMan.JoinGame(session);
});
await RunTicks();
await server.WaitAssertion(() =>
{
Assert.That(cNetMan.IsConnected, Is.True);
var ents = cEntMan.AllEntities<RobustCloneableTestComponent>().ToList();
Assert.That(ents, Has.Count.EqualTo(1));
var testEnt = ents[0];
Assert.That(testEnt.Comp.TestClass.IntValue, Is.EqualTo(50));
Assert.That(testEnt.Comp.TestStruct.IntValue, Is.EqualTo(60));
Assert.That(testEnt.Comp.NullableTestStruct, Is.Not.Null);
Assert.That(testEnt.Comp.NullableTestStruct!.Value.IntValue, Is.EqualTo(70));
});
// Disconnect client
await client.WaitPost(() => cNetMan.ClientDisconnect(""));
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
}
}

View File

@@ -24,7 +24,7 @@ namespace Robust.UnitTesting.Shared.Resources
_testDir = Directory.CreateDirectory(_testDirPath);
var subDir = Path.Combine(_testDirPath, "writable");
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir));
_dirProvider = new WritableDirProvider(Directory.CreateDirectory(subDir), hideRootDir: false);
}
[OneTimeTearDown]