The Script API

Every script has a global Script object. It is your handle on the runtime: loading Java classes, pulling in libraries, and hooking into your own lifecycle.

Method Purpose
Script.loadClass(className) Load a Java class by fully qualified name
Script.addUnloadCallback(callback) Run cleanup before this script unloads
Script.getCaller() Get the Script object of whoever called into you
Script.loadLibrary(relativeJarPath) Add a local jar to the runtime classpath
Script.loadGradleDependency(coordinate) Download a Maven artifact and add it to the classpath
Script.measure(name, callback) Time a block of code and expose it in the profiler

Script.loadClass(className: string): any

Loads a Java class by its fully qualified name and gives you the JavaScript representation of it. Results are cached per file, so repeated calls are cheap.

The returned value behaves like a native JS class: call the constructor, read static fields, call static methods.

const ArrayList = Script.loadClass('java.util.ArrayList');
const list = new ArrayList();
list.add('Hello, PixelScript!');
console.log(list.size()); // 1

const System = Script.loadClass('java.lang.System');
const serverType = System.getenv('SERVER_TYPE');

// Enums work exactly as you would expect
const DayOfWeek = Script.loadClass('java.time.DayOfWeek');
console.log(DayOfWeek.MONDAY); // MONDAY

Convention is to load classes once at the top of the file and keep them in a const:

const MiniMessage = Script.loadClass('net.kyori.adventure.text.minimessage.MiniMessage');
const Component = Script.loadClass('net.kyori.adventure.text.Component');
const MM = MiniMessage.miniMessage();

export function mm(message) {
  return MM.deserialize(message);
}

Getting the raw Class object

Some Java APIs want a java.lang.Class rather than the type itself. Use .class:

const ArchModule = Script.loadClass('dev.pixelib.pp.core.arch.ArchModule').class;
PPPlugin.getInstance().getModule(ArchModule);

Implementing interfaces inline

Any Java interface loaded this way can be implemented by calling it as a constructor with an object literal. The runtime generates a real class at runtime that Java code can hold on to.

const MyListener = Script.loadClass('com.example.myscheduler.api.MyListener');

const listenerImpl = new MyListener({
  onEvent(event) {
    console.log(`Event received: ${event}`);
  }
});

MySchedulerApi.registerListener(listenerImpl);

Functional interfaces such as Runnable and Consumer are converted automatically, so a plain arrow function is enough:

MySchedulerApi.scheduleTask((currentTime) => {
  console.log(`Current time is: ${currentTime}`);
}, 1000);

More on this, including abstract classes, in Interfaces and abstract classes. If you need Java to call into JS through a stable, type-safe handle, see Implementing Java interfaces instead.

Types follow automatically

Whenever a script loads a Java class the runtime has not seen before, definitions.d.ts is regenerated to include it. Your IDE picks it up on the next reload and starts autocompleting the real API.

Auto-generated d.ts file

For simple name lookups without the fully qualified string, see magic imports.

Script.addUnloadCallback(callback: () => void): void

Registers a function to run just before this script is unloaded. This fires on reload, on a parent unloading, and on server shutdown.

Use it for anything the runtime cannot clean up for you: despawning entities, removing boss bars, closing inventories, cancelling external subscriptions.

const BossBar = Bukkit.createBossBar('Welcome!', $.BarColor.BLUE, $.BarStyle.SEGMENTED_6);

registerListener($.PlayerJoinEvent, (event) => {
  BossBar.addPlayer(event.getPlayer());
});

// Without this, a reload leaves an orphaned boss bar on every player's screen
Script.addUnloadCallback(() => {
  BossBar.removeAll();
  BossBar.setVisible(false);
});

You do not need to clean up commands, event listeners, or scheduler tasks. Those are unregistered automatically. See Commands, Events and Scheduler.

Script.getCaller(): Script

Returns the Script object of the script that called into the current one. This lets a shared utility register cleanup in its caller’s lifecycle rather than its own, which matters because a utility module outlives any single importer.

// utils/entities.js
export function makeHologram(location, text) {
  const entity = spawnDisplay(location, text);

  // Tie the entity to whoever asked for it, not to this module
  Script.getCaller().addUnloadCallback(() => entity.remove());

  return entity;
}

Throws if the calling script has multiple instances (which happens with .isolate.js modules), because the caller is then ambiguous. Pass the Script object explicitly in that case.

Script.loadLibrary(relativeJarPath: string): void

Loads a jar sitting next to your script into the runtime classpath. The path is relative to the current script file. The jar is loaded once no matter how many scripts ask for it, and dropped again when they all unload.

Script.loadLibrary('./uni-dialog-core-1.5.0.jar');
Script.loadLibrary('./uni-dialog-paper-1.5.0.jar');
Script.loadLibrary('./uni-dialog-adventure-1.5.0.jar');

export const Dialog = Script.loadClass('io.github.projectunified.unidialog.core.dialog.Dialog');
dialogue/
├── dialog.js
├── uni-dialog-adventure-1.5.0.jar
├── uni-dialog-core-1.5.0.jar
└── uni-dialog-paper-1.5.0.jar

Classes from a loaded library are added to the type generator too, so they show up in your IDE.

Script.loadGradleDependency(coordinate: string): void

Same idea, but resolved from Maven Central instead of a local file. Takes a Gradle-style coordinate and caches the download in plugins/PixelScript/maven_cache, so it is only fetched once.

Script.loadGradleDependency('com.saicone:rtag:1.5.16');
Script.loadGradleDependency('com.saicone:rtag-item:1.5.16');

const RtagItem = Script.loadClass('com.saicone.rtag.RtagItem');

Call it at the top of the module that needs it, before the matching loadClass calls. Transitive dependencies are not resolved, so list every artifact you need explicitly.

Script.measure(name: string, callback: () => void): void

Runs a callback and records how long it took under name in this script’s profiler. Useful for finding the expensive part of a feature.

Script.measure('rebuild-scoreboard', () => {
  Bukkit.getOnlinePlayers().forEach(updateScoreboard);
});

Read the results with /script info <file>. See Performance profiling.

Exceptions thrown inside the callback are caught and logged rather than propagated, so measure never changes your control flow.