using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using Robust.Server.Maps; using Robust.Shared.ContentPack; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Map.Events; using Robust.Shared.Maths; 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.Value; using Robust.Shared.Timing; using Robust.Shared.Utility; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; namespace Robust.Server.GameObjects; public sealed class MapLoaderSystem : EntitySystem { /* * Not a partial of MapSystem so we don't have to deal with additional test dependencies. */ [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly ISerializationManager _serManager = default!; private IServerEntityManagerInternal _serverEntityManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly MetaDataSystem _meta = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private ISawmill _logLoader = default!; private ISawmill _logWriter = default!; private const int MapFormatVersion = 6; private const int BackwardsVersion = 2; private MapSerializationContext _context = default!; private Stopwatch _stopwatch = new(); public override void Initialize() { base.Initialize(); _serverEntityManager = (IServerEntityManagerInternal)EntityManager; _logLoader = Logger.GetSawmill("loader"); _logWriter = Logger.GetSawmill("writer"); _logLoader.Level = LogLevel.Info; _context = new MapSerializationContext(_serverEntityManager, _timing); } #region Public [Obsolete("Use TryLoad")] public EntityUid? LoadGrid(MapId mapId, string path, MapLoadOptions? options = null) { if (!TryLoad(mapId, path, out var grids, options)) { DebugTools.Assert(false); return null; } var actualGrids = new List(); var gridQuery = GetEntityQuery(); foreach (var ent in grids) { if (!gridQuery.HasComponent(ent)) continue; actualGrids.Add(ent); } DebugTools.Assert(actualGrids.Count == 1); return actualGrids[0]; } [Obsolete("Use TryLoad")] public IReadOnlyList LoadMap(MapId mapId, string path, MapLoadOptions? options = null) { if (TryLoad(mapId, path, out var grids, options)) { var actualGrids = new List(); var gridQuery = GetEntityQuery(); foreach (var ent in grids) { if (!gridQuery.HasComponent(ent)) continue; actualGrids.Add(ent); } return actualGrids; } DebugTools.Assert(false); return new List(); } public void Load(MapId mapId, string path, MapLoadOptions? options = null) { TryLoad(mapId, path, out _, options); } /// /// Tries to load the supplied path onto the supplied Mapid. /// Will return false if something went wrong and needs handling. /// /// The Mapid to load onto. Depending on the supplied options this map may or may not already exist. /// The resource path to the required map. /// The root Uids of the map; not guaranteed to be grids! /// The required options for loading. /// public bool TryLoad(MapId mapId, string path, [NotNullWhen(true)] out IReadOnlyList? rootUids, MapLoadOptions? options = null) { options ??= new(); var resPath = new ResPath(path).ToRootedPath(); if (!TryGetReader(resPath, out var reader)) { rootUids = new List(); return false; } bool result; using (reader) { _logLoader.Info($"Loading Map: {resPath}"); _stopwatch.Restart(); var data = new MapData(mapId, reader, options); _logLoader.Debug($"Loaded yml stream in {_stopwatch.Elapsed}"); var sw = new Stopwatch(); sw.Start(); result = Deserialize(data); _logLoader.Debug($"Loaded map in {sw.Elapsed}"); var mapEnt = _mapManager.GetMapEntityId(mapId); var xformQuery = _serverEntityManager.GetEntityQuery(); var rootEnts = new List(); // aeoeoeieioe content if (HasComp(mapEnt)) { rootEnts.Add(mapEnt); } else { foreach (var ent in data.Entities) { if (xformQuery.GetComponent(ent).ParentUid == mapEnt) rootEnts.Add(ent); } } rootUids = rootEnts; } _context.Clear(); #if DEBUG DebugTools.Assert(result); #endif return result; } public void Save(EntityUid uid, string ymlPath) { if (!Exists(uid)) { _logLoader.Error($"Unable to find entity {uid} for saving."); return; } if (Transform(uid).MapUid == null) { _logLoader.Error($"Found invalid map for {ToPrettyString(uid)}, aborting saving."); return; } _logLoader.Debug($"Saving entity {ToPrettyString(uid)} to {ymlPath}"); var document = new YamlDocument(GetSaveData(uid).ToYaml()); var resPath = new ResPath(ymlPath).ToRootedPath(); _resourceManager.UserData.CreateDir(resPath.Directory); using var writer = _resourceManager.UserData.OpenWriteText(resPath); { var stream = new YamlStream { document }; stream.Save(new YamlMappingFix(new Emitter(writer)), false); } _logLoader.Info($"Saved {ToPrettyString(uid)} to {ymlPath}"); } public void SaveMap(MapId mapId, string ymlPath) { if (!_mapManager.MapExists(mapId)) { _logLoader.Error($"Unable to find map {mapId}"); return; } Save(_mapManager.GetMapEntityId(mapId), ymlPath); } #endregion #region Loading private bool TryGetReader(ResPath resPath, [NotNullWhen(true)] out TextReader? reader) { // try user if (!_resourceManager.UserData.Exists(resPath)) { _logLoader.Info($"No user map found: {resPath}"); // fallback to content if (_resourceManager.TryContentFileRead(resPath, out var contentReader)) { reader = new StreamReader(contentReader); } else { _logLoader.Error($"No map found: {resPath}"); reader = null; return false; } } else { reader = _resourceManager.UserData.OpenText(resPath); } return true; } private bool Deserialize(MapData data) { var ev = new BeforeEntityReadEvent(); RaiseLocalEvent(ev); // First we load map meta data like version. if (!ReadMetaSection(data)) return false; // Verify that prototypes for all the entities exist if (!VerifyEntitiesExist(data, ev)) return false; // Tile map ReadTileMapSection(data); // Alloc entities var toDelete = AllocEntities(data, ev); // Load the prototype data onto entities, e.g. transform parents, etc. LoadEntities(data); // Assign MapSaveTileMapComponent to all read grids. SaveGridTileMap(data); // Build the scene graph / transform hierarchy to know the order to startup entities. // This also allows us to swap out the root node up front if necessary. BuildEntityHierarchy(data); // From hierarchy work out root node; if we're loading onto an existing map then see if we need to swap out // the root from the yml. SwapRootNode(data); ReadGrids(data); // grids prior to engine v175 might've been serialized with empty chunks which now throw debug asserts. RemoveEmptyChunks(data); // Then, go hierarchically in order and do the entity things. StartupEntities(data); // At the very end, delete entities belonging to removed prototypes. This is being done after startup just in // case these entities have any children that somehow rely on startup in order to properly shut down. // This is pretty cursed and might cause unexpected errors. foreach (var uid in toDelete) { Del(uid); data.Entities.Remove(uid); } return true; } private void RemoveEmptyChunks(MapData data) { var gridQuery = _serverEntityManager.GetEntityQuery(); foreach (var uid in data.EntitiesToDeserialize.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: {ToPrettyString(uid)}. Chunk index: {index}"); gridComp.Chunks.Remove(index); } } } private bool VerifyEntitiesExist(MapData data, BeforeEntityReadEvent ev) { _stopwatch.Restart(); var fail = false; var reportedError = new HashSet(); var key = data.Version >= 4 ? "proto" : "type"; var entities = data.RootMappingNode.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 (ev.RenamedPrototypes.TryGetValue(type, out var newType)) type = newType; if (_prototypeManager.HasIndex(type)) continue; if (!reportedError.Add(type)) continue; if (ev.DeletedPrototypes.Contains(type)) { _logLoader.Warning("Map contains an obsolete/removed prototype: {0}. This may cause unexpected errors.", type); continue; } _logLoader.Error("Missing prototype for map: {0}", type); fail = true; reportedError.Add(type); } _logLoader.Debug($"Verified entities in {_stopwatch.Elapsed}"); if (fail) { _logLoader.Error("Found missing prototypes in map file. Missing prototypes have been dumped to logs."); return false; } return true; } private bool ReadMetaSection(MapData data) { var meta = data.RootMappingNode.Get("meta"); var ver = meta.Get("format").AsInt(); if (ver < BackwardsVersion) { _logLoader.Error($"Cannot handle this map file version, found {ver} and require {MapFormatVersion}"); return false; } data.Version = ver; if (meta.TryGet("postmapinit", out var mapInitNode)) { data.MapIsPostInit = mapInitNode.AsBool(); } else { data.MapIsPostInit = true; } return true; } private void ReadTileMapSection(MapData data) { _stopwatch.Restart(); // Load tile mapping so that we can map the stored tile IDs into the ones actually used at runtime. var tileMap = data.RootMappingNode.Get("tilemap"); _context.TileMap = new Dictionary(tileMap.Count); foreach (var (key, value) in tileMap.Children) { var tileId = ((ValueDataNode)key).AsInt(); var tileDefName = ((ValueDataNode)value).Value; _context.TileMap.Add(tileId, tileDefName); } _logLoader.Debug($"Read tilemap in {_stopwatch.Elapsed}"); } private HashSet AllocEntities(MapData data, BeforeEntityReadEvent ev) { _stopwatch.Restart(); var mapUid = _mapManager.GetMapEntityId(data.TargetMap); var pauseTime = mapUid.IsValid() ? _meta.GetPauseTime(mapUid) : TimeSpan.Zero; _context.Set(data.UidEntityMap, new Dictionary(), data.MapIsPostInit, pauseTime, null); HashSet deletedPrototypeUids = new(); if (data.Version >= 4) { var metaEntities = data.RootMappingNode.Get("entities"); foreach (var metaDef in metaEntities.Cast()) { string? type = null; var deletedPrototype = false; if (metaDef.TryGet("proto", out var typeNode) && !string.IsNullOrWhiteSpace(typeNode.Value)) { if (ev.DeletedPrototypes.Contains(typeNode.Value)) deletedPrototype = true; else if (ev.RenamedPrototypes.TryGetValue(typeNode.Value, out var newType)) type = newType; else type = typeNode.Value; } var entities = (SequenceDataNode) metaDef["entities"]; EntityPrototype? proto = null; if (type != null) _prototypeManager.TryIndex(type, out proto); foreach (var entityDef in entities.Cast()) { var uid = entityDef.Get("uid").AsInt(); var entity = _serverEntityManager.AllocEntity(proto); data.Entities.Add(entity); data.UidEntityMap.Add(uid, entity); data.EntitiesToDeserialize.Add(entity, entityDef); if (deletedPrototype) { deletedPrototypeUids.Add(entity); } else if (data.Options.StoreMapUids) { var comp = _serverEntityManager.AddComponent(entity); comp.Uid = uid; } } } } else { var entities = data.RootMappingNode.Get("entities"); foreach (var entityDef in entities.Cast()) { EntityUid entity; if (entityDef.TryGet("type", out var typeNode)) { if (ev.DeletedPrototypes.Contains(typeNode.Value)) { entity = _serverEntityManager.AllocEntity(null); deletedPrototypeUids.Add(entity); } else if (ev.RenamedPrototypes.TryGetValue(typeNode.Value, out var newType)) { _prototypeManager.TryIndex(newType, out var prototype); entity = _serverEntityManager.AllocEntity(prototype); } else { _prototypeManager.TryIndex(typeNode.Value, out var prototype); entity = _serverEntityManager.AllocEntity(prototype); } } else { entity = _serverEntityManager.AllocEntity(null); } var uid = entityDef.Get("uid").AsInt(); data.Entities.Add(entity); data.UidEntityMap.Add(uid, entity); data.EntitiesToDeserialize.Add(entity, entityDef); if (data.Options.StoreMapUids) { var comp = _serverEntityManager.AddComponent(entity); comp.Uid = uid; } } } _logLoader.Debug($"Allocated {data.Entities.Count} entities in {_stopwatch.Elapsed}"); return deletedPrototypeUids; } private void LoadEntities(MapData mapData) { _stopwatch.Restart(); var metaQuery = GetEntityQuery(); foreach (var (entity, data) in mapData.EntitiesToDeserialize) { LoadEntity(entity, data, metaQuery.GetComponent(entity)); } _logLoader.Debug($"Loaded {mapData.Entities.Count} entities in {_stopwatch.Elapsed}"); } private void LoadEntity(EntityUid uid, MappingDataNode data, MetaDataComponent meta) { _context.CurrentReadingEntityComponents.Clear(); _context.CurrentlyIgnoredComponents.Clear(); if (data.TryGet("components", out SequenceDataNode? componentList)) { var prototype = meta.EntityPrototype; _context.CurrentReadingEntityComponents.EnsureCapacity(componentList.Count); foreach (var compData in componentList.Cast()) { var datanode = compData.Copy(); datanode.Remove("type"); var value = ((ValueDataNode)compData["type"]).Value; if (!_factory.TryGetRegistration(value, out var reg)) { if (!_factory.IsIgnored(value)) _logLoader.Error($"Encountered unregistered component ({value}) while loading entity {ToPrettyString(uid)}"); continue; } var compType = reg.Type; if (prototype?.Components != null && prototype.Components.TryGetValue(value, out var protData)) { datanode = _serManager.PushCompositionWithGenericNode( compType, new[] { protData.Mapping }, datanode, _context); } _context.CurrentComponent = value; _context.CurrentReadingEntityComponents[value] = (IComponent) _serManager.Read(compType, datanode, _context)!; _context.CurrentComponent = null; } } if (data.TryGet("missingComponents", out SequenceDataNode? missingComponentList)) _context.CurrentlyIgnoredComponents = missingComponentList.Cast().Select(x => x.Value).ToHashSet(); _serverEntityManager.FinishEntityLoad(uid, meta.EntityPrototype, _context); if (_context.CurrentlyIgnoredComponents.Count > 0) meta.LastComponentRemoved = _timing.CurTick; } private void SaveGridTileMap(MapData mapData) { DebugTools.Assert(_context.TileMap != null); foreach (var entity in mapData.EntitiesToDeserialize.Keys) { if (HasComp(entity)) { EnsureComp(entity).TileMap = _context.TileMap; } } } private void BuildEntityHierarchy(MapData mapData) { _stopwatch.Restart(); var hierarchy = mapData.Hierarchy; var xformQuery = GetEntityQuery(); foreach (var ent in mapData.Entities) { if (xformQuery.TryGetComponent(ent, out var xform)) { hierarchy[ent] = xform.ParentUid; } else { hierarchy[ent] = EntityUid.Invalid; } } // mapData.Entities = new List(mapData.Entities.Count); var added = new HashSet(mapData.Entities.Count); mapData.Entities.Clear(); while (hierarchy.Count > 0) { var enumerator = hierarchy.GetEnumerator(); enumerator.MoveNext(); var (current, parent) = enumerator.Current; BuildTopology(hierarchy, added, mapData.Entities, current, parent); enumerator.Dispose(); } _logLoader.Debug($"Built entity hierarchy for {mapData.Entities.Count} entities in {_stopwatch.Elapsed}"); } private void BuildTopology(Dictionary hierarchy, HashSet added, List Entities, EntityUid current, EntityUid parent) { // If we've already added it then skip. if (!added.Add(current)) return; // Ensure parent is done first. if (hierarchy.TryGetValue(parent, out var parentValue)) { BuildTopology(hierarchy, added, Entities, parent, parentValue); } DebugTools.Assert(current.IsValid()); // DebugTools.Assert(!Entities.Contains(current)); Entities.Add(current); hierarchy.Remove(current); } private void SwapRootNode(MapData data) { _stopwatch.Restart(); // There's 4 scenarios // 1. We're loading a map file onto an existing map. Dump the map file's map and use the existing one // 2. We're loading a map file onto an existing map. Use the map file's map and swap entities to it. // 3. We're loading a map file onto a new map. Use CreateMap (for now) and swap out the uid to the correct one // 4. We're loading a non-map file; in this case it depends whether the map exists or not, then proceed with the above. var rootNode = data.Entities[0]; var xformQuery = GetEntityQuery(); // We just need to cache the old mapuid and point to the new mapuid. if (TryComp(rootNode, out MapComponent? mapComp)) { // If map exists swap out if (_mapSystem.TryGetMap(data.TargetMap, out var existing)) { data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap); data.MapIsPaused = _mapSystem.IsPaused(existing.Value); // Map exists but we also have a map file with stuff on it soooo swap out the old map. if (data.Options.LoadMap) { _logLoader.Info($"Loading map file with a root node onto an existing map!"); // Smelly if (HasComp(rootNode)) { data.Options.Offset = Vector2.Zero; data.Options.Rotation = Angle.Zero; } Del(existing); EnsureComp(rootNode); mapComp.MapId = data.TargetMap; DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing); } // Otherwise just ignore the map in the file. else { var oldRootUid = data.Entities[0]; data.Entities[0] = existing.Value; foreach (var ent in data.Entities) { if (ent == existing) continue; var xform = xformQuery.GetComponent(ent); if (!xform.ParentUid.IsValid() || xform.ParentUid.Equals(oldRootUid)) { _transform.SetParent(ent, xform, existing.Value); } } Del(oldRootUid); } } else { data.MapIsPaused = !data.MapIsPostInit; mapComp.MapId = data.TargetMap; DebugTools.Assert(mapComp.LifeStage < ComponentLifeStage.Initializing); EnsureComp(rootNode); // Nothing should have invalid uid except for the root node. } } else { // No map file root, in that case create a new map / get the one we're loading onto. if (!_mapSystem.TryGetMap(data.TargetMap, out var mapNode)) { // Map doesn't exist so we'll start it up now so we can re-attach the preinit entities to it for later. mapNode = _mapSystem.CreateMap(data.TargetMap, false); } data.Options.DoMapInit |= _mapSystem.IsInitialized(data.TargetMap); data.MapIsPaused = _mapSystem.IsPaused(mapNode.Value); // If anything has an invalid parent (e.g. it's some form of root node) then parent it to the map. foreach (var ent in data.Entities) { // If it's the map itself don't reparent. if (ent.Equals(mapNode)) continue; var xform = xformQuery.GetComponent(ent); if (!xform.ParentUid.IsValid()) { _transform.SetParent(ent, xform, mapNode.Value); } } } _logLoader.Debug($"Swapped out root node in {_stopwatch.Elapsed}"); } private void ReadGrids(MapData data) { // TODO: Kill this when we get map format v3 and remove grid-specific yml area. // MapGrids already contain their assigned GridId from their ctor, and the MapComponents just got deserialized. // Now we need to actually bind the MapGrids to their components so that you can resolve GridId -> EntityUid // After doing this, it should be 100% safe to use the MapManager API like normal. if (data.Version != BackwardsVersion) return; var yamlGrids = data.RootMappingNode.Get("grids"); // There were no new grids, nothing to do here. if (yamlGrids.Count == 0) return; // get ents that the grids will bind to var gridComps = new Entity[yamlGrids.Count]; var gridQuery = _serverEntityManager.GetEntityQuery(); // linear search for new grid comps foreach (var uid in data.EntitiesToDeserialize.Keys) { if (!gridQuery.TryGetComponent(uid, out var gridComp)) continue; // These should actually be new, pre-init DebugTools.Assert(gridComp.LifeStage == ComponentLifeStage.Added); gridComps[gridComp.GridIndex] = new Entity(uid, gridComp); } for (var index = 0; index < yamlGrids.Count; index++) { // Here is where the implicit index pairing magic happens from the yaml. var yamlGrid = (MappingDataNode)yamlGrids[index]; // designed to throw if something is broken, every grid must map to an ent var gridComp = gridComps[index]; // TODO Once maps have been updated (save+load), remove GridComponent.GridIndex altogether and replace it with: // var savedUid = ((ValueDataNode)yamlGrid["uid"]).Value; // var gridUid = UidEntityMap[int.Parse(savedUid)]; // var gridComp = gridQuery.GetComponent(gridUid); MappingDataNode yamlGridInfo = (MappingDataNode)yamlGrid["settings"]; SequenceDataNode yamlGridChunks = (SequenceDataNode)yamlGrid["chunks"]; AllocateMapGrid(gridComp, yamlGridInfo); var gridUid = gridComp.Owner; foreach (var chunkNode in yamlGridChunks.Cast()) { var (chunkOffsetX, chunkOffsetY) = _serManager.Read(chunkNode["ind"]); _serManager.Read(chunkNode, _context, instanceProvider: () => _mapSystem.GetOrAddChunk(gridUid, gridComp, chunkOffsetX, chunkOffsetY), notNullableOverride: true); } } } private static void AllocateMapGrid(MapGridComponent gridComp, MappingDataNode yamlGridInfo) { // sane defaults ushort csz = 16; ushort tsz = 1; foreach (var kvInfo in yamlGridInfo) { var key = ((ValueDataNode)kvInfo.Key).Value; var val = ((ValueDataNode)kvInfo.Value).Value; if (key == "chunksize") csz = ushort.Parse(val); else if (key == "tilesize") tsz = ushort.Parse(val); else if (key == "snapsize") continue; // obsolete } gridComp.ChunkSize = csz; gridComp.TileSize = tsz; } private void StartupEntities(MapData data) { _stopwatch.Restart(); var metaQuery = GetEntityQuery(); var rootEntity = data.Entities[0]; var mapQuery = GetEntityQuery(); var xformQuery = GetEntityQuery(); // If the root node is a map that's already existing don't bother with it. // If we're loading a grid then the map is already started up elsewhere in which case this // just loads the grid outside of the loop which is also fine. if (MetaData(rootEntity).EntityLifeStage < EntityLifeStage.Initialized) { StartupEntity(rootEntity, metaQuery.GetComponent(rootEntity), data); if (xformQuery.TryGetComponent(rootEntity, out var xform) && IsRoot(xform, mapQuery) && !HasComp(rootEntity)) { _transform.SetLocalPosition(xform, Vector2.Transform(xform.LocalPosition, data.Options.TransformMatrix)); xform.LocalRotation += data.Options.Rotation; } } for (var i = 1; i < data.Entities.Count; i++) { var entity = data.Entities[i]; if (xformQuery.TryGetComponent(entity, out var xform) && IsRoot(xform, mapQuery)) { // Don't want to trigger events xform._localPosition = Vector2.Transform(xform.LocalPosition, data.Options.TransformMatrix); if (!xform.NoLocalRotation) xform._localRotation += data.Options.Rotation; DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); } StartupEntity(entity, metaQuery.GetComponent(entity), data); } _logLoader.Debug($"Started up {data.Entities.Count} entities in {_stopwatch.Elapsed}"); } private bool IsRoot(TransformComponent xform, EntityQuery mapQuery) { return !xform.ParentUid.IsValid() || mapQuery.HasComponent(xform.ParentUid); } private void StartupEntity(EntityUid uid, MetaDataComponent metadata, MapData data) { ResetNetTicks(uid, metadata, data.EntitiesToDeserialize[uid]); var isPaused = data is { MapIsPaused: true, MapIsPostInit: false }; _meta.SetEntityPaused(uid, isPaused, metadata); // TODO: Apply map transforms if root node. _serverEntityManager.FinishEntityInitialization(uid, metadata); _serverEntityManager.FinishEntityStartup(uid); if (data.MapIsPostInit) { EntityManager.SetLifeStage(metadata, EntityLifeStage.MapInitialized); } else if (data.Options.DoMapInit) { _serverEntityManager.RunMapInit(uid, metadata); } } private void ResetNetTicks(EntityUid entity, MetaDataComponent metadata, MappingDataNode data) { if (!data.TryGet("components", out SequenceDataNode? componentList)) { return; } if (metadata.EntityPrototype is not {} prototype) { return; } foreach (var component in metadata.NetComponents.Values) { var compName = _factory.GetComponentName(component.GetType()); if (componentList.Cast().Any(p => ((ValueDataNode)p["type"]).Value == compName)) { 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(); } continue; } // 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(); } } #endregion #region Saving public MappingDataNode GetSaveData(EntityUid uid) { var ev = new BeforeSaveEvent(uid, Transform(uid).MapUid); RaiseLocalEvent(ev); var data = new MappingDataNode(); WriteMetaSection(data, uid); var entityUidMap = new Dictionary(); var uidEntityMap = new Dictionary(); var entities = new List(); _stopwatch.Restart(); PopulateEntityList(uid, entities, uidEntityMap, entityUidMap); WriteTileMapSection(data, entities); _logLoader.Debug($"Populated entity list in {_stopwatch.Elapsed}"); var metadata = Comp(uid); var pauseTime = _meta.GetPauseTime(uid, metadata); // TODO replace MapPreInit with the map's entity lifestage // Yes, post-init maps do not have EntityLifeStage >= EntityLifeStage.MapInitialized bool postInit; if (TryComp(uid, out MapComponent? mapComp)) postInit = mapComp.MapInitialized; else postInit = metadata.EntityLifeStage >= EntityLifeStage.MapInitialized; var rootXform = _serverEntityManager.GetComponent(uid); _context.Set(uidEntityMap, entityUidMap, postInit, pauseTime, rootXform.ParentUid); _stopwatch.Restart(); WriteEntitySection(data, uidEntityMap, entityUidMap); _logLoader.Debug($"Wrote entity section for {entities.Count} entities in {_stopwatch.Elapsed}"); _context.Clear(); return data; } private void WriteMetaSection(MappingDataNode rootNode, EntityUid uid) { var meta = new MappingDataNode(); rootNode.Add("meta", meta); meta.Add("format", MapFormatVersion.ToString(CultureInfo.InvariantCulture)); var xform = Transform(uid); var isPostInit = _mapManager.IsMapInitialized(xform.MapID); meta.Add("postmapinit", isPostInit ? "true" : "false"); } private void WriteTileMapSection(MappingDataNode rootNode, List entities) { // Although we could use tiledefmanager it might write tiledata we don't need so we'll compress it var gridQuery = GetEntityQuery(); var tileDefs = new HashSet(); Dictionary? origTileMap = null; foreach (var ent in entities) { if (!gridQuery.TryGetComponent(ent, out var grid)) continue; var tileEnumerator = _mapSystem.GetAllTilesEnumerator(ent, grid, ignoreEmpty: false); while (tileEnumerator.MoveNext(out var tileRef)) { tileDefs.Add(tileRef.Value.Tile.TypeId); } if (TryComp(ent, out MapSaveTileMapComponent? saveTileMap)) origTileMap ??= saveTileMap.TileMap; } Dictionary tileIdMap; if (origTileMap != null) { tileIdMap = new Dictionary(); // We are re-saving a map, so we have an original tile map we can preserve. foreach (var (origId, prototypeId) in origTileMap) { // Skip removed tile definitions. if (!_tileDefManager.TryGetDefinition(prototypeId, out var definition)) continue; tileIdMap.Add(definition.TileId, origId); } // Assign new IDs for all new tile types. var nextId = 0; foreach (var tileId in tileDefs) { if (tileIdMap.ContainsKey(tileId)) continue; // New tile, assign new ID that isn't taken by original tile map. while (origTileMap.ContainsKey(nextId)) { nextId += 1; } tileIdMap.Add(tileId, nextId); nextId += 1; } } else { // Make no-op tile ID map. tileIdMap = tileDefs.ToDictionary(x => x, x => x); } DebugTools.Assert( tileIdMap.Count == tileIdMap.Values.Distinct().Count(), "Tile ID map has double mapped values??"); _context.TileWriteMap = tileIdMap; var tileMap = new MappingDataNode(); rootNode.Add("tilemap", tileMap); foreach (var (nativeId, mapId) in tileIdMap.OrderBy(x => x.Key)) { tileMap.Add( mapId.ToString(CultureInfo.InvariantCulture), _tileDefManager[nativeId].ID); } } private void PopulateEntityList(EntityUid uid, List entities, Dictionary uidEntityMap, Dictionary entityUidMap) { var withoutUid = new HashSet(); var saveCompQuery = GetEntityQuery(); var transformCompQuery = GetEntityQuery(); var metaCompQuery = GetEntityQuery(); RecursivePopulate(uid, entities, uidEntityMap, withoutUid, metaCompQuery, transformCompQuery, saveCompQuery); var uidCounter = 1; foreach (var entity in withoutUid) { while (uidEntityMap.ContainsKey(uidCounter)) { // Find next available UID. uidCounter += 1; } uidEntityMap.Add(uidCounter, entity); uidCounter += 1; } // Build a reverse lookup entityUidMap.EnsureCapacity(uidEntityMap.Count); foreach(var (saveId, mapId) in uidEntityMap) { entityUidMap.Add(mapId, saveId); } } private bool IsSaveable(EntityUid uid) { // Don't serialize things parented to un savable things. // For example clothes inside a person. while (uid.IsValid()) { var meta = MetaData(uid); if (meta.EntityDeleted || meta.EntityPrototype?.MapSavable == false) break; uid = Transform(uid).ParentUid; } // If we manage to get up to the map (root node) then it's saveable. return !uid.IsValid(); } private void RecursivePopulate(EntityUid uid, List entities, Dictionary uidEntityMap, HashSet withoutUid, EntityQuery metaQuery, EntityQuery transformQuery, EntityQuery saveCompQuery) { if (!IsSaveable(uid)) return; entities.Add(uid); // TODO: Given there's some structure to this now we can probably omit the parent / child a bit. if (!saveCompQuery.TryGetComponent(uid, out var mapSaveComp) || mapSaveComp.Uid == 0 || !uidEntityMap.TryAdd(mapSaveComp.Uid, uid)) { // If the id was already saved before, or has no save component we need to find a new id for this entity withoutUid.Add(uid); } var xform = transformQuery.GetComponent(uid); foreach (var child in xform._children) { RecursivePopulate(child, entities, uidEntityMap, withoutUid, metaQuery, transformQuery, saveCompQuery); } } private void WriteEntitySection(MappingDataNode rootNode, Dictionary uidEntityMap, Dictionary entityUidMap) { var metaQuery = GetEntityQuery(); var metaName = _factory.GetComponentName(typeof(MetaDataComponent)); var xformName = _factory.GetComponentName(typeof(TransformComponent)); // As metadata isn't on components we'll special-case it. var prototypeCompCache = new Dictionary>(); var emptyMetaNode = _serManager.WriteValueAs(typeof(MetaDataComponent), new MetaDataComponent(), alwaysWrite: true, context: _context); _context.CurrentComponent = _factory.GetComponentName(typeof(TransformComponent)); var emptyXformNode = _serManager.WriteValueAs(typeof(TransformComponent), new TransformComponent(), alwaysWrite: true, context: _context); _context.CurrentComponent = null; var prototypes = new Dictionary>(); foreach (var (entityUid, saveId) in entityUidMap) { var meta = metaQuery.GetComponent(entityUid); if (!_context.MapInitialized && meta.EntityLifeStage >= EntityLifeStage.MapInitialized) _logWriter.Error($"Encountered a post-init entity in a pre-init map. Entity: {ToPrettyString(entityUid)}"); var id = meta.EntityPrototype?.ID; id ??= string.Empty; var uids = prototypes.GetOrNew(id); uids.Add(saveId); } var protos = prototypes.Keys.ToList(); protos.Sort(); var entityPrototypes = new SequenceDataNode(); rootNode.Add("entities", entityPrototypes); foreach (var proto in protos) { var saveIds = prototypes[proto]; saveIds.Sort(); var entities = new SequenceDataNode(); var node = new MappingDataNode() { { "proto", proto }, { "entities", entities}, }; entityPrototypes.Add(node); foreach (var saveId in saveIds) { var entityUid = uidEntityMap[saveId]; _context.CurrentWritingEntity = entityUid; var mapping = new MappingDataNode { {"uid", saveId.ToString(CultureInfo.InvariantCulture)} }; var md = metaQuery.GetComponent(entityUid); Dictionary? cache = null; if (md.EntityPrototype is {} prototype) { if (!prototypeCompCache.TryGetValue(prototype.ID, out cache)) { prototypeCompCache[prototype.ID] = cache = new Dictionary(prototype.Components.Count); _context.WritingReadingPrototypes = true; foreach (var (compType, comp) in prototype.Components) { _context.CurrentComponent = compType; cache.Add(compType, _serManager.WriteValueAs(comp.Component.GetType(), comp.Component, alwaysWrite: true, context: _context)); } _context.CurrentComponent = null; _context.WritingReadingPrototypes = false; cache.TryAdd(metaName, emptyMetaNode); cache.TryAdd(xformName, emptyXformNode); } } var components = new SequenceDataNode(); var xform = Transform(entityUid); if (xform.NoLocalRotation && xform.LocalRotation != 0) { Log.Error($"Encountered a no-rotation entity with non-zero local rotation: {ToPrettyString(entityUid)}"); xform._localRotation = 0; } foreach (var component in EntityManager.GetComponents(entityUid)) { var compType = component.GetType(); var registration = _factory.GetRegistration(compType); if (registration.Unsaved) continue; var compName = registration.Name; _context.CurrentComponent = compName; MappingDataNode? compMapping; MappingDataNode? protMapping = null; if (cache != null && cache.TryGetValue(compName, out protMapping)) { // 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 = _serManager.WriteValueAs(compType, component, alwaysWrite: true, context: _context); // 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(protMapping); if(compMapping == null) continue; } else { compMapping = _serManager.WriteValueAs(compType, component, alwaysWrite: false, context: _context); } // 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 || protMapping == null) { compMapping.InsertAt(0, "type", new ValueDataNode(compName)); // Something actually got written! components.Add(compMapping); } } if (components.Count != 0) { mapping.Add("components", components); } if (md.EntityPrototype == null) { // No prototype - we are done. entities.Add(mapping); continue; } // an entity may have less components than the original prototype, so we need to check if any are missing. var missingComponents = new SequenceDataNode(); foreach (var (name, comp) in md.EntityPrototype.Components) { // try comp instead of has-comp as it checks whether the component is supposed to have been // deleted. if (_serverEntityManager.TryGetComponent(entityUid, comp.Component.GetType(), out _)) continue; missingComponents.Add(new ValueDataNode(name)); } if (missingComponents.Count != 0) { mapping.Add("missingComponents", missingComponents); } entities.Add(mapping); } } } #endregion /// /// 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? /// private sealed class MapData { public MappingDataNode RootMappingNode { get; } public readonly MapId TargetMap; public bool MapIsPostInit; public bool MapIsPaused; public readonly MapLoadOptions Options; public int Version; // Loading data public readonly List Entities = new(); public readonly Dictionary UidEntityMap = new(); public readonly Dictionary EntitiesToDeserialize = new(); public readonly Dictionary Hierarchy = new(); public MapData(MapId mapId, TextReader reader, MapLoadOptions options) { var documents = DataNodeParser.ParseYamlStream(reader).ToArray(); if (documents.Length < 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 (documents.Length > 1) { throw new InvalidDataException("Stream too many YAML documents. Map files store exactly one."); } RootMappingNode = (MappingDataNode) documents[0].Root!; Options = options; TargetMap = mapId; } } }