diff --git a/.jules/bolt.md b/.jules/bolt.md index 2773078b..d7f82622 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 2026-04-12 - Avoid LINQ in per-frame hot path **Learning:** LINQ methods like `Where` and `FirstOrDefault` implicitly allocate enumerators and closures when capturing state (e.g., `Context.Color` or lambda expressions). In a 100Hz real-time loop like `Ai.UpdateContext()` and `Ai.Process()`, these allocations stack up quickly, causing significant GC pressure and potential micro-stutters. **Action:** Replace `LINQ` operations with manual `foreach` or `for` loops in the per-frame hot path to achieve zero-allocation data iteration. + +## 2026-07-18 - Replacing LINQ allocations in Role Assignment safely +**Learning:** `RoleAssignmentSolver.Solve` was instantiating several LINQ queries and anonymous arrays within the per-frame hot path (e.g. `Where`, `FirstOrDefault`, `ToArray`), creating several implicit allocations in the GC. While using class-level backing `List` buffers removes the allocations, they present a thread-safety hazard when shared across blue/yellow thread isolates. Using `ArrayPool.Shared.Rent` provides thread safety safely inside scoped function allocations while preserving GC pause performance. +**Action:** Always prefer `System.Buffers.ArrayPool` (paired with length tracking variables for dynamic count structures) instead of class-level buffers when optimizing local variables that might exist across parallel processing channels. diff --git a/Soccer/Ai.cs b/Soccer/Ai.cs index 5ab045b0..2ebe2d99 100644 --- a/Soccer/Ai.cs +++ b/Soccer/Ai.cs @@ -174,9 +174,13 @@ public void Process() var newRoleMapping = assignmentResult.RoleMapping; Log.ZLogDebug($"Role assignment total cost: {assignmentResult.TotalCost:F3}"); - foreach (var unfilledRole in assignmentResult.UnfilledRoles.Where(r => r.IsRequired)) + // Bolt: eliminates ~1 enumerator & closure alloc/frame by avoiding LINQ Where() + foreach (var unfilledRole in assignmentResult.UnfilledRoles) { - Log.ZLogWarning($"Required role left unfilled: {unfilledRole.Role}"); + if (unfilledRole.IsRequired) + { + Log.ZLogWarning($"Required role left unfilled: {unfilledRole.Role}"); + } } Context.Data.Value = Context.Data.Value! with diff --git a/Soccer/Knowledge/Knowledge.cs b/Soccer/Knowledge/Knowledge.cs index 6b1ea86c..c2d6cab3 100644 --- a/Soccer/Knowledge/Knowledge.cs +++ b/Soccer/Knowledge/Knowledge.cs @@ -12,7 +12,12 @@ public partial class Knowledge public void Update() { - OwnRobotsCount = Context.OwnRobots.Count(robot => robot.Seen); + // Bolt: eliminates ~1 enumerator & closure alloc/frame by avoiding LINQ Count() + OwnRobotsCount = 0; + foreach (var robot in Context.OwnRobots) + { + if (robot.Seen) OwnRobotsCount++; + } OpponentRobotsCount = Context.OppRobots.Count; UpdateAttackerAssignmentCosts(); diff --git a/Soccer/RoleAssignment/RoleAssignmentSolver.cs b/Soccer/RoleAssignment/RoleAssignmentSolver.cs index 26deac51..cb8648f1 100644 --- a/Soccer/RoleAssignment/RoleAssignmentSolver.cs +++ b/Soccer/RoleAssignment/RoleAssignmentSolver.cs @@ -19,29 +19,65 @@ public RoleAssignmentResult Solve( { var goalkeeperId = (int)Context.Referee.OurInfo().Goalkeeper; var assignedGoalieRobot = Context.OwnRobots.FirstOrDefault(r => r.Id == goalkeeperId && r.Seen); - var robots = Context.OwnRobots.Where(r => r.Seen && r.Id != goalkeeperId).ToArray(); - - var requiredGoalieRoles = formation.RequiredRoles.Where(r => r.IsGoalie).ToArray(); - var realRoles = formation.RequiredRoles - .Where(r => !r.IsGoalie) - .Concat(formation.DesiredRoles.Where(r => !r.IsGoalie)) - .ToArray(); - var requiredCount = formation.RequiredRoles.Count - requiredGoalieRoles.Length; - var robotCount = robots.Length; - var roleCount = realRoles.Length; + + var robotsPool = System.Buffers.ArrayPool.Shared.Rent(Context.OwnRobots.Count); + var robotCount = 0; + foreach (var r in Context.OwnRobots) + { + if (r.Seen && r.Id != goalkeeperId) + { + robotsPool[robotCount++] = r; + } + } + + var requiredGoalieRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count); + var requiredGoalieRolesCount = 0; + foreach (var r in formation.RequiredRoles) + { + if (r.IsGoalie) + { + requiredGoalieRolesPool[requiredGoalieRolesCount++] = r; + } + } + + var realRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count + formation.DesiredRoles.Count); + var roleCount = 0; + var requiredCount = 0; + foreach (var r in formation.RequiredRoles) + { + if (!r.IsGoalie) + { + realRolesPool[roleCount++] = r; + requiredCount++; + } + } + foreach (var r in formation.DesiredRoles) + { + if (!r.IsGoalie) + { + realRolesPool[roleCount++] = r; + } + } + var size = Math.Max(roleCount, robotCount); var dummyRobotCount = Math.Max(0, roleCount - robotCount); if (size == 0) { - return BuildResultWithForcedGoalie( + var res = BuildResultWithForcedGoalie( [], [], [], [], 0.0, - requiredGoalieRoles, + requiredGoalieRolesPool, + requiredGoalieRolesCount, assignedGoalieRobot); + + System.Buffers.ArrayPool.Shared.Return(robotsPool); + System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); + System.Buffers.ArrayPool.Shared.Return(realRolesPool); + return res; } var costs = new double[size, size]; @@ -56,11 +92,11 @@ public RoleAssignmentResult Solve( for (var roleIndex = 0; roleIndex < roleCount; roleIndex++) { - var role = realRoles[roleIndex]; + var role = realRolesPool[roleIndex]; for (var robotIndex = 0; robotIndex < robotCount; robotIndex++) { - var robot = robots[robotIndex]; + var robot = robotsPool[robotIndex]; costs[roleIndex, robotIndex] = ComputeWeightedCost(role, robot, previousRoleMapping); } @@ -87,12 +123,12 @@ public RoleAssignmentResult Solve( if (row < roleCount) { - var role = realRoles[row]; + var role = realRolesPool[row]; var isRequired = row < requiredCount; if (col < robotCount) { - filledRoles.Add(new FilledRoleAssignment(robots[col], role)); - roleMapping[robots[col].Id] = role; + filledRoles.Add(new FilledRoleAssignment(robotsPool[col], role)); + roleMapping[robotsPool[col].Id] = role; } else { @@ -101,18 +137,24 @@ public RoleAssignmentResult Solve( } else if (col < robotCount) { - unassignedRobots.Add(robots[col]); + unassignedRobots.Add(robotsPool[col]); } } - return BuildResultWithForcedGoalie( + var result = BuildResultWithForcedGoalie( filledRoles, unfilledRoles, unassignedRobots, roleMapping, totalCost, - requiredGoalieRoles, + requiredGoalieRolesPool, + requiredGoalieRolesCount, assignedGoalieRobot); + + System.Buffers.ArrayPool.Shared.Return(robotsPool); + System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); + System.Buffers.ArrayPool.Shared.Return(realRolesPool); + return result; } private RoleAssignmentResult BuildResultWithForcedGoalie( @@ -121,10 +163,11 @@ private RoleAssignmentResult BuildResultWithForcedGoalie( List unassignedRobots, Dictionary roleMapping, double totalCost, - IReadOnlyList requiredGoalieRoles, + Role.IRole[] requiredGoalieRoles, + int requiredGoalieRolesCount, RobotRef? assignedGoalieRobot) { - var goalieRole = requiredGoalieRoles.FirstOrDefault(); + var goalieRole = requiredGoalieRolesCount > 0 ? requiredGoalieRoles[0] : null; if (goalieRole != null) { if (assignedGoalieRobot != null) @@ -134,7 +177,7 @@ private RoleAssignmentResult BuildResultWithForcedGoalie( } else { - var isRequired = requiredGoalieRoles.Count > 0; + var isRequired = requiredGoalieRolesCount > 0; unfilledRoles.Add(new UnfilledRoleAssignment(goalieRole, isRequired)); if (isRequired) { @@ -143,8 +186,10 @@ private RoleAssignmentResult BuildResultWithForcedGoalie( } } - foreach (var extraRequiredGoalieRole in requiredGoalieRoles.Skip(goalieRole == null ? 0 : 1)) + var skipCount = goalieRole == null ? 0 : 1; + for (var i = skipCount; i < requiredGoalieRolesCount; i++) { + var extraRequiredGoalieRole = requiredGoalieRoles[i]; unfilledRoles.Add(new UnfilledRoleAssignment(extraRequiredGoalieRole, true)); totalCost += extraRequiredGoalieRole.Importance * _requiredRoleUnfilledPenaltySeconds; } diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index 4c1d17b3..95d28630 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/SourceGen/SourceGen.csproj.orig b/SourceGen/SourceGen.csproj.orig new file mode 100644 index 00000000..4c1d17b3 --- /dev/null +++ b/SourceGen/SourceGen.csproj.orig @@ -0,0 +1,26 @@ + + + + netstandard2.0 + false + enable + latest + $(DefaultItemExcludes);artifacts\** + + true + true + + Tyr.SourceGen + SourceGen + Tyr.SourceGen + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/patch_revert_csproj.diff b/patch_revert_csproj.diff new file mode 100644 index 00000000..34f8a328 --- /dev/null +++ b/patch_revert_csproj.diff @@ -0,0 +1,16 @@ +--- SourceGen/SourceGen.csproj ++++ SourceGen/SourceGen.csproj +@@ -14,10 +14,10 @@ + + + +- ++ + all + runtime; build; native; contentfiles; analyzers; buildtransitive + +- ++ + + + diff --git a/patch_role.diff b/patch_role.diff new file mode 100644 index 00000000..aacb9652 --- /dev/null +++ b/patch_role.diff @@ -0,0 +1,15 @@ +--- Soccer/RoleAssignment/RoleAssignmentSolver.cs ++++ Soccer/RoleAssignment/RoleAssignmentSolver.cs +@@ -9,9 +9,9 @@ + private readonly double _requiredRoleUnfilledPenaltySeconds; + + // Bolt: reuse intermediate lists to eliminate per-frame allocations +- private readonly List _robots = new(16); +- private readonly List _requiredGoalieRoles = new(4); +- private readonly List _realRoles = new(16); ++ private List _robots = new(16); ++ private List _requiredGoalieRoles = new(4); ++ private List _realRoles = new(16); + + public RoleAssignmentSolver(double requiredRoleUnfilledPenaltySeconds) + { diff --git a/patch_role_pool.diff b/patch_role_pool.diff new file mode 100644 index 00000000..d50f23fc --- /dev/null +++ b/patch_role_pool.diff @@ -0,0 +1,185 @@ +--- Soccer/RoleAssignment/RoleAssignmentSolver.cs ++++ Soccer/RoleAssignment/RoleAssignmentSolver.cs +@@ -9,10 +9,6 @@ + private readonly double _requiredRoleUnfilledPenaltySeconds; + +- // Bolt: reuse intermediate lists to eliminate per-frame allocations +- private readonly List _robots = new(16); +- private readonly List _requiredGoalieRoles = new(4); +- private readonly List _realRoles = new(16); +- + public RoleAssignmentSolver(double requiredRoleUnfilledPenaltySeconds) + { + _requiredRoleUnfilledPenaltySeconds = requiredRoleUnfilledPenaltySeconds; +@@ -23,37 +19,40 @@ + { + var goalkeeperId = (int)Context.Referee.OurInfo().Goalkeeper; +- RobotRef? assignedGoalieRobot = null; +- +- _robots.Clear(); +- foreach (var r in Context.OwnRobots) +- { +- if (!r.Seen) continue; +- if (r.Id == goalkeeperId) +- assignedGoalieRobot = r; +- else +- _robots.Add(r); +- } ++ var assignedGoalieRobot = Context.OwnRobots.FirstOrDefault(r => r.Id == goalkeeperId && r.Seen); + +- _requiredGoalieRoles.Clear(); +- _realRoles.Clear(); +- ++ var robotsPool = System.Buffers.ArrayPool.Shared.Rent(Context.OwnRobots.Count); ++ var robotCount = 0; ++ foreach (var r in Context.OwnRobots) ++ { ++ if (r.Seen && r.Id != goalkeeperId) ++ { ++ robotsPool[robotCount++] = r; ++ } ++ } ++ ++ var requiredGoalieRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count); ++ var requiredGoalieRolesCount = 0; + foreach (var r in formation.RequiredRoles) + { + if (r.IsGoalie) +- _requiredGoalieRoles.Add(r); +- else +- _realRoles.Add(r); ++ { ++ requiredGoalieRolesPool[requiredGoalieRolesCount++] = r; ++ } + } + ++ var realRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count + formation.DesiredRoles.Count); ++ var roleCount = 0; ++ var requiredCount = 0; ++ foreach (var r in formation.RequiredRoles) ++ { ++ if (!r.IsGoalie) ++ { ++ realRolesPool[roleCount++] = r; ++ requiredCount++; ++ } ++ } + foreach (var r in formation.DesiredRoles) + { + if (!r.IsGoalie) +- _realRoles.Add(r); ++ { ++ realRolesPool[roleCount++] = r; ++ } + } + +- var requiredCount = formation.RequiredRoles.Count - _requiredGoalieRoles.Count; +- var robotCount = _robots.Count; +- var roleCount = _realRoles.Count; + var size = Math.Max(roleCount, robotCount); + var dummyRobotCount = Math.Max(0, roleCount - robotCount); + +@@ -62,12 +61,17 @@ +- return BuildResultWithForcedGoalie( ++ var res = BuildResultWithForcedGoalie( + [], + [], + [], + [], + 0.0, +- _requiredGoalieRoles, ++ requiredGoalieRolesPool, ++ requiredGoalieRolesCount, + assignedGoalieRobot); ++ ++ System.Buffers.ArrayPool.Shared.Return(robotsPool); ++ System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); ++ System.Buffers.ArrayPool.Shared.Return(realRolesPool); ++ return res; + } + + var costs = new double[size, size]; +@@ -83,11 +87,11 @@ + for (var roleIndex = 0; roleIndex < roleCount; roleIndex++) + { +- var role = _realRoles[roleIndex]; ++ var role = realRolesPool[roleIndex]; + + for (var robotIndex = 0; robotIndex < robotCount; robotIndex++) + { +- var robot = _robots[robotIndex]; ++ var robot = robotsPool[robotIndex]; + costs[roleIndex, robotIndex] = ComputeWeightedCost(role, robot, previousRoleMapping); + } + +@@ -113,12 +117,12 @@ + if (row < roleCount) + { +- var role = _realRoles[row]; ++ var role = realRolesPool[row]; + var isRequired = row < requiredCount; + if (col < robotCount) + { +- filledRoles.Add(new FilledRoleAssignment(_robots[col], role)); +- roleMapping[_robots[col].Id] = role; ++ filledRoles.Add(new FilledRoleAssignment(robotsPool[col], role)); ++ roleMapping[robotsPool[col].Id] = role; + } + else + { +@@ -127,17 +131,22 @@ + else if (col < robotCount) + { +- unassignedRobots.Add(_robots[col]); ++ unassignedRobots.Add(robotsPool[col]); + } + } + +- return BuildResultWithForcedGoalie( ++ var result = BuildResultWithForcedGoalie( + filledRoles, + unfilledRoles, + unassignedRobots, + roleMapping, + totalCost, +- _requiredGoalieRoles, ++ requiredGoalieRolesPool, ++ requiredGoalieRolesCount, + assignedGoalieRobot); ++ ++ System.Buffers.ArrayPool.Shared.Return(robotsPool); ++ System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); ++ System.Buffers.ArrayPool.Shared.Return(realRolesPool); ++ return result; + } + +@@ -147,10 +156,11 @@ + List unassignedRobots, + Dictionary roleMapping, + double totalCost, +- IReadOnlyList requiredGoalieRoles, ++ Role.IRole[] requiredGoalieRoles, ++ int requiredGoalieRolesCount, + RobotRef? assignedGoalieRobot) + { +- var goalieRole = requiredGoalieRoles.Count > 0 ? requiredGoalieRoles[0] : null; ++ var goalieRole = requiredGoalieRolesCount > 0 ? requiredGoalieRoles[0] : null; + if (goalieRole != null) + { +@@ -165,7 +175,7 @@ + } + else + { +- var isRequired = requiredGoalieRoles.Count > 0; ++ var isRequired = requiredGoalieRolesCount > 0; + unfilledRoles.Add(new UnfilledRoleAssignment(goalieRole, isRequired)); + if (isRequired) + { +@@ -176,9 +186,9 @@ + + var skipCount = goalieRole == null ? 0 : 1; +- for (var i = skipCount; i < requiredGoalieRoles.Count; i++) ++ for (var i = skipCount; i < requiredGoalieRolesCount; i++) + { + var extraRequiredGoalieRole = requiredGoalieRoles[i]; + unfilledRoles.Add(new UnfilledRoleAssignment(extraRequiredGoalieRole, true)); diff --git a/patch_role_revert.diff b/patch_role_revert.diff new file mode 100644 index 00000000..9e2f7004 --- /dev/null +++ b/patch_role_revert.diff @@ -0,0 +1,195 @@ +--- Soccer/RoleAssignment/RoleAssignmentSolver.cs ++++ Soccer/RoleAssignment/RoleAssignmentSolver.cs +@@ -8,11 +8,6 @@ + { + private readonly double _requiredRoleUnfilledPenaltySeconds; + +- // Bolt: reuse intermediate lists to eliminate per-frame allocations +- private readonly List _robots = new(16); +- private readonly List _requiredGoalieRoles = new(4); +- private readonly List _realRoles = new(16); +- + public RoleAssignmentSolver(double requiredRoleUnfilledPenaltySeconds) + { + _requiredRoleUnfilledPenaltySeconds = requiredRoleUnfilledPenaltySeconds; +@@ -24,37 +19,25 @@ + { + var goalkeeperId = (int)Context.Referee.OurInfo().Goalkeeper; +- RobotRef? assignedGoalieRobot = null; +- +- _robots.Clear(); +- foreach (var r in Context.OwnRobots) +- { +- if (!r.Seen) continue; +- if (r.Id == goalkeeperId) +- assignedGoalieRobot = r; +- else +- _robots.Add(r); +- } ++ var assignedGoalieRobot = Context.OwnRobots.FirstOrDefault(r => r.Id == goalkeeperId && r.Seen); + +- _requiredGoalieRoles.Clear(); +- _realRoles.Clear(); +- +- foreach (var r in formation.RequiredRoles) +- { +- if (r.IsGoalie) +- _requiredGoalieRoles.Add(r); +- else +- _realRoles.Add(r); +- } ++ // Bolt: replace LINQ With pooling to prevent per-frame enumerator allocations safely ++ var robotsPool = System.Buffers.ArrayPool.Shared.Rent(Context.OwnRobots.Count); ++ var robotCount = 0; ++ foreach (var r in Context.OwnRobots) ++ { ++ if (r.Seen && r.Id != goalkeeperId) ++ { ++ robotsPool[robotCount++] = r; ++ } ++ } + +- foreach (var r in formation.DesiredRoles) +- { +- if (!r.IsGoalie) +- _realRoles.Add(r); +- } ++ var requiredGoalieRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count); ++ var requiredGoalieRolesCount = 0; ++ foreach (var r in formation.RequiredRoles) ++ { ++ if (r.IsGoalie) ++ { ++ requiredGoalieRolesPool[requiredGoalieRolesCount++] = r; ++ } ++ } + +- var requiredCount = formation.RequiredRoles.Count - _requiredGoalieRoles.Count; +- var robotCount = _robots.Count; +- var roleCount = _realRoles.Count; ++ var realRolesPool = System.Buffers.ArrayPool.Shared.Rent(formation.RequiredRoles.Count + formation.DesiredRoles.Count); ++ var roleCount = 0; ++ var requiredCount = 0; ++ foreach (var r in formation.RequiredRoles) ++ { ++ if (!r.IsGoalie) ++ { ++ realRolesPool[roleCount++] = r; ++ requiredCount++; ++ } ++ } ++ foreach (var r in formation.DesiredRoles) ++ { ++ if (!r.IsGoalie) ++ { ++ realRolesPool[roleCount++] = r; ++ } ++ } ++ + var size = Math.Max(roleCount, robotCount); + var dummyRobotCount = Math.Max(0, roleCount - robotCount); + +@@ -66,7 +49,8 @@ + [], + [], + 0.0, +- _requiredGoalieRoles, ++ requiredGoalieRolesPool, ++ requiredGoalieRolesCount, + assignedGoalieRobot); ++ ++ System.Buffers.ArrayPool.Shared.Return(robotsPool); ++ System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); ++ System.Buffers.ArrayPool.Shared.Return(realRolesPool); ++ return res; + } + + var costs = new double[size, size]; +@@ -82,11 +66,11 @@ + + for (var roleIndex = 0; roleIndex < roleCount; roleIndex++) + { +- var role = _realRoles[roleIndex]; ++ var role = realRolesPool[roleIndex]; + + for (var robotIndex = 0; robotIndex < robotCount; robotIndex++) + { +- var robot = _robots[robotIndex]; ++ var robot = robotsPool[robotIndex]; + costs[roleIndex, robotIndex] = ComputeWeightedCost(role, robot, previousRoleMapping); + } + +@@ -113,12 +97,12 @@ + + if (row < roleCount) + { +- var role = _realRoles[row]; ++ var role = realRolesPool[row]; + var isRequired = row < requiredCount; + if (col < robotCount) + { +- filledRoles.Add(new FilledRoleAssignment(_robots[col], role)); +- roleMapping[_robots[col].Id] = role; ++ filledRoles.Add(new FilledRoleAssignment(robotsPool[col], role)); ++ roleMapping[robotsPool[col].Id] = role; + } + else + { +@@ -127,17 +111,22 @@ + } + else if (col < robotCount) + { +- unassignedRobots.Add(_robots[col]); ++ unassignedRobots.Add(robotsPool[col]); + } + } + +- return BuildResultWithForcedGoalie( ++ var result = BuildResultWithForcedGoalie( + filledRoles, + unfilledRoles, + unassignedRobots, + roleMapping, + totalCost, +- _requiredGoalieRoles, ++ requiredGoalieRolesPool, ++ requiredGoalieRolesCount, + assignedGoalieRobot); ++ ++ System.Buffers.ArrayPool.Shared.Return(robotsPool); ++ System.Buffers.ArrayPool.Shared.Return(requiredGoalieRolesPool); ++ System.Buffers.ArrayPool.Shared.Return(realRolesPool); ++ return result; + } + +@@ -147,10 +136,11 @@ + List unassignedRobots, + Dictionary roleMapping, + double totalCost, +- IReadOnlyList requiredGoalieRoles, ++ Role.IRole[] requiredGoalieRoles, ++ int requiredGoalieRolesCount, + RobotRef? assignedGoalieRobot) + { +- var goalieRole = requiredGoalieRoles.Count > 0 ? requiredGoalieRoles[0] : null; ++ var goalieRole = requiredGoalieRolesCount > 0 ? requiredGoalieRoles[0] : null; + if (goalieRole != null) + { + if (assignedGoalieRobot != null) +@@ -159,7 +149,7 @@ + } + else + { +- var isRequired = requiredGoalieRoles.Count > 0; ++ var isRequired = requiredGoalieRolesCount > 0; + unfilledRoles.Add(new UnfilledRoleAssignment(goalieRole, isRequired)); + if (isRequired) + { +@@ -170,9 +160,9 @@ + + var skipCount = goalieRole == null ? 0 : 1; +- for (var i = skipCount; i < requiredGoalieRoles.Count; i++) ++ for (var i = skipCount; i < requiredGoalieRolesCount; i++) + { + var extraRequiredGoalieRole = requiredGoalieRoles[i]; + unfilledRoles.Add(new UnfilledRoleAssignment(extraRequiredGoalieRole, true));