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)