Refactor map loading & saving (#5572)

* Refactor map loading & saving

* test fixes

* ISerializationManager tweaks

* Fix component composition

* Try fix entity deserialization component composition

* comments

* CL

* error preinit

* a

* cleanup

* error if version is too new

* Add AlwaysPushSerializationTest

* Add auto-inclusion test

* Better categorization

* Combine test components

* Save -> TrySave

Also better handling for saving multiple entities individually

* Create new partial class for map loading

* Add OrphanSerializationTest

* Include MapIds in BeforeSerializationEvent

* Addd LifetimeSerializationTest

* Add TestMixedLifetimeSerialization

* Add CategorizationTest

* explicitly serialize list of nullspace entities

* Add backwards compatibility test

* Version comments

also fixes wrong v4 format

* add MapMergeTest

* Add NetEntity support

* Optimize EntityDeserializer

Avoid unnecessary component deserialization

* fix assert & other bugs

* fucking containers strike again

* Fix deletion of pre-init entities

* fix release note merge conflict

* Update Robust.Shared/Map/MapManager.GridCollection.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* VV

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Leon Friedrich
2025-02-16 21:25:07 +11:00
committed by GitHub
parent 9d1b15ab4b
commit fbc706f37b
79 changed files with 6950 additions and 2193 deletions

View File

@@ -35,19 +35,30 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* `ITileDefinitionManager.AssignAlias` and general tile alias functionality has been removed. `TileAliasPrototype` still exist, but are only used during entity deserialization.
* `IMapManager.AddUninitializedMap` has been removed. Use the map-init options on `CreateMap()` instead.
* Re-using a MapId will now log a warning. This may cause some integration tests to fail if they are configured to fail
when warnings are logged.
* The minimum supported map format / version has been increased from 2 to 3.
* The server-side `MapLoaderSystem` and associated classes & structs has been moved to `Robust.Shared`, and has been significantly modified.
* The`TryLoad` and `Save` methods have been replaced with grid, map, generic entity variants. I.e, `SaveGrid`, `SaveMap`, and `SaveEntities`.
* Most of the serialization logic and methods have been moved out of `MapLoaderSystem` and into new `EntitySerializer`
and `EntityDeserializer` classes, which also replace the old `MapSerializationContext`.
* The `MapLoadOptions` class has been split into `MapLoadOptions`, `SerializationOptions`, and `DeserializationOptions`
structs.
### New features
*None yet*
* The current map format/version has increased from 6 to 7 and now contains more information to try support serialization of maps with null-space entities and full game saves.
* `IEntitySystemManager` now provides access to the system `IDependencyCollection`.
### Bugfixes
*None yet*
* Fixed entity deserialization for components with a data fields that have a AlwaysPushInheritance Attribute
### Other
*None yet*
* `MapChangedEvent` has been marked as obsolete, and should be replaced with `MapCreatedEvent` and `MapRemovedEvent.
### Internal

View File

@@ -17,7 +17,7 @@ public sealed class MapSystem : SharedMapSystem
{
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
var id = new MapId(--LastMapId);
while (MapManager.MapExists(id))
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(--LastMapId);
}

View File

@@ -1,14 +1,15 @@
using System.Linq;
using System.Numerics;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Server.Console.Commands
{
@@ -42,7 +43,7 @@ namespace Robust.Server.Console.Commands
return;
}
_ent.System<MapLoaderSystem>().Save(uid, args[1]);
_ent.System<MapLoaderSystem>().TrySaveGrid(uid, new ResPath(args[1]));
shell.WriteLine("Save successful. Look in the user data directory.");
}
@@ -63,7 +64,6 @@ namespace Robust.Server.Console.Commands
public sealed class LoadGridCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "loadgrid";
@@ -91,13 +91,14 @@ namespace Robust.Server.Console.Commands
return;
}
if (!_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (!sys.MapExists(mapId))
{
shell.WriteError("Target map does not exist.");
return;
}
var loadOptions = new MapLoadOptions();
Vector2 offset = default;
if (args.Length >= 4)
{
if (!float.TryParse(args[2], out var x))
@@ -112,9 +113,10 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.Offset = new Vector2(x, y);
offset = new Vector2(x, y);
}
Angle rot = default;
if (args.Length >= 5)
{
if (!float.TryParse(args[4], out var rotation))
@@ -123,9 +125,10 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.Rotation = Angle.FromDegrees(rotation);
rot = Angle.FromDegrees(rotation);
}
var opts = DeserializationOptions.Default;
if (args.Length >= 6)
{
if (!bool.TryParse(args[5], out var storeUids))
@@ -134,10 +137,11 @@ namespace Robust.Server.Console.Commands
return;
}
loadOptions.StoreMapUids = storeUids;
opts.StoreYamlUids = storeUids;
}
_system.GetEntitySystem<MapLoaderSystem>().Load(mapId, args[1], loadOptions);
var path = new ResPath(args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TryLoadGrid(mapId, path, out _, opts, offset, rot);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
@@ -149,7 +153,6 @@ namespace Robust.Server.Console.Commands
public sealed class SaveMap : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "savemap";
@@ -189,13 +192,14 @@ namespace Robust.Server.Console.Commands
if (mapId == MapId.Nullspace)
return;
if (!_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (!sys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-savemap-not-exist"));
return;
}
if (_map.IsMapInitialized(mapId) &&
if (sys.IsInitialized(mapId) &&
( args.Length < 3 || !bool.TryParse(args[2], out var force) || !force))
{
shell.WriteError(Loc.GetString("cmd-savemap-init-warning"));
@@ -203,7 +207,7 @@ namespace Robust.Server.Console.Commands
}
shell.WriteLine(Loc.GetString("cmd-savemap-attempt", ("mapId", mapId), ("path", args[1])));
_system.GetEntitySystem<MapLoaderSystem>().SaveMap(mapId, args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TrySaveMap(mapId, new ResPath(args[1]));
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
}
}
@@ -211,7 +215,6 @@ namespace Robust.Server.Console.Commands
public sealed class LoadMap : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IResourceManager _resource = default!;
public override string Command => "loadmap";
@@ -267,61 +270,49 @@ namespace Robust.Server.Console.Commands
return;
}
if (_map.MapExists(mapId))
var sys = _system.GetEntitySystem<SharedMapSystem>();
if (sys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-loadmap-exists", ("mapId", mapId)));
return;
}
var loadOptions = new MapLoadOptions();
float x = 0, y = 0;
if (args.Length >= 3)
float x = 0;
if (args.Length >= 3 && !float.TryParse(args[2], out x))
{
if (!float.TryParse(args[2], out x))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
if (args.Length >= 4)
float y = 0;
if (args.Length >= 4 && !float.TryParse(args[3], out y))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
var offset = new Vector2(x, y);
if (!float.TryParse(args[3], out y))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
float rotation = 0;
if (args.Length >= 5 && !float.TryParse(args[4], out rotation))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
return;
}
var rot = new Angle(rotation);
bool storeUids = false;
if (args.Length >= 6 && !bool.TryParse(args[5], out storeUids))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
return;
}
loadOptions.Offset = new Vector2(x, y);
var opts = new DeserializationOptions {StoreYamlUids = storeUids};
if (args.Length >= 5)
{
if (!float.TryParse(args[4], out var rotation))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[4])));
return;
}
var path = new ResPath(args[1]);
_system.GetEntitySystem<MapLoaderSystem>().TryLoadMapWithId(mapId, path, out _, out _, opts, offset, rot);
loadOptions.Rotation = new Angle(rotation);
}
if (args.Length >= 6)
{
if (!bool.TryParse(args[5], out var storeUids))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[5])));
return;
}
loadOptions.StoreMapUids = storeUids;
}
_system.GetEntitySystem<MapLoaderSystem>().TryLoad(mapId, args[1], out _, loadOptions);
if (_map.MapExists(mapId))
if (sys.MapExists(mapId))
shell.WriteLine(Loc.GetString("cmd-loadmap-success", ("mapId", mapId), ("path", args[1])));
else
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ namespace Robust.Server.GameObjects
protected override MapId GetNextMapId()
{
var id = new MapId(++LastMapId);
while (MapManager.MapExists(id))
while (MapExists(id) || UsedIds.Contains(id))
{
id = new MapId(++LastMapId);
}

View File

@@ -1,21 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Robust.Server.GameObjects
{
internal interface IServerEntityManagerInternal : IServerEntityManager
{
// These methods are used by the map loader to do multi-stage entity construction during map load.
// I would recommend you refer to the MapLoader for usage.
EntityUid AllocEntity(EntityPrototype? prototype);
void FinishEntityLoad(EntityUid entity, IEntityLoadContext? context = null);
void FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context = null);
void FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta = null);
void FinishEntityStartup(EntityUid entity);
}
}

View File

@@ -1,17 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects
{
/// <summary>
/// Metadata component used to keep consistent UIDs inside map files cross saving.
/// </summary>
/// <remarks>
/// This component stores the previous map UID of entities from map load.
/// This can then be used to re-serialize the entity with the same UID for the merge driver to recognize.
/// </remarks>
[RegisterComponent, UnsavedComponent]
public sealed partial class MapSaveIdComponent : Component
{
public int Uid { get; set; }
}
}

View File

@@ -27,7 +27,7 @@ namespace Robust.Server.GameObjects
/// Manager for entities -- controls things like template loading and instantiation
/// </summary>
[UsedImplicitly] // DI Container
public sealed class ServerEntityManager : EntityManager, IServerEntityManagerInternal
public sealed class ServerEntityManager : EntityManager, IServerEntityManager
{
private static readonly Gauge EntitiesCount = Metrics.CreateGauge(
"robust_entities_count",
@@ -61,32 +61,6 @@ namespace Robust.Server.GameObjects
_pvs = System<PvsSystem>();
}
EntityUid IServerEntityManagerInternal.AllocEntity(EntityPrototype? prototype)
{
return AllocEntity(prototype, out _);
}
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, IEntityLoadContext? context)
{
LoadEntity(entity, context);
}
void IServerEntityManagerInternal.FinishEntityLoad(EntityUid entity, EntityPrototype? prototype, IEntityLoadContext? context)
{
LoadEntity(entity, context, prototype);
}
void IServerEntityManagerInternal.FinishEntityInitialization(EntityUid entity, MetaDataComponent? meta)
{
InitializeEntity(entity, meta);
}
[Obsolete("Use StartEntity")]
void IServerEntityManagerInternal.FinishEntityStartup(EntityUid entity)
{
StartEntity(entity);
}
internal override EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
{
if (prototypeName == null)

View File

@@ -29,7 +29,8 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
base.Initialize();
EntityManager.EntityDeleted += OnDeleted;
_player.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<MapRemovedEvent>(OnMapRemoved);
SubscribeLocalEvent<MapCreatedEvent>(OnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(OnGridCreated);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
@@ -270,14 +271,6 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
#region Map/Grid Events
private void OnMapChanged(MapChangedEvent ev)
{
if (ev.Created)
OnMapCreated(ev);
else
OnMapDestroyed(ev);
}
private void OnGridRemoved(GridRemovalEvent ev)
{
RemoveForceSend(ev.EntityUid);
@@ -290,12 +283,12 @@ public sealed class PvsOverrideSystem : SharedPvsOverrideSystem
AddForceSend(ev.EntityUid);
}
private void OnMapDestroyed(MapChangedEvent ev)
private void OnMapRemoved(MapRemovedEvent ev)
{
RemoveForceSend(ev.Uid);
}
private void OnMapCreated(MapChangedEvent ev)
private void OnMapCreated(MapCreatedEvent ev)
{
// TODO PVS remove this requirement.
// I think this just required refactoring client game state logic so it doesn't sending maps/grids to nullspace.

View File

@@ -303,11 +303,8 @@ internal sealed partial class PvsSystem
RemoveRoot(ev.EntityUid);
}
private void OnMapChanged(MapChangedEvent ev)
private void OnMapChanged(MapRemovedEvent ev)
{
if (!ev.Destroyed)
return;
RemoveRoot(ev.Uid);
}

View File

@@ -127,7 +127,7 @@ internal sealed partial class PvsSystem : EntitySystem
_metaQuery = GetEntityQuery<MetaDataComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeLocalEvent<MapRemovedEvent>(OnMapChanged);
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
SubscribeLocalEvent<TransformComponent, TransformStartupEvent>(OnTransformStartup);

View File

@@ -1,12 +0,0 @@
using Robust.Shared.GameObjects;
namespace Robust.Server.Maps;
/// <summary>
/// Added to Maps that were loaded by MapLoaderSystem. If not present then this map was created externally.
/// </summary>
[RegisterComponent]
public sealed partial class LoadedMapComponent : Component
{
}

View File

@@ -1,59 +0,0 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
namespace Robust.Server.Maps
{
[PublicAPI]
public sealed class MapLoadOptions
{
/// <summary>
/// If true, UID components will be created for loaded entities
/// to maintain consistency upon subsequent savings.
/// </summary>
public bool StoreMapUids { get; set; }
/// <summary>
/// Offset to apply to the loaded objects.
/// </summary>
public Vector2 Offset
{
get => _offset;
set
{
TransformMatrix = Matrix3Helpers.CreateTransform(value, Rotation);
_offset = value;
}
}
private Vector2 _offset = Vector2.Zero;
/// <summary>
/// Rotation to apply to the loaded objects as a collective, around 0, 0.
/// </summary>
/// <remarks>Setting this overrides </remarks>
public Angle Rotation
{
get => _rotation;
set
{
TransformMatrix = Matrix3Helpers.CreateTransform(Offset, value);
_rotation = value;
}
}
private Angle _rotation = Angle.Zero;
public Matrix3x2 TransformMatrix { get; set; } = Matrix3x2.Identity;
/// <summary>
/// If there is a map entity serialized should we also load it.
/// </summary>
/// <remarks>
/// This should be set to false if you want to load a map file onto an existing map and do not wish to overwrite the existing entity.
/// </remarks>
public bool LoadMap { get; set; } = true;
public bool DoMapInit = false;
}
}

View File

@@ -67,7 +67,6 @@ namespace Robust.Server
deps.Register<IResourceManagerInternal, ResourceManager>();
deps.Register<EntityManager, ServerEntityManager>();
deps.Register<IServerEntityManager, ServerEntityManager>();
deps.Register<IServerEntityManagerInternal, ServerEntityManager>();
deps.Register<IServerGameStateManager, ServerGameStateManager>();
deps.Register<IReplayRecordingManager, ReplayRecordingManager>();
deps.Register<IReplayRecordingManagerInternal, ReplayRecordingManager>();

View File

@@ -59,7 +59,7 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
UpdatesAfter.Add(typeof(SharedTransformSystem));
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
SubscribeLocalEvent<MapChangedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<MapCreatedEvent>(MapManagerOnMapCreated);
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
@@ -143,11 +143,8 @@ public abstract class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
RemComp(uid, component);
}
private void MapManagerOnMapCreated(MapChangedEvent e)
private void MapManagerOnMapCreated(MapCreatedEvent e)
{
if (e.Destroyed || e.Map == MapId.Nullspace)
return;
EnsureComp<TTreeComp>(e.Uid);
}

View File

@@ -138,7 +138,7 @@ internal sealed class ListMapsCommand : LocalizedEntityCommands
{
var msg = new StringBuilder();
foreach (var mapId in _map.GetAllMapIds().OrderBy(id => id.Value))
foreach (var mapId in _mapSystem.GetAllMapIds().OrderBy(id => id.Value))
{
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
continue;

View File

@@ -0,0 +1,12 @@
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
namespace Robust.Shared.EntitySerialization.Components;
/// <summary>
/// Added to Maps that were loaded by <see cref="MapLoaderSystem"/>. If not present then this map was created externally.
/// </summary>
[RegisterComponent, UnsavedComponent]
public sealed partial class LoadedMapComponent : Component
{
}

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
namespace Robust.Server.GameObjects;
namespace Robust.Shared.EntitySerialization.Components;
/// <summary>
/// Used by <see cref="MapLoaderSystem"/> to track the original tile map from when a map was loaded.

View File

@@ -0,0 +1,20 @@
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.EntitySerialization.Components;
/// <summary>
/// This component is optionally added to entities that get loaded from yaml files. It stores the UID that the entity
/// had within the yaml file. This is used when saving the entity back to a yaml file so that it re-uses the same UID.
/// </summary>
/// <remarks>
/// This is primarily intended to reduce the diff sizes when modifying yaml maps. Note that there is no guarantee that
/// the given uid will be used when writing the entity. E.g., if more than one entity have this component with the
/// same uid, only one of those entities will be saved with the requested id.
/// </remarks>
[RegisterComponent, UnsavedComponent]
public sealed partial class YamlUidComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public int Uid;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,985 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Robust.Shared.Configuration;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.EntitySerialization;
/// <summary>
/// This class provides methods for serializing entities into yaml. It provides some more control over
/// serialization than the methods provided by <see cref="MapLoaderSystem"/>.
/// </summary>
/// <remarks>
/// There are several methods (e.g., <see cref="SerializeEntityRecursive"/> that serialize entities into a
/// per-entity <see cref="MappingDataNode"/> stored in the <see cref="EntityData"/> dictionary, which is indexed by the
/// entity's assigned yaml id (see <see cref="GetYamlUid"/>. The generated data can then be written to a larger yaml
/// document using the various "Write" methods. (e.g., <see cref="WriteEntitySection"/>). After a one has finished using
/// the generated data, the serializer needs to be reset (<see cref="Reset"/>) using it again to serialize other entities.
/// </remarks>
public sealed class EntitySerializer : ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
{
public const int MapFormatVersion = 7;
// v6->v7: PR #5572 - Added more metadata, List maps/grids/orphans, include some life-stage information
// v5->v6: PR #4307 - Converted Tile.TypeId from ushort to int
// v4->v5: PR #3992 - Removed name & author fields
// v3->v4: PR #3913 - Grouped entities by prototype
// v2->v3: PR #3468
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
[Dependency] public readonly EntityManager EntMan = default!;
[Dependency] public readonly IGameTiming Timing = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly ITileDefinitionManager _tileDef = default!;
[Dependency] private readonly IConfigurationManager _conf = default!;
[Dependency] private readonly ILogManager _logMan = default!;
private readonly ISawmill _log;
public readonly Dictionary<EntityUid, int> YamlUidMap = new();
public readonly HashSet<int> YamlIds = new();
public string? CurrentComponent { get; private set; }
public Entity<MetaDataComponent>? CurrentEntity { get; private set; }
public int CurrentEntityYamlUid { get; private set; }
/// <summary>
/// Tile ID -> yaml tile ID mapping.
/// </summary>
private readonly Dictionary<int, int> _tileMap = new();
private readonly HashSet<int> _yamlTileIds = new();
/// <inheritdoc/>
public bool WritingReadingPrototypes { get; private set; }
/// <summary>
/// If set, the serializer will refuse to serialize the given entity and will orphan any entity that is parented to
/// it. This is useful for serializing things like a grid (or multiple grids & entities) that are parented to a map
/// without actually serializing the map itself.
/// </summary>
public EntityUid Truncate { get; private set; }
/// <summary>
/// List of all entities that have previously been ignored via <see cref="Truncate"/>.
/// </summary>
/// <remarks>
/// This is tracked in case somebody does something weird, like trying to save a grid w/o its map, and then later on
/// including the map in the file. AFAIK, that should work in principle, though it would lead to a weird file where
/// the grid is orphaned and not on the map where it should be.
/// </remarks>
public readonly HashSet<EntityUid> Truncated = new();
public readonly SerializationOptions Options;
/// <summary>
/// Cached prototype data. This is used to avoid writing redundant data that is already specified in an entity's
/// prototype.
/// </summary>
public readonly Dictionary<string, Dictionary<string, MappingDataNode>> PrototypeCache = new();
/// <summary>
/// The serialized entity data.
/// </summary>
public readonly Dictionary<int, (EntityUid Uid, MappingDataNode Node)> EntityData = new();
/// <summary>
/// <see cref="EntityData"/> indices grouped by their entity prototype ids.
/// </summary>
public readonly Dictionary<string, List<int>> Prototypes = new();
/// <summary>
/// Yaml ids of all serialized map entities.
/// </summary>
public readonly List<int> Maps = new();
/// <summary>
/// Yaml ids of all serialized null-space entities.
/// This only includes entities that were initially in null-space, it does not include entities that were
/// serialized without their parents. Those are in <see cref="Orphans"/>.
/// </summary>
public readonly List<int> Nullspace = new();
/// <summary>
/// Yaml ids of all serialized grid entities.
/// </summary>
public readonly List<int> Grids = new();
/// <summary>
/// Yaml ids of all serialized entities in the file whose parents were not serialized. This does not include
/// entities that did not have a parent (e.g., maps or null-space entities). I.e., these are the entities that
/// need to be attached to a new parent when loading the file, unless you want to load them into null-space.
/// </summary>
public readonly List<int> Orphans = new();
private readonly string _metaName;
private readonly string _xformName;
private readonly MappingDataNode _emptyMetaNode;
private readonly MappingDataNode _emptyXformNode;
private int _nextYamlUid = 1;
private int _nextYamlTileId;
private readonly List<EntityUid> _autoInclude = new();
private readonly EntityQuery<YamlUidComponent> _yamlQuery;
private readonly EntityQuery<MapGridComponent> _gridQuery;
private readonly EntityQuery<MapComponent> _mapQuery;
private readonly EntityQuery<MetaDataComponent> _metaQuery;
private readonly EntityQuery<TransformComponent> _xformQuery;
/// <summary>
/// C# event for checking whether an entity is serializable. Can be used by content to prevent specific entities
/// from getting serialized.
/// </summary>
public event IsSerializableDelegate? OnIsSerializeable;
public delegate void IsSerializableDelegate(Entity<MetaDataComponent> ent, ref bool serializable);
public EntitySerializer(IDependencyCollection _dependency, SerializationOptions options)
{
_dependency.InjectDependencies(this);
_log = _logMan.GetSawmill("entity_serializer");
SerializerProvider.RegisterSerializer(this);
_metaName = _factory.GetComponentName(typeof(MetaDataComponent));
_xformName = _factory.GetComponentName(typeof(TransformComponent));
_emptyMetaNode = _serialization.WriteValueAs<MappingDataNode>(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this);
CurrentComponent = _xformName;
_emptyXformNode = _serialization.WriteValueAs<MappingDataNode>(typeof(TransformComponent), new TransformComponent(), alwaysWrite: true, context: this);
CurrentComponent = null;
_yamlQuery = EntMan.GetEntityQuery<YamlUidComponent>();
_gridQuery = EntMan.GetEntityQuery<MapGridComponent>();
_mapQuery = EntMan.GetEntityQuery<MapComponent>();
_metaQuery = EntMan.GetEntityQuery<MetaDataComponent>();
_xformQuery = EntMan.GetEntityQuery<TransformComponent>();
Options = options;
}
public bool IsSerializable(Entity<MetaDataComponent?> ent)
{
if (ent.Comp == null && !EntMan.TryGetComponent(ent.Owner, out ent.Comp))
return false;
if (ent.Comp.EntityPrototype?.MapSavable == false)
return false;
bool serializable = true;
OnIsSerializeable?.Invoke(ent!, ref serializable);
return serializable;
}
#region Serialize API
/// <summary>
/// Serialize a single entity. This does not automatically include
/// children, though depending on the setting of <see cref="SerializationOptions.MissingEntityBehaviour"/> it may
/// auto-include additional entities aside from the one provided.
/// </summary>
public void SerializeEntity(EntityUid uid)
{
if (!IsSerializable(uid))
throw new Exception($"{EntMan.ToPrettyString(uid)} is not serializable");
DebugTools.AssertNull(CurrentEntity);
ReserveYamlId(uid);
SerializeEntityInternal(uid);
DebugTools.AssertNull(CurrentEntity);
if (_autoInclude.Count != 0)
ProcessAutoInclude();
}
/// <summary>
/// Serialize a set of entities. This does not automatically include children or parents, though depending on the
/// setting of <see cref="SerializationOptions.MissingEntityBehaviour"/> it may auto-include additional entities
/// aside from the one provided.
/// </summary>
public void SerializeEntities(HashSet<EntityUid> entities)
{
foreach (var uid in entities)
{
if (!IsSerializable(uid))
throw new Exception($"{EntMan.ToPrettyString(uid)} is not serializable");
}
ReserveYamlIds(entities);
SerializeEntitiesInternal(entities);
}
/// <summary>
/// Serializes an entity and all of its serializable children. Note that this will not automatically serialize the
/// entity's parents.
/// </summary>
public void SerializeEntityRecursive(EntityUid root)
{
if (!IsSerializable(root))
throw new Exception($"{EntMan.ToPrettyString(root)} is not serializable");
Truncate = _xformQuery.GetComponent(root).ParentUid;
Truncated.Add(Truncate);
InitializeTileMap(root);
HashSet<EntityUid> entities = new();
RecursivelyIncludeChildren(root, entities);
ReserveYamlIds(entities);
SerializeEntitiesInternal(entities);
Truncate = EntityUid.Invalid;
}
#endregion
/// <summary>
/// Initialize the <see cref="_tileMap"/> that is used to serialize grid chunks using
/// <see cref="MapChunkSerializer"/>. This initialization just involves checking to see if any of the entities being
/// serialized were previously deserialized. If they were, it will re-use the old tile map. This is not actually required,
/// and is just meant to prevent large map file diffs when the internal tile ids change. I.e., you can serialize entities
/// without initializing the tile map.
/// </summary>
private void InitializeTileMap(EntityUid root)
{
if (!FindSavedTileMap(root, out var savedMap))
return;
// Note: some old maps were saved with duplicate id strings.
// I.e, multiple integers that correspond to the same prototype id.
// Hence the TryAdd()
//
// Though now we also need to use TryAdd in case InitializeTileMap() is called multiple times.
// E.g., if different grids get added separately to a single save file, in which case the
// tile map may already be partially populated.
foreach (var (origId, prototypeId) in savedMap)
{
if (_tileDef.TryGetDefinition(prototypeId, out var definition))
_tileMap.TryAdd(definition.TileId, origId);
}
}
private bool FindSavedTileMap(EntityUid root, [NotNullWhen(true)] out Dictionary<int, string>? map)
{
// Try and fetch the mapping directly
if (EntMan.TryGetComponent(root, out MapSaveTileMapComponent? comp))
{
map = comp.TileMap;
return true;
}
// iterate over all of its children and grab the first grid with a mapping
var xform = _xformQuery.GetComponent(root);
foreach (var child in xform._children)
{
if (!EntMan.TryGetComponent(child, out MapSaveTileMapComponent? cComp))
continue;
map = cComp.TileMap;
return true;
}
map = null;
return false;
}
#region AutoInclude
private void ProcessAutoInclude()
{
DebugTools.AssertEqual(_autoInclude.ToHashSet().Count, _autoInclude.Count);
var ents = new HashSet<EntityUid>();
switch (Options.MissingEntityBehaviour)
{
case MissingEntityBehaviour.PartialInclude:
// Include the entity and any of its direct parents
foreach (var uid in _autoInclude)
{
RecursivelyIncludeParents(uid, ents);
}
break;
case MissingEntityBehaviour.IncludeNullspace:
case MissingEntityBehaviour.AutoInclude:
// Find the root transform of all the included entities
var roots = new HashSet<EntityUid>();
foreach (var uid in _autoInclude)
{
GetRootNode(uid, roots);
}
// Recursively include all children of these root nodes.
foreach (var root in roots)
{
RecursivelyIncludeChildren(root, ents);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
_autoInclude.Clear();
SerializeEntitiesInternal(ents);
}
private void RecursivelyIncludeChildren(EntityUid uid, HashSet<EntityUid> ents)
{
if (!IsSerializable(uid))
return;
ents.Add(uid);
var xform = _xformQuery.GetComponent(uid);
foreach (var child in xform._children)
{
RecursivelyIncludeChildren(child, ents);
}
}
private void GetRootNode(EntityUid uid, HashSet<EntityUid> ents)
{
if (!IsSerializable(uid))
throw new NotSupportedException($"Attempted to auto-include an unserializable entity: {EntMan.ToPrettyString(uid)}");
var xform = _xformQuery.GetComponent(uid);
while (xform.ParentUid.IsValid() && xform.ParentUid != Truncate)
{
uid = xform.ParentUid;
xform = _xformQuery.GetComponent(uid);
if (!IsSerializable(uid))
throw new NotSupportedException($"Encountered an un-serializable parent entity: {EntMan.ToPrettyString(uid)}");
}
ents.Add(uid);
}
private void RecursivelyIncludeParents(EntityUid uid, HashSet<EntityUid> ents)
{
while (uid.IsValid() && uid != Truncate)
{
if (!ents.Add(uid))
break;
if (!IsSerializable(uid))
throw new NotSupportedException($"Encountered an un-serializable parent entity: {EntMan.ToPrettyString(uid)}");
uid = _xformQuery.GetComponent(uid).ParentUid;
}
}
#endregion
private void SerializeEntitiesInternal(HashSet<EntityUid> entities)
{
foreach (var uid in entities)
{
DebugTools.AssertNull(CurrentEntity);
SerializeEntityInternal(uid);
}
DebugTools.AssertNull(CurrentEntity);
if (_autoInclude.Count != 0)
ProcessAutoInclude();
}
/// <summary>
/// Serialize a single entity, and store the results in <see cref="EntityData"/>.
/// </summary>
private void SerializeEntityInternal(EntityUid uid)
{
var saveId = GetYamlUid(uid);
DebugTools.Assert(!EntityData.ContainsKey(saveId));
// It might be possible that something could cause an entity to be included twice.
// E.g., if someone serializes a grid w/o its map, and then tries to separately include the map and all its children.
// In that case, the grid would already have been serialized as a orphan.
// uhhh.... I guess its fine?
if (EntityData.ContainsKey(saveId))
return;
var meta = _metaQuery.GetComponent(uid);
var protoId = meta.EntityPrototype?.ID ?? string.Empty;
switch (meta.EntityLifeStage)
{
case <= EntityLifeStage.Initializing:
_log.Error($"Encountered an uninitialized entity: {EntMan.ToPrettyString(uid)}");
break;
case >= EntityLifeStage.Terminating:
_log.Error($"Encountered terminating or deleted entity: {EntMan.ToPrettyString(uid)}");
break;
}
CurrentEntityYamlUid = saveId;
CurrentEntity = (uid, meta);
Prototypes.GetOrNew(protoId).Add(saveId);
var xform = _xformQuery.GetComponent(uid);
if (_mapQuery.HasComp(uid))
Maps.Add(saveId);
else if (xform.ParentUid == EntityUid.Invalid)
Nullspace.Add(saveId);
if (_gridQuery.HasComp(uid))
{
// The current assumption is that grids cannot be in null-space, because the rest of the code
// (broadphase, etc) don't support grids without maps.
DebugTools.Assert(xform.ParentUid != EntityUid.Invalid || _mapQuery.HasComp(uid));
Grids.Add(saveId);
}
var entData = new MappingDataNode
{
{"uid", saveId.ToString(CultureInfo.InvariantCulture)}
};
EntityData[saveId] = (uid, entData);
var cache = GetProtoCache(meta.EntityPrototype);
// Store information about whether a given entity has been map-initialized.
// In principle, if a map has been map-initialized, then all entities on that map should also be map-initialized.
// But technically there is nothing that prevents someone from moving a post-init entity onto a pre-init map and vice-versa.
// Also, we need to record this information even if the map is not being serialized.
// In 99% of cases, this data is probably redundant and just bloats the file, but I can't think of a better way of handling it.
// At least it should only bloat post-init maps, which aren't really getting used so far.
if (meta.EntityLifeStage == EntityLifeStage.MapInitialized)
{
if (Options.ExpectPreInit)
_log.Error($"Expected all entities to be pre-mapinit, but encountered post-init entity: {EntMan.ToPrettyString(uid)}");
entData.Add("mapInit", "true");
// If an entity has been map-initialized, we assume it is un-paused.
// If it is paused, we have to specify it.
if (meta.EntityPaused)
entData.Add("paused", "true");
}
else
{
// If an entity has not yet been map-initialized, we assume it is paused.
// I don't know in what situations it wouldn't be, but might as well future proof this.
if (!meta.EntityPaused)
entData.Add("paused", "false");
}
var components = new SequenceDataNode();
if (xform.NoLocalRotation && xform.LocalRotation != 0)
{
_log.Error($"Encountered a no-rotation entity with non-zero local rotation: {EntMan.ToPrettyString(uid)}");
xform._localRotation = 0;
}
foreach (var component in EntMan.GetComponentsInternal(uid))
{
var compType = component.GetType();
var reg = _factory.GetRegistration(compType);
if (reg.Unsaved)
continue;
CurrentComponent = reg.Name;
MappingDataNode? compMapping;
MappingDataNode? protoMapping = null;
if (cache != null && cache.TryGetValue(reg.Name, out protoMapping))
{
// If this has a prototype, we need to use alwaysWrite: true.
// E.g., an anchored prototype might have anchored: true. If we we are saving an un-anchored
// instance of this entity, and if we have alwaysWrite: false, then compMapping would not include
// the anchored data-field (as false is the default for this bool data field), so the entity would
// implicitly be saved as anchored.
compMapping = _serialization.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: true, context: this);
// This will not recursively call Except() on the values of the mapping. It will only remove
// key-value pairs if both the keys and values are equal.
compMapping = compMapping.Except(protoMapping);
if(compMapping == null)
continue;
}
else
{
compMapping = _serialization.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: false, context: this);
}
// Don't need to write it if nothing was written! Note that if this entity has no associated
// prototype, we ALWAYS want to write the component, because merely the fact that it exists is
// information that needs to be written.
if (compMapping.Children.Count != 0 || protoMapping == null)
{
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
components.Add(compMapping);
}
}
CurrentComponent = null;
if (components.Count != 0)
entData.Add("components", components);
// TODO ENTITY SERIALIZATION
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
// want to serialize.
if (meta.EntityPrototype == null)
{
CurrentEntityYamlUid = 0;
CurrentEntity = null;
return;
}
// an entity may have less components than the original prototype, so we need to check if any are missing.
SequenceDataNode? missingComponents = null;
foreach (var (name, comp) in meta.EntityPrototype.Components)
{
// try comp instead of has-comp as it checks whether the component is supposed to have been
// deleted.
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
continue;
missingComponents ??= new();
missingComponents.Add(new ValueDataNode(name));
}
if (missingComponents != null)
entData.Add("missingComponents", missingComponents);
CurrentEntityYamlUid = 0;
CurrentEntity = null;
}
private Dictionary<string, MappingDataNode>? GetProtoCache(EntityPrototype? proto)
{
if (proto == null)
return null;
if (PrototypeCache.TryGetValue(proto.ID, out var cache))
return cache;
PrototypeCache[proto.ID] = cache = new(proto.Components.Count);
WritingReadingPrototypes = true;
foreach (var (compName, comp) in proto.Components)
{
CurrentComponent = compName;
cache.Add(compName, _serialization.WriteValueAs<MappingDataNode>(comp.Component.GetType(), comp.Component, alwaysWrite: true, context: this));
}
CurrentComponent = null;
WritingReadingPrototypes = false;
cache.TryAdd(_metaName, _emptyMetaNode);
cache.TryAdd(_xformName, _emptyXformNode);
return cache;
}
#region Write
public MappingDataNode Write()
{
DebugTools.AssertEqual(Maps.ToHashSet().Count, Maps.Count, "Duplicate maps?");
DebugTools.AssertEqual(Grids.ToHashSet().Count, Grids.Count, "Duplicate frids?");
DebugTools.AssertEqual(Orphans.ToHashSet().Count, Orphans.Count, "Duplicate orphans?");
DebugTools.AssertEqual(Nullspace.ToHashSet().Count, Nullspace.Count, "Duplicate nullspace?");
return new MappingDataNode
{
{"meta", WriteMetadata()},
{"maps", WriteIds(Maps)},
{"grids", WriteIds(Grids)},
{"orphans", WriteIds(Orphans)},
{"nullspace", WriteIds(Nullspace)},
{"tilemap", WriteTileMap()},
{"entities", WriteEntitySection()},
};
}
public MappingDataNode WriteMetadata()
{
return new MappingDataNode
{
{"format", MapFormatVersion.ToString(CultureInfo.InvariantCulture)},
{"category", GetCategory().ToString()},
{"engineVersion", _conf.GetCVar(CVars.BuildEngineVersion) },
{"forkId", _conf.GetCVar(CVars.BuildForkId)},
{"forkVersion", _conf.GetCVar(CVars.BuildVersion)},
{"time", DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)},
{"entityCount", EntityData.Count.ToString(CultureInfo.InvariantCulture)}
};
}
public SequenceDataNode WriteIds(List<int> ids)
{
var result = new SequenceDataNode();
foreach (var id in ids)
{
result.Add(new ValueDataNode(id.ToString(CultureInfo.InvariantCulture)));
}
return result;
}
/// <summary>
/// Serialize the <see cref="_tileMap"/> to yaml. This data is required to deserialize any serialized grid chunks using <see cref="MapChunkSerializer"/>.
/// </summary>
public MappingDataNode WriteTileMap()
{
var map = new MappingDataNode();
foreach (var (tileId, yamlTileId) in _tileMap.OrderBy(x => x.Key))
{
// This can come up if tests try to serialize test maps with custom / placeholder tile ids without registering them with the tile def manager..
if (!_tileDef.TryGetDefinition(tileId, out var def))
throw new Exception($"Attempting to serialize a tile {tileId} with no valid tile definition.");
var yamlId = yamlTileId.ToString(CultureInfo.InvariantCulture);
map.Add(yamlId, def.ID);
}
return map;
}
public SequenceDataNode WriteEntitySection()
{
if (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count)
{
// Maybe someone reserved a yaml id with ReserveYamlId() or implicitly with GetId() without actually
// ever serializing the entity, This can lead to references to non-existent entities.
throw new Exception($"Entity count mismatch");
}
var prototypes = new SequenceDataNode();
var protos = Prototypes.Keys.ToList();
protos.Sort(StringComparer.InvariantCulture);
foreach (var protoId in protos)
{
var entities = new SequenceDataNode();
var node = new MappingDataNode
{
{ "proto", protoId },
{ "entities", entities},
};
prototypes.Add(node);
var saveIds = Prototypes[protoId];
saveIds.Sort();
foreach (var saveId in saveIds)
{
var entData = EntityData[saveId].Node;
entities.Add(entData);
}
}
return prototypes;
}
/// <summary>
/// Get the category that the serialized data belongs to. If one was specified in the
/// <see cref="SerializationOptions"/> it will use that after validating it, otherwise it will attempt to infer a
/// category.
/// </summary>
public FileCategory GetCategory()
{
switch (Options.Category)
{
case FileCategory.Save:
return FileCategory.Save;
case FileCategory.Map:
return Maps.Count == 1 ? FileCategory.Map : FileCategory.Unknown;
case FileCategory.Grid:
if (Maps.Count > 0 || Grids.Count != 1)
return FileCategory.Unknown;
return FileCategory.Grid;
case FileCategory.Entity:
if (Maps.Count > 0 || Grids.Count > 0 || Orphans.Count != 1)
return FileCategory.Unknown;
return FileCategory.Entity;
default:
if (Maps.Count == 1)
{
// Contains a single map, and no orphaned entities that need reparenting.
if (Orphans.Count == 0)
return FileCategory.Map;
}
else if (Grids.Count == 1)
{
// Contains a single orphaned grid.
if (Orphans.Count == 1 && Grids[0] == Orphans[0])
return FileCategory.Grid;
}
else if (Orphans.Count == 1)
{
// A lone orphaned entity.
return FileCategory.Entity;
}
return FileCategory.Unknown;
}
}
#endregion
#region YamlIds
/// <summary>
/// Get (or allocate) the integer id that will be used in the serialized file to refer to the given entity.
/// </summary>
public int GetYamlUid(EntityUid uid)
{
return !YamlUidMap.TryGetValue(uid, out var id) ? AllocateYamlUid(uid) : id;
}
private int AllocateYamlUid(EntityUid uid)
{
if (Truncated.Contains(uid))
{
_log.Error(
"Including a previously truncated entity within the serialization process? Something probably wrong");
}
DebugTools.Assert(!YamlUidMap.ContainsKey(uid));
while (!YamlIds.Add(_nextYamlUid))
{
_nextYamlUid++;
}
YamlUidMap.Add(uid, _nextYamlUid);
return _nextYamlUid++;
}
/// <summary>
/// Get (or allocate) the integer id that will be used in the serialized file to refer to the given grid tile id.
/// </summary>
public int GetYamlTileId(int tileId)
{
if (_tileMap.TryGetValue(tileId, out var yamlId))
return yamlId;
return AllocateYamlTileId(tileId);
}
private int AllocateYamlTileId(int tileId)
{
while (!_yamlTileIds.Add(_nextYamlTileId))
{
_nextYamlTileId++;
}
_tileMap[tileId] = _nextYamlTileId;
return _nextYamlTileId++;
}
/// <summary>
/// This method ensures that the given entities have a yaml ids assigned. If the entities have a
/// <see cref="YamlUidComponent"/>, they will attempt to use that id, which exists to prevent large map file diffs
/// due to changing yaml ids.
/// </summary>
public void ReserveYamlIds(HashSet<EntityUid> entities)
{
List<EntityUid> needIds = new();
foreach (var uid in entities)
{
if (YamlUidMap.ContainsKey(uid))
continue;
if (_yamlQuery.TryGetComponent(uid, out var comp) && comp.Uid > 0 && YamlIds.Add(comp.Uid))
{
if (Truncated.Contains(uid))
{
_log.Error(
"Including a previously truncated entity within the serialization process? Something probably wrong");
}
YamlUidMap.Add(uid, comp.Uid);
}
else
{
needIds.Add(uid);
}
}
foreach (var uid in needIds)
{
AllocateYamlUid(uid);
}
}
/// <summary>
/// This method ensures that the given entity has a yaml id assigned to it. If the entity has a
/// <see cref="YamlUidComponent"/>, it will attempt to use that id, which exists to prevent large map file diffs due
/// to changing yaml ids.
/// </summary>
public void ReserveYamlId(EntityUid uid)
{
if (YamlUidMap.ContainsKey(uid))
return;
if (_yamlQuery.TryGetComponent(uid, out var comp) && comp.Uid > 0 && YamlIds.Add(comp.Uid))
{
if (Truncated.Contains(uid))
{
_log.Error(
"Including a previously truncated entity within the serialization process? Something probably wrong");
}
YamlUidMap.Add(uid, comp.Uid);
}
else
AllocateYamlUid(uid);
}
#endregion
#region ITypeSerializer
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
if (!int.TryParse(node.Value, out _))
return new ErrorNode(node, "Invalid EntityUid");
return new ValidatedValueNode(node);
}
public DataNode Write(
ISerializationManager serializationManager,
EntityUid value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (YamlUidMap.TryGetValue(value, out var yamlId))
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
if (CurrentComponent == _xformName)
{
if (value == EntityUid.Invalid)
return new ValueDataNode("invalid");
DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid));
Orphans.Add(CurrentEntityYamlUid);
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate)
_log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}");
return new ValueDataNode("invalid");
}
if (value == EntityUid.Invalid)
{
if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore)
_log.Error($"Encountered an invalid entityUid reference.");
return new ValueDataNode("invalid");
}
if (value == Truncate)
{
_log.Error(
$"{EntMan.ToPrettyString(CurrentEntity)}:{CurrentComponent} is attempting to serialize references to a truncated entity {EntMan.ToPrettyString(Truncate)}.");
}
switch (Options.MissingEntityBehaviour)
{
case MissingEntityBehaviour.Error:
_log.Error(EntMan.Deleted(value)
? $"Encountered a reference to a deleted entity {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}."
: $"Encountered a reference to a missing entity: {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}.");
return new ValueDataNode("invalid");
case MissingEntityBehaviour.Ignore:
return new ValueDataNode("invalid");
case MissingEntityBehaviour.IncludeNullspace:
if (!EntMan.TryGetComponent(value, out TransformComponent? xform)
|| xform.ParentUid != EntityUid.Invalid
|| _gridQuery.HasComp(value)
|| _mapQuery.HasComp(value))
{
goto case MissingEntityBehaviour.Error;
}
goto case MissingEntityBehaviour.AutoInclude;
case MissingEntityBehaviour.PartialInclude:
case MissingEntityBehaviour.AutoInclude:
if (Options.LogAutoInclude is {} level)
_log.Log(level, $"Auto-including entity {EntMan.ToPrettyString(value)} referenced by {EntMan.ToPrettyString(CurrentEntity)}");
_autoInclude.Add(value);
var id = GetYamlUid(value);
return new ValueDataNode(id.ToString(CultureInfo.InvariantCulture));
default:
throw new ArgumentOutOfRangeException();
}
}
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<EntityUid>? _)
{
return node.Value == "invalid" ? EntityUid.Invalid : EntityUid.Parse(node.Value);
}
public ValidationNode Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
if (node.Value == "invalid")
return new ValidatedValueNode(node);
if (!int.TryParse(node.Value, out _))
return new ErrorNode(node, "Invalid NetEntity");
return new ValidatedValueNode(node);
}
public NetEntity Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
{
return node.Value == "invalid" ? NetEntity.Invalid : NetEntity.Parse(node.Value);
}
public DataNode Write(
ISerializationManager serializationManager,
NetEntity value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
var uid = EntMan.GetEntity(value);
return serializationManager.WriteValue(uid, alwaysWrite, context);
}
#endregion
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
namespace Robust.Shared.EntitySerialization;
/// <summary>
/// Class containing information about entities that were loaded from a yaml file.
/// </summary>
public sealed class LoadResult
{
/// <summary>
/// The file format version.
/// </summary>
public int Version;
/// <summary>
/// The category of the file that was loaded in.
/// This might not match the actual final result. E.g., when loading in a grid file, a map may automatically gets
/// generated for it via <see cref="EntityDeserializer.AdoptGrids"/>.
/// </summary>
public FileCategory Category = FileCategory.Unknown;
/// <summary>
/// The engine version that was used to write the file. See <see cref="CVars.BuildEngineVersion"/>.
/// </summary>
public string? EngineVersion;
/// <summary>
/// The fork that was used to write the file. See <see cref="CVars.BuildForkId"/>.
/// </summary>
public string? ForkId;
/// <summary>
/// The fork version that was used to write the file. See <see cref="CVars.BuildVersion"/>.
/// </summary>
public string? ForkVersion;
/// <summary>
/// The <see cref="DateTime.UtcNow"/> when the file was created.
/// </summary>
public DateTime? Time;
/// <summary>
/// Set of all entities that were created while the file was being loaded.
/// </summary>
public readonly HashSet<EntityUid> Entities = new();
/// <summary>
/// Set of entities that are not parented to other entities. This will be a combination of <see cref="Maps"/>,
/// <see cref="Orphans"/>, and <see cref="NullspaceEntities"/>.
/// </summary>
public readonly HashSet<EntityUid> RootNodes = new();
public readonly HashSet<Entity<MapComponent>> Maps = new();
public readonly HashSet<Entity<MapGridComponent>> Grids = new();
/// <summary>
/// Deserialized entities that need to be assigned a new parent. These differ from "true" null-space entities.
/// E,g, saving a grid without saving the map would make the grid an "orphan".
/// </summary>
public readonly HashSet<EntityUid> Orphans = new();
/// <summary>
/// List of null-space entities. This contains all entities without a parent that don't have a
/// <see cref="MapComponent"/>, and were not listed as orphans
/// </summary>
public readonly HashSet<EntityUid> NullspaceEntities = new();
}

View File

@@ -14,19 +14,25 @@ using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Utility;
namespace Robust.Server.Maps;
namespace Robust.Shared.EntitySerialization;
[TypeSerializer]
internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingDataNode>, ITypeCopyCreator<MapChunk>
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
public ValidationNode Validate(
ISerializationManager serializationManager,
MappingDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
throw new NotImplementedException();
}
public MapChunk Read(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<MapChunk>? instantiationDelegate = null)
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<MapChunk>? instantiationDelegate = null)
{
var ind = (Vector2i) serializationManager.Read(typeof(Vector2i), node["ind"], hookCtx, context)!;
var tileNode = (ValueDataNode)node["tiles"];
@@ -50,10 +56,8 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
IReadOnlyDictionary<int, string>? tileMap = null;
if (context is MapSerializationContext serContext)
{
if (context is EntityDeserializer serContext)
tileMap = serContext.TileMap;
}
if (tileMap == null)
{
@@ -104,16 +108,12 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
root.Add("version", new ValueDataNode("6"));
Dictionary<int, int>? tileWriteMap = null;
if (context is MapSerializationContext mapContext)
tileWriteMap = mapContext.TileWriteMap;
gridNode.Value = SerializeTiles(value, tileWriteMap);
gridNode.Value = SerializeTiles(value, context as EntitySerializer);
return root;
}
private static string SerializeTiles(MapChunk chunk, Dictionary<int, int>? tileWriteMap)
private static string SerializeTiles(MapChunk chunk, EntitySerializer? serializer)
{
// number of bytes written per tile, because sizeof(Tile) is useless.
const int structSize = 6;
@@ -124,17 +124,34 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
using (var stream = new MemoryStream(barr))
using (var writer = new BinaryWriter(stream))
{
if (serializer == null)
{
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var tile = chunk.GetTile(x, y);
writer.Write(tile.TypeId);
writer.Write((byte) tile.Flags);
writer.Write(tile.Variant);
}
}
return Convert.ToBase64String(barr);
}
var lastTile = -1;
var yamlId = -1;
for (ushort y = 0; y < chunk.ChunkSize; y++)
{
for (ushort x = 0; x < chunk.ChunkSize; x++)
{
var tile = chunk.GetTile(x, y);
var typeId = tile.TypeId;
if (tileWriteMap != null)
typeId = tileWriteMap[typeId];
if (tile.TypeId != lastTile)
yamlId = serializer.GetYamlTileId(tile.TypeId);
writer.Write(typeId);
writer.Write((byte)tile.Flags);
lastTile = tile.TypeId;
writer.Write(yamlId);
writer.Write((byte) tile.Flags);
writer.Write(tile.Variant);
}
}
@@ -143,8 +160,12 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
return Convert.ToBase64String(barr);
}
public MapChunk CreateCopy(ISerializationManager serializationManager, MapChunk source,
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
public MapChunk CreateCopy(
ISerializationManager serializationManager,
MapChunk source,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null)
{
var mapManager = dependencies.Resolve<IMapManager>();
mapManager.SuppressOnTileChanged = true;

View File

@@ -0,0 +1,139 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Shared.EntitySerialization;
public record struct SerializationOptions
{
public static readonly SerializationOptions Default = new();
/// <summary>
/// What to do when serializing the EntityUid of an entity that is not one of entities currently being serialized.
/// I.e., What should happen when serializing a map that has entities with components that store references to a
/// null-space entity? Note that this does not affect the treatment of <see cref="TransformComponent.ParentUid"/>,
/// which will never auto-include parents.
/// </summary>
public MissingEntityBehaviour MissingEntityBehaviour = MissingEntityBehaviour.IncludeNullspace;
/// <summary>
/// Whether or not to log an error when serializing an entity without its parent.
/// </summary>
public bool ErrorOnOrphan = true;
/// <summary>
/// Log level to use when auto-including entities while serializing. Null implies no logs.
/// See <see cref="MissingEntityBehaviour"/>.
/// </summary>
public LogLevel? LogAutoInclude = LogLevel.Info;
/// <summary>
/// If true, the serializer will log an error if it encounters a post map-init entity.
/// </summary>
public bool ExpectPreInit;
public FileCategory Category;
public SerializationOptions()
{
}
}
public record struct DeserializationOptions()
{
public static readonly DeserializationOptions Default = new();
/// <summary>
/// If true, each loaded entity will get a <see cref="YamlUidComponent"/> that stores the uid that the entity
/// had in the yaml file. This is used to maintain consistent entity labelling on subsequent saves.
/// </summary>
public bool StoreYamlUids = false;
/// <summary>
/// If true, all maps that get created while loading this file will get map-initialized.
/// </summary>
public bool InitializeMaps = false;
/// <summary>
/// If true, all maps that get created while loading this file will get paused.
/// Note that the converse is not true, paused maps will not get un-paused if this is false.
/// Pre-mapinit maps are assumed to be paused.
/// </summary>
public bool PauseMaps = false;
/// <summary>
/// Whether or not to log an error when starting up a grid entity that has no map.
/// This usually indicates that someone is attempting to load an incorrect file type (e.g. loading a grid as a map).
/// </summary>
public bool LogOrphanedGrids = true;
/// <summary>
/// Whether or not to log an error when encountering an yaml entity id.
/// <see cref="TransformComponent.ParentUid"/> is exempt from this.
/// </summary>
public bool LogInvalidEntities = true;
/// <summary>
/// Whether or not to automatically assign map ids to any deserialized map entities.
/// If false, maps need to be manually given ids before entities are initialized.
/// </summary>
public bool AssignMapids = true;
}
/// <summary>
/// Superset of <see cref="EntitySerialization.DeserializationOptions"/> that contain information relevant to loading
/// maps & grids, potentially onto other existing maps.
/// </summary>
public struct MapLoadOptions()
{
public static readonly MapLoadOptions Default = new();
/// <summary>
/// If specified, all orphaned entities and the children of all loaded maps will be re-parented onto this map.
/// I.e., this will merge map contents onto an existing map. This will also cause any maps that get loaded to
/// delete themselves after their children have been moved.
/// </summary>
/// <remarks>
/// Note that this option effectively causes <see cref="DeserializationOptions.InitializeMaps"/> and
/// <see cref="DeserializationOptions.PauseMaps"/> to have no effect, as the target map is not a map that was
/// created by the deserialization.
/// </remarks>
public MapId? MergeMap = null;
/// <summary>
/// Offset to apply to the position of any loaded entities that are directly parented to a map.
/// </summary>
public Vector2 Offset;
/// <summary>
/// Rotation to apply to the position & local rotation of any loaded entities that are directly parented to a map.
/// </summary>
public Angle Rotation;
/// <summary>
/// Options to use when deserializing entities.
/// </summary>
public DeserializationOptions DeserializationOptions = DeserializationOptions.Default;
/// <summary>
/// When loading a single map, this will attempt to force the map to use the given map id. Generally, it is better
/// to allow the map system to auto-allocate a map id, to avoid accidentally re-using an old id.
/// </summary>
public MapId? ForceMapId;
/// <summary>
/// The expected <see cref="LoadResult.Category"/> for the file currently being read in, at the end of the entity
/// creation step. Will log errors if the category doesn't match the expected one (e.g., trying to load a "map" from a file
/// that doesn't contain any map entities).
/// </summary>
/// <remarks>
/// Note that the effective final category may change by the time the file has fully loaded. E.g., when loading a
/// file containing an orphaned grid, a map may be automatically created for the grid, but the category will still
/// be <see cref="FileCategory.Grid"/>
/// </remarks>
public FileCategory? ExpectedCategory;
}

View File

@@ -0,0 +1,88 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Upload;
namespace Robust.Shared.EntitySerialization;
/// <summary>
/// This enum is used to indicate the type of entity data that was written to a file. The actual format of the file does
/// not change, but it helps avoid mistakes like accidentally using a map file when trying to load a single grid.
/// </summary>
public enum FileCategory : byte
{
Unknown,
/// <summary>
/// File should contain a single orphaned entity, its children, and maybe some null-space entities.
/// </summary>
Entity,
/// <summary>
/// File should contain a single grid, its children, and maybe some null-space entities.
/// </summary>
Grid,
/// <summary>
/// File should contain a single map, its children, and maybe some null-space entities.
/// </summary>
Map,
/// <summary>
/// File is a full game save, and will likely contain at least one map and a few null-space entities.
/// </summary>
/// <remarks>
/// The file might also contain additional yaml entries for things like prototypes uploaded via
/// <see cref="IGamePrototypeLoadManager"/>, and might contain references to additional resources that need to be
/// loaded (e.g., files uploaded using <see cref="SharedNetworkResourceManager"/>).
/// </remarks>
Save,
}
public enum MissingEntityBehaviour
{
/// <summary>
/// Log an error and replace the reference with <see cref="EntityUid.Invalid"/>
/// </summary>
Error,
/// <summary>
/// Ignore the reference, replace it with <see cref="EntityUid.Invalid"/>
/// </summary>
Ignore,
/// <summary>
/// Automatically include & serialize any referenced null-space entities and their children.
/// I.e., entities that are not attached to any parent and are not maps. Any non-nullspace entities will result in
/// an error.
/// </summary>
/// <remarks>
/// This is primarily intended to make it easy to auto-include information carrying null-space entities. E.g., the
/// "minds" of players, or entities that represent power or gas networks on a grid. Note that a full game save
/// should still try to explicitly include all relevant entities, as this could still easily fail to auto-include
/// relevant entities if they are not explicitly referenced in a data-field by some other entity.
/// </remarks>
IncludeNullspace,
/// <summary>
/// Automatically include & serialize any referenced entity. Note that this means that the missing entity's
/// parents will (generally) also be included, however this will not include other children. E.g., if serializing a
/// grid that references an entity on the map, this will also cause the map to get serialized, but will not necessarily
/// serialize everything on the map.
/// </summary>
/// <remarks>
/// If trying to serialize an entity without its parent (i.e., its parent is truncated via
/// <see cref="EntitySerializer.Truncate"/>), this will try to respect that. E.g., if a referenced entity is on the
/// same map as a grid that is getting serialized, it should include the entity without including the map.
/// </remarks>
/// <remarks>
/// Note that this might unexpectedly change the <see cref="FileCategory"/>. I.e., trying to serialize a grid might
/// accidentally lead to serializing a (partial?) map file.
/// </remarks>
PartialInclude,
/// <summary>
/// Variant of <see cref="PartialInclude"/> that will also automatically include the children of any entities that
/// that are automatically included. Note that because auto-inclusion generally needs to include an entity's
/// parents, this will include more than just the missing entity's direct children.
/// </summary>
AutoInclude,
}

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
using Vector2 = System.Numerics.Vector2;
namespace Robust.Shared.EntitySerialization.Systems;
// This partial class file contains methods for loading generic entities and grids. Map specific methods are in another
// file
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.
/// </summary>
public bool TryLoadGeneric(
ResPath file,
[NotNullWhen(true)] out HashSet<Entity<MapComponent>>? maps,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
MapLoadOptions? options = null)
{
grids = null;
maps = null;
if (!TryLoadGeneric(file, out var data, options))
return false;
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.
/// </summary>
/// <param name="file">The file to load.</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(ResPath file, [NotNullWhen(true)] out LoadResult? result, MapLoadOptions? options = null)
{
result = null;
if (!TryReadFile(file, out var data))
return false;
_stopwatch.Restart();
var ev = new BeforeEntityReadEvent();
RaiseLocalEvent(ev);
var opts = options ?? MapLoadOptions.Default;
// If we are forcing a map id, we cannot auto-assign ids.
opts.DeserializationOptions.AssignMapids = opts.ForceMapId == null;
if (opts.MergeMap is { } targetId && !_mapSystem.MapExists(targetId))
throw new Exception($"Target map {targetId} does not exist");
if (opts.MergeMap != null && opts.ForceMapId != null)
throw new Exception($"Invalid combination of MapLoadOptions");
if (_mapSystem.MapExists(opts.ForceMapId))
throw new Exception($"Target map already exists");
// Using a local deserializer instead of a cached value, both to ensure that we don't accidentally carry over
// data from a previous serializations, and because some entities cause other maps/grids to be loaded during
// during mapinit.
var deserializer = new EntityDeserializer(
_dependency,
data,
opts.DeserializationOptions,
ev.RenamedPrototypes,
ev.DeletedPrototypes);
if (!deserializer.TryProcessData())
{
Log.Debug($"Failed to process entity data in {file}");
return false;
}
try
{
deserializer.CreateEntities();
}
catch (Exception e)
{
Log.Error($"Caught exception while creating entities: {e}");
Delete(deserializer.Result);
throw;
}
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($"File does not contain the expected data. Expected {exp} but got {deserializer.Result.Category}");
Delete(deserializer.Result);
return false;
}
// Reparent entities if loading entities onto an existing map.
var merged = new HashSet<EntityUid>();
MergeMaps(deserializer, opts, merged);
if (!SetMapId(deserializer, opts))
return false;
// Apply any offsets & rotations specified by the load options
ApplyTransform(deserializer, opts);
try
{
deserializer.StartEntities();
}
catch (Exception e)
{
Log.Error($"Caught exception while starting entities: {e}");
Delete(deserializer.Result);
throw;
}
if (opts.MergeMap is {} map)
MapInitalizeMerged(merged, map);
result = deserializer.Result;
Log.Debug($"Loaded map in {_stopwatch.Elapsed}");
return true;
}
/// <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.
/// </summary>
public bool TryLoadEntity(
ResPath path,
[NotNullWhen(true)] out Entity<TransformComponent>? entity,
DeserializationOptions? options = null)
{
var opts = new MapLoadOptions
{
DeserializationOptions = options ?? DeserializationOptions.Default,
ExpectedCategory = FileCategory.Entity
};
entity = null;
if (!TryLoadGeneric(path, out var result, opts))
return false;
if (result.Orphans.Count == 1)
{
var uid = result.Orphans.Single();
entity = (uid, Transform(uid));
return true;
}
Delete(result);
return false;
}
/// <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.
/// </summary>
public bool TryLoadGrid(
MapId map,
ResPath path,
[NotNullWhen(true)] out Entity<MapGridComponent>? grid,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
var opts = new MapLoadOptions
{
MergeMap = map,
Offset = offset,
Rotation = rot,
DeserializationOptions = options ?? DeserializationOptions.Default,
ExpectedCategory = FileCategory.Grid
};
grid = null;
if (!TryLoadGeneric(path, out var result, opts))
return false;
if (result.Grids.Count == 1)
{
grid = result.Grids.Single();
return true;
}
Delete(result);
return false;
}
private void ApplyTransform(EntityDeserializer deserializer, MapLoadOptions opts)
{
if (opts.Rotation == Angle.Zero && opts.Offset == Vector2.Zero)
return;
// If merging onto a single map, the transformation was already applied by SwapRootNode()
if (opts.MergeMap != null)
return;
var matrix = Matrix3Helpers.CreateTransform(opts.Offset, opts.Rotation);
// We want to apply the transforms to all children of any loaded maps. However, we can't just iterate over the
// children of loaded maps, as transform component has not yet been initialized. I.e. xform.Children is empty.
// Hence we iterate over all entities and check which ones are attached to maps.
foreach (var uid in deserializer.Result.Entities)
{
var xform = Transform(uid);
if (!_mapQuery.HasComp(xform.ParentUid))
continue;
// The original comment around this bit of logic was just:
// > Smelly
// I don't know what sloth meant by that, but I guess applying transforms to grid-maps is a no-no?
// Or more generally, loading a mapgrid onto another (potentially non-mapgrid) map is just generally kind of weird.
if (_gridQuery.HasComponent(xform.ParentUid))
continue;
var rot = xform.LocalRotation + opts.Rotation;
var pos = Vector2.Transform(xform.LocalPosition, matrix);
_xform.SetLocalPositionRotation(uid, pos, rot, xform);
DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0);
}
}
}

View File

@@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Vector2 = System.Numerics.Vector2;
namespace Robust.Shared.EntitySerialization.Systems;
// This partial class file contains methods specific to loading maps
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.
/// </summary>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMap(
ResPath path,
[NotNullWhen(true)] out Entity<MapComponent>? map,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
var opts = new MapLoadOptions
{
Offset = offset,
Rotation = rot,
DeserializationOptions = options ?? DeserializationOptions.Default,
ExpectedCategory = FileCategory.Map
};
map = null;
grids = null;
if (!TryLoadGeneric(path, out var result, opts))
return false;
if (result.Maps.Count == 1)
{
map = result.Maps.First();
grids = result.Grids;
return true;
}
Delete(result);
return false;
}
/// <summary>
/// Attempts to load a 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"/>.
/// </remarks>
/// <remarks>
/// Note that this will not automatically initialize the map, unless specified via the <see cref="DeserializationOptions"/>.
/// </remarks>
public bool TryLoadMapWithId(
MapId mapId,
ResPath path,
[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;
var opts = new MapLoadOptions
{
Offset = offset,
Rotation = rot,
DeserializationOptions = options ?? DeserializationOptions.Default,
ExpectedCategory = FileCategory.Map
};
if (_mapSystem.MapExists(mapId))
throw new Exception($"Target map already exists");
opts.ForceMapId = mapId;
if (!TryLoadGeneric(path, out var result, opts))
return false;
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))
return false;
map = new(uid.Value, comp);
grids = result.Grids;
return true;
}
/// <summary>
/// Attempts to load a 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,
ResPath path,
[NotNullWhen(true)] out HashSet<Entity<MapGridComponent>>? grids,
DeserializationOptions? options = null,
Vector2 offset = default,
Angle rot = default)
{
grids = null;
var opts = new MapLoadOptions
{
Offset = offset,
Rotation = rot,
DeserializationOptions = options ?? DeserializationOptions.Default,
ExpectedCategory = FileCategory.Map
};
if (!_mapSystem.MapExists(mapId))
throw new Exception($"Target map {mapId} does not exist");
opts.MergeMap = mapId;
if (!TryLoadGeneric(path, out var result, opts))
return false;
if (!_mapSystem.TryGetMap(mapId, out var uid) || !TryComp(uid, out MapComponent? comp))
return false;
grids = result.Grids;
return true;
}
private void MergeMaps(EntityDeserializer deserializer, MapLoadOptions opts, HashSet<EntityUid> merged)
{
if (opts.MergeMap is not {} targetId)
return;
if (!_mapSystem.TryGetMap(targetId, out var targetUid))
throw new Exception($"Target map {targetId} does not exist");
deserializer.Result.Category = FileCategory.Unknown;
var rotation = opts.Rotation;
var matrix = Matrix3Helpers.CreateTransform(opts.Offset, rotation);
var target = new Entity<TransformComponent>(targetUid.Value, Transform(targetUid.Value));
// We want to apply the transforms to all children of any loaded maps. However, we can't just iterate over the
// children of loaded maps, as transform component has not yet been initialized. I.e. xform.Children is empty.
// Hence we iterate over all entities and check which ones are attached to maps.
HashSet<EntityUid> maps = new();
HashSet<EntityUid> logged = new();
foreach (var uid in deserializer.Result.Entities)
{
var xform = Transform(uid);
if (!_mapQuery.HasComp(xform.ParentUid))
continue;
if (_gridQuery.HasComponent(xform.ParentUid) && logged.Add(xform.ParentUid))
{
Log.Error($"Merging a grid-map onto another map is not supported.");
continue;
}
maps.Add(xform.ParentUid);
Merge(merged, uid, target, matrix, rotation);
}
deserializer.ToDelete.UnionWith(maps);
deserializer.Result.Maps.RemoveWhere(x => maps.Contains(x.Owner));
foreach (var uid in deserializer.Result.Orphans)
{
Merge(merged, uid, target, matrix, rotation);
}
deserializer.Result.Orphans.Clear();
}
private void Merge(
HashSet<EntityUid> merged,
EntityUid uid,
Entity<TransformComponent> target,
in Matrix3x2 matrix,
Angle rotation)
{
merged.Add(uid);
var xform = Transform(uid);
var angle = xform.LocalRotation + rotation;
var pos = Vector2.Transform(xform.LocalPosition, matrix);
var coords = new EntityCoordinates(target.Owner, pos);
_xform.SetCoordinates((uid, xform, MetaData(uid)), coords, rotation: angle, newParent: target.Comp);
}
private void MapInitalizeMerged(HashSet<EntityUid> merged, MapId targetId)
{
// fuck me I hate this map merging bullshit.
// loading a map "onto" another map shouldn't need to be supported by the generic map loading methods.
// If something needs to do that, it should implement it itself.
// AFAIK this only exists for the loadgamemap command?
if (!_mapSystem.TryGetMap(targetId, out var targetUid))
throw new Exception($"Target map {targetId} does not exist");
if (_mapSystem.IsInitialized(targetUid.Value))
{
foreach (var uid in merged)
{
_mapSystem.RecursiveMapInit(uid);
}
}
var paused = _mapSystem.IsPaused(targetUid.Value);
foreach (var uid in merged)
{
_mapSystem.RecursiveSetPaused(uid, paused);
}
}
private bool SetMapId(EntityDeserializer deserializer, MapLoadOptions opts)
{
if (opts.ForceMapId is not { } id)
return true;
if (deserializer.Result.Maps.Count != 1)
{
Log.Error(
$"The {nameof(MapLoadOptions.ForceMapId)} option is only supported when loading a file containing a single map.");
Delete(deserializer.Result);
return false;
}
var map = deserializer.Result.Maps.Single();
_mapSystem.AssignMapId(map, id);
return true;
}
}

View File

@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Events;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
namespace Robust.Shared.EntitySerialization.Systems;
// This partial class file contains methods for serializing and saving entities, grids, and maps.
public sealed partial class MapLoaderSystem
{
/// <inheritdoc cref="EntitySerializer.OnIsSerializeable"/>
public event EntitySerializer.IsSerializableDelegate? OnIsSerializable;
/// <summary>
/// Recursively serialize the given entity and its children.
/// </summary>
public (MappingDataNode Node, FileCategory Category) SerializeEntitiesRecursive(
HashSet<EntityUid> entities,
SerializationOptions? options = null)
{
_stopwatch.Restart();
if (!entities.All(Exists))
throw new Exception($"Cannot serialize deleted entities");
Log.Info($"Serializing entities: {string.Join(", ", entities.Select(x => ToPrettyString(x).ToString()))}");
var maps = entities.Select(x => Transform(x).MapID).ToHashSet();
var ev = new BeforeSerializationEvent(entities, maps);
RaiseLocalEvent(ev);
// In case no options were provided, we assume that if all of the starting entities are pre-init, we should
// expect that **all** entities that get serialized should be pre-init.
var opts = options ?? SerializationOptions.Default with
{
ExpectPreInit = (entities.All(x => LifeStage(x) < EntityLifeStage.MapInitialized))
};
var serializer = new EntitySerializer(_dependency, opts);
serializer.OnIsSerializeable += OnIsSerializable;
foreach (var ent in entities)
{
serializer.SerializeEntityRecursive(ent);
}
var data = serializer.Write();
var cat = serializer.GetCategory();
var ev2 = new AfterSerializationEvent(entities, data, cat);
RaiseLocalEvent(ev2);
Log.Debug($"Serialized {serializer.EntityData.Count} entities in {_stopwatch.Elapsed}");
return (data, cat);
}
/// <summary>
/// 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)
{
if (_mapQuery.HasComp(entity))
{
Log.Error($"{ToPrettyString(entity)} is a map. Use {nameof(TrySaveMap)}.");
return false;
}
if (_gridQuery.HasComp(entity))
{
Log.Error($"{ToPrettyString(entity)} is a grid. Use {nameof(TrySaveGrid)}.");
return false;
}
var opts = options ?? SerializationOptions.Default;
opts.Category = FileCategory.Entity;
MappingDataNode data;
FileCategory cat;
try
{
(data, cat) = SerializeEntitiesRecursive([entity], opts);
}
catch (Exception e)
{
Log.Error($"Caught exception while trying to serialize entity {ToPrettyString(entity)}:\n{e}");
return false;
}
if (cat != FileCategory.Entity)
{
Log.Error($"Failed to save {ToPrettyString(entity)} as a singular entity. Output: {cat}");
return false;
}
Write(path, data);
return true;
}
/// <summary>
/// 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)
{
if (_mapSystem.TryGetMap(mapId, out var mapUid))
return TrySaveMap(mapUid.Value, path, options);
Log.Error($"Unable to find map {mapId}");
return false;
}
/// <summary>
/// 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)
{
if (!_mapQuery.HasComp(map))
{
Log.Error($"{ToPrettyString(map)} is not a map.");
return false;
}
var opts = options ?? SerializationOptions.Default;
opts.Category = FileCategory.Map;
MappingDataNode data;
FileCategory cat;
try
{
(data, cat) = SerializeEntitiesRecursive([map], opts);
}
catch (Exception e)
{
Log.Error($"Caught exception while trying to serialize map {ToPrettyString(map)}:\n{e}");
return false;
}
if (cat != FileCategory.Map)
{
Log.Error($"Failed to save {ToPrettyString(map)} as a map. Output: {cat}");
return false;
}
Write(path, data);
return true;
}
/// <summary>
/// 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)
{
if (!_gridQuery.HasComp(grid))
{
Log.Error($"{ToPrettyString(grid)} is not a grid.");
return false;
}
if (_mapQuery.HasComp(grid))
{
Log.Error($"{ToPrettyString(grid)} is a map, not (just) a grid. Use {nameof(TrySaveMap)}");
return false;
}
var opts = options ?? SerializationOptions.Default;
opts.Category = FileCategory.Grid;
MappingDataNode data;
FileCategory cat;
try
{
(data, cat) = SerializeEntitiesRecursive([grid], opts);
}
catch (Exception e)
{
Log.Error($"Caught exception while trying to serialize grid {ToPrettyString(grid)}:\n{e}");
return false;
}
if (cat != FileCategory.Grid)
{
Log.Error($"Failed to save {ToPrettyString(grid)} as a grid. Output: {cat}");
return false;
}
Write(path, 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.
/// </summary>
public bool TrySaveGeneric(
EntityUid uid,
ResPath path,
out FileCategory category,
SerializationOptions? options = null)
{
return TrySaveGeneric([uid], path, 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> entities,
ResPath path,
out FileCategory category,
SerializationOptions? options = null)
{
category = FileCategory.Unknown;
if (entities.Count == 0)
return false;
var opts = options ?? SerializationOptions.Default;
MappingDataNode data;
try
{
(data, category) = SerializeEntitiesRecursive(entities, opts);
}
catch (Exception e)
{
Log.Error($"Caught exception while trying to serialize entities:\n{e}");
return false;
}
Write(path, data);
return true;
}
}

View File

@@ -0,0 +1,132 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map.Components;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.EntitySerialization.Systems;
/// <summary>
/// This class provides methods for saving and loading maps and grids.
/// </summary>
/// <remarks>
/// The save & load methods are basically wrappers around <see cref="EntitySerializer"/> and
/// <see cref="EntityDeserializer"/>, which can be used for more control over serialization.
/// </remarks>
public sealed partial class MapLoaderSystem : EntitySystem
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly IDependencyCollection _dependency = default!;
private Stopwatch _stopwatch = new();
private EntityQuery<MapComponent> _mapQuery;
private EntityQuery<MapGridComponent> _gridQuery;
public override void Initialize()
{
base.Initialize();
_gridQuery = GetEntityQuery<MapGridComponent>();
_mapQuery = GetEntityQuery<MapComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
}
private void Write(ResPath path, MappingDataNode data)
{
Log.Info($"Saving serialized results to {path}");
path = path.ToRootedPath();
var document = new YamlDocument(data.ToYaml());
using var writer = _resourceManager.UserData.OpenWriteText(path);
{
var stream = new YamlStream {document};
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}
}
public bool TryReadFile(ResPath file, [NotNullWhen(true)] out MappingDataNode? data)
{
var resPath = file.ToRootedPath();
data = null;
if (!TryGetReader(resPath, out var reader))
return false;
Log.Info($"Loading file: {resPath}");
_stopwatch.Restart();
using var textReader = reader;
var documents = DataNodeParser.ParseYamlStream(reader).ToArray();
Log.Debug($"Loaded yml stream in {_stopwatch.Elapsed}");
// Yes, logging errors in a "try" method is kinda shit, but it was throwing exceptions when I found it and it does
// make sense to at least provide some kind of feedback for why it failed.
switch (documents.Length)
{
case < 1:
Log.Error("Stream has no YAML documents.");
return false;
case > 1:
Log.Error("Stream too many YAML documents. Map files store exactly one.");
return false;
default:
data = (MappingDataNode) documents[0].Root;
return true;
}
}
private bool TryGetReader(ResPath resPath, [NotNullWhen(true)] out TextReader? reader)
{
if (_resourceManager.UserData.Exists(resPath))
{
// Log warning if file exists in both user and content data.
if (_resourceManager.ContentFileExists(resPath))
Log.Warning("Reading map user data instead of content");
reader = _resourceManager.UserData.OpenText(resPath);
return true;
}
if (_resourceManager.TryContentFileRead(resPath, out var contentReader))
{
reader = new StreamReader(contentReader);
return true;
}
Log.Error($"File not found: {resPath}");
reader = null;
return false;
}
/// <summary>
/// Helper method for deleting all loaded entities.
/// </summary>
public void Delete(LoadResult result)
{
foreach (var uid in result.Maps)
{
Del(uid);
}
foreach (var uid in result.Orphans)
{
Del(uid);
}
foreach (var uid in result.Entities)
{
Del(uid);
}
}
}

View File

@@ -311,11 +311,16 @@ namespace Robust.Shared.GameObjects
}
/// <inheritdoc />
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null)
public EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null)
{
return CreateEntity(prototypeName, out _, overrides);
}
public EntityUid CreateEntityUninitialized(string? prototypeName, out MetaDataComponent meta, ComponentRegistry? overrides = null)
{
return CreateEntity(prototypeName, out meta, overrides);
}
/// <inheritdoc />
public virtual EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
{
@@ -548,7 +553,28 @@ namespace Robust.Shared.GameObjects
TransformComponent? parentXform = null;
if (xform.ParentUid.IsValid())
TransformQuery.Resolve(xform.ParentUid, ref parentXform);
{
if (xform.LifeStage < ComponentLifeStage.Initialized)
{
// Entity is being deleted before initialization ever finished.
// The entity will not yet have been added to the parent's transform component.
// This is seemingly pretty error prone ATM, and I'm not even sure if it should be supported?
// Just in case it HAS somehow been added, make sure we remove it.
if (TransformQuery.TryComp(xform.ParentUid, out parentXform) && parentXform._children.Remove(e))
DebugTools.Assert($"Child entity {ToPrettyString(e)} was added to the parent's child set prior to being initialized?");
parentXform = null;
xform._parent = EntityUid.Invalid;
xform._anchored = false;
}
else
{
// Use resolve for automatic error logging.
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
TransformQuery.Resolve(xform.ParentUid, ref parentXform);
}
}
// Then actually delete them
RecursiveDeleteEntity(e, meta, xform, parentXform);
@@ -783,7 +809,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Allocates an entity and stores it but does not load components or do initialization.
/// </summary>
private protected EntityUid AllocEntity(
protected internal EntityUid AllocEntity(
EntityPrototype? prototype,
out MetaDataComponent metadata)
{
@@ -793,6 +819,9 @@ namespace Robust.Shared.GameObjects
return entity;
}
/// <inheritdoc cref="AllocEntity(Robust.Shared.Prototypes.EntityPrototype?,out Robust.Shared.GameObjects.MetaDataComponent)"/>
internal EntityUid AllocEntity(EntityPrototype? prototype) => AllocEntity(prototype, out _);
/// <summary>
/// Allocates an entity and stores it but does not load components or do initialization.
/// </summary>
@@ -872,21 +901,9 @@ namespace Robust.Shared.GameObjects
}
}
private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context)
{
EntityPrototype.LoadEntity((entity, MetaQuery.GetComponent(entity)), ComponentFactory, this, _serManager, context);
}
private protected void LoadEntity(EntityUid entity, IEntityLoadContext? context, EntityPrototype? prototype)
{
var meta = MetaQuery.GetComponent(entity);
DebugTools.Assert(meta.EntityPrototype == prototype);
EntityPrototype.LoadEntity((entity, meta), ComponentFactory, this, _serManager, context);
}
public void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null)
{
var doMapInit = _mapManager.IsMapInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
var doMapInit = _mapSystem.IsInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
InitializeAndStartEntity(entity, doMapInit);
}

View File

@@ -22,11 +22,11 @@ namespace Robust.Shared.GameObjects
{
public sealed class EntitySystemManager : IEntitySystemManager, IPostInjectInit
{
[IoC.Dependency] private readonly IReflectionManager _reflectionManager = default!;
[IoC.Dependency] private readonly IEntityManager _entityManager = default!;
[IoC.Dependency] private readonly ProfManager _profManager = default!;
[IoC.Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[IoC.Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ProfManager _profManager = default!;
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
[Dependency] private readonly ILogManager _logManager = default!;
#if EXCEPTION_TOLERANCE
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
@@ -35,6 +35,18 @@ namespace Robust.Shared.GameObjects
private ISawmill _sawmill = default!;
internal DependencyCollection SystemDependencyCollection = default!;
public IDependencyCollection DependencyCollection
{
get
{
if (_initialized)
return SystemDependencyCollection;
throw new InvalidOperationException($"{nameof(EntitySystemManager)} has not been initialized.");
}
}
private readonly List<Type> _systemTypes = new();
private static readonly Histogram _tickUsageHistogram = Metrics.CreateHistogram("robust_entity_systems_update_usage",

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.IoC;
using Robust.Shared.IoC.Exceptions;
namespace Robust.Shared.GameObjects
@@ -131,5 +132,10 @@ namespace Robust.Shared.GameObjects
IEnumerable<Type> GetEntitySystemTypes();
bool TryGetEntitySystem(Type sysType, [NotNullWhen(true)] out object? system);
object GetEntitySystem(Type sysType);
/// <summary>
/// Dependency collection that contains all the loaded systems.
/// </summary>
public IDependencyCollection DependencyCollection { get; }
}
}

View File

@@ -3,6 +3,7 @@ using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -12,7 +13,7 @@ namespace Robust.Shared.GameObjects;
/// <summary>
/// Network identifier for entities; used by client and server to refer to the same entity where their local <see cref="EntityUid"/> may differ.
/// </summary>
[Serializable, NetSerializable]
[Serializable, NetSerializable, CopyByRef]
public readonly struct NetEntity : IEquatable<NetEntity>, IComparable<NetEntity>, ISpanFormattable
{
public readonly int Id;

View File

@@ -125,7 +125,7 @@ public sealed partial class EntityLookupSystem : EntitySystem
SubscribeLocalEvent<BroadphaseComponent, ComponentAdd>(OnBroadphaseAdd);
SubscribeLocalEvent<BroadphaseComponent, ComponentInit>(OnBroadphaseInit);
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
SubscribeLocalEvent<MapChangedEvent>(OnMapChange);
SubscribeLocalEvent<MapCreatedEvent>(OnMapChange);
_transform.OnBeforeMoveEvent += OnMove;
EntityManager.EntityInitialized += OnEntityInit;
@@ -194,9 +194,9 @@ public sealed partial class EntityLookupSystem : EntitySystem
}
}
private void OnMapChange(MapChangedEvent ev)
private void OnMapChange(MapCreatedEvent ev)
{
if (ev.Created && ev.Map != MapId.Nullspace)
if (ev.MapId != MapId.Nullspace)
{
EnsureComp<BroadphaseComponent>(ev.Uid);
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
@@ -70,14 +71,15 @@ public abstract partial class SharedMapSystem
if (component.MapId == MapId.Nullspace)
{
if (state.MapId == MapId.Nullspace)
throw new Exception($"Received invalid map state? {ToPrettyString(uid)}");
throw new Exception($"Received invalid map state for {ToPrettyString(uid)}");
component.MapId = state.MapId;
Maps.Add(component.MapId, uid);
AssignMapId((uid, component), state.MapId);
RecursiveMapIdUpdate(uid, uid, component.MapId);
}
DebugTools.AssertEqual(component.MapId, state.MapId);
if (component.MapId != state.MapId)
throw new Exception($"Received invalid map state for {ToPrettyString(uid)}");
component.LightingEnabled = state.LightingEnabled;
component.MapInitialized = state.Initialized;
@@ -119,26 +121,75 @@ public abstract partial class SharedMapSystem
EnsureComp<MovedGridsComponent>(uid);
}
private void OnCompInit(EntityUid uid, MapComponent component, ComponentInit args)
internal void AssignMapId(Entity<MapComponent> map, MapId? id = null)
{
if (component.MapId == MapId.Nullspace)
component.MapId = GetNextMapId();
DebugTools.AssertEqual(component.MapId.IsClientSide, IsClientSide(uid));
if (!Maps.TryAdd(component.MapId, uid))
if (map.Comp.MapId != MapId.Nullspace)
{
if (Maps[component.MapId] != uid)
throw new Exception($"Attempted to initialize a map {ToPrettyString(uid)} with a duplicate map id {component.MapId}");
if (id != null && map.Comp.MapId != id)
{
QueueDel(map.Owner);
throw new Exception($"Map entity {ToPrettyString(map.Owner)} has already been assigned an id");
}
if (!Maps.TryGetValue(map.Comp.MapId, out var existing) || existing != map.Owner)
{
QueueDel(map.Owner);
throw new Exception($"Map entity {ToPrettyString(map.Owner)} was improperly assigned a map id?");
}
DebugTools.Assert(UsedIds.Contains(map.Comp.MapId));
return;
}
var msg = new MapChangedEvent(uid, component.MapId, true);
RaiseLocalEvent(uid, msg, true);
map.Comp.MapId = id ?? GetNextMapId();
if (IsClientSide(map) != map.Comp.MapId.IsClientSide)
throw new Exception($"Attempting to assign a client-side map id to a networked entity or vice-versa");
if (!UsedIds.Add(map.Comp.MapId))
Log.Warning($"Re-using a previously used map id ({map.Comp.MapId}) for map entity {ToPrettyString(map)}");
if (Maps.TryAdd(map.Comp.MapId, map.Owner))
return;
if (Maps[map.Comp.MapId] == map.Owner)
return;
QueueDel(map);
throw new Exception(
$"Attempted to assign an existing mapId {map.Comp} to a map entity {ToPrettyString(map.Owner)}");
}
private void OnCompInit(Entity<MapComponent> map, ref ComponentInit args)
{
AssignMapId(map);
#pragma warning disable CS0618 // Type or member is obsolete
var msg = new MapChangedEvent(map, map.Comp.MapId, true);
#pragma warning restore CS0618 // Type or member is obsolete
RaiseLocalEvent(map, msg, true);
var ev = new MapCreatedEvent(map, map.Comp.MapId);
RaiseLocalEvent(map, ev, true);
}
private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args)
{
DebugTools.Assert(!component.MapInitialized);
component.MapInitialized = true;
Dirty(uid, component);
}
private void OnCompStartup(EntityUid uid, MapComponent component, ComponentStartup args)
{
if (component.MapPaused)
RecursiveSetPaused(uid, true);
// un-initialized maps are always paused.
component.MapPaused |= !component.MapInitialized;
if (!component.MapPaused)
return;
// Recursively pause all entities on the map
component.MapPaused = false;
SetPaused(uid, true);
}
private void OnMapRemoved(EntityUid uid, MapComponent component, ComponentShutdown args)
@@ -146,8 +197,13 @@ public abstract partial class SharedMapSystem
DebugTools.Assert(component.MapId != MapId.Nullspace);
Maps.Remove(component.MapId);
#pragma warning disable CS0618 // Type or member is obsolete
var msg = new MapChangedEvent(uid, component.MapId, false);
#pragma warning restore CS0618 // Type or member is obsolete
RaiseLocalEvent(uid, msg, true);
var ev = new MapRemovedEvent(uid, component.MapId);
RaiseLocalEvent(uid, ev, true);
}
/// <summary>
@@ -181,19 +237,17 @@ public abstract partial class SharedMapSystem
if (_netManager.IsClient && _netManager.IsConnected && !mapId.IsClientSide)
throw new ArgumentException($"Attempted to create a client-side map entity with a non client-side map ID?");
var uid = EntityManager.CreateEntityUninitialized(null);
var map = _factory.GetComponent<MapComponent>();
map.MapId = mapId;
AddComp(uid, map);
if (UsedIds.Contains(mapId))
Log.Warning($"Re-using MapId: {mapId}");
// Give the entity a name, mainly for debugging. Content can always override this with a localized name.
var meta = MetaData(uid);
_meta.SetEntityName(uid, $"Map Entity", meta);
var (uid, map, meta) = CreateUninitializedMap();
DebugTools.AssertEqual(map.MapId, MapId.Nullspace);
AssignMapId((uid, map), mapId);
// Initialize components. this should add the map id to the collections.
EntityManager.InitializeComponents(uid, meta);
EntityManager.StartComponents(uid);
DebugTools.Assert(Maps[mapId] == uid);
EntityManager.InitializeEntity(uid, meta);
EntityManager.StartEntity(uid);
DebugTools.AssertEqual(Maps[mapId], uid);
if (runMapInit)
InitializeMap((uid, map));
@@ -202,4 +256,22 @@ public abstract partial class SharedMapSystem
return uid;
}
public Entity<MapComponent, MetaDataComponent> CreateUninitializedMap()
{
var uid = EntityManager.CreateEntityUninitialized(null, out var meta);
_meta.SetEntityName(uid, $"Map Entity", meta);
return (uid, AddComp<MapComponent>(uid), meta);
}
public void DeleteMap(MapId mapId)
{
if (TryGetMap(mapId, out var uid))
Del(uid);
}
public IEnumerable<MapId> GetAllMapIds()
{
return Maps.Keys;
}
}

View File

@@ -34,13 +34,6 @@ public abstract partial class SharedMapSystem
return map.Comp.MapInitialized;
}
private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args)
{
DebugTools.Assert(!component.MapInitialized);
component.MapInitialized = true;
EntityManager.Dirty(uid, component);
}
public void InitializeMap(MapId mapId, bool unpause = true)
{
if(!Maps.TryGetValue(mapId, out var uid))
@@ -63,7 +56,7 @@ public abstract partial class SharedMapSystem
SetPaused(map, false);
}
private void RecursiveMapInit(EntityUid entity)
internal void RecursiveMapInit(EntityUid entity)
{
var toInitialize = new List<EntityUid> {entity};
for (var i = 0; i < toInitialize.Count; i++)

View File

@@ -22,7 +22,7 @@ public abstract partial class SharedMapSystem
if (!_mapQuery.Resolve(map, ref map.Comp))
return false;
return map.Comp.MapPaused;
return map.Comp.MapPaused || !map.Comp.MapInitialized;
}
public void SetPaused(MapId mapId, bool paused)
@@ -49,7 +49,7 @@ public abstract partial class SharedMapSystem
RecursiveSetPaused(map, paused);
}
private void RecursiveSetPaused(EntityUid entity, bool paused)
internal void RecursiveSetPaused(EntityUid entity, bool paused)
{
_meta.SetEntityPaused(entity, paused);
foreach (var child in Transform(entity)._children)

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
@@ -32,6 +33,13 @@ namespace Robust.Shared.GameObjects
internal Dictionary<MapId, EntityUid> Maps { get; } = new();
/// <summary>
/// This hashset is used to try prevent MapId re-use. This is mainly for auto-assigned map ids.
/// Loading a map with a specific id (e.g., the various mapping commands) may still result in an id being
/// reused.
/// </summary>
protected HashSet<MapId> UsedIds = new();
public override void Initialize()
{
base.Initialize();
@@ -53,6 +61,7 @@ namespace Robust.Shared.GameObjects
/// <summary>
/// Arguments for when a map is created or deleted.
/// </summary>
[Obsolete("Use map creation or deletion events")]
public sealed class MapChangedEvent : EntityEventArgs
{
public EntityUid Uid;
@@ -83,6 +92,16 @@ namespace Robust.Shared.GameObjects
public bool Destroyed => !Created;
}
/// <summary>
/// Event raised whenever a map is created.
/// </summary>
public readonly record struct MapCreatedEvent(EntityUid Uid, MapId MapId);
/// <summary>
/// Event raised whenever a map is removed.
/// </summary>
public readonly record struct MapRemovedEvent(EntityUid Uid, MapId MapId);
#pragma warning disable CS0618
public sealed class GridStartupEvent : EntityEventArgs
{

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Utility;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Robust.Shared.Map.Components;
@@ -214,7 +214,15 @@ public abstract partial class SharedTransformSystem
}
else if (_mapQuery.TryComp(uid, out var mapComp))
{
DebugTools.AssertNotEqual(mapComp.MapId, MapId.Nullspace);
if (mapComp.MapId == MapId.Nullspace)
{
#if !EXCEPTION_TOLERANCE
throw new Exception("Transform is initialising before map ids have been assigned?");
#endif
Log.Error($"Transform is initialising before map ids have been assigned?");
_map.AssignMapId((uid, mapComp));
}
xform.MapUid = uid;
xform.MapID = mapComp.MapId;
}

View File

@@ -201,7 +201,7 @@ namespace Robust.Shared.GameObjects
if (xform.GridUid == xform.ParentUid)
return (xform.Coordinates, GetWorldRotation(xform, XformQuery));
DebugTools.Assert(!_mapManager.IsGrid(uid) && !_mapManager.IsMap(uid));
DebugTools.Assert(!HasComp<MapComponent>(uid) && !HasComp<MapComponent>(uid));
var (pos, worldRot) = GetWorldPositionRotation(xform, XformQuery);

View File

@@ -15,7 +15,7 @@ namespace Robust.Shared.Map.Components
[DataField]
public bool LightingEnabled { get; set; } = true;
[ViewVariables(VVAccess.ReadOnly)]
[ViewVariables(VVAccess.ReadOnly), Access(typeof(SharedMapSystem), Other = AccessPermissions.ReadExecute)]
public MapId MapId { get; internal set; } = MapId.Nullspace;
[DataField, Access(typeof(SharedMapSystem), typeof(MapManager))]

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using Robust.Shared.EntitySerialization;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Markdown.Mapping;
namespace Robust.Shared.Map.Events;
@@ -28,17 +30,13 @@ public sealed class BeforeEntityReadEvent
}
/// <summary>
/// This event is broadcast just before an entity gets serialized.
/// This event is broadcast just before the given entities (and their children) are serialized.
/// For convenience, the event also contains a set with all the maps that the entities are on. This does not
/// necessarily mean that the maps are themselves getting serialized.
/// </summary>
public sealed class BeforeSaveEvent(EntityUid entity, EntityUid? map)
{
/// <summary>
/// The entity that is going to be saved. usually a map or grid.
/// </summary>
public EntityUid Entity = entity;
public readonly record struct BeforeSerializationEvent(HashSet<EntityUid> Entities, HashSet<MapId> MapIds);
/// <summary>
/// The map that the <see cref="Entity"/> is on.
/// </summary>
public EntityUid? Map = map;
}
/// <summary>
/// This event is broadcast just after entities (and their children) have been serialized, but before it gets written to a yaml file.
/// </summary>
public readonly record struct AfterSerializationEvent(HashSet<EntityUid> Entities, MappingDataNode Node, FileCategory Category);

View File

@@ -47,21 +47,25 @@ namespace Robust.Shared.Map
/// </summary>
/// <param name="mapId">The map ID to check existence of.</param>
/// <returns>True if the map exists, false otherwise.</returns>
[Obsolete("Use MapSystem")]
bool MapExists([NotNullWhen(true)] MapId? mapId);
/// <summary>
/// Returns the map entity ID for a given map, or an invalid entity Id if the map does not exist.
/// </summary>
[Obsolete("Use TryGetMap")]
[Obsolete("Use MapSystem")]
EntityUid GetMapEntityId(MapId mapId);
/// <summary>
/// Replaces GetMapEntity()'s throw-on-failure semantics.
/// </summary>
[Obsolete("Use MapSystem")]
EntityUid GetMapEntityIdOrThrow(MapId mapId);
[Obsolete("Use MapSystem")]
IEnumerable<MapId> GetAllMapIds();
[Obsolete("Use MapSystem")]
void DeleteMap(MapId mapId);
// ReSharper disable once MethodOverloadWithOptionalParameter
@@ -205,20 +209,22 @@ namespace Robust.Shared.Map
[Obsolete("Just delete the grid entity")]
void DeleteGrid(EntityUid euid);
[Obsolete("Use HasComp")]
bool IsGrid(EntityUid uid);
[Obsolete("Use HasComp")]
bool IsMap(EntityUid uid);
//
// Pausing functions
//
[Obsolete("Use MapSystem")]
void SetMapPaused(MapId mapId, bool paused);
[Obsolete("Use MapSystem")]
void DoMapInitialize(MapId mapId);
[Obsolete("Use CreateMap's runMapInit argument")]
void AddUninitializedMap(MapId mapId);
[Obsolete("Use MapSystem")]
bool IsMapPaused(MapId mapId);

View File

@@ -67,15 +67,5 @@ namespace Robust.Shared.Map
/// </summary>
/// <param name="tileDef">THe definition to register.</param>
void Register(ITileDefinition tileDef);
/// <summary>
/// Register a tile alias with this manager.
/// The tile need not exist yet - the alias's creation will be deferred until it exists.
/// Tile aliases do not have IDs of their own and do not show up in enumeration.
/// Their main utility is for easier map migration.
/// </summary>
/// <param name="src">The source tile (i.e. name of the alias).</param>
/// <param name="dst">The destination tile (i.e. the actual concrete tile).</param>
void AssignAlias(string src, string dst);
}
}

View File

@@ -51,7 +51,7 @@ namespace Robust.Shared.Map
public override string ToString()
{
return Value.ToString();
return IsClientSide ? $"c{-Value}" : Value.ToString();
}
public bool IsClientSide => Value < 0;

View File

@@ -125,6 +125,9 @@ internal partial class MapManager
EntityManager.System<MetaDataSystem>().SetEntityName(gridEnt, $"grid", meta);
EntityManager.InitializeComponents(gridEnt, meta);
EntityManager.StartComponents(gridEnt);
// Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized.
// I don't know whether that is intentional or not.
return (gridEnt, grid);
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Robust.Shared.Map;
@@ -28,16 +27,10 @@ public sealed class MapEventArgs : EventArgs
internal partial class MapManager
{
private Dictionary<MapId, EntityUid> _mapEntities => _mapSystem.Maps;
/// <inheritdoc />
public virtual void DeleteMap(MapId mapId)
{
if (!_mapEntities.TryGetValue(mapId, out var ent) || !ent.IsValid())
throw new InvalidOperationException($"Attempted to delete nonexistent map '{mapId}'");
EntityManager.DeleteEntity(ent);
DebugTools.Assert(!_mapEntities.ContainsKey(mapId));
_mapSystem.DeleteMap(mapId);
}
/// <inheritdoc />
@@ -81,7 +74,7 @@ internal partial class MapManager
/// <inheritdoc />
public IEnumerable<MapId> GetAllMapIds()
{
return _mapEntities.Keys;
return _mapSystem.GetAllMapIds();
}
/// <inheritdoc />

View File

@@ -1,7 +1,5 @@
using System;
using System.Globalization;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
namespace Robust.Shared.Map
{
@@ -27,14 +25,6 @@ namespace Robust.Shared.Map
return _mapSystem.IsInitialized(mapId);
}
public void AddUninitializedMap(MapId mapId)
{
var ent = GetMapEntityId(mapId);
EntityManager.GetComponent<MapComponent>(ent).MapInitialized = false;
var meta = EntityManager.GetComponent<MetaDataComponent>(ent);
((EntityManager)EntityManager).SetLifeStage(meta, EntityLifeStage.Initialized);
}
/// <inheritdoc />
public bool IsMapPaused(MapId mapId)
{
@@ -87,7 +77,7 @@ namespace Robust.Shared.Map
return;
}
shell.WriteLine(IsMapPaused(mapId).ToString());
shell.WriteLine(_mapSystem.IsPaused(mapId).ToString());
});
_conhost.RegisterCommand("unpausemap",

View File

@@ -45,54 +45,54 @@ internal partial class MapManager
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform,
ref List<Entity<MapGridComponent>> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx, includeMap);
}
public void FindGridsIntersecting<T>(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, shape, transform, callback, includeMap, approx);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, shape, transform, callback, includeMap, approx);
}
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, worldAABB, callback, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx, includeMap);
}
public void FindGridsIntersecting<TState>(MapId mapId, Box2 worldAABB, ref TState state, GridCallback<TState> callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldAABB, ref state, callback, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var map))
FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx, includeMap);
}
public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List<Entity<MapGridComponent>> grids,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var map))
FindGridsIntersecting(map, worldAABB, ref grids, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var map))
FindGridsIntersecting(map.Value, worldAABB, ref grids, approx, includeMap);
}
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate,
bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, worldBounds, callback, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx, includeMap);
}
public void FindGridsIntersecting<TState>(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback<TState> callback,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, worldBounds, ref state, callback, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx, includeMap);
}
public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List<Entity<MapGridComponent>> grids,
bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap)
{
if (_mapEntities.TryGetValue(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt, worldBounds, ref grids, approx, includeMap);
if (_mapSystem.TryGetMap(mapId, out var mapEnt))
FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx, includeMap);
}
#endregion
@@ -338,8 +338,8 @@ internal partial class MapManager
/// </summary>
public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid)
{
if (_mapEntities.TryGetValue(mapId, out var map))
return TryFindGridAt(map, worldPos, out uid, out grid);
if (_mapSystem.TryGetMap(mapId, out var map))
return TryFindGridAt(map.Value, worldPos, out uid, out grid);
uid = default;
grid = null;

View File

@@ -11,10 +11,10 @@ namespace Robust.Shared.Map;
/// <inheritdoc cref="IMapManager" />
[Virtual]
internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber, IPostInjectInit
internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber
{
[field: Dependency] public IGameTiming GameTiming { get; } = default!;
[field: Dependency] public IEntityManager EntityManager { get; } = default!;
[Dependency] public readonly IGameTiming GameTiming = default!;
[Dependency] public readonly IEntityManager EntityManager = default!;
[Dependency] private readonly IManifoldManager _manifolds = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IConsoleHost _conhost = default!;
@@ -34,6 +34,7 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber,
_gridTreeQuery = EntityManager.GetEntityQuery<GridTreeComponent>();
_gridQuery = EntityManager.GetEntityQuery<MapGridComponent>();
InitializeMapPausing();
_sawmill = _logManager.GetSawmill("system.map");
}
/// <inheritdoc />
@@ -74,9 +75,4 @@ internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber,
EntityManager.DeleteEntity(uid);
}
}
void IPostInjectInit.PostInject()
{
_sawmill = _logManager.GetSawmill("system.map");
}
}

View File

@@ -1,173 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Timing;
namespace Robust.Shared.Map;
internal sealed class MapSerializationContext : ISerializationContext, IEntityLoadContext,
ITypeSerializer<EntityUid, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
// Run-specific data
public Dictionary<int, string>? TileMap;
public readonly Dictionary<string, IComponent> CurrentReadingEntityComponents = new();
public HashSet<string> CurrentlyIgnoredComponents = new();
public string? CurrentComponent;
public EntityUid? CurrentWritingEntity;
public IEntityManager EntityManager;
public IGameTiming Timing;
private Dictionary<int, EntityUid> _uidEntityMap = new();
private Dictionary<EntityUid, int> _entityUidMap = new();
// Native tile ID -> map tile ID map for writing maps.
public Dictionary<int, int> TileWriteMap = [];
/// <summary>
/// Are we currently iterating prototypes or entities for writing.
/// </summary>
public bool WritingReadingPrototypes { get; set; }
/// <summary>
/// Whether the map has been MapInitialized or not.
/// </summary>
public bool MapInitialized;
/// <summary>
/// How long the target map has been paused. Used for time offsets.
/// </summary>
public TimeSpan PauseTime;
/// <summary>
/// The parent of the entity being saved, This entity is not itself getting saved.
/// </summary>
private EntityUid? _parentUid;
public MapSerializationContext(IEntityManager entityManager, IGameTiming timing)
{
EntityManager = entityManager;
Timing = timing;
SerializerProvider.RegisterSerializer(this);
}
public void Set(
Dictionary<int, EntityUid> uidEntityMap,
Dictionary<EntityUid, int> entityUidMap,
bool mapPreInit,
TimeSpan pauseTime,
EntityUid? parentUid)
{
_uidEntityMap = uidEntityMap;
_entityUidMap = entityUidMap;
MapInitialized = mapPreInit;
PauseTime = pauseTime;
if (parentUid != null && parentUid.Value.IsValid())
_parentUid = parentUid;
}
public void Clear()
{
CurrentReadingEntityComponents.Clear();
CurrentlyIgnoredComponents.Clear();
CurrentComponent = null;
CurrentWritingEntity = null;
PauseTime = TimeSpan.Zero;
}
// Create custom object serializers that will correctly allow data to be overriden by the map file.
bool IEntityLoadContext.TryGetComponent(string componentName, [NotNullWhen(true)] out IComponent? component)
{
return CurrentReadingEntityComponents.TryGetValue(componentName, out component);
}
public IEnumerable<string> GetExtraComponentTypes()
{
return CurrentReadingEntityComponents!.Keys;
}
public bool ShouldSkipComponent(string compName)
{
return CurrentlyIgnoredComponents.Contains(compName);
}
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
{
if (node.Value == "null")
{
return new ValidatedValueNode(node);
}
if (!int.TryParse(node.Value, out var val) || !_uidEntityMap.ContainsKey(val))
{
return new ErrorNode(node, "Invalid EntityUid", true);
}
return new ValidatedValueNode(node);
}
public DataNode Write(ISerializationManager serializationManager, EntityUid value,
IDependencyCollection dependencies, bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (!_entityUidMap.TryGetValue(value, out var entityUidMapped))
{
if (CurrentComponent == "Transform")
{
if (!value.IsValid() || value == _parentUid)
return new ValueDataNode("invalid");
}
dependencies
.Resolve<ILogManager>()
.GetSawmill("map")
.Error("Encountered an invalid entityUid '{0}' while serializing a map.", value);
return new ValueDataNode("invalid");
}
return new ValueDataNode(entityUidMapped.ToString(CultureInfo.InvariantCulture));
}
EntityUid ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context, ISerializationManager.InstantiationDelegate<EntityUid>? _)
{
if (node.Value == "invalid" && CurrentComponent == "Transform")
return EntityUid.Invalid;
if (int.TryParse(node.Value, out var val) && _uidEntityMap.TryGetValue(val, out var entity))
return entity;
dependencies
.Resolve<ILogManager>()
.GetSawmill("map")
.Error("Error in map file: found local entity UID '{0}' which does not exist.", val);
return EntityUid.Invalid;
}
[MustUseReturnValue]
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
bool skipHook,
ISerializationContext? context = null)
{
return new((int)source);
}
}

View File

@@ -27,10 +27,6 @@ namespace Robust.Shared.Map
public virtual void Initialize()
{
foreach (var prototype in _prototypeManager.EnumeratePrototypes<TileAliasPrototype>())
{
AssignAlias(prototype.ID, prototype.Target);
}
}
public virtual void Register(ITileDefinition tileDef)
@@ -45,46 +41,8 @@ namespace Robust.Shared.Map
tileDef.AssignTileId(id);
TileDefs.Add(tileDef);
_tileNames[name] = tileDef;
AliasingHandleDeferred(name);
}
private void AliasingHandleDeferred(string name)
{
// Aliases may have been held back due to tiles not being registered yet, handle this.
if (_awaitingAliases.ContainsKey(name))
{
var list = _awaitingAliases[name];
_awaitingAliases.Remove(name);
foreach (var alias in list)
{
AssignAlias(alias, name);
}
}
}
public virtual void AssignAlias(string src, string dst)
{
if (_tileNames.ContainsKey(src))
{
throw new ArgumentException("Another tile definition or alias with the same name has already been registered.", nameof(src));
}
if (_tileNames.ContainsKey(dst))
{
// Simple enough, source to destination.
_tileNames[src] = _tileNames[dst];
AliasingHandleDeferred(src);
}
else
{
// Less simple - stash this alias for later so it appears when the target does.
if (!_awaitingAliases.ContainsKey(dst))
_awaitingAliases[dst] = new();
_awaitingAliases[dst].Add(src);
}
}
public Tile GetVariantTile(string name, IRobustRandom random)
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Robust.Shared.EntitySerialization;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -78,13 +79,11 @@ public sealed class FixtureSerializer : ITypeSerializer<Dictionary<string, Fixtu
if (value.Count == 0)
return seq;
if (context is MapSerializationContext mapContext)
if (context is EntitySerializer ctx)
{
// Don't serialize mapgrid fixtures because it's bloat and we'll just generate them at runtime.
if (dependencies.Resolve<IEntityManager>().HasComponent<MapGridComponent>(mapContext.CurrentWritingEntity))
{
if (ctx.EntMan.HasComponent<MapGridComponent>(ctx.CurrentEntity))
return seq;
}
}
foreach (var (id, fixture) in value)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.EntitySerialization;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
@@ -268,7 +269,7 @@ namespace Robust.Shared.Prototypes
component = newComponent;
}
if (context is not MapSerializationContext map)
if (context is not EntityDeserializer map)
{
serManager.CopyTo(data, ref component, context, notNullableOverride: true);
return;

View File

@@ -4,8 +4,7 @@ using Robust.Shared.ViewVariables;
namespace Robust.Shared.Prototypes;
/// <summary>
/// Prototype that represents an alias from one tile ID to another.
/// Tile alias prototypes, unlike tile prototypes, are implemented here, as they're really just fed to TileDefinitionManager.
/// Prototype that represents an alias from one tile ID to another. These are used when deserializing entities from yaml.
/// </summary>
[Prototype("tileAlias")]
public sealed partial class TileAliasPrototype : IPrototype
@@ -13,13 +12,13 @@ public sealed partial class TileAliasPrototype : IPrototype
/// <summary>
/// The target tile ID to alias to.
/// </summary>
[DataField("target")]
[DataField]
public string Target { get; private set; } = default!;
/// <summary>
/// The source tile ID (and the ID of this tile alias).
/// </summary>
[ViewVariables]
[IdDataFieldAttribute]
[IdDataField]
public string ID { get; private set; } = default!;
}

View File

@@ -11,7 +11,10 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Prototypes;
internal sealed class YamlValidationContext : ISerializationContext, ITypeSerializer<EntityUid, ValueDataNode>
internal sealed class YamlValidationContext :
ISerializationContext,
ITypeSerializer<EntityUid, ValueDataNode>,
ITypeSerializer<NetEntity, ValueDataNode>
{
public SerializationManager.SerializerProvider SerializerProvider { get; } = new();
public bool WritingReadingPrototypes => true;
@@ -24,7 +27,7 @@ internal sealed class YamlValidationContext : ISerializationContext, ITypeSerial
ValidationNode ITypeValidator<EntityUid, ValueDataNode>.Validate(ISerializationManager serializationManager,
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
{
if (node.Value == "null" || node.Value == "invalid")
if (node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain EntityUids", true);
@@ -52,11 +55,42 @@ internal sealed class YamlValidationContext : ISerializationContext, ITypeSerial
return EntityUid.Parse(node.Value);
}
[MustUseReturnValue]
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
bool skipHook,
public ValidationNode Validate(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
return new((int)source);
if (node.Value == "invalid")
return new ValidatedValueNode(node);
return new ErrorNode(node, "Prototypes should not contain NetEntities");
}
public NetEntity Read(
ISerializationManager serializationManager,
ValueDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<NetEntity>? instanceProvider = null)
{
if (node.Value == "invalid")
return NetEntity.Invalid;
return NetEntity.Parse(node.Value);
}
public DataNode Write(
ISerializationManager serializationManager,
NetEntity value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (!value.Valid)
return new ValueDataNode("invalid");
return new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture));
}
}

View File

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
@@ -440,6 +441,12 @@ namespace Robust.Shared.Serialization.Manager
return (TNode) PushComposition(type, parents, child, context);
}
/// <summary>
/// Simple <see cref="MappingDataNode"/> inheritance pusher clones data and overrides a parent's values with
/// the child's.
/// </summary>
MappingDataNode CombineMappings(MappingDataNode child, MappingDataNode parent);
#endregion
public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true)] out Type? variableType);

View File

@@ -273,6 +273,26 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
return newMapping;
}
/// <summary>
/// Variant of <see cref="Copy"/> that doesn't clone the keys or values.
/// </summary>
public MappingDataNode ShallowClone()
{
var newMapping = new MappingDataNode(_children.Count)
{
Tag = Tag,
Start = Start,
End = End
};
foreach (var (key, val) in _list)
{
newMapping.Add(key, val);
}
return newMapping;
}
/// <summary>
/// Variant of <see cref="Except(MappingDataNode)"/> that will recursively call except rather than only checking equality.
/// </summary>

View File

@@ -1,8 +1,8 @@
using System;
using System.Globalization;
using Robust.Shared.EntitySerialization;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
@@ -13,8 +13,8 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
/// <summary>
/// This serializer offsets a timespan by the game's current time. If the entity is currently paused, the pause time
/// will also be accounted for,
/// This serializer offsets a timespan by the game's current time. If the entity is currently paused, the the offset will
/// instead be the time at which the entity was paused.
/// </summary>
/// <remarks>
/// Prototypes and pre map-init entities will always serialize this as zero. This is done mainly as a brute force fix
@@ -29,14 +29,13 @@ public sealed class TimeOffsetSerializer : ITypeSerializer<TimeSpan, ValueDataNo
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<TimeSpan>? instanceProvider = null)
{
if (context is not MapSerializationContext mapContext
|| mapContext.WritingReadingPrototypes
|| !mapContext.MapInitialized)
{
if (context is {WritingReadingPrototypes: true})
return TimeSpan.Zero;
}
var timing = mapContext.Timing;
if (context is not EntityDeserializer {CurrentReadingEntity.PostInit: true} ctx)
return TimeSpan.Zero;
var timing = ctx.Timing;
var seconds = double.Parse(node.Value, CultureInfo.InvariantCulture);
return TimeSpan.FromSeconds(seconds) + timing.CurTime;
}
@@ -50,34 +49,29 @@ public sealed class TimeOffsetSerializer : ITypeSerializer<TimeSpan, ValueDataNo
: new ErrorNode(node, "Failed parsing TimeSpan");
}
public DataNode Write(ISerializationManager serializationManager, TimeSpan value, IDependencyCollection dependencies, bool alwaysWrite = false,
public DataNode Write(
ISerializationManager serializationManager,
TimeSpan value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
if (context is not MapSerializationContext mapContext
|| mapContext.WritingReadingPrototypes
|| !mapContext.MapInitialized)
if (context is not EntitySerializer serializer
|| serializer.WritingReadingPrototypes
|| !serializer.EntMan.TryGetComponent(serializer.CurrentEntity, out MetaDataComponent? meta)
|| meta.EntityLifeStage < EntityLifeStage.MapInitialized)
{
DebugTools.Assert(value == TimeSpan.Zero || context?.WritingReadingPrototypes != true,
"non-zero time offsets in prototypes are not supported. If required, initialize offsets on map-init");
return new ValueDataNode("0");
}
if (!mapContext.MapInitialized)
return new ValueDataNode("0");
if (mapContext.EntityManager.TryGetComponent(mapContext.CurrentWritingEntity, out MetaDataComponent? meta))
{
// Here, PauseTime is a time -- not a duration.
if (meta.PauseTime != null)
value -= meta.PauseTime.Value;
}
// We subtract the current time, unless the entity is paused, in which case we subtract the time at which
// it was paused.
if (meta.PauseTime != null)
value -= meta.PauseTime.Value;
else
{
// But here, PauseTime is a duration instead of a time
// What jolly fun.
value = value - mapContext.Timing.CurTime + mapContext.PauseTime;
}
value -= serializer.Timing.CurTime;
return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture));
}

View File

@@ -13,6 +13,8 @@ using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -157,7 +159,6 @@ namespace Robust.UnitTesting
systems.LoadExtraSystemType<DebugRayDrawingSystem>();
systems.LoadExtraSystemType<PrototypeReloadSystem>();
systems.LoadExtraSystemType<DebugPhysicsSystem>();
systems.LoadExtraSystemType<MapLoaderSystem>();
systems.LoadExtraSystemType<InputSystem>();
systems.LoadExtraSystemType<PvsOverrideSystem>();
systems.LoadExtraSystemType<MapSystem>();
@@ -179,12 +180,10 @@ namespace Robust.UnitTesting
if (ExtraComponents != null)
compFactory.RegisterTypes(ExtraComponents);
if (Project == UnitTestProject.Server)
{
compFactory.RegisterClass<MapSaveTileMapComponent>();
compFactory.RegisterClass<MapSaveIdComponent>();
}
else
compFactory.RegisterClass<MapSaveTileMapComponent>();
compFactory.RegisterClass<YamlUidComponent>();
if (Project != UnitTestProject.Server)
{
compFactory.RegisterClass<PointLightComponent>();
compFactory.RegisterClass<SpriteComponent>();

View File

@@ -19,7 +19,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components
{
public override UnitTestProject Project => UnitTestProject.Server;
private IServerEntityManagerInternal EntityManager = default!;
private IEntityManager EntityManager = default!;
private IMapManager MapManager = default!;
private SharedTransformSystem XformSystem => EntityManager.System<SharedTransformSystem>();
@@ -47,7 +47,7 @@ namespace Robust.UnitTesting.Server.GameObjects.Components
{
IoCManager.Resolve<IComponentFactory>().GenerateNetIds();
EntityManager = IoCManager.Resolve<IServerEntityManagerInternal>();
EntityManager = IoCManager.Resolve<IEntityManager>();
MapManager = IoCManager.Resolve<IMapManager>();
IoCManager.Resolve<ISerializationManager>().Initialize();

View File

@@ -1,51 +1,59 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Server.GameObjects;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Map.Components;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using IgnoreUIRangeComponent = Robust.Shared.GameObjects.IgnoreUIRangeComponent;
namespace Robust.UnitTesting.Server.Maps
{
[TestFixture]
public sealed partial class MapLoaderTest : RobustUnitTest
public sealed partial class MapLoaderTest : RobustIntegrationTest
{
private const string MapData = @"
meta:
format: 2
name: DemoStation
author: Space-Wizards
postmapinit: false
format: 7
category: Grid
engineVersion: 238.0.0
forkId: """"
forkVersion: """"
time: 12/22/2024 04:08:12
entityCount: 3
maps: []
grids:
- settings:
chunksize: 16
tilesize: 1
snapsize: 1
chunks: []
- 1
orphans:
- 1
nullspace: []
tilemap: {}
entities:
- uid: 0
components:
- parent: null
type: Transform
- index: 0
type: MapGrid
- uid: 1
type: MapDeserializeTest
components:
- type: MapDeserializeTest
foo: 3
- parent: 0
type: Transform
- proto: """"
entities:
- uid: 1
mapInit: true
components:
- type: MetaData
- type: Transform
- type: MapGrid
chunks: {}
- type: Broadphase
- type: Physics
canCollide: False
- type: Fixtures
fixtures: {}
- type: MapSaveTileMap
- proto: MapDeserializeTest
entities:
- uid: 2
mapInit: true
components:
- type: Transform
parent: 1
- type: MapDeserializeTest
foo: 3
";
private const string Prototype = @"
@@ -57,46 +65,35 @@ entities:
bar: 2
";
protected override Type[]? ExtraComponents => new[] { typeof(MapDeserializeTestComponent), typeof(VisibilityComponent), typeof(IgnoreUIRangeComponent)};
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
var resourceManager = IoCManager.Resolve<IResourceManagerInternal>();
resourceManager.Initialize(null);
resourceManager.MountString("/TestMap.yml", MapData);
resourceManager.MountString("/EnginePrototypes/TestMapEntity.yml", Prototype);
var protoMan = IoCManager.Resolve<IPrototypeManager>();
protoMan.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype));
protoMan.LoadDirectory(new ("/EnginePrototypes"));
protoMan.LoadDirectory(new ("/Prototypes"));
protoMan.ResolveResults();
}
[Test]
public void TestDataLoadPriority()
public async Task TestDataLoadPriority()
{
// TODO: Fix after serv3
// fix what?
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.System<SharedMapSystem>().CreateMap(out var mapId);
var traversal = entMan.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
var mapLoad = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
if (!mapLoad.TryLoad(mapId, "/TestMap.yml", out var root)
|| root.FirstOrDefault() is not { Valid:true } geid)
var opts = new ServerIntegrationOptions()
{
Assert.Fail();
return;
}
ExtraPrototypes = Prototype
};
var entity = entMan.GetComponent<TransformComponent>(geid)._children.Single();
var c = entMan.GetComponent<MapDeserializeTestComponent>(entity);
var server = StartServer(opts);
await server.WaitIdleAsync();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
resourceManager.MountString("/TestMap.yml", MapData);
var traversal = server.System<SharedGridTraversalSystem>();
traversal.Enabled = false;
var mapLoad = server.System<MapLoaderSystem>();
Entity<MapGridComponent>? grid = default;
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out var mapId);
Assert.That(mapLoad.TryLoadGrid(mapId, new ResPath("/TestMap.yml"), out grid));
});
var geid = grid!.Value.Owner;
var entity = server.EntMan.GetComponent<TransformComponent>(geid)._children.Single();
var c = server.EntMan.GetComponent<MapDeserializeTestComponent>(entity);
traversal.Enabled = true;
Assert.That(c.Bar, Is.EqualTo(2));
@@ -104,7 +101,7 @@ entities:
Assert.That(c.Baz, Is.EqualTo(-1));
}
[DataDefinition]
[RegisterComponent]
private sealed partial class MapDeserializeTestComponent : Component
{
[DataField("foo")] public int Foo { get; set; } = -1;

View File

@@ -0,0 +1,143 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed class AlwaysPushSerializationTest : RobustIntegrationTest
{
private const string Prototype = @"
- type: entity
id: TestEntityCompositionParent
components:
- type: EntitySaveTest
list: [ 1, 2 ]
- type: entity
id: TestEntityCompositionChild
parent: TestEntityCompositionParent
components:
- type: EntitySaveTest
list: [ 3 , 4 ]
";
/// <summary>
/// This test checks that deserializing an entity with some component that has the
/// <see cref="AlwaysPushInheritanceAttribute"/> works as intended. Previously the attribute would cause the entity
/// prototype to **always** append it's contents to the loaded entity, effectively causing
/// the <see cref="TestAlwaysPushComponent.List"/> data-field to grow each time a map was loaded and saved.
/// </summary>
[Test]
[TestOf(typeof(AlwaysPushInheritanceAttribute))]
public async Task TestAlwaysPushSerialization()
{
var opts = new ServerIntegrationOptions
{
ExtraPrototypes = Prototype
};
var server = StartServer(opts);
await server.WaitIdleAsync();
var entMan = server.EntMan;
// Create a new map and spawn in some entities.
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> parent1 = default;
Entity<TransformComponent, EntitySaveTestComponent> parent2 = default;
Entity<TransformComponent, EntitySaveTestComponent> parent3 = default;
Entity<TransformComponent, EntitySaveTestComponent> child1 = default;
Entity<TransformComponent, EntitySaveTestComponent> child2 = default;
Entity<TransformComponent, EntitySaveTestComponent> child3 = default;
var path = new ResPath($"{nameof(TestAlwaysPushSerialization)}.yml");
await server.WaitPost(() =>
{
server.System<SharedMapSystem>().CreateMap(out mapId);
var coords = new MapCoordinates(0, 0, mapId);
var parent1Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var parent2Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var parent3Uid = entMan.Spawn("TestEntityCompositionParent", coords);
var child1Uid = entMan.Spawn("TestEntityCompositionChild", coords);
var child2Uid = entMan.Spawn("TestEntityCompositionChild", coords);
var child3Uid = entMan.Spawn("TestEntityCompositionChild", coords);
parent1 = Get(parent1Uid, entMan);
parent2 = Get(parent2Uid, entMan);
parent3 = Get(parent3Uid, entMan);
child1 = Get(child1Uid, entMan);
child2 = Get(child2Uid, entMan);
child3 = Get(child3Uid, entMan);
});
// Assign a unique id to each entity (so they can be identified after saving & loading a map)
parent1.Comp2!.Id = nameof(parent1);
parent2.Comp2!.Id = nameof(parent2);
parent3.Comp2!.Id = nameof(parent3);
child1.Comp2!.Id = nameof(child1);
child2.Comp2!.Id = nameof(child2);
child3.Comp2!.Id = nameof(child3);
// The inheritance pushing for the prototypes should ensure that the parent & child prototype's lists were merged.
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
// Modify data on some components.
parent2.Comp2.List.Add(-1);
child2.Comp2.List.Add(-1);
parent3.Comp2.List.RemoveAt(1);
child3.Comp2.List.RemoveAt(1);
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2, -1}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2, -1}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 1, 2}));
// Save map to yaml
var loader = server.System<MapLoaderSystem>();
var map = server.System<SharedMapSystem>();
Assert.That(loader.TrySaveMap(mapId, path));
// Delete the entities
await server.WaitPost(() => map.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the map
await server.WaitPost(() =>
{
Assert.That(loader.TryLoadMap(path, out var ent, out _));
mapId = ent!.Value.Comp.MapId;
});
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(6));
// Find the deserialized entities
parent1 = Find(nameof(parent1), entMan);
parent2 = Find(nameof(parent2), entMan);
parent3 = Find(nameof(parent3), entMan);
child1 = Find(nameof(child1), entMan);
child2 = Find(nameof(child2), entMan);
child3 = Find(nameof(child3), entMan);
// Verify that the entity data has not changed.
Assert.That(parent1.Comp2.List.SequenceEqual(new[] {1, 2}));
Assert.That(parent2.Comp2.List.SequenceEqual(new[] {1, 2, -1}));
Assert.That(parent3.Comp2.List.SequenceEqual(new[] {1}));
Assert.That(child1.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2}));
Assert.That(child2.Comp2.List.SequenceEqual(new[] {3, 4, 1, 2, -1}));
Assert.That(child3.Comp2.List.SequenceEqual(new[] {3, 1, 2}));
}
}

View File

@@ -0,0 +1,306 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class AutoIncludeSerializationTest : RobustIntegrationTest
{
[Test]
public async Task TestAutoIncludeSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var mapPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_map.yml");
var gridPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_grid.yml");
tileMan.Register(new TileDef("space"));
var tDef = new TileDef("a");
tileMan.Register(tDef);
// Create a map that contains an entity that references a nullspace entity.
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> grid = default;
Entity<TransformComponent, EntitySaveTestComponent> onGrid = default;
Entity<TransformComponent, EntitySaveTestComponent> offGrid = default;
Entity<TransformComponent, EntitySaveTestComponent> nullSpace = default;
void AssertCount(int expected) => Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(expected));
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId);
var gridUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridUid, Vector2i.Zero, new Tile(tDef.TileId));
var onGridUid = entMan.SpawnEntity(null, new EntityCoordinates(gridUid, 0.5f, 0.5f));
var offGridUid = entMan.SpawnEntity(null, new MapCoordinates(10f, 10f, mapId));
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
map = Get(mapUid, entMan);
grid = Get(gridUid, entMan);
onGrid = Get(onGridUid, entMan);
offGrid = Get(offGridUid, entMan);
nullSpace = Get(nullSpaceUid, entMan);
});
await server.WaitRunTicks(5);
Assert.That(map.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1!.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(nullSpace.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Assign unique ids.
map.Comp2!.Id = nameof(map);
grid.Comp2!.Id = nameof(grid);
onGrid.Comp2!.Id = nameof(onGrid);
offGrid.Comp2!.Id = nameof(offGrid);
nullSpace.Comp2!.Id = nameof(nullSpace);
// First simple map loading without any references to other entities.
// This will cause the null-space entity to be lost.
// Save the map, then delete all the entities.
AssertCount(5);
Assert.That(loader.TrySaveMap(mapId, mapPath));
Assert.That(loader.TrySaveGrid(grid, gridPath));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the file that only saved the grid and check that the expected entities exist.
await server.WaitPost(() => mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
AssertCount(2);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(0);
// Load up the map, and check that the expected entities exist.
Entity<MapComponent>? loadedMap = default;
HashSet<Entity<MapGridComponent>>? loadedGrids = default!;
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(4);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
// Re-spawn the nullspace entity
await server.WaitPost(() =>
{
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
nullSpace = Get(nullSpaceUid, entMan);
nullSpace.Comp2.Id = nameof(nullSpace);
});
// Repeat the previous saves, but with an entity that references the null-space entity.
onGrid.Comp2.Entity = nullSpace.Owner;
AssertCount(5);
Assert.That(loader.TrySaveMap(mapId, mapPath));
Assert.That(loader.TrySaveGrid(grid, gridPath));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the file that only saved the grid and check that the expected entities exist.
await server.WaitPost(() => mapSys.CreateMap(out mapId));
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
AssertCount(3);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(onGrid.Comp2.Entity, Is.EqualTo(nullSpace.Owner));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(1);
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Load up the map, and check that the expected entities exist.
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(5);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp2.Entity, Is.EqualTo(nullSpace.Owner));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Check that attempting to save a reference to a non-null-space entity does not auto-include it.
Entity<TransformComponent, EntitySaveTestComponent> otherMap = default;
await server.WaitPost(() =>
{
var otherMapUid = mapSys.CreateMap();
otherMap = Get(otherMapUid, entMan);
otherMap.Comp2.Id = nameof(otherMap);
});
onGrid.Comp2.Entity = otherMap.Owner;
// By default it should log an error, but tests don't have a nice way to validate that an error was logged, so we'll just suppress it.
var opts = SerializationOptions.Default with {MissingEntityBehaviour = MissingEntityBehaviour.Ignore};
AssertCount(6);
Assert.That(loader.TrySaveMap(mapId, mapPath, opts));
Assert.That(loader.TrySaveGrid(grid, gridPath, opts));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
AssertCount(0);
// Check the grid file
await server.WaitPost(() => mapSys.CreateMap(out mapId));
var dOpts = DeserializationOptions.Default with {LogInvalidEntities = false};
await server.WaitAssertion(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _, dOpts)));
AssertCount(2);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
AssertCount(0);
// Check the map file
await server.WaitAssertion(() => Assert.That(loader.TryLoadMap(mapPath, out loadedMap, out loadedGrids, dOpts)));
mapId = loadedMap!.Value.Comp.MapId;
Assert.That(loadedGrids, Has.Count.EqualTo(1));
AssertCount(4);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
// repeat the check, but this time with auto inclusion fully enabled.
Entity<TransformComponent, EntitySaveTestComponent> otherEnt = default;
await server.WaitPost(() =>
{
var otherMapUid = mapSys.CreateMap(out var otherMapId);
otherMap = Get(otherMapUid, entMan);
otherMap.Comp2.Id = nameof(otherMap);
var otherEntUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, otherMapId));
otherEnt = Get(otherEntUid, entMan);
otherEnt.Comp2.Id = nameof(otherEnt);
var nullSpaceUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
nullSpace = Get(nullSpaceUid, entMan);
nullSpace.Comp2.Id = nameof(nullSpace);
});
onGrid.Comp2.Entity = otherMap.Owner;
otherEnt.Comp2!.Entity = nullSpace;
AssertCount(7);
opts = opts with {MissingEntityBehaviour = MissingEntityBehaviour.AutoInclude};
Assert.That(loader.TrySaveGeneric(map.Owner, mapPath, out var cat, opts));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
Assert.That(loader.TrySaveGeneric(grid.Owner, gridPath, out cat, opts));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Check the grid file
await server.WaitPost(() => mapSys.CreateMap(out mapId));
var mapLoadOpts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
LoadResult? result = default;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(gridPath, out result, mapLoadOpts)));
Assert.That(result!.Grids, Has.Count.EqualTo(1));
Assert.That(result.Orphans, Is.Empty); // Grid was orphaned, but was adopted after a new map was created
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1)); // auto-generated map isn't marked as "loaded"
AssertCount(5);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(grid.Comp1.ParentUid));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
// Check the map file
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(mapPath, out result)));
Assert.That(result.Orphans, Is.Empty);
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(result.Grids, Has.Count.EqualTo(1));
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(2));
AssertCount(7);
map = Find(nameof(map), entMan);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
offGrid = Find(nameof(offGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(offGrid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(map));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
}
}

View File

@@ -0,0 +1,432 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v3 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// See also the comments around <see cref="EntityDeserializer.OldestSupportedVersion"/> that point out that v3
/// isn't even really loadable anymore.
/// </remarks>
[Test]
public async Task TestLoadV3()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV3});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
tileMan.Register(new TileDef("Space"));
for (var i = 1; i <= 88; i++)
{
tileMan.Register(new TileDef(i.ToString()));
}
var gridPath = new ResPath($"{nameof(MapDataV3Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV3Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV3Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV3Map);
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV3Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV3Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
private const string MapDataV3Grid = @"
meta:
format: 3
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
10: 10
11: 11
12: 12
13: 13
14: 14
15: 15
16: 16
17: 17
18: 18
19: 19
20: 20
21: 21
22: 22
23: 23
24: 24
25: 25
26: 26
27: 27
28: 28
29: 29
30: 30
31: 31
32: 32
33: 33
34: 34
35: 35
36: 36
37: 37
38: 38
39: 39
40: 40
41: 41
42: 42
43: 43
44: 44
45: 45
46: 46
47: 47
48: 48
49: 49
50: 50
51: 51
52: 52
53: 53
54: 54
55: 55
56: 56
57: 57
58: 58
59: 59
60: 60
61: 61
62: 62
63: 63
64: 64
65: 65
66: 66
67: 67
68: 68
69: 69
70: 70
71: 71
72: 72
73: 73
74: 74
75: 75
76: 76
77: 77
78: 78
79: 79
80: 80
81: 81
82: 82
83: 83
84: 84
85: 85
86: 86
87: 87
88: 88
entities:
- uid: 0
components:
- type: MetaData
- parent: null
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAPgAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- uid: 1
type: V3TestProto
components:
- pos: 0.5,0.5
parent: 0
type: Transform
- id: ent
type: EntitySaveTest
...
";
private const string MapDataV3Map = @"
meta:
format: 3
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
10: 10
11: 11
12: 12
13: 13
14: 14
15: 15
16: 16
17: 17
18: 18
19: 19
20: 20
21: 21
22: 22
23: 23
24: 24
25: 25
26: 26
27: 27
28: 28
29: 29
30: 30
31: 31
32: 32
33: 33
34: 34
35: 35
36: 36
37: 37
38: 38
39: 39
40: 40
41: 41
42: 42
43: 43
44: 44
45: 45
46: 46
47: 47
48: 48
49: 49
50: 50
51: 51
52: 52
53: 53
54: 54
55: 55
56: 56
57: 57
58: 58
59: 59
60: 60
61: 61
62: 62
63: 63
64: 64
65: 65
66: 66
67: 67
68: 68
69: 69
70: 70
71: 71
72: 72
73: 73
74: 74
75: 75
76: 76
77: 77
78: 78
79: 79
80: 80
81: 81
82: 82
83: 83
84: 84
85: 85
86: 86
87: 87
88: 88
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 0
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAPgAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- uid: 1
type: V3TestProto
components:
- pos: 0.5,0.5
parent: 0
type: Transform
- id: ent
type: EntitySaveTest
...
";
private const string PrototypeV3 = @"
- type: entity
id: V3TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
";
}

View File

@@ -0,0 +1,257 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v4 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV4()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV4});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
tileMan.Register(new TileDef("Space"));
tileMan.Register(new TileDef("A"));
tileMan.Register(new TileDef("B"));
var gridPath = new ResPath($"{nameof(MapDataV4Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV4Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV4Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV4Map);
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV4Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV4Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
private const string MapDataV4Grid = @"
meta:
format: 4
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
11: A
68: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- parent: invalid
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARAAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V4TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
...
";
private const string MapDataV4Map = @"
meta:
format: 4
name: DemoStation
author: Space-Wizards
postmapinit: false
tilemap:
0: Space
11: A
68: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARAAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V4TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string PrototypeV4 = @"
- type: entity
id: V4TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
";
}

View File

@@ -0,0 +1,256 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v5 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV5()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV5});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
tileMan.Register(new TileDef("Space"));
tileMan.Register(new TileDef("A"));
tileMan.Register(new TileDef("B"));
var gridPath = new ResPath($"{nameof(MapDataV5Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV5Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV5Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV5Map);
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV5Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV5Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
private const string MapDataV5Grid = @"
meta:
format: 5
postmapinit: false
tilemap:
0: Space
11: A
69: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- parent: invalid
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARQAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V5TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string MapDataV5Map = @"
meta:
format: 5
postmapinit: false
tilemap:
0: Space
11: A
69: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- parent: 123
type: Transform
- chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAARQAAAA==
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
type: MapGrid
- type: Broadphase
- angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
type: Physics
- fixtures: {}
type: Fixtures
- type: OccluderTree
- id: grid
type: EntitySaveTest
- proto: V5TestProto
entities:
- uid: 1
components:
- pos: 0.5,0.5
parent: 2
type: Transform
- id: ent
type: EntitySaveTest
";
private const string PrototypeV5 = @"
- type: entity
id: V5TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
";
}

View File

@@ -0,0 +1,263 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class BackwardsCompatibilityTest
{
/// <summary>
/// Check that v6 maps can be loaded. This simply tries to load a file and doesn't do a lot of extra validation.
/// </summary>
/// <remarks>
/// The file was pilfered from content integration tests ("floor3x3.yml") and modified slightly.
/// </remarks>
[Test]
public async Task TestLoadV6()
{
var server = StartServer(new ServerIntegrationOptions {ExtraPrototypes = PrototypeV6});
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var meta = server.System<MetaDataSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
tileMan.Register(new TileDef("Space"));
tileMan.Register(new TileDef("A"));
tileMan.Register(new TileDef("B"));
var gridPath = new ResPath($"{nameof(MapDataV6Grid)}.yml");
resourceManager.MountString(gridPath.ToString(), MapDataV6Grid);
MapId mapId = default;
EntityUid mapUid = default;
Entity<TransformComponent, EntitySaveTestComponent> map;
Entity<TransformComponent, EntitySaveTestComponent> ent;
Entity<TransformComponent, EntitySaveTestComponent> grid;
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => mapUid = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, gridPath, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(mapUid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(mapUid), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(mapUid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(mapUid));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var mapPath = new ResPath($"{nameof(MapDataV6Map)}.yml");
resourceManager.MountString(mapPath.ToString(), MapDataV6Map);
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath, out _, out _)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.True);
Assert.That(meta.EntityPaused(grid), Is.True);
Assert.That(meta.EntityPaused(map), Is.True);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.Initialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Repeat test, but with the initialize maps option enabled.
// Apparently mounted strings can only be read a single time.
// So have to re-mount them.
var mapPath2 = new ResPath($"{nameof(MapDataV6Map)}2.yml");
resourceManager.MountString(mapPath2.ToString(), MapDataV6Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await server.WaitPost(() => Assert.That(loader.TryLoadMap(mapPath2, out _, out _, opts)));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
map = Find(nameof(map), entMan);
Assert.That(ent.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(grid.Comp1.ParentUid, Is.EqualTo(map.Owner));
Assert.That(map.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(meta.EntityPaused(ent), Is.False);
Assert.That(meta.EntityPaused(grid), Is.False);
Assert.That(meta.EntityPaused(map), Is.False);
Assert.That(entMan.GetComponent<MetaDataComponent>(ent).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(grid).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
Assert.That(entMan.GetComponent<MetaDataComponent>(map).EntityLifeStage,
Is.EqualTo(EntityLifeStage.MapInitialized));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
private const string MapDataV6Grid = @"
meta:
format: 6
postmapinit: false
tilemap:
0: Space
11: A
89: B
entities:
- proto: """"
entities:
- uid: 2
components:
- type: MetaData
- type: Transform
parent: invalid
- type: MapGrid
chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAWQAAAAAA
version: 6
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
- type: Broadphase
- type: Physics
bodyStatus: InAir
angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
- type: OccluderTree
- type: EntitySaveTest
id: grid
- proto: V6TestProto
entities:
- uid: 1
components:
- type: Transform
pos: 0.5,0.5
parent: 2
- type: EntitySaveTest
id: ent
";
private const string MapDataV6Map = @"
meta:
format: 6
postmapinit: false
tilemap:
0: Space
11: A
89: B
entities:
- proto: """"
entities:
- uid: 123
components:
- type: MetaData
- type: Transform
- type: Map
mapPaused: True
- type: EntitySaveTest
id: map
- uid: 2
components:
- type: MetaData
- type: Transform
parent: 123
- type: MapGrid
chunks:
-1,-1:
ind: -1,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAWQAAAAAA
version: 6
-1,0:
ind: -1,0
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,0:
ind: 0,0
tiles: CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
0,-1:
ind: 0,-1
tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
version: 6
- type: Broadphase
- type: Physics
bodyStatus: InAir
angularDamping: 0.05
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
- type: OccluderTree
- type: EntitySaveTest
id: grid
- proto: V6TestProto
entities:
- uid: 1
components:
- type: Transform
pos: 0.5,0.5
parent: 2
- type: EntitySaveTest
id: ent
";
private const string PrototypeV6 = @"
- type: entity
id: V6TestProto
components:
- type: EntitySaveTest
list: [ 1, 2 ]
";
}

View File

@@ -0,0 +1,344 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
/// <summary>
/// Test that older file formats can still be loaded.
/// </summary>
[TestFixture]
public sealed partial class BackwardsCompatibilityTest : RobustIntegrationTest
{
/// <summary>
/// Check that v7 maps can be loaded. This just re-uses some map files that are generated by other tests, and then
/// checks that the post-load debug asserts still pass. specifically, it uses the second to last file from
/// <see cref="AutoIncludeSerializationTest"/> and the initial file from
/// <see cref="LifestageSerializationTest.TestMixedLifestageSerialization"/>.
/// </summary>
/// <remarks>
/// At the time of writing, v7 is the current version, but this is here for when the version increases in the future.
/// </remarks>
[Test]
public async Task TestLoadV7()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var resourceManager = server.ResolveDependency<IResourceManagerInternal>();
tileMan.Register(new TileDef("space"));
tileMan.Register(new TileDef("a"));
void AssertCount(int expected) => Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(expected));
await server.WaitPost(() => mapSys.CreateMap(out _));
var mapLoadOpts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
// Test the file from AutoIncludeSerializationTest
{
var path = new ResPath($"{nameof(MapDataV7)}.yml");
resourceManager.MountString(path.ToString(), MapDataV7);
Entity<TransformComponent, EntitySaveTestComponent> grid;
Entity<TransformComponent, EntitySaveTestComponent> onGrid;
Entity<TransformComponent, EntitySaveTestComponent> otherMap;
Entity<TransformComponent, EntitySaveTestComponent> otherEnt;
Entity<TransformComponent, EntitySaveTestComponent> nullSpace;
LoadResult? result = default;
await server.WaitAssertion(() => Assert.That(loader.TryLoadGeneric(path, out result, mapLoadOpts)));
Assert.That(result!.Version, Is.EqualTo(7));
Assert.That(result.Grids, Has.Count.EqualTo(1));
Assert.That(result.Orphans, Is.Empty); // Grid was orphaned, but was adopted after a new map was created
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(result.NullspaceEntities, Has.Count.EqualTo(1));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(1)); // auto-generated map isn't marked as "loaded"
AssertCount(5);
grid = Find(nameof(grid), entMan);
onGrid = Find(nameof(onGrid), entMan);
otherMap = Find(nameof(otherMap), entMan);
otherEnt = Find(nameof(otherEnt), entMan);
nullSpace = Find(nameof(nullSpace), entMan);
Assert.That(onGrid.Comp1.ParentUid, Is.EqualTo(grid.Owner));
Assert.That(otherEnt.Comp1.ParentUid, Is.EqualTo(otherMap.Owner));
Assert.That(otherMap.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(nullSpace.Comp1.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(otherMap));
await server.WaitPost(() => entMan.DeleteEntity(grid.Comp1.ParentUid));
await server.WaitPost(() => entMan.DeleteEntity(nullSpace));
AssertCount(0);
}
// Test the file from LifestageSerializationTest.TestMixedLifestageSerialization
{
var pathLifestage = new ResPath($"{nameof(MapDataV7Lifestage)}.yml");
resourceManager.MountString(pathLifestage.ToString(), MapDataV7Lifestage);
Entity<TransformComponent, EntitySaveTestComponent> mapA; // preinit Map
Entity<TransformComponent, EntitySaveTestComponent> mapB; // postinit unpaused Map
Entity<TransformComponent, EntitySaveTestComponent> entA; // postinit entity on preinit map
Entity<TransformComponent, EntitySaveTestComponent> entB; // paused entity on postinit unpaused map
Entity<TransformComponent, EntitySaveTestComponent> entC; // preinit entity on postinit map
Entity<TransformComponent, EntitySaveTestComponent> nullA; // postinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullB; // preinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullC; // paused postinit nullspace entity
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
LoadResult? result = default;
await server.WaitPost(() => Assert.That(loader.TryLoadGeneric(pathLifestage, out result)));
Assert.That(result!.Version, Is.EqualTo(7));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
mapA = Find(nameof(mapA), entMan);
mapB = Find(nameof(mapB), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
entC = Find(nameof(entC), entMan);
nullA = Find(nameof(nullA), entMan);
nullB = Find(nameof(nullB), entMan);
nullC = Find(nameof(nullC), entMan);
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
}
}
private const string MapDataV7 = @"
meta:
format: 7
category: Unknown
engineVersion: 238.0.0
forkId: """"
forkVersion: """"
time: 12/25/2024 00:40:09
entityCount: 5
maps:
- 3
grids:
- 1
orphans:
- 1
nullspace:
- 5
tilemap:
1: space
0: a
entities:
- proto: """"
entities:
- uid: 1
paused: false
components:
- type: MetaData
name: grid
- type: Transform
parent: invalid
- type: MapGrid
chunks:
0,0:
ind: 0,0
tiles: AAAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAAAQAAAAAA
version: 6
- type: Broadphase
- type: Physics
- type: Fixtures
fixtures: {}
- type: OccluderTree
- type: EntitySaveTest
list: []
id: grid
- uid: 2
mapInit: true
components:
- type: MetaData
- type: Transform
pos: 0.5,0.5
parent: 1
- type: EntitySaveTest
list: []
entity: 3
id: onGrid
- uid: 3
mapInit: true
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapInitialized: True
- type: PhysicsMap
- type: GridTree
- type: MovedGrids
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: otherMap
- uid: 4
mapInit: true
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
entity: 5
id: otherEnt
- uid: 5
mapInit: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullSpace
";
private const string MapDataV7Lifestage = @"
meta:
format: 7
category: Unknown
engineVersion: 238.0.0
forkId: """"
forkVersion: """"
time: 12/25/2024 00:50:59
entityCount: 8
maps:
- 1
- 3
grids: []
orphans: []
nullspace:
- 6
- 7
- 8
tilemap: {}
entities:
- proto: """"
entities:
- uid: 1
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapPaused: True
- type: PhysicsMap
- type: GridTree
- type: MovedGrids
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: mapA
- uid: 2
mapInit: true
components:
- type: MetaData
- type: Transform
parent: 1
- type: EntitySaveTest
list: []
id: entA
- uid: 3
mapInit: true
components:
- type: MetaData
name: Map Entity
- type: Transform
- type: Map
mapInitialized: True
- type: PhysicsMap
- type: GridTree
- type: MovedGrids
- type: Broadphase
- type: OccluderTree
- type: EntitySaveTest
list: []
id: mapB
- uid: 4
mapInit: true
paused: true
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
id: entB
- uid: 5
paused: false
components:
- type: MetaData
- type: Transform
parent: 3
- type: EntitySaveTest
list: []
id: entC
- uid: 6
mapInit: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullA
- uid: 7
paused: false
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullB
- uid: 8
mapInit: true
paused: true
components:
- type: MetaData
- type: Transform
- type: EntitySaveTest
list: []
id: nullC
";
}

View File

@@ -0,0 +1,129 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class CategorizationTest : RobustIntegrationTest
{
/// <summary>
/// Check that file categories are correctly assigned when saving & loading different combinations of entites.
/// </summary>
[Test]
[TestOf(typeof(FileCategory))]
public async Task TestCategorization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var meta = server.System<MetaDataSystem>();
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var path = new ResPath($"{nameof(TestCategorization)}.yml");
tileMan.Register(new TileDef("space"));
var tDef = new TileDef("a");
tileMan.Register(tDef);
EntityUid mapA = default;
EntityUid mapB = default;
EntityUid gridA = default; // grid on map A
EntityUid gridB = default; // grid on map B
EntityUid entA = default; // ent on grid A
EntityUid entB = default; // ent on grid B
EntityUid entC = default; // a separate entity on grid B
EntityUid child = default; // child of entB
EntityUid @null = default; // nullspace entity
await server.WaitPost(() =>
{
mapA = mapSys.CreateMap(out var mapIdA);
mapB = mapSys.CreateMap(out var mapIdB);
var gridEntA = mapMan.CreateGridEntity(mapIdA);
var gridEntB = mapMan.CreateGridEntity(mapIdB);
mapSys.SetTile(gridEntA, Vector2i.Zero, new Tile(tDef.TileId));
mapSys.SetTile(gridEntB, Vector2i.Zero, new Tile(tDef.TileId));
gridA = gridEntA.Owner;
gridB = gridEntB.Owner;
entA = entMan.SpawnEntity(null, new EntityCoordinates(gridA, 0.5f, 0.5f));
entB = entMan.SpawnEntity(null, new EntityCoordinates(gridB, 0.5f, 0.5f));
entC = entMan.SpawnEntity(null, new EntityCoordinates(gridB, 0.5f, 0.5f));
child = entMan.SpawnEntity(null, new EntityCoordinates(entB, 0.5f, 0.5f));
@null = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
});
FileCategory Save(params EntityUid[] ents)
{
FileCategory cat = FileCategory.Unknown;
Assert.That(loader.TrySaveGeneric(ents.ToHashSet(), path, out cat));
return cat;
}
async Task<LoadResult> Load(FileCategory expected, int count)
{
var opts = MapLoadOptions.Default with
{
ExpectedCategory = expected,
DeserializationOptions = DeserializationOptions.Default with { LogOrphanedGrids = false}
};
LoadResult? result = null;
await server.WaitPost(() => Assert.That(loader.TryLoadGeneric(path, out result, opts)));
Assert.That(result!.Category, Is.EqualTo(expected));
Assert.That(result.Entities, Has.Count.EqualTo(count));
return result;
}
async Task SaveAndLoad(FileCategory expected, int count, params EntityUid[] ents)
{
var cat = Save(ents);
Assert.That(cat, Is.EqualTo(expected));
var result = await Load(expected, count);
await server.WaitPost(() => loader.Delete(result));
}
// Saving a single entity works as expected, even if it also serializes their children
await SaveAndLoad(FileCategory.Entity, 1, entA);
await SaveAndLoad(FileCategory.Entity, 2, entB);
await SaveAndLoad(FileCategory.Entity, 1, child);
// Including nullspace entities doesn't change the category, though a file containing only null-space entities
// is "unkown". Maybe in future they will get their own category
await SaveAndLoad(FileCategory.Entity, 2, entA, @null);
await SaveAndLoad(FileCategory.Entity, 3, entB, @null);
await SaveAndLoad(FileCategory.Entity, 2, child, @null);
await SaveAndLoad(FileCategory.Unknown, 1, @null);
// More than one entity is unknown
await SaveAndLoad(FileCategory.Unknown, 3, entA, entB);
await SaveAndLoad(FileCategory.Unknown, 4, entA, entB, @null);
// Saving grids works as expected. All counts are 1 higher than expected due to a map being automatically created.
await SaveAndLoad(FileCategory.Grid, 3, gridA);
await SaveAndLoad(FileCategory.Grid, 5, gridB);
await SaveAndLoad(FileCategory.Grid, 4, gridA, @null);
await SaveAndLoad(FileCategory.Grid, 6, gridB, @null);
// And saving maps also works
await SaveAndLoad(FileCategory.Map, 3, mapA);
await SaveAndLoad(FileCategory.Map, 5, mapB);
await SaveAndLoad(FileCategory.Map, 4, mapA, @null);
await SaveAndLoad(FileCategory.Map, 6, mapB, @null);
// Combinations of grids, entities, and maps, are unknown
await SaveAndLoad(FileCategory.Unknown, 4, mapA, child);
await SaveAndLoad(FileCategory.Unknown, 4, gridA, child);
await SaveAndLoad(FileCategory.Unknown, 8, gridA, mapB);
await SaveAndLoad(FileCategory.Unknown, 5, mapA, child, @null);
await SaveAndLoad(FileCategory.Unknown, 5, gridA, child, @null);
await SaveAndLoad(FileCategory.Unknown, 9, gridA, mapB, @null);
}
}

View File

@@ -0,0 +1,374 @@
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class LifestageSerializationTest : RobustIntegrationTest
{
/// <summary>
/// Check that whether or not an entity has been paused or map-initialized is preserved across saves & loads.
/// </summary>
[Test]
public async Task TestLifestageSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var preInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_preInit.yml");
var postInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_postInit.yml");
var pausedPostInitPath = new ResPath($"{nameof(TestLifestageSerialization)}_paused.yml");
// Create a pre-init map, and spawn multiple entities on it
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> entA = default;
Entity<TransformComponent, EntitySaveTestComponent> entB = default;
Entity<TransformComponent, EntitySaveTestComponent> childA = default;
Entity<TransformComponent, EntitySaveTestComponent> childB = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out var mapId, runMapInit: false);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var childAUid = entMan.SpawnEntity(null, new EntityCoordinates(entAUid, 0, 0));
var childBUid = entMan.SpawnEntity(null, new EntityCoordinates(entBUid, 0, 0));
map = Get(mapUid, entMan);
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
childA = Get(childAUid, entMan);
childB = Get(childBUid, entMan);
map.Comp2.Id = nameof(map);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
childA.Comp2.Id = nameof(childA);
childB.Comp2.Id = nameof(childB);
});
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
// All entities should initially be un-initialized and paused.
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(true, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, preInitPath));
async Task Delete()
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(5));
await server.WaitPost(() => entMan.DeleteEntity(map));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
async Task Load(ResPath f, DeserializationOptions? o)
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
await server.WaitPost(() => Assert.That(loader.TryLoadMap(f, out _, out _, o)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(5));
}
void FindAll()
{
map = Find(nameof(map), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
childA = Find(nameof(childA), entMan);
childB = Find(nameof(childB), entMan);
}
async Task Reload(ResPath f, DeserializationOptions? o = null)
{
await Delete();
await Load(f, o);
FindAll();
}
// Saving and loading the pre-init map should have no effect.
await Reload(preInitPath);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(true, map, entA, entB, childA, childB);
// Saving and loading with the map-init option set to true should initialize & unpause all entities
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(preInitPath, opts);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, postInitPath));
// re-loading the post-init map should keep everything initialized, even without explicitly asking to initialize maps.
await Reload(postInitPath);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// Load & initialize a pre-init map, but with the pause maps option enabled.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(preInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
Assert.That(loader.TrySaveMap(map, pausedPostInitPath));
// The pause map option also works when loading un-paused post-init maps
opts = DeserializationOptions.Default with {PauseMaps = true};
await Reload(postInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// loading & initializing a post-init map should cause no issues.
opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(postInitPath, opts);
AssertPaused(false, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// Loading a paused post init map does NOT automatically un-pause entities
await Reload(pausedPostInitPath);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// The above holds even if we are explicitly initialising maps.
opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(pausedPostInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
// And re-paused an already paused map should have no impact.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(pausedPostInitPath, opts);
AssertPaused(true, map, entA, entB, childA, childB);
AssertPreInit(false, map, entA, entB, childA, childB);
}
/// <summary>
/// Variant of <see cref="TestLifestageSerialization"/> that has multiple maps and combinations. E.g., a single
/// paused entity on an un-paused map.
/// </summary>
[Test]
public async Task TestMixedLifestageSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var meta = server.System<MetaDataSystem>();
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var path = new ResPath($"{nameof(TestMixedLifestageSerialization)}.yml");
var altPath = new ResPath($"{nameof(TestMixedLifestageSerialization)}_alt.yml");
Entity<TransformComponent, EntitySaveTestComponent> mapA = default; // preinit Map
Entity<TransformComponent, EntitySaveTestComponent> mapB = default; // postinit unpaused Map
Entity<TransformComponent, EntitySaveTestComponent> entA = default; // postinit entity on preinit map
Entity<TransformComponent, EntitySaveTestComponent> entB = default; // paused entity on postinit unpaused map
Entity<TransformComponent, EntitySaveTestComponent> entC = default; // preinit entity on postinit map
Entity<TransformComponent, EntitySaveTestComponent> nullA = default; // postinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullB = default; // preinit nullspace entity
Entity<TransformComponent, EntitySaveTestComponent> nullC = default; // paused postinit nullspace entity
await server.WaitPost(() =>
{
var mapAUid = mapSys.CreateMap(out var mapIdA, runMapInit: false);
var mapBUid = mapSys.CreateMap(out var mapIdB, runMapInit: true);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapIdA));
entMan.RunMapInit(entAUid, entMan.GetComponent<MetaDataComponent>(entAUid));
meta.SetEntityPaused(entAUid, false);
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapIdB));
meta.SetEntityPaused(entBUid, true);
var entCUid = entMan.CreateEntityUninitialized(null, new MapCoordinates(0, 0, mapIdB));
entMan.InitializeAndStartEntity(entCUid, doMapInit: false);
var nullAUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var nullBUid = entMan.CreateEntityUninitialized(null, MapCoordinates.Nullspace);
entMan.InitializeAndStartEntity(nullBUid, doMapInit: false);
var nullCUid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
meta.SetEntityPaused(nullCUid, true);
mapA = Get(mapAUid, entMan);
mapB = Get(mapBUid, entMan);
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
entC = Get(entCUid, entMan);
nullA = Get(nullAUid, entMan);
nullB = Get(nullBUid, entMan);
nullC = Get(nullCUid, entMan);
mapA.Comp2.Id = nameof(mapA);
mapB.Comp2.Id = nameof(mapB);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
entC.Comp2.Id = nameof(entC);
nullA.Comp2.Id = nameof(nullA);
nullB.Comp2.Id = nameof(nullB);
nullC.Comp2.Id = nameof(nullC);
});
string? Name(EntityUid uid)
{
return entMan.GetComponentOrNull<EntitySaveTestComponent>(uid)?.Id;
}
void AssertPaused(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected), Name(uid));
}
}
void AssertPreInit(bool expected, params EntityUid[] uids)
{
foreach (var uid in uids)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
}
void Save(ResPath f)
{
Assert.That(loader.TrySaveGeneric([mapA, mapB, nullA, nullB, nullC], f, out _));
}
async Task Delete()
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
await server.WaitPost(() => entMan.DeleteEntity(mapA));
await server.WaitPost(() => entMan.DeleteEntity(mapB));
await server.WaitPost(() => entMan.DeleteEntity(nullA));
await server.WaitPost(() => entMan.DeleteEntity(nullB));
await server.WaitPost(() => entMan.DeleteEntity(nullC));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
async Task Load(ResPath f, DeserializationOptions? o)
{
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
var oo = MapLoadOptions.Default with
{
DeserializationOptions = o ?? DeserializationOptions.Default
};
await server.WaitPost(() => Assert.That(loader.TryLoadGeneric(f, out _, oo)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(8));
}
void FindAll()
{
mapA = Find(nameof(mapA), entMan);
mapB = Find(nameof(mapB), entMan);
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
entC = Find(nameof(entC), entMan);
nullA = Find(nameof(nullA), entMan);
nullB = Find(nameof(nullB), entMan);
nullC = Find(nameof(nullC), entMan);
}
async Task Reload(ResPath f, DeserializationOptions? o = null)
{
await Delete();
await Load(f, o);
FindAll();
}
// All entities should initially be in their respective expected states.
// entC (pre-mapinit entity on a post-mapinit map) is a bit fucky, and I don't know if that should even be allowed.
// Note that its just pre-init, not paused, as pre-mapinit entities get paused due to the maps state, not as a general result of being pre-mapinit.
// If this ever changes, these assers need fixing.
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Saving and re-loading entities should leave their metadata unchanged.
Save(path);
await Reload(path);
AssertPaused(true, mapA, entB, nullC);
AssertPaused(false, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// reload maps with the mapinit option. This should only affect mapA, as entA is the only one on the map and it
// is already initialized,
var opts = DeserializationOptions.Default with {InitializeMaps = true};
await Reload(path, opts);
AssertPaused(true, entB, nullC);
AssertPaused(false, mapA, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, entB, nullC);
AssertPaused(false, mapA, mapB, entA, entC, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Pause all maps. This will not actually pause entityA, as mapA is already paused (due to being pre-init), so
// it will not iterate through its children. Maybe this will change in future, but I don't think we should even
// be trying to actively support having post-init entities on a pre-init map. This is subject to maybe change
// one day, though if it does the option should be changed to PauseEntities to clarify that it will pause ALL
// entities, not just maps.
opts = DeserializationOptions.Default with {PauseMaps = true};
await Reload(path, opts);
AssertPaused(true, mapA, mapB, entC, entB, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, mapA, mapB, entC, entB, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, mapA, entC, nullB);
AssertPreInit(false, mapB, entA, entB, nullA, nullC);
// Initialise and pause all maps. Similar to the previous test with entA, this will not affect entC even
// though it is pre-init, because it is on a post-init map. Again, this is subject to maybe change one day.
// Though if it does, the option should be changed to MapInitializeEntities to clarify that it will mapinit ALL
// entities, not just maps.
opts = DeserializationOptions.Default with {InitializeMaps = true, PauseMaps = true};
await Reload(path, opts);
AssertPaused(true, mapA, mapB, entB, entC, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
// Reloading the new configuration changes nothing
Save(altPath);
await Reload(altPath, opts);
AssertPaused(true, mapA, mapB, entB, entC, nullC);
AssertPaused(false, entA, nullA, nullB);
AssertPreInit(true, entC, nullB);
AssertPreInit(false, mapA, mapB, entA, entB, nullA, nullC);
}
}

View File

@@ -0,0 +1,231 @@
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
/// <summary>
/// Test that loading a pre-init map/grid onto a post-init map should initialize, while loading a post-init map/grid
/// onto a paused map should pause it.
/// </summary>
[TestFixture]
public sealed partial class MapMergeTest : RobustIntegrationTest
{
[Test]
public async Task TestMapMerge()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var mapPath = new ResPath($"{nameof(TestMapMerge)}_map.yml");
var gridPath = new ResPath($"{nameof(TestMapMerge)}_grid.yml");
tileMan.Register(new TileDef("space"));
var tDef = new TileDef("a");
tileMan.Register(tDef);
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> ent = default;
Entity<TransformComponent, EntitySaveTestComponent> grid = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId, runMapInit: false);
var gridEnt = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridEnt, Vector2i.Zero, new Tile(tDef.TileId));
var entUid = entMan.SpawnEntity(null, new MapCoordinates(10, 10, mapId));
map = Get(mapUid, entMan);
ent = Get(entUid, entMan);
grid = Get(gridEnt.Owner, entMan);
});
void AssertPaused(EntityUid uid, bool expected = true)
{
Assert.That(entMan.GetComponent<MetaDataComponent>(uid).EntityPaused, Is.EqualTo(expected));
}
void AssertPreInit(EntityUid uid, bool expected = true)
{
Assert.That(entMan!.GetComponent<MetaDataComponent>(uid).EntityLifeStage,
expected
? Is.LessThan(EntityLifeStage.MapInitialized)
: Is.EqualTo(EntityLifeStage.MapInitialized));
}
map.Comp2!.Id = nameof(map);
ent.Comp2!.Id = nameof(ent);
grid.Comp2!.Id = nameof(grid);
AssertPaused(map);
AssertPreInit(map);
AssertPaused(ent);
AssertPreInit(ent);
AssertPaused(grid);
AssertPreInit(grid);
// Save then delete everything
Assert.That(loader.TrySaveMap(map, mapPath));
Assert.That(loader.TrySaveGrid(grid, gridPath));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a pre-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
Assert.That(loader.TryLoadGrid(mapId, gridPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a pre-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
Assert.That(loader.TryMergeMap(mapId, mapPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2)); // The loaded map entity gets deleted after merging
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
AssertPaused(ent);
AssertPreInit(ent);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
Assert.That(loader.TryLoadGrid(mapId, gridPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
Assert.That(loader.TryMergeMap(mapId, mapPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
AssertPaused(ent, false);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a paused post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
await server.WaitPost(() => mapSys.SetPaused(mapId, true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.True);
Assert.That(loader.TryLoadGrid(mapId, gridPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a paused post-init map.
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
await server.WaitPost(() => mapSys.SetPaused(mapId, true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.True);
Assert.That(loader.TryMergeMap(mapId, mapPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid, false);
AssertPaused(ent);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Check that the map initialization deserialziation options have no effect.
// We are loading onto an existing map, deserialization shouldn't modify it directly.
// Load a grid onto a pre-init map, with InitializeMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
Assert.That(loader.TryLoadGrid(mapId, gridPath, out _, opts));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Merge a map onto a pre-init map, with InitializeMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: false));
Assert.That(mapSys.IsInitialized(mapId), Is.False);
Assert.That(mapSys.IsPaused(mapId), Is.True);
opts = DeserializationOptions.Default with {InitializeMaps = true};
Assert.That(loader.TryMergeMap(mapId, mapPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2)); // The loaded map entity gets deleted after merging
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid);
AssertPreInit(grid);
AssertPaused(ent);
AssertPreInit(ent);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map, with PauseMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
opts = DeserializationOptions.Default with {PauseMaps = true};
Assert.That(loader.TryLoadGrid(mapId, gridPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load a grid onto a post-init map, with PauseMaps = true
await server.WaitPost(() => mapSys.CreateMap(out mapId, runMapInit: true));
Assert.That(mapSys.IsInitialized(mapId), Is.True);
Assert.That(mapSys.IsPaused(mapId), Is.False);
opts = DeserializationOptions.Default with {PauseMaps = true};
Assert.That(loader.TryMergeMap(mapId, mapPath, out _));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
ent = Find(nameof(ent), entMan);
grid = Find(nameof(grid), entMan);
AssertPaused(grid, false);
AssertPreInit(grid, false);
AssertPaused(ent, false);
AssertPreInit(ent, false);
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,233 @@
using System.Numerics;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using static Robust.UnitTesting.Shared.EntitySerialization.EntitySaveTestComponent;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[TestFixture]
public sealed partial class OrphanSerializationTest : RobustIntegrationTest
{
/// <summary>
/// Check that we can save & load a file containing multiple orphaned (non-grid) entities.
/// </summary>
[Test]
public async Task TestMultipleOrphanSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var xform = server.System<SharedTransformSystem>();
var pathA = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_A.yml");
var pathB = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_B.yml");
var pathCombined = new ResPath($"{nameof(TestMultipleOrphanSerialization)}_C.yml");
// Spawn multiple entities on a map
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> entA = default;
Entity<TransformComponent, EntitySaveTestComponent> entB = default;
Entity<TransformComponent, EntitySaveTestComponent> child = default;
await server.WaitPost(() =>
{
mapSys.CreateMap(out mapId);
var entAUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var entBUid = entMan.SpawnEntity(null, new MapCoordinates(0, 0, mapId));
var childUid = entMan.SpawnEntity(null, new EntityCoordinates(entBUid, 0, 0));
entA = Get(entAUid, entMan);
entB = Get(entBUid, entMan);
child = Get(childUid, entMan);
entA.Comp2.Id = nameof(entA);
entB.Comp2.Id = nameof(entB);
child.Comp2.Id = nameof(child);
xform.SetLocalPosition(entB.Owner, new (100,100));
});
// Entities are not in null-space
Assert.That(entA.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
// Save the entities without their map
Assert.That(loader.TrySaveEntity(entA, pathA));
Assert.That(loader.TrySaveEntity(entB, pathB));
Assert.That(loader.TrySaveGeneric([entA.Owner, entB.Owner], pathCombined, out var cat));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
// Delete all the entities.
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing only entA.
await server.WaitPost(() => Assert.That(loader.TryLoadEntity(pathA, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
entA = Find(nameof(entA), entMan);
Assert.That(entA.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
await server.WaitPost(() => entMan.DeleteEntity(entA));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing entB and its child
await server.WaitPost(() => Assert.That(loader.TryLoadEntity(pathB, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
entB = Find(nameof(entB), entMan);
child = Find(nameof(child), entMan);
// Even though the entities are in null-space their local position is preserved.
// This is so that you can save multiple entities on a map, without saving the map, while still preserving
// relative positions for loading them onto some other map.
Assert.That(entB.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(entB.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
await server.WaitPost(() => entMan.DeleteEntity(entB));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the file that contains both of them
LoadResult? result = null;
await server.WaitPost(() => Assert.That(loader.TryLoadGeneric(pathCombined, out result)));
Assert.That(result!.Category, Is.EqualTo(FileCategory.Unknown));
Assert.That(result.Orphans, Has.Count.EqualTo(2));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
entA = Find(nameof(entA), entMan);
entB = Find(nameof(entB), entMan);
child = Find(nameof(child), entMan);
Assert.That(entA.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
Assert.That(entB.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(entB.Owner));
await server.WaitPost(() => entMan.DeleteEntity(entA));
await server.WaitPost(() => entMan.DeleteEntity(entB));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
/// <summary>
/// Check that we can save & load a file containing multiple orphaned grid entities.
/// </summary>
[Test]
public async Task TestOrphanedGridSerialization()
{
var server = StartServer();
await server.WaitIdleAsync();
var entMan = server.EntMan;
var mapSys = server.System<SharedMapSystem>();
var loader = server.System<MapLoaderSystem>();
var xform = server.System<SharedTransformSystem>();
var mapMan = server.ResolveDependency<IMapManager>();
var tileMan = server.ResolveDependency<ITileDefinitionManager>();
var pathA = new ResPath($"{nameof(TestOrphanedGridSerialization)}_A.yml");
var pathB = new ResPath($"{nameof(TestOrphanedGridSerialization)}_B.yml");
var pathCombined = new ResPath($"{nameof(TestOrphanedGridSerialization)}_C.yml");
tileMan.Register(new TileDef("space"));
var tDef = new TileDef("a");
tileMan.Register(tDef);
// Spawn multiple entities on a map
MapId mapId = default;
Entity<TransformComponent, EntitySaveTestComponent> map = default;
Entity<TransformComponent, EntitySaveTestComponent> gridA = default;
Entity<TransformComponent, EntitySaveTestComponent> gridB = default;
Entity<TransformComponent, EntitySaveTestComponent> child = default;
await server.WaitPost(() =>
{
var mapUid = mapSys.CreateMap(out mapId);
map = Get(mapUid, entMan);
var gridAUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridAUid, Vector2i.Zero, new Tile(tDef.TileId));
gridA = Get(gridAUid, entMan);
xform.SetLocalPosition(gridA.Owner, new(100, 100));
var gridBUid = mapMan.CreateGridEntity(mapId);
mapSys.SetTile(gridBUid, Vector2i.Zero, new Tile(tDef.TileId));
gridB = Get(gridBUid, entMan);
var childUid = entMan.SpawnEntity(null, new EntityCoordinates(gridBUid, 0.5f, 0.5f));
child = Get(childUid, entMan);
map.Comp2.Id = nameof(map);
gridA.Comp2.Id = nameof(gridA);
gridB.Comp2.Id = nameof(gridB);
child.Comp2.Id = nameof(child);
});
await server.WaitRunTicks(5);
// grids are not in null-space
Assert.That(gridA.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(gridB.Comp1!.ParentUid, Is.EqualTo(map.Owner));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
Assert.That(map.Comp1!.ParentUid, Is.EqualTo(EntityUid.Invalid));
// Save the grids without their map
Assert.That(loader.TrySaveGrid(gridA, pathA));
Assert.That(loader.TrySaveGrid(gridB, pathB));
Assert.That(loader.TrySaveGeneric([gridA.Owner, gridB.Owner], pathCombined, out var cat));
Assert.That(cat, Is.EqualTo(FileCategory.Unknown));
// Delete all the entities.
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(4));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing only gridA.
EntityUid newMap = default;
await server.WaitPost(() => newMap = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, pathA, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(1));
gridA = Find(nameof(gridA), entMan);
Assert.That(gridA.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(gridA.Comp1!.ParentUid, Is.EqualTo(newMap));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load in the file containing gridB and its child
await server.WaitPost(() => newMap = mapSys.CreateMap(out mapId));
await server.WaitPost(() => Assert.That(loader.TryLoadGrid(mapId, pathB, out _)));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(2));
gridB = Find(nameof(gridB), entMan);
child = Find(nameof(child), entMan);
Assert.That(gridB.Comp1!.ParentUid, Is.EqualTo(newMap));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
await server.WaitPost(() => mapSys.DeleteMap(mapId));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
// Load the file that contains both of them.
// This uses the generic loader, and should automatically create maps for both grids.
LoadResult? result = null;
var opts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with {LogOrphanedGrids = false}
};
await server.WaitPost(() => Assert.That(loader.TryLoadGeneric(pathCombined, out result, opts)));
Assert.That(result!.Category, Is.EqualTo(FileCategory.Unknown));
Assert.That(result.Grids, Has.Count.EqualTo(2));
Assert.That(result.Maps, Has.Count.EqualTo(2));
Assert.That(entMan.Count<LoadedMapComponent>(), Is.EqualTo(0));
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(3));
gridA = Find(nameof(gridA), entMan);
gridB = Find(nameof(gridB), entMan);
child = Find(nameof(child), entMan);
Assert.That(gridA.Comp1.LocalPosition, Is.Approximately(new Vector2(100, 100)));
Assert.That(gridA.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(gridB.Comp1!.ParentUid, Is.Not.EqualTo(EntityUid.Invalid));
Assert.That(child.Comp1!.ParentUid, Is.EqualTo(gridB.Owner));
await server.WaitPost(() =>
{
foreach (var ent in result.Maps)
{
entMan.DeleteEntity(ent.Owner);
}
});
Assert.That(entMan.Count<EntitySaveTestComponent>(), Is.EqualTo(0));
}
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.EntitySerialization;
[RegisterComponent]
public sealed partial class EntitySaveTestComponent : Component
{
/// <summary>
/// Give each entity a unique id to identify them across map saves & loads.
/// </summary>
[DataField] public string? Id;
[DataField] public EntityUid? Entity;
[DataField, AlwaysPushInheritance] public List<int> List = [];
/// <summary>
/// Find an entity with a <see cref="EntitySaveTestComponent"/> with the matching id.
/// </summary>
public static Entity<TransformComponent, EntitySaveTestComponent> Find(string id, IEntityManager entMan)
{
var ents = entMan.AllEntities<EntitySaveTestComponent>();
var matching = ents.Where(x => x.Comp.Id == id).ToArray();
Assert.That(matching, Has.Length.EqualTo(1));
return (matching[0].Owner, entMan.GetComponent<TransformComponent>(matching[0].Owner), matching[0].Comp);
}
public static Entity<TransformComponent, EntitySaveTestComponent> Get(EntityUid uid, IEntityManager entMan)
{
return new Entity<TransformComponent, EntitySaveTestComponent>(
uid,
entMan.GetComponent<TransformComponent>(uid),
entMan.EnsureComponent<EntitySaveTestComponent>(uid));
}
}
/// <summary>
/// Dummy tile definition for serializing grids.
/// </summary>
public sealed class TileDef(string id) : ITileDefinition
{
public ushort TileId { get; set; }
public string Name => id;
public string ID => id;
public ResPath? Sprite => null;
public Dictionary<Direction, ResPath> EdgeSprites => new();
public int EdgeSpritePriority => 0;
public float Friction => 0;
public byte Variants => 0;
public void AssignTileId(ushort id) => TileId = id;
}

View File

@@ -7,10 +7,12 @@ using Robust.Client.Timing;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.GameObjects
{
@@ -291,15 +293,16 @@ namespace Robust.UnitTesting.Shared.GameObjects
await Task.WhenAll(server.WaitIdleAsync());
var sEntManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSys = sEntManager.System<SharedMapSystem>();
var sContainerSys = sEntManager.System<SharedContainerSystem>();
var sMetadataSys = sEntManager.System<MetaDataSystem>();
var path = new ResPath("container_test.yml");
await server.WaitAssertion(() =>
{
// build the map
sEntManager.System<SharedMapSystem>().CreateMap(out var mapIdOne);
Assert.That(mapManager.IsMapInitialized(mapIdOne), Is.True);
Assert.That(mapSys.IsInitialized(mapIdOne), Is.True);
var containerEnt = sEntManager.SpawnEntity(null, new MapCoordinates(1, 1, mapIdOne));
sMetadataSys.SetEntityName(containerEnt, "ContainerEnt");
@@ -315,8 +318,8 @@ namespace Robust.UnitTesting.Shared.GameObjects
// save the map
var mapLoader = sEntManager.EntitySysManager.GetEntitySystem<MapLoaderSystem>();
mapLoader.SaveMap(mapIdOne, "container_test.yml");
mapManager.DeleteMap(mapIdOne);
Assert.That(mapLoader.TrySaveMap(mapIdOne, path));
mapSys.DeleteMap(mapIdOne);
});
// A few moments later...
@@ -325,11 +328,10 @@ namespace Robust.UnitTesting.Shared.GameObjects
await server.WaitAssertion(() =>
{
var mapLoader = sEntManager.System<MapLoaderSystem>();
sEntManager.System<SharedMapSystem>().CreateMap(out var mapIdTwo);
// load the map
mapLoader.Load(mapIdTwo, "container_test.yml");
Assert.That(mapManager.IsMapInitialized(mapIdTwo), Is.True); // Map Initialize-ness is saved in the map file.
Assert.That(mapLoader.TryLoadMap(path, out var map, out _));
Assert.That(mapSys.IsInitialized(map), Is.True); // Map Initialize-ness is saved in the map file.
});
await server.WaitRunTicks(1);

View File

@@ -40,7 +40,6 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems
var mapManager = sim.Resolve<IMapManager>();
// Adds the map with id 1, and spawns entity 1 as the map entity.
var testMapId = sim.CreateMap().MapId;
var coords = new MapCoordinates(new Vector2(7, 7), testMapId);
// Add grid 1, as the default grid to anchor things to.

View File

@@ -108,8 +108,6 @@ namespace Robust.UnitTesting.Shared.Map
public void GetGridId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));
@@ -139,8 +137,6 @@ namespace Robust.UnitTesting.Shared.Map
public void GetMapId_Map()
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
var mapEnt = entityManager.System<SharedMapSystem>().CreateMap(out var mapId);
var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId));

View File

@@ -1,111 +0,0 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers.Custom;
[TestFixture]
public sealed class TimeOffsetSerializerTest : RobustIntegrationTest
{
[Test]
public async Task SerializationTest()
{
var sim = StartServer();
await sim.WaitIdleAsync();
var serialization = sim.ResolveDependency<ISerializationManager>();
var timing = sim.ResolveDependency<IGameTiming>();
var entMan = sim.ResolveDependency<IEntityManager>();
var ctx = new MapSerializationContext(entMan, timing);
await sim.WaitRunTicks(10);
Assert.That(timing.CurTime.TotalSeconds, Is.GreaterThan(0));
// "pause" a map at this time
var pauseTime = timing.CurTime;
await sim.WaitRunTicks(10);
// Spawn a paused entity
var uid = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var metaSys = entMan.System<MetaDataSystem>();
metaSys.SetEntityPaused(uid, true);
await sim.WaitRunTicks(10);
Assert.That(metaSys.GetPauseTime(uid).TotalSeconds, Is.GreaterThan(0));
var curTime = timing.CurTime;
var dataTime = curTime + TimeSpan.FromSeconds(2);
ctx.PauseTime = curTime - pauseTime;
var entPauseDuration = metaSys.GetPauseTime(uid);
Assert.That(curTime.TotalSeconds, Is.GreaterThan(0));
Assert.That(entPauseDuration.TotalSeconds, Is.GreaterThan(0));
Assert.That(ctx.PauseTime.TotalSeconds, Is.GreaterThan(0));
Assert.That(ctx.PauseTime, Is.Not.EqualTo(curTime));
Assert.That(ctx.PauseTime, Is.Not.EqualTo(entPauseDuration));
Assert.That(entPauseDuration, Is.Not.EqualTo(curTime));
// time gets properly offset when reading a post-init map
ctx.MapInitialized = true;
var node = serialization.WriteValue<TimeSpan, TimeOffsetSerializer>(dataTime, context: ctx);
var value = ((ValueDataNode) node).Value;
var expected = (dataTime - curTime + ctx.PauseTime).TotalSeconds.ToString(CultureInfo.InvariantCulture);
Assert.That(value, Is.EqualTo(expected));
// When writing paused entities, it will instead use the entity's pause time:
ctx.CurrentWritingEntity = uid;
node = serialization.WriteValue<TimeSpan, TimeOffsetSerializer>(dataTime, context: ctx);
value = ((ValueDataNode) node).Value;
expected = (dataTime - curTime + entPauseDuration).TotalSeconds.ToString(CultureInfo.InvariantCulture);
Assert.That(value, Is.EqualTo(expected));
// Uninitialized maps always serialize as zero
ctx.MapInitialized = false;
node = serialization.WriteValue<TimeSpan, TimeOffsetSerializer>(dataTime, context: ctx);
value = ((ValueDataNode) node).Value;
Assert.That(value, Is.EqualTo("0"));
ctx.CurrentWritingEntity = null;
node = serialization.WriteValue<TimeSpan, TimeOffsetSerializer>(dataTime, context: ctx);
value = ((ValueDataNode) node).Value;
Assert.That(value, Is.EqualTo("0"));
}
[Test]
public async Task DeserializationTest()
{
var sim = StartServer();
await sim.WaitIdleAsync();
var serialization = sim.ResolveDependency<ISerializationManager>();
await sim.WaitRunTicks(10);
var timing = sim.ResolveDependency<IGameTiming>();
var entMan = sim.ResolveDependency<IEntityManager>();
var ctx = new MapSerializationContext(entMan, timing);
var curTime = timing.CurTime;
var node = new ValueDataNode("2");
// time gets properly offset when reading a post-init map
ctx.MapInitialized = true;
var time = serialization.Read<TimeSpan, ValueDataNode, TimeOffsetSerializer>(node, ctx);
Assert.That(time, Is.EqualTo(curTime + TimeSpan.FromSeconds(2)));
// pre-init maps read time offsets as 0.
ctx.MapInitialized = false;
time = serialization.Read<TimeSpan, ValueDataNode, TimeOffsetSerializer>(node, ctx);
Assert.That(time, Is.EqualTo(TimeSpan.Zero));
// Same goes for no-context reads
time = serialization.Read<TimeSpan, ValueDataNode, TimeOffsetSerializer>(node);
Assert.That(time, Is.EqualTo(TimeSpan.Zero));
}
}