Repository navigation
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.
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 platform bridge calls the three run* entry points; a plugin implements the on* hooks.
runECPluginInstantiate(), from the bridge's instance initializer:
- publishes this instance - see the running instance
-
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.
runECPluginEnable():
- 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
-
getPluginData().loadDebugConfig()- so the whole enable already logs under the switches the file asks for -
onECPluginEnablePre()- optional -
onECPluginEnable()- mandatory, the shared wiring -
onECPluginEnablePost()- optional, the usual home for a platform main class's extras - the
successfully enabled!banner -
runOnFirstTick(), if it returns non-null, handed toECScheduler.onMainThreadLater(...)
runECPluginShutdown():
-
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 throughECListener.register, commands registered throughFinalCMDManager, argument parsers registered throughArgParserManager(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 -
onECPluginShutdown()- mandatory, the shared teardown -
onECPluginShutdownPost()- optional - the file loggers this plugin opened and left open
- this instance is unpublished
⚠️ OverridingonECPluginShutdownPre()replaces that cleanup. To keep it, ask for it back:super.onECPluginShutdownPre()from a subclass of a bridge, orIECPluginBootstrap.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.
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();
}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:nullbefore it is built, andnullagain 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 avolatilefield 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.
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.
| 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
|
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.
- Quick Start - the simpler, Bukkit-only entry point, without a bootstrap.
-
Platform Abstraction - how the bridges reach
commonat all. -
Logging and Debug Modules -
getLog(), and why debug modules are declared inonInstantiate(). - Events - the subscriptions the default pre-shutdown drains.
- Command Framework - the commands it unregisters with them.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Regions
- Groups
- Notifications
- NPCs
- Logic
- Quests
- Provenance
- Scheduler & Threading
- Items & NBT
- Screens
- Dialogues
- Menus
- Integrations
- Economy
- Boosters
- Deliveries
- Audit Log
- Dynamic Values
- Player Ignore
- Version Compatibility
Architecture & Reference