This commit is contained in:
Brian Neumann-Fopiano
2026-08-12 13:52:16 -04:00
parent 12b97b7994
commit 365205ad0a
82 changed files with 7978 additions and 5625 deletions
+182 -42
View File
@@ -1,9 +1,11 @@
# 94 - API - Modded
`art.arcane.iris.modded.api` is the surface a **mod** compiles against on Fabric, Forge, and NeoForge: detect Iris levels, drive pregeneration and mantle storage, and supply custom blocks/items/mobs to packs. It ships only in those mod jars, is absent from the Bukkit plugin jar, and shares no types with `art.arcane.iris.api`. Bukkit terrain, world events, pregen events, and tree-feller services do not exist here — see `90 - API - Getting Started.md` through `93 - API - Tree Feller.md` for the plugin surface.
`art.arcane.iris.modded.api` is the surface a **mod** compiles against on Fabric, Forge, and NeoForge: detect Iris levels, drive pregeneration and mantle storage, and supply custom blocks/items/mobs to packs. Reach for it when your mod's content has to appear in Iris-generated terrain, or when it needs to know whether a `ServerLevel` is Iris at all. It ships only in the mod jars, is absent from the Bukkit plugin jar, and shares no types with `art.arcane.iris.api`.
Assumes Minecraft 26.2, Java 25, and Fabric, Forge, or NeoForge. Mod id is `irisworldgen` on all three.
The Bukkit terrain, world-event, pregen-event, and tree-feller **services** have no counterpart here — see `90 - API - Getting Started.md` through `93 - API - Tree Feller.md` for that surface. The tree feller itself does run on mod loaders as a player-facing feature (settings plus the `irisworldgen:treefeller` node), but exposes no integration hooks.
| Goal | Entry |
|---|---|
| Detect Iris, read engine token, start pregen, mantle R/W | `IrisModdedAPI` |
@@ -22,7 +24,9 @@ Assumes Minecraft 26.2, Java 25, and Fabric, Forge, or NeoForge. Mod id is `iris
./gradlew buildNeoforge # -> dist/Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Each adapter is a standalone Gradle build (`adapters/<loader>/settings.gradle` `includeBuild('../..')`). Compile against the jar you run:
Each task shells out to the adapter's own wrapper and copies the jar into `dist/`. The loader segment is that adapter's loader version verbatim — for example `26.2+0.19.3` on Fabric and `26.2+26.2.0.12-beta` on NeoForge.
Each adapter is a standalone Gradle build (`adapters/<loader>/settings.gradle` `includeBuild('../..')` plus a dependency substitution for `art.arcane:core` and `art.arcane:spi`). Compile against the jar you run:
```gradle
dependencies {
@@ -91,11 +95,11 @@ All static, null-tolerant: null or non-Iris `ServerLevel` → `false`, `null`, o
| `isStudioLevel(ServerLevel)` | Throwaway pack-authoring world. Persist nothing |
| `getEngine(ServerLevel)` | Internal `Engine` or null. See stability note |
| `pregenerate(ServerLevel, int radiusBlocks)` | Cached async pregen around origin |
| `pregenerate(ServerLevel, int, int centerX, int centerZ, boolean sync, boolean cached)` | Same with centre and write mode |
| `pregenerate(ServerLevel, int, int centerBlockX, int centerBlockZ, boolean sync, boolean cached)` | Same with centre and write mode |
| `getMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Read per-block mantle |
| `setMantleData(ServerLevel, int x, int y, int z, T)` | Write (creates region; can touch disk) |
| `deleteMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Remove type at position (creates region) |
| `retainMantleDataForSlice(Class<?>)` | Keep custom mantle types after trim |
| `retainMantleDataForSlice(Class<?>)` | Mark a mantle slice type as retained |
| `registerProvider(ModdedDataProvider)` | Imperative provider registration |
| `registerCustomBlockData(String namespace, String key, String state)` | Static block-state alias |
@@ -103,7 +107,8 @@ All static, null-tolerant: null or non-Iris `ServerLevel` → `false`, `null`, o
| Method | Thread guidance |
|---|---|
| `isIrisLevel`, `isStudioLevel`, `getEngine` | Safe from any thread once the level is loaded |
| `isIrisLevel` | Any thread; a reference read on the chunk source |
| `isStudioLevel`, `getEngine` | Any thread, but not free — see below |
| Mantle get/set/delete | Safe off server thread but touch engine storage; writes can disk I/O |
| `pregenerate`, `registerProvider` | Server thread / mod setup / command |
| `retainMantleDataForSlice`, `registerCustomBlockData` | Mod setup |
@@ -112,29 +117,75 @@ All static, null-tolerant: null or non-Iris `ServerLevel` → `false`, `null`, o
`getEngine` returns `art.arcane.iris.engine.framework.Engine`. Internal; changes without deprecation. Treat as an opaque token to hand back to Iris. Prefer the wrappers on this class.
Never cache an `Engine`. Pack hotload or level unload replaces it. `getEngine` returns null while binding and during shutdown.
`getEngine` is **not** a plain field read: for a level whose engine is not bound yet it binds one, which loads the pack and builds the biome complex under the generator's monitor. `isIrisLevel` is the cheap probe; use it first and call `getEngine` only when you need the token. `isStudioLevel` and every mantle accessor route through `getEngine` and inherit this.
Never cache an `Engine`. Pack hotload or level unload replaces it. `getEngine` returns null while binding fails and during shutdown.
### Pregeneration
Returns as soon as the job is queued. Progress is Iris logging and boss bar only — no caller callback (no modded equivalent of `IrisPregenerationEvent`; see limitations). One job server-wide; returns `false` if a job is already active **or** the level is not Iris — check `isIrisLevel` first to distinguish. Call on the server thread.
`cached = true` uses on-disk pregen cache for resume. `sync = true` writes chunks synchronously (slower; bypasses async write queue). Simple overload is `pregenerate(level, radius, 0, 0, false, true)`.
`radiusBlocks` is the half-extent of a square measured from the centre. `cached = true` uses the on-disk pregen cache under `<world>/iris/pregen` for resume. `sync = true` writes chunks synchronously (slower; bypasses async write queue). The simple overload is `pregenerate(level, radius, 0, 0, false, true)`.
Operator pregen concepts: `07 - Pregeneration.md`. Platform matrix: `30 - Platform Differences.md`.
### Mantle data
Iris per-block sidecar, independent of chunk NBT.
The mantle is Iris's per-block sidecar, independent of chunk NBT.
1. **Coordinates are world-space.** `y` is translated by engine min height internally. Out-of-range `y` reads null / writes no-op.
2. **Reads never create storage; writes do.** `getMantleData` returns null when no mantle region exists for that column. `set`/`delete` create the region (possible disk).
3. **Declare custom types or lose them.**
3. **Value types are fixed.** The mantle stores values in typed slices, and only registered slice types exist. A type with no slice raises `IllegalArgumentException: Unsupported matter slice type <name>` — from `set`/`delete` always, and from `get` once a mantle region exists for that column. Iris's own slice types are internal and must not be written by a mod.
Types a mod can use:
| Type | Survives normal generation | Survives pregeneration |
|---|---|---|
| `Boolean`, `Integer`, `Long` | Yes | Yes |
| `String` | Yes | **No** — pregen's chunk cleanup deletes the `String` slice |
`retainMantleDataForSlice(Class<?>)` records a canonical class name in a process-wide, irreversible set. It guards `Mantle.deleteChunkSlice`, which Iris does not currently call; the per-chunk cleanup that runs after generation deletes a fixed slice list and does not consult it. Declaring a type is therefore harmless insurance, not a guarantee — do not rely on it to keep `String` values through a pregen.
All three mantle accessors throw `IllegalStateException` if the engine mantle is already closed.
### Worked example: setup, pregen, mantle
```java
IrisModdedAPI.retainMantleDataForSlice(MyMarker.class);
```
package com.example.yourmod.iris;
Registration is by canonical class name, process-wide, irreversible. All three mantle accessors throw `IllegalStateException` if the engine mantle is already closed.
import art.arcane.iris.modded.api.IrisModdedAPI;
import net.minecraft.server.level.ServerLevel;
public final class IrisBridge {
private IrisBridge() {
}
/** Mod setup, before any level resolves a pack key. */
public static void onModSetup() {
IrisModdedAPI.registerProvider(new YourIrisProvider());
IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]");
IrisModdedAPI.retainMantleDataForSlice(Integer.class);
}
/** Server thread. False when the level is not Iris or a pregen job is already running. */
public static boolean warmUp(ServerLevel level, int radiusBlocks) {
if (!IrisModdedAPI.isIrisLevel(level)) {
return false;
}
return IrisModdedAPI.pregenerate(level, radiusBlocks);
}
public static void markShrineTier(ServerLevel level, int x, int y, int z, int tier) {
IrisModdedAPI.setMantleData(level, x, y, z, tier);
}
public static int shrineTier(ServerLevel level, int x, int y, int z) {
Integer tier = IrisModdedAPI.getMantleData(level, x, y, z, Integer.class);
return tier == null ? 0 : tier;
}
}
```
---
@@ -153,7 +204,7 @@ public interface ModdedDataProvider {
}
```
`ModdedDataType`: `BLOCK`, `ITEM`, `ENTITY`. May gain constants — use `default` in switch expressions.
`Identifier` is `net.minecraft.resources.Identifier`. `ModdedDataType`: `BLOCK`, `ITEM`, `ENTITY`. May gain constants — use `default` in switch expressions.
### Contract
@@ -161,12 +212,12 @@ public interface ModdedDataProvider {
- `isValidProvider` — gate before every resolution callback on generation threads. Namespace or set lookup only.
- `isReady()` — late registries: Iris **skips** false rather than treating as absent.
- `getTypes` — suggestions/tooling only; empty collection, never null.
- `getBlockData``state` is parsed `[prop=value]` map, never null, possibly empty. Null return declines (next provider, then air). `ModdedBlockData.direct(state)` for final; `deferred(placeholder)` for second pass.
- `processBlockPlacement` — finishes deferred placement on server thread with chunk loaded. Only the **first** provider claiming the id runs for a position. Placeholder should match final shape/occlusion.
- `spawnMob` — server thread; null declines.
- `getBlockData``state` is the parsed `[prop=value]` map, never null, possibly empty. Null return declines (next provider, then air). `ModdedBlockData.direct(state)` for final; `deferred(placeholder)` for a second pass.
- `processBlockPlacement` — finishes deferred placement on the server thread with the chunk loaded. Only the first **ready** provider claiming the id runs for a position. The placeholder should match the final block's shape and occlusion.
- `spawnMob` — server thread. **You must add the entity to the level yourself**; Iris uses the returned reference as-is and does not call `addFreshEntity`. Null declines. Iris applies pack entity settings (name, attributes, loot, passengers) to the result only when the pack sets `applySettingsToCustomMobAnyways`.
- `init()` — once after accept.
`ModdedBlockPlacementContext` record: `engine`, `level`, `position`, `blockId`, `state` (defensive copy, unmodifiable), `blockState` (current at position). Immutable; constructed by Iris. `engine` is internal.
`ModdedBlockPlacementContext` record: `engine`, `level`, `position`, `blockId`, `state` (defensive copy, unmodifiable), `blockState` (state currently at the position). Immutable; constructed by Iris; every component non-null. `engine` is internal.
`ModdedBlockData`: `direct(BlockState)` / `deferred(BlockState)`; null state rejected.
@@ -180,6 +231,83 @@ public interface ModdedDataProvider {
| `init` | Discovering/registering thread | No server/level assumed |
| `modId`, `isReady`, `getTypes` | Any | |
### Worked example: provider
```java
package com.example.yourmod.iris;
import art.arcane.iris.modded.api.ModdedBlockData;
import art.arcane.iris.modded.api.ModdedBlockPlacementContext;
import art.arcane.iris.modded.api.ModdedDataProvider;
import art.arcane.iris.modded.api.ModdedDataType;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public final class YourIrisProvider implements ModdedDataProvider {
private static final String NAMESPACE = "yourmod";
private static final Identifier ALTAR = Identifier.parse("yourmod:altar");
@Override
public String modId() {
return NAMESPACE;
}
@Override
public Collection<Identifier> getTypes(ModdedDataType type) {
return type == ModdedDataType.BLOCK ? List.of(ALTAR) : List.of();
}
@Override
public boolean isValidProvider(Identifier id, ModdedDataType type) {
return NAMESPACE.equals(id.getNamespace());
}
@Override
public ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
if (!ALTAR.equals(blockId)) {
return null;
}
// The altar carries a block entity, so write a solid placeholder now and
// finish once the chunk is loaded.
return ModdedBlockData.deferred(Blocks.COBBLESTONE.defaultBlockState());
}
@Override
public void processBlockPlacement(ModdedBlockPlacementContext context) {
Block altar = BuiltInRegistries.BLOCK.getValue(ALTAR);
context.level().setBlock(
context.position(),
altar.defaultBlockState(),
Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
}
@Override
public Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
EntityType<?> type = BuiltInRegistries.ENTITY_TYPE.getValue(entityId);
if (type == null) {
return null;
}
// EntityType.spawn adds the entity to the level and returns it.
return type.spawn(level, BlockPos.containing(x, y, z), EntitySpawnReason.NATURAL);
}
}
```
### ServiceLoader registration
`META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider` — binary names, one per line. Nested classes use `$`. Public no-arg constructor required.
@@ -188,10 +316,10 @@ Iris loads with **its** class loader: `ServiceLoader.load(ModdedDataProvider.cla
### When discovery runs
`ModdedCustomContentRegistry.discover()` runs inside `ModdedEngineBootstrap.bootCommon(...)` at loader entry (`IrisFabricBootstrap.onInitialize`, Forge/NeoForge bootstrap construction) — before chunk-generator registration and long before a server starts.
`ModdedCustomContentRegistry.discover()` runs inside `ModdedEngineBootstrap.bind()`, which `ModdedEngineBootstrap.bootCommon(...)` calls at loader entry (`IrisFabricBootstrap.onInitialize`, the Forge and NeoForge bootstrap constructors) — before chunk-generator registration and long before a server starts.
- ServiceLoader providers are available before any world resolves blocks.
- Discovery once per process; second `discover()` is no-op.
- Discovery once per process; a second `discover()` is a no-op returning an inert handle.
- `init()` must not assume a server, level, or full game registry. Gate late work with `isReady()`.
### Imperative registration
@@ -204,6 +332,7 @@ Under the `Iris` logger:
```
Iris registered custom content provider 'yourmod'
Iris registered custom block data yourmod:fancy_log -> minecraft:oak_log[axis=y]
```
Duplicates: `already registered; ignoring duplicate`.
@@ -214,10 +343,18 @@ Per-callback failures are caught, logged against `modId()`, and generation conti
Iris custom content provider 'yourmod' failed resolving block yourmod:thing
Iris custom content provider 'yourmod' failed post-placement for yourmod:thing at BlockPos{...}
Iris custom content provider 'yourmod' failed spawning mob yourmod:critter
Iris custom content provider 'yourmod' failed listing BLOCK types
Iris custom content provider 'yourmod' failed to initialize
```
ServiceLoader `init` failure is all-or-nothing: aborts discovery, rolls back that pass, rethrows:
A deferred placement with no ready provider, or an unparseable key, warns and is skipped:
```
Iris deferred custom block placement has no provider for yourmod:thing
Iris deferred custom block placement rejected invalid id yourmod:Thing
```
ServiceLoader `init` failure is all-or-nothing: aborts discovery, rolls back that pass, and rethrows. A `RuntimeException` or `Error` propagates unchanged; anything else is wrapped in `IllegalStateException`.
```
Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider)
@@ -229,9 +366,9 @@ Iris custom content provider discovery failed at provider 'yourmod' (com.example
IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]");
```
State string uses pack syntax; parsed immediately. Bad state logged and dropped at startup. Aliases take precedence over providers for the same key. Null args ignored.
`state` is parsed immediately with the vanilla block-state parser against the block registry, so call it after blocks are registered; an invalid identifier or unparseable state is logged and the registration dropped at startup rather than showing up as missing blocks. Aliases are matched on the identifier alone and win over providers for the same key, so `[prop=value]` in a pack key is ignored for an aliased block — the alias's fixed state is what gets written. Null arguments are ignored.
Go through `IrisModdedAPI`, not `ModdedCustomContentRegistry` resolution methods (public only for Iris internal packages).
Go through `IrisModdedAPI`, not `ModdedCustomContentRegistry` resolution methods. `hasProviders()`, `aliasBlockKeys()`, and `providerKeys(ModdedDataType)` on that class are read-only snapshots intended for pack tooling; everything else on it is Iris internal and public only because the adapter's generation code lives in another package.
---
@@ -244,13 +381,15 @@ Paths relative to the loader config directory (`config/`):
| `config/irisworldgen/packs/<pack>/` | Installed packs (`dimensions/<dimension>.json` required) |
| `config/irisworldgen/generated/datapack/iris/` | Generated forced datapack — Iris-owned |
| `config/irisworldgen/modded.json` | Default pack, auto-download, primary world routing |
| `config/iris/` | Engine data: settings and per-world engine state |
| `config/iris/` | Engine data: `settings.json` and `worlds.json` |
Pack install root is `config/irisworldgen/packs`, not `config/iris`. Missing pack at world open is a hard failure with the expected absolute path (no silent vanilla terrain).
Pack install root is `config/irisworldgen/packs`, not `config/iris`. Per-level engine state that must travel with the save (`iris-dimensions.json`) lives in the world folder under `<world>/iris/`, not in `config/`. Missing pack at world open is a hard failure with the expected absolute path (no silent vanilla terrain).
When `autoDownloadDefaultPack` is enabled, startup asynchronously installs missing `overworld` and `underworld` managed beta releases, plus a distinct configured non-managed `defaultPack`. Failures log a pointer to `/iris download <pack>`; managed beta names always resolve to their release assets rather than the requested branch.
`modded.json` keys: `defaultPack`, `autoDownloadDefaultPack`, `primaryWorld`, `routePlayersToPrimaryWorld`, `mainWorldPack`, `mainWorldSeed`, `mainWorldAutoRestart`.
Forced datapack contributes presets, dimension types, and biomes under `irisworldgen` (ids from pack/dimension names). Regenerated on pack change / studio hotload. Failure to inject (mixin/event not applied) logs once:
When `autoDownloadDefaultPack` is enabled, startup installs missing packs on a daemon thread: the managed beta releases `overworld` and `underworld`, plus a configured non-managed `defaultPack`. Managed names always resolve to their release assets rather than the requested branch. A failure warns with a pointer to `/iris download <pack>`.
Forced datapack id is `iris_worldgen`; it contributes presets, dimension types, and biomes under the `irisworldgen` namespace (ids derived from pack/dimension names). Regenerated on pack change / studio hotload. Failure to inject (mixin/event not applied) logs once at startup:
```
Iris forced datapack 'iris_worldgen' was never loaded by this server.
@@ -260,19 +399,20 @@ That is a loader/environment problem, not a pack problem.
### Commands (modded)
`/iris` (`/ir`, `/irs`), gamemaster level. Full tree: `04 - Commands & Permissions.md`. Notable:
`/iris` (`/ir`, `/irs`). The root literal is ungated; each subcommand carries its own check — read-only nodes (`version`, `info`, `what`, `height`, `worlds`, `help`) require `LEVEL_ALL`, everything else `LEVEL_GAMEMASTERS` (op level 2 or the equivalent permission). Full tree: `04 - Commands & Permissions.md`. Notable:
| Command | Role |
|---|---|
| `/iris pack validate [pack]` | Validate packs on a worker |
| `/iris pack validate [pack]` | Validate packs on a worker; blank validates all |
| `/iris pack status [pack]` | Recorded validation results |
| `/iris pack cleanup` / `restore` | Unused resource preview/apply |
| `/iris pack cleanup <pack> [apply]` / `restore <pack>` | Unused resource preview/apply |
| `/iris datapack status` | Active vs pack dimension-type heights |
| `/iris datapack install` | Write pack dimension type override into world datapacks |
| `/iris datapack list` | Configured and installed world datapacks |
| `/iris download <pack>` | Install pack (`dl` alias) |
| `/iris version` | Version and loader |
`/iris datapack ingest|pull|remove` refuse on modded (Bukkit Modrinth tooling). Native/datapack structure placement works: install into `<world>/datapacks/` and restart. Structures overview: `22 - Native Structures & Datapacks.md`.
`/iris datapack ingest` (`pull`) and `remove` (`rm`) exist but always refuse on modded — the Modrinth tooling is Bukkit-only. Native/datapack structure placement still works: install into `<world>/datapacks/` and restart. Structures overview: `22 - Native Structures & Datapacks.md`.
---
@@ -284,12 +424,12 @@ Iris replaces the chunk generator. Vanilla/mod worldgen runs only if Iris runs i
|---|---|---|
| Structures (vanilla, datapack, mod) | **Yes**, on by default | Vertical fit, stilts, vegetation clear. Deny families with `importedStructures.disabled`, one complete key with `disabledExact`, or scale an exact structure set with `frequencyOverrides` |
| Placed features (ores, trees, plants, …) | **Yes**, **off** by default | Dimension `importedFeatures.enabled` |
| Carvers | **Never** | No noise router / aquifer for vanilla carvers |
| Carvers | **Never** | `applyCarvers` is an empty override; no noise router or aquifer for vanilla carvers |
| Mod biomes as sources | Only as derivative / scatter targets | Iris chooses biomes from the pack |
| Mob spawning (incl. mod mobs) | **Yes** | Merges pack biome table with vanilla derivative |
| Surface builders / rules | **Never** | Pack palettes |
| Mob spawning (incl. mod mobs) | **Yes** | Pack biome table merged ahead of the vanilla derivative's |
| Surface builders / rules | **Never** | `buildSurface` is an empty override; pack palettes decide |
`importedStructures.frequencyOverrides` has Bukkit parity on all three mod loaders. Entries use `{ "structureSet": "namespace:path", "multiplier": 0.01..16 }`; keys are exact registered structure-set keys, last duplicate wins, and changes affect new chunks only. Random-spread sets scale probability first and then derive the nearest legal integer spacing, while concentric rings can scale probability only. Custom placement types outside the affected override and exclusion-zone graph remain untouched; an unsupported placement that must be copied fails level binding rather than applying a partial override. Full semantics and the Nether `1.1` example are in `22 - Native Structures & Datapacks.md`.
`importedStructures.frequencyOverrides` has Bukkit parity on all three mod loaders. Entries use `{ "structureSet": "namespace:path", "multiplier": 0.01..16 }`; keys are exact registered structure-set keys, last duplicate wins, and changes affect new chunks only. Random-spread sets scale probability first (clamped at 1) and then derive the nearest legal integer spacing, while concentric rings can scale probability only. Sets outside the affected override and exclusion-zone graph are returned untouched; an unsupported placement type that must be copied throws during level structure-state construction, so binding fails rather than a partial override being applied. Full semantics and the Nether `1.1` example are in `22 - Native Structures & Datapacks.md`.
### `importedFeatures`
@@ -315,7 +455,7 @@ Disabled by default. Absent or `enabled: false` → no feature table; terrain ma
Vanilla step order: `RAW_GENERATION`, `LAKES`, `LOCAL_MODIFICATIONS`, `UNDERGROUND_STRUCTURES`, `SURFACE_STRUCTURES`, `STRONGHOLDS`, `UNDERGROUND_ORES`, `UNDERGROUND_DECORATION`, `FLUID_SPRINGS`, `VEGETAL_DECORATION`, `TOP_LAYER_MODIFICATION`.
Features come from the biome's **vanilla derivative**. Feature seeds match vanilla derivation so denying one feature does not shift another's seed. Feature pass runs on the worldgen thread owning the chunk (not Iris gen pool), after Iris structures — early-step features can cut into placed structures. Feature-order cycles at bind degrade `importedFeatures` to off with ERROR log (no chunk crash). Same control exists on Bukkit with the same semantics.
Features come from the biome's **vanilla derivative**. Feature seeds match vanilla derivation so denying one feature does not shift another's seed. The feature pass runs on the worldgen thread owning the chunk (not the Iris gen pool), after Iris structures — early-step features can cut into placed structures. A feature-order cycle detected at bind degrades `importedFeatures` to off with an ERROR log rather than failing the chunk. Same control exists on Bukkit with the same semantics.
### Carvers
@@ -326,10 +466,10 @@ Features come from the biome's **vanilla derivative**. Feature seeds match vanil
| Registry | 26.2 key |
|---|---|
| Block | `minecraft:pointed_dripstone` unchanged |
| Placed / configured feature | `minecraft:pointed_dripstone` unchanged |
| Feature **type** | `minecraft:speleothem` renamed |
| Configured / placed feature | `minecraft:pointed_dripstone` unchanged |
| Feature **type** | `minecraft:speleothem` |
Pack palette / object / `importedFeatures.disabled` keys using the block or placed-feature id remain correct.
Pack palette / object / `importedFeatures.disabled` keys using the block or placed-feature id remain correct. Only code registering or matching a feature *type* sees the new name.
### Biome tags
@@ -341,11 +481,11 @@ Custom biomes inherit derivative tag membership plus pack `tags`. Files use `"re
- **No published mod artifact** — build from source.
- **Core packages internal** — `engine.*`, `core.*`, `util.*`, `spi.*`. `Engine` is the only internal type exposed (as a token) via `getEngine` / placement context.
- **No event bus** — `IrisPlatform.callEvent` is a no-op on mod loaders. No modded `IrisWorldEngineEvent` / `IrisPregenerationEvent`; poll `isIrisLevel` / `getEngine`.
- **No `art.arcane.iris.api`** in mod jars. No modded terrain-query service yet.
- **No event bus** — `ModdedPlatform.callEvent` is an empty method on mod loaders. No modded `IrisWorldEngineEvent` / `IrisPregenerationEvent`; poll `isIrisLevel` / `getEngine`.
- **No `art.arcane.iris.api`** in mod jars (verified: zero classes under that package in each loader jar). No modded terrain-query service, and no tree-feller integration API.
- **No PlaceholderAPI** — `09 - PlaceholderAPI.md` is Bukkit-only.
- **Datapack ingest commands** Bukkit-only.
- **Datapack ingest/remove commands** Bukkit-only.
- **Vanilla carvers and surface rules never run.**
- **`ModdedCustomContentRegistry` resolution APIs** are Iris internals; use `IrisModdedAPI`.
- **No custom mantle slice types** — the mantle accepts only registered value types.
Install and platform notes: `01 - Installation & Platforms.md`, `30 - Platform Differences.md`.