using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Xml.Linq; namespace Barotrauma { abstract partial class MapEntity : Entity { public static List mapEntityList = new List(); public readonly MapEntityPrefab prefab; protected List linkedToID; //observable collection because some entities may need to be notified when the collection is modified public ObservableCollection linkedTo; private bool flippedX, flippedY; public bool FlippedX { get { return flippedX; } } public bool FlippedY { get { return flippedY; } } public bool ShouldBeSaved = true; //the position and dimensions of the entity protected Rectangle rect; //is the mouse inside the rect protected bool isHighlighted; public bool IsHighlighted { get { return isHighlighted; } set { isHighlighted = value; } } 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 DrawDamageEffect { get { return false; } } public virtual bool Linkable { get { return false; } } public List AllowedLinks => prefab == null ? new List() : prefab.AllowedLinks; public bool ResizeHorizontal { get { return prefab != null && prefab.ResizeHorizontal; } } public bool ResizeVertical { get { return prefab != null && prefab.ResizeVertical; } } 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; } } public RuinGeneration.Ruin ParentRuin { get; set; } public virtual string Name { get { return ""; } } // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. private Memento rectMemento; public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine) { this.prefab = prefab; Scale = prefab != null ? prefab.Scale : 1; } public virtual void Move(Vector2 amount) { rect.X += (int)amount.X; rect.Y += (int)amount.Y; } public virtual bool IsMouseOn(Vector2 position) { return (Submarine.RectContains(WorldRect, position)); } 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, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Cloning entity \"" + e.Name + "\" failed (" + ex.Message + ").\n" + ex.StackTrace); 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 for (int i = 0; i < clones.Count; i++) { var cloneItem = clones[i] as Item; if (cloneItem == null) continue; 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) continue; var connectedItem = originalWire.Connections[n].Item; if (connectedItem == null) 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, GameAnalyticsSDK.Net.EGAErrorSeverity.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, GameAnalyticsSDK.Net.EGAErrorSeverity.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], false); } } return clones; } protected void InsertToList() { int i = 0; if (Sprite==null) { mapEntityList.Add(this); return; } while (i /// 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 if (selectedList.Contains(this)) { selectedList = selectedList.FindAll(e => e != this); } #endif if (aiTarget != null) aiTarget.Remove(); 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) { foreach (Hull hull in Hull.hullList) { hull.Update(deltaTime, cam); } foreach (Gap gap in Gap.GapList) { gap.Update(deltaTime, cam); } foreach (Item item in Item.ItemList) { item.Update(deltaTime, cam); } UpdateAllProjSpecific(deltaTime); Spawner?.Update(); } 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 public virtual void FlipX(bool relativeToSub) { 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 public virtual void FlipY(bool relativeToSub) { flippedY = !flippedY; if (!relativeToSub || Submarine == null) return; Vector2 relative = WorldPosition - Submarine.WorldPosition; relative.X = 0.0f; Move(-relative * 2.0f); } public static List LoadAll(Submarine submarine, XElement parentElement, string filePath) { List entities = new List(); foreach (XElement element in parentElement.Elements()) { 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; } try { MethodInfo loadMethod = t.GetMethod("Load", new [] { typeof(XElement), typeof(Submarine) }); 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 { object newEntity = loadMethod.Invoke(t, new object[] { element, submarine }); 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) { 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); } } List linkedSubs = new List(); for (int i = 0; i < entities.Count; i++) { if (entities[i].mapLoadedCalled) continue; if (entities[i] is LinkedSubmarine) { linkedSubs.Add((LinkedSubmarine)entities[i]); continue; } entities[i].OnMapLoaded(); } if (updateHulls) { Item.UpdateHulls(); Gap.UpdateHulls(); } entities.ForEach(e => e.mapLoadedCalled = true); foreach (LinkedSubmarine linkedSub in linkedSubs) { linkedSub.OnMapLoaded(); } } 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); } #region Serialized properties // 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, true)] 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.999f); SpriteDepthOverrideIsSet = true; } } } // The value should always be copied from the prefab. Editing is enabled only for testing the scale in the sub editor (changes are not saved). #if DEBUG [Serialize(1f, false), Editable(0.1f, 10f, DecimalCount = 3, ValueStep = 0.1f)] #else [Serialize(1f, false)] #endif public float Scale { get; set; } = 1; #endregion } }