The Addon API allows external Bukkit/Paper plugins to extend MinecraftLuaScripting by registering custom Lua API tables that become available inside every loaded script.
An addon is a separate plugin that:
- Declares
depend: [ MinecraftLuaScripting ]in itsplugin.yml. - Obtains the
MinecraftLuaScriptingplugin instance at runtime viaMinecraftLuaScripting.getInstance()(or the BukkitServicesManager). - Creates an
Addonsubclass that exposes one or moreAddonApitables. - Registers the addon through
mls.getAddonManager().registerAddon(this, addon), passing its own plugin instance as the owner so the addon is automatically unregistered when the owning plugin is disabled.
Each AddonApi table is installed as a global Lua variable with the name returned by getApiName(). New scripts loaded after the addon is registered automatically receive the APIs.
MinecraftLuaScripting mls = MinecraftLuaScripting.getInstance();or via the ServicesManager:
RegisteredServiceProvider<MinecraftLuaScripting> provider =
Bukkit.getServicesManager().getRegistration(MinecraftLuaScripting.class);
MinecraftLuaScripting mls = provider != null ? provider.getProvider() : null;Base class all addons must extend.
public abstract class Addon {
public abstract Set<AddonApi> getApiTables();
public abstract String getName();
public abstract String getVersion();
// optional lifecycle hooks
public void onEnable(MinecraftLuaScripting plugin);
public void onDisable();
public void onScriptsReloaded();
public void log(String message);
public void logWarning(String message);
public void logSevere(String message);
}Methods
getApiTables(): Returns the set of API tables this addon exposes.getName(): Returns the addon name. Must be non-empty and unique among registered addons.getVersion(): Returns the addon version string.onEnable(plugin): Called after the addon has been registered and its APIs applied to the Lua state.onDisable(): Called after the addon has been unregistered and its APIs removed.onScriptsReloaded(): Called after/mls reloadscriptsrecreates the Lua state and re-applies the addon's APIs.log,logWarning,logSevere: Convenience loggers prefixed with the addon name.
Base class for each Lua API table an addon exposes. There are three ways to define the table contents:
public class MyApi extends AddonApi {
@Override
public String getApiName() { return "MyApi"; }
@LuaFunction
public LuaValue greet(LuaValue name) {
return LuaValue.valueOf("Hello, " + name.tojstring());
}
@LuaFunction("add")
public LuaValue addNumbers(LuaValue a, LuaValue b) {
return LuaValue.valueOf(a.checkint() + b.checkint());
}
}Annotated methods must return LuaValue (or void) and take either zero or more LuaValue parameters or a single LuaValue[] parameter for varargs. Missing arguments are passed as LuaValue.NIL. The Lua field name defaults to the method name and can be overridden via the annotation value.
AddonApi api = AddonApi.builder("MyApi")
.function("greet", args -> LuaValue.valueOf("Hello, " + args[0].tojstring()))
.constant("version", LuaValue.valueOf("1.0"))
.build();public abstract class AddonApi extends LuaTable {
public Map<String, LuaValue> getTableContents(); // optional, defaults to null
public abstract String getApiName();
public static Builder builder(String apiName);
}getApiName(): Name of the global Lua variable that will hold this table.getTableContents(): Optional map of Lua field names toLuaValues.
Base class for Lua functions exposed by addons. It transparently handles both dot syntax (MyApi.fn(...)) and colon syntax (MyApi:fn(...)) by stripping the implicit self argument, so addon authors never need to worry about the calling convention:
public abstract class AddonFunction extends VarArgFunction {
protected abstract LuaValue execute(LuaValue[] args);
}Exceptions thrown from execute are wrapped in a LuaError and surface as script errors.
Functional interface used with the builder:
@FunctionalInterface
public interface LuaCallable {
LuaValue invoke(LuaValue[] args);
}public class AddonManager {
public boolean registerAddon(Plugin owner, Addon addon); // preferred
public boolean registerAddon(Addon addon);
public boolean unregisterAddon(String name);
public Addon getAddon(String name);
public List<Addon> getRegisteredAddons();
}Obtained via MinecraftLuaScripting.getAddonManager().
Prefer registerAddon(owner, addon): when the owning plugin is disabled, its addons are automatically unregistered and their globals removed from the Lua state. Registration also warns if an addon declares an API name that conflicts with a built-in global (PlayerApi, WorldApi, etc.).
AddonApi extends ProtectedLuaTable: once the table is built and installed, it is locked and scripts can no longer write to it. Its global name is locked too, exactly like the built-in APIs (see API Guardrails for the full list of rejected operations and error messages).
What this means for addon authors:
- Build the table completely before it is installed. The table is populated from
@LuaFunctionmethods, the builder, andgetTableContents(), and is locked immediately afterwards. Mutating theLuaTableafter that throws aLuaError. - Locking is deep. Any plain nested
LuaTable— e.g.builder.table("sub", subTable)— is replaced by a locked copy when the API is locked, soMyApi.sub.fn = evilis rejected as well. Because it is a copy, later writes to the instance you passed in are not reflected in Lua; finish building the nested table first. - Nested metatables are frozen as well, and shared or cyclic nested tables are frozen once and keep sharing the same locked copy.
- Scripts cannot shadow your API name with a local, a function parameter, or a loop variable; such a script is refused at load time. Pick an API name distinctive enough that scripts will not want it as a variable name.
- Unregistering the addon unlocks and removes its global, so a script loaded afterwards may reuse the name freely.
If your addon invokes Lua itself — a callback a script handed you, a function stored in your API table — route the call through MinecraftLuaScripting.getLuaDispatcher() rather than calling LuaValue.call() directly:
plugin.getLuaDispatcher().dispatch(scriptName, "MyAddon callback", () -> callback.call());The dispatcher holds the Lua lock, runs the call on the main server thread, attributes anything the callback registers to the owning script, and opens the script-timeout-ms execution budget around it. A direct call() gets none of that: it runs with no budget, so a script that never returns freezes the server exactly as it would have before the watchdog existed.
MyAddon.java
public class MyAddon extends Addon {
@Override
public Set<AddonApi> getApiTables() {
return Set.of(
AddonApi.builder("MyApi")
.function("greet", args -> LuaValue.valueOf("Hello, " + args[0].tojstring()))
.constant("version", LuaValue.valueOf("1.0"))
.build()
);
}
@Override
public String getName() { return "MyAddon"; }
@Override
public String getVersion() { return "1.0"; }
@Override
public void onEnable(MinecraftLuaScripting plugin) {
log("MyAddon is ready");
}
}PluginMain.java
import me.touchie771.minecraftLuaScripting.MinecraftLuaScripting;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
public class PluginMain extends JavaPlugin {
@Override
public void onEnable() {
MinecraftLuaScripting mls = MinecraftLuaScripting.getInstance();
if (mls == null) {
getLogger().severe("MinecraftLuaScripting not found, disabling.");
Bukkit.getPluginManager().disablePlugin(this);
return;
}
mls.getAddonManager().registerAddon(this, new MyAddon());
}
}After the addon is registered, the API name becomes a global variable:
MyApi.greet("world") -- returns "Hello, world"
MyApi:greet("world") -- also works: AddonFunction strips the implicit self argumentFunctions defined via @LuaFunction, the builder, or AddonFunction support both dot and colon syntax. Only raw OneArgFunction/TwoArgFunction values placed directly into getTableContents() require dot syntax.
- Register addons during
onEnableso they are available before scripts load. - Addon API names must not conflict with existing globals such as
PlayerApi,WorldApi, or another addon's API name. A warning is logged at registration if a name conflicts with a built-in global. - Unregistering an addon removes its API tables from the currently loaded Lua state.
- Addons registered with an owning plugin are automatically unregistered when the owner is disabled.
/mls viewaddonslists each registered addon along with its API table names.- API tables are locked once installed; see API Table Locking.