rune

docs · concepts

Lifecycle.

A Rune goes through three transitions: load (server start), reload (you ran /rune reload), and unload (server stop). What survives each transition is the most important thing to internalize.

Load

When Paper enables the Rune plugin, three things happen, in order. First, the host reflects the live classpath and writes bukkit.d.ts plus any types/<alias>.d.ts for plugins you declared in rune.jsonc. Second, the loader brings up the Node runtime (V8 isolate, Node context, event loop). Third, the host walks plugins/Rune/scripts/ and hands each script to the runtime in turn. Each script's top-level code executes immediately; decorators run during that execution, registering listeners and commands against Paper.

By the time the Paper console says "Done!" every script's top-level body has finished and every handler is live.

Reload

/rune reload is the iteration loop. It does five things, in order:

  1. Drains rune.serve() HTTP servers.
  2. Invalidates the ref registry — every outstanding live Bukkit reference goes stale.
  3. Tears down the Node runtime entirely.
  4. Brings up a fresh runtime and replays every script's source.
  5. Re-registers all handlers.

From your script's point of view, reload is a clean evaluation from scratch. Module-level consts, closures, timers — gone. You should write code that's safe to re-execute, and you should keep durable state in the right place.

What survives reload

Survives

  • Anything you put through rune.store(). The host writes it to plugins/Rune/store/<script-name>.json and replays it on the next boot.
  • External state: files, databases, network endpoints. Mongo keeps running across reloads; your reconnect logic should be a singleton promise so the first script-load wins.
  • The Paper main thread itself, plus the world, players, and every entity. Reload only resets your script, not the server.

Does not survive

  • Closure-captured Bukkit references. Re-fetch them after reload.
  • Module-level variables. If you cached a list of online players in a const, it's gone.
  • setInterval / setTimeout handles. Re-schedule from top-level code so reload re-creates them.
  • HTTP server bindings. They're rebuilt on reload.

A reload-safe pattern

The canonical reload-safe pattern is "register at top level, do work in handlers, persist through rune.store or an external system." Below is the lifecycle entry from ward — every method is short and side-effect-light because the containing class is constructed fresh on every reload.

@Listener
export class WardLifecycle {
  @EventHandler(Events.PlayerJoinEvent)
  async onJoin(e: PlayerJoinEvent) {
    try {
      await applyToPlayer(e.getPlayer());  // reads from Mongo, attaches perms
    } catch (err) {
      console.error("[ward] join apply failed:", err);
    }
  }

  @EventHandler(Events.PlayerQuitEvent)
  onQuit(e: PlayerQuitEvent) {
    const uuid = String(e.getPlayer().getUniqueId());
    dropPlayer(uuid);                       // detach attachment
    clearAutoPromoteState(uuid);            // clear local cache
  }
}

// Top-level boot work runs on every reload — keep it idempotent.
await ensureDBConnection();
await primeGroupCache();
registerDefaultTransactions();
startAutoPromoteTicker();
serveWebApi(3000);

Unload

On server shutdown the host stops accepting events, drains HTTP servers gracefully, and tears down the runtime. There's no per-script unload hook today — anything you need to close cleanly should be tied to a Paper shutdown event or a process signal handler.