For developers
Plugin API
The plugin connects your pack to the Studio: a player runs
/link, you press Sync, and models animate and emotes play. What it can't know
is what any of that means on your server — when a statue should appear, who
has earned an emote, what a screen is for.
That's what this API is. It hands the whole engine to your own plugin, so the things a pack can do stop being commands somebody types and become part of how your server actually works:
- A quest completes, and the statue you generated places itself at the shrine.
- A player wins a duel, and their character takes a bow in front of the crowd.
- Somebody clicks a model you made, and your shop opens with your GUI art drawn over it.
- A boss loses health, and the meter you drew updates on everyone's screen.
Without it, a pack is art the server hands out. With it, the art is something your server can react to and drive.
It's the same engine either way
These calls are exactly the ones the plugin makes for its own commands —
/emote, /gui, /hud and the rest are thin wrappers over what's on these
pages. Nothing here is a limited version of anything, and nothing the plugin
does is off-limits to you.
Installing#
The plugin ships the whole API inside it, so a server that runs
ResourcePackAI.jar already has everything. Your plugin only needs to compile
against it.
- 1
Add the repository and the dependency
repositories { maven { url = 'https://repo.resourcepack.ai' } } dependencies { compileOnly 'ai.resourcepack:library:2.0.0' }repositories { maven("https://repo.resourcepack.ai") } dependencies { compileOnly("ai.resourcepack:library:2.0.0") }Sources and javadoc are published beside the jar, so your IDE shows the documentation for every call rather than decompiled bytecode.
compileOnlymatters. The classes come from the plugin at runtime, and a second copy inside your jar would be a second copy of the engine fighting the first over the same entities.Not using a build tool? Download the plugin jar, drop it in a
libs/folder and usecompileOnly files("libs/resourcepackai.jar")instead — it contains the whole API. - 2
Tell Bukkit the order
In your
plugin.yml:softdepend: [ResourcePackAI]softdepend, notdepend— your plugin should still load on a server that doesn't run ours. The next step is how you handle that.
Your first call#
import ai.resourcepack.api.ResourcePackAI;
public class MyPlugin extends JavaPlugin {
private ResourcePackAI rpai;
@Override
public void onEnable() {
rpai = Bukkit.getServicesManager().load(ResourcePackAI.class);
if (rpai == null) {
getLogger().info("ResourcePack AI isn't installed — statues are off.");
return;
}
for (String model : rpai.models().ids()) {
getLogger().info("This server knows how to animate " + model);
}
}
}import ai.resourcepack.api.ResourcePackAI
class MyPlugin : JavaPlugin() {
private var rpai: ResourcePackAI? = null
override fun onEnable() {
val api = server.servicesManager.load(ResourcePackAI::class.java)
if (api == null) {
logger.info("ResourcePack AI isn't installed — statues are off.")
return
}
rpai = api
for (model in api.models().ids()) {
logger.info("This server knows how to animate $model")
}
}
}Two things to copy from that, because both bite people:
- Load it in
onEnable, not in your constructor. Services are registered as plugins enable, and yours may run before ours. - Null means "not installed". No exception, no crash. Check it once and keep the reference, or check it each time — both are fine, but never assume.
Is a feature actually usable?
Being installed isn't the same as being ready. rpai.supports(Feature.EMOTES)
answers whether this server has emote data and a rig for somebody to wear;
Feature.SKINS is false on Spigot, which can't change a skin after a player
joins. Ask before offering something in your own menu.
The shape of everything#
The API is deliberately repetitive. Learn one service and you know the rest.
| You want | You call | You get |
|---|---|---|
| Everything of a kind | ids() | List<String> |
| Details of one | info(id) | Optional<…Info> |
| Do a thing that can fail | play(…), draw(…) | boolean |
| Do a thing that returns something | place(…), open(…) | Optional<…> |
| Do a thing that can fail several ways | emotes().play(…) | a result with a reason |
And the rules that hold everywhere:
- Nothing returns
null. Empty lists,Optional.empty(),false. - Nothing throws because you passed
null. A model id read out of a config that turned out to be missing gives youOptional.empty(), not a stack trace in your console. - Whoever the action is about comes first.
play(player, …),draw(viewer, …),place(location, …). - Optional arguments come last, as an options object.
PlaceOptions.defaults().scale(2f)— build it by chaining, in any order.
Threading#
Anything that touches a player, an entity or the world must be on the main
thread. It throws IllegalStateException if it isn't, rather than letting
Bukkit corrupt something quietly hours later.
Asking what the server holds is safe from anywhere — ids(), info(),
animationsOf(), isAnimated() and everything on content() read
thread-safe state. So you can build a menu on an async task and only hop back
when you act on it:
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
List<String> models = rpai.models().ids(); // fine off-thread
Bukkit.getScheduler().runTask(this, () -> {
rpai.models().place(spot, models.get(0), PlaceOptions.defaults());
});
});server.scheduler.runTaskAsynchronously(this, Runnable {
val models = rpai.models().ids() // fine off-thread
server.scheduler.runTask(this, Runnable {
rpai.models().place(spot, models[0], PlaceOptions.defaults())
})
})