Skip to content

Commit

Permalink
Merge pull request #477 from FFXIV-CombatReborn/mergeWIP
Browse files Browse the repository at this point in the history
navigationdecision stuff
  • Loading branch information
CarnifexOptimus authored Nov 29, 2024
2 parents 412b540 + 02f91dc commit bcc3e58
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 76 deletions.
5 changes: 2 additions & 3 deletions BossMod/Modules/Dawntrail/Hunt/RankS/Neyoozoteel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell)
class SapSpiller(BossModule module) : Components.GenericAOEs(module)
{
private static readonly AOEShapeCone cone = new(40, 60.Degrees());
private static readonly Angle a180 = 180.Degrees();
private static readonly Angle a90 = 90.Degrees();
private static readonly Angle a180 = 180.Degrees(), a90 = 90.Degrees();
private readonly List<AOEInstance> _aoes = [];
private static readonly HashSet<AID> castEnd = [AID.NoxiousSap2, AID.NoxiousSap3, AID.NoxiousSap4,
AID.NoxiousSap5, AID.NoxiousSap6, AID.NoxiousSap7, AID.NoxiousSap8, AID.NoxiousSap9];
Expand Down Expand Up @@ -114,7 +113,7 @@ private void AddAOEs(Angle first, Angle second, Angle third, ActorCastInfo spell

public override void OnEventCast(Actor caster, ActorCastEvent spell)
{
if (_aoes.Count > 0 && castEnd.Contains((AID)spell.Action.ID))
if (_aoes.Count != 0 && castEnd.Contains((AID)spell.Action.ID))
_aoes.RemoveAt(0);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ class ExpandingOrb(BossModule module) : Components.GenericAOEs(module)
{
private readonly List<Actor> _aoes = [];
private static readonly AOEShapeCircle circle = new(1.5f);
private int Size => (int)(NumCasts * 0.25);

public override IEnumerable<AOEInstance> ActiveAOEs(int slot, Actor actor)
{
var count = _aoes.Count;
if (count > 0)
{
var size = Math.Clamp(circle.Radius + NumCasts / 4, default, 12);
for (var i = 0; i < count; ++i)
yield return new(circle with { Radius = Math.Clamp(circle.Radius + Size, default, 12) }, _aoes[i].Position, default, WorldState.FutureTime(1.1f));
yield return new(circle with { Radius = size }, _aoes[i].Position, default, WorldState.FutureTime(1.1f));
}
}

Expand Down
91 changes: 71 additions & 20 deletions BossMod/Pathfinding/NavigationDecision.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,48 +48,93 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints
targetRadius = 1; // ensure targetRadius is at least 1 to prevent game from freezing

// TODO: skip pathfinding if there are no forbidden zones, just find closest point in circle/cone...

(Func<WPos, float> shapeDistance, DateTime activation)[] localForbiddenZones = [.. hints.ForbiddenZones];
var imminent = ImminentExplosionTime(ws.CurrentTime);
var numImminentZones = hints.ForbiddenZones.FindIndex(z => z.activation > imminent);
if (numImminentZones < 0)
numImminentZones = hints.ForbiddenZones.Count;
var len = localForbiddenZones.Length;
var numImminentZones = len;
int left = 0, right = len - 1;
while (left <= right)
{
var mid = (left + right) / 2;
if (localForbiddenZones[mid].activation <= imminent)
left = mid + 1;
else
{
numImminentZones = mid;
right = mid - 1;
}
}

// check whether player is inside each forbidden zone
var inZone = hints.ForbiddenZones.Select(f => f.shapeDistance(player.Position) <= forbiddenZoneCushion - 0.1f).ToList(); // we might have a situation where player's cell center is outside, but player is not, yet player is too close to center for navigation to work...
if (inZone.Any(inside => inside))
// Check whether player is inside each forbidden zone
var inZone = new bool[len];
var inImminentForbiddenZone = false;
var isInsideAnyZone = false;
for (var i = 0; i < len; ++i)
{
var zone = localForbiddenZones[i];
var inside = zone.shapeDistance(player.Position) <= forbiddenZoneCushion - 0.1f;
inZone[i] = inside;
if (inside)
{
isInsideAnyZone = true;
if (i < numImminentZones)
inImminentForbiddenZone = true;
}
}

if (isInsideAnyZone)
{
// we're in forbidden zone => find path to safety (and ideally to uptime zone)
// if such a path can't be found (that's always the case if we're inside imminent forbidden zone, but can also happen in other cases), try instead to find a path to safety that doesn't enter any other zones that we're not inside
// first build a map with zones that we're outside of as blockers
hints.PathfindMapBounds.PathfindMap(ctx.Map, hints.PathfindMapCenter);
foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone))
if (!inside)
for (var i = 0; i < len; ++i)
{
if (!inZone[i])
{
var zf = localForbiddenZones[i];
AddBlockerZone(ctx.Map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion);
}
}

var inImminentForbiddenZone = inZone.Take(numImminentZones).Any(inside => inside);
if (!inImminentForbiddenZone)
{
ctx.Map2.Init(ctx.Map, ctx.Map.Center);
foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone))
if (inside)
for (var i = 0; i < len; ++i)
{
if (inZone[i])
{
var zf = localForbiddenZones[i];
AddBlockerZone(ctx.Map2, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion);
}
}
var maxGoal = targetPos != null ? AddTargetGoal(ctx.Map2, targetPos.Value, targetRadius, targetRot, positional, 0) : 0;
var res = FindPathFromUnsafe(ctx.ThetaStar, ctx.Map2, player.Position, 0, maxGoal, targetPos, targetRot, positional, playerSpeed);
if (res != null)
return res.Value;

// pathfind to any spot outside aoes we're in that doesn't enter new aoes
foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone))
if (inside)
for (var i = 0; i < len; ++i)
{
if (inZone[i])
{
var zf = localForbiddenZones[i];
ctx.Map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1);
}
}
return FindPathFromImminent(ctx.ThetaStar, ctx.Map, player.Position, playerSpeed);
}
else
{
// try to find a path out of imminent aoes that we're in, while remaining in non-imminent aoes that we're already in - it might be worth it...
foreach (var (zf, inside) in hints.ForbiddenZones.Zip(inZone).Take(numImminentZones))
if (inside)
for (var i = 0; i < len; ++i)
{
if (inZone[i])
{
var zf = localForbiddenZones[i];
ctx.Map.AddGoal(zf.shapeDistance, forbiddenZoneCushion, 0, -1);
}
}
return FindPathFromImminent(ctx.ThetaStar, ctx.Map, player.Position, playerSpeed);
}
}
Expand All @@ -101,8 +146,11 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints
{
// we're not in uptime zone, just run to it, avoiding any aoes
hints.PathfindMapBounds.PathfindMap(ctx.Map, hints.PathfindMapCenter);
foreach (var (shape, activation) in hints.ForbiddenZones)
AddBlockerZone(ctx.Map, imminent, activation, shape, forbiddenZoneCushion);
for (var i = 0; i < len; ++i)
{
var zf = localForbiddenZones[i];
AddBlockerZone(ctx.Map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion);
}
var maxGoal = AddTargetGoal(ctx.Map, targetPos.Value, targetRadius, targetRot, Positional.Any, 0);
if (maxGoal != 0)
{
Expand Down Expand Up @@ -146,8 +194,11 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints
// we're in uptime zone, but not in correct quadrant - move there, avoiding all aoes and staying within uptime zone
hints.PathfindMapBounds.PathfindMap(ctx.Map, hints.PathfindMapCenter);
ctx.Map.BlockPixelsInside(ShapeDistance.InvertedCircle(targetPos.Value, targetRadius), 0, 0);
foreach (var (shape, activation) in hints.ForbiddenZones)
AddBlockerZone(ctx.Map, imminent, activation, shape, forbiddenZoneCushion);
for (var i = 0; i < len; ++i)
{
var zf = localForbiddenZones[i];
AddBlockerZone(ctx.Map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion);
}
var maxGoal = AddPositionalGoal(ctx.Map, targetPos.Value, targetRadius, targetRot, positional, 0);
if (maxGoal > 0)
{
Expand Down
103 changes: 58 additions & 45 deletions BossMod/Pathfinding/ThetaStar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public struct Node
private float _deltaGSide;
private float _deltaGDiag;
private const float Epsilon = 1e-5f;
private readonly object _lock = new();

public ref Node NodeByIndex(int index) => ref _nodes[index];
public int CellIndex(int x, int y) => y * _map.Width + x;
Expand All @@ -27,67 +28,79 @@ public struct Node
// gMultiplier is typically inverse speed, which turns g-values into time
public void Start(Map map, IEnumerable<(int x, int y)> goals, (int x, int y) start, float gMultiplier)
{
_map = map;
_goals.Clear();
_goals.AddRange(goals);
var numPixels = map.Width * map.Height;
if (_nodes.Length < numPixels)
_nodes = new Node[numPixels];
Array.Fill(_nodes, default, 0, numPixels);
_openList.Clear();
_deltaGSide = map.Resolution * gMultiplier;
_deltaGDiag = _deltaGSide * 1.414214f;
lock (_lock)
{
_map = map;
_goals.Clear();
_goals.AddRange(goals);
var numPixels = map.Width * map.Height;
if (_nodes == null || _nodes.Length < numPixels)
_nodes = new Node[numPixels];
Array.Fill(_nodes, default, 0, numPixels);
_openList.Clear();
_deltaGSide = map.Resolution * gMultiplier;
_deltaGDiag = _deltaGSide * 1.414214f;

start = map.ClampToGrid(start);
var startIndex = CellIndex(start.x, start.y);
_nodes[startIndex].GScore = 0;
_nodes[startIndex].HScore = HeuristicDistance(start.x, start.y);
_nodes[startIndex].ParentX = start.x; // start's parent is self
_nodes[startIndex].ParentY = start.y;
_nodes[startIndex].PathLeeway = float.MaxValue; // min diff along path between node's g-value and cell's g-value
AddToOpen(startIndex);
start = map.ClampToGrid(start);
var startIndex = CellIndex(start.x, start.y);
_nodes[startIndex].GScore = 0;
_nodes[startIndex].HScore = HeuristicDistance(start.x, start.y);
_nodes[startIndex].ParentX = start.x; // start's parent is self
_nodes[startIndex].ParentY = start.y;
_nodes[startIndex].PathLeeway = float.MaxValue; // min diff along path between node's g-value and cell's g-value
AddToOpen(startIndex);
}
}

public void Start(Map map, int goalPriority, WPos startPos, float gMultiplier) => Start(map, map.Goals().Where(g => g.priority >= goalPriority).Select(g => (g.x, g.y)), map.WorldToGrid(startPos), gMultiplier);

// returns whether search is to be terminated; on success, first node of the open list would contain found goal
public bool ExecuteStep()
{
if (_goals.Count == 0 || _openList.Count == 0 || _nodes[_openList[0]].HScore <= 0)
return false;

var nextNodeIndex = PopMinOpen();
var nextNodeX = nextNodeIndex % _map.Width;
var nextNodeY = nextNodeIndex / _map.Width;
var haveN = nextNodeY > 0;
var haveS = nextNodeY < _map.Height - 1;
var haveE = nextNodeX > 0;
var haveW = nextNodeX < _map.Width - 1;
if (haveN)
lock (_lock)
{
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX, nextNodeY - 1, nextNodeIndex - _map.Width, _deltaGSide);
if (_goals.Count == 0 || _openList.Count == 0 || _nodes[_openList[0]].HScore <= 0)
return false;

var nextNodeIndex = PopMinOpen();
var nextNodeX = nextNodeIndex % _map.Width;
var nextNodeY = nextNodeIndex / _map.Width;
var haveN = nextNodeY > 0;
var haveS = nextNodeY < _map.Height - 1;
var haveE = nextNodeX > 0;
var haveW = nextNodeX < _map.Width - 1;
if (haveN)
{
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX, nextNodeY - 1, nextNodeIndex - _map.Width, _deltaGSide);
if (haveE)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY - 1, nextNodeIndex - _map.Width - 1, _deltaGDiag);
if (haveW)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY - 1, nextNodeIndex - _map.Width + 1, _deltaGDiag);
}
if (haveE)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY - 1, nextNodeIndex - _map.Width - 1, _deltaGDiag);
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY, nextNodeIndex - 1, _deltaGSide);
if (haveW)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY - 1, nextNodeIndex - _map.Width + 1, _deltaGDiag);
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY, nextNodeIndex + 1, _deltaGSide);
if (haveS)
{
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX, nextNodeY + 1, nextNodeIndex + _map.Width, _deltaGSide);
if (haveE)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY + 1, nextNodeIndex + _map.Width - 1, _deltaGDiag);
if (haveW)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY + 1, nextNodeIndex + _map.Width + 1, _deltaGDiag);
}
return true;
}
if (haveE)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY, nextNodeIndex - 1, _deltaGSide);
if (haveW)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY, nextNodeIndex + 1, _deltaGSide);
if (haveS)
}

public int CurrentResult()
{
lock (_lock)
{
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX, nextNodeY + 1, nextNodeIndex + _map.Width, _deltaGSide);
if (haveE)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX - 1, nextNodeY + 1, nextNodeIndex + _map.Width - 1, _deltaGDiag);
if (haveW)
VisitNeighbour(nextNodeX, nextNodeY, nextNodeIndex, nextNodeX + 1, nextNodeY + 1, nextNodeIndex + _map.Width + 1, _deltaGDiag);
return _openList.Count > 0 && _nodes[_openList[0]].HScore <= 0 ? _openList[0] : -1;
}
return true;
}

public int CurrentResult() => _openList.Count > 0 && _nodes[_openList[0]].HScore <= 0 ? _openList[0] : -1;

public int Execute()
{
while (ExecuteStep())
Expand Down
11 changes: 5 additions & 6 deletions BossMod/ThirdParty/Clipper2/Clipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public static class Clipper
{
private const double DoublePI = 2 * Math.PI;
private const double Half = 0.5;
private const double Third = 1d / 3;

private static Rect64 invalidRect64 = new Rect64(false);
public static Rect64 InvalidRect64 => invalidRect64;
Expand Down Expand Up @@ -612,7 +611,7 @@ public static RectD GetBounds(PathsD paths)

public static Path64 MakePath(int[] arr)
{
int len = (int)(arr.Length * Half);
int len = arr.Length / 2;
Path64 p = new Path64(len);
for (int i = 0; i < len; ++i)
p.Add(new Point64(arr[i * 2], arr[i * 2 + 1]));
Expand All @@ -621,7 +620,7 @@ public static Path64 MakePath(int[] arr)

public static Path64 MakePath(long[] arr)
{
int len = (int)(arr.Length * Half);
int len = arr.Length / 2;
Path64 p = new Path64(len);
for (int i = 0; i < len; ++i)
p.Add(new Point64(arr[i * 2], arr[i * 2 + 1]));
Expand All @@ -630,7 +629,7 @@ public static Path64 MakePath(long[] arr)

public static PathD MakePath(double[] arr)
{
int len = (int)(arr.Length * Half);
int len = arr.Length / 2;
PathD p = new PathD(len);
for (int i = 0; i < len; ++i)
p.Add(new PointD(arr[i * 2], arr[i * 2 + 1]));
Expand All @@ -640,15 +639,15 @@ public static PathD MakePath(double[] arr)
#if USINGZ
public static Path64 MakePathZ(long[] arr)
{
int len = (int)(arr.Length * Third);
int len = arr.Length / 2;
Path64 p = new Path64(len);
for (int i = 0; i < len; ++i)
p.Add(new Point64(arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2]));
return p;
}
public static PathD MakePathZ(double[] arr)
{
int len = (int)(arr.Length * Third);
int len = arr.Length / 3;
PathD p = new PathD(len);
for (int i = 0; i < len; ++i)
p.Add(new PointD(arr[i * 3], arr[i * 3 + 1], (long)arr[i * 3 + 2]));
Expand Down

0 comments on commit bcc3e58

Please sign in to comment.