mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* oops
* fixes serialization il
* copytest
* typo & misc fixes
* 139 moment
* boxing
* mesa dum
* stuff
* goodbye bad friend
* last commit before the big (4) rewrite
* adds datanodes
* kills yamlobjserializer in favor of the new system
* adds more serializers, actually implements them & removes most of the last of the old system
* changed yamlfieldattribute namespace
* adds back iselfserialize
* refactors consts&flags
* renames everything to data(field/definition)
* adds afterserialization
* help
* dataclassgen
* fuggen help me mannen
* Fix most errors on content
* Fix engine errors except map loader
* maploader & misc fix
* misc fixes
* thing
* help
* refactors datanodes
* help me mannen
* Separate ITypeSerializer into reader and writer
* Convert all type serializers
* priority
* adds alot
* il fixes
* adds robustgen
* argh
* adds array & enum serialization
* fixes dataclasses
* adds vec2i / misc fixes
* fixes inheritance
* a very notcursed todo
* fixes some custom dataclasses
* push dis
* Remove data classes
* boutta box
* yes
* Add angle and regex serializer tests
* Make TypeSerializerTest abstract
* sets up ioc etc
* remove pushinheritance
* fixes
* Merge fixes, fix yaml hot reloading
* General fixes2
* Make enum serialization ignore case
* Fix the tag not being copied in data nodes
* Fix not properly serializing flag enums
* Fix component serialization on startup
* Implement ValueDataNode ToString
* Serialization IL fixes, fix return and string equality
* Remove async from prototype manager
* Make serializing unsupported node as enum exception more descriptive
* Fix serv3 tryread casting to serializer instead of reader
* Add constructor for invalid node type exception
* Temporary fix for SERV3: Turn populate delegate into regular code
* Fix not copying the data of non primitive types
* Fix not using the data definition found in copying
* Make ISerializationHooks require explicit implementations
* Add test for serialization inheritance
* Improve IsOverridenIn method
* Fix error message when a data definition is null
* Add method to cast a read value in Serv3Manager
* Rename IServ3Manager to ISerializationManager
* Rename usages of serv3manager, add generic copy method
* Fix IL copy method lookup
* Rename old usages of serv3manager
* Add ITypeCopier
* resistance is futile
* we will conquer this codebase
* Add copy method to all serializers
* Make primitive mismatch error message more descriptive
* bing bong im going to freacking heck
* oopsie moment
* hello are you interested in my wares
* does generic serializers under new architecture
* Convert every non generic serializer to the new format, general fixes
* Update usgaes of generic serializers, cleanup
* does some pushinheritance logic
* finishes pushinheritance FRAMEWORK
* shed
* Add box2, color and component registry serializer tests
* Create more deserialized types and store prototypes with their deserialized results
* Fixes and serializer updates
* Add serialization manager extensions
* adds pushinheritance
* Update all prototypes to have a parent and have consistent id/parent properties
* Fix grammar component serialization
* Add generic serializer tests
* thonk
* Add array serializer test
* Replace logger warning calls with exceptions
* fixes
* Move redundant methods to serialization manager extensions, cleanup
* Add array serialization
* fixes context
* more fixes
* argh
* inheritance
* this should do it
* fixes
* adds copiers & fixes some stuff
* copiers use context v1
* finishing copy context
* more context fixes
* Test fixes
* funky maps
* Fix server user interface component serialization
* Fix value tuple serialization
* Add copying for value types and arrays. Fix copy internal for primitives, enums and strings
* fixes
* fixes more stuff
* yes
* Make abstract/interface skips debugs instead of warnings
* Fix typo
* Make some dictionaries readonly
* Add checks for the serialization manager initializing and already being initialized
* Add base type required and usage for MeansDataDefinition and ImplicitDataDefinitionForInheritorsAttribute
* copy by ref
* Fix exception wording
* Update data field required summary with the new forbidden docs
* Use extension in map loader
* wanna erp
* Change serializing to not use il temporarily
* Make writing work with nullable types
* pushing
* check
* cuddling slaps HARD
* Add serialization priority test
* important fix
* a serialization thing
* serializer moment
* Add validation for some type serializers
* adds context
* moar context
* fixes
* Do the thing for appearance
* yoo lmao
* push haha pp
* Temporarily make copy delegate regular c# code
* Create deserialized component registry to handle not inheriting conflicting references
* YAML LINTER BABY
* ayes
* Fix sprite component norot not being default true like in latest master
* Remove redundant todos
* Add summary doc to every ISerializationManager method
* icon fixes
* Add skip hook argument to readers and copiers
* Merge fixes
* Fix ordering of arguments in read and copy reflection call
* Fix user interface components deserialization
* pew pew
* i am going to HECK
* Add MustUseReturnValue to copy-over methods
* Make serialization log calls use the same sawmill
* gamin
* Fix doc errors in ISerializationManager.cs
* goodbye brave soldier
* fixes
* WIP merge fixes and entity serialization
* aaaaaaaaaaaaaaa
* aaaaaaaaaaaaaaa
* adds inheritancebehaviour
* test/datafield fixes
* forgot that one
* adds more verbose validation
* This fixes the YAML hot reloading
* Replace yield break with Enumerable.Empty
* adds copiers
* aaaaaaaaaaaaa
* array fix
priority fix
misc fixes
* fix(?)
* fix.
* funny map serialization (wip)
* funny map serialization (wip)
* Add TODO
* adds proper info the validation
* Make yaml linter 5 times faster (~80% less execution time)
* Improves the error message for missing fields in the linter
* Include component name in unknown component type error node
* adds alwaysrelevant usa
* fixes mapsaving
* moved surpressor to analyzers proj
* warning cleanup & moves surpressor
* removes old msbuild targets
* Revert "Make yaml linter 5 times faster (~80% less execution time)"
This reverts commit 2ee4cc2c26.
* Add serialization to RobustServerSimulation and mock reflection methods
Fixes container tests
* Fix nullability warnings
* Improve yaml linter message feedback
* oops moment
* Add IEquatable, IComparable, ToString and operators to DataPosition
Rename it to NodeMark
Make it a readonly struct
* Remove try catch from enum parsing
* Make dependency management in serialization less bad
* Make dependencies an argument instead of a property on the serialization manager
* Clean up type serializers
* Improve validation messages and resourc epath checking
* Fix sprite error message
* reached perfection
Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Vera Aguilera Puerto <zddm@outlook.es>
1047 lines
40 KiB
C#
1047 lines
40 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using JetBrains.Annotations;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.ContentPack;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Serialization.Manager;
|
|
using Robust.Shared.Serialization.Manager.Result;
|
|
using Robust.Shared.Serialization.Markdown;
|
|
using Robust.Shared.Serialization.Markdown.Validation;
|
|
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using YamlDotNet.Core;
|
|
using YamlDotNet.RepresentationModel;
|
|
|
|
namespace Robust.Server.Maps
|
|
{
|
|
/// <summary>
|
|
/// Saves and loads maps to the disk.
|
|
/// </summary>
|
|
public class MapLoader : IMapLoader
|
|
{
|
|
private static readonly MapLoadOptions DefaultLoadOptions = new();
|
|
|
|
private const int MapFormatVersion = 2;
|
|
|
|
[Dependency] private readonly IResourceManager _resMan = default!;
|
|
[Dependency] private readonly IMapManagerInternal _mapManager = default!;
|
|
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
|
|
[Dependency] private readonly IServerEntityManagerInternal _serverEntityManager = default!;
|
|
[Dependency] private readonly IPauseManager _pauseManager = default!;
|
|
[Dependency] private readonly IComponentManager _componentManager = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
|
|
public event Action<YamlStream, string>? LoadedMapData;
|
|
|
|
/// <inheritdoc />
|
|
public void SaveBlueprint(GridId gridId, string yamlPath)
|
|
{
|
|
var grid = _mapManager.GetGrid(gridId);
|
|
|
|
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
|
_componentManager, _prototypeManager);
|
|
context.RegisterGrid(grid);
|
|
var root = context.Serialize();
|
|
var document = new YamlDocument(root);
|
|
|
|
var resPath = new ResourcePath(yamlPath).ToRootedPath();
|
|
_resMan.UserData.CreateDir(resPath.Directory);
|
|
|
|
using (var file = _resMan.UserData.Create(resPath))
|
|
{
|
|
using (var writer = new StreamWriter(file))
|
|
{
|
|
var stream = new YamlStream();
|
|
|
|
stream.Add(document);
|
|
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IMapGrid? LoadBlueprint(MapId mapId, string path)
|
|
{
|
|
return LoadBlueprint(mapId, path, DefaultLoadOptions);
|
|
}
|
|
|
|
public IMapGrid? LoadBlueprint(MapId mapId, string path, MapLoadOptions options)
|
|
{
|
|
TextReader reader;
|
|
var resPath = new ResourcePath(path).ToRootedPath();
|
|
|
|
// try user
|
|
if (!_resMan.UserData.Exists(resPath))
|
|
{
|
|
Logger.InfoS("map", $"No user blueprint path: {resPath}");
|
|
|
|
// fallback to content
|
|
if (_resMan.TryContentFileRead(resPath, out var contentReader))
|
|
{
|
|
reader = new StreamReader(contentReader);
|
|
}
|
|
else
|
|
{
|
|
Logger.ErrorS("map", $"No blueprint found: {resPath}");
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var file = _resMan.UserData.OpenRead(resPath);
|
|
reader = new StreamReader(file);
|
|
}
|
|
|
|
IMapGrid grid;
|
|
using (reader)
|
|
{
|
|
Logger.InfoS("map", $"Loading Grid: {resPath}");
|
|
|
|
var data = new MapData(reader);
|
|
|
|
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
|
|
|
|
if (data.GridCount != 1)
|
|
{
|
|
throw new InvalidDataException("Cannot instance map with multiple grids as blueprint.");
|
|
}
|
|
|
|
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
|
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
|
|
context.Deserialize();
|
|
grid = context.Grids[0];
|
|
|
|
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(mapId))
|
|
{
|
|
foreach (var entity in context.Entities)
|
|
{
|
|
entity.RunMapInit();
|
|
}
|
|
}
|
|
|
|
if (_pauseManager.IsMapPaused(mapId))
|
|
{
|
|
foreach (var entity in context.Entities)
|
|
{
|
|
entity.Paused = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return grid;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SaveMap(MapId mapId, string yamlPath)
|
|
{
|
|
Logger.InfoS("map", $"Saving map {mapId} to {yamlPath}");
|
|
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
|
_componentManager, _prototypeManager);
|
|
foreach (var grid in _mapManager.GetAllMapGrids(mapId))
|
|
{
|
|
context.RegisterGrid(grid);
|
|
}
|
|
|
|
var document = new YamlDocument(context.Serialize());
|
|
|
|
var resPath = new ResourcePath(yamlPath).ToRootedPath();
|
|
_resMan.UserData.CreateDir(resPath.Directory);
|
|
|
|
using (var file = _resMan.UserData.Create(resPath))
|
|
{
|
|
using (var writer = new StreamWriter(file))
|
|
{
|
|
var stream = new YamlStream();
|
|
|
|
stream.Add(document);
|
|
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
|
}
|
|
}
|
|
|
|
Logger.InfoS("map", "Save completed!");
|
|
}
|
|
|
|
public void LoadMap(MapId mapId, string path)
|
|
{
|
|
LoadMap(mapId, path, DefaultLoadOptions);
|
|
}
|
|
|
|
public void LoadMap(MapId mapId, string path, MapLoadOptions options)
|
|
{
|
|
TextReader reader;
|
|
var resPath = new ResourcePath(path).ToRootedPath();
|
|
|
|
// try user
|
|
if (!_resMan.UserData.Exists(resPath))
|
|
{
|
|
Logger.InfoS("map", $"No user map found: {resPath}");
|
|
|
|
// fallback to content
|
|
if (_resMan.TryContentFileRead(resPath, out var contentReader))
|
|
{
|
|
reader = new StreamReader(contentReader);
|
|
}
|
|
else
|
|
{
|
|
Logger.ErrorS("map", $"No map found: {resPath}");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var file = _resMan.UserData.OpenRead(resPath);
|
|
reader = new StreamReader(file);
|
|
}
|
|
|
|
using (reader)
|
|
{
|
|
Logger.InfoS("map", $"Loading Map: {resPath}");
|
|
|
|
var data = new MapData(reader);
|
|
|
|
LoadedMapData?.Invoke(data.Stream, resPath.ToString());
|
|
|
|
var context = new MapContext(_mapManager, _tileDefinitionManager, _serverEntityManager, _pauseManager,
|
|
_componentManager, _prototypeManager, (YamlMappingNode) data.RootNode, mapId, options);
|
|
context.Deserialize();
|
|
|
|
if (!context.MapIsPostInit && _pauseManager.IsMapInitialized(mapId))
|
|
{
|
|
foreach (var entity in context.Entities)
|
|
{
|
|
entity.RunMapInit();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the primary bulk of state during the map serialization process.
|
|
/// </summary>
|
|
private class MapContext : ISerializationContext, IEntityLoadContext,
|
|
ITypeSerializer<GridId, ValueDataNode>,
|
|
ITypeSerializer<EntityUid, ValueDataNode>,
|
|
ITypeReaderWriter<IEntity, ValueDataNode>
|
|
{
|
|
private readonly IMapManagerInternal _mapManager;
|
|
private readonly ITileDefinitionManager _tileDefinitionManager;
|
|
private readonly IServerEntityManagerInternal _serverEntityManager;
|
|
private readonly IPauseManager _pauseManager;
|
|
private readonly IComponentManager _componentManager;
|
|
private readonly IPrototypeManager _prototypeManager;
|
|
|
|
private readonly MapLoadOptions? _loadOptions;
|
|
private readonly Dictionary<GridId, int> GridIDMap = new();
|
|
public readonly List<IMapGrid> Grids = new();
|
|
|
|
private readonly Dictionary<EntityUid, int> EntityUidMap = new();
|
|
private readonly Dictionary<int, EntityUid> UidEntityMap = new();
|
|
public readonly List<IEntity> Entities = new();
|
|
|
|
private readonly List<(IEntity, YamlMappingNode)> _entitiesToDeserialize
|
|
= new();
|
|
|
|
private bool IsBlueprintMode => GridIDMap.Count == 1;
|
|
|
|
private readonly YamlMappingNode RootNode;
|
|
private readonly MapId TargetMap;
|
|
|
|
private Dictionary<string, YamlMappingNode>? CurrentReadingEntityComponents;
|
|
|
|
private string? CurrentWritingComponent;
|
|
private IEntity? CurrentWritingEntity;
|
|
|
|
private Dictionary<ushort, string>? _tileMap;
|
|
|
|
public Dictionary<(Type, Type), object> TypeReaders { get; }
|
|
public Dictionary<Type, object> TypeWriters { get; }
|
|
public Dictionary<Type, object> TypeCopiers => TypeWriters;
|
|
|
|
public bool MapIsPostInit { get; private set; }
|
|
|
|
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
|
|
IServerEntityManagerInternal entities, IPauseManager pauseManager, IComponentManager componentManager,
|
|
IPrototypeManager prototypeManager)
|
|
{
|
|
_mapManager = maps;
|
|
_tileDefinitionManager = tileDefs;
|
|
_serverEntityManager = entities;
|
|
_pauseManager = pauseManager;
|
|
_componentManager = componentManager;
|
|
_prototypeManager = prototypeManager;
|
|
|
|
RootNode = new YamlMappingNode();
|
|
TypeWriters = new Dictionary<Type, object>()
|
|
{
|
|
{typeof(IEntity), this},
|
|
{typeof(GridId), this},
|
|
{typeof(EntityUid), this}
|
|
};
|
|
TypeReaders = new Dictionary<(Type, Type), object>()
|
|
{
|
|
{(typeof(IEntity), typeof(ValueDataNode)), this},
|
|
{(typeof(GridId), typeof(ValueDataNode)), this},
|
|
{(typeof(EntityUid), typeof(ValueDataNode)), this}
|
|
};
|
|
}
|
|
|
|
public MapContext(IMapManagerInternal maps, ITileDefinitionManager tileDefs,
|
|
IServerEntityManagerInternal entities,
|
|
IPauseManager pauseManager, IComponentManager componentManager, IPrototypeManager prototypeManager,
|
|
YamlMappingNode node, MapId targetMapId, MapLoadOptions options)
|
|
{
|
|
_mapManager = maps;
|
|
_tileDefinitionManager = tileDefs;
|
|
_serverEntityManager = entities;
|
|
_pauseManager = pauseManager;
|
|
_componentManager = componentManager;
|
|
_loadOptions = options;
|
|
|
|
RootNode = node;
|
|
TargetMap = targetMapId;
|
|
_prototypeManager = prototypeManager;
|
|
TypeWriters = new Dictionary<Type, object>()
|
|
{
|
|
{typeof(IEntity), this},
|
|
{typeof(GridId), this},
|
|
{typeof(EntityUid), this}
|
|
};
|
|
TypeReaders = new Dictionary<(Type, Type), object>()
|
|
{
|
|
{(typeof(IEntity), typeof(ValueDataNode)), this},
|
|
{(typeof(GridId), typeof(ValueDataNode)), this},
|
|
{(typeof(EntityUid), typeof(ValueDataNode)), this}
|
|
};
|
|
}
|
|
|
|
// Deserialization
|
|
public void Deserialize()
|
|
{
|
|
// Verify that prototypes for all the entities exist and throw if they don't.
|
|
VerifyEntitiesExist();
|
|
|
|
// First we load map meta data like version.
|
|
ReadMetaSection();
|
|
|
|
// Create the new map.
|
|
AllocMap();
|
|
|
|
// Load grids.
|
|
ReadTileMapSection();
|
|
ReadGridSection();
|
|
|
|
// Entities are first allocated. This allows us to know the future UID of all entities on the map before
|
|
// even ExposeData is loaded. This allows us to resolve serialized EntityUid instances correctly.
|
|
AllocEntities();
|
|
|
|
// Actually instance components and run ExposeData on them.
|
|
FinishEntitiesLoad();
|
|
|
|
// Clear the net tick numbers so that components from prototypes (not modified by map)
|
|
// aren't sent over the wire initially.
|
|
ResetNetTicks();
|
|
|
|
// Grid entities were NOT created inside ReadGridSection().
|
|
// We have to fix the created grids up with the grid entities deserialized from the map.
|
|
FixMapEntities();
|
|
|
|
// We have to attach grids to the target map here.
|
|
// If we don't, initialization & startup can fail for some entities.
|
|
AttachMapEntities();
|
|
|
|
// Run Initialize on all components.
|
|
FinishEntitiesInitialization();
|
|
|
|
// Run Startup on all components.
|
|
FinishEntitiesStartup();
|
|
}
|
|
|
|
private void VerifyEntitiesExist()
|
|
{
|
|
var fail = false;
|
|
var entities = RootNode.GetNode<YamlSequenceNode>("entities");
|
|
var reportedError = new HashSet<string>();
|
|
foreach (var entityDef in entities.Cast<YamlMappingNode>())
|
|
{
|
|
if (entityDef.TryGetNode("type", out var typeNode))
|
|
{
|
|
var type = typeNode.AsString();
|
|
if (!_prototypeManager.HasIndex<EntityPrototype>(type) && !reportedError.Contains(type))
|
|
{
|
|
Logger.Error("Missing prototype for map: {0}", type);
|
|
fail = true;
|
|
reportedError.Add(type);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fail)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Found missing prototypes in map file. Missing prototypes have been dumped to logs.");
|
|
}
|
|
}
|
|
|
|
private void ResetNetTicks()
|
|
{
|
|
foreach (var (entity, data) in _entitiesToDeserialize)
|
|
{
|
|
if (!data.TryGetNode("components", out YamlSequenceNode? componentList))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entity.Prototype == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var component in _componentManager.GetNetComponents(entity.Uid))
|
|
{
|
|
var castComp = (Component) component;
|
|
|
|
if (componentList.Any(p => p["type"].AsString() == component.Name))
|
|
{
|
|
if (entity.Prototype.Components.ContainsKey(component.Name))
|
|
{
|
|
// This component is modified by the map so we have to send state.
|
|
// Though it's still in the prototype itself so creation doesn't need to be sent.
|
|
castComp.ClearCreationTick();
|
|
}
|
|
else
|
|
{
|
|
// New component that the prototype normally does not have, need to sync full data.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// This component is not modified by the map file,
|
|
// so the client will have the same data after instantiating it from prototype ID.
|
|
castComp.ClearTicks();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AttachMapEntities()
|
|
{
|
|
var mapEntity = _mapManager.GetMapEntity(TargetMap);
|
|
|
|
foreach (var grid in Grids)
|
|
{
|
|
var entity = _serverEntityManager.GetEntity(grid.GridEntityId);
|
|
entity.Transform.AttachParent(mapEntity);
|
|
}
|
|
}
|
|
|
|
private void FixMapEntities()
|
|
{
|
|
foreach (var entity in Entities)
|
|
{
|
|
if (entity.TryGetComponent(out IMapGridComponent? grid))
|
|
{
|
|
var castGrid = (MapGrid) grid.Grid;
|
|
castGrid.GridEntityId = entity.Uid;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadMetaSection()
|
|
{
|
|
var meta = RootNode.GetNode<YamlMappingNode>("meta");
|
|
var ver = meta.GetNode("format").AsInt();
|
|
if (ver != MapFormatVersion)
|
|
{
|
|
throw new InvalidDataException("Cannot handle this map file version.");
|
|
}
|
|
|
|
if (meta.TryGetNode("postmapinit", out var mapInitNode))
|
|
{
|
|
MapIsPostInit = mapInitNode.AsBool();
|
|
}
|
|
else
|
|
{
|
|
MapIsPostInit = true;
|
|
}
|
|
}
|
|
|
|
private void ReadTileMapSection()
|
|
{
|
|
// Load tile mapping so that we can map the stored tile IDs into the ones actually used at runtime.
|
|
_tileMap = new Dictionary<ushort, string>();
|
|
|
|
var tileMap = RootNode.GetNode<YamlMappingNode>("tilemap");
|
|
foreach (var (key, value) in tileMap)
|
|
{
|
|
var tileId = (ushort) key.AsInt();
|
|
var tileDefName = value.AsString();
|
|
_tileMap.Add(tileId, tileDefName);
|
|
}
|
|
}
|
|
|
|
private void ReadGridSection()
|
|
{
|
|
var grids = RootNode.GetNode<YamlSequenceNode>("grids");
|
|
|
|
foreach (var grid in grids)
|
|
{
|
|
var newId = new GridId?();
|
|
YamlGridSerializer.DeserializeGrid(
|
|
_mapManager, TargetMap, ref newId,
|
|
(YamlMappingNode) grid["settings"],
|
|
(YamlSequenceNode) grid["chunks"],
|
|
_tileMap!,
|
|
_tileDefinitionManager
|
|
);
|
|
|
|
if (newId != null)
|
|
{
|
|
Grids.Add(_mapManager.GetGrid(newId.Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AllocMap()
|
|
{
|
|
// Both blueprint and map deserialization use this,
|
|
// so we need to ensure the map exists (and the map entity)
|
|
// before allocating entities.
|
|
|
|
if (!_mapManager.MapExists(TargetMap))
|
|
{
|
|
_mapManager.CreateMap(TargetMap);
|
|
|
|
if (!MapIsPostInit)
|
|
{
|
|
_pauseManager.AddUninitializedMap(TargetMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AllocEntities()
|
|
{
|
|
var entities = RootNode.GetNode<YamlSequenceNode>("entities");
|
|
foreach (var entityDef in entities.Cast<YamlMappingNode>())
|
|
{
|
|
string? type = null;
|
|
if (entityDef.TryGetNode("type", out var typeNode))
|
|
{
|
|
type = typeNode.AsString();
|
|
}
|
|
|
|
var uid = Entities.Count;
|
|
if (entityDef.TryGetNode("uid", out var uidNode))
|
|
{
|
|
uid = uidNode.AsInt();
|
|
}
|
|
|
|
var entity = _serverEntityManager.AllocEntity(type);
|
|
Entities.Add(entity);
|
|
UidEntityMap.Add(uid, entity.Uid);
|
|
_entitiesToDeserialize.Add((entity, entityDef));
|
|
|
|
if (_loadOptions!.StoreMapUids)
|
|
{
|
|
var comp = entity.AddComponent<MapSaveIdComponent>();
|
|
comp.Uid = uid;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void FinishEntitiesLoad()
|
|
{
|
|
foreach (var (entity, data) in _entitiesToDeserialize)
|
|
{
|
|
CurrentReadingEntityComponents = new Dictionary<string, YamlMappingNode>();
|
|
if (data.TryGetNode("components", out YamlSequenceNode? componentList))
|
|
{
|
|
foreach (var compData in componentList)
|
|
{
|
|
var copy = new YamlMappingNode(((YamlMappingNode)compData).AsEnumerable());
|
|
copy.Children.Remove(new YamlScalarNode("type"));
|
|
//TODO Paul: maybe replace mapping with datanode
|
|
CurrentReadingEntityComponents[compData["type"].AsString()] = copy;
|
|
}
|
|
}
|
|
|
|
_serverEntityManager.FinishEntityLoad(entity, this);
|
|
}
|
|
}
|
|
|
|
private void FinishEntitiesInitialization()
|
|
{
|
|
foreach (var entity in Entities)
|
|
{
|
|
_serverEntityManager.FinishEntityInitialization(entity);
|
|
}
|
|
}
|
|
|
|
private void FinishEntitiesStartup()
|
|
{
|
|
foreach (var entity in Entities)
|
|
{
|
|
_serverEntityManager.UpdateEntityTree(entity);
|
|
}
|
|
|
|
foreach (var entity in Entities)
|
|
{
|
|
_serverEntityManager.FinishEntityStartup(entity);
|
|
}
|
|
|
|
foreach (var entity in Entities)
|
|
{
|
|
_serverEntityManager.UpdateEntityTree(entity);
|
|
}
|
|
}
|
|
|
|
// Serialization
|
|
public void RegisterGrid(IMapGrid grid)
|
|
{
|
|
if (GridIDMap.ContainsKey(grid.Index))
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
Grids.Add(grid);
|
|
GridIDMap.Add(grid.Index, GridIDMap.Count);
|
|
}
|
|
|
|
public YamlNode Serialize()
|
|
{
|
|
WriteMetaSection();
|
|
WriteTileMapSection();
|
|
WriteGridSection();
|
|
|
|
PopulateEntityList();
|
|
WriteEntitySection();
|
|
|
|
return RootNode;
|
|
}
|
|
|
|
private void WriteMetaSection()
|
|
{
|
|
var meta = new YamlMappingNode();
|
|
RootNode.Add("meta", meta);
|
|
meta.Add("format", MapFormatVersion.ToString(CultureInfo.InvariantCulture));
|
|
// TODO: Make these values configurable.
|
|
meta.Add("name", "DemoStation");
|
|
meta.Add("author", "Space-Wizards");
|
|
|
|
var isPostInit = false;
|
|
foreach (var grid in Grids)
|
|
{
|
|
if (_pauseManager.IsMapInitialized(grid.ParentMapId))
|
|
{
|
|
isPostInit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
meta.Add("postmapinit", isPostInit ? "true" : "false");
|
|
}
|
|
|
|
private void WriteTileMapSection()
|
|
{
|
|
var tileMap = new YamlMappingNode();
|
|
RootNode.Add("tilemap", tileMap);
|
|
foreach (var tileDefinition in _tileDefinitionManager)
|
|
{
|
|
tileMap.Add(tileDefinition.TileId.ToString(CultureInfo.InvariantCulture), tileDefinition.Name);
|
|
}
|
|
}
|
|
|
|
private void WriteGridSection()
|
|
{
|
|
var grids = new YamlSequenceNode();
|
|
RootNode.Add("grids", grids);
|
|
|
|
foreach (var grid in Grids)
|
|
{
|
|
var entry = YamlGridSerializer.SerializeGrid(grid);
|
|
grids.Add(entry);
|
|
}
|
|
}
|
|
|
|
private void PopulateEntityList()
|
|
{
|
|
var withUid = new List<MapSaveIdComponent>();
|
|
var withoutUid = new List<IEntity>();
|
|
var takenIds = new HashSet<int>();
|
|
|
|
foreach (var entity in _serverEntityManager.GetEntities())
|
|
{
|
|
if (IsMapSavable(entity))
|
|
{
|
|
Entities.Add(entity);
|
|
if (entity.TryGetComponent(out MapSaveIdComponent? mapSaveId))
|
|
{
|
|
withUid.Add(mapSaveId);
|
|
}
|
|
else
|
|
{
|
|
withoutUid.Add(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go over entities with a MapSaveIdComponent and assign those.
|
|
|
|
foreach (var mapIdComp in withUid)
|
|
{
|
|
var uid = mapIdComp.Uid;
|
|
if (takenIds.Contains(uid))
|
|
{
|
|
// Duplicate ID. Just pretend it doesn't have an ID and use the without path.
|
|
withoutUid.Add(mapIdComp.Owner);
|
|
}
|
|
else
|
|
{
|
|
EntityUidMap.Add(mapIdComp.Owner.Uid, uid);
|
|
takenIds.Add(uid);
|
|
}
|
|
}
|
|
|
|
var uidCounter = 0;
|
|
foreach (var entity in withoutUid)
|
|
{
|
|
while (takenIds.Contains(uidCounter))
|
|
{
|
|
// Find next available UID.
|
|
uidCounter += 1;
|
|
}
|
|
|
|
EntityUidMap.Add(entity.Uid, uidCounter);
|
|
takenIds.Add(uidCounter);
|
|
}
|
|
}
|
|
|
|
private void WriteEntitySection()
|
|
{
|
|
var serializationManager = IoCManager.Resolve<ISerializationManager>();
|
|
var entities = new YamlSequenceNode();
|
|
RootNode.Add("entities", entities);
|
|
|
|
var prototypeCompCache = new Dictionary<string, Dictionary<string, MappingDataNode>>();
|
|
foreach (var entity in Entities.OrderBy(e => EntityUidMap[e.Uid]))
|
|
{
|
|
CurrentWritingEntity = entity;
|
|
var mapping = new YamlMappingNode
|
|
{
|
|
{"uid", EntityUidMap[entity.Uid].ToString(CultureInfo.InvariantCulture)}
|
|
};
|
|
|
|
if (entity.Prototype != null)
|
|
{
|
|
mapping.Add("type", entity.Prototype.ID);
|
|
if (!prototypeCompCache.ContainsKey(entity.Prototype.ID))
|
|
{
|
|
prototypeCompCache[entity.Prototype.ID] = new Dictionary<string, MappingDataNode>();
|
|
foreach (var (compType, comp) in entity.Prototype.Components)
|
|
{
|
|
prototypeCompCache[entity.Prototype.ID].Add(compType, serializationManager.WriteValueAs<MappingDataNode>(comp.GetType(), comp));
|
|
}
|
|
}
|
|
}
|
|
|
|
var components = new YamlSequenceNode();
|
|
// See engine#636 for why the Distinct() call.
|
|
foreach (var component in entity.GetAllComponents())
|
|
{
|
|
if (component is MapSaveIdComponent)
|
|
continue;
|
|
|
|
CurrentWritingComponent = component.Name;
|
|
var compMapping = serializationManager.WriteValueAs<MappingDataNode>(component.GetType(), component, context: this);
|
|
|
|
if (entity.Prototype != null && prototypeCompCache[entity.Prototype.ID].TryGetValue(component.Name, out var protMapping))
|
|
{
|
|
compMapping = compMapping.Except(protMapping);
|
|
if(compMapping == null) continue;
|
|
}
|
|
|
|
// Don't need to write it if nothing was written!
|
|
if (compMapping.Children.Count != 0)
|
|
{
|
|
compMapping.AddNode("type", new ValueDataNode(component.Name));
|
|
// Something actually got written!
|
|
components.Add(compMapping.ToYamlNode());
|
|
}
|
|
}
|
|
|
|
if (components.Children.Count != 0)
|
|
{
|
|
mapping.Add("components", components);
|
|
}
|
|
|
|
entities.Add(mapping);
|
|
}
|
|
}
|
|
|
|
// Create custom object serializers that will correctly allow data to be overriden by the map file.
|
|
IComponent IEntityLoadContext.GetComponentData(string componentName,
|
|
IComponent? protoData)
|
|
{
|
|
if (CurrentReadingEntityComponents == null)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
var serializationManager = IoCManager.Resolve<ISerializationManager>();
|
|
var factory = IoCManager.Resolve<IComponentFactory>();
|
|
|
|
IComponent data = protoData != null
|
|
? serializationManager.CreateCopy(protoData, this)!
|
|
: (IComponent) Activator.CreateInstance(factory.GetRegistration(componentName).Type)!;
|
|
|
|
if (CurrentReadingEntityComponents.TryGetValue(componentName, out var mapping))
|
|
{
|
|
var mapData = (IDeserializedDefinition) serializationManager.Read(
|
|
factory.GetRegistration(componentName).Type,
|
|
mapping.ToDataNode(), this);
|
|
var newData = serializationManager.PopulateDataDefinition(data, mapData);
|
|
data = (IComponent) newData.RawValue!;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
public IEnumerable<string> GetExtraComponentTypes()
|
|
{
|
|
return CurrentReadingEntityComponents!.Keys;
|
|
}
|
|
|
|
private bool IsMapSavable(IEntity entity)
|
|
{
|
|
if (entity.Prototype?.MapSavable == false || !GridIDMap.ContainsKey(entity.Transform.GridID))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Don't serialize things parented to un savable things.
|
|
// For example clothes inside a person.
|
|
var current = entity.Transform;
|
|
while (current.Parent != null)
|
|
{
|
|
if (current.Parent.Owner.Prototype?.MapSavable == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
current = current.Parent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public DeserializationResult Read(ISerializationManager serializationManager, ValueDataNode node,
|
|
IDependencyCollection dependencies,
|
|
bool skipHook,
|
|
ISerializationContext? context = null)
|
|
{
|
|
if (node.Value == "null") return new DeserializedValue<GridId>(GridId.Invalid);
|
|
|
|
var val = int.Parse(node.Value);
|
|
if (val >= Grids.Count)
|
|
{
|
|
Logger.ErrorS("map", "Error in map file: found local grid ID '{0}' which does not exist.", val);
|
|
}
|
|
else
|
|
{
|
|
return new DeserializedValue<GridId>(Grids[val].Index);
|
|
}
|
|
|
|
return new DeserializedValue<GridId>(GridId.Invalid);
|
|
}
|
|
|
|
ValidationNode ITypeReader<IEntity, ValueDataNode>.Validate(ISerializationManager serializationManager,
|
|
ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context)
|
|
{
|
|
if (!int.TryParse(node.Value, out var val) || val >= Entities.Count)
|
|
{
|
|
return new ErrorNode(node, "Invalid EntityUid", true);
|
|
}
|
|
|
|
return new ValidatedValueNode(node);
|
|
}
|
|
|
|
ValidationNode ITypeReader<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) || val >= Entities.Count)
|
|
{
|
|
return new ErrorNode(node, "Invalid EntityUid", true);
|
|
}
|
|
|
|
return new ValidatedValueNode(node);
|
|
}
|
|
|
|
ValidationNode ITypeReader<GridId, 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) || val >= Grids.Count)
|
|
{
|
|
return new ErrorNode(node, "Invalid GridId", true);
|
|
}
|
|
|
|
return new ValidatedValueNode(node);
|
|
}
|
|
|
|
public DataNode Write(ISerializationManager serializationManager, IEntity value, bool alwaysWrite = false,
|
|
ISerializationContext? context = null)
|
|
{
|
|
if (!EntityUidMap.TryGetValue(value.Uid, out var entityMapped))
|
|
{
|
|
Logger.WarningS("map", "Cannot write entity UID '{0}'.", value.Uid);
|
|
return new ValueDataNode("");
|
|
}
|
|
else
|
|
{
|
|
return new ValueDataNode(entityMapped.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
public DataNode Write(ISerializationManager serializationManager, EntityUid value, bool alwaysWrite = false,
|
|
ISerializationContext? context = null)
|
|
{
|
|
if (!EntityUidMap.TryGetValue(value, out var entityUidMapped))
|
|
{
|
|
// Terrible hack to mute this warning on the grids themselves when serializing blueprints.
|
|
if (!IsBlueprintMode || !CurrentWritingEntity!.HasComponent<MapGridComponent>() ||
|
|
CurrentWritingComponent != "Transform")
|
|
{
|
|
Logger.WarningS("map", "Cannot write entity UID '{0}'.", value);
|
|
}
|
|
|
|
return new ValueDataNode("null");
|
|
}
|
|
else
|
|
{
|
|
return new ValueDataNode(entityUidMapped.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
public DataNode Write(ISerializationManager serializationManager, GridId value, bool alwaysWrite = false,
|
|
ISerializationContext? context = null)
|
|
{
|
|
if (!GridIDMap.TryGetValue(value, out var gridMapped))
|
|
{
|
|
Logger.WarningS("map", "Cannot write grid ID '{0}', falling back to nullspace.", gridMapped);
|
|
return new ValueDataNode("");
|
|
}
|
|
else
|
|
{
|
|
return new ValueDataNode(gridMapped.ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
|
|
DeserializationResult ITypeReader<EntityUid, ValueDataNode>.Read(ISerializationManager serializationManager,
|
|
ValueDataNode node,
|
|
IDependencyCollection dependencies,
|
|
bool skipHook,
|
|
ISerializationContext? context)
|
|
{
|
|
if (node.Value == "null")
|
|
{
|
|
return new DeserializedValue<EntityUid>(EntityUid.Invalid);
|
|
}
|
|
|
|
var val = int.Parse(node.Value);
|
|
if (val >= Entities.Count)
|
|
{
|
|
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
|
|
}
|
|
else
|
|
{
|
|
return new DeserializedValue<EntityUid>(UidEntityMap[val]);
|
|
}
|
|
|
|
return new DeserializedValue<EntityUid>(EntityUid.Invalid);
|
|
}
|
|
|
|
DeserializationResult ITypeReader<IEntity, ValueDataNode>.Read(ISerializationManager serializationManager,
|
|
ValueDataNode node,
|
|
IDependencyCollection dependencies,
|
|
bool skipHook,
|
|
ISerializationContext? context)
|
|
{
|
|
var val = int.Parse(node.Value);
|
|
|
|
if (val >= Entities.Count)
|
|
{
|
|
Logger.ErrorS("map", "Error in map file: found local entity UID '{0}' which does not exist.", val);
|
|
return null!;
|
|
}
|
|
else
|
|
{
|
|
return new DeserializedValue<IEntity>(Entities[val]);
|
|
}
|
|
}
|
|
|
|
[MustUseReturnValue]
|
|
public GridId Copy(ISerializationManager serializationManager, GridId source, GridId target,
|
|
bool skipHook,
|
|
ISerializationContext? context = null)
|
|
{
|
|
return new(source.Value);
|
|
}
|
|
|
|
[MustUseReturnValue]
|
|
public EntityUid Copy(ISerializationManager serializationManager, EntityUid source, EntityUid target,
|
|
bool skipHook,
|
|
ISerializationContext? context = null)
|
|
{
|
|
return new((int) source);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Does basic pre-deserialization checks on map file load.
|
|
/// For example, let's not try to use maps with multiple grids as blueprints, shall we?
|
|
/// </summary>
|
|
private class MapData
|
|
{
|
|
public YamlStream Stream { get; }
|
|
|
|
public YamlNode RootNode => Stream.Documents[0].RootNode;
|
|
public int GridCount { get; }
|
|
|
|
public MapData(TextReader reader)
|
|
{
|
|
var stream = new YamlStream();
|
|
stream.Load(reader);
|
|
|
|
if (stream.Documents.Count < 1)
|
|
{
|
|
throw new InvalidDataException("Stream has no YAML documents.");
|
|
}
|
|
|
|
// Kinda wanted to just make this print a warning and pick [0] but screw that.
|
|
// What is this, a hug box?
|
|
if (stream.Documents.Count > 1)
|
|
{
|
|
throw new InvalidDataException("Stream too many YAML documents. Map files store exactly one.");
|
|
}
|
|
|
|
Stream = stream;
|
|
GridCount = ((YamlSequenceNode) RootNode["grids"]).Children.Count;
|
|
}
|
|
}
|
|
}
|
|
}
|