Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/awgil/ffxiv_bossmod into wip
Browse files Browse the repository at this point in the history
  • Loading branch information
xanunderscore committed Jan 29, 2025
2 parents b1747f8 + 52205cf commit 203614e
Show file tree
Hide file tree
Showing 28 changed files with 281 additions and 170 deletions.
5 changes: 3 additions & 2 deletions BossMod/AI/AIBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ private void UpdateMovement(Actor player, Actor master, Targeting target, bool g
}
else if (misdirectionAngle != default && _naviDecision.Destination != null)
{
ctrl.NaviTargetPos = _naviDecision.NextTurn == 0 ? _naviDecision.Destination
: player.Position + (_naviDecision.Destination.Value - player.Position).Rotate(_naviDecision.NextTurn > 0 ? -misdirectionAngle : misdirectionAngle);
var turn = (_naviDecision.Destination.Value - player.Position).OrthoL().Dot((_naviDecision.NextWaypoint ?? _naviDecision.Destination).Value - _naviDecision.Destination.Value);
ctrl.NaviTargetPos = turn == 0 ? _naviDecision.Destination
: player.Position + (_naviDecision.Destination.Value - player.Position).Rotate(turn > 0 ? -misdirectionAngle : misdirectionAngle);
ctrl.AllowInterruptingCastByMovement = true;

// debug
Expand Down
55 changes: 28 additions & 27 deletions BossMod/Autorotation/MiscAI/AutoFarm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
public sealed class AutoFarm(RotationModuleManager manager, Actor player) : RotationModule(manager, player)
{
public enum Track { General, Fate, Specific }
public enum GeneralStrategy { AllowPull, FightBack, Aggressive, Passive }
public enum GeneralStrategy { FightBack, AllowPull, Aggressive, Passive }
public enum PriorityStrategy { None, Prioritize }

public static RotationModuleDefinition Definition()
{
RotationModuleDefinition res = new("Misc AI: Automatic farming", "Make sure this is ordered before standard rotation modules!", "Misc", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000);
RotationModuleDefinition res = new("Automatic targeting", "Collection of utilities to automatically target and pull mobs based on different criteria.", "AI", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000, 1, RotationModuleOrder.HighLevel);

res.Define(Track.General).As<GeneralStrategy>("General")
.AddOption(GeneralStrategy.AllowPull, "AllowPull", "Automatically engage any mobs that are in combat with player; if player is not in combat, pull new mobs")
.AddOption(GeneralStrategy.FightBack, "FightBack", "Automatically engage any mobs that are in combat with player, but don't pull new mobs")
.AddOption(GeneralStrategy.Aggressive, "Aggressive", "Aggressively pull all mobs that are not yet in combat")
.AddOption(GeneralStrategy.FightBack, "FightBack", "Automatically engage any mobs that are in combat with player, but don't pull new mobs", supportedTargets: ActionTargets.Hostile)
.AddOption(GeneralStrategy.AllowPull, "AllowPull", "Automatically engage any mobs that are in combat with player; if player is not in combat, pull new mobs", supportedTargets: ActionTargets.Hostile)
.AddOption(GeneralStrategy.Aggressive, "Aggressive", "Aggressively pull all mobs that are not yet in combat", supportedTargets: ActionTargets.Hostile)
.AddOption(GeneralStrategy.Passive, "Passive", "Do nothing");

res.Define(Track.Fate).As<PriorityStrategy>("FATE")
Expand All @@ -29,7 +29,8 @@ public static RotationModuleDefinition Definition()

public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving)
{
var generalStrategy = strategy.Option(Track.General).As<GeneralStrategy>();
var generalOpt = strategy.Option(Track.General);
var generalStrategy = generalOpt.As<GeneralStrategy>();
if (generalStrategy == GeneralStrategy.Passive)
return;

Expand All @@ -40,17 +41,17 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
_ => false
};

Actor? closestTargetToSwitchTo = null; // non-null if we bump any priorities
float closestTargetDistSq = float.MaxValue;
Actor? switchTarget = null; // non-null if we bump any priorities
(int, float) switchTargetKey = (0, float.MinValue); // priority and negated squared distance
void prioritize(AIHints.Enemy e, int prio)
{
e.Priority = prio;

var distSq = (e.Actor.Position - Player.Position).LengthSq();
if (distSq < closestTargetDistSq)
var key = (prio, -(e.Actor.Position - Player.Position).LengthSq());
if (key.CompareTo(switchTargetKey) > 0)
{
closestTargetToSwitchTo = e.Actor;
closestTargetDistSq = distSq;
switchTarget = e.Actor;
switchTargetKey = key;
}
}

Expand All @@ -75,25 +76,25 @@ void prioritize(AIHints.Enemy e, int prio)
}
}

// if we're not going to pull anyone, but we are already in combat and not targeting aggroed enemy, find one to target
if (closestTargetToSwitchTo == null && Player.InCombat && !(primaryTarget?.AggroPlayer ?? false))
// we are done with priority changes
// if we've updated any priorities, we need to re-sort target array
if (switchTarget != null)
{
foreach (var e in Hints.PotentialTargets)
{
if (e.Actor.AggroPlayer)
{
prioritize(e, 3);
}
}
Hints.PotentialTargets.SortByReverse(x => x.Priority);
Hints.HighestPotentialTargetPriority = Math.Max(0, Hints.PotentialTargets[0].Priority);
}

// if we have target to attack, do that
if (closestTargetToSwitchTo != null)
// if we did not select an enemy to pull, see if we can target something higher-priority than what we have now
if (switchTarget == null && Player.InCombat)
{
// if we've updated any priorities, we need to re-sort target array
Hints.PotentialTargets.SortByReverse(x => x.Priority);
Hints.HighestPotentialTargetPriority = Math.Max(0, Hints.PotentialTargets[0].Priority);
primaryTarget = Hints.ForcedTarget = closestTargetToSwitchTo;
var curTargetPrio = Hints.FindEnemy(primaryTarget)?.Priority ?? int.MinValue;
switchTarget = ResolveTargetOverride(generalOpt.Value) ?? (curTargetPrio < Hints.HighestPotentialTargetPriority ? Hints.PriorityTargets.MinBy(e => (e.Actor.Position - Player.Position).LengthSq())?.Actor : null);
}

// if we have target to switch to, do that
if (switchTarget != null)
{
primaryTarget = Hints.ForcedTarget = switchTarget;
}
}
}
94 changes: 77 additions & 17 deletions BossMod/Autorotation/MiscAI/NormalMovement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public enum SpecialModesStrategy { Automatic, Ignore }

public static RotationModuleDefinition Definition()
{
var res = new RotationModuleDefinition("Misc AI: Movement", "Automatically move character. Make sure this is ordered after standard rotation modules!", "Movement", "veyn", RotationModuleQuality.WIP, new(~0ul), 100);
var res = new RotationModuleDefinition("Automatic movement", "Automatically move character based on pathfinding or explicit coordinates.", "AI", "veyn", RotationModuleQuality.WIP, new(~0ul), 1000, 1, RotationModuleOrder.Movement);
res.Define(Track.Destination).As<DestinationStrategy>("Destination", "Destination", 30)
.AddOption(DestinationStrategy.None, "None", "No automatic movement")
.AddOption(DestinationStrategy.Pathfind, "Pathfind", "Use standard pathfinding to find best position")
Expand Down Expand Up @@ -51,7 +51,8 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
Hints.ForceCancelCast |= castStrategy == CastStrategy.DropInstants;
}

if (strategy.Option(Track.SpecialModes).As<SpecialModesStrategy>() == SpecialModesStrategy.Automatic)
var allowSpecialModes = strategy.Option(Track.SpecialModes).As<SpecialModesStrategy>() == SpecialModesStrategy.Automatic;
if (allowSpecialModes)
{
if (Player.PendingKnockbacks.Count > 0)
return; // do not move if there are any unresolved knockbacks - the positions are taken at resolve time, so we might fuck things up
Expand Down Expand Up @@ -79,7 +80,8 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,

var speed = Player.FindStatus(ClassShared.SID.Sprint) != null ? 7.8f : 6;
var destinationOpt = strategy.Option(Track.Destination);
var navi = destinationOpt.As<DestinationStrategy>() switch
var destinationStrategy = destinationOpt.As<DestinationStrategy>();
var navi = destinationStrategy switch
{
DestinationStrategy.Pathfind => NavigationDecision.Build(_navCtx, World, Hints, Player, speed),
DestinationStrategy.Explicit => new() { Destination = ResolveTargetLocation(destinationOpt.Value), TimeToGoal = destinationOpt.Value.ExpireIn },
Expand Down Expand Up @@ -121,27 +123,85 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
}

var dir = navi.Destination.Value - Player.Position;
if (dir.LengthSq() > 0.01f)
var distSq = dir.LengthSq();
if (distSq <= 0.01f)
{
Hints.ForcedMovement = dir.ToVec3(Player.PosRot.Y);
// we're already very close to destination
// TODO: what should we do if forced-movement is already set to something?.. not sure who could set it, some other module?..
Hints.ForcedMovement = default;
return;
}

var maxCastTime = castStrategy switch
// we want to move somewhere, check whether we're allowed to
if (allowSpecialModes && Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Misdirection && Hints.ImminentSpecialMode.activation <= World.CurrentTime)
{
// special case for misdirection
// assume it's always fine to drop casts during misdirection (add new option to the specialmode track if it's ever not the case, i guess...)
// we have only two options really - either move to the current forced direction, or wait (and this direction will change) - so see whether moving now brings us closer to the destination
// if our destination is not the last one (turn != 0), we can only move if it will move us *further* from second-next point - otherwise we're moving towards the wall
// the tolerance angle can be inferred from following consideration: in the worst case our movement should keep us at the same distance to destination (or it can move us closer)
// so let's consider isosceles triangle with legs equal to distance to target, and base equal to distance we move over a period of time - the base angle is then our threshold
// this means that cos(threshold) = speed * dt / 2 / distance
// assuming we wanna move at least for a second, speed is standard 6, threshold of 60 degrees would be fine for distances >= 6
// for micro adjusts, if we move for 1 frame (1/60s), threshold of 60 degrees would be fine for distance 0.1, which is our typical threshold
var threshold = 30.Degrees();
var allowMovement = World.Client.ForcedMovementDirection.AlmostEqual(Angle.FromDirection(dir), threshold.Rad);
if (allowMovement && destinationStrategy == DestinationStrategy.Pathfind)
{
CastStrategy.Leeway => navi.LeewaySeconds,
CastStrategy.Explicit => castOpt.Value.ExpireIn,
CastStrategy.Greedy => float.MaxValue,
_ => 0,
};
Hints.MaxCastTime = Math.Min(Hints.MaxCastTime, maxCastTime);
Hints.ForceCancelCast |= castStrategy == CastStrategy.DropMove;
// if we have a map, we can try to see if current direction has long enough unobstructed path
// TODO: maybe just check a single closest grid cell that we would intersect if we go forward?..
allowMovement = CalculateUnobstructedPathLength(World.Client.ForcedMovementDirection) >= Math.Min(4, distSq);
}
Hints.ForcedMovement = allowMovement ? World.Client.ForcedMovementDirection.ToDirection().ToVec3(Player.PosRot.Y) : default;

//var halfThreshold = Hints.MisdirectionThreshold; // even much smaller threshold seems to work fine in practice (TODO: reconsider...)
//var idealDir = Angle.FromDirection(dir);
//if (destinationStrategy == DestinationStrategy.Pathfind)
//{
// var lenL = CalculateUnobstructedPathLength(idealDir + halfThreshold);
// var lenR = CalculateUnobstructedPathLength(idealDir - halfThreshold);
// if (lenL < 4)
// idealDir -= halfThreshold;
// if (lenR < 4)
// idealDir += halfThreshold;
//}
//var withinThreshold = World.Client.ForcedMovementDirection.AlmostEqual(idealDir, halfThreshold.Rad);
//Hints.ForcedMovement = withinThreshold ? World.Client.ForcedMovementDirection.ToDirection().ToVec3(Player.PosRot.Y) : default;
}
else
{
// we're already very close to destination
// TODO: what should we do if forced-movement is already set to something?.. not sure who could set it, some other module?..
Hints.ForcedMovement = default;
// fine to move if we won't interrupt cast (or are explicitly allowed to)
var allowMovement = Player.CastInfo == null || Player.CastInfo.EventHappened || castStrategy is CastStrategy.DropMove or CastStrategy.DropInstants;
Hints.ForcedMovement = allowMovement ? dir.ToVec3(Player.PosRot.Y) : default;
}

// TODO: misdirection support
var maxCastTime = castStrategy switch
{
CastStrategy.Leeway => navi.LeewaySeconds,
CastStrategy.Explicit => castOpt.Value.ExpireIn,
CastStrategy.Greedy => float.MaxValue,
_ => 0,
};
Hints.MaxCastTime = Math.Min(Hints.MaxCastTime, maxCastTime);
Hints.ForceCancelCast |= castStrategy == CastStrategy.DropMove;
}

private float CalculateUnobstructedPathLength(Angle dir)
{
var start = _navCtx.Map.WorldToGrid(Player.Position);
if (!_navCtx.Map.InBounds(start.x, start.y))
return 0;

var end = _navCtx.Map.WorldToGrid(Player.Position + 100 * dir.ToDirection());
var startG = _navCtx.Map.PixelMaxG[_navCtx.Map.GridToIndex(start.x, start.y)];
foreach (var p in _navCtx.Map.EnumeratePixelsInLine(start.x, start.y, end.x, end.y))
{
if (!_navCtx.Map.InBounds(p.x, p.y) || _navCtx.Map.PixelMaxG[_navCtx.Map.GridToIndex(p.x, p.y)] < startG)
{
var dest = _navCtx.Map.GridToWorld(p.x, p.y, 0.5f, 0.5f);
return (dest - Player.Position).LengthSq();
}
}
return float.MaxValue;
}
}
2 changes: 1 addition & 1 deletion BossMod/Autorotation/MiscAI/StayCloseToTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum RangeDefinition

public static RotationModuleDefinition Definition()
{
RotationModuleDefinition def = new("Misc AI: Stay within range of target", "Module for use by AutoDuty preset.", "Misc", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000);
RotationModuleDefinition def = new("Misc AI: Stay within range of target", "Module for use by AutoDuty preset.", "AI", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000);

var configRef = def.Define(Tracks.Range).As<RangeDefinition>("range");

Expand Down
11 changes: 4 additions & 7 deletions BossMod/Autorotation/MiscAI/StayWithinLeylines.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using static BossMod.ActionQueue;

namespace BossMod.Autorotation.MiscAI;
namespace BossMod.Autorotation.MiscAI;

public sealed class StayWithinLeylines(RotationModuleManager manager, Actor player) : RotationModule(manager, player)
{
Expand All @@ -24,7 +22,7 @@ public enum BetweenTheLinesDefinition

public static RotationModuleDefinition Definition()
{
RotationModuleDefinition def = new("Misc AI: Stay within leylines when active", "Black Mage utility module.", "Misc", "Taurenkey", RotationModuleQuality.Basic, BitMask.Build(Class.BLM), 1000);
RotationModuleDefinition def = new("Misc AI: Stay within leylines when active", "Black Mage utility module.", "AI", "Taurenkey", RotationModuleQuality.Basic, BitMask.Build(Class.BLM), 1000);

var retrace = def.Define(Tracks.UseRetrace).As<RetraceDefinition>("Use Retrace", "Use Retrace");
retrace.AddOption(RetraceDefinition.No, "No");
Expand Down Expand Up @@ -55,13 +53,12 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,

//BTL first, followed by retrace, then walk
if (btlStrat == BetweenTheLinesDefinition.Yes && ActionUnlocked(btl) && btlCd.HasValue && World.Client.Cooldowns[btlCd.Value].Elapsed <= 2f && !isMoving)
Hints.ActionsToExecute.Push(btl, Player, Priority.Low, targetPos: zone.PosRot.XYZ());
Hints.ActionsToExecute.Push(btl, Player, ActionQueue.Priority.Low, targetPos: zone.PosRot.XYZ());
else if (retraceStrat == RetraceDefinition.Yes && ActionUnlocked(retrace) && retraceCd.HasValue && World.Client.Cooldowns[retraceCd.Value].Elapsed <= 2f && !isMoving)
Hints.ActionsToExecute.Push(retrace, null, Priority.Low, targetPos: Player.PosRot.XYZ());
Hints.ActionsToExecute.Push(retrace, null, ActionQueue.Priority.Low, targetPos: Player.PosRot.XYZ());
else
Hints.GoalZones.Add(Hints.GoalSingleTarget(zone.Position, 1f));
}
}

}
}
Loading

0 comments on commit 203614e

Please sign in to comment.