diff --git a/Source/CombatExtended/CombatExtended/BlackSmokeTracker.cs b/Source/CombatExtended/CombatExtended/BlackSmokeTracker.cs
new file mode 100644
index 0000000000..e1c679278b
--- /dev/null
+++ b/Source/CombatExtended/CombatExtended/BlackSmokeTracker.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Verse;
+
+namespace CombatExtended;
+
+///
+/// Manages ticking for black smoke.
+///
+public class BlackSmokeTracker(Map map) : MapComponent(map)
+{
+ private readonly List _smoke = [];
+
+ public override void MapComponentTick()
+ {
+ Parallel.ForEach(_smoke, smoke => smoke.ParallelTick());
+
+ // Apply previously calculated smoke spread.
+ // This hopefully avoids destroying and recreating low-density smoke in the same cell within one tick.
+ for (int i = 0; i < _smoke.Count; i++)
+ {
+ _smoke[i].DoSpreadToAdjacentCells();
+ }
+ }
+
+ public void Register(Smoke smoke) => _smoke.Add(smoke);
+
+ public void Unregister(Smoke smoke) => _smoke.Remove(smoke);
+}
diff --git a/Source/CombatExtended/CombatExtended/Things/Smoke.cs b/Source/CombatExtended/CombatExtended/Things/Smoke.cs
index aa1fff385d..41513c9ce9 100755
--- a/Source/CombatExtended/CombatExtended/Things/Smoke.cs
+++ b/Source/CombatExtended/CombatExtended/Things/Smoke.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using CombatExtended.AI;
using RimWorld;
using UnityEngine;
@@ -8,14 +7,26 @@
namespace CombatExtended;
public class Smoke : Gas
{
- public const int UpdateIntervalTicks = 30;
+ public const int UpdateIntervalTicks = 60;
private const float InhalationPerSec = 0.045f * UpdateIntervalTicks / GenTicks.TicksPerRealSecond;
- private const float DensityDissipationThreshold = 3.0f;
- private const float MinSpreadDensity = 1.0f; //ensures smoke clouds don't spread to infinitely small densities. Should be lower than DensityDissipationThreshold to avoid clouds stuck indoors.
+ private const float DensityDissipationThreshold = 30.0f;
+ private const float MinSpreadDensity = 10.0f; //ensures smoke clouds don't spread to infinitely small densities. Should be lower than DensityDissipationThreshold to avoid clouds stuck indoors.
private const float MaxDensity = 12800f;
private const float BugConfusePercent = 0.15f; // Percent of MaxDensity where bugs go into confused wander
private const float LethalAirPPM = 10000f; // Level of PPM where target severity hits 100% (about 2x the WHO/FDA immediately-dangerous-to-everyone threshold).
+ private struct DensityTransfer
+ {
+ public Smoke Target;
+ public IntVec3 Position;
+ public float Amount;
+ }
+
+ ///
+ /// List of pending density transfers to neighboring cells.
+ ///
+ private List _transfers = [];
+
private float density;
private DangerTracker _dangerTracker = null;
@@ -41,15 +52,41 @@ public override string LabelNoCount
}
}
+ ///
+ /// Overridden to a fixed tick rate, this also avoids expensive checks for whether the smoke is in the viewport.
+ ///
+ public override int UpdateRateTicks => 15;
+
+ public override void SpawnSetup(Map map, bool respawningAfterLoad)
+ {
+ base.SpawnSetup(map, respawningAfterLoad);
+
+ // BlackSmokeTracker manages ticking for Smoke instances.
+ Map.GetComponent().Register(this);
+ }
+
+ public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
+ {
+ Map.GetComponent().Unregister(this);
+ base.DeSpawn(mode);
+ }
+
private bool CanMoveTo(IntVec3 pos)
{
- return
- pos.InBounds(Map)
- && (
- !pos.Filled(Map)
- || (pos.GetDoor(Map)?.Open ?? false)
- || (pos.GetFirstThing(Map) is Building_Vent vent && vent.TryGetComp().SwitchIsOn)
- );
+ Map map = Map;
+ if (!pos.InBounds(map))
+ {
+ return false;
+ }
+
+ Building edifice = pos.GetEdifice(map);
+ if (edifice?.def.Fillage != FillCategory.Full)
+ {
+ return true;
+ }
+
+ return edifice is Building_Door { Open: true } ||
+ edifice is Building_Vent vent && FlickUtility.WantsToBeOn(vent);
}
public override void TickInterval(int delta)
@@ -57,36 +94,16 @@ public override void TickInterval(int delta)
if (density > DensityDissipationThreshold) //very low density smoke clouds eventually dissipate on their own
{
destroyTick += delta;
- if (Rand.Range(0, 10) == 5)
- {
- float d = density * 0.0001f * delta;
- if (density > 300)
- {
- if (Rand.Range(0, (int)(MaxDensity)) < d)
- {
- FilthMaker.TryMakeFilth(Position, Map, ThingDefOf.Filth_Ash, 1, FilthSourceFlags.None);
- }
- }
- density -= d;
-
- }
- }
+ float dissipation = Mathf.Max(0.001f, density * 0.00001f) * delta;
- if (this.IsHashIntervalTick(UpdateIntervalTicks, delta))
- {
- if (!CanMoveTo(Position)) //cloud is in inaccessible cell, probably a recently closed door or vent. Spread to nearby cells and delete.
+ if (density > 300 && Rand.Range(0, (int)(MaxDensity)) < dissipation)
{
- SpreadToAdjacentCells();
- Destroy();
- return;
+ FilthMaker.TryMakeFilth(Position, Map, ThingDefOf.Filth_Ash, 1, FilthSourceFlags.None);
}
- if (!Position.Roofed(Map))
- {
- UpdateDensityBy(-60);
- }
- SpreadToAdjacentCells();
- ApplyHediffs();
+
+ density -= dissipation;
}
+
if (this.IsHashIntervalTick(120, delta))
{
DangerTracker?.Notify_SmokeAt(Position, density / MaxDensity);
@@ -95,93 +112,140 @@ public override void TickInterval(int delta)
base.TickInterval(delta);
}
- private void ApplyHediffs()
+ public void DoSpreadToAdjacentCells()
{
- if (!Position.InBounds(Map))
+ if (this.IsHashIntervalTick(UpdateIntervalTicks))
{
- return;
- }
+ for (int i = 0; i < _transfers.Count; i++)
+ {
+ DensityTransfer transfer = _transfers[i];
+ Smoke target = transfer.Target ?? (Smoke)GenSpawn.Spawn(CE_ThingDefOf.Gas_BlackSmoke, transfer.Position, Map);
+ TransferDensityTo(target, transfer.Amount);
+ }
- var pawns = Position.GetThingList(Map).Where(t => t is Pawn).ToList();
- var baseTargetSeverity = Mathf.Pow(density / LethalAirPPM, 1.25f);
- var baseSeverityRate = InhalationPerSec * density / MaxDensity;
+ _transfers.Clear();
+ }
+ }
- foreach (Pawn pawn in pawns)
+ public void ParallelTick()
+ {
+ if (this.IsHashIntervalTick(UpdateIntervalTicks))
{
- if (pawn.RaceProps.FleshType == FleshTypeDefOf.Insectoid)
+ if (!CanMoveTo(Position)) //cloud is in inaccessible cell, probably a recently closed door or vent. Spread to nearby cells and delete.
{
- if (density > MaxDensity * BugConfusePercent)
- {
- pawn.mindState.mentalStateHandler.TryStartMentalState(CE_MentalStateDefOf.WanderConfused);
- }
- continue;
+ destroyTick = 0;
}
- if (pawn.RaceProps.Humanlike && !pawn.IsSubhuman)
+ if (!Position.Roofed(Map))
{
- pawn.TryGetComp()?.GetTacticalComp()?.Notify_ShouldEquipGasMask(false);
+ UpdateDensityBy(-60);
}
- var sensitivity = pawn.GetStatValue(CE_StatDefOf.SmokeSensitivity);
- var breathing = PawnCapacityUtility.CalculateCapacityLevel(pawn.health.hediffSet, PawnCapacityDefOf.Breathing);
- float curSeverity = pawn.health.hediffSet.GetFirstHediffOfDef(CE_HediffDefOf.SmokeInhalation, false)?.Severity ?? 0f;
+ CalcSpreadToAdjacentCells();
+ }
+ }
+ public void ApplyHediffs(Pawn pawn)
+ {
+ var baseTargetSeverity = Mathf.Pow(density / LethalAirPPM, 1.25f);
+ var baseSeverityRate = InhalationPerSec * density / MaxDensity;
- if (breathing < 0.01f)
- {
- breathing = 0.01f;
- }
- var targetSeverity = sensitivity / breathing * baseTargetSeverity;
- if (targetSeverity > 1.5f)
+ if (pawn.RaceProps.FleshType == FleshTypeDefOf.Insectoid)
+ {
+ if (density > MaxDensity * BugConfusePercent)
{
- targetSeverity = 1.5f;
+ pawn.mindState.mentalStateHandler.TryStartMentalState(CE_MentalStateDefOf.WanderConfused);
}
- var severityDelta = targetSeverity - curSeverity;
+ return;
+ }
- bool downed = pawn.Downed;
- bool awake = pawn.Awake();
+ if (pawn.RaceProps.Humanlike && !pawn.IsSubhuman)
+ {
+ pawn.TryGetComp()?.GetTacticalComp()?.Notify_ShouldEquipGasMask(false);
+ }
+ var sensitivity = pawn.GetStatValue(CE_StatDefOf.SmokeSensitivity);
+ var breathing = PawnCapacityUtility.CalculateCapacityLevel(pawn.health.hediffSet, PawnCapacityDefOf.Breathing);
+ float curSeverity = pawn.health.hediffSet.GetFirstHediffOfDef(CE_HediffDefOf.SmokeInhalation, false)?.Severity ?? 0f;
- var severityRate = baseSeverityRate * sensitivity / breathing * Mathf.Pow(severityDelta, 1.5f);
+ if (breathing < 0.01f)
+ {
+ breathing = 0.01f;
+ }
+ var targetSeverity = sensitivity / breathing * baseTargetSeverity;
+ if (targetSeverity > 1.5f)
+ {
+ targetSeverity = 1.5f;
+ }
- if (downed)
- {
- severityRate /= 100;
- }
+ var severityDelta = targetSeverity - curSeverity;
- if (!awake)
- {
- severityRate /= 2;
- if (curSeverity > 0.1)
- {
- RestUtility.WakeUp(pawn);
- }
- }
+ bool downed = pawn.Downed;
+ bool awake = pawn.Awake();
- if (severityRate > 0 && severityDelta > 0)
+
+ var severityRate = baseSeverityRate * sensitivity / breathing * Mathf.Pow(severityDelta, 1.5f);
+
+ if (downed)
+ {
+ severityRate /= 100;
+ }
+
+ if (!awake)
+ {
+ severityRate /= 2;
+ if (curSeverity > 0.1)
{
- HealthUtility.AdjustSeverity(pawn, CE_HediffDefOf.SmokeInhalation, severityRate);
+ RestUtility.WakeUp(pawn);
}
}
+
+ if (severityRate > 0 && severityDelta > 0)
+ {
+ HealthUtility.AdjustSeverity(pawn, CE_HediffDefOf.SmokeInhalation, severityRate);
+ }
}
- private void SpreadToAdjacentCells()
+ private void CalcSpreadToAdjacentCells()
{
- if (density >= MinSpreadDensity)
+ if (density < MinSpreadDensity)
+ {
+ return;
+ }
+
+ Map map = Map;
+ IntVec3 position = Position;
+ float curDensity = density;
+
+ foreach (IntVec3 cardinal in GenAdj.CardinalDirections.InRandomOrder())
{
- var freeCells = GenAdjFast.AdjacentCellsCardinal(Position).InRandomOrder().Where(CanMoveTo).ToList();
- foreach (var freeCell in freeCells)
+ IntVec3 freeCell = position + cardinal;
+
+ if (!CanMoveTo(freeCell))
{
- if (freeCell.GetGas(Map) is Smoke existingSmoke)
+ continue;
+ }
+
+ float transferred;
+ if (freeCell.GetGas(map) is Smoke existingSmoke)
+ {
+ transferred = (curDensity - existingSmoke.density) / 2;
+ _transfers.Add(new DensityTransfer
{
- var densityDiff = this.density - existingSmoke.density;
- TransferDensityTo(existingSmoke, densityDiff / 2);
- }
- else
+ Target = existingSmoke,
+ Amount = transferred
+ });
+ }
+ else
+ {
+ transferred = curDensity / 2;
+ _transfers.Add(new DensityTransfer
{
- var newSmokeCloud = (Smoke)GenSpawn.Spawn(CE_ThingDefOf.Gas_BlackSmoke, freeCell, Map);
- TransferDensityTo(newSmokeCloud, this.density / 2);
- }
+ Position = freeCell,
+ Amount = transferred
+ });
}
+
+ curDensity -= transferred;
}
}
diff --git a/Source/CombatExtended/Harmony/Harmony_Fire.cs b/Source/CombatExtended/Harmony/Harmony_Fire.cs
index 5e9979c227..e351889790 100755
--- a/Source/CombatExtended/Harmony/Harmony_Fire.cs
+++ b/Source/CombatExtended/Harmony/Harmony_Fire.cs
@@ -74,7 +74,7 @@ internal static void Postfix(Fire __instance)
[HarmonyPatch(typeof(Fire), "Tick")]
internal static class Harmony_Fire_Tick
{
- private const float SmokeDensityPerInterval = 900f;
+ private const int SmokeDensityPerInterval = 1800;
internal static void Postfix(Fire __instance)
{
diff --git a/Source/CombatExtended/Harmony/Harmony_GasUtility_PawnGasEffectsTickInterval.cs b/Source/CombatExtended/Harmony/Harmony_GasUtility_PawnGasEffectsTickInterval.cs
index 324a6969dc..76d7a5c56c 100755
--- a/Source/CombatExtended/Harmony/Harmony_GasUtility_PawnGasEffectsTickInterval.cs
+++ b/Source/CombatExtended/Harmony/Harmony_GasUtility_PawnGasEffectsTickInterval.cs
@@ -11,14 +11,15 @@
namespace CombatExtended.HarmonyCE;
internal static class Harmony_GasUtility
{
- ///
- /// Transpile to tell AI pawns to wear mask
- /// when exposed to toxic gas
- ///
+
[HarmonyPatch(typeof(GasUtility), nameof(GasUtility.PawnGasEffectsTickInterval))]
static class Patch_PawnGasEffectsTickInterval
{
+ ///
+ /// Transpile to tell AI pawns to wear mask
+ /// when exposed to toxic gas
+ ///
public static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
{
List codes = instructions.ToList();
@@ -46,6 +47,25 @@ public static IEnumerable Transpiler(IEnumerable
+ /// Apply black smoke health effects to a pawn.
+ ///
+ /// The pawn.
+ /// VTR delta.
+ public static void Postfix(Pawn pawn, int delta)
+ {
+ if (!(Controller.settings.SmokeEffects && pawn.Spawned &&
+ pawn.IsHashIntervalTick(GasUtility.GasCheckInterval, delta)))
+ {
+ return;
+ }
+
+ if (pawn.Position.GetGas(pawn.Map) is Smoke smoke)
+ {
+ smoke.ApplyHediffs(pawn);
+ }
+ }
}
public static void TryNotify_ShouldEquipGasMask(Pawn pawn)