Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 242 additions & 14 deletions Radar.Pathfinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public partial class Radar
{
private Func<Vector2, Action<List<Vector2i>>, CancellationToken, Task> _addRouteAction;
private Func<Color> _getColor;

private void LoadTargets()
{
var fileText = File.ReadAllText(Path.Combine(DirectoryFullName, "targets.json"));
Expand Down Expand Up @@ -182,6 +182,56 @@ private async Task FindPath(PathFinder pf, Vector2 point, Action<List<Vector2i>>
callback(path);
}
}
private List<Vector2i> FilterByClusterSize(IReadOnlyCollection<Vector2i> positions, int clusterSize)
{
if (clusterSize <= 1) return positions.ToList();

var validClusters = new HashSet<Vector2i>();
var positionSet = new HashSet<Vector2i>(positions);

foreach (var pos in positions)
{
if (validClusters.Contains(pos)) continue;

// Flood fill to find connected component
var cluster = new HashSet<Vector2i>();
var queue = new Queue<Vector2i>();
queue.Enqueue(pos);
cluster.Add(pos);

while (queue.Count > 0)
{
var current = queue.Dequeue();

// Check neighbors at TILE distance (23 grid units)
var neighbors = new[]
{
new Vector2i(current.X + TileToGridConversion, current.Y),
new Vector2i(current.X - TileToGridConversion, current.Y),
new Vector2i(current.X, current.Y + TileToGridConversion),
new Vector2i(current.X, current.Y - TileToGridConversion),
};

foreach (var neighbor in neighbors)
{
if (positionSet.Contains(neighbor) && !cluster.Contains(neighbor))
{
cluster.Add(neighbor);
queue.Enqueue(neighbor);
}
}
}

// If cluster is large enough, add all positions in it
if (cluster.Count >= clusterSize)
{
foreach (var p in cluster)
validClusters.Add(p);
}
}

return validClusters.ToList();
}

private ConcurrentDictionary<string, List<Vector2i>> GetTargets()
{
Expand Down Expand Up @@ -259,27 +309,54 @@ private IReadOnlyCollection<Vector2i> GetLocationsFromTilePattern(string tilePat

private TargetLocations ClusterTarget(TargetDescription target)
{
// NEW: Use pattern matching if pattern is defined
if (target.ClusterPattern != null && target.ClusterPattern.Length > 0)
{
var locations = FindPatternMatches(target);
if (locations == null || locations.Count == 0)
return null;

return new TargetLocations
{
Locations = locations.Select(v => (Vector2)v).ToArray(),
Target = target,
};
}

// OLD: Fallback to original clustering for backward compatibility
var expectedCount = target.ExpectedCount;
var targetName = target.Name;
var locations = ClusterTarget(targetName, expectedCount);
if (locations == null) return null;
var clusterSize = target.ClusterSize;
var locations2 = ClusterTarget(targetName, expectedCount, clusterSize);
if (locations2 == null) return null;

return new TargetLocations
{
Locations = locations,
Locations = locations2,
Target = target,
};
}

private Vector2[] ClusterTarget(string targetName, int expectedCount)
{
var tileList = GetLocationsFromTilePattern(targetName);
if (tileList is not { Count: > 0 })
{
return null;
}

var clusterIndexes = KMeans.Cluster(tileList.Select(x => new Vector2d(x.X, x.Y)).ToArray(), expectedCount);
var resultList = new List<Vector2>();
private Vector2[] ClusterTarget(string targetName, int expectedCount, int clusterSize = 1) // ADD clusterSize parameter
{
var tileList = GetLocationsFromTilePattern(targetName);
if (tileList is not { Count: > 0 })
{
return null;
}

// ADD THIS: Filter by cluster size before KMeans clustering
if (clusterSize > 1)
{
tileList = FilterByClusterSize(tileList, clusterSize);
if (tileList.Count == 0)
{
return null;
}
}

var clusterIndexes = KMeans.Cluster(tileList.Select(x => new Vector2d(x.X, x.Y)).ToArray(), expectedCount);
var resultList = new List<Vector2>();
foreach (var tileGroup in tileList.Zip(clusterIndexes).GroupBy(x => x.Second))
{
var v = new Vector2();
Expand Down Expand Up @@ -318,7 +395,158 @@ private bool IsGridWalkable(Vector2i tile)
{
return _processedTerrainData[tile.Y][tile.X] is 5 or 4;
}
private List<Vector2i> FindPatternMatches(TargetDescription target)
{
if (target.ClusterPattern == null || target.ClusterPattern.Length == 0)
return null;



var matches = new List<Vector2i>();
var pattern = target.ClusterPattern;
var aliases = target.TileAliases ?? new Dictionary<string, string>();



// CACHE tile data for performance - read once instead of millions of times
var tileData = GameController.Memory.ReadStdVector<TileStructure>(_terrainMetadata.TgtArray);


var rotations = new[] {
pattern,
RotatePattern90(pattern),
RotatePattern90(RotatePattern90(pattern)),
RotatePattern90(RotatePattern90(RotatePattern90(pattern)))
};

// Calculate map dimensions in tiles
int maxTileX = _areaDimensions.Value.X / TileToGridConversion;
int maxTileY = _areaDimensions.Value.Y / TileToGridConversion;


// Scan the entire map
for (int tileY = 0; tileY < maxTileY; tileY++)
{
for (int tileX = 0; tileX < maxTileX; tileX++)
{
var gridPos = new Vector2i(tileX * TileToGridConversion, tileY * TileToGridConversion);

// Check if any rotation matches at this position
foreach (var rotatedPattern in rotations)
{
if (PatternMatchesAt(gridPos, rotatedPattern, aliases, tileData))
{
// Calculate center position of the matched pattern
int patternWidth = rotatedPattern[0].Length;
int patternHeight = rotatedPattern.Length;
int centerX = gridPos.X + (patternWidth * TileToGridConversion) / 2;
int centerY = gridPos.Y + (patternHeight * TileToGridConversion) / 2;
matches.Add(new Vector2i(centerX, centerY));
break; // Don't count same position multiple times with different rotations
}
}
}
}


return matches;
}
/// <summary>
/// Check if pattern matches at specific position
/// </summary>
private bool PatternMatchesAt(Vector2i startPos, string[][] pattern, Dictionary<string, string> aliases, TileStructure[] tileData)
{
int patternHeight = pattern.Length;
int patternWidth = pattern[0].Length;

// Check each cell in the pattern
for (int py = 0; py < patternHeight; py++)
{
for (int px = 0; px < patternWidth; px++)
{
var patternCell = pattern[py][px];
var gridPos = new Vector2i(
startPos.X + px * TileToGridConversion,
startPos.Y + py * TileToGridConversion
);

// Skip wildcards (null or "*")
if (patternCell == null || patternCell == "*")
continue;

// Resolve alias to full tile name
var requiredTile = aliases.ContainsKey(patternCell)
? aliases[patternCell]
: patternCell;

// Get actual tile at this position
var actualTile = GetTileAt(gridPos, tileData);



// Check if tiles match
if (actualTile != requiredTile)
return false;
}
}

return true;
}

/// <summary>
/// Rotate pattern 90 degrees clockwise
/// </summary>
private string[][] RotatePattern90(string[][] pattern)
{
int rows = pattern.Length;
int cols = pattern[0].Length;
var rotated = new string[cols][];

for (int i = 0; i < cols; i++)
{
rotated[i] = new string[rows];
for (int j = 0; j < rows; j++)
{
rotated[i][j] = pattern[rows - 1 - j][i];
}
}

return rotated;
}


/// <summary>
/// Get tile name at grid position
/// </summary>
private string GetTileAt(Vector2i gridPos, TileStructure[] tileData)
{
// Convert grid position to tile coordinates
int tileX = gridPos.X / TileToGridConversion;
int tileY = gridPos.Y / TileToGridConversion;
int tileIndex = tileY * _terrainMetadata.NumCols + tileX;

// Bounds check
if (tileIndex < 0 || tileX < 0 || tileY < 0 || tileX >= _terrainMetadata.NumCols)
return null;

try
{
if (tileIndex >= tileData.Length)
return null;

var tgtTileStruct = GameController.Memory.Read<TgtTileStruct>(tileData[tileIndex].TgtFilePtr);

// Read the TgtPath (the .tdt file path) instead of the detail name
var tilePath = tgtTileStruct.TgtPath.ToString(GameController.Memory);

return tilePath;
}
catch (Exception ex)
{

return null;
}
}
private IEnumerable<Vector2i> GetAllNeighborTiles(Vector2i start)
{
foreach (var range in Enumerable.Range(1, 100000))
Expand Down
16 changes: 12 additions & 4 deletions Radar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ public override bool Initialise()
GameController.PluginBridge.SaveMethod("Radar.LookForRoute",
(Vector2 target, Action<List<Vector2i>> callback, CancellationToken cancellationToken) =>
AddRoute(target, null, callback, cancellationToken));
GameController.PluginBridge.SaveMethod("Radar.ClusterTarget",
(string targetName, int expectedCount) => ClusterTarget(targetName, expectedCount));
GameController.PluginBridge.SaveMethod("Radar.ClusterTarget",
(string targetName, int expectedCount) => ClusterTarget(targetName, expectedCount, 1));

Input.RegisterKey(Settings.ManuallyDumpInstance.Value);
Settings.ManuallyDumpInstance.OnValueChanged += () => { Input.RegisterKey(Settings.ManuallyDumpInstance.Value); };
Expand All @@ -67,7 +67,7 @@ public override void AreaChange(AreaInstance area)
{
_targetDescriptionsInArea = GetTargetDescriptionsInArea().DistinctBy(x => x.Name).ToDictionary(x => x.Name);
_currentZoneTargetEntityPaths = _targetDescriptionsInArea.Values.Where(x => x.TargetType == TargetType.Entity).DistinctBy(x => x.Name).Select(x=>(x.Name.ToLikeRegex(), x)).ToList();
_terrainMetadata = GameController.IngameState.Data.Terrain;
_terrainMetadata = GameController.IngameState.Data.Terrain;
_heightData = GameController.IngameState.Data.RawTerrainHeightData;
_allTargetLocations = GetTargets();
_locationsByPosition = new ConcurrentDictionary<Vector2i, List<string>>(_allTargetLocations
Expand Down Expand Up @@ -157,6 +157,13 @@ public override void EntityAdded(Entity entity)
_allTargetLocations.AddOrUpdate(targetDescription.Name, _ => [truncatedPos],
// ReSharper disable once AssignmentInConditionalExpression
(_, l) => (alreadyContains = l.Contains(truncatedPos)) ? l : [..l, truncatedPos]);
if (targetDescription.ClusterSize > 1)
{
var currentLocations = _allTargetLocations[targetDescription.Name];
var filteredLocations = FilterByClusterSize(currentLocations, targetDescription.ClusterSize);
_allTargetLocations[targetDescription.Name] = filteredLocations;
alreadyContains = !filteredLocations.Contains(truncatedPos);
}
_locationsByPosition.AddOrUpdate(truncatedPos, _ => [targetDescription.Name],
(_, l) => l.Contains(targetDescription.Name) ? l : [..l, targetDescription.Name]);
if (!alreadyContains)
Expand All @@ -173,7 +180,8 @@ public override void EntityAdded(Entity entity)
}
}
}



private Vector2 GetPlayerPosition()
{
var player = GameController.Game.IngameState.Data.LocalPlayer;
Expand Down
9 changes: 8 additions & 1 deletion TargetDescription.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Radar;
using System.Collections.Generic;

namespace Radar;

public record TargetDescription
{
Expand All @@ -7,4 +9,9 @@ public record TargetDescription
public int ExpectedCount { get; set; } = 1;
public TargetType TargetType { get; set; }
public string Color { get; set; }

public string[][] ClusterPattern { get; set; }
public Dictionary<string, string> TileAliases { get; set; }

public int ClusterSize { get; set; } = 1;
}
Loading