Skip to content

Autorotation Development Tutorial

Aimsucks edited this page Sep 7, 2024 · 7 revisions

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!

Creating an Autorotation Class

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)

Defining the Rotation Module

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:

  1. Name - in this case, we're using Standard MCH
  2. Description - make it whatever you want, I will use Description
  3. Author - however you want to be identified, I am leaving it as Author
  4. RotationModuleQuality, an enum for the status of the autorotation module - set this as WIP
  5. Class - set this as BitMask.Build((int)Class.MCH) to set it as Machinist
  6. Maximum level - set this to 100
var res = new RotationModuleDefinition(
    "Standard MCH",
    "Description",
    "Author",
    RotationModuleQuality.WIP,
    BitMask.Build((int)Class.MCH),
    100);

return res;

Creating the Execute Function

Underneath the RotationModuleDefinition, create a public override void Execute() function. In the function parameters, provide the following:

  1. StrategyValues strategy - strategies the rotation will implement, which we haven't covered yet
  2. Actor? primaryTarget - the target of the player
  3. float estimatedAnimLockDelay - self-explanatory
  4. float forceMovementIn - how long until forced movement is coming, used during a boss module
  5. 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;
}

Results

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;
    }
}

Using Dev. Plugins

Build the project and load up the development plugin in-game by following these steps:

  1. Open Dalamud's settings interface with /xlsettings
  2. Navigate to the Experimental tab
  3. Scroll down to Dev Plugin Locations
  4. Enter the location of the built DLL from your IDE (in my case, C:\Projects\CS\ffxiv_bossmod\BossMod\bin\x64\Debug\BossMod.dll)
  5. Click the + to add it as a dev. plugin location
  6. Click the save icon on the bottom right
  7. Navigate to Dalamud's plugin installer interface with /xlplugins
  8. On the top of the sidebar, click Dev Tools
  9. You should see Boss Mod (dev plugin) in the list
  10. 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!

image

Adding Actions

We're going to add the Machinist's 1-2-3 ability combo to the rotation: Split Shot, Slug Shot, and Clean Shot.

Create the GCD Priority

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:

  1. DelayCombo = 350 to represent when we want to delay our 1-2-3 combo
  2. FlexibleCombo = 400 to represent our "default" state where we can cast our combo whenever
  3. 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
}

Create a Combo State Machine

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 enums 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.

  1. _ => MCH.AID.SplitShot - by default, cast Split Shot
  2. MCH.AID.SplitShot => MCH.AID.SlugShot - if Split Shot was the last action, use Slug Shot
  3. 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 the GCD Queueing Function

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:

  1. NextComboSingleTarget() - providing the AID of what we want to queue
  2. primaryTarget - use action on the target of the player
  3. GCDPriority.FlexibleCombo - our "standard" priority
QueueGCD(NextComboSingleTarget(), primaryTarget, GCDPriority.FlexibleCombo);

Results

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!