Addon delivery
An addon is anything that registers types or providers against AbstractMenus. The plugin’s own built-in actions, rules, etc. register through the same SPI (Service Provider Interface - a standard Java mechanism that lets a plugin expose registration points for third-party jars to plug their implementations into) an external addon uses - there’s no privileged path for first-party content.
There are two delivery formats. Pick the one that fits your situation.
| Path 1 — AM-loaded addon | Path 2 — plugin-as-addon | |
|---|---|---|
| Jar drops into | plugins/AbstractMenus/addons/ | plugins/ |
| Loaded by | AbstractMenus | Bukkit |
| Manifest | addon.conf (HOCON) | plugin.yml |
| Main class extends | MenuExtension only (no-arg ctor) | JavaPlugin + MenuExtension |
Visible in /plugins | No | Yes |
Visible in /am addons list | Yes | Yes (tagged [as-plugin]) |
| Reload at runtime | /am addons reload <name> | /reload (whole server) |
| Per-jar classloader | Yes (isolated, child-first) | No (Bukkit shares) |
| Cross-plugin dependency order | pluginDependencies: in addon.conf | Bukkit’s depend: graph |
Pick Path 1 when your addon is purely an AM extension (a provider bridge, a custom action type, a custom rule). The addon can be reloaded without restarting the server, the classloader is isolated (your dependencies don’t clash with anyone else’s), and your type ids don’t collide with Bukkit plugins.
Pick Path 2 when your addon is also a real Bukkit plugin: registers Bukkit listeners, owns commands, exposes its own API for other plugins, or needs Bukkit’s depend: for hard ordering against another plugin.
For a pure provider bridge, both work. As an example, PlayerPointsAddon ships both branches side by side for comparison.
MenuExtension lifecycle
Section titled “MenuExtension lifecycle”Both paths implement MenuExtension. It has three lifecycle hooks plus three metadata methods.
public interface MenuExtension { default void onLoad(AbstractMenusApi api) {} void onEnable(AbstractMenusApi api); default void onDisable(AbstractMenusApi api) {}
default String name() { return getClass().getSimpleName(); } default String version() { return "unknown"; } default String targetApiVersion() { return null; }}onLoadruns for every loaded extension before any extension’sonEnable. Use it for ordering-independent setup. Do not register types here — other extensions may not be loaded yet.onEnableruns in dependency order. Register types and providers here.onDisableruns on server shutdown or/am addons reload. Type and provider registrations are cleaned up automatically by AbstractMenus — you only need to clean up things AbstractMenus can’t see (Bukkit listeners, scheduler tasks, JDBC connections, files, threads).
All three hooks run on the main server thread.
Path 1 — AM-loaded addon
Section titled “Path 1 — AM-loaded addon”Implement MenuExtension directly. No JavaPlugin, no plugin.yml. Ship the jar with an addon.conf at its root.
package com.example.myaddon;
import ru.abstractmenus.api.AbstractMenusApi;import ru.abstractmenus.api.MenuExtension;
public final class MyAddon implements MenuExtension {
public MyAddon() { // Required no-arg constructor. AbstractMenus instantiates the // class reflectively after the addon.conf parse succeeds. }
@Override public void onEnable(AbstractMenusApi api) { api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); }
@Override public String name() { return "MyAddon"; }
@Override public String version() { return "1.0.0"; }}name = "MyAddon"version = "1.0.0"main = "com.example.myaddon.MyAddon"authors = ["yourname"]description = "Adds a custom action type to AbstractMenus."targetApiVersion = "2.0.0"
# Names of other AM-loaded addons that must enable before this one.# AbstractMenus topo-sorts the dependency graph and reports cycles.addonDependencies = []
# Bukkit plugins that must be present and enabled. AbstractMenus skips# the addon (with a warning) if any of these are missing.pluginDependencies = []
# Bukkit plugins this addon optionally integrates with. Missing soft# deps log a notice but don't block the addon.pluginSoftDependencies = []Drop the resulting jar into plugins/AbstractMenus/addons/. AbstractMenus picks it up on startup, or via /am addons rescan at runtime.
addon.conf field reference
Section titled “addon.conf field reference”| Field | Required | Type | Notes |
|---|---|---|---|
name | Yes | string | Display name. Shown in /am addons list. Must be unique across all loaded addons. |
version | Yes | string | Free-form version string. |
main | Yes | string | Fully-qualified class name. Must implement MenuExtension and have a no-arg constructor. |
authors | No | string[] or string | Empty list if absent. |
description | No | string | Empty if absent. |
targetApiVersion | No | string | Diagnostic only. Logged on enable, shown in /am addons info. |
addonDependencies | No | string[] or string | Other AM-loaded addon names. Sorted into enable order via DAG. |
pluginDependencies | No | string[] or string | Bukkit plugin names. Hard-required. |
pluginSoftDependencies | No | string[] or string | Bukkit plugin names. Optional. |
Both list-typed fields accept either a HOCON list or a single string (auto-wrapped).
Path 2 — plugin-as-addon
Section titled “Path 2 — plugin-as-addon”Make your JavaPlugin also implement MenuExtension. Bukkit calls your plugin’s own onEnable; you forward to the SPI hook from there.
package com.example.myaddon;
import org.bukkit.plugin.java.JavaPlugin;import ru.abstractmenus.api.AbstractMenusApi;import ru.abstractmenus.api.MenuExtension;
public final class MyAddon extends JavaPlugin implements MenuExtension {
@Override public void onEnable() { AbstractMenusApi api = AbstractMenusApi.get(); if (api == null) { getLogger().severe("AbstractMenus API not available — disabling."); getServer().getPluginManager().disablePlugin(this); return; } onEnable(api); }
@Override public void onEnable(AbstractMenusApi api) { api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this); // ... register the rest of your types and providers }
@Override public String name() { return "MyAddon"; }
@Override public String version() { return getPluginMeta().getVersion(); }}name: MyAddonversion: 1.0.0main: com.example.myaddon.MyAddonapi-version: '1.21'depend: - AbstractMenusdepend: [AbstractMenus] is what guarantees AbstractMenus is enabled before your onEnable runs. Add other plugins (PlayerPoints, WorldGuard, etc.) to the same list if your registration logic touches their APIs at startup.
Dependency resolution
Section titled “Dependency resolution”When AbstractMenus loads addons:
- Every jar in
plugins/AbstractMenus/addons/is opened.addon.confis parsed and validated. Bad addons log a warning and are skipped. - Each addon’s
pluginDependenciesare checked. Missing hard deps mark the addon FAILED in the loaded set so operators see it in/am addons list. Soft deps log a notice. - The
addonDependenciesgraph is topo-sorted. Cycles fail the whole batch with a clear error. Addons with unsatisfied deps are flagged individually (transitive failures included — A → B → missing C marks both A and B failed). - Stage 1: every addon’s
onLoadruns. - Stage 2: every addon’s
onEnableruns in dependency order. - AbstractMenus parses menus. Your registered types are now resolvable from HOCON.
If onEnable throws, AbstractMenus rolls back any registrations the addon managed to make and marks the addon FAILED. The rest of the addons continue.
Owner-based cleanup
Section titled “Owner-based cleanup”Every register(...) call takes the MenuExtension instance as the last parameter - that’s the “owner” of the registration, the addon that made it:
api.actions().register("myAction", MyAction.class, new MyAction.Serializer(), this);// ^^^^// owner — your addonWhen your addon disables, AbstractMenus drops every type and provider it registered. You don’t (and can’t) call unregisterAll from your addon — the public API doesn’t expose it, so one addon can’t wipe another’s registrations.
For Path 1 this is automatic: AbstractMenus owns the addon’s lifecycle through its AddonManager and clears the registrations on disable. For Path 2, “disable” means JavaPlugin.onDisable and auto-cleanup doesn’t fire — your registration sits in the owner-tracking map until AbstractMenus itself shuts down. That’s harmless: the next onEnable overwrites the existing entry under the same id.
/am addons command tree
Section titled “/am addons command tree”Operators manage AM-loaded addons through /am addons:
/am addons list List every loaded addon (Path 1, Path 2, built-in)/am addons info <name> Full metadata: status, version, deps, error/am addons load <name> Load a Path 1 jar that's in addons/ but not yet loaded/am addons reload <name> Disable, rebuild classloader, re-enable a Path 1 addon/am addons rescan Scan addons/ for new jars and load any not yet loadedreload and load are Path 1 only (they need a jar in addons/). info works for all three kinds.
Example implementation
Section titled “Example implementation”PlayerPointsAddon is a small example addon that registers PlayerPoints as an economy provider. The repo has three branches:
master— README only, comparison overview.as-addon— Path 1 implementation.as-plugin— Path 2 implementation.
Both functional branches register the same provider through the same SPI; the diff between them is mostly the lifecycle plumbing.