resourcepack.ai
DOCS

For developers

Models from code

A placed model is a rig of display entities with a clickable hitbox holding it together. This page is the things you'll actually want to do with one, roughly in the order you'll want them.

Everything here needs rpai — see Plugin API for how to get it.

Place one#

Optional<Placement> statue = rpai.models().place(
    spot,
    "wizard_statue",
    PlaceOptions.defaults()
        .animation("Idle")     // which animation this copy prefers
        .scale(2f)             // 0.125 to 8
        .placer(player));      // who is doing it, if anybody
  • The location names a block: the rig is centred in it, exactly as a player's placement is.
  • The yaw is used as given and not snapped to a cardinal — so a statue can face any direction, which a player placing one by hand can't do.
  • Empty comes back if the world isn't loaded, there's already a rig in that block, or a listener refused it.

Pass a placer if there is one

With a placer, other plugins get a ModelPlaceEvent and can refuse the placement — which is how a region protection plugin does its job. With no placer, nothing is fired and nothing can refuse, because that event carries a player and inventing one would be a lie. Use it for placements your plugin makes on its own behalf; pass the player whenever somebody asked.

Remove one#

statue.remove(true);   // true drops the item, as breaking it by hand would

false comes back if it was already gone or a listener refused. If you're holding a handle across ticks — perfectly fine to do — isValid() tells you whether the rig is still standing.

Give out the item#

The item the panel hands out is just an item. You can mint it yourself for a shop, a kit or a reward:

rpai.models()
    .itemFor("wizard_statue", ItemOptions.defaults()
        .name("Wizard Statue")
        .amount(2)
        .animation("Idle")
        .scale(2f))
    .ifPresent(item -> player.getInventory().addItem(item));

The animation and scale ride on the stack, so breaking a placement puts them back on the item that drops.

Find the model you mean#

Three ways in, and which one you use is usually decided by what you already have in your hand.

// From a place in the world — nearest first.
List<Placement> nearby = rpai.models().near(location, 8);
 
// From an entity, straight out of an event.
Optional<Placement> clicked = rpai.models().at(event.getRightClicked());
 
// From a chunk, for a sweep with a bound on it.
List<Placement> here = rpai.models().in(chunk);

`in` is a keyword in Kotlin

Hence the backticks. It reads badly enough that a Kotlin plugin usually wants a one-line extension — fun Models.inChunk(chunk: Chunk) = this.in(chunk) — and never thinks about it again.

There's deliberately no "every model on the server" call. That means walking every entity in every world, and a cost like that should be something you ask for with a radius rather than something the API makes look free.

A rig in an unloaded chunk isn't there. near only finds what's loaded, which is also the only thing anybody could be looking at. It keeps whatever it was last told to do and picks it up again when its chunk comes back.

Play an animation#

Triggers cover "when a player does something to this model". For anything else — a quest completing, a boss dying, a timer — name the animation and play it.

for (Placement statue : rpai.models().near(altar, 8)) {
    statue.play("Wave");
}

Naming it is all it takes. Unlike a trigger, this doesn't care what the animation claims — so an animation with no triggers is playable this way and unreachable any other. That's a good way to build: give the animations you drive from code no triggers, and a player clicking around can never set them off by accident.

CallWhat it does
animations()Every animation name this model has, in editor order
play(name)Plays it. false if the name is unknown, the model has no moving parts, the rig is gone, or that one-shot is already running
play(name, true)Same, but restarts one that's already running
stop()Back to the idle loop if there is one, otherwise the rest pose
playing()What's on screen now, including an idle loop it fell back to on its own

Why play() sometimes returns false

A trigger asks "did somebody handle this?" — a caller asks "did it take effect?" So asking for a one-shot that is already mid-play returns false rather than re-cutting it. Use play(name, true) when you mean start it over.

React to a player clicking one#

isModel answers "is this entity part of a model" without building a handle you're about to throw away:

@EventHandler(priority = EventPriority.LOWEST)
public void onUse(PlayerInteractAtEntityEvent event) {
    if (event.getHand() != EquipmentSlot.HAND) return;      // offhand fires twice
    if (!rpai.models().isModel(event.getRightClicked())) return;
 
    Placement rig = rpai.models().at(event.getRightClicked()).orElseThrow();
    if ("shop_sign".equals(rig.modelId())) {
        openShop(event.getPlayer());
        event.setCancelled(true);
    }
}

Listen at LOWEST or LOW

We handle that event at NORMAL and cancel it when the model has a right-click trigger — so a handler at NORMAL or later sees a cancelled event for some models and not others, depending on how they were set up in the editor. Left click is EntityDamageByEntityEvent, and that one is cancelled for every rig, because punching a model must not break it.

Keep your own data on one statue#

Persistent data on the hitbox, saved with the chunk and gone when the rig is:

NamespacedKey owner = Placement.key(this, "owner");
 
statue.data().set(owner, PersistentDataType.STRING, player.getUniqueId().toString());
String who = statue.data().get(owner, PersistentDataType.STRING);

The container is shared with the animator, and the namespace is what keeps you apart — a key made with your own plugin can never collide with one of ours.

Handles are values#

Two handles on the same rig are equal and hash alike, so you can keep them in a set without collecting duplicates of one statue. Every lookup mints a fresh object, so identity comparison is never what you want.

Set<Placement> animated = new HashSet<>();
for (Placement rig : rpai.models().near(spawn, 32)) {
    if (animated.add(rig)) rig.play("Greet");   // each statue once
}