Park/Areas/AreaTile.Helpers.cs
using HC3.Terrain;
using System;
namespace HC3;
public partial class AreaTile
{
/// <summary>
/// Cached boundary tile data: the tile and which edge faces outside the enclosure.
/// </summary>
public readonly record struct BoundaryInfo( AreaTile Tile, TileEdge OutwardEdge );
private static readonly Dictionary<int, List<AreaTile>> _regionTileCache = new();
private static readonly Dictionary<int, List<BoundaryInfo>> _regionBoundaryCache = new();
private static uint _cacheNavVersion;
/// <summary>
/// Gets all non-ghost enclosure AreaTiles in the given region. Cached until the grid changes.
/// </summary>
public static IReadOnlyList<AreaTile> GetTilesInRegion( int regionId )
{
EnsureCache();
if ( _regionTileCache.TryGetValue( regionId, out var list ) )
return list;
return Array.Empty<AreaTile>();
}
/// <summary>
/// Gets boundary tiles (tiles with at least one fence edge) in the given region.
/// Each entry includes which edge faces outside. Cached until the grid changes.
/// </summary>
public static IReadOnlyList<BoundaryInfo> GetBoundaryTiles( int regionId )
{
EnsureCache();
if ( _regionBoundaryCache.TryGetValue( regionId, out var list ) )
return list;
return Array.Empty<BoundaryInfo>();
}
/// <summary>
/// Finds the nearest boundary tile to a world position within a region.
/// Returns null if no boundary tiles exist.
/// </summary>
public static BoundaryInfo? FindNearestBoundary( int regionId, Vector3 worldPosition )
{
var boundaries = GetBoundaryTiles( regionId );
if ( boundaries.Count == 0 )
return null;
BoundaryInfo? best = null;
float bestDist = float.MaxValue;
bool hasStaleEntries = false;
foreach ( var info in boundaries )
{
if ( !info.Tile.IsValid() )
{
hasStaleEntries = true;
continue;
}
float dist = info.Tile.WorldPosition.DistanceSquared( worldPosition );
if ( dist < bestDist )
{
bestDist = dist;
best = info;
}
}
// If we found stale entries but no valid result, force a cache rebuild and retry
if ( best == null && hasStaleEntries )
{
InvalidateCache();
boundaries = GetBoundaryTiles( regionId );
foreach ( var info in boundaries )
{
if ( !info.Tile.IsValid() )
continue;
float dist = info.Tile.WorldPosition.DistanceSquared( worldPosition );
if ( dist < bestDist )
{
bestDist = dist;
best = info;
}
}
}
return best;
}
/// <summary>
/// Forces the cache to rebuild on next access.
/// </summary>
public static void InvalidateCache()
{
_cacheNavVersion = 0;
_regionTileCache.Clear();
_regionBoundaryCache.Clear();
}
private static void EnsureCache()
{
var navVersion = GridManager.Instance?.NavigationVersion ?? 0;
if ( _cacheNavVersion == navVersion && _regionTileCache.Count > 0 )
return;
RebuildCache( navVersion );
}
private static void RebuildCache( uint navVersion )
{
_cacheNavVersion = navVersion;
_regionTileCache.Clear();
_regionBoundaryCache.Clear();
var scene = Game.ActiveScene;
if ( scene == null )
return;
foreach ( var area in scene.GetAllComponents<AreaTile>() )
{
if ( area.GhostPreview || area.AreaType != AreaType.Enclosure )
continue;
var gridPos = GridManager.WorldToGridPosition3D( area.WorldPosition );
int regionId = GridManager.GetRegion( gridPos );
if ( regionId < 0 )
continue;
// Add to tile cache
if ( !_regionTileCache.TryGetValue( regionId, out var tileList ) )
{
tileList = new List<AreaTile>();
_regionTileCache[regionId] = tileList;
}
tileList.Add( area );
// If boundary tile, add to boundary cache
if ( area.Mask == PathMask.All )
continue;
if ( !_regionBoundaryCache.TryGetValue( regionId, out var boundaryList ) )
{
boundaryList = new List<BoundaryInfo>();
_regionBoundaryCache[regionId] = boundaryList;
}
foreach ( var edge in GridManager.AllEdges )
{
if ( !area.Mask.CanConnectTile( edge ) )
{
boundaryList.Add( new BoundaryInfo( area, edge ) );
}
}
}
}
}