lstwoMODS Core • Creating Mods

Extending Macros

The MacroRegistry class is used for all macros. It automatically includes all ModActions and ModSettings that are marked as Macroable (default).

Step IDs from ModRegistry steps are {DeclaringType.FullName}.{Member} with .get or .set at the end for settings. Categories come from the Category field and are / separated. For ModRegistry steps it’s Mods/{mod} for actions and Mods/{mod}/{setting} for settings.

Creating your own Steps

Let’s say we have this method:

public static void MyMacroStep(string msg)
{
    Plugin.Logger.LogInfo(msg);
}

We can turn this into a macro like so, once at Plugin.Start:

MacroRegistry.Register(
    "mymod.log", // id
    "Log", // Macro step name
    (Action<string>)MyMacroStep, // delegate
    "My Mod" // Category name
);

Parameters and return types are found using reflection. Calling Register again with the same id will replace the step with the new one.

MacroMethodDescriptor

When you need more control over the macro step, you can instead pass a MacroMethodDescriptor into the Register method:

MacroRegistry.Register(new MacroMethodDescriptor
{
    Id          = "mymod.notify",
    Label       = "Show Notification",
    Category    = "My Mod",
    PickerLabel = "Show Notification",
    Parameters  = new[]
    {
        new MacroParam { Name = "Message",  Type = typeof(string) },
        new MacroParam { Name = "Severity", Type = typeof(NotifyKind) },
        new MacroParam { Name = "Seconds",  Type = typeof(float) },
    },
    Execute = args =>
    {
        Notifications.Show(
            (string)args[0],
            (NotifyKind)MacroValues.Coerce(args[1], typeof(NotifyKind)),
            (float)MacroValues.Coerce(args[2], typeof(float))
        );

        return null;
    },
});

Arguments can arrive from the widget, an expression or a JSON literal loaded from disk, so you need to use MacroValues.Coerce for any parameter that isn’t a string.

Returning Data

To return something from a step you can set the ReturnType:

MacroRegistry.Register(new MacroMethodDescriptor
{
    Id         = "mymod.getHealth",
    Label      = "Get Health",
    Category   = "My Mod",
    ReturnType = typeof(float),
    Execute    = _ => Player.Health,
});

Waiting

To wait you can return an IEnumerator and it will yield it within the macro’s coroutine:

MacroRegistry.Register(new MacroMethodDescriptor
{
    Id = "mymod.waitForLoad",
    Label = "Wait For Level Load",
    Category = "My Mod",
    ReturnType = typeof(IEnumerator),
    Execute = _ => MacroFlow.WaitUntil(() => LevelManager.IsLoaded),
});

The MacroFlow class provides WaitUntil, WaitFrames, Wait and WaitWhile which all stop when the macro gets stopped.

If you made your own Coroutine without using MacroFlow you can use a MacroCallChain object to make it cancellable:

Execute = args =>
{
    var chain = MacroRunner.CurrentChain;   // capture here, not inside the routine, otherwise it will be null
    return FadeRoutine((float)MacroValues.Coerce(args[0], typeof(float)), chain);
},

private static IEnumerator FadeRoutine(float seconds, MacroCallChain chain)
{
    var end = Time.time + seconds;

    while (Time.time < end)
    {
        if (chain != null && chain.Stopped) yield break;   // Stop cancels within a frame
        Fader.Set(1f - (end - Time.time) / seconds);
        yield return null;
    }
}

A bare IEnumerator step doesn’t return anything. If you want to return something from a Coroutine you need to specify the OutputType and return a MacroValueRoutine:

MacroRegistry.Register(new MacroMethodDescriptor
{
    Id = "mymod.awaitPurchase",
    Label = "Await Purchase",
    Category = "My Mod",
    ReturnType = typeof(IEnumerator), // Return type stays IEnumerator
    OutputType = typeof(bool), // Output type defines the actual coroutine return type for the editor
    Execute = _ =>
    {
        var chain = MacroRunner.CurrentChain;
        return new MacroValueRoutine(r => AwaitPurchase(r, chain));
    },
});

private static IEnumerator AwaitPurchase(MacroValueRoutine r, MacroCallChain chain)
{
    while (!Shop.Settled)
    {
        if (chain != null && chain.Stopped) yield break;
        yield return null;
    }

    r.Result = Shop.LastSucceeded;
}

Custom Value Type

When creating steps using parameter types that the editor doesn’t already know how to display, you need to register it with a way for the user to select a value:

MacroTypes.Register(new MacroTypeDescriptor
{
    Type = typeof(Waypoint),
    DisplayName = "Waypoint",
    DefaultModeId = "nearest",
    ResolveFromString = FindByName,
    ContextCacheKey = v => (v as Waypoint)?.Id,
    Modes =
    {
        new MacroTypeMode
        {
            Id = "nearest", Label = "Nearest Waypoint",
            Resolve = _ => Nearest(),
        },
        new MacroTypeMode
        {
            Id = "byName", Label = "By Name",
            Param   = new MacroParam { Name = "name", Type = typeof(string) },
            Resolve = args => FindByName((string)args[0]),
            Choices = () => All().Select(w => w.Name).ToArray(),
        },
    },
});

A MacroTypeMode is a mode that can be selected alongside ‘Expression’.

Custom Triggers

MacroTriggerRegistry.Register(new MacroTriggerDescriptor
{
    Id     = "mymod.onDamage",
    Label  = "On Player Damaged",
    Params = new[] // Parameters for the config in the UI
    {
        new MacroTriggerParam
        {
            Key = "minAmount", Label = "Minimum Damage",
            Type = typeof(float), Default = 0f,
            Tooltip = "Ignore hits smaller than this",
        },
    },
    Outputs = new[] // Optional outputs that are given to the macro to use in expressions
    {
        new MacroTriggerOutput { Key = "amount", Label = "Damage", Type = typeof(float) },
        new MacroTriggerOutput { Key = "source", Label = "Attacker", Type = typeof(Player) },
    },
    Arm = ctx =>
    {
        var minAmount = ctx.GetFloat("minAmount"); // Get a parameter

        void OnDamaged(float amount, Player source) // Subscriber method
        {
            if (amount < minAmount) return;
            ctx.Fire(("amount", amount), ("source", source)); // Fire macro with parameters
        }

        Health.Damaged += OnDamaged; // Subscribe to an event to call the macro
        return new CallbackDisposable(() => Health.Damaged -= OnDamaged); // Unsubscribe when a macro changes or gets deleted
    },
});

The trigger must trigger on the main thread, so if it doesn’t you can do this:

MainThread.Enqueue(() => ctx.Fire(/* params */));

Custom Macro UI

If you need special UI for your parameters you can create a MacroStepEditor:

public sealed class MyStepEditor : MacroStepEditor
{
    // One instance is shared by every step using this method, so keep no per-step state in fields.
    // Transient handles go in ctx.Tag, persisted data in the step.

    public override IEnumerable<BaseUIElement> Build(MacroStepEditorContext ctx)
    {
        var data = ctx.GetData<MyStepData>();
        var input = new InputText($"{ctx.IdScope}-name", value: data.Name, onChanged: v =>
        {
            data.Name = v;
            ctx.SetData(data);
            ctx.NotifyEdited();
        });

        ctx.Tag = input;
        return new BaseUIElement[] { input };
    }

    public override void Refresh(MacroStepEditorContext ctx) { /* push stored data into widgets */ }

    public override string Summary(MacroStep step) => "..."; // appended to the collapsed header
}

Then you can use it like this:

MacroRegistry.Register(new MacroMethodDescriptor
{
    Id = "mymod.custom",
    Label = "My Custom Step",
    Category = "My Mod",
    CustomEditor = new MyStepEditor(),
    ExecuteCustom = ctx =>
    {
        var data = ReadData(ctx.Step);
        return DoThing(data, ctx.Eval("someExpression"));
    },
});

private static MyStepData ReadData(MacroStep step)
{
    if (step.Custom == null) return new MyStepData();
    try { return step.Custom.ToObject<MyStepData>() ?? new MyStepData(); }
    catch { return new MyStepData(); }
}

Firing Macros from Code

Use MacroManager.Fire to run a macro:

var macro = MacroManager.FindByRef("my_macro");

MacroManager.Fire(macro);

FindByRef accepts a macro id, its slug or its name. A slug is the identifier version of the name, so My Macro! becomes my_macro. It throws when nothing matches and lists the available slugs in the message, only a null or empty string resolves to null instead.

You can also hand the run named values, the same ones a trigger would provide:

MacroManager.Fire(macro, new Dictionary<string, object>
{
    ["amount"] = 25f,
});

The macro reads those in expressions as amount or trigger("amount").

Always use Fire instead of MacroRunner.Run. It respects the Enabled flag, skips macros that are already running and alternates the On and Off step lists for Toggle macros. It also has to be called on the main thread.

To check or stop a macro:

if (MacroRunner.IsRunning(macro))
    MacroRunner.Stop(macro);

And to react to one finishing:

MacroRunner.Completed += (macro, success, returned) =>
    Plugin.Logger.LogInfo($"{macro.Name} finished, success: {success}, returned: {returned}");

This only fires for macros started by a trigger or by Fire, nested Run Macro calls don’t fire it.

Adding Your Steps to the Context Menu

Right clicking a mod setting or action offers Add to macro and Create hotkey. The Custom Mod UI guide covers ActionMenu and SettingMenu, which do this for ModAction and ModSetting members.

Steps you registered yourself have no such member, so use ModContextMenu.Items with the step id instead:

public override Container BuildPanel(string id)
{
    var button = new Button("Show Notification", ShowNotification).WithContentWidth();

    return new Container(id,
        new ContextMenu(
            $"{id}-notify-ctx", // Unique element id
            button, // The element that gets right clicked
            ModContextMenu.Items(
                "mymod.notify", // The macro step id
                "Show Notification" // Default name for the created macro or hotkey
            ).ToArray()
        )
    );
}

Pass true as the third argument for a step that sets a bool, that makes the created hotkey offer to flip the value on each press.