From 2cbe724f29bdad5dbe4e4ef5a94f4c8007054040 Mon Sep 17 00:00:00 2001 From: mszabo Date: Fri, 1 Nov 2024 22:55:52 +0100 Subject: [PATCH] Remove LINQ from suppression code Suppression code can be fairly hot, e.g. when an explosion occurs within a large group of pawns that sends them looking for cover. Some of the relevant logic in SuppressionUtility is using LINQ heavily, which causes significant memory pressure in such scenarios. When profiling a save with a doomsday hitting a 10k point tribal raid, InterceptorZonesFor() was responsible for ~3.7% of total time despite there being no shields on the map at all. So, replace LINQ in relevant methods with iterator functions, which are less memory intensive, eliminating about ~3.5% of the total time. --- .../Projectiles/ProjectileCE.cs | 46 ++++++++--- .../CombatExtended/SuppressionUtility.cs | 77 ++++++++++++++++--- .../Compatibility/BlockerRegistry.cs | 11 ++- 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs index 0d0146dfc1..5748decb38 100644 --- a/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs +++ b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs @@ -778,6 +778,11 @@ protected bool CheckForCollisionBetween() return collided; } + /// + /// Cache field holding things that a projectile might collide with. + /// + private static readonly List potentialCollisionCandidates = new List(); + /// /// Checks whether a collision occurs along flight path within this cell. /// @@ -791,35 +796,49 @@ protected bool CheckCellForCollision(IntVec3 cell) } var roofChecked = false; - var mainThingList = new List(Map.thingGrid.ThingsListAtFast(cell)).Where(t => t is Pawn || t.def.Fillage != FillCategory.None).ToList(); + potentialCollisionCandidates.Clear(); + + foreach (var thing in Map.thingGrid.ThingsListAtFast(cell)) + { + if (thing is Pawn || thing.def.Fillage != FillCategory.None) + { + potentialCollisionCandidates.AddDistinct(thing); + } + } //Find pawns in adjacent cells and append them to main list - var adjList = new List(); var rot4 = Rot4.FromAngleFlat(shotRotation); if (rot4.rotInt > 1) { //For some reason south and west returns incorrect adjacent cells collection rot4 = rot4.Opposite; } - adjList.AddRange(GenAdj.CellsAdjacentCardinal(cell, rot4, new IntVec2(collisionCheckSize, 0)).ToList()); + if (Controller.settings.DebugDrawInterceptChecks) { Map.debugDrawer.debugCells.Clear(); Map.debugDrawer.DebugDrawerUpdate(); } //Iterate through adjacent cells and find all the pawns - foreach (var curCell in adjList) + foreach (var curCell in GenAdj.CellsAdjacentCardinal(cell, rot4, new IntVec2(collisionCheckSize, 0))) { - if (curCell != cell && curCell.InBounds(Map)) + if (curCell == cell || !curCell.InBounds(Map)) { - mainThingList.AddRange(Map.thingGrid.ThingsListAtFast(curCell) - .Where(x => x is Pawn)); + continue; + } - if (Controller.settings.DebugDrawInterceptChecks) + foreach (var thing in Map.thingGrid.ThingsListAtFast(curCell)) + { + if (thing is Pawn) { - Map.debugDrawer.FlashCell(curCell, 0.7f); + potentialCollisionCandidates.AddDistinct(thing); } } + + if (Controller.settings.DebugDrawInterceptChecks) + { + Map.debugDrawer.FlashCell(curCell, 0.7f); + } } //If the last position is above the wallCollisionHeight, we should check for roof intersections first @@ -832,8 +851,15 @@ protected bool CheckCellForCollision(IntVec3 cell) roofChecked = true; } - foreach (var thing in mainThingList.Distinct().Where(x => !(x is ProjectileCE)).OrderBy(x => (x.DrawPos - LastPos).sqrMagnitude)) + potentialCollisionCandidates.SortBy(thing => (thing.DrawPos - LastPos).sqrMagnitude); + + foreach (var thing in potentialCollisionCandidates) { + if (thing is ProjectileCE) + { + continue; + } + if ((thing == launcher || thing == mount) && !canTargetSelf) { continue; diff --git a/Source/CombatExtended/CombatExtended/SuppressionUtility.cs b/Source/CombatExtended/CombatExtended/SuppressionUtility.cs index c88cea0785..6bbbf82560 100644 --- a/Source/CombatExtended/CombatExtended/SuppressionUtility.cs +++ b/Source/CombatExtended/CombatExtended/SuppressionUtility.cs @@ -24,8 +24,6 @@ public static class SuppressionUtility private static DangerTracker dangerTracker; - private static IEnumerable Interceptors(Thing pawn) => pawn.Map.listerThings.ThingsInGroup(ThingRequestGroup.ProjectileInterceptor).Select(t => t.TryGetComp()).Where(x => x.Props.interceptNonHostileProjectiles || !x.parent.HostileTo(pawn)); - public static bool TryRequestHelp(Pawn pawn) { //TODO: 1.5 @@ -150,7 +148,7 @@ private static float GetCellCoverRatingForPawn(Pawn pawn, IntVec3 cell, IntVec3 } } - float cellRating = 0f, bonusCellRating = 1f, distToSuppressor = (pawn.Position - shooterPos).LengthHorizontal, + float cellRating = 0f, bonusCellRating = 1f, pawnHeightFactor = CE_Utility.GetCollisionBodyFactors(pawn).y, pawnVisibleOverCoverFillPercent = pawnHeightFactor * (1f - CollisionVertical.BodyRegionMiddleHeight) + 0.01f, pawnLowestCrouchFillPercent = pawnHeightFactor * CollisionVertical.BodyRegionBottomHeight + pawnVisibleOverCoverFillPercent, @@ -209,7 +207,7 @@ private static float GetCellCoverRatingForPawn(Pawn pawn, IntVec3 cell, IntVec3 cellRating += 10f - (bonusCellRating * 10f); // If the cell is covered by a shield and there are no enemies inside, then increases by 15 (for each such shield) - cellRating += InterceptorZonesFor(pawn).Where(x => !IsOccupiedByEnemies(x, pawn)).Count(x => x.Contains(cell)) * 15; + cellRating += CalculateShieldRating(cell, pawn); // Avoid bullets and other danger sources; // Yet do not discard cover that is extremely good, even if it may be dangerous @@ -245,19 +243,78 @@ private static float GetCellCoverRatingForPawn(Pawn pawn, IntVec3 cell, IntVec3 } return cellRating; } + + /// + /// Calculate the additional cover rating from shields covering the given cell. + /// + /// The cell to compute the cover rating for. + /// The pawn seeking cover. + /// The computed cover rating (15 for each shield covering the cell). + private static int CalculateShieldRating(IntVec3 cell, Pawn pawn) + { + int rating = 0; + foreach (var zone in InterceptorZonesFor(pawn)) + { + foreach (var zoneCell in zone) + { + if (zoneCell == cell) + { + if (!IsOccupiedByEnemies(zone, pawn)) + { + rating += 15; + } + + break; + } + } + } + + return rating; + } + + /// + /// Get areas covered by a shield that may be suitable for protecting the given pawn. + /// + /// The pawn seeking cover. + /// An enumerator of areas covered by shields on the map that may protect the pawn. public static IEnumerable> InterceptorZonesFor(Pawn pawn) { - var result = Interceptors(pawn).Where(x => x.Active).Select(x => GenRadial.RadialCellsAround(x.parent.Position, x.Props.radius, true)); - var compatibilityZones = BlockerRegistry.ShieldZonesCallback(pawn); - if (compatibilityZones != null) + foreach (var interceptor in pawn.Map.listerThings.ThingsInGroup(ThingRequestGroup.ProjectileInterceptor)) + { + var comp = interceptor.TryGetComp(); + if (comp.Active && (comp.Props.interceptNonHostileProjectiles || !interceptor.HostileTo(pawn))) + { + yield return GenRadial.RadialCellsAround(interceptor.Position, comp.Props.radius, true); + } + } + + foreach (var zone in BlockerRegistry.ShieldZonesCallback(pawn)) { - result = result.Union(compatibilityZones); + yield return zone; } - return result; } + + /// + /// Check whether the given area contains any objects hostile to the given pawn. + /// + /// The area to scan for hostile objects. + /// The pawn. + /// true if the area contained any hostile objects, false otherwise. private static bool IsOccupiedByEnemies(IEnumerable cells, Pawn pawn) { - return cells.Any(cell => pawn.Map.thingGrid.ThingsListAt(cell).Any(thing => (thing.HostileTo(pawn)))); + foreach (var cell in cells) + { + var things = pawn.Map.thingGrid.ThingsListAt(cell); + foreach (var thing in things) + { + if (thing.HostileTo(pawn)) + { + return true; + } + } + } + + return false; } private static float GetCoverRating(Thing cover) { diff --git a/Source/CombatExtended/Compatibility/BlockerRegistry.cs b/Source/CombatExtended/Compatibility/BlockerRegistry.cs index 5489f34c28..3418a1dbdb 100644 --- a/Source/CombatExtended/Compatibility/BlockerRegistry.cs +++ b/Source/CombatExtended/Compatibility/BlockerRegistry.cs @@ -155,9 +155,16 @@ public static IEnumerable> ShieldZonesCallback(Thing thing) { if (!enabledSZ) { - return null; + yield break; + } + + foreach (var callback in shieldZonesCallback) + { + foreach (var zone in callback(thing)) + { + yield return zone; + } } - return shieldZonesCallback.SelectMany(cb => cb(thing)); } public static bool PawnUnsuppressableFromCallback(Pawn pawn, IntVec3 origin) {