using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using MoonSharp.Interpreter; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Xml.Linq; namespace Barotrauma { /// /// Thread-safe wrapper for Gap list operations. /// Uses copy-on-write pattern for lock-free reads. /// internal class ThreadSafeGapList : IEnumerable { private volatile List _list = new List(); private readonly object _writeLock = new object(); public int Count => _list.Count; public void Add(Gap gap) { lock (_writeLock) { var newList = new List(_list) { gap }; Interlocked.Exchange(ref _list, newList); } } public bool Remove(Gap gap) { lock (_writeLock) { var newList = new List(_list); bool removed = newList.Remove(gap); if (removed) { Interlocked.Exchange(ref _list, newList); } return removed; } } public void Clear() { Interlocked.Exchange(ref _list, new List()); } public bool Contains(Gap gap) => _list.Contains(gap); public Gap this[int index] => _list[index]; public IEnumerator GetEnumerator() => _list.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); // LINQ-friendly methods public List ToList() => new List(_list); public Gap FirstOrDefault(Func predicate) => _list.FirstOrDefault(predicate); public Gap 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() => _list.Any(); public bool Any(Func predicate) => _list.Any(predicate); public IOrderedEnumerable OrderBy(Func keySelector) => _list.OrderBy(keySelector); } partial class Gap : MapEntity, ISerializableEntity { public static ThreadSafeGapList GapList = new ThreadSafeGapList(); const float MaxFlowForce = 500.0f; public static bool ShowGaps = true; const float OutsideColliderRaycastIntervalLowPrio = 1.5f; const float OutsideColliderRaycastIntervalHighPrio = 0.1f; public bool IsHorizontal { get; private set; } /// /// "Diagonal" gaps are used on sloped walls to allow characters to pass through them either horizontally or vertically. /// Water still flows through them only horizontally or vertically /// public bool IsDiagonal { get; } public readonly float GlowEffectT; private readonly List overlappingGaps = new List(); /// /// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow? /// private bool overlappingGapsDirty; /// /// How much overlapping gaps reduce the flow rate of this one? /// private float overlappingGapFlowRateReduction; //a value between 0.0f-1.0f (0.0 = closed, 1.0f = open) private float open; //the force of the water flow which is exerted on physics bodies private Vector2 flowForce; private Hull flowTargetHull; private float openedTimer = 1.0f; private float higherSurface; private float lowerSurface; private float waterFlowThisFrame; private Vector2 lerpedFlowForce; //if set to true, hull connections of this gap won't be updated when changes are being done to hulls public bool DisableHullRechecks; //can ambient light get through the gap even if it's not open public bool PassAmbientLight; //a collider outside the gap (for example an ice wall next to the sub) //used by ragdolls to prevent them from ending up inside colliders when teleporting out of the sub private Body outsideCollisionBlocker; private float outsideColliderRaycastTimer; private bool wasRoomToRoom; public float Open { get { return open; } set { if (float.IsNaN(value)) { return; } float prevValue = open; if (value > open) { openedTimer = 1.0f; } open = MathHelper.Clamp(value, 0.0f, 1.0f); if (!MathUtils.NearlyEqual(open, prevValue)) { overlappingGapsDirty = true; FlagOverlappingGapsDirty(); if (connectedDoor == null && !IsHorizontal && linkedTo.Any(e => e is Hull)) { if (open > prevValue && open >= 1.0f) { InformWaypointsAboutGapState(this, open: true); } else if (open < prevValue && prevValue >= 1.0f) { InformWaypointsAboutGapState(this, open: false); } } } static void InformWaypointsAboutGapState(Gap gap, bool open) { foreach (var wp in WayPoint.WayPointList) { if (IsWaypointRightAboveGap(gap, wp)) { wp.OnGapStateChanged(open, gap); } } } static bool IsWaypointRightAboveGap(Gap gap, WayPoint wp) { if (wp.SpawnType != SpawnType.Path) { return false; } if (!gap.linkedTo.Contains(wp.CurrentHull)) { return false; } if (wp.Position.Y < gap.Rect.Top) { return false; } if (wp.Position.X > gap.Rect.Right) { return false; } if (wp.Position.X < gap.Rect.Left) { return false; } return true; } } } public float Size => IsHorizontal ? Rect.Height : Rect.Width; public float PressureDistributionSpeed => Size / 100.0f * open; private Door connectedDoor; public Door ConnectedDoor { get { if (connectedDoor != null && connectedDoor.Item.Removed) { connectedDoor = null; } return connectedDoor; } set { connectedDoor = value; } } public Structure ConnectedWall; public Vector2 LerpedFlowForce { get { return lerpedFlowForce; } } public Hull FlowTargetHull { get { return flowTargetHull; } } public bool IsRoomToRoom { get { return linkedTo.Count == 2; } } public override Rectangle Rect { get { return base.Rect; } set { base.Rect = value; FindHulls(); } } public override string Name => "Gap"; public readonly Dictionary properties; public Dictionary SerializableProperties { get { return properties; } } public Gap(Rectangle rectangle) : this(rectangle, Submarine.MainSub) { #if CLIENT if (SubEditorScreen.IsSubEditor()) { SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { this }, false)); } #endif } public Gap(Rectangle rect, Submarine submarine) : this(rect, rect.Width < rect.Height, submarine) { } public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, bool isDiagonal = false, ushort id = Entity.NullEntityID) : base(CoreEntityPrefab.GapPrefab, submarine, id) { this.rect = rect; flowForce = Vector2.Zero; IsHorizontal = isHorizontal; IsDiagonal = isDiagonal; open = 1.0f; properties = SerializableProperty.GetProperties(this); FindHulls(); GapList.Add(this); InsertToList(); GlowEffectT = Rand.Range(0.0f, 1.0f); float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize, BodyType.Static, Physics.CollisionWall, Physics.CollisionCharacter, findNewContacts: false); outsideCollisionBlocker.UserData = this; outsideCollisionBlocker.Enabled = false; #if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; # endif wasRoomToRoom = IsRoomToRoom; RefreshOutsideCollider(); DebugConsole.Log("Created gap (" + ID + ")"); } public override MapEntity Clone() { return new Gap(rect, IsHorizontal, Submarine); } public override void Move(Vector2 amount, bool ignoreContacts = true) { if (!MathUtils.IsValid(amount)) { DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}"); return; } base.Move(amount, ignoreContacts); if (!DisableHullRechecks) { FindHulls(); } } public static void UpdateHulls() { foreach (Gap g in GapList) { for (int i = g.linkedTo.Count - 1; i >= 0; i--) { if (g.linkedTo[i].Removed) { g.linkedTo.RemoveAt(i); } } if (g.DisableHullRechecks) continue; g.FindHulls(); } } public override bool IsMouseOn(Vector2 position) { return ShowGaps && Submarine.RectContains(WorldRect, position) && !Submarine.RectContains(MathUtils.ExpandRect(WorldRect, -5), position); } public void AutoOrient() { Vector2 searchPosLeft = new Vector2(rect.X, rect.Y - rect.Height / 2); Hull hullLeft = Hull.FindHullUnoptimized(searchPosLeft, null, false); Vector2 searchPosRight = new Vector2(rect.Right, rect.Y - rect.Height / 2); Hull hullRight = Hull.FindHullUnoptimized(searchPosRight, null, false); if (hullLeft != null && hullRight != null && hullLeft != hullRight) { IsHorizontal = true; return; } Vector2 searchPosTop = new Vector2(rect.Center.X, rect.Y); Hull hullTop = Hull.FindHullUnoptimized(searchPosTop, null, false); Vector2 searchPosBottom = new Vector2(rect.Center.X, rect.Y - rect.Height); Hull hullBottom = Hull.FindHullUnoptimized(searchPosBottom, null, false); if (hullTop != null && hullBottom != null && hullTop != hullBottom) { IsHorizontal = false; return; } if ((hullLeft == null) != (hullRight == null)) { IsHorizontal = true; } else if ((hullTop == null) != (hullBottom == null)) { IsHorizontal = false; } } private void FindHulls() { Hull[] hulls = new Hull[2]; foreach (var linked in linkedTo) { if (linked is Hull hull) { hull.ConnectedGaps.Remove(this); } } linkedTo.Clear(); int tolerance = 1; Vector2[] searchPos = new Vector2[2]; if (IsHorizontal) { searchPos[0] = new Vector2(rect.X - tolerance, rect.Y - rect.Height / 2); searchPos[1] = new Vector2(rect.Right + tolerance, rect.Y - rect.Height / 2); } else { searchPos[0] = new Vector2(rect.Center.X, rect.Y + tolerance); searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height - tolerance); } for (int i = 0; i < 2; i++) { hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false); if (hulls[i] == null) hulls[i] = Hull.FindHullUnoptimized(searchPos[i], null, false, true); } if (hulls[0] == null && hulls[1] == null) { return; } if (hulls[0] == null && hulls[1] != null) { Hull temp = hulls[0]; hulls[0] = hulls[1]; hulls[1] = temp; } flowTargetHull = hulls[0]; for (int i = 0; i < 2; i++) { if (hulls[i] == null) { continue; } linkedTo.Add(hulls[i]); if (!hulls[i].ConnectedGaps.Contains(this)) { hulls[i].ConnectedGaps.Add(this); } foreach (var gap in hulls[i].ConnectedGaps) { gap.overlappingGapsDirty = true; } } } private int updateCount; public override void Update(float deltaTime, Camera cam) { Hull hull1 = linkedTo.Count < 1 ? null : linkedTo[0] as Hull; Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1]; int updateInterval = 4; //if one hull is at lethal pressure (connected to outside), and the other not yet, //we need frequent updates to quickly move water into the other hull if (hull1 != null && hull2 != null && hull1.LethalPressure > 0.0f != hull2.LethalPressure > 0.0f) { updateInterval = 1; } else { float flowMagnitude = flowForce.LengthSquared(); if (flowMagnitude < 1.0f) { //very sparse updates if there's practically no water moving updateInterval = 8; } else if (linkedTo.Count == 2 && flowMagnitude > 10.0f) { //frequent updates if water is moving between hulls updateInterval = 1; } } updateCount++; if (updateCount < updateInterval) { return; } deltaTime *= updateCount; updateCount = 0; if (overlappingGapsDirty) { RefreshOverlappingGaps(); overlappingGapsDirty = false; } flowForce = Vector2.Zero; outsideColliderRaycastTimer -= deltaTime; if (IsRoomToRoom != wasRoomToRoom) { RefreshOutsideCollider(); wasRoomToRoom = IsRoomToRoom; } if (open == 0.0f || linkedTo.Count == 0) { lerpedFlowForce = Vector2.Zero; return; } if (hull1 == hull2) { return; } UpdateOxygen(hull1, hull2, deltaTime); if (linkedTo.Count == 1) { //gap leading from a room to outside UpdateRoomToOut(deltaTime, hull1); } else if (linkedTo.Count == 2) { //gap leading from a room to another UpdateRoomToRoom(deltaTime, hull1, hull2); } flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce); if (openedTimer > 0.0f && flowForce.LengthSquared() > lerpedFlowForce.LengthSquared()) { //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing lerpedFlowForce = flowForce; } else { lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); } openedTimer -= deltaTime; EmitParticles(deltaTime); } partial void EmitParticles(float deltaTime); void UpdateRoomToRoom(float deltaTime, Hull hull1, Hull hull2) { Vector2 subOffset = Vector2.Zero; if (hull1.Submarine != Submarine) { subOffset = Submarine.Position - hull1.Submarine.Position; } else if (hull2.Submarine != Submarine) { subOffset = hull2.Submarine.Position - Submarine.Position; } if (hull1.WaterVolume <= 0.0 && hull2.WaterVolume <= 0.0) { return; } //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows float sizeModifier = Size / 100.0f * open * (1.0f - overlappingGapFlowRateReduction); //horizontal gap (such as a regular door) if (IsHorizontal) { higherSurface = Math.Max(hull1.Surface, hull2.Surface + subOffset.Y); float delta = 0.0f; Hull flowSourceHull = null; //water level is above the lower boundary of the gap if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - Size) { int dir = (hull1.Pressure > hull2.Pressure + subOffset.Y) ? 1 : -1; //water flowing from the righthand room to the lefthand room if (dir == -1) { if (!(hull2.WaterVolume > 0.0f)) { return; } lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1]; flowTargetHull = hull1; flowSourceHull = hull2; //make sure not to move more than what the room contains delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 300.0f * sizeModifier * deltaTime, Math.Min(hull2.WaterVolume, hull2.Volume)); //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; waterFlowThisFrame += delta; if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + hull2.Pressure+subOffset.Y) / 2); } flowForce = new Vector2(-delta * (float)(Timing.Step / deltaTime), 0.0f); } else if (dir == 1) { if (!(hull1.WaterVolume > 0.0f)) { return; } lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1]; flowTargetHull = hull2; flowSourceHull = hull1; //make sure not to move more than what the room contains delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 300.0f * sizeModifier * deltaTime, Math.Min(hull1.WaterVolume, hull1.Volume)); //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - hull2.WaterVolume); hull1.WaterVolume -= delta; hull2.WaterVolume += delta; if (hull2.WaterVolume > hull2.Volume) { hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure-subOffset.Y) + hull2.Pressure) / 2); } waterFlowThisFrame += delta; flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f); } if (delta > 1.5f && subOffset == Vector2.Zero) { float avg = (hull1.Surface + hull2.Surface) / 2.0f; if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1] < rect.Y) { hull1.WaveVel[hull1.WaveY.Length - 1] = (avg - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f; hull1.WaveVel[hull1.WaveY.Length - 2] = hull1.WaveVel[hull1.WaveY.Length - 1]; } if (hull2.WaterVolume < hull2.Volume / Hull.MaxCompress && hull2.Surface + hull2.WaveY[0] < rect.Y) { hull2.WaveVel[0] = (avg - (hull2.Surface + hull2.WaveY[0])) * 0.1f; hull2.WaveVel[1] = hull2.WaveVel[0]; } } } } else { //lower room is full of water if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f) { float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier); //make sure not to place more water to the target room than it can hold if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress) { delta -= (hull1.WaterVolume + delta) - (hull1.Volume * Hull.MaxCompress); } delta = Math.Max(delta, 0.0f); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; waterFlowThisFrame += delta; flowForce = new Vector2( 0.0f, Math.Min(Math.Min((hull2.Pressure + subOffset.Y) - hull1.Pressure, 200.0f), delta * (float)(Timing.Step / deltaTime))); flowTargetHull = hull1; if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure = Math.Max(hull1.Pressure, (hull1.Pressure + (hull2.Pressure + subOffset.Y)) / 2); } } //there's water in the upper room, drop to lower else if (hull1.WaterVolume > 0) { flowTargetHull = hull2; //make sure the amount of water moved isn't more than what the room contains float delta = Math.Min(hull1.WaterVolume, deltaTime * 25000f * sizeModifier); //make sure not to place more water to the target room than it can hold if (hull2.WaterVolume + delta > hull2.Volume * Hull.MaxCompress) { delta -= (hull2.WaterVolume + delta) - (hull2.Volume * Hull.MaxCompress); } hull1.WaterVolume -= delta; hull2.WaterVolume += delta; waterFlowThisFrame += delta; flowForce = new Vector2( hull1.WaveY[hull1.GetWaveIndex(rect.X)] - hull1.WaveY[hull1.GetWaveIndex(rect.Right)], MathHelper.Clamp(-delta * (float)(Timing.Step / deltaTime), -200.0f, 0.0f)); if (hull2.WaterVolume > hull2.Volume) { hull2.Pressure = Math.Max(hull2.Pressure, ((hull1.Pressure - subOffset.Y) + hull2.Pressure) / 2); } } } if (open > 0.0f) { if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) { //both hulls full -> distribute pressure float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f; changePressure(hull1, avgLethality, PressureDistributionSpeed, deltaTime); changePressure(hull2, avgLethality, PressureDistributionSpeed, deltaTime); static void changePressure(Hull hull, float target, float speed, float deltaTime) { float diff = target - hull.LethalPressure; float maxChange = Hull.PressureBuildUpSpeed * speed * deltaTime; hull.LethalPressure += MathHelper.Clamp(diff, -maxChange, maxChange); } } else { //either hull not full -> pressure drops hull1.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; hull2.LethalPressure -= Hull.PressureDropSpeed * PressureDistributionSpeed * deltaTime; } } } /// /// How much water can flow through the gap to the hull if the gap is connected outside. /// private float GetWaterFlowFromOutside(Hull hull, float deltaTime, bool ignoreCurrentWater = false) { //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction); float delta = 500.0f * sizeModifier * deltaTime; if (!ignoreCurrentWater) { delta = Math.Min(delta, hull.Volume * Hull.MaxCompress - hull.WaterVolume); } return delta; } void UpdateRoomToOut(float deltaTime, Hull hull1) { float delta = GetWaterFlowFromOutside(hull1, deltaTime); //make sure not to place more water to the target room than it can hold hull1.WaterVolume += delta; if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 100.0f * deltaTime; } flowTargetHull = hull1; if (IsHorizontal) { //water flowing from right to left if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { flowForce = new Vector2(-delta * (float)(Timing.Step / deltaTime), 0.0f); } else { flowForce = new Vector2(delta * (float)(Timing.Step / deltaTime), 0.0f); } higherSurface = hull1.Surface; lowerSurface = rect.Y; if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface < rect.Y) { //create a wave from the side of the hull the water is leaking from if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) { CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime); } else { CreateWave(rect, hull1, 0, 1, flowForce, deltaTime); } static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime) { float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]); vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f); if (vel > 0.0f) { hull1.WaveVel[index1] += vel * deltaTime; hull1.WaveVel[index2] += vel * deltaTime; } } } else { hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } else { if (rect.Y > hull1.Rect.Y - hull1.Rect.Height / 2.0f) { flowForce = new Vector2(0.0f, -delta * (float)(Timing.Step / deltaTime)); } else { flowForce = new Vector2(0.0f, delta * (float)(Timing.Step / deltaTime)); } if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime; } } if (hull1.LethalPressure > 0) { SimulateWaterFlowFromOutsideToConnectedHulls(hull1, maxFlow: GetWaterFlowFromOutside(hull1, deltaTime, ignoreCurrentWater: true), deltaTime: deltaTime); } } private Hull GetOtherLinkedHull(Hull hull1) { if (linkedTo.Count != 2 || hull1 == null) { return null; } return (linkedTo[0] == hull1 ? linkedTo[1] : linkedTo[0]) as Hull; } public void ResetWaterFlowThisFrame() { waterFlowThisFrame = 0.0f; } /// /// Simulates water flow from the source to all the hulls it's connected to across the sub, as if the water was coming directly from outside. /// Used to prevent gaps from slowing down flooding when hulls are directly connected outside and highly pressurized. /// void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime) { List checkedHulls = new List(); checkedHulls.Add(hull); foreach (var connectedGap in hull.ConnectedGaps) { if (connectedGap == this || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; } var otherHull = connectedGap.GetOtherLinkedHull(hull); if (otherHull == null) { continue; } SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, hull, maxFlow, deltaTime); } } static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, List checkedHulls, Hull originHull, float maxFlow, float deltaTime) { const float decay = 0.95f; maxFlow = Math.Min(maxFlow, gap.GetWaterFlowFromOutside(targetHull, deltaTime, ignoreCurrentWater: true)) * decay; //if the hulls are not linked (i.e. not parts of the same room), limit the flow a bit var sourceHull = gap.GetOtherLinkedHull(targetHull); if (sourceHull != null && !sourceHull.linkedTo.Contains(targetHull)) { maxFlow *= 0.5f; } //take the amount of water that has already passed through this gap into account //(if there's multiple leaks to the outside recursively passing water through the same gap, the flow should not go above the maximum flow through this gap) maxFlow -= gap.waterFlowThisFrame; if (maxFlow <= 0.001f) { return; } checkedHulls.Add(targetHull); gap.waterFlowThisFrame += maxFlow; //don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside targetHull.WaterVolume += maxFlow; //lerp lethal pressure up very fast if (targetHull.WaterVolume > targetHull.Volume) { targetHull.LethalPressure = Math.Max(targetHull.LethalPressure, MathHelper.Lerp(targetHull.LethalPressure, originHull.LethalPressure, 0.1f)); } //stop pushing water to the following hulls once we get to a hull that's not at high pressure yet if (targetHull.LethalPressure <= 0 || targetHull.WaterVolume < targetHull.Volume) { return; } foreach (var connectedGap in targetHull.ConnectedGaps) { if (connectedGap == gap || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; } var otherHull = connectedGap.GetOtherLinkedHull(targetHull); if (otherHull == null || checkedHulls.Contains(otherHull)) { continue; } SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, originHull, maxFlow, deltaTime); } } public bool RefreshOutsideCollider() { if (outsideCollisionBlocker == null) { return false; } if (IsRoomToRoom || Submarine == null || open <= 0.0f || linkedTo.Count == 0 || linkedTo[0] is not Hull) { SingleThreadWorker.Instance.AddAction(() => { if (outsideCollisionBlocker == null) { return; } outsideCollisionBlocker.Enabled = false; }); return false; } if (outsideColliderRaycastTimer <= 0.0f) { UpdateOutsideColliderState((Hull)linkedTo[0]); outsideColliderRaycastTimer = outsideCollisionBlocker.Enabled ? OutsideColliderRaycastIntervalHighPrio : OutsideColliderRaycastIntervalLowPrio; } return outsideCollisionBlocker.Enabled; } private void UpdateOutsideColliderState(Hull hull) { if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; } Vector2 rayDir; if (IsHorizontal) { rayDir = new Vector2(Math.Sign(rect.Center.X - hull.Rect.Center.X), 0); } else { rayDir = new Vector2(0, Math.Sign((rect.Y - rect.Height / 2) - (hull.Rect.Y - hull.Rect.Height / 2))); } Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition); Vector2 rayEnd = rayStart + rayDir * 5.0f; var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1); foreach (var cell in levelCells) { if (cell.IsPointInside(WorldPosition)) { outsideCollisionBlocker.Enabled = true; Vector2 colliderPos = rayStart - Submarine.SimPosition; float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2; outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); return; } } var blockingBody = Submarine.CheckVisibility(rayStart, rayEnd); if (blockingBody != null) { //if the ray hit the body of the submarine itself (for example, if there's 2 layers of walls) we can ignore it if (blockingBody.UserData == Submarine) { return; } outsideCollisionBlocker.Enabled = true; Vector2 colliderPos = Submarine.LastPickedPosition - Submarine.SimPosition; float colliderRotation = MathUtils.VectorToAngle(Submarine.LastPickedNormal) - MathHelper.PiOver2; outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); } else { outsideCollisionBlocker.Enabled = false; } } private void UpdateOxygen(Hull hull1, Hull hull2, float deltaTime) { if (hull1 == null || hull2 == null) { return; } if (IsHorizontal) { //if the water level is above the gap, oxygen doesn't circulate if (Math.Max(hull1.WorldSurface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.WorldSurface + hull2.WaveY[0]) > WorldRect.Y) { return; } } bool? should = null; LuaCsSetup.Instance.EventService.PublishEvent(x => should = x.OnGapOxygenUpdate(this, hull1, hull2) ?? should); if (should != null && should.Value) return; float totalOxygen = hull1.Oxygen + hull2.Oxygen; float totalVolume = hull1.Volume + hull2.Volume; float deltaOxygen = (totalOxygen * hull1.Volume / totalVolume) - hull1.Oxygen; deltaOxygen = MathHelper.Clamp(deltaOxygen, -Hull.OxygenDistributionSpeed * deltaTime, Hull.OxygenDistributionSpeed * deltaTime); hull1.Oxygen += deltaOxygen; hull2.Oxygen -= deltaOxygen; } public static Gap FindAdjacent(IEnumerable gaps, Vector2 worldPos, float allowedOrthogonalDist, bool allowRoomToRoom = false) { foreach (Gap gap in gaps) { if (gap.Open == 0.0f) { continue; } if (gap.IsRoomToRoom && !allowRoomToRoom) { continue; } if (gap.ConnectedWall != null) { int sectionIndex = gap.ConnectedWall.FindSectionIndex(gap.Position); if (sectionIndex > -1 && !gap.ConnectedWall.SectionBodyDisabled(sectionIndex)) { continue; } } if (gap.IsHorizontal || gap.IsDiagonal) { if (worldPos.Y < gap.WorldRect.Y && worldPos.Y > gap.WorldRect.Y - gap.WorldRect.Height && Math.Abs(gap.WorldRect.Center.X - worldPos.X) < allowedOrthogonalDist) { return gap; } } if (!gap.IsHorizontal || gap.IsDiagonal) { if (worldPos.X > gap.WorldRect.X && worldPos.X < gap.WorldRect.Right && Math.Abs(gap.WorldRect.Y - gap.WorldRect.Height / 2 - worldPos.Y) < allowedOrthogonalDist) { return gap; } } } return null; } private void RefreshOverlappingGaps() { overlappingGapFlowRateReduction = 0.0f; overlappingGaps.Clear(); foreach (var linked in linkedTo) { if (linked is not Hull hull) { continue; } foreach (var connectedGap in hull.ConnectedGaps) { if (connectedGap == this) { continue; } if (connectedGap.IsRoomToRoom != IsRoomToRoom) { continue; } //let the "more open" gap reduce this gap's flow rate //or if they're both equally open, let the one that was created first handle it //(note that we can't use Entity.ID here because gaps on walls don't have IDs) if (connectedGap.open > open || (connectedGap.open == open && connectedGap.CreationIndex < CreationIndex)) { Rectangle intersection = Rectangle.Intersect(rect.ToWorldRect(), connectedGap.rect.ToWorldRect()); if (intersection.Width > 0 && intersection.Height > 0) { //reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is float relativeOverlap = IsHorizontal ? intersection.Height / (float)rect.Height : intersection.Width / (float)rect.Width; overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open; overlappingGaps.Add(connectedGap); } } if (overlappingGapFlowRateReduction >= 1.0f) { overlappingGapFlowRateReduction = 1.0f; break; } } } } /// /// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps /// private void FlagOverlappingGapsDirty() { foreach (var overlappingGap in overlappingGaps) { overlappingGap.overlappingGapsDirty = true; } } public override void ShallowRemove() { base.ShallowRemove(); GapList.Remove(this); foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } } public override void Remove() { base.Remove(); GapList.Remove(this); foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } if (outsideCollisionBlocker != null) { GameMain.World.Remove(outsideCollisionBlocker); outsideCollisionBlocker = null; } } public override void OnMapLoaded() { if (!DisableHullRechecks) FindHulls(); } public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); } else { //backwards compatibility rect = new Rectangle( int.Parse(element.GetAttribute("x").Value), int.Parse(element.GetAttribute("y").Value), int.Parse(element.GetAttribute("width").Value), int.Parse(element.GetAttribute("height").Value)); } bool isHorizontal = rect.Height > rect.Width; var horizontalAttribute = element.GetAttribute("horizontal"); if (horizontalAttribute != null) { isHorizontal = horizontalAttribute.Value.ToString() == "true"; } Gap g = new Gap(rect, isHorizontal, submarine, id: idRemap.GetOffsetId(element)) { linkedToID = new List(), Layer = element.GetAttributeString(nameof(Layer), null) }; g.HiddenInGame = element.GetAttributeBool(nameof(HiddenInGame), g.HiddenInGame); return g; } public override XElement Save(XElement parentElement) { XElement element = new XElement("Gap"); element.Add( new XAttribute("ID", ID), new XAttribute("horizontal", IsHorizontal ? "true" : "false"), new XAttribute(nameof(HiddenInGame), HiddenInGame), new XAttribute(nameof(Layer), Layer ?? string.Empty)); element.Add(new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + (int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," + rect.Width + "," + rect.Height)); parentElement.Add(element); return element; } } }