Skip to content

Commit

Permalink
Merge pull request #565 from xanunderscore/newprio
Browse files Browse the repository at this point in the history
extend prio system
  • Loading branch information
awgil authored Jan 18, 2025
2 parents e8d2a06 + c7bf89f commit f53690f
Show file tree
Hide file tree
Showing 15 changed files with 60 additions and 52 deletions.
3 changes: 3 additions & 0 deletions BossMod/ActionTweaks/AutoAutosTweak.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public bool GetDesiredState(bool currentState)
if (_config.PyreticThreshold > 0 && hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && hints.ImminentSpecialMode.activation < ws.FutureTime(_config.PyreticThreshold))
return false; // pyretic => disable autos

if (hints.FindEnemy(target)?.Priority == AIHints.Enemy.PriorityForbidden)
return false;

return player.InCombat || ws.Client.CountdownRemaining <= PrePullThreshold; // no reason not to enable autos!
}
}
2 changes: 1 addition & 1 deletion BossMod/Autorotation/Legacy/LegacyModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ protected void PushResult(ActionID action, Actor? target)
return;
if (data.Range == 0)
target = Player; // override range-0 actions to always target player
if (target == null || Hints.ForbiddenTargets.FirstOrDefault(e => e.Actor == target)?.Priority == AIHints.Enemy.PriorityForbidFully)
if (target == null || Hints.ForbiddenTargets.FirstOrDefault(e => e.Actor == target)?.Priority == AIHints.Enemy.PriorityForbidden)
return; // forbidden
Hints.ActionsToExecute.Push(action, target, (data.IsGCD ? ActionQueue.Priority.High : ActionQueue.Priority.Low) + 500);
}
Expand Down
52 changes: 29 additions & 23 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
// information relevant for AI decision making process for a specific player
public sealed class AIHints
{
public class Enemy(Actor actor, bool shouldBeTanked)
public class Enemy(Actor actor, int priority, bool shouldBeTanked)
{
// TODO: split 'pointless to attack' (eg invulnerable, but fine to hit by aoes) vs 'actually bad to hit' (eg can lead to wipe)
public const int PriorityForbidAI = -1; // ai is forbidden from attacking this enemy, but player explicitly targeting it is not (e.g. out of combat enemies that we might not want to pull)
public const int PriorityForbidFully = -2; // attacking this enemy is forbidden both by ai or player (e.g. invulnerable, or attacking/killing might lead to a wipe)
public const int PriorityPointless = -1; // attacking enemy won't improve your parse, but will give gauge and advance combo (e.g. boss locked to 1 HP, useless add in raid, etc)
public const int PriorityInvincible = -2; // attacking enemy will have no effect at all besides breaking your combo, but hitting it with AOEs is fine
public const int PriorityUndesirable = -3; // enemy can be attacked if targeted manually by a player, but should be considered forbidden for AOE actions (i.e. mobs that are not in combat, or are in combat with someone else's party)
public const int PriorityForbidden = -4; // attacking this enemy will probably lead to a wipe; autoattacks and actions that target it will be forcibly prevented (if custom queueing is enabled)

public Actor Actor = actor;
public int Priority = actor.InCombat ? 0 : PriorityForbidAI; // <0 means damaging is actually forbidden, 0 is default (TODO: revise default...)
public int Priority = priority;
//public float TimeToKill;
public float AttackStrength = 0.05f; // target's predicted HP percent is decreased by this amount (0.05 by default)
public WPos DesiredPosition = actor.Position; // tank AI will try to move enemy to this position
Expand Down Expand Up @@ -40,6 +41,10 @@ public enum SpecialMode
public Bitmap.Region PathfindMapObstacles;

// list of potential targets
private readonly Enemy?[] _enemies = new Enemy?[99];
public Enemy? FindEnemy(Actor actor) => actor.SpawnIndex % 2 == 0 ? _enemies[actor.SpawnIndex / 2] : null;

// enemies in priority order
public List<Enemy> PotentialTargets = [];
public int HighestPotentialTargetPriority;

Expand Down Expand Up @@ -125,27 +130,28 @@ public void FillPotentialTargets(WorldState ws, bool playerIsDefaultTank)
var allowedFateID = playerInFate ? ws.Client.ActiveFate.ID : 0;
foreach (var actor in ws.Actors.Where(a => a.IsTargetable && !a.IsAlly && !a.IsDead))
{
// fate mob in fate we are NOT a part of, skip entirely. it's okay to "attack" these (i.e., they won't be added as forbidden targets) because we can't even hit them
// (though aggro'd mobs will continue attacking us after we unsync, but who really cares)
int prio = Enemy.PriorityUndesirable;
// fate mob in fate we are NOT a part of; we can't damage them at all
if (actor.FateID > 0 && actor.FateID != allowedFateID)
continue;
prio = Enemy.PriorityInvincible;
else if (Utils.ActorIsDying(actor, ws))
prio = Enemy.PriorityPointless;
else
{
var allowedAttack = actor.InCombat && ws.Party.FindSlot(actor.TargetID) >= 0;
// enemies in our enmity list can also be attacked, regardless of who they are targeting (since they are keeping us in combat)
allowedAttack |= actor.AggroPlayer;
// all fate mobs can be attacked if we are level synced (non synced mobs are skipped above)
allowedAttack |= actor.FateID > 0;

// target is dying; skip it so that AI retargets, but ensure that it's not marked as a forbidden target
// skip this check on striking dummies (name ID 541) as they die constantly
var predictedHP = ws.PendingEffects.PendingHPDifference(actor.InstanceID);
if (actor.HPMP.CurHP + predictedHP <= 0 && actor.NameID != 541)
continue;
if (allowedAttack)
prio = 0;
}

var allowedAttack = actor.InCombat && ws.Party.FindSlot(actor.TargetID) >= 0;
// enemies in our enmity list can also be attacked, regardless of who they are targeting (since they are keeping us in combat)
allowedAttack |= actor.AggroPlayer;
// all fate mobs can be attacked if we are level synced (non synced mobs are skipped above)
allowedAttack |= actor.FateID > 0;
var enemy = new Enemy(actor, prio, playerIsDefaultTank);

PotentialTargets.Add(new(actor, playerIsDefaultTank)
{
Priority = allowedAttack ? 0 : Enemy.PriorityForbidAI
});
PotentialTargets.Add(enemy);
_enemies[actor.SpawnIndex / 2] = enemy;
}
}

Expand Down Expand Up @@ -211,7 +217,7 @@ public void InitPathfindMap(Pathfinding.Map map)
// query utilities
public IEnumerable<Enemy> PotentialTargetsEnumerable => PotentialTargets;
public IEnumerable<Enemy> PriorityTargets => PotentialTargets.TakeWhile(e => e.Priority == HighestPotentialTargetPriority);
public IEnumerable<Enemy> ForbiddenTargets => PotentialTargetsEnumerable.Reverse().TakeWhile(e => e.Priority < 0);
public IEnumerable<Enemy> ForbiddenTargets => PotentialTargetsEnumerable.Reverse().TakeWhile(e => e.Priority <= Enemy.PriorityUndesirable);

// TODO: verify how source/target hitboxes are accounted for by various aoe shapes
public int NumPriorityTargetsInAOE(Func<Enemy, bool> pred) => ForbiddenTargets.Any(pred) ? 0 : PriorityTargets.Count(pred);
Expand Down
11 changes: 9 additions & 2 deletions BossMod/Framework/ActionManagerEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,15 @@ public void FinishActionGather()
AutoQueue = _hints.ActionsToExecute.FindBest(_ws, player, _ws.Client.Cooldowns, EffectiveAnimationLock, _hints, _animLockTweak.DelayEstimate);
if (AutoQueue.Delay > 0)
AutoQueue = default;
if (Config.PyreticThreshold > 0 && _hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && _hints.ImminentSpecialMode.activation < _ws.FutureTime(Config.PyreticThreshold) && AutoQueue.Priority < ActionQueue.Priority.ManualEmergency)
AutoQueue = default; // do not execute non-emergency actions when pyretic is imminent

if (AutoQueue.Priority < ActionQueue.Priority.ManualEmergency)
{
if (Config.PyreticThreshold > 0 && _hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && _hints.ImminentSpecialMode.activation < _ws.FutureTime(Config.PyreticThreshold))
AutoQueue = default; // do not execute non-emergency actions when pyretic is imminent

if (AutoQueue.Target is Actor t && _hints.FindEnemy(t)?.Priority == AIHints.Enemy.PriorityForbidden)
AutoQueue = default; // or if selected target is forbidden
}
}

public Vector3? GetWorldPosUnderCursor()
Expand Down
10 changes: 10 additions & 0 deletions BossMod/Framework/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ public static string ObjectKindString(IGameObject obj)
public static string CastTimeString(ActorCastInfo cast, DateTime now) => CastTimeString(cast.ElapsedTime, cast.TotalTime);
public static string LogMessageString(uint id) => $"{id} '{Service.LuminaRow<Lumina.Excel.Sheets.LogMessage>(id)?.Text}'";

public static bool ActorIsDying(Actor actor, WorldState ws)
{
// striking dummy - HP resets to full when "killed"
if (actor.NameID == 541)
return false;

var predicted = ws.PendingEffects.PendingHPDifference(actor.InstanceID);
return actor.HPMP.CurHP + predicted <= 0;
}

public static unsafe T ReadField<T>(void* address, int offset) where T : unmanaged => *(T*)((IntPtr)address + offset);
public static unsafe void WriteField<T>(void* address, int offset, T value) where T : unmanaged => *(T*)((IntPtr)address + offset) = value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
if (slot < _assignedBoss.Length && _assignedBoss[slot] != null)
foreach (var enemy in hints.PotentialTargets)
if (enemy.Actor != _assignedBoss[slot])
enemy.Priority = AIHints.Enemy.PriorityForbidFully;
enemy.Priority = AIHints.Enemy.PriorityInvincible;
}

public override void OnTethered(Actor source, ActorTetherInfo tether)
Expand Down
4 changes: 1 addition & 3 deletions BossMod/Modules/Endwalker/Ultimate/TOP/P2LimitlessSynergy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
{
var e = hints.PotentialTargets.FirstOrDefault(e => e.Actor == _invincible);
if (e != null)
{
e.Priority = AIHints.Enemy.PriorityForbidFully;
}
e.Priority = AIHints.Enemy.PriorityInvincible;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRoles
e.Priority = (OID)e.Actor.OID switch
{
OID.MythrilVerge => 3,
OID.SunJuror => WorldState.Actors.Where(other => (OID)other.OID is OID.Platform1 or OID.Platform2 or OID.Platform3).InRadius(e.Actor.Position, 1).Any() ? 2 : AIHints.Enemy.PriorityForbidAI,
OID.SunJuror => WorldState.Actors.Where(other => (OID)other.OID is OID.Platform1 or OID.Platform2 or OID.Platform3).InRadius(e.Actor.Position, 1).Any() ? 2 : AIHints.Enemy.PriorityPointless,
OID.Boss => 1,
_ => 0
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
e.ShouldBeTanked = false;
break;
case OID.SpinyPlume:
e.Priority = Module.PrimaryActor.IsTargetable ? AIHints.Enemy.PriorityForbidAI : 6;
e.Priority = Module.PrimaryActor.IsTargetable ? AIHints.Enemy.PriorityPointless : 6;
e.AttackStrength = 0;
e.ShouldBeTanked = false;
if (actor.Role == Role.Tank && e.Actor.TargetID != actor.InstanceID && (WorldState.Actors.Find(e.Actor.TargetID)?.FindStatus(SID.ThermalLow)?.Extra ?? 0) >= 2)
Expand Down
12 changes: 0 additions & 12 deletions BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3Titan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@ public class Ex3Titan : BossModule
Bombs = Enemies(OID.BombBoulder);
}

protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
var heart = Heart();
if (heart != null && heart.IsTargetable)
{
// heart is not added by default, since it has weird actor type
// boss is not really a valid target, but it still hits tank pretty hard, so we want to set attacker strength (?)
hints.PotentialTargets.Add(new(heart, false));
//hints.PotentialTargets.Add(new(PrimaryActor, false));
}
}

protected override void DrawEnemies(int pcSlot, Actor pc)
{
Arena.Actor(PrimaryActor, ArenaColor.Enemy, true);
Expand Down
4 changes: 2 additions & 2 deletions BossMod/Modules/RealmReborn/Raid/T01Caduceus/T01AI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
{
e.Priority = 1; // this is a baseline; depending on whether we want to prioritize clone vs boss, clone's priority changes
if (cloneSpawningSoon && e.Actor.FindStatus(SID.SteelScales) != null)
e.Priority = AIHints.Enemy.PriorityForbidAI; // stop dps until stack can be dropped
e.Priority = AIHints.Enemy.PriorityPointless; // stop dps until stack can be dropped
}
else
{
Expand All @@ -114,7 +114,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme
// for now, let kiter damage it until 20%
var predictedHP = (int)e.Actor.HPMP.CurHP + WorldState.PendingEffects.PendingHPDifference(e.Actor.InstanceID);
//e.Priority = predictedHP > 0.7f * e.Actor.HPMP.MaxHP ? (actor.Role is Role.Ranged or Role.Melee ? 3 : AIHints.Enemy.PriorityForbidAI) : AIHints.Enemy.PriorityForbidAI;
e.Priority = predictedHP > 0.2f * e.Actor.HPMP.MaxHP ? (e.Actor.TargetID == actor.InstanceID ? 3 : AIHints.Enemy.PriorityForbidAI) : AIHints.Enemy.PriorityForbidAI;
e.Priority = predictedHP > 0.2f * e.Actor.HPMP.MaxHP ? (e.Actor.TargetID == actor.InstanceID ? 3 : AIHints.Enemy.PriorityPointless) : AIHints.Enemy.PriorityPointless;
e.ShouldBeTanked = false;
e.ForbidDOTs = true;
}
Expand Down
2 changes: 0 additions & 2 deletions BossMod/Modules/RealmReborn/Trial/T02TitanN/T02TitanN.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ public class T02TitanN : BossModule

protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
foreach (var heart in ActiveHeart)
hints.PotentialTargets.Add(new(heart, actor.Role == Role.Tank));
foreach (var e in hints.PotentialTargets)
{
e.Priority = (OID)e.Actor.OID switch
Expand Down
2 changes: 0 additions & 2 deletions BossMod/Modules/RealmReborn/Trial/T07TitanH/T07TitanH.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ public class T07TitanH : BossModule

protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints)
{
foreach (var heart in ActiveHeart)
hints.PotentialTargets.Add(new(heart, assignment == PartyRolesConfig.Assignment.MT));
foreach (var enemy in hints.PotentialTargets)
{
enemy.Priority = (OID)enemy.Actor.OID switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ public override void AddQuestAIHints(Actor player, AIHints hints)
{
foreach (var h in hints.PotentialTargets)
if (h.Actor.OID == 0x2955)
h.Priority = AIHints.Enemy.PriorityForbidFully;
h.Priority = AIHints.Enemy.PriorityForbidden;
}
}
2 changes: 1 addition & 1 deletion BossMod/QuestBattle/Stormblood/MSQ/ItsProbablyATrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override void AddQuestAIHints(Actor player, AIHints hints)
foreach (var h in hints.PotentialTargets)
// attacking sekiseigumi fails the mission
if (h.Actor.OID is 0x1A6B or 0x1A66)
h.Priority = AIHints.Enemy.PriorityForbidFully;
h.Priority = AIHints.Enemy.PriorityForbidden;

if (SmokeBomb && player.FindStatus(SID.Bind) != null)
hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.SmokeScreen), player, ActionQueue.Priority.Medium);
Expand Down

0 comments on commit f53690f

Please sign in to comment.