Files
RobustToolbox/Robust.Server/Maps/MapLoader.cs
Paul Ritter 80f9f24243 Serialization v3 aka constant suffering (#1606)
* 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>
2021-03-04 15:59:14 -08:00

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;
}
}
}
}