Skip to content

Plugin Bootstrap

Pétrus Pradella edited this page Nov 2, 2026 · 4 revisions

Plugin Bootstrap

IECPluginBootstrap is the platform-agnostic lifecycle contract of a plugin. A plugin that implements it writes its shared wiring once, in a Bukkit-free module, and each platform entry point adds only what that platform has.

It is optional: a Bukkit-only plugin can stay an ordinary JavaPlugin and call ECPluginManager.getOrCreateECorePluginData(this) from onEnable(), as Quick Start does. The bootstrap is what you reach for when the same plugin has to run on more than one platform.


The layering

Three files, and each has exactly one job:

File Module Holds
MyPluginBootstrap (interface) common the wiring both platforms share
MyPluginBukkitPlugin minecraft Bukkit-only extras
MyPluginHytalePlugin hytale Hytale-only extras
// common - the shared half
public interface MyPluginBootstrap extends IECPluginBootstrap {

    ECBootstrap<MyPluginBootstrap> INSTANCE = ECBootstrap.of(MyPluginBootstrap.class);

    /** The plugin running on this server, whatever the platform, or null while there is none. */
    static MyPluginBootstrap get() {
        return INSTANCE.get();
    }

    @Override
    default void onECPluginEnable() {
        ConfigManager.initialize(getPluginData());
        CommandRegisterer.registerCommands(getPluginData());
    }

    @Override
    default void onECPluginShutdown() {
    }

    @Override
    default void onECPluginReload() {
        ConfigManager.reload();
    }
}
// minecraft - the Bukkit half
@ECPlugin
public class MyPluginBukkitPlugin extends ECBukkitPlugin implements MyPluginBootstrap {

    @Override
    public void onECPluginEnablePost() {
        ECListener.register(getPluginData(), JoinListener.class);  // Bukkit-only
    }
}

ECBukkitPlugin and ECHytalePlugin are the bridges: they extend their platform's JavaPlugin and translate its lifecycle into the hooks below. They also answer getPluginData() from a field, so a plugin logging from a hot path reads a field instead of a map.


The lifecycle

The platform bridge calls the three run* entry points; a plugin implements the on* hooks.

Instantiate

runECPluginInstantiate(), from the bridge's instance initializer:

  1. publishes this instance - see the running instance
  2. onInstantiate() - optional, no-op by default

This is the earliest point there is an instance at all. Resolving a short logger name or declaring debug modules belongs here, not in the enable - see Logging and Debug Modules.

Enable

runECPluginEnable():

  1. publishes this instance again - an enable can follow a shutdown, when a plugin manager disables and re-enables the same object, and the constructor does not run twice
  2. getPluginData().loadDebugConfig() - so the whole enable already logs under the switches the file asks for
  3. onECPluginEnablePre() - optional
  4. onECPluginEnable() - mandatory, the shared wiring
  5. onECPluginEnablePost() - optional, the usual home for a platform main class's extras
  6. the successfully enabled! banner
  7. runOnFirstTick(), if it returns non-null, handed to ECScheduler.onMainThreadLater(...)

Shutdown

runECPluginShutdown():

  1. onECPluginShutdownPre() - the default is not a no-op: it takes back everything the core keeps for the plugin - its managers (regions, groups and quests written while its storage is still open; boosters, audit log, cooldowns, variables, values, contributions, rankings, dialogues, screens closed), its registrations (flags, currencies, destinations, types, multipliers, prompts, menus, notify channels, npcs, waypoints...) and every entry point it opened - listeners registered through ECListener.register, commands registered through FinalCMDManager, argument parsers registered through ArgParserManager (its own and the global ones it owns), and subscriptions taken in this plugin's name on the event bus. Each step runs on its own: one that throws is logged on the plugin's log and the rest still runs
  2. onECPluginShutdown() - mandatory, the shared teardown
  3. onECPluginShutdownPost() - optional
  4. the file loggers this plugin opened and left open
  5. this instance is unpublished

⚠️ Overriding onECPluginShutdownPre() replaces that cleanup. To keep it, ask for it back: super.onECPluginShutdownPre() from a subclass of a bridge, or IECPluginBootstrap.super.onECPluginShutdownPre() from a class implementing the interface directly. The qualified form does not compile anywhere else.

The order is the point: the plugin's entry points are gone before the main teardown closes the resources those listeners and commands would have touched.

A plugin can also leave without this hook - one that does not implement IECPluginBootstrap, or one that overrides it without calling super. On Bukkit, the core's PluginDisableEvent listener then calls ECPluginManager.removePluginData(name), which takes back the same set, step by step, except that the regions, groups and quests discard what they still had to write (the plugin's storage is gone by then). Running both is harmless: every step is idempotent.

Reload

onECPluginReload() is mandatory, and implementing it is what makes the plugin reloadable. A method annotated @ECPlugin.Reload wins over it, and is the only way to also declare reloadAfter - the plugins whose reload should drag this one along:

@ECPlugin.Reload(reloadAfter = {"SomeOtherPlugin"})
public void reload() {
    ConfigManager.reload();
}

The running instance: ECBootstrap

Most plugins want a static accessor - MyPluginBootstrap.get() - reachable from a listener, a command or a helper. ECBootstrap<T> is that instance, held by the core:

ECBootstrap<MyPluginBootstrap> INSTANCE = ECBootstrap.of(MyPluginBootstrap.class);

static MyPluginBootstrap get() {
    return INSTANCE.get();
}

Two lines, and no bookkeeping in any hook. The core fills the holder from runECPluginInstantiate() and from every runECPluginEnable(), and empties it once runECPluginShutdown() has finished. What that buys:

  • get() answers exactly while there is a plugin to hand out: null before it is built, and null again after it shut down. A dead instance is never handed to a caller.
  • Because the holder is emptied last, after the final shutdown hook, the plugin's own teardown can still reach itself through its accessor.
  • An outgoing instance only takes back its own publication. If a plugin manager re-instantiates the plugin, a late shutdown from the replaced object cannot unpublish the one that replaced it.
  • get() is a volatile field read - it costs what the hand-written static it replaces cost.

A holder takes whatever is an instance of its type, so a platform main class may declare one of its own beside the shared one when it needs the platform-typed object:

// minecraft, only if you need the Bukkit type - the shared accessor already answers for the rest
public static final ECBootstrap<MyPluginBukkitPlugin> INSTANCE =
        ECBootstrap.of(MyPluginBukkitPlugin.class);

Both then hold the same object. Prefer the shared accessor: MyPluginBootstrap.get().getLog() already works from a platform module.

A plain public static MyPlugin instance; assigned in an initializer does none of this: it keeps answering with a plugin that has already shut down.


Without the base class

A plugin that cannot change its base class implements IECPluginBootstrap directly and makes the same three bridge calls from its own lifecycle methods:

public class MyPlugin extends SomeOtherBase implements MyPluginBootstrap {

    public MyPlugin() {
        runECPluginInstantiate();
    }

    @Override
    public void onEnable() {
        runECPluginEnable();
    }

    @Override
    public void onDisable() {
        runECPluginShutdown();
    }
}

It gives up only what the bridges add on top: caching getPluginData() in a field.


What the interface gives you

Member
getPluginData() this plugin's ECPluginData (created on first access)
getLog() shorthand for getPluginData().getLog()
getPlugin() the platform object this bootstrap runs as, as Object - common cannot name it
runOnFirstTick() a task for the first tick after every plugin enabled, or null

Testing

A test double stands in for the bridge by making the same constructor call:

static final class TestPlugin implements MyPluginBootstrap {

    TestPlugin(ECPluginData data) {
        this.data = data;
        runECPluginInstantiate();
    }
}

A double that is built but never shut down has no lifecycle to empty its holder, so the test engine offers the reset:

@AfterEach
void teardown() {
    Bootstraps.forgetAll();
}

See Testing Toolkit for the rest of the apparatus.


See also

Clone this wiki locally