Custom Elements

DISCLAIMER
The following is a feature only available in Mythic Dungeons 1.3.0+!

Mythic Dungeons has an extensive API for creating your own functions, triggers, and conditions for use in dungeons. We call these "Dungeon Elements". Many of them share code, and require a little work to setup in order to support the in-game menus needed to access them. Here, we will explore concepts that apply to all elements.

Once you have read this page, continue to the element you want to build: Custom Functions, Custom Triggers or Custom Conditions.

Note: dungeon elements are compiled against the full plugin jar, not the slim API artifact published to mvn.lumine.io. MenuButton, which every element has to return from buildMenuButton(), lives outside the published packages. See Introduction to API.

Important Annotations

Declaring an element

Mark your class with the annotation matching its kind. This is what package scanning looks for.

@DeclaredFunction  public class FunctionMessage extends DungeonFunction { }
@DeclaredTrigger   public class TriggerChat     extends DungeonTrigger  { }
@DeclaredCondition public class ConditionChance extends TriggerCondition { }

All three are runtime-retained type annotations with no attributes. They do nothing on their own: they are only read when a package is scanned (see Registering your elements below). If you register a class one at a time instead, the annotation is optional.

Saved Fields

Mythic Dungeons will automatically save and load values stored on a Dungeon Element when players configure them in-game, however you must specify what you want to save and load! This is indicated with the @SavedField annotation.

@SavedField private String message; // Mythic Dungeons will save and load this value.
private int delay; // Mythic Dungeons will NOT save and load this value.

Saved fields will also be displayed in the menus of meta elements, such as the multi-function.

The Java field name is the config key, both when saving and when loading. Renaming a saved field therefore orphans every value already written into existing dungeons.

@SavedField accepts a legacyNames attribute (@SavedField(legacyNames = {"oldName"})). It is honoured by the plugin's general-purpose serializer, but not by dungeon elements: element loading matches on the field name alone. To rename a saved field on an element, override initLegacyFields(Map) as described below.

Hidden Fields

But what if you don't want your saved field to appear in the menus of meta elements? This is achieved with the @Hidden annotation.

@SavedField private String message; // Mythic Dungeons will display this in the meta element menu.
@Hidden @SavedField private int delay; // Mythic Dungeons will NOT display this in the meta element menu.

@Hidden affects display only. The field is still saved and loaded exactly as before. It is honoured in the five editor windows that list an element's parameters: the condition editor, the gate-trigger editor, the multi-function editor, the multi-function trigger editor, and the conditional-else editor. Anywhere else, it has no effect.

Relocatable

@Relocatable exists in api.annotations, with a precise() attribute for double-precision rotation. It is currently inert. The two methods on DungeonElement that read it, for rotating and shifting a Location field, are commented out in the source. Do not rely on it to move your element's locations. Procedural room rotation is handled elsewhere, and DungeonFunction.offset(int x, int z) shifts every Location-typed @SavedField on a function without consulting @Relocatable at all.

Required Constructors

Due to constraints with automatically saving and loading elements, all dungeon elements must include two constructors.

  • A constructor with no parameters: public FunctionMessage()
  • A constructor taking a <String, Object> map as a parameter: public FunctionMessage(Map<String, Object> config)
    public FunctionMessage() {
        super("Message"); // Parameter for the element's unique name.
    }

    public FunctionMessage(Map<String, Object> config) {
        super("Message", config); // Parameter for the element's unique name and the config map.
    }

The first constructor is used to create a brand new instance of the element with default options. The second constructor is used to load an existing element and all of its saved fields, which are stored in the map.

Within these constructors, you can configure certain details of your element, such as what category it belongs to, its colour when displayed, whether it requires a target, etc. What options are available for different kinds of elements will be explored in their tutorials.

You shouldn't ever need to call these constructors yourself! They are only required for Mythic Dungeons' internal systems to be able to use your custom element!

Two hard requirements come from the way registration works, and both fail loudly rather than silently:

  • The no-argument constructor must be public. Registration instantiates your class with getDeclaredConstructor().newInstance() purely to ask it for its menu button.
  • buildMenuButton() must be declared on your own class. It is looked up with getDeclaredMethod, which does not see inherited methods. Inheriting it from an intermediate base class of your own is not enough; override it on the concrete class.

If either is missing you get ERROR :: The <function|trigger|condition> <ClassName> has a misconfigured menu button declaration! in the console. If buildMenuButton() is declared but returns null, you get ... does not have a menu button! instead and the element is dropped from the browser menu. (For conditions that second message says "function selection menu"; that wording is a copy-paste artefact in the source, it does mean the condition browser.)

Loading Hooks

Beyond the constructors, DungeonElement gives you three hooks that run during field loading. All three are called from initFields(), in this order, immediately after every @SavedField has been populated from the config map:

Hook Override it when
protected void initLegacyFields() You need to read raw keys out of the saved config map without the map being passed in.
protected void initLegacyFields(Map<String, Object> config) You renamed a saved field and want old dungeons to keep working.
protected void initAdditionalFields() You need to finish initialising objects that are stored on your element and have their own fields to set up.

The renaming case is the common one. If your field message was renamed to msg:

    @Override
    public void initLegacyFields(Map<String, Object> config) {
        if (config.containsKey("message")) this.msg = (String) config.get("message");
    }

On functions and triggers, only the Map form is yours to override. DungeonFunction and DungeonTrigger both declare initLegacyFields() and initAdditionalFields() as final and use them internally, for the trigger/location fields and the retrigger/condition fields respectively. Conditions may override all three.

Element Lifecycle

Elements are long-lived objects that get enabled and disabled with the instance they belong to. The two hooks you are meant to override are onEnable() and onDisable(), present on functions, triggers and conditions alike.

Stage What happens
Construction One of your two constructors runs.
init() Saved fields are loaded and the hotbar menu is built. A function also initialises its trigger here, and a trigger its conditions. Runs once; a second call is ignored.
enable(...) The element is bound to its instance and location, then your onEnable() runs. Functions and triggers are also registered as listeners on that instance at this point.
Execution A function's runFunction(...), a trigger's firing path, or a condition's check(...).
disable() Listener registration is torn down (functions and triggers), the menu is unregistered, and your onDisable() runs.

Use onEnable() for anything that needs to start with the dungeon, such as a repeating task, and onDisable() to cancel exactly that. Anything you start in onEnable() and do not cancel in onDisable() outlives the instance, and because every element holds a reference to its instance, that keeps the dungeon world in memory. See Avoiding Memory Leaks.

Listening to Bukkit Events

Functions and triggers may declare @EventHandler methods directly on the element class:

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

You do not register these yourself, and the class does not implement Listener. Mythic Dungeons scans the class at registration time and wires each handler through a shared dispatcher, which then invokes your method on every enabled copy of your element across every active instance. The declared priority is honoured.

Because your method is called for every copy of the element in every running dungeon, the first thing it must do is decide whether this event is any of its business. Check the instance, as above.

Conditions do not get this. TriggerCondition registration does no event wiring at all, so an @EventHandler on a condition is dead code. Conditions are evaluated on demand through check(...).

Registering your elements

Nothing is registered automatically. From your own plugin's onEnable(), ask Mythic Dungeons to scan your package:

    MythicDungeons md = MythicDungeons.inst();

    md.registerFunctions("com.example.myplugin.functions");
    md.registerTriggers("com.example.myplugin.triggers");
    md.registerConditions("com.example.myplugin.conditions");

Each call scans that package for classes annotated with the matching @Declared... annotation and registers every one it finds. You can also register a single class, in which case the annotation is not needed:

    md.registerFunction(FunctionMessage.class);
    md.registerTrigger(TriggerChat.class);
    md.registerCondition(ConditionChance.class);

ConfigurationSerialization.registerClass is called for you by all three; do not call it yourself.

Register on your plugin's enable, and make sure Mythic Dungeons loads first by adding it to depend in your plugin.yml. Registering a trigger after Mythic Dungeons has already built its menus works: the trigger picker is updated in place. The function and condition browsers are built once at startup, so a function or condition registered late is usable but may not appear in its browser until the next restart.

Updated Aug 19, 2026