using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; public abstract partial class SharedMapSystem { protected int LastMapId; private Dictionary _reserved = new(); private void InitializeMap() { SubscribeLocalEvent(OnComponentAdd); SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnCompStartup); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnMapRemoved); SubscribeLocalEvent(OnMapHandleState); SubscribeLocalEvent(OnMapGetState); } public bool MapExists([NotNullWhen(true)] MapId? mapId) { return mapId != null && Maps.ContainsKey(mapId.Value); } public EntityUid GetMap(MapId mapId) { return Maps[mapId]; } /// /// Get the entity UID for a map, or if the map doesn't exist. /// /// The ID of the map to look up. /// /// The entity UID of the map entity with the specific map ID, /// or if the map doesn't exist. /// /// public EntityUid GetMapOrInvalid(MapId? mapId) { if (TryGetMap(mapId, out var uid)) return uid.Value; return EntityUid.Invalid; } public bool TryGetMap([NotNullWhen(true)] MapId? mapId, [NotNullWhen(true)] out EntityUid? uid) { if (mapId == null || !Maps.TryGetValue(mapId.Value, out var map)) { uid = null; return false; } uid = map; return true; } private void OnMapHandleState(EntityUid uid, MapComponent component, ref ComponentHandleState args) { if (args.Current is not MapComponentState state) return; if (component.MapId == MapId.Nullspace) { if (state.MapId == MapId.Nullspace) throw new Exception($"Received invalid map state for {ToPrettyString(uid)}"); AssignMapId((uid, component), state.MapId); RecursiveMapIdUpdate(uid, uid, component.MapId); } if (component.MapId != state.MapId) throw new Exception($"Received invalid map state for {ToPrettyString(uid)}"); component.LightingEnabled = state.LightingEnabled; component.MapInitialized = state.Initialized; if (LifeStage(uid) >= EntityLifeStage.Initialized) SetPaused(uid, state.MapPaused); else component.MapPaused = state.MapPaused; } private void RecursiveMapIdUpdate(EntityUid uid, EntityUid mapUid, MapId mapId) { // This is required only in the event where an entity becomes a map AFTER children have already been attached to it. // AFAIK, this currently only happens when the client applies entity states out of order (i.e., ignoring transform hierarchy), // which itself only happens if PVS is disabled. // TODO MAPS remove this var xform = Transform(uid); xform.MapUid = mapUid; xform.MapID = mapId; xform._mapIdInitialized = true; foreach (var child in xform._children) { RecursiveMapIdUpdate(child, mapUid, mapId); } } private void OnMapGetState(EntityUid uid, MapComponent component, ref ComponentGetState args) { args.State = new MapComponentState(component.MapId, component.LightingEnabled, component.MapPaused, component.MapInitialized); } protected MapId TakeNextMapId() { var id = GetNextMapId(); LastMapId = id.Value; return id; } [Pure] internal abstract MapId GetNextMapId(); private void OnComponentAdd(EntityUid uid, MapComponent component, ComponentAdd args) { // ordered startups when EnsureComp(uid); } /// /// Generate & reserve a map-id for a map-entity before it is actually given the component. /// internal MapId AllocateMapId(EntityUid ent) { var id = _reserved[ent] = TakeNextMapId(); Maps.Add(id, ent); UsedIds.Add(id); return id; } internal void AssignMapId(Entity map, MapId? id = null) { if (map.Comp.MapId != MapId.Nullspace) { if (id != null && map.Comp.MapId != id) { QueueDel(map.Owner); throw new Exception($"Map entity {ToPrettyString(map.Owner)} has already been assigned an id"); } if (!Maps.TryGetValue(map.Comp.MapId, out var existing) || existing != map.Owner) { QueueDel(map.Owner); throw new Exception($"Map entity {ToPrettyString(map.Owner)} was improperly assigned a map id?"); } DebugTools.Assert(UsedIds.Contains(map.Comp.MapId)); return; } if (_reserved.TryGetValue(map.Owner, out var reserved)) { DebugTools.AssertNull(id); DebugTools.AssertEqual(Maps[reserved], map.Owner); DebugTools.Assert(UsedIds.Contains(reserved)); map.Comp.MapId = reserved; return; } map.Comp.MapId = id ?? TakeNextMapId(); if (IsClientSide(map) != map.Comp.MapId.IsClientSide) throw new Exception($"Attempting to assign a client-side map id to a networked entity or vice-versa"); if (!UsedIds.Add(map.Comp.MapId)) Log.Warning($"Re-using a previously used map id ({map.Comp.MapId}) for map entity {ToPrettyString(map)}"); if (Maps.TryAdd(map.Comp.MapId, map.Owner)) return; if (Maps[map.Comp.MapId] == map.Owner) return; QueueDel(map); throw new Exception( $"Attempted to assign an existing mapId {map.Comp} to a map entity {ToPrettyString(map.Owner)}"); } private void OnCompInit(Entity map, ref ComponentInit args) { AssignMapId(map); #pragma warning disable CS0618 // Type or member is obsolete var msg = new MapChangedEvent(map, map.Comp.MapId, true); #pragma warning restore CS0618 // Type or member is obsolete RaiseLocalEvent(map, msg, true); var ev = new MapCreatedEvent(map, map.Comp.MapId); RaiseLocalEvent(map, ev, true); } private void OnMapInit(EntityUid uid, MapComponent component, MapInitEvent args) { DebugTools.Assert(!component.MapInitialized); component.MapInitialized = true; Dirty(uid, component); } private void OnCompStartup(EntityUid uid, MapComponent component, ComponentStartup args) { // un-initialized maps are always paused. component.MapPaused |= !component.MapInitialized; if (!component.MapPaused) return; // Recursively pause all entities on the map component.MapPaused = false; SetPaused(uid, true); } private void OnMapRemoved(EntityUid uid, MapComponent component, ComponentShutdown args) { DebugTools.Assert(component.MapId != MapId.Nullspace); Maps.Remove(component.MapId); #pragma warning disable CS0618 // Type or member is obsolete var msg = new MapChangedEvent(uid, component.MapId, false); #pragma warning restore CS0618 // Type or member is obsolete RaiseLocalEvent(uid, msg, true); var ev = new MapRemovedEvent(uid, component.MapId); RaiseLocalEvent(uid, ev, true); } /// /// Creates a new map, automatically assigning a map id. /// public EntityUid CreateMap(out MapId mapId, bool runMapInit = true) { mapId = TakeNextMapId(); var uid = CreateMap(mapId, runMapInit); return uid; } /// public EntityUid CreateMap(bool runMapInit = true) => CreateMap(out _, runMapInit); /// /// Creates a new map with the specified map id. /// /// Throws if an invalid or already existing map id is provided. public EntityUid CreateMap(MapId mapId, bool runMapInit = true) { if (Maps.ContainsKey(mapId)) throw new ArgumentException($"Map with id {mapId} already exists"); if (mapId == MapId.Nullspace) throw new ArgumentException($"Cannot create a null-space map"); if (_netManager.IsServer && mapId.IsClientSide) throw new ArgumentException($"Attempted to create a client-side map on the server?"); if (_netManager.IsClient && _netManager.IsConnected && !mapId.IsClientSide) throw new ArgumentException($"Attempted to create a client-side map entity with a non client-side map ID?"); if (UsedIds.Contains(mapId)) Log.Warning($"Re-using MapId: {mapId}"); var (uid, map, meta) = CreateUninitializedMap(); DebugTools.AssertEqual(map.MapId, MapId.Nullspace); AssignMapId((uid, map), mapId); // Initialize components. this should add the map id to the collections. EntityManager.InitializeEntity(uid, meta); EntityManager.StartEntity(uid); DebugTools.AssertEqual(Maps[mapId], uid); if (runMapInit) InitializeMap((uid, map)); else SetPaused((uid, map), true); return uid; } public Entity CreateUninitializedMap() { var uid = EntityManager.CreateEntityUninitialized(null, out var meta); _meta.SetEntityName(uid, $"Map Entity", meta); return (uid, AddComp(uid), meta); } /// /// Deletes a map with the specified map id. /// public void DeleteMap(MapId mapId) { if (TryGetMap(mapId, out var uid)) Del(uid); } /// /// Deletes a map with the specified map id in the next tick. /// public void QueueDeleteMap(MapId mapId) { if (TryGetMap(mapId, out var uid)) QueueDel(uid); } public Dictionary.KeyCollection GetAllMapIds() { return Maps.Keys; } }