using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Destructible;
using Content.Shared.Access.Components;
using Content.Shared.Doors.Components;
using Content.Shared.NPC;
using Content.Shared.Physics;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
private static readonly TimeSpan UpdateCooldown = TimeSpan.FromSeconds(0.45);
// What relevant collision groups we track for pathfinding.
// Stuff like chairs have collision but aren't relevant for mobs.
public const int PathfindingCollisionMask = (int) CollisionGroup.MobMask;
public const int PathfindingCollisionLayer = (int) CollisionGroup.MobLayer;
///
/// If true, UpdateGrid() will not process grids.
///
///
/// Useful if something like a large explosion is in the process of shredding the grid, as it avoids unneccesary
/// updating.
///
public bool PauseUpdating = false;
private readonly Stopwatch _stopwatch = new();
// Probably can't pool polys as there might be old pathfinding refs to them.
private void InitializeGrid()
{
SubscribeLocalEvent(OnGridInit);
SubscribeLocalEvent(OnGridRemoved);
SubscribeLocalEvent(OnGridPathPause);
SubscribeLocalEvent(OnGridPathShutdown);
SubscribeLocalEvent(OnCollisionChange);
SubscribeLocalEvent(OnCollisionLayerChange);
SubscribeLocalEvent(OnBodyTypeChange);
SubscribeLocalEvent(OnTileChange);
SubscribeLocalEvent(OnMoveEvent);
}
private void OnTileChange(ref TileChangedEvent ev)
{
if (ev.OldTile.IsEmpty == ev.NewTile.Tile.IsEmpty)
return;
DirtyChunk(ev.Entity, Comp(ev.Entity).GridTileToLocal(ev.NewTile.GridIndices));
}
private void OnGridPathPause(EntityUid uid, GridPathfindingComponent component, ref EntityUnpausedEvent args)
{
component.NextUpdate += args.PausedTime;
}
private void OnGridPathShutdown(EntityUid uid, GridPathfindingComponent component, ComponentShutdown args)
{
foreach (var chunk in component.Chunks)
{
// Invalidate all polygons in case there's portals or the likes.
foreach (var poly in chunk.Value.Polygons)
{
ClearTilePolys(poly);
}
}
component.DirtyChunks.Clear();
component.Chunks.Clear();
}
private void UpdateGrid()
{
if (PauseUpdating)
return;
var curTime = _timing.CurTime;
#if DEBUG
var updateCount = 0;
#endif
_stopwatch.Restart();
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
};
// We defer chunk updates because rebuilding a navmesh is hella costly
// Still run even when paused.
var query = AllEntityQuery();
while (query.MoveNext(out var uid, out var comp))
{
// TODO: Dump all this shit and just do it live it's probably fast enough.
if (comp.DirtyChunks.Count == 0 ||
curTime < comp.NextUpdate ||
!TryComp(uid, out var mapGridComp))
{
continue;
}
var dirtyPortals = comp.DirtyPortals;
dirtyPortals.Clear();
// TODO: Often we invalidate the entire chunk when it might be something as simple as an airlock change
// Would be better to handle that though this was safer and max it's taking is like 1-2ms every half-second.
var dirt = new GridPathfindingChunk[comp.DirtyChunks.Count];
var idx = 0;
foreach (var origin in comp.DirtyChunks)
{
var chunk = GetChunk(origin, uid, comp);
dirt[idx] = chunk;
idx++;
}
// We force clear portals in a single-threaded context to be safe
// as they may not be thread-safe to touch.
foreach (var chunk in dirt)
{
foreach (var (_, poly) in chunk.PortalPolys)
{
ClearPoly(poly);
}
chunk.PortalPolys.Clear();
foreach (var portal in chunk.Portals)
{
dirtyPortals.Add(portal);
}
}
// TODO: Inflate grid bounds slightly and get chunks.
// This is for map <> grid pathfinding
// Without parallel this is roughly 3x slower on my desktop.
Parallel.For(0, dirt.Length, options, i =>
{
// Doing the queries per task seems faster.
var accessQuery = GetEntityQuery();
var destructibleQuery = GetEntityQuery();
var doorQuery = GetEntityQuery();
var fixturesQuery = GetEntityQuery();
var physicsQuery = GetEntityQuery();
var xformQuery = GetEntityQuery();
BuildBreadcrumbs(dirt[i], mapGridComp, accessQuery, destructibleQuery, doorQuery, fixturesQuery,
physicsQuery, xformQuery);
});
const int Division = 4;
// You can safely do this in parallel as long as no neighbor chunks are being touched in the same iteration.
// You essentially do bottom left, bottom right, top left, top right in quadrants.
// For each 4x4 block of chunks.
// i.e. first iteration: 0,0; 2,0; 0,2
// second iteration: 1,0; 3,0; 1;2
// third iteration: 0,1; 2,1; 0,3 etc
for (var it = 0; it < Division; it++)
{
var it1 = it;
Parallel.For(0, dirt.Length, options, j =>
{
var chunk = dirt[j];
// Check if the chunk is safe on this iteration.
var x = Math.Abs(chunk.Origin.X % 2);
var y = Math.Abs(chunk.Origin.Y % 2);
var index = x * 2 + y;
if (index != it1)
return;
ClearOldPolys(chunk);
});
}
// TODO: You can probably skimp on some neighbor chunk caches
for (var it = 0; it < Division; it++)
{
var it1 = it;
Parallel.For(0, dirt.Length, options, j =>
{
var chunk = dirt[j];
// Check if the chunk is safe on this iteration.
var x = Math.Abs(chunk.Origin.X % 2);
var y = Math.Abs(chunk.Origin.Y % 2);
var index = x * 2 + y;
if (index != it1)
return;
BuildNavmesh(chunk, comp);
#if DEBUG
Interlocked.Increment(ref updateCount);
#endif
});
}
// Handle portals at the end after having cleared their neighbors above.
// We do this because there's no guarantee of where these are for chunks.
foreach (var portal in dirtyPortals)
{
var polyA = GetPoly(portal.CoordinatesA);
var polyB = GetPoly(portal.CoordinatesB);
if (polyA == null || polyB == null)
continue;
DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
var chunkA = GetChunk(polyA.ChunkOrigin, polyA.GraphUid);
var chunkB = GetChunk(polyB.ChunkOrigin, polyB.GraphUid);
chunkA.PortalPolys.TryAdd(portal, polyA);
chunkB.PortalPolys.TryAdd(portal, polyB);
AddNeighbors(polyA, polyB);
}
comp.DirtyChunks.Clear();
}
}
private bool IsBodyRelevant(FixturesComponent fixtures)
{
foreach (var fixture in fixtures.Fixtures.Values)
{
if (!fixture.Hard)
continue;
if ((fixture.CollisionMask & PathfindingCollisionLayer) != 0x0 ||
(fixture.CollisionLayer & PathfindingCollisionMask) != 0x0)
{
return true;
}
}
return false;
}
private void OnCollisionChange(ref CollisionChangeEvent ev)
{
var xform = Transform(ev.Body.Owner);
if (xform.GridUid == null)
return;
// This will also rebuild on door open / closes which I think is good?
var aabb = _lookup.GetAABBNoContainer(ev.Body.Owner, xform.Coordinates.Position, xform.LocalRotation);
DirtyChunkArea(xform.GridUid.Value, aabb);
}
private void OnCollisionLayerChange(ref CollisionLayerChangeEvent ev)
{
var xform = Transform(ev.Body.Owner);
if (xform.GridUid == null)
return;
var aabb = _lookup.GetAABBNoContainer(ev.Body.Owner, xform.Coordinates.Position, xform.LocalRotation);
DirtyChunkArea(xform.GridUid.Value, aabb);
}
private void OnBodyTypeChange(ref PhysicsBodyTypeChangedEvent ev)
{
if (TryComp(ev.Entity, out var xform) &&
xform.GridUid != null)
{
var aabb = _lookup.GetAABBNoContainer(ev.Entity, xform.Coordinates.Position, xform.LocalRotation);
DirtyChunkArea(xform.GridUid.Value, aabb);
}
}
private void OnMoveEvent(ref MoveEvent ev)
{
if (!TryComp(ev.Sender, out var fixtures) ||
!IsBodyRelevant(fixtures) ||
HasComp(ev.Sender))
{
return;
}
var gridUid = ev.Component.GridUid;
var oldGridUid = ev.OldPosition.EntityId == ev.NewPosition.EntityId
? gridUid
: ev.OldPosition.GetGridUid(EntityManager);
if (oldGridUid != null && oldGridUid != gridUid)
{
var aabb = _lookup.GetAABBNoContainer(ev.Sender, ev.OldPosition.Position, ev.OldRotation);
DirtyChunkArea(oldGridUid.Value, aabb);
}
if (gridUid != null)
{
var aabb = _lookup.GetAABBNoContainer(ev.Sender, ev.NewPosition.Position, ev.NewRotation);
DirtyChunkArea(gridUid.Value, aabb);
}
}
private void OnGridInit(GridInitializeEvent ev)
{
EnsureComp(ev.EntityUid);
// Pathfinder refactor
var mapGrid = Comp(ev.EntityUid);
for (var x = Math.Floor(mapGrid.LocalAABB.Left); x <= Math.Ceiling(mapGrid.LocalAABB.Right + ChunkSize); x += ChunkSize)
{
for (var y = Math.Floor(mapGrid.LocalAABB.Bottom); y <= Math.Ceiling(mapGrid.LocalAABB.Top + ChunkSize); y += ChunkSize)
{
DirtyChunk(ev.EntityUid, mapGrid.GridTileToLocal(new Vector2i((int) x, (int) y)));
}
}
}
private void OnGridRemoved(GridRemovalEvent ev)
{
RemComp(ev.EntityUid);
}
///
/// Queues the entire relevant chunk to be re-built in the next update.
///
private void DirtyChunk(EntityUid gridUid, EntityCoordinates coordinates)
{
if (!TryComp(gridUid, out var comp))
return;
var currentTime = _timing.CurTime;
if (comp.NextUpdate < currentTime && !MetaData(gridUid).EntityPaused)
comp.NextUpdate = currentTime + UpdateCooldown;
var chunks = comp.DirtyChunks;
// TODO: Change these args around.
chunks.Add(GetOrigin(coordinates, gridUid));
}
private void DirtyChunkArea(EntityUid gridUid, Box2 aabb)
{
if (!TryComp(gridUid, out var comp))
return;
var currentTime = _timing.CurTime;
if (comp.NextUpdate < currentTime)
comp.NextUpdate = currentTime + UpdateCooldown;
var chunks = comp.DirtyChunks;
// This assumes you never have bounds equal to or larger than 2 * ChunkSize.
var corners = new Vector2[] { aabb.BottomLeft, aabb.TopRight, aabb.BottomRight, aabb.TopLeft };
foreach (var corner in corners)
{
var sampledPoint = new Vector2i(
(int) Math.Floor((corner.X) / ChunkSize),
(int) Math.Floor((corner.Y) / ChunkSize));
chunks.Add(sampledPoint);
}
}
private GridPathfindingChunk GetChunk(Vector2i origin, EntityUid uid, GridPathfindingComponent? component = null)
{
if (!Resolve(uid, ref component))
{
throw new InvalidOperationException();
}
if (component.Chunks.TryGetValue(origin, out var chunk))
return chunk;
chunk = new GridPathfindingChunk()
{
Origin = origin,
};
component.Chunks[origin] = chunk;
return chunk;
}
private bool TryGetChunk(Vector2i origin, GridPathfindingComponent component, [NotNullWhen(true)] out GridPathfindingChunk? chunk)
{
return component.Chunks.TryGetValue(origin, out chunk);
}
private byte GetIndex(int x, int y)
{
return (byte) (x * ChunkSize + y);
}
private Vector2i GetOrigin(Vector2 localPos)
{
return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
}
private Vector2i GetOrigin(EntityCoordinates coordinates, EntityUid gridUid)
{
var gridXform = Transform(gridUid);
var localPos = gridXform.InvWorldMatrix.Transform(coordinates.ToMapPos(EntityManager));
return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
}
private void BuildBreadcrumbs(GridPathfindingChunk chunk,
MapGridComponent grid,
EntityQuery accessQuery,
EntityQuery destructibleQuery,
EntityQuery doorQuery,
EntityQuery fixturesQuery,
EntityQuery physicsQuery,
EntityQuery xformQuery)
{
var sw = new Stopwatch();
sw.Start();
var points = chunk.Points;
var gridOrigin = chunk.Origin * ChunkSize;
var tileEntities = new ValueList();
var chunkPolys = chunk.BufferPolygons;
for (var i = 0; i < chunkPolys.Length; i++)
{
chunkPolys[i].Clear();
}
var tilePolys = new ValueList(SubStep);
// Need to get the relevant polygons in each tile.
// If we wanted to create a larger navmesh we could triangulate these points but in our case we're just going
// to treat them as tile-based.
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
// Tile
var tilePos = new Vector2i(x, y) + gridOrigin;
tilePolys.Clear();
var tile = grid.GetTileRef(tilePos);
var flags = tile.Tile.IsEmpty ? PathfindingBreadcrumbFlag.Space : PathfindingBreadcrumbFlag.None;
// var isBorder = x < 0 || y < 0 || x == ChunkSize - 1 || y == ChunkSize - 1;
tileEntities.Clear();
var available = _lookup.GetEntitiesIntersecting(tile);
foreach (var ent in available)
{
// Irrelevant for pathfinding
if (!fixturesQuery.TryGetComponent(ent, out var fixtures) ||
!IsBodyRelevant(fixtures))
{
continue;
}
tileEntities.Add(ent);
}
for (var subX = 0; subX < SubStep; subX++)
{
for (var subY = 0; subY < SubStep; subY++)
{
var xOffset = x * SubStep + subX;
var yOffset = y * SubStep + subY;
// Subtile
var localPos = new Vector2(StepOffset + gridOrigin.X + x + (float) subX / SubStep, StepOffset + gridOrigin.Y + y + (float) subY / SubStep);
var collisionMask = 0x0;
var collisionLayer = 0x0;
var damage = 0f;
foreach (var ent in tileEntities)
{
if (!fixturesQuery.TryGetComponent(ent, out var fixtures))
continue;
var colliding = false;
foreach (var fixture in fixtures.Fixtures.Values)
{
// Don't need to re-do it.
if (!fixture.Hard ||
(collisionMask & fixture.CollisionMask) == fixture.CollisionMask &&
(collisionLayer & fixture.CollisionLayer) == fixture.CollisionLayer)
continue;
// Do an AABB check first as it's probably faster, then do an actual point check.
var intersects = false;
foreach (var proxy in fixture.Proxies)
{
if (!proxy.AABB.Contains(localPos))
continue;
intersects = true;
}
if (!intersects ||
!xformQuery.TryGetComponent(ent, out var xform))
{
continue;
}
if (!_fixtures.TestPoint(fixture.Shape, new Transform(xform.LocalPosition, xform.LocalRotation), localPos))
{
continue;
}
collisionLayer |= fixture.CollisionLayer;
collisionMask |= fixture.CollisionMask;
colliding = true;
}
// If entity doesn't intersect this node (e.g. thindows) then ignore it.
if (!colliding)
continue;
if (accessQuery.HasComponent(ent))
{
flags |= PathfindingBreadcrumbFlag.Access;
}
if (doorQuery.HasComponent(ent))
{
flags |= PathfindingBreadcrumbFlag.Door;
}
if (destructibleQuery.TryGetComponent(ent, out var damageable))
{
damage += _destructible.DestroyedAt(ent, damageable).Float();
}
}
if ((flags & PathfindingBreadcrumbFlag.Space) != 0x0)
{
DebugTools.Assert(tileEntities.Count == 0);
}
var crumb = new PathfindingBreadcrumb()
{
Coordinates = new Vector2i(xOffset, yOffset),
Data = new PathfindingData(flags, collisionLayer, collisionMask, damage),
};
points[xOffset, yOffset] = crumb;
}
}
// Now we got tile data and we can get the polys
var data = points[x * SubStep, y * SubStep].Data;
var start = Vector2i.Zero;
for (var i = 0; i < SubStep * SubStep; i++)
{
var ix = i / SubStep;
var iy = i % SubStep;
var nextX = (i + 1) / SubStep;
var nextY = (i + 1) % SubStep;
// End point
if (iy == SubStep - 1 ||
!points[x * SubStep + nextX, y * SubStep + nextY].Data.Equals(data))
{
tilePolys.Add(new Box2i(start, new Vector2i(ix, iy)));
if (i < (SubStep * SubStep) - 1)
{
start = new Vector2i(nextX, nextY);
data = points[x * SubStep + nextX, y * SubStep + nextY].Data;
}
}
}
// Now combine the lines
var anyCombined = true;
while (anyCombined)
{
anyCombined = false;
for (var i = 0; i < tilePolys.Count; i++)
{
var poly = tilePolys[i];
data = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
for (var j = i + 1; j < tilePolys.Count; j++)
{
var nextPoly = tilePolys[j];
var nextData = points[x * SubStep + nextPoly.Left, y * SubStep + nextPoly.Bottom].Data;
// Oh no, Combine
if (poly.Bottom == nextPoly.Bottom &&
poly.Top == nextPoly.Top &&
poly.Right + 1 == nextPoly.Left &&
data.Equals(nextData))
{
tilePolys.RemoveAt(j);
j--;
poly = new Box2i(poly.Left, poly.Bottom, poly.Right + 1, poly.Top);
anyCombined = true;
}
}
tilePolys[i] = poly;
}
}
// TODO: Can store a hash for each tile and check if the breadcrumbs match and avoid allocating these at all.
var tilePoly = chunkPolys[x * ChunkSize + y];
var polyOffset = gridOrigin + new Vector2(x, y);
foreach (var poly in tilePolys)
{
var box = new Box2((Vector2) poly.BottomLeft / SubStep + polyOffset,
(Vector2) (poly.TopRight + Vector2i.One) / SubStep + polyOffset);
var polyData = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
var neighbors = new HashSet();
tilePoly.Add(new PathPoly(grid.Owner, chunk.Origin, GetIndex(x, y), box, polyData, neighbors));
}
}
}
// _sawmill.Debug($"Built breadcrumbs in {sw.Elapsed.TotalMilliseconds}ms");
SendBreadcrumbs(chunk, grid.Owner);
}
///
/// Clears all of the polygons on a tile.
///
private void ClearTilePolys(List polys)
{
foreach (var poly in polys)
{
ClearPoly(poly);
}
polys.Clear();
}
///
/// Clears a polygon and invalidates its flags if anyone still has a reference to it.
///
private void ClearPoly(PathPoly poly)
{
foreach (var neighbor in poly.Neighbors)
{
neighbor.Neighbors.Remove(poly);
}
// If any paths have a ref to it let them know that the class is no longer a valid node.
poly.Data.Flags = PathfindingBreadcrumbFlag.Invalid;
poly.Neighbors.Clear();
}
private void ClearOldPolys(GridPathfindingChunk chunk)
{
// Can't do this in BuildBreadcrumbs because it mutates neighbors
// but also we need this entirely done before BuildNavmesh
var chunkPolys = chunk.Polygons;
var bufferPolygons = chunk.BufferPolygons;
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var index = x * ChunkSize + y;
var polys = bufferPolygons[index];
var existing = chunkPolys[index];
var isEquivalent = true;
if (polys.Count == existing.Count)
{
// May want to update damage or the likes if it's different but not invalidate the ref.
for (var i = 0; i < existing.Count; i++)
{
var ePoly = existing[i];
var poly = polys[i];
if (!ePoly.IsEquivalent(poly))
{
isEquivalent = false;
break;
}
ePoly.Data.Damage = poly.Data.Damage;
}
if (isEquivalent)
continue;
}
ClearTilePolys(existing);
existing.AddRange(polys);
}
}
}
private void BuildNavmesh(GridPathfindingChunk chunk, GridPathfindingComponent component)
{
var sw = new Stopwatch();
sw.Start();
var chunkPolys = chunk.Polygons;
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(-1, 0), out var leftChunk);
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, -1), out var bottomChunk);
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(1, 0), out var rightChunk);
component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, 1), out var topChunk);
// Now we can get the neighbors for our tile polys
for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var index = GetIndex(x, y);
var tile = chunkPolys[index];
for (byte i = 0; i < tile.Count; i++)
{
var poly = tile[i];
var enlarged = poly.Box.Enlarged(StepOffset);
// Shouldn't need to wraparound as previous neighbors would've handled us.
for (var j = (byte) (i + 1); j < tile.Count; j++)
{
var neighbor = tile[j];
var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
// Need to ensure they intersect by at least 2 tiles.
if (overlap <= 0.5f / SubStep)
continue;
AddNeighbors(poly, neighbor);
}
// TODO: Get neighbor tile polys
for (var ix = -1; ix <= 1; ix++)
{
for (var iy = -1; iy <= 1; iy++)
{
if (ix != 0 && iy != 0)
continue;
var neighborX = x + ix;
var neighborY = y + iy;
var neighborIndex = GetIndex(neighborX, neighborY);
List neighborTile;
if (neighborX < 0)
{
if (leftChunk == null)
continue;
neighborX = ChunkSize - 1;
neighborIndex = GetIndex(neighborX, neighborY);
neighborTile = leftChunk.Polygons[neighborIndex];
}
else if (neighborY < 0)
{
if (bottomChunk == null)
continue;
neighborY = ChunkSize - 1;
neighborIndex = GetIndex(neighborX, neighborY);
neighborTile = bottomChunk.Polygons[neighborIndex];
}
else if (neighborX >= ChunkSize)
{
if (rightChunk == null)
continue;
neighborX = 0;
neighborIndex = GetIndex(neighborX, neighborY);
neighborTile = rightChunk.Polygons[neighborIndex];
}
else if (neighborY >= ChunkSize)
{
if (topChunk == null)
continue;
neighborY = 0;
neighborIndex = GetIndex(neighborX, neighborY);
neighborTile = topChunk.Polygons[neighborIndex];
}
else
{
neighborTile = chunkPolys[neighborIndex];
}
for (byte j = 0; j < neighborTile.Count; j++)
{
var neighbor = neighborTile[j];
var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
// Need to ensure they intersect by at least 2 tiles.
if (overlap <= 0.5f / SubStep)
continue;
AddNeighbors(poly, neighbor);
}
}
}
}
}
}
// _sawmill.Debug($"Built navmesh in {sw.Elapsed.TotalMilliseconds}ms");
SendPolys(chunk, component.Owner, chunkPolys);
}
private void AddNeighbors(PathPoly polyA, PathPoly polyB)
{
DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
polyA.Neighbors.Add(polyB);
polyB.Neighbors.Add(polyA);
}
}