This commit is contained in:
Brian Neumann-Fopiano
2026-07-26 12:12:49 -05:00
parent de25b7d7ab
commit d5a55ccfcf
66 changed files with 6300 additions and 424 deletions
+196
View File
@@ -0,0 +1,196 @@
# 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](terrain.md) |
| `art.arcane.iris.api.world` | Learn when an engine becomes usable and when it stops being usable | [world-events.md](world-events.md) |
| `art.arcane.iris.api.pregen` | Follow a pregeneration job | [world-events.md](world-events.md) |
| `art.arcane.iris.api.tree` | Drive the tree feller and charge for it | [tree-feller.md](tree-feller.md) |
PlaceholderAPI keys are not a compile surface, but they are a contract an operator depends on:
[placeholders.md](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.
```gradle
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.
```gradle
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`):
```yaml
softdepend: [Iris]
```
Paper plugin (`paper-plugin.yml`):
```yaml
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.
```java
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:
1. **You do not need VolmLib to use this API.** No type in `art.arcane.iris.api` mentions it. You
never import it, never shade it, never declare it.
2. **If you also use VolmLib yourself, shade and relocate your own copy.** Do not compile against
`art.arcane.volmlib` expecting Iris's copy to satisfy it at runtime. Under Paper's isolated
classloaders you would need `join-classpath: true` on 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.
3. **A relocated sibling and Iris do not share those classes.** `art.arcane.adapt.util.arcane.volmlib.X`
and `art.arcane.volmlib.X` are 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](tree-feller.md#istreeblock-is-the-expensive-one) | 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](terrain.md#threading) 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:
```java
String label = switch (kind) {
case LAND -> "land";
case OCEAN -> "water";
default -> "";
};
```