Custom Triggers

DISCLAIMER
The API update will only be available in Mythic Dungeons 1.3.0+!

A trigger is the "when" half of a dungeon element. It watches for something to happen (a player types in chat, a mob dies, a redstone signal changes) and then fires, which runs the function it is attached to.

This page assumes you have read Getting Started with Elements and GUI Menus, which cover the annotations, the two required constructors, the saved-field system and the hotbar menu. Everything below is specific to triggers.

The skeleton

Extend DungeonTrigger, annotate the class with @DeclaredTrigger, and implement two abstract methods.

@DeclaredTrigger
public class TriggerChat extends DungeonTrigger {

    @SavedField private String text = "DEFAULT";

    public TriggerChat() {
        super("Chat Message");
        setCategory(TriggerCategory.PLAYER);
        setHasTarget(true);
    }

    public TriggerChat(Map<String, Object> config) {
        super("Chat Message", config);
        setCategory(TriggerCategory.PLAYER);
        setHasTarget(true);
    }

    @Override
    public MenuButton buildMenuButton() { return null; }

    @Override
    public void buildHotbarMenu() { }
}
Method Purpose
MenuButton buildMenuButton() The item shown in the trigger picker. Must be declared on this class, and must not return null.
void buildHotbarMenu() The in-game options for this trigger. Add items to the existing menu field; do not create a new one.

Notice what is not in that list: there is no abstract "check" method. A trigger is not polled. It watches for its own condition and calls trigger(...) when it sees it, usually from a Bukkit event handler.

Unlike a function, the String you pass to super(...) is both the trigger's namespace and its display name, and it is the name shown on the Trigger button in the attached function's hotbar menu.

Configuring the trigger

Set these from both constructors, since neither runs when the other does.

Call What it controls
setCategory(TriggerCategory) Which tab of the trigger picker your trigger appears in. One of DUNGEON, PLAYER, GENERAL, META, ROOM.
setHasTarget(boolean) Default false. Set it to true if your trigger fires with a specific player attached. This is what lets the attached function use the PLAYER target type; when it is false, that option is skipped and a PARTY target is displayed as "ALL PLAYERS".
waitForConditions = true Assign the field directly. See Firing below. Off by default.

allowRetrigger is not yours to set from the constructor. It is seeded from the attached function's isAllowRetriggerByDefault() when the trigger is initialised, and the builder can change it in-game.

Firing

Call one of these when your trigger's condition is met:

    trigger();                          // no player attached
    trigger(MythicPlayer aPlayer);      // this player is the trigger's target
    trigger(MythicPlayer aPlayer, boolean disable);

The shorter forms default disable to !allowRetrigger, which is almost always what you want: a one-shot trigger disables itself after firing, a repeatable one does not. Only pass the flag explicitly if you need to override that.

Firing is not immediate. The trigger's configured delay (delayTicks) is applied first: the whole check runs that many ticks later. Then the conditions are evaluated, then TriggerFireEvent is posted so other plugins can cancel it, and only then does the attached function execute. If the trigger is set to wait for its conditions and retriggering is off, the check repeats once a second until the conditions pass.

That last part is what waitForConditions selects. With it off (the default), the check runs once after the delay and is dropped if the conditions fail. With it on, and retriggering off, the trigger keeps re-checking every second until its conditions are satisfied. Triggers for one-off moments (a mob dying, a player leaving) use it, so that a condition which is not yet true does not throw the event away.

You do not check conditions yourself, post the event yourself, or call the function yourself. trigger(...) does all of that.

In a continuous dungeon, each player may only fire a given trigger once, and the trigger is never disabled by firing. Both are handled for you inside trigger(...).

Listening for the event

Declare a Bukkit @EventHandler directly on the trigger class. Do not implement Listener and do not register anything: Mythic Dungeons wires it up at registration time and calls your method on every copy of the trigger in every running dungeon. That last part is why the first line of the handler always has to be a scope check.

    @EventHandler(priority = EventPriority.LOW)
    public void onChat(AsyncPlayerChatEvent event) {
        Player player = event.getPlayer();
        if (!instance.isPlayerInInstance(player)) return;

        if (!event.getMessage().equalsIgnoreCase(text)) return;
        if (!matchesRoom(player.getLocation())) return;

        event.setCancelled(true);
        trigger(MythicDungeons.inst().getMythicPlayer(player));
    }

If your trigger cannot be expressed as an event (a timer, a poll), start it in onEnable() instead and cancel it in onDisable().

Room limiting

In a procedural dungeon the same trigger exists once per copy of the room. Without a room check, a chat message typed in one copy fires the trigger in all of them.

Call addRoomLimitToggleButton() at the end of your buildHotbarMenu() to give builders the "limit to room" toggle (it costs one hotbar slot), then honour the resulting limitToRoom field before you fire:

    private boolean matchesRoom(Location origin) {
        InstanceProcedural inst = instance.as(InstanceProcedural.class);
        if (inst == null) return true;   // not a procedural dungeon, nothing to limit
        if (!limitToRoom) return true;   // the builder turned it off

        return inst.getRoom(origin) == inst.getRoom(location);
    }

location is the trigger's own position, inherited from DungeonElement.

Optional overrides

Method When it runs
void onEnable() The trigger is bound to a running instance. Start timers here.
void onDisable() The instance is tearing down, or a one-shot trigger has fired. Cancel here whatever you started in onEnable(), or you will leak the dungeon world. See Avoiding Memory Leaks.
void onTrigger(TriggerFireEvent event) After the attached function has executed. For side effects that belong to the trigger rather than the function.
void initLegacyFields(Map<String, Object> config) Field loading, for renamed saved fields. See Getting Started with Elements.

disable(), checkConditions(...) and the two-argument trigger(...) are final. init() and enable(...) are managed for you and should not need overriding.

A worked example

The built-in Chat Message trigger, trimmed to its essentials. (The real one looks its display strings up from Mythic Dungeons' own language file with LangUtils, and offers case-sensitivity, exact-match and delay options; literal strings and a single option are used here.)

@DeclaredTrigger
public class TriggerChat extends DungeonTrigger {

    @SavedField private String text = "DEFAULT";

    public TriggerChat(Map<String, Object> config) {
        super("Chat Message", config);
        setCategory(TriggerCategory.PLAYER);
        setHasTarget(true);
    }
    public TriggerChat() {
        super("Chat Message");
        setCategory(TriggerCategory.PLAYER);
        setHasTarget(true);
    }

    @Override
    public MenuButton buildMenuButton() {
        MenuButton triggerButton = new MenuButton(Material.NAME_TAG);
        triggerButton.setDisplayName("&aChat Message");
        triggerButton.addLore("&eFires when a player types");
        triggerButton.addLore("&ethe required text in chat.");

        return triggerButton;
    }

    @EventHandler(priority = EventPriority.LOW)
    public void onChat(AsyncPlayerChatEvent event) {
        Player player = event.getPlayer();
        if (!instance.isPlayerInInstance(player)) return;

        if (!event.getMessage().equalsIgnoreCase(text)) return;
        if (!matchesRoom(player.getLocation())) return;

        event.setMessage("");
        player.playSound(player.getLocation(), "minecraft:entity.experience_orb.pickup", 0.5F, 1.2F);
        event.setCancelled(true);

        trigger(MythicDungeons.inst().getMythicPlayer(player));
    }

    private boolean matchesRoom(Location origin) {
        InstanceProcedural inst = instance.as(InstanceProcedural.class);
        if (inst == null) return true;
        if (!limitToRoom) return true;

        return inst.getRoom(origin) == inst.getRoom(location);
    }

    @Override
    public void buildHotbarMenu() {
        menu.addMenuItem(new ToggleMenuItem() {
            @Override
            public void buildButton() {
                button = new MenuButton(Material.REDSTONE_TORCH);
                button.setDisplayName("&d&lAllow Retriggering");
                button.setEnchanted(allowRetrigger);
            }

            @Override
            public void onSelect(Player player) {
                allowRetrigger = !allowRetrigger;
            }
        });

        menu.addMenuItem(new ChatMenuItem() {
            @Override
            public void buildButton() {
                button = new MenuButton(Material.PAPER);
                button.setDisplayName("&d&lEdit Required Text");
            }

            @Override
            public void onSelect(Player player) {
                MessageUtils.sendMessage(player, Util.fullColorModern("&eWhat should the player type in chat?"));
                MessageUtils.sendMessage(player, Util.fullColorModern("&eCurrent text: &6" + text));
            }

            @Override
            public void onInput(Player player, String message) {
                text = message;

                MessageUtils.sendMessage(player, Util.fullColorModern("&aSet required text to '&6" + message + "&a'"));
            }
        });

        addRoomLimitToggleButton();
    }
}

Registering it

From your own plugin's onEnable():

    MythicDungeons.inst().registerTriggers("com.example.myplugin.triggers");

or, one class at a time:

    MythicDungeons.inst().registerTrigger(TriggerChat.class);

Add MythicDungeons to depend in your plugin.yml so it loads first. Triggers registered after Mythic Dungeons has built its menus are inserted into the trigger picker in place, so a late registration still shows up without a restart. See Getting Started with Elements for the full registration rules.

Updated Aug 19, 2026