using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma { /// /// Thread-safe wrapper for MapEntity list operations. /// Uses copy-on-write pattern for lock-free reads. /// internal class ThreadSafeMapEntityList : IEnumerable { private volatile List _list = new List(); private readonly object _writeLock = new object(); public int Count => _list.Count; public void Add(MapEntity entity) { lock (_writeLock) { var newList = new List(_list) { entity }; Interlocked.Exchange(ref _list, newList); } } public void Insert(int index, MapEntity entity) { lock (_writeLock) { var newList = new List(_list); newList.Insert(index, entity); Interlocked.Exchange(ref _list, newList); } } /// /// Atomically inserts an entity at a position determined by the insertAction. /// The insertAction is executed within the lock to ensure thread-safety. /// public void InsertWithAction(MapEntity entity, Action, MapEntity> insertAction) { lock (_writeLock) { var newList = new List(_list); insertAction(newList, entity); Interlocked.Exchange(ref _list, newList); } } public bool Remove(MapEntity entity) { lock (_writeLock) { var newList = new List(_list); bool removed = newList.Remove(entity); if (removed) { Interlocked.Exchange(ref _list, newList); } return removed; } } public int RemoveAll(Predicate match) { lock (_writeLock) { var newList = new List(_list); int count = newList.RemoveAll(match); if (count > 0) { Interlocked.Exchange(ref _list, newList); } return count; } } public void Clear() { Interlocked.Exchange(ref _list, new List()); } public bool Contains(MapEntity entity) => _list.Contains(entity); public MapEntity this[int index] => _list[index]; public IEnumerator GetEnumerator() => _list.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); // LINQ-friendly methods that work on a snapshot public List ToList() => new List(_list); public MapEntity FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); public MapEntity Find(Predicate predicate) => _list.Find(predicate); public List FindAll(Predicate predicate) => _list.FindAll(predicate); public IEnumerable Where(Func predicate) => _list.Where(predicate); public bool Any(Func predicate) => _list.Any(predicate); public bool Exists(Predicate predicate) => _list.Exists(predicate); public IOrderedEnumerable OrderBy(Func keySelector) => _list.OrderBy(keySelector); public void ForEach(Action action) => _list.ForEach(action); } abstract partial class MapEntity : Entity, ISpatialEntity { public readonly static ThreadSafeMapEntityList MapEntityList = new ThreadSafeMapEntityList(); public readonly MapEntityPrefab Prefab; protected List linkedToID; public List unresolvedLinkedToID; public static int MapEntityUpdateInterval = 1; public static int PoweredUpdateInterval = 1; private static int mapEntityUpdateTick; /// /// List of upgrades this item has /// protected readonly List Upgrades = new List(); public readonly HashSet DisallowedUpgradeSet = new HashSet(); [Editable, Serialize("", IsPropertySaveable.Yes)] public string DisallowedUpgrades { get { return string.Join(",", DisallowedUpgradeSet); } set { DisallowedUpgradeSet.Clear(); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); foreach (string tag in splitTags) { string[] splitTag = tag.Trim().Split(':'); DisallowedUpgradeSet.Add(string.Join(":", splitTag).ToIdentifier()); } } } } public readonly List linkedTo = new List(); public bool FlippedX { get; protected set; } public bool FlippedY { get; protected set; } public bool ShouldBeSaved = true; //the position and dimensions of the entity protected Rectangle rect; protected static readonly HashSet highlightedEntities = new HashSet(); public static IEnumerable HighlightedEntities => highlightedEntities; private bool externalHighlight = false; public bool ExternalHighlight { get { return externalHighlight; } set { if (value != externalHighlight) { externalHighlight = value; CheckIsHighlighted(); } } } //is the mouse inside the rect private bool isHighlighted; public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } set { if (value != isHighlighted) { isHighlighted = value; CheckIsHighlighted(); } } } public virtual float RotationRad { get; protected set; } /// /// Rotation taking into account flipping: if the entity is flipped on either axis, the rotation is negated /// (but not if it's flipped on both axes, two flips is essentially double negation). /// public float RotationRadWithFlipping => FlippedX ^ FlippedY ? -RotationRad : RotationRad; public float RotationWithFlipping => MathHelper.ToDegrees(RotationRadWithFlipping); public virtual Rectangle Rect { get { return rect; } set { rect = value; } } public Rectangle WorldRect { get { return Submarine == null ? rect : new Rectangle((int)(Submarine.Position.X + rect.X), (int)(Submarine.Position.Y + rect.Y), rect.Width, rect.Height); } } public virtual Sprite Sprite { get { return null; } } public virtual bool DrawBelowWater { get { return Sprite != null && SpriteDepth > 0.5f; } } public virtual bool DrawOverWater { get { return !DrawBelowWater; } } public virtual bool Linkable { get { return false; } } public IEnumerable AllowedLinks => Prefab == null ? Enumerable.Empty() : Prefab.AllowedLinks; public bool ResizeHorizontal { get { return Prefab != null && Prefab.ResizeHorizontal; } } public bool ResizeVertical { get { return Prefab != null && Prefab.ResizeVertical; } } //for upgrading the dimensions of the entity from xml [Serialize(0, IsPropertySaveable.No)] public int RectWidth { get { return rect.Width; } set { if (value <= 0) { return; } Rect = new Rectangle(rect.X, rect.Y, value, rect.Height); } } //for upgrading the dimensions of the entity from xml [Serialize(0, IsPropertySaveable.No)] public int RectHeight { get { return rect.Height; } set { if (value <= 0) { return; } Rect = new Rectangle(rect.X, rect.Y, rect.Width, value); } } // We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used. // Nullable on the other requires boxing that we don't want to do too often, since it generates garbage. public bool SpriteDepthOverrideIsSet { get; private set; } public float SpriteOverrideDepth => SpriteDepth; private float _spriteOverrideDepth = float.NaN; [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, IsPropertySaveable.Yes)] public float SpriteDepth { get { if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; } return Sprite != null ? Sprite.Depth : 0; } set { if (!float.IsNaN(value)) { _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999999f); if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } SpriteDepthOverrideIsSet = true; } } } [Serialize(1f, IsPropertySaveable.Yes), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] public virtual float Scale { get; set; } = 1; [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HiddenInGame { get; set; } /// /// Is the layer this entity is in currently hidden? If it is, the entity is not updated and should do nothing. /// public bool IsLayerHidden { get; set; } /// /// Is the entity hidden due to being enabled or the layer the entity is in being hidden? /// public bool IsHidden => HiddenInGame || IsLayerHidden; public override Vector2 Position { get { Vector2 rectPos = new Vector2( rect.X + rect.Width / 2.0f, rect.Y - rect.Height / 2.0f); //if (MoveWithLevel) rectPos += Level.Loaded.Position; return rectPos; } } public override Vector2 SimPosition { get { return ConvertUnits.ToSimUnits(Position); } } public float SoundRange { get { if (aiTarget == null) return 0.0f; return aiTarget.SoundRange; } set { if (aiTarget == null) return; aiTarget.SoundRange = value; } } public float SightRange { get { if (aiTarget == null) return 0.0f; return aiTarget.SightRange; } set { if (aiTarget == null) return; aiTarget.SightRange = value; } } [Serialize(true, IsPropertySaveable.Yes)] public bool RemoveIfLinkedOutpostDoorInUse { get; protected set; } = true; [Serialize("", IsPropertySaveable.Yes, "Submarine editor layer")] public string Layer { get; set; } /// /// The index of the outpost module this entity originally spawned in (-1 if not an outpost item) /// public int OriginalModuleIndex = -1; public int OriginalContainerIndex = -1; public virtual string Name { get { return ""; } } public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { this.Prefab = prefab; Scale = prefab != null ? prefab.Scale : 1; } protected void ParseLinks(XElement element, IdRemap idRemap) { string linkedToString = element.GetAttributeString("linked", ""); if (!string.IsNullOrEmpty(linkedToString)) { string[] linkedToIds = linkedToString.Split(','); for (int i = 0; i < linkedToIds.Length; i++) { int srcId = int.Parse(linkedToIds[i]); int targetId = idRemap.GetOffsetId(srcId); if (targetId <= 0) { unresolvedLinkedToID ??= new List(); unresolvedLinkedToID.Add((ushort)srcId); continue; } linkedToID.Add((ushort)targetId); } } } public void ResolveLinks(IdRemap childRemap) { if (unresolvedLinkedToID == null) { return; } for (int i = 0; i < unresolvedLinkedToID.Count; i++) { int srcId = unresolvedLinkedToID[i]; int targetId = childRemap.GetOffsetId(srcId); if (targetId > 0) { var otherEntity = FindEntityByID((ushort)targetId) as MapEntity; linkedTo.Add(otherEntity); if (otherEntity.Linkable && otherEntity.linkedTo != null) otherEntity.linkedTo.Add(this); unresolvedLinkedToID.RemoveAt(i); i--; } } } public virtual void Move(Vector2 amount, bool ignoreContacts = true) { rect.X += (int)amount.X; rect.Y += (int)amount.Y; } public virtual bool IsMouseOn(Vector2 position) { return (Submarine.RectContains(WorldRect, position)); } public bool HasUpgrade(Identifier identifier) { return GetUpgrade(identifier) != null; } public Upgrade GetUpgrade(Identifier identifier) { return Upgrades.Find(upgrade => upgrade.Identifier == identifier); } public List GetUpgrades() { return Upgrades; } public void SetUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier); if (existingUpgrade != null) { existingUpgrade.Level = upgrade.Level; existingUpgrade.ApplyUpgrade(); upgrade.Dispose(); } else { AddUpgrade(upgrade, createNetworkEvent); } DebugConsole.Log($"Set (ID: {ID} {Prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); } /// /// Adds a new upgrade to the item /// public virtual bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { if (!upgrade.Prefab.UpgradeCategories.Any(category => category.CanBeApplied(this, upgrade.Prefab))) { return false; } if (DisallowedUpgradeSet.Contains(upgrade.Identifier)) { return false; } Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier); if (existingUpgrade != null) { existingUpgrade.Level += upgrade.Level; existingUpgrade.ApplyUpgrade(); upgrade.Dispose(); } else { upgrade.ApplyUpgrade(); Upgrades.Add(upgrade); } return true; } protected virtual void CheckIsHighlighted() { if (IsHighlighted || ExternalHighlight) { highlightedEntities.Add(this); } else { highlightedEntities.Remove(this); } } private static readonly List tempHighlightedEntities = new List(); public static void ClearHighlightedEntities() { highlightedEntities.RemoveWhere(e => e.Removed); tempHighlightedEntities.Clear(); tempHighlightedEntities.AddRange(highlightedEntities); foreach (var entity in tempHighlightedEntities) { entity.IsHighlighted = false; } } public abstract MapEntity Clone(); public static List Clone(List entitiesToClone) { List clones = new List(); foreach (MapEntity e in entitiesToClone) { Debug.Assert(e != null); try { clones.Add(e.Clone()); } catch (Exception ex) { DebugConsole.ThrowError("Cloning entity \"" + e.Name + "\" failed.", ex); GameAnalyticsManager.AddErrorEventOnce( "MapEntity.Clone:" + e.Name, GameAnalyticsManager.ErrorSeverity.Error, "Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace.CleanupStackTrace()); return clones; } Debug.Assert(clones.Last() != null); } Debug.Assert(clones.Count == entitiesToClone.Count); //clone links between the entities for (int i = 0; i < clones.Count; i++) { if (entitiesToClone[i].linkedTo == null) { continue; } foreach (MapEntity linked in entitiesToClone[i].linkedTo) { if (!entitiesToClone.Contains(linked)) { continue; } clones[i].linkedTo.Add(clones[entitiesToClone.IndexOf(linked)]); } } //connect clone wires to the clone items and refresh links between doors and gaps List orphanedWires = new List(); for (int i = 0; i < clones.Count; i++) { if (clones[i] is not Item cloneItem) { continue; } var door = cloneItem.GetComponent(); door?.RefreshLinkedGap(); var cloneWire = cloneItem.GetComponent(); if (cloneWire == null) { continue; } var originalWire = ((Item)entitiesToClone[i]).GetComponent(); cloneWire.SetNodes(originalWire.GetNodes()); for (int n = 0; n < 2; n++) { if (originalWire.Connections[n] == null) { var disconnectedFrom = entitiesToClone.Find(e => e is Item item && (item.GetComponent()?.DisconnectedWires.Contains(originalWire) ?? false)); if (disconnectedFrom == null) { continue; } int disconnectedFromIndex = entitiesToClone.IndexOf(disconnectedFrom); var disconnectedFromClone = (clones[disconnectedFromIndex] as Item)?.GetComponent(); if (disconnectedFromClone == null) { continue; } disconnectedFromClone.DisconnectedWires.Add(cloneWire); if (cloneWire.Item.body != null) { cloneWire.Item.body.Enabled = false; } cloneWire.IsActive = false; continue; } var connectedItem = originalWire.Connections[n].Item; if (connectedItem == null || !entitiesToClone.Contains(connectedItem)) { continue; } //index of the item the wire is connected to int itemIndex = entitiesToClone.IndexOf(connectedItem); if (itemIndex < 0) { DebugConsole.ThrowError("Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone."); GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectedNotFound" + connectedItem.ID, GameAnalyticsManager.ErrorSeverity.Error, "Error while cloning wires - item \"" + connectedItem.Name + "\" was not found in entities to clone."); continue; } //index of the connection in the connectionpanel of the target item int connectionIndex = connectedItem.Connections.IndexOf(originalWire.Connections[n]); if (connectionIndex < 0) { DebugConsole.ThrowError("Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\"."); GameAnalyticsManager.AddErrorEventOnce("MapEntity.Clone:ConnectionNotFound" + connectedItem.ID, GameAnalyticsManager.ErrorSeverity.Error, "Error while cloning wires - connection \"" + originalWire.Connections[n].Name + "\" was not found in connected item \"" + connectedItem.Name + "\"."); continue; } (clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire); cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false); } if (originalWire.Connections.Any(c => c != null) && (cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent() == null) { if (!clones.Any(c => (c as Item)?.GetComponent()?.DisconnectedWires.Contains(cloneWire) ?? false)) { orphanedWires.Add(cloneWire); } } } foreach (var orphanedWire in orphanedWires) { orphanedWire.Item.Remove(); clones.Remove(orphanedWire.Item); } return clones; } protected void InsertToList() { if (Sprite == null) { MapEntityList.Add(this); return; } // Use atomic insertion to ensure thread-safety MapEntityList.InsertWithAction(this, (list, entity) => { int i = 0; //sort damageable walls by sprite depth: //necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise if (entity is Structure { DrawDamageEffect: true } structure) { //insertion sort according to draw depth float drawDepth = structure.SpriteDepth; while (i < list.Count) { float otherDrawDepth = (list[i] as Structure)?.SpriteDepth ?? 1.0f; if (otherDrawDepth < drawDepth) { break; } i++; } list.Insert(i, entity); return; } i = 0; var mapEntity = (MapEntity)entity; while (i < list.Count) { i++; if (list[i - 1]?.Prefab == mapEntity.Prefab) { list.Insert(i, entity); return; } } #if CLIENT i = 0; while (i < list.Count) { i++; Sprite existingSprite = list[i - 1].Sprite; if (existingSprite == null) { continue; } if (existingSprite.Texture == mapEntity.Sprite?.Texture) { break; } } #endif list.Insert(i, entity); }); } /// /// Remove the entity from the entity list without removing links to other entities /// public virtual void ShallowRemove() { base.Remove(); MapEntityList.Remove(this); if (aiTarget != null) aiTarget.Remove(); } public override void Remove() { base.Remove(); MapEntityList.Remove(this); #if CLIENT Submarine.ForceRemoveFromVisibleEntities(this); SelectedList.Remove(this); #endif if (aiTarget != null) { aiTarget.Remove(); aiTarget = null; } if (linkedTo != null) { for (int i = linkedTo.Count - 1; i >= 0; i--) { linkedTo[i].RemoveLinked(this); } linkedTo.Clear(); } } /// /// Call Update() on every object in Entity.list /// public static void UpdateAll(float deltaTime, Camera cam , ParallelOptions parallelOptions) { #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); #endif // Buffer lists to avoid repeated allocations var hullList = Hull.HullList.ToList(); var structureList = Structure.WallList.ToList(); var gapList = Gap.GapList.ToList(); var itemList = Item.ItemList.ToList(); // First phase: parallel updates that have no order dependencies Parallel.Invoke(parallelOptions, // Hull parallel update () => { Parallel.ForEach(hullList, parallelOptions, hull => { PhysicsBodyQueue.IsInParallelContext = true; try { hull.Update(deltaTime, cam); } finally { PhysicsBodyQueue.IsInParallelContext = false; } }); }, // Structure parallel update () => { Parallel.ForEach(structureList, parallelOptions, structure => { PhysicsBodyQueue.IsInParallelContext = true; try { structure.Update(deltaTime, cam); } finally { PhysicsBodyQueue.IsInParallelContext = false; } }); }, // Gap reset (must be done before update) () => { Parallel.ForEach(gapList, parallelOptions, gap => { gap.ResetWaterFlowThisFrame(); }); }, // Powered components update () => { Powered.UpdatePower(deltaTime); } ); // Process any physics operations queued during Hull/Structure updates. // BallastFlora growth (from Hull.Update) may queue physics body creations/transforms. PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT // Hull Cheats need to be executed after Hull update Hull.UpdateCheats(deltaTime, cam); #endif // Gap update (has order dependencies, keep random order but execute sequentially) var shuffledGaps = gapList.OrderBy(g => Rand.Int(int.MaxValue)).ToList(); Parallel.ForEach(gapList, parallelOptions, gap => { PhysicsBodyQueue.IsInParallelContext = true; try { gap.Update(deltaTime, cam); } finally { PhysicsBodyQueue.IsInParallelContext = false; } }); // Process any physics operations queued during Gap updates. PhysicsBodyQueue.ProcessPendingOperations(); #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Misc", sw.ElapsedTicks); sw.Restart(); #endif // Item update (Item.Update() is not thread-safe and must be executed on the main thread) Item.UpdatePendingConditionUpdates(deltaTime); float scaledDeltaTime = deltaTime * MapEntityUpdateInterval; Item lastUpdatedItem = null; try { Parallel.ForEach(itemList, parallelOptions, item => { PhysicsBodyQueue.IsInParallelContext = true; try { lastUpdatedItem = item; item.Update(scaledDeltaTime, cam); } finally { PhysicsBodyQueue.IsInParallelContext = false; } }); } catch (InvalidOperationException e) { GameAnalyticsManager.AddErrorEventOnce( "MapEntity.UpdateAll:ItemUpdateInvalidOperation", GameAnalyticsManager.ErrorSeverity.Critical, $"Error while updating item {lastUpdatedItem?.Name ?? "null"}: {e.Message}"); throw new InvalidOperationException($"Error while updating item {lastUpdatedItem?.Name ?? "null"}", innerException: e); } // Process any physics operations that were queued during the parallel update. // This must be done on the main thread because Farseer Physics is not thread-safe. PhysicsBodyQueue.ProcessPendingOperations(); UpdateAllProjSpecific(scaledDeltaTime); Spawner?.Update(); #if CLIENT sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("Update:MapEntity:Items", sw.ElapsedTicks); #endif } static partial void UpdateAllProjSpecific(float deltaTime); public virtual void Update(float deltaTime, Camera cam) { } /// /// Flip the entity horizontally /// /// Should the entity be flipped across the y-axis of the sub it's inside /// Forces the item to be flipped even if it's configured not to be flippable. public virtual void FlipX(bool relativeToSub, bool force = false) { FlippedX = !FlippedX; if (!relativeToSub || Submarine == null) { return; } Vector2 relative = WorldPosition - Submarine.WorldPosition; relative.Y = 0.0f; Move(-relative * 2.0f); } /// /// Flip the entity vertically /// /// Should the entity be flipped across the x-axis of the sub it's inside /// Forces the item to be flipped even if it's configured not to be flippable. public virtual void FlipY(bool relativeToSub, bool force = false) { FlippedY = !FlippedY; if (!relativeToSub || Submarine == null) { return; } Vector2 relative = WorldPosition - Submarine.WorldPosition; relative.X = 0.0f; Move(-relative * 2.0f); } public virtual Quad2D GetTransformedQuad() => Quad2D.FromSubmarineRectangle(rect); public static List LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset) { IdRemap idRemap = new IdRemap(parentElement, idOffset); bool containsHiddenContainers = false; bool hiddenContainerCreated = false; MTRandom hiddenContainerRNG = new MTRandom(ToolBox.StringToInt(submarine.Info.Name)); foreach (var element in parentElement.Elements()) { if (element.NameAsIdentifier() != "Item") { continue; } var tags = element.GetAttributeIdentifierArray("tags", Array.Empty()); if (tags.Contains(Tags.HiddenItemContainer)) { containsHiddenContainers = true; break; } } List entities = new List(); foreach (var element in parentElement.Elements()) { #if CLIENT GameMain.GameSession?.Campaign?.ThrowIfStartRoundCancellationRequested(); #endif string typeName = element.Name.ToString(); Type t; try { t = Type.GetType("Barotrauma." + typeName, true, true); if (t == null) { DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\"."); continue; } } catch (Exception e) { DebugConsole.ThrowError("Error in " + filePath + "! Could not find a entity of the type \"" + typeName + "\".", e); continue; } Identifier identifier = element.GetAttributeIdentifier("identifier", ""); Identifier replacementIdentifier = Identifier.Empty; if (t == typeof(Structure)) { string name = element.Attribute("name").Value; StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null) { ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier); if (itemPrefab != null) { DebugConsole.AddWarning($"Could not find a structure with the identifier {identifier}, but there's a matching item with the identifier. Converting to an item."); t = typeof(Item); } } } else if (t == typeof(Item) && !containsHiddenContainers && identifier == "vent" && submarine.Info.Type == SubmarineType.Player && !submarine.Info.HasTag(SubmarineTag.Shuttle)) { if (!hiddenContainerCreated) { DebugConsole.AddWarning($"There are no hidden containers such as loose vents or loose panels in the submarine \"{submarine.Info.Name}\". Certain traitor events require these to function properly. Converting one of the vents to a loose vent..."); } if (!hiddenContainerCreated || hiddenContainerRNG.NextDouble() < 0.2) { replacementIdentifier = "loosevent".ToIdentifier(); containsHiddenContainers = true; hiddenContainerCreated = true; } } try { MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(ContentXElement), typeof(Submarine), typeof(IdRemap) }); if (loadMethod == null) { DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + "."); } else if (!loadMethod.ReturnType.IsSubclassOf(typeof(MapEntity))) { DebugConsole.ThrowError("Error loading entity of the type \"" + t.ToString() + "\" - load method does not return a valid map entity."); } else { var newElement = element.FromPackage(null); if (!replacementIdentifier.IsEmpty) { newElement.SetAttributeValue("identifier", replacementIdentifier.ToString()); } object newEntity = loadMethod.Invoke(t, new object[] { newElement, submarine, idRemap }); if (newEntity != null) { entities.Add((MapEntity)newEntity); } } } catch (TargetInvocationException e) { DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException); } catch (Exception e) { DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e); } } return entities; } /// /// Update the linkedTo-lists of the entities based on the linkedToID-lists /// Has to be done after all the entities have been loaded (an entity can't /// be linked to some other entity that hasn't been loaded yet) /// private bool mapLoadedCalled; public static void MapLoaded(List entities, bool updateHulls) { InitializeLoadedLinks(entities); List linkedSubs = new List(); for (int i = 0; i < entities.Count; i++) { if (entities[i].mapLoadedCalled || entities[i].Removed) { continue; } if (entities[i] is LinkedSubmarine sub) { linkedSubs.Add(sub); continue; } entities[i].OnMapLoaded(); } if (updateHulls) { Item.UpdateHulls(); Gap.UpdateHulls(); } entities.ForEach(e => e.mapLoadedCalled = true); foreach (LinkedSubmarine linkedSub in linkedSubs) { linkedSub.OnMapLoaded(); } CreateDroppedStacks(entities); } private static void CreateDroppedStacks(List entities) { const float MaxDist = 10.0f; List itemsInStack = new List(); for (int i = 0; i < entities.Count; i++) { if (entities[i] is not Item item1 || item1.Prefab.MaxStackSize <= 1 || item1.body is not { Enabled: true }) { continue; } itemsInStack.Clear(); itemsInStack.Add(item1); for (int j = i + 1; j < entities.Count; j++) { if (entities[j] is not Item item2) { continue; } if (item1.Prefab != item2.Prefab) { continue; } if (item2.body is not { Enabled: true }) { continue; } if (item2.DroppedStack.Any()) { continue; } if (Math.Abs(item1.Position.X - item2.Position.X) > MaxDist) { continue; } if (Math.Abs(item1.Position.Y - item2.Position.Y) > MaxDist) { continue; } itemsInStack.Add(item2); } if (itemsInStack.Count > 1) { item1.CreateDroppedStack(itemsInStack, allowClientExecute: true); DebugConsole.Log($"Merged x{itemsInStack.Count} of {item1.Name} into a dropped stack."); } } } public static void InitializeLoadedLinks(IEnumerable entities) { foreach (MapEntity e in entities) { if (e.mapLoadedCalled) { continue; } if (e.linkedToID == null) { continue; } if (e.linkedToID.Count == 0) { continue; } e.linkedTo.Clear(); foreach (ushort i in e.linkedToID) { if (FindEntityByID(i) is MapEntity linked) { e.linkedTo.Add(linked); } else { #if DEBUG DebugConsole.ThrowError($"Linking the entity \"{e.Name}\" to another entity failed. Could not find an entity with the ID \"{i}\"."); #endif } } e.linkedToID.Clear(); (e as WayPoint)?.InitializeLinks(); } } public virtual void OnMapLoaded() { } public virtual XElement Save(XElement parentElement) { DebugConsole.ThrowError("Saving entity " + GetType() + " failed."); return null; } public void RemoveLinked(MapEntity e) { if (linkedTo == null) return; if (linkedTo.Contains(e)) linkedTo.Remove(e); } /// /// Gets all linked entities of specific type. /// public HashSet GetLinkedEntities(HashSet list = null, int? maxDepth = null, Func filter = null) where T : MapEntity { list = list ?? new HashSet(); int startDepth = 0; GetLinkedEntitiesRecursive(this, list, ref startDepth, maxDepth, filter); return list; } /// /// Gets all linked entities of specific type. /// private static void GetLinkedEntitiesRecursive(MapEntity mapEntity, HashSet linkedTargets, ref int depth, int? maxDepth = null, Func filter = null) where T : MapEntity { if (depth > maxDepth) { return; } foreach (var linkedEntity in mapEntity.linkedTo) { if (linkedEntity is T linkedTarget) { if (!linkedTargets.Contains(linkedTarget) && (filter == null || filter(linkedTarget))) { linkedTargets.Add(linkedTarget); depth++; GetLinkedEntitiesRecursive(linkedEntity, linkedTargets, ref depth, maxDepth, filter); } } } } } }