Compare commits

...

21 Commits

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

* Make BUIs inject systems and entity queries

* Fix import

* We parallelize those

* RIDER I BEG YOU

* Mocked unit tests are my passion

* Perhaps we do not care about fractional milliseconds

* Forgor to make it debug only

* Use Parallel.For instead of ForEach

* Rider I am going to become the joker

* Fix EntMan resolve

* Now with lazy resolve technology

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

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

* lets see

* goida

---------

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

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

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

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

* Make EntitySystemManager use the new Value method
2026-01-28 21:26:01 +01:00
DrSmugleaf
aaf5003fcf Initialize ProfManager on the server (#6401) 2026-01-28 16:01:58 +01:00
PJB3005
3bec89aaa5 Fix MapCoordinate spawns using grid-relative rotation
This was an undocumented breaking change introduced by https://github.com/space-wizards/RobustToolbox/pull/5915. The behavior does not make much sense: you're specifying coordinates relative to the map, so the rotation should be relative to the map too.
2026-01-26 19:20:12 +01:00
31 changed files with 663 additions and 179 deletions

View File

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

View File

@@ -39,7 +39,9 @@ END TEMPLATE-->
### New features
*None yet*
* If a sandbox error is caused by a compiler-generated method, the engine will now attempt to point out which using code is responsible.
* Added `OrderedDictionary<TKey, TValue>` and `System.StringComparer` to the sandbox whitelist.
* Added more overloads to `MapLoaderSystem` taking `TextReader`/`TextWriter` where appropriate.
### Bugfixes
@@ -47,13 +49,32 @@ END TEMPLATE-->
### Other
*None yet*
* Public APIs involving `System.Random` have been obsoleted. Use `IRobustRandom`/`RobustRandom` and such instead.
### Internal
*None yet*
## 272.0.0
### Breaking changes
* Reversed an undocumented breaking change from `v267.3.0`: entity spawning with a `MapCoordinates` now takes the rotation as relative to the map again instead of relative to the grid the entity was attached to.
### New features
* Added `ProfManager.Value` guard method.
### Bugfixes
* Fixed `ValidateMemberAnalyzer` taking a ridiculous amount of compile time.
### Other
* `ProfManager` is now initialized on the server.
## 271.2.0
### New features

View File

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

View File

@@ -439,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)

View File

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

View File

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

View File

@@ -279,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
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -366,7 +366,8 @@ namespace Robust.Shared.GameObjects
&& meta.EntityLifeStage < EntityLifeStage.Terminating)
{
coords = new EntityCoordinates(gridUid, _mapSystem.WorldToLocal(gridUid, grid, coordinates.Position));
_xforms.SetCoordinates(newEntity, transform, coords, rotation, unanchor: false);
var relativeRotation = rotation - _xforms.GetWorldRotation(gridUid);
_xforms.SetCoordinates(newEntity, transform, coords, relativeRotation, unanchor: false);
}
else
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
@@ -192,6 +193,14 @@ namespace Robust.Shared.GameObjects
_systemTypes.Remove(baseType);
}
var queryMethod = typeof(EntityManager).GetMethod(nameof(EntityManager.GetEntityQuery), 1, [])!;
SystemDependencyCollection.RegisterBaseGenericLazy(
typeof(EntityQuery<>),
(queryType, dep) => queryMethod
.MakeGenericMethod(queryType.GetGenericArguments()[0])
.Invoke(dep.Resolve<IEntityManager>(), null)!
);
SystemDependencyCollection.BuildGraph();
foreach (var systemType in _systemTypes)
@@ -314,9 +323,10 @@ namespace Robust.Shared.GameObjects
try
{
#endif
var sw = ProfSampler.StartNew();
updReg.System.Update(frameTime);
_profManager.WriteValue(updReg.System.GetType().Name, sw);
using (_profManager.Value(updReg.System.GetType().Name))
{
updReg.System.Update(frameTime);
}
#if EXCEPTION_TOLERANCE
}
catch (Exception e)
@@ -341,9 +351,10 @@ namespace Robust.Shared.GameObjects
try
{
#endif
var sw = ProfSampler.StartNew();
system.FrameUpdate(frameTime);
_profManager.WriteValue(system.GetType().Name, sw);
using (_profManager.Value(system.GetType().Name))
{
system.FrameUpdate(frameTime);
}
#if EXCEPTION_TOLERANCE
}
catch (Exception e)

View File

@@ -104,7 +104,7 @@ namespace Robust.Shared.GameObjects
/// <param name="prototypeName">Name of the <see cref="EntityPrototype"/> to spawn.</param>
/// <param name="coordinates">Coordinates to place the newly spawned entity.</param>
/// <param name="overrides">Overrides to add or remove components that differ from the prototype.</param>
/// <param name="rotation">Local rotation to set the newly spawned entity to.</param>
/// <param name="rotation">World rotation to set the newly spawned entity to.</param>
/// <returns>A new uninitialized entity.</returns>
/// <remarks>If there is a grid at the <paramref name="coordinates"/>, the entity will be parented to the grid.
/// Otherwise, it will be parented to the map.</remarks>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -17,6 +18,11 @@ namespace Robust.Shared.IoC
public delegate T DependencyFactoryDelegate<out T>()
where T : class;
public delegate T DependencyFactoryBaseGenericLazyDelegate<out T>(
Type type,
IDependencyCollection services)
where T : class;
/// <inheritdoc />
internal sealed class DependencyCollection : IDependencyCollection
{
@@ -37,6 +43,12 @@ namespace Robust.Shared.IoC
/// </remarks>
private FrozenDictionary<Type, object> _services = FrozenDictionary<Type, object>.Empty;
/// <summary>
/// Dictionary that maps the types passed to <see cref="Resolve{T}()"/> to their implementation
/// for any types registered through <see cref="RegisterBaseGenericLazy"/>.
/// </summary>
private readonly ConcurrentDictionary<Type, object> _lazyServices = new();
// Start fields used for building new services.
/// <summary>
@@ -48,6 +60,8 @@ namespace Robust.Shared.IoC
private readonly Dictionary<Type, DependencyFactoryDelegateInternal<object>> _resolveFactories = new();
private readonly Queue<Type> _pendingResolves = new();
private readonly ConcurrentDictionary<Type, DependencyFactoryBaseGenericLazyDelegate<object>> _baseGenericLazyFactories = new();
private readonly object _serviceBuildLock = new();
// End fields for building new services.
@@ -79,8 +93,8 @@ namespace Robust.Shared.IoC
public IEnumerable<Type> GetRegisteredTypes()
{
return _parentCollection != null
? _services.Keys.Concat(_parentCollection.GetRegisteredTypes())
: _services.Keys;
? _services.Keys.Concat(_lazyServices.Keys).Concat(_parentCollection.GetRegisteredTypes())
: _services.Keys.Concat(_lazyServices.Keys);
}
public Type[] GetCachedInjectorTypes()
@@ -116,10 +130,7 @@ namespace Robust.Shared.IoC
FrozenDictionary<Type, object> services,
[MaybeNullWhen(false)] out object instance)
{
if (!services.TryGetValue(objectType, out instance))
return _parentCollection is not null && _parentCollection.TryResolveType(objectType, out instance);
return true;
return TryResolveType(objectType, (IReadOnlyDictionary<Type, object>) services, out instance);
}
private bool TryResolveType(
@@ -128,7 +139,16 @@ namespace Robust.Shared.IoC
[MaybeNullWhen(false)] out object instance)
{
if (!services.TryGetValue(objectType, out instance))
{
if (objectType.IsGenericType &&
_baseGenericLazyFactories.TryGetValue(objectType.GetGenericTypeDefinition(), out var factory))
{
instance = _lazyServices.GetOrAdd(objectType, type => factory(type, this));
return true;
}
return _parentCollection is not null && _parentCollection.TryResolveType(objectType, out instance);
}
return true;
}
@@ -267,7 +287,7 @@ namespace Robust.Shared.IoC
_pendingResolves.Enqueue(interfaceType);
}
}
private void CheckRegisterInterface(Type interfaceType, Type implementationType, bool overwrite)
{
lock (_serviceBuildLock)
@@ -312,15 +332,24 @@ namespace Robust.Shared.IoC
Register(type, implementation.GetType(), () => implementation, overwrite);
}
public void RegisterBaseGenericLazy(Type interfaceType, DependencyFactoryBaseGenericLazyDelegate<object> factory)
{
lock (_serviceBuildLock)
{
_baseGenericLazyFactories[interfaceType] = factory;
}
}
/// <inheritdoc />
public void Clear()
{
foreach (var service in _services.Values.OfType<IDisposable>().Distinct())
foreach (var service in _services.Values.Concat(_lazyServices.Values).OfType<IDisposable>().Distinct())
{
service.Dispose();
}
_services = FrozenDictionary<Type, object>.Empty;
_lazyServices.Clear();
lock (_serviceBuildLock)
{

View File

@@ -132,6 +132,15 @@ namespace Robust.Shared.IoC
/// </param>
void RegisterInstance(Type type, object implementation, bool overwrite = false);
/// <summary>
/// Adds a callback to be called when attempting to resolve an unresolved type that matches the specified
/// base generic type, making it accessible to <see cref="IDependencyCollection.Resolve{T}"/>.
/// This instance will only be created the first time that it is attempted to be resolved.
/// </summary>
/// <param name="genericType">The base generic type of the type that will be resolvable.</param>
/// <param name="factory">The callback to call to get an instance of the implementation for that generic type.</param>
void RegisterBaseGenericLazy(Type genericType, DependencyFactoryBaseGenericLazyDelegate<object> factory);
/// <summary>
/// Clear all services and types.
/// Use this between unit tests and on program shutdown.

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Random;
@@ -12,10 +13,12 @@ namespace Robust.Shared.Map
{
Tile GetVariantTile(string name, IRobustRandom random);
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
Tile GetVariantTile(string name, System.Random random);
Tile GetVariantTile(ITileDefinition tileDef, IRobustRandom random);
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
Tile GetVariantTile(ITileDefinition tileDef, System.Random random);
/// <summary>

View File

@@ -132,6 +132,14 @@ public sealed class ProfManager
/// <returns>The absolute position of the written log entry.</returns>
public long WriteValue(string text, long int64) => WriteValue(text, ProfData.Int64(int64));
/// <summary>
/// Make a guarded value for usage with using blocks.
/// </summary>
public ValueGuard Value(string text)
{
return new ValueGuard(this, text);
}
/// <summary>
/// Write the start of a new log group.
/// </summary>
@@ -250,4 +258,23 @@ public sealed class ProfManager
_mgr.WriteGroupEnd(_startIndex, _groupName, _sampler);
}
}
public readonly struct ValueGuard : IDisposable
{
private readonly ProfManager _mgr;
private readonly string _text;
private readonly ProfSampler _sampler;
public ValueGuard(ProfManager mgr, string text)
{
_mgr = mgr;
_text = text;
_sampler = ProfSampler.StartNew();
}
public void Dispose()
{
_mgr.WriteValue(_text, _sampler);
}
}
}

View File

@@ -94,7 +94,14 @@ public partial class PrototypeManager
private bool TryGetIds(FieldInfo field, [NotNullWhen(true)] out string[]? ids)
{
ids = null;
var value = field.GetValue(null);
if (field.DeclaringType?.IsGenericTypeDefinition == true)
{
// field's class has generic parameters, rethrow to say what the field is because
// c# is a great language and doesn't tell you anything in its exception in GetValue
throw new InvalidOperationException($"Field {field.FieldType} {field.Name} cannot be a static field inside a generic class");
}
object? value = field.GetValue(null);
if (value == null)
return false;

View File

@@ -176,8 +176,10 @@ public interface IRobustRandom
}
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static class RandomHelpers
{
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static void Shuffle<T>(this System.Random random, IList<T> list)
{
var n = list.Count;
@@ -189,24 +191,28 @@ public static class RandomHelpers
}
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static bool Prob(this System.Random random, double chance)
{
return random.NextDouble() < chance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static byte NextByte(this System.Random random, byte maxValue)
{
return NextByte(random, 0, maxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static byte NextByte(this System.Random random)
{
return NextByte(random, byte.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static byte NextByte(this System.Random random, byte minValue, byte maxValue)
{
return (byte)random.Next(minValue, maxValue);

View File

@@ -19,7 +19,13 @@ public static class RandomExtensions
/// <param name="σ">The standard deviation of the normal distribution.</param>
public static double NextGaussian(this IRobustRandom random, double μ = 0, double σ = 1)
{
return random.GetRandom().NextGaussian(μ, σ);
// https://stackoverflow.com/a/218600
var α = random.NextDouble();
var β = random.NextDouble();
var randStdNormal = Math.Sqrt(-2.0 * Math.Log(α)) * Math.Sin(2.0 * Math.PI * β);
return μ + σ * randStdNormal;
}
/// <summary>Picks a random element from a collection.</summary>
@@ -78,6 +84,7 @@ public static class RandomExtensions
/// Picks a random element from a set and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static T Pick<T>(this System.Random random, ICollection<T> collection)
{
var index = random.Next(collection.Count);
@@ -97,6 +104,7 @@ public static class RandomExtensions
/// Picks a random from a collection then removes it and returns it.
/// This is O(n) as it has to iterate the collection until the target index.
/// </summary>
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static T PickAndTake<T>(this System.Random random, ICollection<T> set)
{
var tile = Pick(random, set);
@@ -110,6 +118,7 @@ public static class RandomExtensions
/// <param name="random">The random object to generate the number from.</param>
/// <param name="μ">The average or "center" of the normal distribution.</param>
/// <param name="σ">The standard deviation of the normal distribution.</param>
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static double NextGaussian(this System.Random random, double μ = 0, double σ = 1)
{
// https://stackoverflow.com/a/218600
@@ -121,17 +130,21 @@ public static class RandomExtensions
return μ + σ * randStdNormal;
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static Angle NextAngle(this System.Random random) => NextFloat(random) * MathF.Tau;
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static Angle NextAngle(this System.Random random, Angle minAngle, Angle maxAngle)
{
DebugTools.Assert(minAngle < maxAngle);
return minAngle + (maxAngle - minAngle) * random.NextDouble();
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static Vector2 NextPolarVector2(this System.Random random, float minMagnitude, float maxMagnitude)
=> random.NextAngle().RotateVec(new Vector2(random.NextFloat(minMagnitude, maxMagnitude), 0));
[Obsolete("Exists as a method directly on IRobustRandom.")]
public static float NextFloat(this IRobustRandom random)
{
// This is pretty much the CoreFX implementation.
@@ -140,11 +153,13 @@ public static class RandomExtensions
return random.Next() * 4.6566128752458E-10f;
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static float NextFloat(this System.Random random)
{
return random.Next() * 4.6566128752458E-10f;
}
[Obsolete("Always use RobustRandom/IRobustRandom, System.Random does not provide any extra functionality.")]
public static float NextFloat(this System.Random random, float minValue, float maxValue)
=> random.NextFloat() * (maxValue - minValue) + minValue;

View File

@@ -4,15 +4,23 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Random;
/// <summary>
/// Wrapper for <see cref="Random"/>.
/// Provides random numbers, can be constructed in user code or used as a dependency in the form of
/// <see cref="IRobustRandom"/>. Methods that take RNG as input should take an IRobustRandom instead.
/// </summary>
/// <remarks>
/// This should not contain any logic, not directly related to calling specific methods of <see cref="Random"/>.
/// To write additional logic, attached to random roll, please create interface-implemented methods on <see cref="IRobustRandom"/>
/// or add it to <see cref="RandomExtensions"/>.
/// </remarks>
/// <example>
/// <code>
/// var myRng = new RobustRandom();
/// // Optionally, seed your RNG. By default, the RNG is seeded randomly.
/// myRng.SetSeed(17);
/// <br/>
/// var fairDiceRoll = myRng.Next(1, 6); // Will be 4 with this seed.
/// </code>
/// </example>
public sealed class RobustRandom : IRobustRandom
{
// This should not contain any logic, not directly related to calling specific methods of <see cref="Random"/>.
// To write additional logic, attached to random roll, please create interface-implemented methods on <see cref="IRobustRandom"/>
// or add it to <see cref="RandomExtensions"/>.
private System.Random _random = new();
public System.Random GetRandom() => _random;
@@ -24,7 +32,10 @@ public sealed class RobustRandom : IRobustRandom
public float NextFloat()
{
return _random.NextFloat();
// This is pretty much the CoreFX implementation.
// So credits to that.
// Except using float instead of double.
return Next() * 4.6566128752458E-10f;
}
public int Next()