resourcepack.ai
DOCS

Developers

Plugin API

Your own Spigot or Paper plugin can give a custom item, place a model, play an emote, park a vehicle and switch it off, put an icon in a message, and cancel any of those when your rules say no. Every snippet here is in Java and Kotlin — pick one and the whole of the docs follows you.

Getting in#

Everything hangs off the plugin instance.

RPEnginePlugin engine =
    (RPEnginePlugin) Bukkit.getPluginManager().getPlugin("RPEngine");
 
engine.items();      // custom items
engine.models();     // models, and the ones standing in your worlds
engine.emotes();     // emotes and movement sets
engine.vehicles();   // vehicles, and the ones standing in your worlds
engine.sounds();     // custom sounds
engine.icons();      // icons, and putting one into text
engine.registry();   // everything this server holds, by id

Add softdepend: [RPEngine] to your plugin.yml so you load after it.

What is API and what is not

Everything in ai.resourcepack.engine.api is supported. Nothing in ai.resourcepack.engine.core is, and anything there may change in any release, including a patch.

IDs#

One namespace:path ID for everything, and it is an ordinary Minecraft resource location. parse answers empty rather than throwing, so an ID out of somebody's config is a message and not a stack trace.

ContentId ruby = ContentId.parse("mypack:ruby").orElseThrow();

Store IDs, never what they resolve to. An icon's codepoint moves when content changes and an item's model is derived from its ID — the ID is the only stable reference in the system, and everything else is worked out from it at the moment it is needed.

Items#

Optional<ItemStack> stack = engine.items().create(ruby);
Optional<ContentId> what  = engine.items().idOf(hand);
boolean isRuby            = engine.items().is(hand, ruby);

Identity lives in the stack's persistent data, not its name or its model. An item renamed at an anvil is still itself, and a vanilla diamond somebody called "Ruby" is still not one.

Models#

engine.models().place(location, "golem", PlaceOptions.defaults());
engine.models().itemFor("golem", ItemOptions.defaults());
engine.models().near(location, 16);   // what is standing around here
engine.models().at(entity);           // is this entity part of a model?

A Placement is a handle on a model standing in a world: ask it what it is, tell it to play an animation, remove it.

Emotes#

engine.emotes().play(player, "wave");            // solo
engine.emotes().play(lead, "hug", List.of(other)); // with a cast
engine.emotes().canPerform(player);
engine.emotes().stop(player);

A cast is moved, so ask them first

An emote with a cast teleports the people it names. If your plugin plays one, having asked them is your job — the engine's own /emote does that through an invitation, and play does not.

EmoteResult carries a typed reason rather than a sentence, so you write the words in your own palette and your own language.

A worn rig can be kept on another animation's clock: engine.emotes().seek(player, seconds) every tick, fed from a placement's playhead(), poses the rig at that moment of its emote. That is how a vehicle keeps a paddler's arms on the paddle across a loop wrap.

Sounds#

engine.sounds().play(player, id);       // only this player hears it
engine.sounds().playAt(location, id);   // everybody nearby hears it here
engine.sounds().playFrom(entity, id);   // everybody nearby hears it follow this entity

All three use the sound's declared category, volume and pitch; overloads let you replace volume and pitch. playFrom is the moving-source form for engines, creatures and anything else where a fixed coordinate would leave the sound behind. Playing methods are main thread only; ids() and info() are safe from any thread.

Vehicles#

engine.vehicles().spawn(location, id);   // park one, ready to get into
engine.vehicles().of(player);            // the vehicle they are in
engine.vehicles().at(entity);            // is this thing part of a vehicle?
engine.vehicles().near(location, 16);    // nearest first
engine.vehicles().loaded();              // every one in a loaded chunk
engine.vehicles().isRiding(uuid);        // safe from any thread

A Vehicle is a handle on one standing in a world, occupied or not: what it is, where it is going, who is aboard, and the switches a plugin turns.

Vehicle car = engine.vehicles().of(player).orElseThrow();
car.speed();          // blocks per second, negative in reverse
car.states();         // MOVING, TURNING, AIRBORNE, IDLE ...
car.driver();         // Optional<Player>
car.occupants();      // driver first, then in seat order
car.seat(other, 1);   // put somebody in seat 1
car.eject(other);     // and take them out
car.submersion();     // how deep in water, in blocks. 0 out of it
car.pointOn(-0.7, 0.6, 1.5);   // where a point on the bodywork is, in the world

Building a vehicle the engine does not have#

A skateboard is pushed rather than throttled, tucks for speed, lies down on command and rides walls. None of that is in the format and none of it needs to be: the handle exposes the pieces.

Vehicle board = engine.vehicles().of(player).orElseThrow();
VehicleInput keys = board.input();          // what the driver is pressing, this tick
if (keys.forward()) board.nudge(2.2);       // a kick, along the heading
board.dress(player, "myplugin_push");       // wear this over the seat's states
board.dressVariant(player, "goofy");        // ...in this rider's own variant
board.holdOccupant(player, true);           // sneak means something else now
board.spin(-360);                           // mid-air, it carries
board.wallRiding();                         // is it on a wall right now?

input() is the same demand the physics read, so your idea of "the driver pressed forward" and the engine's are on the same tick. Where the keys cannot be read at all (Spigot, or before 1.21.4) keys() is false and only throttle() means anything.

The sneak key is not free. You can read it, but sneak is Minecraft's dismount — so without holdOccupant you see the key on the tick your rider is already stepping into the road. Ask for the hold ONCE, when they get on: the dismount happens on the tick the key goes down, earlier than your next tick task. The engine still lets a short TAP of it through as an ordinary dismount, and lets go by itself once the vehicle stops, so a held rider is never trapped whatever your plugin does or forgets to do.

dressVariant is for a per-person fact a seat cannot know. A seat's animations: is written once for everybody; a stance, a handedness or a team belongs to the rider. The variant is applied to whatever the seat's table names — moving becomes moving_goofy — and falls back per emote, so a set that mirrors the two states worth mirroring and leaves the rest alone works.

Moving the vehicle's own model#

dress is for the rider's body. perform is the same door for the vehicle's: it plays one of the MODEL's animations instead of whatever its animations: table says.

bike.perform("wheelie");   // the whole bike tips back about the rear axle
// ... count your ticks ...
bike.rest();               // back to the state table, wheels where they were

A bike's wheelie, a barspin, a digger's arm coming down: motion that belongs to the model and is decided by a plugin rather than by which of six words describes how fast the thing is going. A vehicle with no animations: at all can be given one this way, so a pack need not have anticipated your trick.

It loops for as long as it is set, exactly as a state's animation does, whatever the animation itself was authored as — so the length of a trick is yours to time: set it, count your ticks, rest(). That is the same shape as wearing an emote for the length of a kick, and it is deliberately not a one-shot with a callback: there is no tick you could be told about that you were not already having.

A performed animation runs on real time, even on a vehicle with animation-follows-speed. That link exists so a wheel turns because the vehicle moved, and a trick is not a wheel — a barspin that ran at a quarter speed because the rider was braking into it would be the mechanism showing through. The speed-driven playhead is put aside while it holds and picked up where it was on rest(), so the wheels do not jump when the trick ends.

One animation at a time, because a rig has one clock — the same rule the state table lives under. An animation that has to keep the wheels turning while it plays has to turn them itself.

Fuel, and anything else that stops a vehicle going#

setEnabled(false) is the switch. A disabled vehicle ignores its driver, coasts to a halt where it is and stays there — it still falls if it was in the air and still floats on water, and people get in and out of it as normal. The flag is written on the chassis, so a car that ran dry is still dry after a restart.

NamespacedKey fuel = car.key(this, "fuel");
 
@EventHandler
public void onState(VehicleStateEvent event) {
    // Burn only while it is actually going somewhere.
    if (event.entered(VehicleState.MOVING)) burners.add(event.vehicle());
    if (event.left(VehicleState.MOVING)) burners.remove(event.vehicle());
}
 
// once a second
for (Vehicle vehicle : burners) {
    int left = vehicle.data().getOrDefault(fuel, PersistentDataType.INTEGER, 0) - 1;
    vehicle.data().set(fuel, PersistentDataType.INTEGER, Math.max(0, left));
    vehicle.setEnabled(left > 0);
}

data() is the chassis's persistent data — the tank, the owner, the price paid — and it survives everything the vehicle survives. uniqueId() is the chassis's id and is the one stable key: the seats and the model are rebuilt on every chunk load with new ids each time, so never remember those.

setSpeedLimit is the softer version, for a damaged engine or a road with a limit: a lower top speed, with braking and reversing scaled to match. An aircraft limited below its takeoff speed cannot take off, which is the honest consequence. Deliberately not remembered — keep it in data() yourself if it should be. stop() is a wall: dead this tick, throttle reset, still answering its driver afterwards.

Who may get in is an event, not a method

There is no setLocked. "Is this your car" is a rule about your server, so it is VehicleEnterEvent — cancel it and they stay standing beside the door. VehicleExitEvent cannot be cancelled: a player who could not be let out would be a player who is trapped, and a race that has not finished is better served by putting them back with seat.

Shipping content in your own jar#

An addon carries its models, items, vehicles and emotes as an ordinary content folder inside its own jar, under resources/content/<namespace>/, and installs it on enable:

if (AddonContent.install(this, engine, "skateboards")) {
    engine.reload();   // only when something actually changed
}

A .version stamp beside the files says which build last wrote them, so restarting on the same jar copies nothing and reloads nothing — a reload rebuilds every pack on the server, which is not a thing to do on every boot for files that have not changed. A new build overwrites its own files and only its own: whatever the server owner added to that folder stays, because the folder is theirs to extend.

Catch the IOException and disable your plugin over it. Content that did not install is items that do not exist, and failing loudly at boot beats a command that says nothing an hour later.

Icons in your own text#

String line = engine.icons().format(config.getString("welcome"));

Every :namespace:id: becomes its picture. An ID naming nothing is left exactly as written, so text never silently loses a chunk of itself.

Events#

All cancellable unless the row says otherwise.

EventWhen
ContentLoadEventContent finished loading and the packs are built. Listen to this before touching anything else — a reload replaces every definition on the server. Not cancellable
PackSendEventA pack went out to a player. Not cancellable
ItemUseEventA custom item was clicked. Cancelling cancels the vanilla use too
ModelPlaceEventA model is about to be put down
ModelBreakEventA model is about to be broken. Carries a drop flag, separate from cancelling
ModelInteractEventA placed model was right-clicked
ModelAnimationEventA model is about to play an animation
ModelAnimationEndEventOne ended — finished, replaced or stopped. The half you need to do anything in sequence. Not cancellable
ModelSeatEventSomebody is about to sit on a chair, a seat bone, or a vehicle seat. Cancelling leaves them standing
VehicleEnterEventSomebody is about to get into a vehicle — fires after ModelSeatEvent for the same seat, with the vehicle and the seat attached. "Is this your car" lives here
VehicleExitEventSomebody got out, or was taken out. Carries why — dismounted, ejected, quit, reloaded, removed, unloaded, shutdown. Not cancellable. RELOADED is followed by them being put back a tick later, with no enter event
VehicleMoveEventA vehicle is about to move — once per tick, only while it is going somewhere. Cancelling stops it dead, like a wall. The event for a region it may not enter; not the event for fuel, which is setEnabled
VehicleBailEventA bad landing is about to throw a rider off a vehicle that asked for bailing. Cancelling leaves them aboard and takes nothing off them — which is how a server switch for it is written without editing the pack
VehicleStateEventWhat a vehicle is doing changed — set off, stopped, took off, went under. The same set that drives its animation, on the change rather than every tick. Not cancellable
ModelBindEventA model is going on an entity, or coming off one — a MythicMobs boss, an NPC, anything that is not ours
EntityDeathEventA custom entity died. Bukkit's own event carries the drops; this one says what it was. Not cancellable
PlayerLiquidEventSomebody went into one of your liquids, or came out of one. Fires on the crossing, not every second. Not cancellable
EmoteStartEventAn emote is about to start
EmoteEndEventAn emote ended. Carries why — finished, stopped, moved, damaged, quit, shutdown

The engine decides whether something can physically happen, never whether it is allowed to. Region protection, plot ownership, an event world where nothing may be built: those are rules about your server, which the engine cannot see. That is what these events are for.

@EventHandler
public void onPlace(ModelPlaceEvent event) {
    if (!myRegions.mayBuild(event.getPlayer(), event.block())) {
        event.setCancelled(true);
    }
}
@EventHandler
public void onLoad(ContentLoadEvent event) {
    menus.rebuild();
}

Everything the API can answer is answerable by then, on a reload as much as at startup — so anything your plugin cached and derived from content is rebuilt here.

A plugin loading after RP Engine misses the first one

STARTUP has been and gone before your listener exists. Do not work around that with a delayed task: ask the API directly in your own onEnable, and use the event for every reload after it.

Priority#

Every one of these is read after all handlers have run — the engine calls the event, then asks whether anything cancelled it. So priority does not order you against the engine, only against other plugins listening to the same event. Listen at NORMAL unless you are deliberately arbitrating with another plugin, and treat MONITOR as read-only: a cancel there still counts, which makes it a cancel nobody downstream can see coming.

Where priority does matter is vanilla's own events. The engine listens to those at LOW with ignoreCancelled = true, so a plugin that cancels a PlayerInteractEvent at LOWEST stops a custom item's use before RP Engine ever sees the click — which is usually exactly what a protection plugin wants.

Registering content from code#

A plugin can add content of its own rather than shipping a folder. Claim a namespace, register against the handle you get back, and the pack builder picks it up on the next build.

ClaimResult claim = engine.registration()
        .claim("myplugin", ContentSource.EMBEDDED);
 
claim.namespace().ifPresent(ns -> {
    ns.define(ContentKind.ITEM, "ruby");
    // ... and release() it when your plugin disables
});

The handle is what proves ownership: a plugin holding myplugin cannot define otherpack:thing by accident or on purpose, so two sources loading at once cannot corrupt each other's half of the ID space. release() drops the namespace and everything in it, which is also the only reload granularity there is — a namespace is replaced whole or not at all.

Content from a plugin is not a second-class source. It lands in the same registry, under the same ID rules, and the pack builder cannot tell it from a hand-written folder.

Threading#

Reads that ask what the server holdsids, info, the whole of ContentRegistry — are safe from any thread. Anything touching a player, an entity or a world is main thread only, and says so on the method.