using System; using System.Collections.Generic; using System.Globalization; using System.Linq; 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 deserializing entities from yaml. It provides some more control over /// serialization than the methods provided by . /// public sealed partial class EntityDeserializer : ISerializationContext, ITypeSerializer, ITypeSerializer, ITypeSerializer { // See the comments around EntitySerializer's version const for information about the different versions. // TBH version three isn't even really fully supported anymore, simply due to changes in engine component serialization. // E.g., PR #3923 changed the physics fixture serialization from a sequence to a dictionary/mapping. // So any unmodified v3 file will with a grid will fail to load, though that's technically not due to any map // file formatting changes. public const int OldestSupportedVersion = 3; public const int NewestSupportedVersion = EntitySerializer.MapFormatVersion; public SerializationManager.SerializerProvider SerializerProvider { get; } [Dependency] public EntityManager EntMan = default!; [Dependency] public IGameTiming Timing = default!; [Dependency] private ISerializationManager _seriMan = default!; [Dependency] private IComponentFactory _factory = default!; [Dependency] private IPrototypeManager _proto = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private ILogManager _logMan = default!; [Dependency] private IDependencyCollection _deps = default!; private readonly ISawmill _log; private Stopwatch _stopwatch = new(); public readonly DeserializationOptions Options; /// /// Serialized entity data that is going to be read. /// public readonly MappingDataNode Data; /// /// Subset of the file's relevant to each entity, indexed by their allocated EntityUids /// public readonly Dictionary Entities = new(); /// /// Variant of indexed by the entity's yaml id. /// public readonly Dictionary YamlEntities = new(); /// /// Entity data grouped by their entity prototype id. Any entities without a prototype or with an invalid or /// deleted prototypes use an empty string. /// public readonly Dictionary> Prototypes = new(); public record struct EntData( int YamlId, MappingDataNode Node, Dictionary? Components, HashSet? MissingComponents, bool PostInit, bool Paused, bool ToDelete); public readonly LoadResult Result = new(); public readonly Dictionary TileMap = new(); public readonly Dictionary UidMap = new(); public readonly Dictionary AllocatedMapIds = new(); public readonly List MapYamlIds = new(); public readonly List GridYamlIds = new(); public readonly List OrphanYamlIds = new(); public readonly List NullspaceYamlIds = new(); public readonly Dictionary RenamedPrototypes; public readonly HashSet DeletedPrototypes; /// /// Entities that need to be flagged as map-initialized. This will not actually run map-init logic, this is for /// loading entities that have already been map-initialized and just need to be flagged as such. /// public readonly HashSet PostMapInit = new(); public readonly HashSet Paused = new(); public readonly HashSet ToDelete = new(); public readonly List SortedEntities = new(); private readonly Dictionary _components = new(); public EntData? CurrentReadingEntity; public string? CurrentComponent; private readonly EntityQuery _mapQuery; private readonly EntityQuery _gridQuery; private readonly EntityQuery _xformQuery; private readonly EntityQuery _metaQuery; public EntityDeserializer( IDependencyCollection deps, MappingDataNode data, DeserializationOptions options, Dictionary? renamedPrototypes = null, HashSet? deletedPrototypes = null) { deps.InjectDependencies(this); _log = _logMan.GetSawmill("entity_deserializer"); _log.Level = LogLevel.Info; SerializerProvider = new(_seriMan); SerializerProvider.RegisterSerializer(this); Data = data; Options = options; RenamedPrototypes = renamedPrototypes ?? new(); DeletedPrototypes = deletedPrototypes ?? new(); _mapQuery = EntMan.GetEntityQuery(); _gridQuery = EntMan.GetEntityQuery(); _xformQuery = EntMan.GetEntityQuery(); _metaQuery = EntMan.GetEntityQuery(); } /// /// Are we currently iterating prototypes or entities for writing. /// This is used to suppress some serialization errors/warnings. /// public bool WritingReadingPrototypes { get; private set; } /// /// This processes some of the data in , including extracting the metadata, tile-map, /// validating that all referenced entity prototypes exists, and generating collections for accessing entity data. /// /// Returns false if the entity data cannot be processed public bool TryProcessData() { ReadMetadata(); if (Result.Version < OldestSupportedVersion) { _log.Error( $"Cannot handle this map file version, found v{Result.Version} and require at least v{OldestSupportedVersion}"); return false; } if (Result.Version > NewestSupportedVersion) { _log.Error( $"Cannot handle this map file version, found v{Result.Version} but require at most v{NewestSupportedVersion}"); return false; } if (!ValidatePrototypes()) return false; ReadEntities(); ReadTileMap(); ReadMapsAndGrids(); return true; } /// /// Allocate entities, load the per-entity serialized data, and populate the various entity collections. /// public void CreateEntities() { // Alloc entities, and populate the yaml uid -> EntityUid maps AllocateEntities(); // Assign a map id to each map entity. This is required to de-serialize map coordinates & map ids if (Options.AssignMapIds) AllocateMapIds(); // Load the prototype data onto entities, e.g. transform parents, etc. LoadEntities(); // Get the lists of maps, grids, orphan, and nullspace entities GetRootEntities(); // grids prior to engine v175 might've been serialized with empty chunks which now throw debug asserts. RemoveEmptyChunks(); // Assign MapSaveTileMapComponent to all read grids. This is used to avoid large file diffs if the tile map changes. StoreGridTileMap(); if (Options.AssignMapIds) AssignMapIds(); CheckCategory(); } /// /// Finish entity startup & initialization, and delete any invalid entities /// public void StartEntities() { AdoptGrids(); ValidateMapIds(); BuildEntityHierarchy(); StartEntitiesInternal(); // Set loaded entity metadata SetMapInitLifestage(); SetPaused(); GetRootNodes(); // Apply entity metadata options PauseMaps(); InitializeMaps(); ProcessDeletions(); if (!Options.AssignMapIds) return; foreach (var yamlId in MapYamlIds) { if (AllocatedMapIds.TryGetValue(yamlId, out var alloc) && EntMan.EntityExists(UidMap[yamlId])) DebugTools.AssertEqual(_map.GetMap(alloc), UidMap[yamlId]); } } private void ReadMetadata() { var meta = Data.Get("meta"); Result.Version = meta.Get("format").AsInt(); if (meta.TryGet("engineVersion", out var engVer)) Result.EngineVersion = engVer.Value; if (meta.TryGet("forkId", out var forkId)) Result.ForkId = forkId.Value; if (meta.TryGet("forkVersion", out var forkVer)) Result.ForkVersion = forkVer.Value; if (meta.TryGet("time", out var timeNode) && DateTime.TryParse(timeNode.Value, out var time)) Result.Time = time; if (meta.TryGet("category", out var catNode) && Enum.TryParse(catNode.Value, out var res)) { Result.Category = res; } } /// /// Verify that the entity prototypes referenced in the file are all valid. /// private bool ValidatePrototypes() { _stopwatch.Restart(); var fail = false; var key = Result.Version >= 4 ? "proto" : "type"; var entities = Data.Get("entities"); foreach (var metaDef in entities.Cast()) { if (!metaDef.TryGet(key, out var typeNode)) continue; var type = typeNode.Value; if (string.IsNullOrWhiteSpace(type)) continue; if (RenamedPrototypes.TryGetValue(type, out var newType)) type = newType; if (DeletedPrototypes.Contains(type)) { _log.Warning("Map contains an obsolete/removed prototype: {0}. This may cause unexpected errors.", type); continue; } if (_proto.HasIndex(type)) continue; _log.Error("Missing prototype for map: {0}", type); fail = true; } _log.Debug($"Verified entities in {_stopwatch.Elapsed}"); if (!fail) return true; _log.Error("Found missing prototypes in map file. Missing prototypes have been dumped to logs."); return false; } /// /// Read entity section and populate groups. This does not actually create entities, it just /// groups them by their prototype. /// private void ReadEntities() { if (Result.Version == 3) { ReadEntitiesV3(); return; } if (Result.Version < 7) { // Older versions do not have per-entity mapinit and paused information. // But otherwise mostly identical ReadEntitiesFallback(); return; } // entities are grouped by prototype. var prototypeGroups = Data.Get("entities"); foreach (var protoGroup in prototypeGroups.Cast()) { EntProtoId? protoId = null; var deletedPrototype = false; if (protoGroup.TryGet("proto", out var protoIdNode) && !string.IsNullOrWhiteSpace(protoIdNode.Value)) { if (DeletedPrototypes.Contains(protoIdNode.Value)) { deletedPrototype = true; if (_proto.HasIndex(protoIdNode.Value)) protoId = protoIdNode.Value; } else if (RenamedPrototypes.TryGetValue(protoIdNode.Value, out var newType)) protoId = newType; else protoId = protoIdNode.Value; } var entities = (SequenceDataNode) protoGroup["entities"]; _proto.TryIndex(protoId, out var proto); var protoData = Prototypes.GetOrNew(proto?.ID ?? string.Empty); foreach (var entityNode in entities.Cast()) { var yamlId = entityNode.Get("uid").AsInt(); var postInit = entityNode.TryGet("mapInit", out var initNode) && initNode.AsBool(); // If the paused field does not exist, the default value depends on whether or not the entity has been // map-initialized. var paused = entityNode.TryGet("paused", out var pausedNode) ? pausedNode.AsBool() : !postInit; var (comps, missing) = GetComponents(entityNode); var entData = new EntData(yamlId, entityNode, comps, missing, postInit, paused, deletedPrototype); protoData.Add(entData); YamlEntities.Add(yamlId, entData); } } } private void ReadEntitiesV3() { var metadata = Data.Get("meta"); var preInit = metadata.TryGet("postmapinit", out var mapInitNode) && !mapInitNode.AsBool(); var entities = Data.Get("entities"); foreach (var entityNode in entities.Cast()) { var yamlId = entityNode.Get("uid").AsInt(); EntProtoId? protoId = null; var toDelete = false; if (entityNode.TryGet("type", out var protoIdNode)) { if (DeletedPrototypes.Contains(protoIdNode.Value)) { toDelete = true; if (_proto.HasIndex(protoIdNode.Value)) protoId = protoIdNode.Value; } else if (RenamedPrototypes.TryGetValue(protoIdNode.Value, out var newType)) protoId = newType; else protoId = protoIdNode.Value; } _proto.TryIndex(protoId, out var proto); var protoData = Prototypes.GetOrNew(proto?.ID ?? string.Empty); var (comps, missing) = GetComponents(entityNode); var entData = new EntData(yamlId, entityNode, comps, missing, PostInit: !preInit, Paused: preInit, toDelete); protoData.Add(entData); YamlEntities.Add(yamlId, entData); } } private void ReadEntitiesFallback() { var metadata = Data.Get("meta"); var preInit = metadata.TryGet("postmapinit", out var mapInitNode) && !mapInitNode.AsBool(); var prototypeGroups = Data.Get("entities"); foreach (var protoGroup in prototypeGroups.Cast()) { EntProtoId? protoId = null; var deletedPrototype = false; if (protoGroup.TryGet("proto", out var protoIdNode) && !string.IsNullOrWhiteSpace(protoIdNode.Value)) { if (DeletedPrototypes.Contains(protoIdNode.Value)) { deletedPrototype = true; if (_proto.HasIndex(protoIdNode.Value)) protoId = protoIdNode.Value; } else if (RenamedPrototypes.TryGetValue(protoIdNode.Value, out var newType)) protoId = newType; else protoId = protoIdNode.Value; } var entities = (SequenceDataNode) protoGroup["entities"]; _proto.TryIndex(protoId, out var proto); var protoData = Prototypes.GetOrNew(proto?.ID ?? string.Empty); foreach (var entityNode in entities.Cast()) { var yamlId = entityNode.Get("uid").AsInt(); var (comps, missing) = GetComponents(entityNode); var entData = new EntData(yamlId, entityNode, comps, missing, PostInit: !preInit, Paused: preInit, ToDelete: deletedPrototype); protoData.Add(entData); YamlEntities.Add(yamlId, entData); } } } private (Dictionary? Comps, HashSet? Missing) GetComponents(MappingDataNode node) { Dictionary? dict = null; HashSet? missing = null; if (node.TryGet("components", out SequenceDataNode? componentList)) { dict = new(componentList.Count); foreach (var compData in componentList.Cast()) { var value = ((ValueDataNode) compData["type"]).Value; compData.Remove("type"); dict.Add(value, compData); } } if (node.TryGet("missingComponents", out SequenceDataNode? missingComponentList)) { missing = new(missingComponentList.Count); foreach (var missNode in missingComponentList) { missing.Add(((ValueDataNode) missNode).Value); } } node.Remove("components"); node.Remove("missingComponents"); return (dict, missing); } private void ReadTileMap() { // Load tile mapping so that we can map the stored tile IDs into the ones actually used at runtime. _stopwatch.Restart(); var tileMap = Data.Get("tilemap"); var migrations = new Dictionary(); foreach (var proto in _proto.EnumeratePrototypes()) { migrations.Add(proto.ID, proto.Target); } foreach (var (key, value) in tileMap.Children) { var yamlTileId = int.Parse(key, CultureInfo.InvariantCulture); var tileName = ((ValueDataNode) value).Value; if (migrations.TryGetValue(tileName, out var @new)) tileName = @new; TileMap.Add(yamlTileId, tileName); } _log.Debug($"Read tilemap in {_stopwatch.Elapsed}"); } private void AllocateEntities() { _stopwatch.Restart(); foreach (var (protoId, ents) in Prototypes) { var proto = protoId == string.Empty ? null : _proto.Index(protoId); foreach (var ent in ents) { var entity = EntMan.AllocEntity(proto); Result.Entities.Add(entity); UidMap.Add(ent.YamlId, entity); Entities.Add(entity, ent); if (ent.PostInit) PostMapInit.Add(entity); if (ent.Paused) Paused.Add(entity); if (ent.ToDelete) ToDelete.Add(entity); if (Options.StoreYamlUids) EntMan.AddComponent(entity).Uid = ent.YamlId; } } _log.Debug($"Allocated {Entities.Count} entities in {_stopwatch.Elapsed}"); } private void AllocateMapIds() { if (Result.Version < 7) return; // MapYamlIds is not populated untill later in older versions foreach (var yamlMapId in MapYamlIds) { var mapUid = UidMap[yamlMapId]; AllocatedMapIds[yamlMapId] = _map.AllocateMapId(mapUid); } } private void ReadMapsAndGrids() { if (Result.Version < 7) return; ReadYamlIdList(Data, "maps", MapYamlIds); ReadYamlIdList(Data, "grids", GridYamlIds); ReadYamlIdList(Data, "orphans", OrphanYamlIds); ReadYamlIdList(Data, "nullspace", NullspaceYamlIds); } private void ReadYamlIdList(MappingDataNode data, string key, List list) { var sequence = data.Get(key); list.EnsureCapacity(sequence.Count); foreach (var node in sequence) { var yamlId = ((ValueDataNode) node).AsInt(); list.Add(yamlId); } } private void LoadEntities() { _stopwatch.Restart(); foreach (var (entity, data) in Entities) { #if EXCEPTION_TOLERANCE try { #endif CurrentReadingEntity = data; LoadEntity(entity, _metaQuery.Comp(entity), data.Components, data.MissingComponents); #if EXCEPTION_TOLERANCE } catch (Exception e) { ToDelete.Add(entity); _log.Error($"Encountered error while loading entity. Yaml uid: {data.YamlId}. Loaded loaded entity: {EntMan.ToPrettyString(entity)}. Error:\n{e}."); } #endif } CurrentReadingEntity = null; _log.Debug($"Loaded {Entities.Count} entities in {_stopwatch.Elapsed}"); } private void LoadEntity( EntityUid uid, MetaDataComponent meta, Dictionary? comps, HashSet? missingComps) { var proto = meta.EntityPrototype; // Get any serialized component data for this entity, and push any implicit data-fields from the component // and store the result in _components. _components.Clear(); if (comps != null) { _components.EnsureCapacity(comps.Count); foreach (var (name, compData) in comps) { DebugTools.Assert(missingComps?.Contains(name) != true); if (!_factory.TryGetRegistration(name, out _)) { if (!_factory.IsIgnored(name)) _log.Error($"Encountered unregistered component ({name}) while loading entity {EntMan.ToPrettyString(uid)}"); continue; } var datanode = compData; if (proto != null && _proto.GetPrototypeData(proto).TryGetValue(name, out var protoData)) datanode = _seriMan.CombineMappings(compData, protoData); _components.Add(name, datanode); } } // Iterate over the prototype's components, and add them to the entity unless the entity has data relevant to // that component from the map file if (proto != null) { foreach (var (name, entry) in proto.Components) { if (missingComps != null && missingComps.Contains(name)) continue; if (_components.ContainsKey(name)) continue; CurrentComponent = name; var compReg = _factory.GetRegistration(name); if (!EntMan.TryGetComponent(uid, compReg.Idx, out var component)) { var newComponent = _factory.GetComponent(compReg); EntMan.AddComponent(uid, newComponent); component = newComponent; } _seriMan.CopyTo(entry.Component, ref component, this, notNullableOverride: true); if (!entry.Component.NetSyncEnabled && compReg.NetID is { } netId) meta.NetComponents.Remove(netId); } } // Finally, copy over the entity specific information foreach (var (name, data) in _components) { CurrentComponent = name; var compReg = _factory.GetRegistration(name); if (!EntMan.TryGetComponent(uid, compReg.Idx, out var existing)) { // New component not present in the prototype. var newComponent = (IComponent) _seriMan.Read(compReg.Type, data, this)!; // TODO ECS remove this when everything has been ECSd if (newComponent is ISerializationHooks) { // Some components depend on Component.Owner being correctly set after serialization // E.g., ContainerManagerComponent // So we have this jank edge case. // I hate this. existing = _factory.GetComponent(compReg); EntMan.AddComponent(uid, existing); _seriMan.CopyTo(newComponent, ref existing, this, notNullableOverride: true); continue; } // TODO ECS also remove this _deps.InjectDependencies(newComponent); EntMan.AddComponent(uid, newComponent); continue; } // TODO ENTITY SERIALIZATION // Copy directly into the existing object // I'm scared turning over this rock will reveal a lot of bugs. So leaving that to a future PR. // I.e., creating "temp" here just unnecessarily slows everything down. var temp = (IComponent) _seriMan.Read(compReg.Type, data, this)!; _seriMan.CopyTo(temp, ref existing, this, notNullableOverride: true); } _components.Clear(); CurrentComponent = null; if (missingComps is {Count: > 0}) { EntMan.DirtyEntity(uid, meta); meta.LastComponentRemoved = Timing.CurTick; } } private void GetRootEntities() { if (Result.Version < 7) { GetRootEntitiesFallback(); return; } foreach (var yamlId in MapYamlIds) { if (UidMap.TryGetValue(yamlId, out var uid) && _mapQuery.TryComp(uid, out var map)) { Result.Maps.Add((uid, map)); EntMan.EnsureComponent(uid); } else _log.Error($"Missing map entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}"); } foreach (var yamlId in GridYamlIds) { if (UidMap.TryGetValue(yamlId, out var uid) && _gridQuery.TryComp(uid, out var grid)) Result.Grids.Add((uid, grid)); else _log.Error($"Missing grid entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}"); } foreach (var yamlId in OrphanYamlIds) { if (!UidMap.TryGetValue(yamlId, out var uid)) _log.Error($"Missing orphan entity with YamlId: {yamlId}"); else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid()) _log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as an orphan? YamlId: {yamlId}"); else Result.Orphans.Add(uid); } foreach (var yamlId in NullspaceYamlIds) { if (!UidMap.TryGetValue(yamlId, out var uid)) _log.Error($"Missing nullspace entity with YamlId: {yamlId}"); else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid()) _log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as a null-space entity?"); else Result.NullspaceEntities.Add(uid); } } public void AssignMapIds() { foreach (var map in Result.Maps) { _map.AssignMapId(map); } } private void GetRootEntitiesFallback() { // Older versions did not support non-grid orphaned entities or nullspace entities. // So we just check for grids & maps. foreach (var uid in Result.Entities) { if (_gridQuery.TryComp(uid, out var grid)) { Result.Grids.Add((uid, grid)); if (_xformQuery.Comp(uid).ParentUid == EntityUid.Invalid && !_mapQuery.HasComp(uid)) Result.Orphans.Add(uid); } if (_mapQuery.TryComp(uid, out var map)) { Result.Maps.Add((uid, map)); EntMan.EnsureComponent(uid); } } } private void RemoveEmptyChunks() { foreach (var uid in Entities.Keys) { if (!_gridQuery.TryGetComponent(uid, out var gridComp)) continue; foreach (var (index, chunk) in gridComp.Chunks) { if (chunk.FilledTiles > 0) continue; _log.Warning( $"Encountered empty chunk while deserializing map. Grid: {EntMan.ToPrettyString(uid)}. Chunk index: {index}"); gridComp.Chunks.Remove(index); } } } private void StoreGridTileMap() { /* if (TileMap.Count == 0) return; */ foreach (var entity in Result.Grids) { EntMan.EnsureComponent(entity).TileMap = TileMap.ShallowClone(); } } private void BuildEntityHierarchy() { _stopwatch.Restart(); var processed = new HashSet(Result.Entities.Count); foreach (var ent in Result.Entities) { BuildEntityHierarchy(ent, processed); } _log.Debug($"Built entity hierarchy for {Result.Entities.Count} entities in {_stopwatch.Elapsed}"); } /// /// Validate that the category read from the metadata section is correct /// private void CheckCategory() { if (Result.Version < 7) { InferCategory(); return; } switch (Result.Category) { case FileCategory.Map: if (Result.Maps.Count == 1 && Result.Orphans.Count == 0) return; _log.Error($"Expected file to contain a single map, but instead found {Result.Maps.Count} maps and {Result.Orphans.Count} orphans"); break; case FileCategory.Grid: if (Result.Maps.Count == 0 && Result.Grids.Count == 1 && Result.Orphans.Count == 1 && Result.Orphans.First() == Result.Grids.First().Owner) return; _log.Error($"Expected file to contain a single grid, but instead found {Result.Grids.Count} grids and {Result.Orphans.Count} orphans"); break; case FileCategory.Entity: if (Result.Maps.Count == 0 && Result.Grids.Count == 0 && Result.Orphans.Count == 1) return; _log.Error($"Expected file to contain a orphaned entity, but instead found {Result.Orphans.Count} orphans"); break; case FileCategory.Save: // No validation for full game saves, they can contain whatever they want. default: return; } } private void InferCategory() { if (Result.Category != FileCategory.Unknown) return; if (Result.Maps.Count == 1) Result.Category = FileCategory.Map; else if (Result.Maps.Count == 0 && Result.Grids.Count == 1) Result.Category = FileCategory.Grid; } /// /// In case there are any "orphaned" grids, we want to ensure that they all have a map before we initialize them, /// as grids in null-space are not currently supported. /// private void AdoptGrids() { foreach (var grid in Result.Grids) { if (_mapQuery.HasComponent(grid.Owner)) continue; var xform = _xformQuery.Comp(grid.Owner); if (xform.ParentUid.IsValid()) continue; DebugTools.Assert(Result.Orphans.Contains(grid.Owner)); if (Options.LogOrphanedGrids) _log.Error($"Encountered an orphaned grid. Automatically creating a map for the grid."); var map = _map.CreateUninitializedMap(); _map.AssignMapId(map); // We intentionally do this after maps have been given the LoadedMapComponent, so this map will not have it. // vague justification is that this entity wasn't actually deserialized from the file, and shouldn't // contain any non-default data. // But the real reason is that this is just how it used to work due to shitty code that never properly // distinguished between grid & map files, and checks for this component after deserialization to check whether // the file was a grid or map. // So we still support code that tries to load a file without knowing what's in it, but unless the options // disable it, the default behaviour is to log an error in this situation. This is meant to try ensure that // people use the `TryLoadGrid` method when appropriate. Result.Entities.Add(map); Result.Maps.Add(map); Result.Orphans.Remove(grid.Owner); xform._parent = map.Owner; DebugTools.Assert(!xform._mapIdInitialized); } } /// /// Verify that all map entities have been assigned a map id. /// private void ValidateMapIds() { foreach (var map in Result.Maps) { if (map.Comp.MapId == MapId.Nullspace || !_map.TryGetMap(map.Comp.MapId, out var e) || e != map.Owner) { throw new Exception($"Map entity {EntMan.ToPrettyString(map)} has not been assigned a map id"); } } } private void PauseMaps() { if (!Options.PauseMaps) return; foreach (var ent in Result.Maps) { _map.SetPaused(ent!, true); } } private void BuildEntityHierarchy(EntityUid uid, HashSet processed) { // If we've already added it then skip. if (!processed.Add(uid)) return; if (!_xformQuery.TryComp(uid, out var xform)) return; // Ensure parent is done first. var parent = xform.ParentUid; if (parent != EntityUid.Invalid) BuildEntityHierarchy(parent, processed); // If entities were moved around or merged onto an existing map, it is possible that the entities passed // to this method were not originally being deserialized. if (!Result.Entities.Contains(uid)) return; SortedEntities.Add(uid); } private void StartEntitiesInternal() { _stopwatch.Restart(); foreach (var uid in SortedEntities) { StartupEntity(uid, _metaQuery.GetComponent(uid)); } _log.Debug($"Started up {Result.Entities.Count} entities in {_stopwatch.Elapsed}"); } private void StartupEntity(EntityUid uid, MetaDataComponent metadata) { ResetNetTicks(uid, metadata); EntMan.InitializeEntity(uid, metadata); EntMan.StartEntity(uid); } private void ResetNetTicks(EntityUid uid, MetaDataComponent metadata) { if (!Entities.TryGetValue(uid, out var entData)) { // AdoptGrids() can create new maps that have no associated yaml data. return; } if (metadata.EntityPrototype is not { } prototype) return; if (entData.Components == null) return; foreach (var component in metadata.NetComponents.Values) { var compName = _factory.GetComponentName(component.GetType()); if (!entData.Components.ContainsKey(compName)) { // This component is not modified by the map file, // so the client will have the same data after instantiating it from prototype ID. component.ClearTicks(); continue; } if (prototype.Components.ContainsKey(compName)) { // 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. component.ClearCreationTick(); } } } private void SetMapInitLifestage() { if (PostMapInit.Count == 0) return; _stopwatch.Restart(); foreach (var uid in PostMapInit) { if (!_metaQuery.TryComp(uid, out var meta)) continue; DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized); EntMan.SetLifeStage(meta, EntityLifeStage.MapInitialized); } _log.Debug($"Finished flagging mapinit in {_stopwatch.Elapsed}"); } private void SetPaused() { if (Paused.Count == 0) return; _stopwatch.Restart(); var time = Timing.CurTime; var ev = new EntityPausedEvent(); foreach (var uid in Paused) { if (!_metaQuery.TryComp(uid, out var meta)) continue; meta.PauseTime = time; // TODO ENTITY SERIALIZATION // TODO PowerNet / NodeNet Serialization // Make this **not** raise an event. // Ideally an event shouldn't be required. However some stinky systems rely on it to actually pause entities. // E.g., power nets don't get serialized properly, and store entity-paused information in a separate object, // so they needs to receive the event to make sure that the entity is **actually** paused. // hours of debugging // AAAAAAAAaaaa // Whenever node nets become nullspace ents, this can probably just be purged. EntMan.EventBus.RaiseLocalEvent(uid, ref ev); } _log.Debug($"Finished setting PauseTime in {_stopwatch.Elapsed}"); } private void InitializeMaps() { if (!Options.InitializeMaps) { if (Options.PauseMaps) return; // Already paused foreach (var ent in Result.Maps) { if (_metaQuery.Comp(ent.Owner).EntityLifeStage < EntityLifeStage.MapInitialized) _map.SetPaused(ent!, true); } return; } foreach (var ent in Result.Maps) { if (!ent.Comp.MapInitialized) _map.InitializeMap(ent!, unpause: !Options.PauseMaps); } } private void ProcessDeletions() { foreach (var uid in ToDelete) { EntMan.DeleteEntity(uid); Result.Entities.Remove(uid); } } private void GetRootNodes() { Result.RootNodes.UnionWith(Result.Orphans); Result.RootNodes.UnionWith(Result.NullspaceEntities); foreach (var map in Result.Maps) { Result.RootNodes.Add(map.Owner); } // These asserts are probably a bit over-kill // but might as well check nothing has gone wrong somehow. #if DEBUG var grids = Result.Grids.Select(x => x.Owner).ToHashSet(); var maps = Result.Maps.Select(x => x.Owner).ToHashSet(); var totalRoots = 0; foreach (var uid in Result.Entities) { if (ToDelete.Contains(uid)) continue; DebugTools.Assert(maps.Contains(uid) == _mapQuery.HasComp(uid)); DebugTools.AssertEqual(grids.Contains(uid), _gridQuery.HasComp(uid)); if (!_xformQuery.TryComp(uid, out var xform)) continue; if (xform.ParentUid != EntityUid.Invalid) continue; totalRoots++; DebugTools.Assert(Result.RootNodes.Contains(uid)); DebugTools.Assert(Result.Orphans.Contains(uid) || Result.NullspaceEntities.Contains(uid) || maps.Contains(uid)); } DebugTools.AssertEqual(Result.RootNodes.Count, totalRoots); DebugTools.AssertEqual(maps.Intersect(Result.Orphans).Count(), 0); DebugTools.AssertEqual(maps.Intersect(Result.NullspaceEntities).Count(), 0); DebugTools.AssertEqual(grids.Intersect(Result.NullspaceEntities).Count(), 0); DebugTools.AssertEqual(Result.Orphans.Intersect(Result.NullspaceEntities).Count(), 0); #endif } #region ITypeSerializer ValidationNode ITypeValidator.Validate( ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context) { if (node.Value is "invalid") return new ValidatedValueNode(node); if (!int.TryParse(node.Value, out _)) return new ErrorNode(node, "Invalid EntityUid"); return new ValidatedValueNode(node); } DataNode ITypeWriter.Write( ISerializationManager serializationManager, EntityUid value, IDependencyCollection dependencies, bool alwaysWrite, ISerializationContext? context) { return value.IsValid() ? new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture)) : new ValueDataNode("invalid"); } EntityUid ITypeReader.Read( ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context, ISerializationManager.InstantiationDelegate? _) { string msg; if (node.Value == "invalid") { if (CurrentComponent == "Transform") return EntityUid.Invalid; if (!Options.LogInvalidEntities) return EntityUid.Invalid; msg = CurrentReadingEntity is not { } curr ? $"Encountered invalid EntityUid reference" : $"Encountered invalid EntityUid reference while reading entity {curr.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return EntityUid.Invalid; } if (int.TryParse(node.Value, out var val) && UidMap.TryGetValue(val, out var entity)) return entity; msg = CurrentReadingEntity is not { } ent ? "Encountered unknown entity yaml uid" : $"Encountered unknown entity yaml uid while reading entity {ent.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return EntityUid.Invalid; } ValidationNode ITypeValidator.Validate( ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context) { if (node.Value is "invalid") return new ValidatedValueNode(node); if (!int.TryParse(node.Value, out _)) return new ErrorNode(node, "Invalid NetEntity"); return new ValidatedValueNode(node); } NetEntity ITypeReader.Read( ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context, ISerializationManager.InstantiationDelegate? instanceProvider) { var uid = serializationManager.Read(node, context); if (EntMan.TryGetNetEntity(uid, out var nent)) return nent.Value; _log.Error($"Failed to get NetEntity entity {EntMan.ToPrettyString(uid)}"); return NetEntity.Invalid; } DataNode ITypeWriter.Write( ISerializationManager serializationManager, NetEntity value, IDependencyCollection dependencies, bool alwaysWrite, ISerializationContext? context) { return value.IsValid() ? new ValueDataNode(value.Id.ToString(CultureInfo.InvariantCulture)) : new ValueDataNode("invalid"); } 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) { if (!Options.AssignMapIds || Result.Version < 7) { _log.Error("Cannot deserialize map ids without pre-allocated ids"); return MapId.Nullspace; } if (int.TryParse(node.Value, out var val) && AllocatedMapIds.TryGetValue(val, out var map)) return map; var msg = CurrentReadingEntity is not { } ent ? "Encountered unknown yaml map id" : $"Encountered unknown yaml map id while reading entity {ent.YamlId}, component: {CurrentComponent}"; _log.Error(msg); return MapId.Nullspace; } DataNode ITypeWriter.Write( ISerializationManager seri, MapId value, IDependencyCollection deps, bool alwaysWrite, ISerializationContext? ctx) { return seri.WriteValue(_map.GetMapOrInvalid(value), alwaysWrite, ctx); } #endregion }