1128 lines
44 KiB
C#
1128 lines
44 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Thread-safe wrapper for Gap list operations.
|
|
/// Uses copy-on-write pattern for lock-free reads.
|
|
/// </summary>
|
|
internal class ThreadSafeGapList : IEnumerable<Gap>
|
|
{
|
|
private volatile List<Gap> _list = new List<Gap>();
|
|
private readonly object _writeLock = new object();
|
|
|
|
public int Count => _list.Count;
|
|
|
|
public void Add(Gap gap)
|
|
{
|
|
lock (_writeLock)
|
|
{
|
|
var newList = new List<Gap>(_list) { gap };
|
|
Interlocked.Exchange(ref _list, newList);
|
|
}
|
|
}
|
|
|
|
public bool Remove(Gap gap)
|
|
{
|
|
lock (_writeLock)
|
|
{
|
|
var newList = new List<Gap>(_list);
|
|
bool removed = newList.Remove(gap);
|
|
if (removed)
|
|
{
|
|
Interlocked.Exchange(ref _list, newList);
|
|
}
|
|
return removed;
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
Interlocked.Exchange(ref _list, new List<Gap>());
|
|
}
|
|
|
|
public bool Contains(Gap gap) => _list.Contains(gap);
|
|
|
|
public Gap this[int index] => _list[index];
|
|
|
|
public IEnumerator<Gap> GetEnumerator() => _list.GetEnumerator();
|
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
// LINQ-friendly methods
|
|
public List<Gap> ToList() => new List<Gap>(_list);
|
|
public Gap FirstOrDefault(Func<Gap, bool> predicate) => _list.FirstOrDefault(predicate);
|
|
public Gap Find(Predicate<Gap> predicate) => _list.Find(predicate);
|
|
public List<Gap> FindAll(Predicate<Gap> predicate) => _list.FindAll(predicate);
|
|
public IEnumerable<Gap> Where(Func<Gap, bool> predicate) => _list.Where(predicate);
|
|
public bool Any() => _list.Any();
|
|
public bool Any(Func<Gap, bool> predicate) => _list.Any(predicate);
|
|
public IOrderedEnumerable<Gap> OrderBy<TKey>(Func<Gap, TKey> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// "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
|
|
/// </summary>
|
|
public bool IsDiagonal { get; }
|
|
|
|
public readonly float GlowEffectT;
|
|
|
|
private readonly List<Gap> overlappingGaps = new List<Gap>();
|
|
|
|
/// <summary>
|
|
/// Do we need to recheck which gaps are overlapping with this one, and how much they should reduce this gap's flow?
|
|
/// </summary>
|
|
private bool overlappingGapsDirty;
|
|
|
|
/// <summary>
|
|
/// How much overlapping gaps reduce the flow rate of this one?
|
|
/// </summary>
|
|
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<Identifier, SerializableProperty> properties;
|
|
public Dictionary<Identifier, SerializableProperty> SerializableProperties
|
|
{
|
|
get { return properties; }
|
|
}
|
|
|
|
public Gap(Rectangle rectangle)
|
|
: this(rectangle, Submarine.MainSub)
|
|
{
|
|
#if CLIENT
|
|
if (SubEditorScreen.IsSubEditor())
|
|
{
|
|
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// How much water can flow through the gap to the hull if the gap is connected outside.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime)
|
|
{
|
|
List<Hull> checkedHulls = new List<Hull>();
|
|
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<Hull> 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)
|
|
{
|
|
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; }
|
|
}
|
|
|
|
var should = GameMain.LuaCs.Hook.Call<bool?>("gapOxygenUpdate", this, hull1, hull2);
|
|
|
|
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<Gap> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mark all gaps that are currently known to overlap with this one as needing a refresh of overlapping gaps
|
|
/// </summary>
|
|
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<ushort>(),
|
|
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;
|
|
}
|
|
}
|
|
}
|