Interfaces and abstract classes

Some Java APIs will not take a plain callback. They want an object that implements an interface, or a subclass of an abstract class, and they hold on to it. PixelScript can produce both.

When to reach for this

Only when a Java API needs to hold a reference to your JavaScript code. This is entirely separate from writing JavaScript classes for your own use, which needs none of this machinery.

If you are exposing a whole API surface to a Java plugin rather than satisfying a single callback parameter, use the implementation API instead. It is type-safe on the Java side and survives reloads.

Functional interfaces: just pass a function

Any single-method interface is satisfied by an arrow function. Nothing else is needed.

@FunctionalInterface
public interface MyFunctionalInterface {
    String execute(String message);
}
MyClass.someFunction((message) => 'Hello, ' + message);

Runnable, Consumer, Supplier, Predicate and friends all work this way, which is why Scheduler takes plain functions.

Multi-method interfaces: call the interface as a constructor

Load the interface and call it with an object literal. The runtime generates a real implementing class.

public interface MyInterface {
    void doSomething();
    int calculate(int a, int b);
}
const MyInterface = Script.loadClass('com.example.MyInterface');

const impl = new MyInterface({
  doSomething() {
    console.log('Doing something!');
  },
  calculate(a, b) {
    return a + b;
  }
});

someJavaApi.register(impl);

Method names must match the interface exactly. A real-world example, a Paper chat renderer:

const ChatRenderer = Script.loadClass('io.papermc.paper.chat.ChatRenderer');
const Component = Script.loadClass('net.kyori.adventure.text.Component');

const myRenderer = new ChatRenderer({
  render(source, sourceDisplayName, message, viewer) {
    return Component.text()
      .append(getPrefix(source))
      .append(sourceDisplayName)
      .append(Component.text(': '))
      .append(message)
      .build();
  }
});

registerListener(AsyncChatEvent, (event) => event.renderer(myRenderer));

Abstract classes: Java.extend

Abstract classes need Java.extend(BaseClass, overrides), which produces a new class you then instantiate with the parent’s constructor arguments.

Here is the canonical case, a ProtocolLib packet adapter:

const ProtocolLibrary = Script.loadClass('com.comphenix.protocol.ProtocolLibrary');
const PacketAdapter = Script.loadClass('com.comphenix.protocol.events.PacketAdapter');
const ListenerPriority = Script.loadClass('com.comphenix.protocol.events.ListenerPriority');
const PacketType = Script.loadClass('com.comphenix.protocol.PacketType');

const MyPacketAdapter = Java.extend(PacketAdapter, {
  onPacketReceiving: function (event) {
    console.log('Swing: ' + event.getPlayer().getName());
  }
});

const listener = new MyPacketAdapter(
  __plugin,
  ListenerPriority.MONITOR,
  [PacketType.Play.Client.ARM_ANIMATION]
);

const protocolManager = ProtocolLibrary.getProtocolManager();
protocolManager.addPacketListener(listener);

Note __plugin, the PixelScript plugin instance, passed where the API wants a Plugin.

Clean up after yourself

Nothing you register directly against a third-party Java API is cleaned up on reload. The runtime only manages what it handed you: commands, listeners registered through registerListener, and scheduler tasks.

Always pair a manual registration with an unload callback:

protocolManager.addPacketListener(listener);

Script.addUnloadCallback(() => {
  protocolManager.removePacketListener(listener);
});

Skip this and every reload stacks another listener on top of the last one. The symptom is a handler that fires two, then three, then ten times, and it is the single most common bug in PixelScript codebases that touch external APIs.

super has limits

Inside a generated class, super reaches parent methods and constructors, but not parent fields. If you need a protected field, expose it through a getter on the Java side.