Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs
Eero e167a34f32 Make entity lists thread-safe with copy-on-write wrappers
Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase.
2025-12-28 21:59:03 +08:00

1452 lines
52 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
namespace Barotrauma.MapCreatures.Behavior
{
/// <summary>
/// Thread-safe wrapper for BallastFloraBehavior list operations.
/// Uses copy-on-write pattern for lock-free reads.
/// </summary>
internal class ThreadSafeBallastFloraList : IEnumerable<BallastFloraBehavior>
{
private volatile List<BallastFloraBehavior> _list = new List<BallastFloraBehavior>();
private readonly object _writeLock = new object();
public int Count => _list.Count;
public void Add(BallastFloraBehavior entity)
{
lock (_writeLock)
{
var newList = new List<BallastFloraBehavior>(_list) { entity };
Interlocked.Exchange(ref _list, newList);
}
}
public bool Remove(BallastFloraBehavior entity)
{
lock (_writeLock)
{
var newList = new List<BallastFloraBehavior>(_list);
bool removed = newList.Remove(entity);
if (removed)
{
Interlocked.Exchange(ref _list, newList);
}
return removed;
}
}
public void Clear()
{
Interlocked.Exchange(ref _list, new List<BallastFloraBehavior>());
}
public IEnumerator<BallastFloraBehavior> GetEnumerator() => _list.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
// LINQ-friendly methods
public List<BallastFloraBehavior> ToList() => new List<BallastFloraBehavior>(_list);
public bool Any() => _list.Any();
public bool Any(Func<BallastFloraBehavior, bool> predicate) => _list.Any(predicate);
public IEnumerable<BallastFloraBehavior> Where(Func<BallastFloraBehavior, bool> predicate) => _list.Where(predicate);
}
class BallastFloraBranch : VineTile
{
public readonly BallastFloraBehavior? ParentBallastFlora;
public int ID = -1;
public Item? ClaimedItem;
public int ClaimedItemId = -1;
public float MaxHealth = 100f;
private float health = 100;
public float Health
{
get { return health; }
set { health = MathHelper.Clamp(value, 0.0f, MaxHealth); }
}
public float RemoveTimer = 60.0f;
public bool SpawningItem;
public Item? AttackItem;
public bool IsRoot;
/// <summary>
/// Decorative branches that grow around the root
/// </summary>
public bool IsRootGrowth;
public bool Removed;
public bool DisconnectedFromRoot;
public Hull? CurrentHull;
public float Pulse = 1.0f;
private bool inflate;
private float pulseDelay = Rand.Range(0f, 3f);
private BallastFloraBranch? parentBranch;
public BallastFloraBranch? ParentBranch
{
get { return parentBranch; }
set
{
if (value != parentBranch)
{
parentBranch = value;
if (parentBranch != null)
{
BranchDepth = parentBranch.BranchDepth + 1;
}
}
}
}
/// <summary>
/// How far from the root this branch is
/// </summary>
public int BranchDepth { get; private set; }
public float AccumulatedDamage;
public float DamageVisualizationTimer;
#if CLIENT
public Vector2 ShakeAmount;
#endif
// Adjacent tiles, used to free up sides when this branch gets removed
public readonly Dictionary<TileSide, BallastFloraBranch> Connections = new Dictionary<TileSide, BallastFloraBranch>();
public BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
: base(null, position, type, flowerConfig, leafConfig, rect)
{
ParentBranch = parentBranch;
ParentBallastFlora = parent;
}
public void UpdateHealth()
{
if (MaxHealth <= Health) { return; }
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f);
}
public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay)
{
if (ParentBallastFlora == null || DisconnectedFromRoot) { return; }
if (pulseDelay > 0)
{
pulseDelay -= deltaTime;
return;
}
if (inflate)
{
Pulse += inflateSpeed * deltaTime;
if (Pulse > 1.25f)
{
inflate = false;
}
}
else
{
Pulse -= deflateSpeed * deltaTime;
if (Pulse < 1f)
{
inflate = true;
pulseDelay = delay;
}
}
}
}
internal partial class BallastFloraBehavior : ISerializableEntity
{
#if DEBUG
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
#endif
private readonly static ThreadSafeBallastFloraList _entityList = new ThreadSafeBallastFloraList();
public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
public enum NetworkHeader
{
Spawn,
Kill,
BranchCreate,
BranchRemove,
BranchDamage,
Infect,
Remove
}
public enum AttackType
{
Fire,
Explosives,
Other,
CutFromRoot
}
public struct AITarget
{
public Identifier[] Tags;
public int Priority;
public AITarget(ContentXElement element)
{
Tags = element.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>())!;
Priority = element.GetAttributeInt("priority", 0);
}
public bool Matches(Item item)
{
foreach (Identifier targetTag in Tags)
{
if (item.HasTag(targetTag)) { return true; }
}
return false;
}
}
[Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the branches.")]
public float BaseBranchScale { get; set; }
[Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the flowers.")]
public float BaseFlowerScale { get; set; }
[Serialize(0.5f, IsPropertySaveable.Yes, "Scale of the leaves.")]
public float BaseLeafScale { get; set; }
[Serialize(0.33f, IsPropertySaveable.Yes, "Chance for a flower to appear on a branch.")]
public float FlowerProbability { get; set; }
[Serialize(0.7f, IsPropertySaveable.Yes, "Chance for leaves to appear on a branch.")]
public float LeafProbability { get; set; }
[Serialize(3f, IsPropertySaveable.Yes, "Delay between pulses.")]
public float PulseDelay { get; set; }
[Serialize(3f, IsPropertySaveable.Yes, "How fast the flower inflates during a pulse.")]
public float PulseInflateSpeed { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "How fast the flower deflates.")]
public float PulseDeflateSpeed { get; set; }
[Serialize(32, IsPropertySaveable.Yes, "How many vines must grow before the plant breaks through the wall.")]
public int BreakthroughPoint { get; set; }
[Serialize(false, IsPropertySaveable.Yes, "Has the plant grown large enough to expose itself.")]
public bool HasBrokenThrough { get; set; }
[Serialize(300, IsPropertySaveable.Yes, "How far the ballast flora can detect items from.")]
public int Sight { get; set; }
[Serialize(100, IsPropertySaveable.Yes, "How much health the branches have.")]
public int BranchHealth { get; set; }
[Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")]
public int RootHealth { get; set; }
[Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
public float HealthRegenPerBranch { get; set; }
[Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")]
public int MaxBranchHealthRegenDistance { get; set; }
[Serialize("255,255,255,255", IsPropertySaveable.Yes)]
public Color RootColor { get; set; }
[Serialize(300f, IsPropertySaveable.Yes, "How much power the ballast flora takes from junction boxes.")]
public float PowerConsumptionMin { get; set; }
[Serialize(3000f, IsPropertySaveable.Yes, "How much the power drain spikes.")]
public float PowerConsumptionMax { get; set; }
[Serialize(10f, IsPropertySaveable.Yes, "How long it takes for power drain to wind down.")]
public float PowerConsumptionDuration { get; set; }
[Serialize(250f, IsPropertySaveable.Yes, "How much power does it take to accelerate growth.")]
public float PowerRequirement { get; set; }
[Serialize(5f, IsPropertySaveable.Yes, "Maximum anger, anger increases when the plant gets damaged and increases growth speed.")]
public float MaxAnger { get; set; }
[Serialize(10000f, IsPropertySaveable.Yes, "Maximum power buffer.")]
public float MaxPowerCapacity { get; set; }
[Serialize("", IsPropertySaveable.Yes, "Item prefab that is spawned when threatened.")]
public Identifier AttackItemPrefab { get; set; } = Identifier.Empty;
[Serialize(0.8f, IsPropertySaveable.Yes, "How resistant the ballast flora is to explosives before it blooms.")]
public float ExplosionResistance { get; set; }
[Serialize(5f, IsPropertySaveable.Yes, "How much damage is taken from open fires.")]
public float FireVulnerability { get; set; }
[Serialize(0.5f, IsPropertySaveable.Yes, "How much resistance against fire is gained while submerged.")]
public float SubmergedWaterResistance { get; set; }
[Serialize(0.8f, IsPropertySaveable.Yes, "What depth the branches will be drawn on.")]
public float BranchDepth { get; set; }
[Serialize("", IsPropertySaveable.Yes, "What sound to play when the ballast flora bursts through walls.")]
public string BurstSound { get; set; } = "";
private float availablePower;
[Serialize(0f, IsPropertySaveable.Yes, "How much power the ballast flora has stored.")]
public float AvailablePower
{
get => availablePower;
set => availablePower = Math.Max(value, MaxPowerCapacity);
}
private float anger;
[Serialize(1f, IsPropertySaveable.Yes, "How enraged the flora is, affects how fast it grows.")]
public float Anger
{
get => anger;
set => anger = Math.Clamp(value, 1f, MaxAnger);
}
public string Name { get; } = "";
public Hull Parent { get; private set; }
public BallastFloraPrefab Prefab { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public Vector2 Offset;
public readonly HashSet<Item> ClaimedTargets = new HashSet<Item>();
public readonly HashSet<PowerTransfer> ClaimedJunctionBoxes = new HashSet<PowerTransfer>();
public readonly HashSet<PowerContainer> ClaimedBatteries = new HashSet<PowerContainer>();
public readonly Dictionary<Item, int> IgnoredTargets = new Dictionary<Item, int>();
private readonly List<Tuple<UInt16, int>> tempClaimedTargets = new List<Tuple<ushort, int>>();
private int flowerVariants, leafVariants;
public readonly List<AITarget> Targets = new List<AITarget>();
public float PowerConsumptionTimer;
private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer;
private readonly List<BallastFloraBranch> branchesVulnerableToFire = new List<BallastFloraBranch>();
public readonly List<BallastFloraBranch> Branches = new List<BallastFloraBranch>();
private BallastFloraBranch? root;
private readonly List<Body> bodies = new List<Body>();
/// <summary>
/// Branches that need physics bodies created on the main thread.
/// </summary>
private readonly List<BallastFloraBranch> pendingBodyCreations = new List<BallastFloraBranch>();
private readonly object pendingBodyCreationsLock = new object();
private bool isDead;
public readonly BallastFloraStateMachine StateMachine;
public int GrowthWarps;
public void OnMapLoaded()
{
foreach ((ushort itemId, int branchid) in tempClaimedTargets)
{
if (Entity.FindEntityByID(itemId) is Item item)
{
ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid), true);
}
else
{
string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by the ballast flora.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:ClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
}
}
foreach (BallastFloraBranch branch in Branches)
{
SetHull(branch);
if (branch.ClaimedItemId > -1)
{
if (Entity.FindEntityByID((ushort)branch.ClaimedItemId) is Item item)
{
branch.ClaimedItem = item;
}
else
{
string errorMsg = $"Error in BallastFloraBehavior.OnMapLoaded: could not find the item claimed by a branch.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.OnMapLoaded:BranchClaimedItemNotFound", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
}
}
UpdateConnections(branch);
// OnMapLoaded runs on the main thread, so we can create bodies immediately
CreateBody(branch, immediate: true);
}
}
private int CreateID()
{
int maxId = Branches.Any() ? Branches.Max(b => b.ID) : 0;
return ++maxId;
}
public Vector2 GetWorldPosition()
{
return Parent.WorldPosition + Offset;
}
public BallastFloraBehavior(Hull parent, BallastFloraPrefab prefab, Vector2 offset, bool firstGrowth = false)
{
Prefab = prefab;
Offset = offset;
Parent = parent;
SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element);
LoadPrefab(prefab.Element);
StateMachine = new BallastFloraStateMachine(this);
if (firstGrowth) { GenerateRoot(); }
_entityList.Add(this);
}
partial void LoadPrefab(ContentXElement element);
public void LoadTargets(ContentXElement element)
{
foreach (var subElement in element.Elements())
{
Targets.Add(new AITarget(subElement));
}
}
public void Save(XElement element)
{
XElement saveElement = new XElement(nameof(BallastFloraBehavior),
new XAttribute("identifier", Prefab.Identifier),
new XAttribute("offset", XMLExtensions.Vector2ToString(Offset)));
SerializableProperty.SerializeProperties(this, saveElement);
foreach (BallastFloraBranch branch in Branches)
{
XElement be = new XElement("Branch",
new XAttribute("flowerconfig", branch.FlowerConfig.Serialize()),
new XAttribute("leafconfig", branch.LeafConfig.Serialize()),
new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)),
new XAttribute("ID", branch.ID),
new XAttribute("isroot", branch.IsRoot),
new XAttribute("isrootgrowth", branch.IsRootGrowth),
new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("sides", (int)branch.Sides),
new XAttribute("blockedsides", (int)branch.BlockedSides),
new XAttribute("tile", (int)branch.Type));
if (branch.ClaimedItem != null)
{
be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1)));
}
if (branch.ParentBranch != null && !branch.ParentBranch.Removed)
{
be.Add(new XAttribute("parentbranch", (int)(branch.ParentBranch?.ID ?? -1)));
}
saveElement.Add(be);
}
foreach (Item target in ClaimedTargets)
{
if (target.Infector == null)
{
string errorMsg = $"Error in BallastFloraBehavior.Save: claimed target \"{target.Prefab.Identifier}\" had no infector set.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("BallastFloraBehavior.Save:InfectorNull", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
continue;
}
XElement te = new XElement("ClaimedTarget", new XAttribute("id", target.ID), new XAttribute("branchId", target.Infector.ID));
saveElement.Add(te);
}
element.Add(saveElement);
}
public void LoadSave(XElement element, IdRemap idRemap)
{
List<(BallastFloraBranch branch, int parentBranchId)> branches = new List<(BallastFloraBranch branch, int parentBranchId)>();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
Offset = element.GetAttributeVector2("offset", Vector2.Zero);
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "branch":
LoadBranch(subElement, idRemap);
break;
case "claimedtarget":
int id = subElement.GetAttributeInt("id", -1);
int branchId = subElement.GetAttributeInt("branchId", -1);
if (id > 0)
{
tempClaimedTargets.Add(Tuple.Create(idRemap.GetOffsetId(id), branchId));
}
break;
}
}
foreach ((BallastFloraBranch branch, int parentBranchId) in branches)
{
if (parentBranchId > -1)
{
var parentBranch = Branches.Find(b => b.ID == parentBranchId);
if (parentBranch == null)
{
DebugConsole.AddWarning($"Error while loading ballast flora: couldn't find a parent branch with the ID {parentBranchId}");
}
else
{
branch.ParentBranch = parentBranch;
}
}
}
if (root == null)
{
Branches.ForEach(b => b.DisconnectedFromRoot = true);
}
else
{
CheckDisconnectedFromRoot();
}
void LoadBranch(XElement branchElement, IdRemap idRemap)
{
Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero);
bool isRoot = branchElement.GetAttributeBool("isroot", false);
bool isRootGrowth = branchElement.GetAttributeBool("isrootgrowth", false);
int flowerConfig = getInt("flowerconfig");
int leafconfig = getInt("leafconfig");
int id = getInt("ID");
float health = getFloat("health");
float maxhealth = getFloat("maxhealth");
int sides = getInt("sides");
int blockedSides = getInt("blockedsides");
int claimedId = branchElement.GetAttributeInt("claimed", -1);
int parentBranchId = branchElement.GetAttributeInt("parentbranch", -1);
VineTileType type = (VineTileType)branchElement.GetAttributeInt("tile", 0);
BallastFloraBranch newBranch = new BallastFloraBranch(this, null, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig))
{
ID = id,
Health = health,
MaxHealth = maxhealth,
Sides = (TileSide)sides,
BlockedSides = (TileSide)blockedSides,
IsRoot = isRoot,
IsRootGrowth = isRootGrowth
};
branches.Add((newBranch, parentBranchId));
if (newBranch.IsRoot) { root = newBranch; }
if (claimedId > -1)
{
newBranch.ClaimedItemId = idRemap.GetOffsetId((ushort)claimedId);
}
Branches.Add(newBranch);
int getInt(string name) => branchElement.GetAttributeInt(name, 0);
float getFloat(string name) => branchElement.GetAttributeFloat(name, 0f);
}
}
public void Update(float deltaTime)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
if (Branches.Count == 0)
{
Remove();
return;
}
}
foreach (BallastFloraBranch branch in Branches)
{
branch.UpdateScale(deltaTime);
branch.UpdatePulse(deltaTime, PulseInflateSpeed, PulseDeflateSpeed, PulseDelay);
#if CLIENT
branch.UpdateHealth();
#endif
}
UpdateDamage(deltaTime);
UpdatePowerDrain(deltaTime);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (root != null && HealthRegenPerBranch > 0.0f)
{
float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; }
float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount;
if (branchHealAmount <= 0.0f) { continue; }
float prevHealth = branch.Health;
branch.Health += branchHealAmount;
branch.AccumulatedDamage += (prevHealth - branch.Health);
}
}
StateMachine.Update(deltaTime);
if (HasBrokenThrough)
{
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 10 seconds
if (fireCheckCooldown <= 0)
{
UpdateFireSources();
fireCheckCooldown = 10f;
}
else
{
fireCheckCooldown -= deltaTime;
}
foreach (BallastFloraBranch branch in branchesVulnerableToFire)
{
if (!branch.Removed)
{
DamageBranch(branch, FireVulnerability * deltaTime, AttackType.Fire, null);
}
}
}
UpdateSelfDamage(deltaTime);
if (Anger > 1f)
{
Anger -= deltaTime;
}
if (toxinsTimer > 0.1f)
{
toxinsSpawnTimer -= deltaTime;
if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f)
{
toxinsSpawnTimer = 1.0f;
Dictionary<Hull, List<BallastFloraBranch>> branches = new Dictionary<Hull, List<BallastFloraBranch>>();
foreach (BallastFloraBranch branch in Branches)
{
if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) { continue; }
if (branches.TryGetValue(branch.CurrentHull, out List<BallastFloraBranch>? list))
{
list.Add(branch);
}
else
{
branches.Add(branch.CurrentHull, new List<BallastFloraBranch> { branch });
}
}
foreach (Hull hull in branches.Keys)
{
List<BallastFloraBranch> list = branches[hull];
if (!list.Any(HasAcidEmitter))
{
BallastFloraBranch? randomBranch = branches[hull].GetRandomUnsynced();
if (randomBranch == null) { continue; }
randomBranch.SpawningItem = true;
ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab);
#warning TODO: Parent needs a nullability sanity check
Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item =>
{
randomBranch.AttackItem = item;
randomBranch.SpawningItem = false;
});
}
static bool HasAcidEmitter(BallastFloraBranch b) => b.SpawningItem || (b.AttackItem != null && !b.AttackItem.Removed);
}
}
toxinsTimer -= deltaTime;
}
if (defenseCooldown >= 0)
{
defenseCooldown -= deltaTime;
}
if (toxinsCooldown >= 0)
{
toxinsCooldown -= deltaTime;
}
}
partial void UpdateDamage(float deltaTime);
private readonly List<BallastFloraBranch> toBeRemoved = new List<BallastFloraBranch>();
private void UpdateSelfDamage(float deltaTime)
{
if (selfDamageTimer <= 0)
{
if (!HasBrokenThrough && !CanGrowMore())
{
Branches.ForEachMod(branch =>
{
float maxHealth = branch.IsRoot ? RootHealth : BranchHealth;
DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
});
}
selfDamageTimer = 1f;
}
toBeRemoved.Clear();
foreach (BallastFloraBranch branch in Branches)
{
if (!branch.IsRoot)
{
if (branch.ParentBranch == null || branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)
{
float parentHealth = branch.ParentBranch == null ? 0.0f : branch.ParentBranch.Health / branch.ParentBranch.MaxHealth;
float speed = MathHelper.Lerp(5.0f, 0.1f, parentHealth);
DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot);
}
}
if (branch.Health <= 0.0f)
{
if (branch.ClaimedItem != null)
{
RemoveClaim(branch.ClaimedItem);
}
branch.RemoveTimer -= deltaTime;
if (branch.RemoveTimer <= 0.0f)
{
toBeRemoved.Add(branch);
}
}
}
foreach (BallastFloraBranch branch in toBeRemoved)
{
RemoveBranch(branch);
}
selfDamageTimer -= deltaTime;
}
private void UpdatePowerDrain(float deltaTime)
{
PowerConsumptionTimer += deltaTime;
if (PowerConsumptionTimer > PowerConsumptionDuration)
{
PowerConsumptionTimer = 0f;
}
float powerConsumption = MathHelper.Lerp(PowerConsumptionMax, PowerConsumptionMin, PowerConsumptionTimer / PowerConsumptionDuration);
float powerDelta = powerConsumption * deltaTime;
foreach (PowerTransfer jb in ClaimedJunctionBoxes)
{
if (jb.ExtraLoad > Math.Max(PowerConsumptionMin, PowerConsumptionMax)) { continue; }
jb.ExtraLoad = powerConsumption;
float currPowerConsumption = -jb.CurrPowerConsumption;
if (currPowerConsumption > powerDelta)
{
AvailablePower += powerDelta;
}
else
{
AvailablePower += currPowerConsumption * deltaTime;
}
}
float batteryDrain = powerDelta * 0.1f;
foreach (PowerContainer battery in ClaimedBatteries)
{
float amount = Math.Min(battery.MaxOutPut, batteryDrain);
if (battery.Charge > amount)
{
battery.Charge -= amount;
AvailablePower += amount;
}
}
}
/// <summary>
/// Update which branches are currently in range of fires
/// </summary>
private void UpdateFireSources()
{
branchesVulnerableToFire.Clear();
foreach (BallastFloraBranch branch in Branches)
{
if (branch.CurrentHull == null) { continue; }
foreach (FireSource source in branch.CurrentHull.FireSources)
{
if (source.IsInDamageRange(GetWorldPosition() + branch.Position, source.DamageRange))
{
branchesVulnerableToFire.Add(branch);
}
}
}
}
private bool IsInWater(BallastFloraBranch branch)
{
if (branch.CurrentHull == null) { return false; }
float surfaceY = branch.CurrentHull.Surface;
Vector2 pos = Parent.Position + Offset + branch.Position;
return Parent.WaterVolume > 0.0f && pos.Y < surfaceY;
}
// could probably be moved to the branch constructor
public void SetHull(BallastFloraBranch branch)
{
branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true);
}
private void GenerateRoot()
{
if (root != null)
{
DebugConsole.ThrowError("Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace);
}
root = new BallastFloraBranch(this, null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig)
{
BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right,
GrowthStep = 1f,
MaxHealth = RootHealth,
Health = RootHealth,
IsRoot = true,
CurrentHull = Parent,
ID = CreateID()
};
Branches.Add(root);
CreateBody(root);
}
public float GetGrowthSpeed(float deltaTime)
{
float load = PowerRequirement * Anger * deltaTime;
if (AvailablePower > load)
{
AvailablePower -= load;
return Anger * 2f * deltaTime;
}
return deltaTime;
}
public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List<BallastFloraBranch> result, bool isRootGrowth = false, Vector2? forcePosition = null)
{
result = new List<BallastFloraBranch>();
if (!isRootGrowth && parent.IsSideBlocked(side)) { return false; }
Vector2 pos = forcePosition ?? parent.AdjacentPositions[side];
Rectangle rect = VineTile.CreatePlantRect(pos);
if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth))
{
parent.BlockedSides |= side;
parent.FailedGrowthAttempts++;
return false;
}
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
if (FlowerProbability > Rand.Range(0d, 1.0d))
{
flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, 0.5f, 1.0f);
}
if (LeafProbability > Rand.Range(0d, 1.0d))
{
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f);
}
BallastFloraBranch newBranch = new BallastFloraBranch(this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect)
{
ID = CreateID(),
MaxHealth = BranchHealth,
Health = BranchHealth,
IsRootGrowth = isRootGrowth
};
SetHull(newBranch);
if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine)
{
if (!isRootGrowth) { parent.BlockedSides |= side; }
parent.FailedGrowthAttempts++;
return false;
}
UpdateConnections(newBranch, parent);
Branches.Add(newBranch);
result.Add(newBranch);
OnBranchGrowthSuccess(newBranch);
if (GrowthWarps > 0)
{
GrowthWarps--;
}
int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
if (rootGrowthCount < GetDesiredRootGrowthAmount())
{
if (root != null)
{
Vector2 rootGrowthPos = Rand.Vector(Math.Max(rootGrowthCount, 1) * Rand.Range(3.0f, 5.0f));
TryGrowBranch(root, TileSide.None, out List<BallastFloraBranch> newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos);
}
}
#if SERVER
CreateNetworkMessage(new BranchCreateEventData(newBranch, parent));
#endif
return true;
}
private int GetDesiredRootGrowthAmount()
{
if (root == null) { return 0; }
return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30);
}
public bool BranchContainsTarget(BallastFloraBranch branch, Item target)
{
Rectangle worldRect = branch.Rect;
worldRect.Location = GetWorldPosition().ToPoint() + worldRect.Location;
return worldRect.IntersectsWorld(target.WorldRect);
}
public void ClaimTarget(Item target, BallastFloraBranch? branch, bool load = false)
{
target.Infector = branch;
if (target.GetComponent<PowerTransfer>() is { } powerTransfer)
{
ClaimedJunctionBoxes.Add(powerTransfer);
}
if (target.GetComponent<PowerContainer>() is { } powerContainer)
{
ClaimedBatteries.Add(powerContainer);
}
ClaimedTargets.Add(target);
if (branch != null)
{
branch.ClaimedItem = target;
}
#if SERVER
if (!load)
{
CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch));
}
#endif
}
private void UpdateConnections(BallastFloraBranch branch, BallastFloraBranch? parent = null)
{
foreach (BallastFloraBranch otherBranch in Branches)
{
var (distX, distY) = branch.Position - otherBranch.Position;
int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
if (absDistX > branch.Rect.Width || absDistY > branch.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
TileSide oppositeSide = connectingSide.GetOppositeSide();
if (parent != null)
{
if (otherBranch.BlockedSides.HasFlag(connectingSide))
{
branch.BlockedSides |= oppositeSide;
continue;
}
if (otherBranch != parent)
{
otherBranch.BlockedSides |= connectingSide;
branch.BlockedSides |= oppositeSide;
}
else
{
otherBranch.Sides |= connectingSide;
branch.Sides |= oppositeSide;
}
}
branch.Connections.TryAdd(oppositeSide, otherBranch);
otherBranch.Connections.TryAdd(connectingSide, branch);
}
}
private void OnBranchGrowthSuccess(BallastFloraBranch newBranch)
{
if (!HasBrokenThrough)
{
if (Branches.Count > BreakthroughPoint)
{
BreakThrough();
}
#if CLIENT
if (newBranch.FlowerConfig.Variant > -1)
{
Vector2 flowerPos = GetWorldPosition() + newBranch.Position;
CreateShapnel(flowerPos);
newBranch.GrowthStep = 2.0f;
SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800);
}
#endif
}
CreateBody(newBranch);
foreach (BallastFloraBranch vine in Branches)
{
vine.UpdateType();
}
}
/// <summary>
/// Queue a physics body creation for a branch.
/// The actual body will be created on the main thread to ensure thread safety.
/// </summary>
/// <param name="branch">The branch to create a body for</param>
/// <param name="immediate">If true, create the body immediately (only safe when called from main thread)</param>
private void CreateBody(BallastFloraBranch branch, bool immediate = false)
{
if (immediate)
{
CreateBodyImmediate(branch);
return;
}
lock (pendingBodyCreationsLock)
{
pendingBodyCreations.Add(branch);
}
PhysicsBodyQueue.EnqueueCreation(() => ProcessPendingBodyCreations());
}
/// <summary>
/// Process all pending body creations on the main thread.
/// This ensures Farseer Physics operations are thread-safe.
/// </summary>
private void ProcessPendingBodyCreations()
{
List<BallastFloraBranch> branchesToProcess;
lock (pendingBodyCreationsLock)
{
if (pendingBodyCreations.Count == 0) { return; }
branchesToProcess = new List<BallastFloraBranch>(pendingBodyCreations);
pendingBodyCreations.Clear();
}
foreach (var branch in branchesToProcess)
{
if (branch.Removed) { continue; }
CreateBodyImmediate(branch);
}
}
/// <summary>
/// Actually create the physics body for a branch.
/// Must be called on the main thread.
/// </summary>
private void CreateBodyImmediate(BallastFloraBranch branch)
{
Rectangle rect = branch.Rect;
Vector2 pos = Parent.Position + Offset + branch.Position;
float scale = branch.IsRoot ? 3.0f : 1f;
Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f);
branchBody.BodyType = BodyType.Static;
branchBody.UserData = branch;
branchBody.SetCollidesWith(Physics.CollisionRepairableWall);
branchBody.SetCollisionCategories(Physics.CollisionRepairableWall);
branchBody.Position = ConvertUnits.ToSimUnits(pos);
branchBody.Enabled = HasBrokenThrough;
bodies.Add(branchBody);
}
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.DamageVisualizationTimer = 1.0f;
}
if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; }
if (type != AttackType.Other && type != AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
Anger += damage * 0.001f;
}
if (GameMain.NetworkMember != null)
{
// damage is handled server side
if (GameMain.NetworkMember.IsClient)
{
return;
}
else
{
//accumulate damage on the server's side to ensure clients get notified
if (type == AttackType.Other || type == AttackType.CutFromRoot)
{
branch.AccumulatedDamage += damage;
}
}
}
if (attacker != null && toxinsCooldown <= 0)
{
toxinsTimer = 25f;
toxinsCooldown = 60f;
}
if (type == AttackType.Fire)
{
if (attacker is not null)
{
damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier);
}
if (IsInWater(branch))
{
damage *= 1f - SubmergedWaterResistance;
}
if (defenseCooldown <= 0)
{
if (StateMachine.State is not DefendWithPumpState)
{
StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
defenseCooldown = 180f;
}
else
{
defenseCooldown = 10f;
}
}
}
if (damage > 0)
{
damage = Math.Min(damage, branch.Health);
}
else
{
damage = Math.Max(damage, branch.Health - branch.MaxHealth);
}
branch.Health -= damage;
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
#endif
if (branch.Health <= 0 && type != AttackType.CutFromRoot)
{
RemoveBranch(branch);
if (branch.IsRoot) { Kill(); }
}
}
private void CheckDisconnectedFromRoot()
{
bool foundDisconnected;
do
{
foundDisconnected = false;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.ParentBranch == null || branch.DisconnectedFromRoot) { continue; }
if (branch.ParentBranch.Removed || branch.ParentBranch.DisconnectedFromRoot)
{
branch.DisconnectedFromRoot = true;
foundDisconnected = true;
}
}
} while (foundDisconnected);
}
public void RemoveBranch(BallastFloraBranch branch)
{
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
Anger += 0.01f;
bool wasRemoved = branch.Removed;
Branches.Remove(branch);
branch.Removed = true;
CheckDisconnectedFromRoot();
bodies.ForEachMod(body =>
{
if (body.UserData == branch)
{
GameMain.World.Remove(body);
bodies.Remove(body);
foreach (var (tileSide, otherBranch) in branch.Connections)
{
TileSide opposite = tileSide.GetOppositeSide();
otherBranch.BlockedSides &= ~opposite;
otherBranch.Sides &= ~opposite;
otherBranch.UpdateType();
if (isClient) { continue; }
// Remove branches that are not connected to anything anymore
if ((otherBranch.Type == VineTileType.Stem || otherBranch.Sides == TileSide.None) && !otherBranch.IsRoot)
{
RemoveBranch(otherBranch);
}
}
}
});
#if CLIENT
CreateDeathParticle(branch, 1.0f);
#endif
if (isClient) { return; }
int rootGrowthCount = Branches.Count(b => b.IsRootGrowth);
if (rootGrowthCount > GetDesiredRootGrowthAmount())
{
var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth);
if (rootGrowth != null)
{
RemoveBranch(rootGrowth);
}
}
if (branch.ClaimedItem != null)
{
RemoveClaim(branch.ClaimedItem);
}
if (branch.IsRoot)
{
Kill();
return;
}
#if SERVER
if (!wasRemoved && Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
#endif
}
public void RemoveClaim(Item item)
{
if (!IgnoredTargets.ContainsKey(item))
{
IgnoredTargets.Add(item, 10);
}
ClaimedTargets.Remove(item);
item.Infector = null;
foreach (var branch in Branches)
{
if (branch.ClaimedItem == item)
{
branch.ClaimedItem = null;
}
}
ClaimedJunctionBoxes.ForEachMod(jb =>
{
if (jb.Item == item)
{
ClaimedJunctionBoxes.Remove(jb);
}
});
ClaimedBatteries.ForEachMod(bat =>
{
if (bat.Item == item)
{
ClaimedBatteries.Remove(bat);
}
});
#if SERVER
if (!item.Removed && Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null));
}
#endif
}
public void Kill()
{
isDead = true;
foreach (var branch in Branches)
{
branch.DisconnectedFromRoot = true;
}
foreach (Item target in ClaimedTargets.ToList())
{
RemoveClaim(target);
target.Infector = null;
}
Debug.Assert(ClaimedTargets.Count == 0);
Debug.Assert(ClaimedJunctionBoxes.Count == 0);
Debug.Assert(ClaimedBatteries.Count == 0);
StateMachine?.State?.Exit();
#if SERVER
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new KillEventData());
}
#endif
}
public void Remove()
{
Kill();
Branches.ForEachMod(RemoveBranch);
Branches.Clear();
toBeRemoved.Clear();
Parent.BallastFlora = null;
// clean up leftover (can probably be removed)
foreach (Body body in bodies)
{
Debug.Assert(false, "Leftover bodies found after the ballast flora has died.");
GameMain.World.Remove(body);
}
_entityList.Remove(this);
#if SERVER
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new RemoveEventData());
}
#endif
}
private void BreakThrough()
{
HasBrokenThrough = true;
foreach (Body body in bodies)
{
body.Enabled = true;
}
#if CLIENT
foreach (BallastFloraBranch branch in Branches)
{
CreateShapnel(GetWorldPosition() + branch.Position);
}
SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800);
#endif
}
private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore());
private bool CollidesWithWorld(Rectangle rect, bool checkOtherBranches = true)
{
if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) { return true; }
Rectangle worldRect = rect;
worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location;
worldRect.Y -= worldRect.Height;
Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
bool hasCollision = LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
return hasCollision;
}
private static bool LineCollides(Vector2 point1, Vector2 point2)
{
const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: CustomPredicate) != null;
static bool CustomPredicate(Fixture f)
{
bool hasCollision = f.CollidesWith.HasFlag(Physics.CollisionItem);
Body body = f.Body;
if (body.UserData == null) { return false; }
switch (body.UserData)
{
case Submarine _:
case Structure _:
return hasCollision;
default:
return false;
}
}
}
}
}