8.4 KiB
Iris API
art.arcane.iris.api is the surface another plugin compiles against. It answers three questions:
what does Iris terrain look like at a coordinate, when does an Iris world engine come up and go
down, and how do I hand an axe-swing to the Iris tree feller and get told what it cost. It is built
from Bukkit types, java.* types and its own types only — no VolmLib, no Adventure, no shaded
types — so it links against a plain Spigot or Paper compile classpath. A test in the Iris build
walks every class in the package and fails the build if any exported signature mentions anything
else.
| Package | What it is for | Document |
|---|---|---|
art.arcane.iris.api.terrain |
Ask what the generator says about a coordinate: is this an Iris world, what biome, what region, how high is the surface, what kind of surface | terrain.md |
art.arcane.iris.api.world |
Learn when an engine becomes usable and when it stops being usable | world-events.md |
art.arcane.iris.api.pregen |
Follow a pregeneration job | world-events.md |
art.arcane.iris.api.tree |
Drive the tree feller and charge for it | tree-feller.md |
PlaceholderAPI keys are not a compile surface, but they are a contract an operator depends on: placeholders.md.
Anything outside art.arcane.iris.api is internal. art.arcane.iris.core.*,
art.arcane.iris.engine.*, art.arcane.iris.util.* and art.arcane.iris.spi.* change without
notice and without a deprecation cycle. If you find yourself importing Engine, IrisBiome or
IrisToolbelt, you are outside the contract.
Platform limitation
art.arcane.iris.api ships in the Bukkit plugin jar only. The Fabric, Forge and NeoForge mod
jars contain the same generator but not this package — there is no Bukkit World, no
ServicesManager and no Event bus to hang it on. A mod that wants generator data uses the mod
loader's own registries.
The mod jars carry a separate, unrelated surface at art.arcane.iris.modded.api, for supplying
custom block data to the generator from a mod. It is not covered by these documents, is absent from
the Bukkit plugin jar, and shares no types with art.arcane.iris.api.
Everything in these documents assumes Paper, Purpur, Leaf, Canvas, Folia or Spigot, Minecraft 26.2, Java 25.
Depending on Iris
Iris is not published to Maven Central. Two routes work.
Against the jar you already have. This is the route that cannot go wrong: the jar you compile against is the jar you run against.
dependencies {
compileOnly(files('libs/Iris.jar'))
}
Against JitPack. This is what Volmit's own plugins do. transitive = false is required — the
Iris build declares a large dependency graph you do not want on your compile classpath.
repositories {
maven { url = uri('https://jitpack.io') }
}
dependencies {
compileOnly('com.github.VolmitSoftware:Iris:<tag-or-branch-SNAPSHOT>') {
changing = true
transitive = false
}
}
Bukkit plugin (plugin.yml):
softdepend: [Iris]
Paper plugin (paper-plugin.yml):
dependencies:
server:
Iris:
load: BEFORE
required: false
join-classpath: true
join-classpath: true is mandatory on Paper. Plugin classloaders are isolated, and without it you
get NoClassDefFoundError on art.arcane.iris.api.* even though the classes ship unrelocated.
Iris declares load: STARTUP and registers its services during its own onEnable. Do not resolve
an Iris service in a static initialiser or a constructor. Resolve it lazily, at the point of use,
and handle null — see below.
Acquiring a service
Two services are registered with the Bukkit ServicesManager at ServicePriority.Normal:
IrisTerrainService and IrisTreeFellerService. Both are unregistered on Iris shutdown.
package com.example.integration;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class IrisLookup {
private IrisLookup() {
}
public static IrisTerrainService terrain() {
RegisteredServiceProvider<IrisTerrainService> provider =
Bukkit.getServicesManager().getRegistration(IrisTerrainService.class);
return provider == null ? null : provider.getProvider();
}
}
Resolve on every use, or cache and invalidate on PluginDisableEvent. A cached reference to a
service whose plugin has been disabled does not throw — every terrain query answers "absent" and
every tree-feller call returns false — but it will never answer usefully again, and the
replacement instance registered by a later enable is a different object.
Neither service is a functional interface and neither is meant to be implemented by a third party.
ServicesManager#getRegistration hands back the highest-priority registration, so registering your
own IrisTerrainService above Normal shadows Iris's for every other plugin on the server. Do not.
It does not shadow it for Iris — Iris resolves its own services from an internal registry, so its
PlaceholderAPI expansion keeps reading the real one, and the two would then disagree.
The shared library is not relocated
Iris bundles art.arcane.volmlib unrelocated, at its real package name. Several sibling Volmit
plugins do relocate it — Adapt shades it to art.arcane.adapt.util.arcane.volmlib, React to
art.arcane.react.util.arcane.volmlib. Three consequences, in order of how likely they are to bite:
-
You do not need VolmLib to use this API. No type in
art.arcane.iris.apimentions it. You never import it, never shade it, never declare it. -
If you also use VolmLib yourself, shade and relocate your own copy. Do not compile against
art.arcane.volmlibexpecting Iris's copy to satisfy it at runtime. Under Paper's isolated classloaders you would needjoin-classpath: trueon the Iris dependency and you would be binding to whatever VolmLib version Iris happens to ship, which changes on Iris's release schedule and not yours. Relocating your copy costs nothing and removes the coupling entirely. -
A relocated sibling and Iris do not share those classes.
art.arcane.adapt.util.arcane.volmlib.Xandart.arcane.volmlib.Xare unrelated types to the JVM. Never pass an object obtained from one plugin's shaded copy into another's; the cast fails at runtime, not at compile time.
Threading, at a glance
This suite runs on Folia, where region threads own chunks and entity schedulers own entities. Each document states its own contract; this is the summary.
| Call | Which thread may call it | Where the callback lands |
|---|---|---|
Every IrisTerrainService read |
Any thread, including async | Returns inline |
IrisColumnSink.accept |
— | The thread that called sampleColumns |
IrisTreeFellerService.tryFell |
The region thread delivering the BlockBreakEvent |
Returns inline |
IrisTreeFellerService.isManagedBreak |
Any thread | Returns inline |
IrisTreeFellerService.isTreeBlock |
The region thread owning the block; it can also block on disk — see tree-feller.md | Returns inline |
TreeFellerRunHooks.onActivationAccepted |
— | The region thread that owns the broken block |
TreeFellerRunHooks.reserveLogCost / commitLogCost / refundLogCost |
— | The feller's entity scheduler thread |
IrisWorldEngineEvent handlers |
— | Main thread; on Folia, the global region thread |
IrisPregenerationEvent handlers |
— | Main thread; on Folia, the global region thread |
"Any thread" is claimed for the terrain reads because they are justified in doing so: they read the world's generator reference and evaluate cached procedural noise, and touch no chunk, no block state, no entity and no mantle storage. See terrain.md for the full argument. It is not a claim any other part of this API makes.
Switching over the enums
IrisSurfaceKind, IrisColumnField, IrisWorldPhase, IrisPregenPhase and TreeFellerAccess may
gain constants in a future release. A switch expression over them is exhaustive, so it stops
compiling — and throws IncompatibleClassChangeError on an already-compiled jar — the moment one is
added.
Always write a default arm in third-party code:
String label = switch (kind) {
case LAND -> "land";
case OCEAN -> "water";
default -> "";
};