-
Notifications
You must be signed in to change notification settings - Fork 114
Autorotation Development Tutorial
For this small tutorial, we are going to be creating an autorotation for Machinist's 1-2-3 combo of Split Shot, Slug Shot, and Clean Shot.
This guide will assume you have some basic knowledge of C# and programming, but a lot of what makes an autorotation good is the logic behind why you cast certain abilities when. So if you can't contribute code directly, you might be able to help with improving the rotation from a logic level!
In the Autorotation
folder in the root of the BossMod
project, navigate to the Standard
folder and create a class called StandardMCH.cs
. When you build the project later, the name of this file and details you add to it will show up in-game!
namespace BossMod.Autorotation;
public class StandardMCH
{
}
Make sure the namespace is correctly set as BossMod.Autorotation
and not BossMod.Autorotation.Standard
.
Add the RotationModuleManager manager
and Actor player
parameters to the class. VBM uses these to run the autorotation. Also, set your class to inherit the RotationModule
base class with the manager
and player
parameters. Lastly, seal the class by adding sealed
after public
where you define it. See the example below:
public sealed class StandardMCH(RotationModuleManager manager, Actor player) : RotationModule(manager, player)
Inside of the class, create a public static RotationModuleDefinition
function with the method Definition()
, like so:
public static RotationModuleDefinition Definition()
{
}
Inside this function, create and return a variable called res
that contains a new RotationModuleDefinition
with the following attributes:
- Name - in this case, we're using Standard MCH
- Description - make it whatever you want, I will use Description
- Author - however you want to be identified, I am leaving it as Author
-
RotationModuleQuality
, an enum for the status of the autorotation module - set this asWIP
- Class - set this as
BitMask.Build((int)Class.MCH)
to set it as Machinist - Maximum level - set this to
100
var res = new RotationModuleDefinition(
"Standard MCH",
"Description",
"Author",
RotationModuleQuality.WIP,
BitMask.Build((int)Class.MCH),
100);
return res;
Underneath the RotationModuleDefinition
, create a public override void Execute()
function. In the function parameters, provide the following:
-
StrategyValues strategy
- strategies the rotation will implement, which we haven't covered yet -
Actor? primaryTarget
- the target of the player -
float estimatedAnimLockDelay
- self-explanatory -
float forceMovementIn
- how long until forced movement is coming, used during a boss module -
bool isMoving
- to know if you're moving, useful for casters
Lastly, add a return
to your function.
public override void Execute(
StrategyValues strategy,
Actor? primaryTarget,
float estimatedAnimLockDelay,
float forceMovementIn,
bool isMoving)
{
return;
}
Your file should look like this:
StandardMCH.cs
namespace BossMod.Autorotation;
public sealed class StandardMCH(RotationModuleManager manager, Actor player) : RotationModule(manager, player)
{
public static RotationModuleDefinition Definition()
{
var res = new RotationModuleDefinition(
"Standard MCH",
"Description",
"Author",
RotationModuleQuality.WIP,
BitMask.Build((int)Class.MCH),
100);
return res;
}
public override void Execute(
StrategyValues strategy,
Actor? primaryTarget,
float estimatedAnimLockDelay,
float forceMovementIn,
bool isMoving)
{
return;
}
}
Build the project and load up the development plugin in-game by following these steps:
- Open Dalamud's settings interface with
/xlsettings
- Navigate to the
Experimental
tab - Scroll down to
Dev Plugin Locations
- Enter the location of the built DLL from your IDE (in my case,
C:\Projects\CS\ffxiv_bossmod\BossMod\bin\x64\Debug\BossMod.dll
) - Click the
+
to add it as a dev. plugin location - Click the save icon on the bottom right
- Navigate to Dalamud's plugin installer interface with
/xlplugins
- On the top of the sidebar, click
Dev Tools
- You should see
Boss Mod (dev plugin)
in the list - Disable your original Boss Mod plugin, then enable the developer version!
Once you navigate to the Autorotation Presets
section of the plugin and add a module, you will see your new module!
We're going to add the Machinist's 1-2-3 ability combo to the rotation: Split Shot, Slug Shot, and Clean Shot.
First, we need to set a GCD priority for our casts. It won't have a ton in them now, but as you start developing your rotation module, you'll add to it to change how the rotation functions.
Create a public enum
called GCDPriority
in your class, above the Execute
function.
public enum GCDPriority
{
}
Add a None = 0
to the enum
. A None
priority means the action will not be queued, so you can use this as a simple conditional to prevent actions from being cast without having to write an entire if
statement.
public enum GCDPriority
{
None = 0
}
Inside the GCDPriority
enum, add the following 3 values:
-
DelayCombo = 350
to represent when we want to delay our 1-2-3 combo -
FlexibleCombo = 400
to represent our "default" state where we can cast our combo whenever -
ForcedCombo = 880
to represent when we need to force a cast of a combo action to maintain our combo (combos only last 30s from the last combo action)
public enum GCDPriority
{
None = 0,
DelayCombo = 350,
FlexibleCombo = 400,
ForcedCombo = 880
}
Now that we have a priority, we can focus on a state machine for the combo. We're going to create a variable called PrevCombo
that provides the AID (action ID) of the previous combo action and an arrow function called NextComboSingleTarget
that rotates depending on what the PrevCombo
action is.
Define a private variable with the class MCH.AID
called PrevCombo
underneath the GCD/oGCD priority enum
s with a lambda operator followed by the expression (MCH.AID)World.Client.ComboState.Action
.
private MCH.AID PrevCombo => (MCH.AID)World.Client.ComboState.Action
This simple expression-bodied property allows us to read the previous combo action, which is exposed by Dalamud.
Next, underneath our Execute
function, create a private arrow function called NextComboSingleTarget
with the class MCH.AID
, whose switch expression begins with PrevCombo switch
private MCH.AID NextComboSingleTarget() => PrevCombo switch
{
}
Inside our function, we're going to define all 3 of our different cases for our 1-2-3 combo.
-
_ => MCH.AID.SplitShot
- by default, cast Split Shot -
MCH.AID.SplitShot => MCH.AID.SlugShot
- if Split Shot was the last action, use Slug Shot -
MCH.AID.SlugShot => MCH.AID.CleanShot
- if Slug Shot was the last action, use Clean Shot
Your arrow function should look like this now:
private MCH.AID NextComboSingleTarget() => PrevCombo switch
{
MCH.AID.SlugShot => MCH.AID.CleanShot,
MCH.AID.SplitShot => MCH.AID.SlugShot,
_ => MCH.AID.SplitShot
}
Create another private function underneath the one we just made called QueueGCD
with the parameters MCH.AID aid
, Actor? target
, and GCDPriority prio
private void QueueGCD(MCH.AID aid, Actor? target, GCDPriority prio)
{
}
Inside the function, paste the below code in. All it does is prevent the function from queueing a GCD if the priority is set to None
.
if (prio != GCDPriority.None)
{
Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, ActionQueue.Priority.High + (int)prio);
}
Lastly, inside our Execute
function, remove the return
and call the QueueGCD
function and provide the following parameters:
-
NextComboSingleTarget()
- providing the AID of what we want to queue -
primaryTarget
- use action on the target of the player -
GCDPriority.FlexibleCombo
- our "standard" priority
QueueGCD(NextComboSingleTarget(), primaryTarget, GCDPriority.FlexibleCombo);
Your file should now look like this:
StandardMCH.cs
namespace BossMod.Autorotation;
public sealed class StandardMCH(RotationModuleManager manager, Actor player) : RotationModule(manager, player)
{
public static RotationModuleDefinition Definition()
{
var res = new RotationModuleDefinition(
"Standard MCH",
"Description",
"Author",
RotationModuleQuality.WIP,
BitMask.Build((int)Class.MCH),
100);
return res;
}
public enum GCDPriority
{
None = 0,
DelayCombo = 350,
FlexibleCombo = 400,
ForcedCombo = 880
};
private MCH.AID PrevCombo => (MCH.AID)World.Client.ComboState.Action;
public override void Execute(
StrategyValues strategy,
Actor? primaryTarget,
float estimatedAnimLockDelay,
float forceMovementIn,
bool isMoving)
{
QueueGCD(NextComboSingleTarget(), primaryTarget, GCDPriority.FlexibleCombo);
}
private MCH.AID NextComboSingleTarget() => PrevCombo switch
{
MCH.AID.SlugShot => MCH.AID.CleanShot,
MCH.AID.SplitShot => MCH.AID.SlugShot,
_ => MCH.AID.SplitShot
};
private void QueueGCD(MCH.AID aid, Actor? target, GCDPriority prio)
{
if (prio != GCDPriority.None)
{
Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, ActionQueue.Priority.High + (int)prio);
}
}
}
Build the plugin, reload it in-game, and test it out!