using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; 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; /// /// This class provides methods for serializing entities into yaml. It provides some more control over /// serialization than the methods provided by . /// /// /// There are several methods (e.g., that serialize entities into a /// per-entity stored in the dictionary, which is indexed by the /// entity's assigned yaml id (see . The generated data can then be written to a larger yaml /// document using the various "Write" methods. (e.g., ). After a one has finished using /// the generated data, the serializer needs to be reset () using it again to serialize other entities. /// public sealed partial class EntitySerializer : ISerializationContext, ITypeSerializer, ITypeSerializer, ITypeSerializer { 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; } [Dependency] public EntityManager EntMan = default!; [Dependency] public IGameTiming Timing = default!; [Dependency] private IComponentFactory _factory = default!; [Dependency] private ISerializationManager _serialization = default!; [Dependency] private ITileDefinitionManager _tileDef = default!; [Dependency] private IConfigurationManager _conf = default!; [Dependency] private ILogManager _logMan = default!; [Dependency] private SharedMapSystem _map = default!; private readonly ISawmill _log; public readonly Dictionary YamlUidMap = new(); public readonly HashSet YamlIds = new(); public readonly ValueDataNode InvalidNode = new("invalid"); public string? CurrentComponent { get; private set; } public Entity? CurrentEntity { get; private set; } public int CurrentEntityYamlUid { get; private set; } /// /// Tile ID -> yaml tile ID mapping. /// private readonly Dictionary _tileMap = new(); private readonly HashSet _yamlTileIds = new(); /// public bool WritingReadingPrototypes { get; private set; } /// /// 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. /// public EntityUid Truncate { get; private set; } /// /// List of all entities that have previously been ignored via . /// /// /// 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. /// public readonly HashSet Truncated = new(); public readonly SerializationOptions Options; /// /// Cached prototype data. This is used to avoid writing redundant data that is already specified in an entity's /// prototype. /// public readonly Dictionary> PrototypeCache = new(); /// /// The serialized entity data. /// public readonly Dictionary EntityData = new(); /// /// indices grouped by their entity prototype ids. /// public readonly Dictionary> Prototypes = new(); /// /// Set of entities that have encountered issues during serialization and are now being ignored. /// public HashSet ErroringEntities = new(); /// /// Yaml ids of all serialized map entities. /// public readonly List Maps = new(); /// /// 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 . /// public readonly List Nullspace = new(); /// /// Yaml ids of all serialized grid entities. /// public readonly List Grids = new(); /// /// 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. /// public readonly List 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 _autoInclude = new(); private readonly List _sortedTileIds = new(); private readonly List _sortedProtoIds = new(); private readonly EntityQuery _yamlQuery; private readonly EntityQuery _gridQuery; private readonly EntityQuery _mapQuery; private readonly EntityQuery _metaQuery; private readonly EntityQuery _xformQuery; /// /// C# event for checking whether an entity is serializable. Can be used by content to prevent specific entities /// from getting serialized. /// public event IsSerializableDelegate? OnIsSerializeable; public delegate void IsSerializableDelegate(Entity ent, ref bool serializable); public EntitySerializer(IDependencyCollection dependency, SerializationOptions options) { dependency.InjectDependencies(this); _log = _logMan.GetSawmill("entity_serializer"); SerializerProvider = new(_serialization); SerializerProvider.RegisterSerializer(this); _metaName = _factory.GetComponentName(); _xformName = _factory.GetComponentName(); _emptyMetaNode = _serialization.WriteValueAs(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: this); CurrentComponent = _xformName; _emptyXformNode = _serialization.WriteValueAs(typeof(TransformComponent), new TransformComponent(), alwaysWrite: true, context: this); CurrentComponent = null; _yamlQuery = EntMan.GetEntityQuery(); _gridQuery = EntMan.GetEntityQuery(); _mapQuery = EntMan.GetEntityQuery(); _metaQuery = EntMan.GetEntityQuery(); _xformQuery = EntMan.GetEntityQuery(); Options = options; } public bool IsSerializable(Entity 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 /// /// Serialize a single entity. This does not automatically include /// children, though depending on the setting of it may /// auto-include additional entities aside from the one provided. /// 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(); } /// /// Serialize a set of entities. This does not automatically include children or parents, though depending on the /// setting of it may auto-include additional entities /// aside from the one provided. /// /// The set of entities to serialize public void SerializeEntities(HashSet entities) { foreach (var uid in entities) { if (!IsSerializable(uid)) throw new Exception($"{EntMan.ToPrettyString(uid)} is not serializable"); } ReserveYamlIds(entities); SerializeEntitiesInternal(entities); } /// /// Serializes an entity and all of its serializable children. Note that this will not automatically serialize the /// entity's parents. /// 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 entities = new(); RecursivelyIncludeChildren(root, entities); ReserveYamlIds(entities); SerializeEntitiesInternal(entities); Truncate = EntityUid.Invalid; } /// /// Serializes several entities and all of their children. Note that this will not automatically serialize the /// entity's parents. /// public void SerializeEntityRecursive(HashSet roots) { if (roots.Count == 0) return; using (var enumerator = roots.GetEnumerator()) { enumerator.MoveNext(); InitializeTileMap(enumerator.Current); } HashSet allEntities = new(); List<(EntityUid Root, HashSet Children)> entities = new(); foreach(var root in roots) { if (!IsSerializable(root)) throw new Exception($"{EntMan.ToPrettyString(root)} is not serializable"); var ents = new HashSet(); RecursivelyIncludeChildren(root, ents); entities.Add((root, ents)); allEntities.UnionWith(ents); } ReserveYamlIds(allEntities); foreach (var (root, children) in entities) { Truncate = _xformQuery.GetComponent(root).ParentUid; Truncated.Add(Truncate); SerializeEntitiesInternal(children); Truncate = EntityUid.Invalid; } } #endregion /// /// Initialize the that is used to serialize grid chunks using /// . 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. /// 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); _yamlTileIds.Add(origId); // Make sure we record the IDs we're using so when we need to reserve new ones we can } } } private bool FindSavedTileMap(EntityUid root, [NotNullWhen(true)] out Dictionary? map) { // Try and fetch the mapping directly if (EntMan.TryGetComponent(root, out MapSaveTileMapComponent? comp)) { map = comp.TileMap; return true; } map = null; // if this is a map, iterate over all of its children and grab the first grid with a mapping if (!_mapQuery.HasComponent(root)) return false; var xform = _xformQuery.GetComponent(root); foreach (var child in xform._children) { if (!EntMan.TryGetComponent(child, out MapSaveTileMapComponent? cComp)) continue; map = cComp.TileMap; return true; } return false; } #region AutoInclude private void ProcessAutoInclude() { DebugTools.Assert(!CollectionHelpers.ContainsDuplicates(_autoInclude)); var ents = new HashSet(); 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(); 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 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 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 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 entities) { foreach (var uid in entities) { DebugTools.AssertNull(CurrentEntity); SerializeEntityInternal(uid); } DebugTools.AssertNull(CurrentEntity); if (_autoInclude.Count != 0) ProcessAutoInclude(); } /// /// Serialize a single entity, and store the results in . /// 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 an 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; } try { SerializeComponents(uid, cache, components); } catch(Exception e) { if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.Rethrow) { _log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}"); throw; } _log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}:\n{e}"); CurrentEntityYamlUid = 0; CurrentEntity = null; CurrentComponent = null; RemoveErroringEntity(uid); return; } CurrentComponent = null; if (components.Count != 0) entData.Add("components", components); // TODO ENTITY SERIALIZATION // Consider adding a Action? 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 fewer 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 var component) && !EntMan.IsComponentPendingRemoval(component)) continue; missingComponents ??= new(); missingComponents.Add(new ValueDataNode(name)); } if (missingComponents != null) entData.Add("missingComponents", missingComponents); CurrentEntityYamlUid = 0; CurrentEntity = null; } /// /// Remove an exception throwing entity (and possibly its children) from the serialized data. /// private void RemoveErroringEntity(EntityUid uid) { if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.IgnoreEntityAndChildren) { foreach (var child in _xformQuery.GetComponent(uid)._children) { RemoveErroringEntity(child); } } ErroringEntities.Add(uid); if (!YamlUidMap.TryGetValue(uid, out var yamlId)) return; Nullspace.Remove(yamlId); Orphans.Remove(yamlId); Maps.Remove(yamlId); Grids.Remove(yamlId); EntityData.Remove(yamlId); if (_metaQuery.TryGetComponent(uid, out var meta) && meta.EntityPrototype != null && Prototypes.TryGetValue(meta.EntityPrototype.ID, out var proto)) { proto.Remove(yamlId); } } private void SerializeComponents(EntityUid uid, Dictionary? cache, SequenceDataNode components) { foreach (var component in EntMan.GetComponentsInternal(uid)) { if (EntMan.IsComponentPendingRemoval(component)) continue; 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(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(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) continue; compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name)); components.Add(compMapping); } } private Dictionary? 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(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.Assert(!CollectionHelpers.ContainsDuplicates(Maps), "Duplicate maps?"); DebugTools.Assert(!CollectionHelpers.ContainsDuplicates(Grids), "Duplicate grids?"); DebugTools.Assert(!CollectionHelpers.ContainsDuplicates(Orphans), "Duplicate orphans?"); DebugTools.Assert(!CollectionHelpers.ContainsDuplicates(Nullspace), "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 ids) { var result = new SequenceDataNode(); foreach (var id in ids) { result.Add(new ValueDataNode(id.ToString(CultureInfo.InvariantCulture))); } return result; } /// /// Serialize the to yaml. This data is required to deserialize any serialized grid chunks using . /// public MappingDataNode WriteTileMap() { var map = new MappingDataNode(); _sortedTileIds.Clear(); foreach (var tileId in _tileMap.Keys) { _sortedTileIds.Add(tileId); } _sortedTileIds.Sort(); foreach (var tileId in _sortedTileIds) { // 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 yamlTileId = _tileMap[tileId]; var yamlId = yamlTileId.ToString(CultureInfo.InvariantCulture); map.Add(yamlId, def.ID); } return map; } public SequenceDataNode WriteEntitySection() { // Check that EntityData contains the expected number of entities. if (Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntity && Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntityAndChildren && (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(); _sortedProtoIds.Clear(); foreach (var protoId in Prototypes.Keys) { _sortedProtoIds.Add(protoId); } _sortedProtoIds.Sort(StringComparer.InvariantCulture); foreach (var protoId in _sortedProtoIds) { 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; } /// /// Get the category that the serialized data belongs to. If one was specified in the /// it will use that after validating it, otherwise it will attempt to infer a /// category. /// 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 /// /// Get (or allocate) the integer id that will be used in the serialized file to refer to the given entity. /// 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++; } /// /// Get (or allocate) the integer id that will be used in the serialized file to refer to the given grid tile id. /// 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++; } /// /// This method ensures that the given entities have a yaml ids assigned. If the entities have a /// , they will attempt to use that id, which exists to prevent large map file diffs /// due to changing yaml ids. /// public void ReserveYamlIds(HashSet entities) { List 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); } } /// /// This method ensures that the given entity has a yaml id assigned to it. If the entity has a /// , it will attempt to use that id, which exists to prevent large map file diffs due /// to changing yaml ids. /// 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.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 InvalidNode; DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid)); Orphans.Add(CurrentEntityYamlUid); if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate && !ErroringEntities.Contains(value)) _log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}"); return InvalidNode; } if (ErroringEntities.Contains(value)) { // Referenced entity already logged an error, so we just silently fail. return InvalidNode; } if (value == EntityUid.Invalid) { if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore) _log.Error($"Encountered an invalid entityUid reference."); return InvalidNode; } 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 InvalidNode; case MissingEntityBehaviour.Ignore: return InvalidNode; 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.Read( ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context, ISerializationManager.InstantiationDelegate? _) { 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? 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); } ValidationNode ITypeValidator.Validate( ISerializationManager seri, ValueDataNode node, IDependencyCollection deps, ISerializationContext? context) { return seri.ValidateNode(node, context); } MapId ITypeReader.Read( ISerializationManager seri, ValueDataNode node, IDependencyCollection deps, SerializationHookContext hookCtx, ISerializationContext? ctx, ISerializationManager.InstantiationDelegate? instanceProvider) { return EntMan.TryGetComponent(seri.Read(node, ctx), out MapComponent? mapComp) ? mapComp.MapId : MapId.Nullspace; } DataNode ITypeWriter.Write( ISerializationManager seri, MapId value, IDependencyCollection deps, bool alwaysWrite, ISerializationContext? ctx) { if (_map.TryGetMap(value, out var uid)) return seri.WriteValue(uid, alwaysWrite, ctx); _log.Error($"Attempted to serialize invalid map id {value} while serializing component '{CurrentComponent}' on entity '{EntMan.ToPrettyString(uid)}'"); return new ValueDataNode("invalid"); } #endregion }