docs · authoring
Java interop.
Bukkit isn't the only Java surface you have access to. Rune exposes the full JVM classpath through a small set of primitives: package proxies, static call helpers, and an interface-implementation bridge that lets you hand JS callbacks to Java code that expects to be called back.
Package proxies
The globals org, java, javax, net, io, and com are proxies. Reading a property navigates the classpath; the final access returns a Java class or static helper.
// Reach into the JDK.
const buf = java.nio.ByteBuffer.allocate(1024);
// Reach into Bukkit.
const material = org.bukkit.Material.DIAMOND_SWORD;
// Reach into Paper.
const component = io.papermc.paper.text.PaperComponents.legacySectionSerializer();The host writes .d.ts declarations for the packages it knows about (Bukkit, Paper, Adventure, declared plugins), so autocomplete works. Anything else returns a weakly-typed proxy — useful but you're on your own for type safety.
bukkit and rune.bukkit
bukkit is a convenience alias for org.bukkit. It's the most common entry point — bukkit.Bukkit for the static API, bukkit.Material for the enum, and so on. rune.bukkit is identical; the redundancy exists so handlers reading e.getPlayer() can stay short while files that prefer namespaced access can use rune.bukkit.Bukkit.getOnlinePlayers().
Calling static methods
Static method calls work through the proxy the same way instance calls work on live refs. rune.callStatic exists for the rare case where you need to call a static method whose class name isn't a clean dotted path (a synthetic class, an inner class with $) — pass the FQN as a string.
// Normal path:
const id = bukkit.Bukkit.getServer().getMaxPlayers();
// Awkward FQN:
const enumValue = rune.callStatic(
"net.example.internal.Inner$State",
"valueOf",
"ACTIVE",
);Implementing Java interfaces from JS
Sometimes a Java API expects you to hand it an implementation of an interface — a listener, a handler, a provider. rune.implement(fqcn, methods) generates a Java subclass at runtime that routes intercepted method calls to your JS object. ward uses it to register a PlaceholderAPI expansion, which PlaceholderAPI insists on receiving as a Java instance.
expansionRef = rune.implement(
"me.clip.placeholderapi.expansion.PlaceholderExpansion",
{
getIdentifier: () => "ward",
getAuthor: () => "ward",
getVersion: () => "1.0",
persist: () => true,
canRegister: () => true,
onRequest: (offlinePlayer: any, rawParams: any): string => {
const params = String(rawParams ?? "");
if (!offlinePlayer) return "";
const uuid = String(offlinePlayer.getUniqueId());
const display = getLastDisplay(uuid);
if (params === "prefix") return display.prefix;
if (params === "suffix") return display.suffix;
return resolveParam(uuid, offlinePlayer, params);
},
},
);
papi.PlaceholderAPI.registerExpansion(expansionRef);Each method is synchronous from Java's point of view: the JVM parks on your JS callback, waits for the return value, and coerces it to the declared type. Don't await inside an implement handler — JS will return a promise object and Java will probably crash trying to use it.
Plugin aliases
Declaring a plugin in rune.jsonc binds two things: a global alias and a generated .d.ts. Inside your code, the alias acts like any other namespace.
{
"plugins": {
"Vault": { "alias": "vault", "package": "net.milkbowl.vault" },
"PlaceholderAPI": { "alias": "papi", "package": "me.clip.placeholderapi" }
}
}if (typeof vault === "undefined") {
console.warn("[ward] Vault not installed; economy disabled.");
} else {
const eco = vault.RegisteredServiceProvider.get(
"net.milkbowl.vault.economy.Economy",
);
// ... transact normally
}Optional vs. required
A declared plugin is optional by default — if it's not installed on the server, the alias is undefined and your code should guard. To make it required, add the plugin to your capabilities.required array; future versions of the host will refuse to load your Rune unless the named plugin is present.
Scheduling on the main thread
Bukkit's API is mostly main-thread-only. JS code, by default, runs on the Node event loop. The bridge is rune.runOnMain(fn): it queues your function on the next tick and returns a promise of its result. Use it anywhere you've left the main thread (HTTP handlers, async handler bodies after the first await, callbacks from a non-Bukkit library).
const playerName = await rune.runOnMain(() => {
const p = bukkit.Bukkit.getPlayer(uuid);
return p ? p.getName() : null;
});