Files
Iris/docs/90 - API - Getting Started.md
T
Brian Neumann-Fopiano 365205ad0a d
2026-08-12 13:52:16 -04:00

8.6 KiB

90 - API - Getting Started

art.arcane.iris.api is the Bukkit plugin surface another plugin compiles against: terrain reads, world-engine and pregen observation, and tree-feller integration. It is built from java.*/javax.*, Bukkit types, and its own types only — no VolmLib, Adventure, or shaded types — so it links against a plain Spigot or Paper compile classpath. A build test walks every class in the package and fails if any exported signature mentions anything else. PlaceholderAPI keys are operator-facing, not compile-time: see 09 - PlaceholderAPI.md.

Reach for this API when your plugin needs to know what Iris will generate before the server generates it — a map renderer, a spawn or settlement picker, a pregen planner, a HUD that names the pack biome — or when you need to act at the moment an Iris world's generator becomes usable or goes away. Everything here is read-only except the tree feller, which you can drive and charge.

Package Purpose Document
art.arcane.iris.api.terrain Generator opinion at a coordinate: Iris world?, biome, region, surface height/kind 91 - API - Terrain.md
art.arcane.iris.api.world Engine ready / hotloaded / closing 92 - API - World Events.md
art.arcane.iris.api.pregen Pregeneration job progress 92 - API - World Events.md
art.arcane.iris.api.tree Drive and charge the tree feller 93 - API - Tree Feller.md

Writing a mod rather than a plugin? Fabric, Forge, and NeoForge jars expose art.arcane.iris.modded.api instead: 94 - API - Modded.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. The separately built SPI jar is for Iris platform adapters, not downstream plugin integrations. Importing Engine, IrisBiome, or IrisToolbelt means you are outside the stable contract.


Platform limitation

art.arcane.iris.api ships in the Bukkit plugin jar only. Fabric, Forge, and NeoForge mod jars carry the generator but not this package — there is no Bukkit World, ServicesManager, or Event bus to hang it on.

The mod jars carry art.arcane.iris.modded.api (94 - API - Modded.md): detect Iris levels, drive pregeneration, read/write mantle data, and register providers so packs can place mod blocks, items, and mobs. It is absent from the Bukkit jar and shares no types with art.arcane.iris.api.

Everything in these API docs 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. The jar you compile against is the jar you run against.

dependencies {
    compileOnly(files('libs/Iris.jar'))
}

Against JitPack. 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; 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 onEnable. Do not resolve an Iris service in a static initialiser or constructor. Resolve lazily at the point of use and handle null.


Acquiring a service

Two services are registered with Bukkit ServicesManager at ServicePriority.Normal: IrisTerrainService and IrisTreeFellerService. Both are unregistered on Iris shutdown. Iris also registers the same instances in an internal IrisServices registry that its own code (including PlaceholderAPI expansion) uses.

A complete integration — resolve lazily, handle null, answer:

package com.example.integration;

import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;

public final class ExamplePlugin extends JavaPlugin {
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("Players only.");
            return true;
        }

        IrisTerrainService terrain = terrain();

        if (terrain == null) {
            player.sendMessage("Iris is not installed or not enabled.");
            return true;
        }

        player.sendMessage(terrain.surfaceBiomeName(
                player.getWorld(),
                player.getLocation().getBlockX(),
                player.getLocation().getBlockZ()).orElse("not an Iris world"));
        return true;
    }

    private IrisTerrainService terrain() {
        RegisteredServiceProvider<IrisTerrainService> provider =
                getServer().getServicesManager().getRegistration(IrisTerrainService.class);
        return provider == null ? null : provider.getProvider();
    }
}

Resolve on every use, as above, or cache and invalidate on PluginDisableEvent. A cached reference after Iris disables does not throw — terrain queries answer absent and tree-feller calls return false — but it never becomes useful again, and a later enable registers a different object.

Neither service is a functional interface and neither is meant for third-party implementation. ServicesManager#getRegistration returns the highest-priority registration; registering your own IrisTerrainService above Normal shadows Iris for every other plugin. It does not shadow Iris for Iris itself (internal registry), so PlaceholderAPI would still read the real service while other plugins would not.


The shared library is not relocated

Iris bundles art.arcane.volmlib unrelocated. Sibling Volmit plugins may relocate it (Adapt → art.arcane.adapt.util.arcane.volmlib, React → art.arcane.react.util.arcane.volmlib). Consequences:

  1. You do not need VolmLib to use this API. No type in art.arcane.iris.api mentions it.
  2. If you use VolmLib yourself, shade and relocate your own copy. Do not bind to Iris's version via join-classpath.
  3. A relocated sibling and Iris do not share those classes. Never pass objects across relocated package boundaries.

Threading, at a glance

This suite runs on Folia (region threads own chunks; entity schedulers own entities). Each document states its contract; 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; can block on disk — see 93 - API - Tree Feller.md Returns inline
TreeFellerRunHooks.onActivationAccepted Region thread that owns the broken block
TreeFellerRunHooks.reserveLogCost / commitLogCost / refundLogCost Player entity scheduler on Folia; may run inline on the server main thread on Paper when already primary
IrisWorldEngineEvent handlers Main thread; on Folia, the global region thread
IrisPregenerationEvent handlers Main thread; on Folia, the global region thread

Terrain reads may use any thread because they only read the world generator reference and evaluate cached procedural noise — no chunk, block state, entity, or mantle storage. See 91 - API - Terrain.md. That claim does not apply to the rest of this API.


Switching over the enums

IrisSurfaceKind, IrisColumnField, IrisWorldPhase, IrisPregenPhase, and TreeFellerAccess may gain constants. A switch expression without default stops compiling (and throws IncompatibleClassChangeError on an already-compiled jar) when a constant is added.

Always write a default arm in third-party code:

String label = switch (kind) {
    case LAND -> "land";
    case OCEAN -> "water";
    default -> "";
};