From ced55099836a89e03731101d1626690312fb2f64 Mon Sep 17 00:00:00 2001
From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com>
Date: Tue, 5 May 2026 23:37:43 +0200
Subject: [PATCH] Add xml docs
---
src/LargeGridPathfinding/Agent.cs | 18 ++++++
src/LargeGridPathfinding/GridFiller.cs | 56 ++++++++++++++++++-
src/LargeGridPathfinding/InputHelper.cs | 6 ++
.../LargeGridPathfindingGame.cs | 45 +++++++++++++--
src/LargeGridPathfinding/Pathfinder.cs | 31 ++++++++--
src/LargeGridPathfinding/ProgressTracker.cs | 25 +++++++++
6 files changed, 169 insertions(+), 12 deletions(-)
diff --git a/src/LargeGridPathfinding/Agent.cs b/src/LargeGridPathfinding/Agent.cs
index aa0b962..cb06c87 100644
--- a/src/LargeGridPathfinding/Agent.cs
+++ b/src/LargeGridPathfinding/Agent.cs
@@ -5,15 +5,33 @@ using System.Collections.Generic;
namespace LargeGridPathfinding;
+///
+/// Represents a moving entity that follows a precomputed grid path.
+///
internal class Agent(Vector2 position)
{
+ ///
+ /// Gets or sets the current world-grid position as floating point coordinates.
+ ///
public Vector2 Position { get; set; } = position;
+ ///
+ /// Gets the integer grid cell for the current position.
+ ///
public Point GridPosition => new Point((int)Math.Round(Position.X), (int)Math.Round(Position.Y));
+ ///
+ /// Gets or sets the next waypoint currently being approached.
+ ///
public Vector2 NextPosition { get; set; }
+ ///
+ /// Gets or sets the current target destination.
+ ///
public Point? Destination { get; set; }
+ ///
+ /// Gets or sets the active path.
+ ///
public List? Path { get; set; }
}
\ No newline at end of file
diff --git a/src/LargeGridPathfinding/GridFiller.cs b/src/LargeGridPathfinding/GridFiller.cs
index e2cb58d..8845b51 100644
--- a/src/LargeGridPathfinding/GridFiller.cs
+++ b/src/LargeGridPathfinding/GridFiller.cs
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
+///
+/// Maintains grid labels/weights and generates rectangular walkable zones.
+///
public class GridFiller
{
private const int RecalculationRadius = 24;
@@ -21,6 +24,9 @@ public class GridFiller
public int Width { get; }
public int Height { get; }
+ ///
+ /// Initializes an empty grid with default tile weights.
+ ///
public GridFiller(int width, int height)
{
Width = width;
@@ -38,6 +44,9 @@ public class GridFiller
}
}
+ ///
+ /// Fills the selected region with maximal same-weight rectangles.
+ ///
public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress? totalProgress = null, IProgress? calculatingCandidatesProgress = null, IProgress? placingCandidatesProgress = null)
{
lock (mutationLock)
@@ -185,16 +194,25 @@ public class GridFiller
}
}
+ ///
+ /// Places a single obstacle rectangle.
+ ///
public void PlaceObstacle(Rectangle rectangle)
{
PlaceObstacles([rectangle]);
}
+ ///
+ /// Places obstacle rectangles and optionally recalculates affected zones.
+ ///
public void PlaceObstacles(IEnumerable rectangles, bool recalculate = true)
{
_ = PlaceObstaclesWithAffected(rectangles, recalculate);
}
+ ///
+ /// Places obstacles and returns changed zone labels.
+ ///
public HashSet PlaceObstaclesWithAffected(IEnumerable rectangles, bool recalculate = true)
{
lock (mutationLock)
@@ -266,7 +284,7 @@ public class GridFiller
Debug.WriteLine("Placed obstacle");
- // Return zones that changed (added, removed, or had their rectangle modified)
+ // Symmetric diff captures both split and merge operations from recalculation.
HashSet affected = [.. zonesBefore];
affected.SymmetricExceptWith(zonesAfter); // zones removed OR added
@@ -286,16 +304,25 @@ public class GridFiller
}
}
+ ///
+ /// Removes a single obstacle rectangle.
+ ///
public void RemoveObstacle(Rectangle rectangle)
{
RemoveObstacles([rectangle]);
}
+ ///
+ /// Removes obstacle rectangles and optionally recalculates affected zones.
+ ///
public void RemoveObstacles(IEnumerable rectangles, bool recalculate = true)
{
_ = RemoveObstaclesWithAffected(rectangles, recalculate);
}
+ ///
+ /// Removes obstacles and returns changed zone labels.
+ ///
public HashSet RemoveObstaclesWithAffected(IEnumerable rectangles, bool recalculate = true)
{
lock (mutationLock)
@@ -384,21 +411,33 @@ public class GridFiller
}
}
+ ///
+ /// Sets a single tile weight.
+ ///
public void SetTileWeight(int x, int y, int weight)
{
SetTileWeights([new Point(x, y)], weight);
}
+ ///
+ /// Resets a single tile weight to default.
+ ///
public void ResetTileWeight(int x, int y)
{
ResetTileWeights([new Point(x, y)]);
}
+ ///
+ /// Sets weights for tiles and optionally recalculates affected zones.
+ ///
public void SetTileWeights(IEnumerable points, int weight, bool recalculate = true)
{
_ = SetTileWeightsWithAffected(points, weight, recalculate);
}
+ ///
+ /// Sets weights and returns changed zone labels.
+ ///
public HashSet SetTileWeightsWithAffected(IEnumerable points, int weight, bool recalculate = true)
{
lock (mutationLock)
@@ -465,9 +504,17 @@ public class GridFiller
}
}
- public void ResetTileWeights(IEnumerable points)
+ public HashSet ResetTileWeightsWithAffected(IEnumerable points, bool recalculate = true)
{
- SetTileWeights(points, 1);
+ return SetTileWeightsWithAffected(points, 1, recalculate);
+ }
+
+ ///
+ /// Resets weights for multiple tiles to default.
+ ///
+ public void ResetTileWeights(IEnumerable points, bool recalculate = true)
+ {
+ SetTileWeights(points, 1, recalculate);
}
private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight)
@@ -648,6 +695,7 @@ public class GridFiller
if (removedRectangles.Count == 0)
{
applyChanges();
+ // Even if no existing zone was removed, edits may have opened space that needs fresh zoning.
FillGrid(left, top, right, bottom);
return;
}
@@ -657,6 +705,8 @@ public class GridFiller
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom));
+ // Expanding fill bounds to include removed rectangle extents avoids edge artifacts
+ // where old zone boundaries would otherwise leave suboptimal splits near the edit area.
applyChanges();
FillGrid(fillLeft, fillTop, fillRight, fillBottom);
}
diff --git a/src/LargeGridPathfinding/InputHelper.cs b/src/LargeGridPathfinding/InputHelper.cs
index 4849499..367dd16 100644
--- a/src/LargeGridPathfinding/InputHelper.cs
+++ b/src/LargeGridPathfinding/InputHelper.cs
@@ -5,8 +5,14 @@ using MonoGame.Extended.Input;
namespace LargeGridPathfinding;
+///
+/// Input extension helpers for game controls.
+///
internal static class InputHelper
{
+ ///
+ /// Converts WASD keyboard input into a movement vector.
+ ///
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
{
Vector2 movementDirection = Vector2.Zero;
diff --git a/src/LargeGridPathfinding/LargeGridPathfindingGame.cs b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs
index 3cdaf63..120b263 100644
--- a/src/LargeGridPathfinding/LargeGridPathfindingGame.cs
+++ b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs
@@ -15,6 +15,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
+///
+/// Main game host that handles UI, map editing, rendering, and background path updates.
+///
public class LargeGridPathfindingGame : Game
{
private enum BrushMode
@@ -42,10 +45,24 @@ public class LargeGridPathfindingGame : Game
PenalizeStretchedRectangles
}
+ ///
+ /// Supported map presets for startup generation.
+ ///
private enum StartupMapPreset
{
+ ///
+ /// Procedural room-like map with random walls and doors.
+ ///
Rooms,
+
+ ///
+ /// No generated obstacles.
+ ///
Empty,
+
+ ///
+ /// Dense 3x3 room lattice with deterministic offset doors for heavy zone fragmentation.
+ ///
WorstCase
}
@@ -78,7 +95,6 @@ public class LargeGridPathfindingGame : Game
private Point? fillSelectionCurrent;
private bool batchProcessingScheduled;
private bool startupMenuActive = true;
- private readonly bool startupInitializationStarted;
private StartupMenuItem startupSelectedItem;
private int startupGridWidth = 1000;
private int startupGridHeight = 1000;
@@ -216,21 +232,27 @@ public class LargeGridPathfindingGame : Game
case StartupMenuItem.GridWidth:
startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000);
break;
+
case StartupMenuItem.GridHeight:
startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000);
break;
+
case StartupMenuItem.AgentCount:
startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000);
break;
+
case StartupMenuItem.MapPreset:
startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase);
break;
+
case StartupMenuItem.ObstacleDivisor:
startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50);
break;
+
case StartupMenuItem.PathRandomization:
startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization);
break;
+
case StartupMenuItem.PenalizeStretchedRectangles:
startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles);
break;
@@ -515,7 +537,7 @@ public class LargeGridPathfindingGame : Game
{
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
- if (startupMenuActive && !startupInitializationStarted)
+ if (startupMenuActive)
{
uiSpriteBatch.DrawString(uiFont, "Startup Configuration", new Vector2(40, 30), Color.Black);
uiSpriteBatch.DrawString(uiFont, "Arrow Up/Down: Select Arrow Left/Right: Change Enter: Start", new Vector2(40, 60), Color.Black);
@@ -748,6 +770,9 @@ public class LargeGridPathfindingGame : Game
base.Draw(gameTime);
}
+ ///
+ /// Builds the initial grid and starts the path update worker using the startup configuration.
+ ///
private void StartInitializationTask(int width, int height, int agentCount, StartupMapPreset mapPreset, int obstacleDivisor, bool pathRandomization, bool penalizeStretchedRectangles)
{
_ = Task.Run(() =>
@@ -769,7 +794,10 @@ public class LargeGridPathfindingGame : Game
int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor));
int minObstacleSize = 5;
int minDimension = Math.Min(width, height);
+ // Using a sub-linear exponent keeps small maps close to previous behavior,
+ // but prevents obstacle size from exploding on huge maps (e.g. 10k x 10k).
int scaledByDimension = (int)Math.Round(Math.Pow(minDimension, 0.4) * 2.1);
+ // This upper cap prevents very long walls on thin maps where one dimension is small.
int maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4);
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
@@ -832,6 +860,9 @@ public class LargeGridPathfindingGame : Game
int local = x % pitch;
int baseX = x - local;
bool oddSegment = (baseX / pitch % 2) == 1;
+ // We intentionally alternate door offsets between left and right side of each room segment.
+ // If we always place centered doors, many transitions become symmetric and produce fewer
+ // distinct rectangular splits. Offsetting increases boundary complexity while staying connected.
int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // left/right offset
bool isDoor = local == offset;
@@ -862,6 +893,8 @@ public class LargeGridPathfindingGame : Game
int local = y % pitch;
int baseY = y - local;
bool oddSegment = (baseY / pitch % 2) == 1;
+ // Same idea vertically: alternating top/bottom door offsets avoids repetitive straight corridors
+ // and keeps the whole lattice navigable with a high number of zone boundaries.
int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // top/bottom offset
bool isDoor = local == offset;
@@ -912,7 +945,9 @@ public class LargeGridPathfindingGame : Game
});
}
- // Handles pathfinding calculations in a separate task
+ ///
+ /// Handles graph/path refresh work on a dedicated long-running task.
+ ///
private void StartUpdatePathTask()
{
_ = Task.Factory.StartNew(async () =>
@@ -1100,7 +1135,7 @@ public class LargeGridPathfindingGame : Game
if (resetWeightPoints.Length > 0)
{
- affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1));
+ affectedZones.UnionWith(gridFiller.ResetTileWeightsWithAffected(resetWeightPoints));
}
if (placeObstacleRectangles.Length > 0)
@@ -1182,4 +1217,4 @@ public class LargeGridPathfindingGame : Game
return [.. points];
}
-}
+}
\ No newline at end of file
diff --git a/src/LargeGridPathfinding/Pathfinder.cs b/src/LargeGridPathfinding/Pathfinder.cs
index e2c8fb9..cb6e1e9 100644
--- a/src/LargeGridPathfinding/Pathfinder.cs
+++ b/src/LargeGridPathfinding/Pathfinder.cs
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding;
+///
+/// Builds a zone adjacency graph and computes paths across rectangular zones.
+///
public class Pathfinder
{
private readonly ConcurrentDictionary rectanglesSource;
@@ -19,11 +22,17 @@ public class Pathfinder
private Dictionary rectangleMap;
private Dictionary> adjacencyList;
+ ///
+ /// Returns the current zone adjacency list.
+ ///
public Dictionary> GetAdjacencyList()
{
return adjacencyList;
}
+ ///
+ /// Returns the zone label at a grid point, or 0 when out of bounds.
+ ///
public int GetGridValue(Point gridPoint)
{
if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0))
@@ -34,6 +43,9 @@ public class Pathfinder
return grid[gridPoint.Y, gridPoint.X];
}
+ ///
+ /// Initializes a pathfinder over a mutable grid/rectangle source.
+ ///
public Pathfinder(ConcurrentDictionary rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
{
rectanglesSource = rectangles;
@@ -45,6 +57,9 @@ public class Pathfinder
adjacencyList = [];
}
+ ///
+ /// Finds a path between two grid points as coordinate waypoints.
+ ///
public List? FindPath(Point startPoint, Point goalPoint)
{
if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y))
@@ -115,6 +130,9 @@ public class Pathfinder
return null;
}
+ ///
+ /// Rebuilds the full adjacency graph from current rectangles.
+ ///
public void BuildGraph()
{
rectangleMap = new Dictionary(rectanglesSource);
@@ -131,6 +149,8 @@ public class Pathfinder
{
if (!rectangleMap.TryGetValue(rectangleLabel, out Rectangle rectangle))
{
+ // Returning a very large cost keeps stale nodes from being attractive
+ // if they slip through due to transient graph inconsistencies.
return int.MaxValue / 4;
}
@@ -247,7 +267,7 @@ public class Pathfinder
graph[id] = [];
}
- // For small graphs, use sequential algorithm (less overhead)
+ // For small graphs, sequential processing is usually faster than parallel setup/synchronization.
if (count < 50)
{
for (int i = 0; i < count; i++)
@@ -273,7 +293,7 @@ public class Pathfinder
}
else
{
- // For larger graphs, use parallelization with partitioning
+ // For larger graphs, parallel pair-checking pays off despite synchronization overhead.
object[] locks = new object[count];
for (int i = 0; i < count; i++)
{
@@ -296,7 +316,7 @@ public class Pathfinder
{
if (AreRectanglesAdjacent(rect1, rect2))
{
- // Use per-rectangle locks to minimize contention
+ // Per-index locks reduce contention compared to one global graph lock.
lock (locks[i])
{
graph[id1].Add(id2);
@@ -314,6 +334,9 @@ public class Pathfinder
return graph;
}
+ ///
+ /// Incrementally refreshes affected parts of the graph after local grid edits.
+ ///
public void IncrementalUpdateGraph(HashSet affectedZones)
{
if (affectedZones.Count == 0)
@@ -484,4 +507,4 @@ public class Pathfinder
return possibleTransitions[possibleTransitions.Count / 2];
}
-}
+}
\ No newline at end of file
diff --git a/src/LargeGridPathfinding/ProgressTracker.cs b/src/LargeGridPathfinding/ProgressTracker.cs
index 1fab90a..81dafc5 100644
--- a/src/LargeGridPathfinding/ProgressTracker.cs
+++ b/src/LargeGridPathfinding/ProgressTracker.cs
@@ -3,20 +3,32 @@ using System.Collections.Generic;
namespace LargeGridPathfinding;
+///
+/// Tracks named progress entries for long-running operations.
+///
internal class ProgressTracker
{
private readonly List progresses = [];
+ ///
+ /// Adds a determinate progress entry.
+ ///
public ProgressData AddProgress(string name, out IProgress progress)
{
return AddProgress(name, false, false, out progress);
}
+ ///
+ /// Adds an optionally indeterminate progress entry.
+ ///
public ProgressData AddProgress(string name, bool indeterminate, out IProgress progress)
{
return AddProgress(name, indeterminate, false, out progress);
}
+ ///
+ /// Adds a progress entry with full control over removal behavior.
+ ///
public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress progress)
{
CustomProgress newProgress = new CustomProgress();
@@ -33,6 +45,9 @@ internal class ProgressTracker
return progressData;
}
+ ///
+ /// Removes a previously added progress entry.
+ ///
public void RemoveProgress(ProgressData progressData)
{
lock (progresses)
@@ -41,6 +56,9 @@ internal class ProgressTracker
}
}
+ ///
+ /// Returns active progress entries.
+ ///
public IReadOnlyList GetProgresses()
{
return progresses;
@@ -50,12 +68,16 @@ internal class ProgressTracker
{
progressData.Progress = progress;
+ // Auto-removal keeps the HUD uncluttered once tasks finish.
if (progress >= 1 && !progressData.DoNotRemove)
{
RemoveProgress(progressData);
}
}
+ ///
+ /// Immutable metadata with mutable progress value for one tracked task.
+ ///
public class ProgressData(string name, bool indeterminate, bool doNotRemove)
{
public string Name { get; } = name;
@@ -64,6 +86,9 @@ internal class ProgressTracker
public bool DoNotRemove { get; } = doNotRemove;
}
+ ///
+ /// Minimal IProgress implementation exposing ProgressChanged events.
+ ///
public class CustomProgress() : IProgress
{
public EventHandler? ProgressChanged { get; set; }