Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP deep dungeon auto clear modules #580

Merged
merged 34 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a8e6498
Auto-engage preset
xanunderscore Jan 28, 2025
5805fe7
deep dungeon modules
xanunderscore Jan 29, 2025
5f95e89
add maps
xanunderscore Jan 29, 2025
7998936
update rotations
xanunderscore Jan 29, 2025
517c914
forgot to display da window
xanunderscore Jan 29, 2025
de433e1
oh whoops
xanunderscore Jan 29, 2025
cef92dd
minor fixes
xanunderscore Jan 29, 2025
0259655
oopC
xanunderscore Jan 29, 2025
03b4d45
add los check to amex
xanunderscore Jan 29, 2025
8418449
basexan fixes
xanunderscore Jan 29, 2025
2a1e048
interaction cleanup stuff
xanunderscore Jan 29, 2025
7f5a570
lol?
xanunderscore Jan 29, 2025
4910879
add transform statuses
xanunderscore Jan 29, 2025
df6a048
fix multi charge stuff
xanunderscore Jan 29, 2025
b1747f8
one more fix
xanunderscore Jan 29, 2025
203614e
Merge branch 'master' of https://github.com/awgil/ffxiv_bossmod into wip
xanunderscore Jan 29, 2025
32156d3
maybe?
xanunderscore Jan 29, 2025
dea1e4a
surely
xanunderscore Jan 29, 2025
2d96749
specify version
xanunderscore Jan 29, 2025
ad54ea4
mark generic AI functionality as usable while transformed
xanunderscore Jan 29, 2025
d7573c8
minify walls file to reduce PR sive
xanunderscore Jan 29, 2025
4efc8ac
Merge remote-tracking branch 'origin/patch-19' into wip
xanunderscore Jan 31, 2025
280ead8
github stuff
xanunderscore Jan 31, 2025
9600985
fix some interact stuff
xanunderscore Jan 31, 2025
8d23034
clarify comment
xanunderscore Jan 31, 2025
c3ed5bc
fix autohint forbidden zone size
xanunderscore Jan 31, 2025
643c70e
add vscode config stuff back in
xanunderscore Jan 31, 2025
a1d207e
vsc stuff fix
xanunderscore Feb 1, 2025
fcbf212
prevent preemptive use of potion for % based damage
xanunderscore Feb 1, 2025
461c8d4
bomb enrage
xanunderscore Feb 1, 2025
181677b
this doesnt work i guess whatever
xanunderscore Feb 2, 2025
ed78262
remove leftover shit
xanunderscore Feb 2, 2025
19fb91e
leftover from bmx
xanunderscore Feb 2, 2025
ed99643
remove thing from other pr
xanunderscore Feb 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/rotation-bug-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: Rotation bug report
about: Unexpected rotation behavior, crash, error in logs, etc.
title: "[BUG] L100 JobNameHere: Issue Summary Here"
labels: ''
assignees: ''

---

**Bug description**
e.g. "Reaper using Enshroud too early before raid buffs"

**Replay info** - only one choice is needed
- [ ] I have a(n anonymized) video recording of this interaction
- [ ] I have a replay file and I will DM it to you upon request (or just post it here if I don't care about privacy)
- [ ] I can attach my `/xllog` from when the issue occurred
- [ ] I have a screenshot of or link to xivanalysis
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/something-that-isn-t-a-rotation-bug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: Something that isn't a rotation bug
about: Not a rotation bug
title: ''
labels: ''
assignees: ''

---


27 changes: 25 additions & 2 deletions BossMod/ActionQueue/ActionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ public float MainReadyIn(ReadOnlySpan<Cooldown> cooldowns, ReadOnlySpan<ClientSt
if (MainCooldownGroup < 0)
return 0;
var cdg = cooldowns[ActualMainCooldownGroup(dutyActions)];
return !IsMultiCharge || cdg.Total < Cooldown ? cdg.Remaining : Cooldown - cdg.Elapsed;
var max = MaxChargesAtCap();

// GCDs with multiple charges can be affected OR unaffected by haste depending on how many charges the user currently has access to
// the only separate-cooldown GCD that increases to >1 charge via trait is currently MCH Drill; others have multiple charges at unlock - SGE Phlegma, RPR Soul Slice, BLU Surpanakha
var cooldownSingleCharge = IsGCD && max > 1 && cdg.Total > 0 ? cdg.Total / max : Cooldown;

return !IsMultiCharge || cdg.Total < cooldownSingleCharge ? cdg.Remaining : cooldownSingleCharge - cdg.Elapsed;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't understand this change... Can you elaborate?

Copy link
Collaborator Author

@xanunderscore xanunderscore Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have the level 94 trait Enhanced Multiweapon on machinist, Drill stops being affected by skill speed/haste/slows, so the separate cooldown is always 20s and using both charges will set it to 40. before then it is affected by haste despite still technically having multiple charges, so like in Palace of the Dead, where you have some inherent skillspeed from the aetherpool gear, the single charge cooldown of Drill is 19.2s and using the one charge you have at that level will set the cooldown to 38.4s. also, if you use a single charge of drill at these levels while slowed or hasted, you can get a cooldown total of like 32 or 54 seconds, and checking Cooldown against that gives you incorrect results.

so what this line is doing is just dividing cdg.Total by the maximum number of charges of the ability, which SHOULD give us the effective single charge CD.

i do not think it's possible to accurately calculate this information based on only the user's stats, because the total cooldown is basically snapshotting the stats we had when the skill was used, which may not be the ones we have now

Copy link
Owner

@awgil awgil Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then why can't we just always return cdg.Total / MaxChargesAtCap() - cdg.Elapsed, clamped to 0? Why do we need IsGCD etc checks here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that should work in theory yeah. this code was originally only checking for specifically Drill, since it's the only action that is affected and i wanted to avoid introducing other bugs, but it seemed to work fine in practice

}

public float ExtraReadyIn(ReadOnlySpan<Cooldown> cooldowns) => ExtraCooldownGroup >= 0 ? cooldowns[ExtraCooldownGroup].Remaining : 0;
Expand Down Expand Up @@ -173,6 +179,14 @@ public sealed class ActionDefinitions : IDisposable
public static readonly ActionID IDPotionInt = new(ActionType.Item, 1044165); // hq grade 2 gemdraught of intelligence
public static readonly ActionID IDPotionMnd = new(ActionType.Item, 1044166); // hq grade 2 gemdraught of mind

// deep dungeon consumables
public static readonly ActionID IDSustainingPotion = new(ActionType.Item, 20309);
public static readonly ActionID IDMaxPotion = new(ActionType.Item, 1013637);
public static readonly ActionID IDEmpyreanPotion = new(ActionType.Item, 23163);
public static readonly ActionID IDSuperPotion = new(ActionType.Item, 1023167);
public static readonly ActionID IDOrthosPotion = new(ActionType.Item, 38944);
public static readonly ActionID IDHyperPotion = new(ActionType.Item, 1038956);

// special general actions that we support
public static readonly ActionID IDGeneralLimitBreak = new(ActionType.General, 3);
public static readonly ActionID IDGeneralSprint = new(ActionType.General, 4);
Expand Down Expand Up @@ -219,6 +233,13 @@ private ActionDefinitions()
RegisterPotion(IDPotionInt);
RegisterPotion(IDPotionMnd);

RegisterPotion(IDSustainingPotion, 1.1f);
RegisterPotion(IDMaxPotion, 1.1f);
RegisterPotion(IDEmpyreanPotion, 1.1f);
RegisterPotion(IDSuperPotion, 1.1f);
RegisterPotion(IDOrthosPotion, 1.1f);
RegisterPotion(IDHyperPotion, 1.1f);

// special content actions - bozja, deep dungeons, etc
for (var i = BozjaHolsterID.None + 1; i < BozjaHolsterID.Count; ++i)
RegisterBozja(i);
Expand Down Expand Up @@ -380,7 +401,7 @@ public void RegisterSpell<AID>(AID aid, bool isPhysRanged = false, float instant

private void Register(ActionID aid, ActionDefinition definition) => _definitions.Add(aid, definition);

private void RegisterPotion(ActionID aid)
private void RegisterPotion(ActionID aid, float animLock = 0.6f)
{
var baseId = aid.ID % 500000;
var item = ItemData(baseId);
Expand All @@ -399,6 +420,7 @@ private void RegisterPotion(ActionID aid)
CastTime = castTime,
MainCooldownGroup = cdgroup,
Cooldown = cooldown,
InstantAnimLock = animLock,
};
var aidHQ = new ActionID(ActionType.Item, baseId + 1000000);
_definitions[aidHQ] = new(aidHQ)
Expand All @@ -408,6 +430,7 @@ private void RegisterPotion(ActionID aid)
CastTime = castTime,
MainCooldownGroup = cdgroup,
Cooldown = cooldown * 0.9f,
InstantAnimLock = animLock
};
}

Expand Down
1 change: 1 addition & 0 deletions BossMod/ActionQueue/Melee/MNK.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public Definitions(ActionDefinitions d)
d.RegisterSpell(AID.ElixirBurst);
d.RegisterSpell(AID.WindsReply);
d.RegisterSpell(AID.FiresReply);
d.RegisterSpell(AID.EarthsReply);
d.RegisterSpell(AID.EnlightenedMeditation);
d.RegisterSpell(AID.ForbiddenMeditation);
d.RegisterSpell(AID.InspiritedMeditation);
Expand Down
13 changes: 13 additions & 0 deletions BossMod/ActionQueue/Roleplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

public enum AID : uint
{
// palace of the dead/EO transformations
Pummel = 6273,
VoidFireII = 6274,
HeavenlyJudge = 6871,
Rotosmash = 32781,
WreckingBall = 32782,

// magitek reaper in Fly Free, My Pretty
MagitekCannon = 7619, // range 30 radius 6 ground targeted aoe
PhotonStream = 7620, // range 10 width 4 rect aoe
Expand Down Expand Up @@ -262,6 +269,12 @@ public sealed class Definitions : IDisposable
{
public Definitions(ActionDefinitions d)
{
d.RegisterSpell(AID.Pummel);
d.RegisterSpell(AID.VoidFireII);
d.RegisterSpell(AID.HeavenlyJudge);
d.RegisterSpell(AID.Rotosmash);
d.RegisterSpell(AID.WreckingBall);

d.RegisterSpell(AID.MagitekCannon);
d.RegisterSpell(AID.PhotonStream);
d.RegisterSpell(AID.DiffractiveMagitekCannon);
Expand Down
2 changes: 1 addition & 1 deletion BossMod/Autorotation/MiscAI/AutoFarm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public enum PriorityStrategy { None, Prioritize }

public static RotationModuleDefinition Definition()
{
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);
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, CanUseWhileRoleplaying: true);

res.Define(Track.General).As<GeneralStrategy>("General")
.AddOption(GeneralStrategy.FightBack, "FightBack", "Automatically engage any mobs that are in combat with player, but don't pull new mobs", supportedTargets: ActionTargets.Hostile)
Expand Down
15 changes: 13 additions & 2 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("Automatic movement", "Automatically move character based on pathfinding or explicit coordinates.", "AI", "veyn", RotationModuleQuality.WIP, new(~0ul), 1000, 1, RotationModuleOrder.Movement);
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, CanUseWhileRoleplaying: true);
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 @@ -64,7 +64,18 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget,
}

if (Hints.InteractWithTarget != null)
Hints.GoalZones.Add(Hints.GoalSingleTarget(Hints.InteractWithTarget.Position, 2, 100)); // strongly prefer moving towards interact target
{
// strongly prefer moving towards interact target
Hints.GoalZones.Add(p =>
{
var length = (p - Hints.InteractWithTarget.Position).Length();

// 99% of eventobjects have an interact range of 3.5y, while the rest have a range of 2.09y
// checking only for the shorter range here would be fine in the vast majority of cases, but it can break interact pathfinding in the case that the target object is partially covered by a forbidden zone with a radius between 2.1 and 3.5
// this is specifically an issue in the metal gear thancred solo duty in endwalker
return length <= 2.09f ? 101 : length <= 3.5f ? 100 : 0;
});
}
}

var speed = Player.FindStatus(ClassShared.SID.Sprint) != null ? 7.8f : 6;
Expand Down
8 changes: 6 additions & 2 deletions BossMod/Autorotation/RotationModuleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public Preset? Preset
(uint)Roleplay.SID.BorrowedFlesh, // used specifically for In from the Cold (Endwalker)
(uint)Roleplay.SID.FreshPerspective, // sapphire weapon quest
565, // "Transfiguration" from certain pomanders in Palace of the Dead
439, // "Toad", palace of the dead
1546, // "Odder", heaven-on-high
3502, // "Owlet", EO
404, // "Transporting", not a transformation but prevents actions
];

public static bool IsTransformStatus(ActorStatus st) => TransformationStatuses.Contains(st.ID);
Expand All @@ -69,8 +73,8 @@ public RotationModuleManager(RotationDatabase db, BossModuleManager bmm, AIHints
WorldState.Actors.InCombatChanged.Subscribe(OnCombatChanged),
WorldState.Actors.IsDeadChanged.Subscribe(OnDeadChanged),
WorldState.Actors.CastEvent.Subscribe(OnCastEvent),
WorldState.Actors.StatusGain.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && a.Statuses[idx].ID == (uint)Roleplay.SID.RolePlaying)),
WorldState.Actors.StatusLose.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && a.Statuses[idx].ID == (uint)Roleplay.SID.RolePlaying)),
WorldState.Actors.StatusGain.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && IsTransformStatus(a.Statuses[idx]))),
WorldState.Actors.StatusLose.Subscribe((a, idx) => DirtyActiveModules(PlayerInstanceId == a.InstanceID && IsTransformStatus(a.Statuses[idx]))),
WorldState.Party.Modified.Subscribe(op => DirtyActiveModules(op.Slot == PlayerSlot)),
WorldState.Client.ActionRequested.Subscribe(OnActionRequested),
WorldState.Client.CountdownChanged.Subscribe(OnCountdownChanged),
Expand Down
198 changes: 198 additions & 0 deletions BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
namespace BossMod.Autorotation.xan;

public class DeepDungeonAI(RotationModuleManager manager, Actor player) : AIBase(manager, player)
{
public enum Track { Potion, Kite }

public static RotationModuleDefinition Definition()
{
var def = new RotationModuleDefinition("Deep Dungeon AI", "Utilities for deep dungeon - potion/pomander user", "AI (xan)", "xan", RotationModuleQuality.Basic, new BitMask(~0ul), 100, CanUseWhileRoleplaying: true);

def.AbilityTrack(Track.Potion, "Potion");
def.AbilityTrack(Track.Kite, "Kite enemies");

return def;
}

enum OID : uint
{
Unei = 0x3E1A,
}

enum Transformation : uint
{
None,
Manticore,
Succubus,
Kuribu,
Dreadnaught
}

enum SID : uint
{
Transfiguration = 565,
ItemPenalty = 1094,
}

public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving)
{
if (World.DeepDungeon.DungeonId == 0)
return;

var transformation = Transformation.None;
if (Player.FindStatus(SID.Transfiguration) is { } status)
{
transformation = (status.Extra & 0xFF) switch
{
42 => Transformation.Manticore,
43 => Transformation.Succubus,
49 => Transformation.Kuribu,
244 => Transformation.Dreadnaught,
_ => Transformation.None
};
}

if (transformation != Transformation.None)
{
DoTransformActions(strategy, primaryTarget, transformation);
return;
}

if (IsRanged && !Player.InCombat && primaryTarget is Actor target && !target.InCombat && !target.IsAlly)
// bandaid fix to help deal with constant LOS issues
Hints.GoalZones.Add(Hints.GoalSingleTarget(target, 3, 0.1f));

SetupKiteZone(strategy, primaryTarget);

if (Player.FindStatus(SID.ItemPenalty) != null)
return;

var (regenAction, potAction) = World.DeepDungeon.DungeonId switch
{
DeepDungeonState.DungeonType.POTD => (ActionDefinitions.IDSustainingPotion, ActionDefinitions.IDMaxPotion),
DeepDungeonState.DungeonType.HOH => (ActionDefinitions.IDEmpyreanPotion, ActionDefinitions.IDSuperPotion),
DeepDungeonState.DungeonType.EO => (ActionDefinitions.IDOrthosPotion, ActionDefinitions.IDHyperPotion),
_ => (default, default)
};

if (regenAction != default && ShouldPotion(strategy))
Hints.ActionsToExecute.Push(regenAction, Player, ActionQueue.Priority.Medium);

if (potAction != default && Player.HPRatio <= 0.3f)
Hints.ActionsToExecute.Push(potAction, Player, ActionQueue.Priority.VeryHigh);
}

private bool IsRanged => Player.Class.GetRole() is Role.Ranged or Role.Healer;

private static readonly HashSet<uint> NoMeleeAutos = [
// hoh
0x22C3, // heavenly onibi
0x22C5, // heavenly dhruva
0x22C6, // heavenly sai taisui
0x22DC, // heavenly dogu
0x22DE, // heavenly ganseki
0x22ED, // heavenly kongorei
0x22EF, // heavenly maruishi
0x22F3, // heavenly rachimonai
0x22FC, // heavenly doguzeri
0x2320, // heavenly nuppeppo (WHM) (uses stone)

// orthos
0x3DCC, // orthos imp
0x3DCE, // orthos fachan
0x3DD2, // orthos water sprite
0x3DD4, // orthos microsystem
0x3DD5, // orthosystem β
0x3DE0, // orthodemolisher
0x3DE2, // orthodroid
0x3DFD, // orthos apa
0x3E10, // orthos ice sprite
0x3E5C, // orthos ahriman
0x3E62, // orthos abyss
0x3E63, // orthodrone
0x3E64, // orthosystem γ
0x3E66, // orthosystem α
];

private void SetupKiteZone(StrategyValues strategy, Actor? primaryTarget)
{
if (!IsRanged || primaryTarget == null || !Player.InCombat || !strategy.Enabled(Track.Kite))
return;

// wew
if (NoMeleeAutos.Contains(primaryTarget.OID))
return;

// assume we don't need to kite if mob is busy casting (TODO: some mob spells can be cast while moving, maybe there's a column in sheets for it)
if (primaryTarget.CastInfo != null)
return;

float maxRange = 25;
float maxKite = 9;

var primaryPos = primaryTarget.Position;
var total = maxRange + Player.HitboxRadius + primaryTarget.HitboxRadius;
var totalKite = maxKite + Player.HitboxRadius + primaryTarget.HitboxRadius;
float goalFactor = 0.05f;
Hints.GoalZones.Add(pos =>
{
var dist = (pos - primaryPos).Length();
return dist <= total && dist >= totalKite ? goalFactor : 0;
});
}

private void DoTransformActions(StrategyValues strategy, Actor? primaryTarget, Transformation t)
{
if (primaryTarget == null)
return;

Func<WPos, float> goal;
ActionID attack;
int numTargets;
var castTime = 0f;

switch (t)
{
case Transformation.Manticore:
goal = Hints.GoalSingleTarget(primaryTarget, 3);
numTargets = 1;
attack = ActionID.MakeSpell(Roleplay.AID.Pummel);
break;
case Transformation.Succubus:
goal = Hints.GoalSingleTarget(primaryTarget, 25);
numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 5);
attack = ActionID.MakeSpell(Roleplay.AID.VoidFireII);
castTime = 2.5f;
break;
case Transformation.Kuribu:
// heavenly judge is ground targeted
goal = Hints.GoalSingleTarget(primaryTarget.Position, 25);
numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 6);
attack = ActionID.MakeSpell(Roleplay.AID.HeavenlyJudge);
castTime = 2.5f;
break;
case Transformation.Dreadnaught:
goal = Hints.GoalSingleTarget(primaryTarget, 3);
numTargets = 1;
attack = ActionID.MakeSpell(Roleplay.AID.Rotosmash);
break;
default:
return;
}

if (numTargets == 0)
return;

Hints.GoalZones.Add(goal);
Hints.ActionsToExecute.Push(attack, primaryTarget, ActionQueue.Priority.High, targetPos: primaryTarget.PosRot.XYZ(), castTime: castTime - 0.5f);
}

private bool ShouldPotion(StrategyValues strategy)
{
if (World.Actors.Any(w => w.OID == (uint)OID.Unei) || !strategy.Enabled(Track.Potion))
return false;

var ratio = Player.ClassCategory is ClassCategory.Tank ? 0.4f : 0.6f;
return Player.PredictedHPRatio < ratio && Player.FindStatus(648) == null && Player.InCombat;
}
}
Loading