Skip to content

Commit

Permalink
Remove LINQ from suppression code
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mszabo-wikia committed Nov 1, 2024
1 parent 5c87933 commit 2cbe724
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 22 deletions.
46 changes: 36 additions & 10 deletions Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,11 @@ protected bool CheckForCollisionBetween()
return collided;
}

/// <summary>
/// Cache field holding things that a projectile might collide with.
/// </summary>
private static readonly List<Thing> potentialCollisionCandidates = new List<Thing>();

/// <summary>
/// Checks whether a collision occurs along flight path within this cell.
/// </summary>
Expand All @@ -791,35 +796,49 @@ protected bool CheckCellForCollision(IntVec3 cell)
}
var roofChecked = false;

var mainThingList = new List<Thing>(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<IntVec3>();
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
Expand All @@ -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;
Expand Down
77 changes: 67 additions & 10 deletions Source/CombatExtended/CombatExtended/SuppressionUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ public static class SuppressionUtility

private static DangerTracker dangerTracker;

private static IEnumerable<CompProjectileInterceptor> Interceptors(Thing pawn) => pawn.Map.listerThings.ThingsInGroup(ThingRequestGroup.ProjectileInterceptor).Select(t => t.TryGetComp<CompProjectileInterceptor>()).Where(x => x.Props.interceptNonHostileProjectiles || !x.parent.HostileTo(pawn));

public static bool TryRequestHelp(Pawn pawn)
{
//TODO: 1.5
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -245,19 +243,78 @@ private static float GetCellCoverRatingForPawn(Pawn pawn, IntVec3 cell, IntVec3
}
return cellRating;
}

/// <summary>
/// Calculate the additional cover rating from shields covering the given cell.
/// </summary>
/// <param name="cell">The cell to compute the cover rating for.</param>
/// <param name="pawn">The pawn seeking cover.</param>
/// <returns>The computed cover rating (15 for each shield covering the cell).</returns>
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;
}

/// <summary>
/// Get areas covered by a shield that may be suitable for protecting the given pawn.
/// </summary>
/// <param name="pawn">The pawn seeking cover.</param>
/// <returns>An enumerator of areas covered by shields on the map that may protect the pawn.</returns>
public static IEnumerable<IEnumerable<IntVec3>> 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<CompProjectileInterceptor>();
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;
}

/// <summary>
/// Check whether the given area contains any objects hostile to the given pawn.
/// </summary>
/// <param name="cells">The area to scan for hostile objects.</param>
/// <param name="pawn">The pawn.</param>
/// <returns>true if the area contained any hostile objects, false otherwise.</returns>
private static bool IsOccupiedByEnemies(IEnumerable<IntVec3> 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)
{
Expand Down
11 changes: 9 additions & 2 deletions Source/CombatExtended/Compatibility/BlockerRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,16 @@ public static IEnumerable<IEnumerable<IntVec3>> 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)
{
Expand Down

0 comments on commit 2cbe724

Please sign in to comment.