Updated Docs, and Cortections

This commit is contained in:
Brian Neumann-Fopiano
2026-08-08 00:29:48 -06:00
parent c40e1cf152
commit 506787f51a
70 changed files with 9485 additions and 3050 deletions
+128
View File
@@ -0,0 +1,128 @@
# 00 - Overview
Iris is a world generation engine for Minecraft servers and mod loaders. It builds terrain, biomes, caves, structures, objects, and entities from editable JSON packs, exposes an in-game studio authoring workflow, and runs as a Bukkit-family plugin or as a Fabric, Forge, or NeoForge server mod. Cross-platform generation is designed and tested for deterministic parity when artifacts, pack bytes, seeds, and test areas are identical; verify release candidates with GoldenHash. This branch targets Minecraft 26.2; Java 25 is required everywhere.
## Platforms
| Platform | Artifact | Minecraft | Notes |
|---|---|---|---|
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 26.2 | Full plugin feature set |
| Folia | plugin jar | 26.1.2 26.2 | Region-safe scheduling; runtime world create is staged for restart (see `01 - Installation & Platforms.md`, `06 - Worlds & Lifecycle.md`) |
| Spigot / CraftBukkit | plugin jar | 26.1.2 26.2 | Full plugin feature set |
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; Fabric Loader 0.19.3+ |
| Forge | mod jar | 26.2 | Server worldgen + client HUD; Forge 65.0.4+ |
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; NeoForge 26.2.0.12-beta+ |
Plugin identity: name `Iris` (from root project name), command `iris` with aliases `ir` / `irs`, `folia-supported: true`, `load: STARTUP`, `api-version` 26.1 (loads on 26.1.2 and 26.2). Soft-depends include PlaceholderAPI, WorldEdit, item plugins, MythicMobs; Multiverse-Core is ordered after Iris (`loadbefore` / paper `load: AFTER`).
Mod id on all three loaders: `irisworldgen`.
## Feature map
| Area | What it covers | Doc |
|---|---|---|
| Install and platforms | Plugin vs mod jars, data dirs, first boot, native worldgen matrix | `01 - Installation & Platforms.md` |
| First steps | Create, load, teleport, pregen, studio | `02 - Getting Started.md` |
| Configuration | `settings.json` keys, defaults, hotload | `03 - Configuration.md` |
| Commands and permissions | Full `/iris` tree, Bukkit vs modded argument style | `04 - Commands & Permissions.md` |
| Pack layout | Roots, keys, snippets, world snapshot vs studio | `05 - Concepts & Pack Layout.md` |
| Worlds | create / load / unload / remove, main world, Folia, pack copy | `06 - Worlds & Lifecycle.md` |
| Pregeneration | Jobs, cache, mantle, HUD | `07 - Pregeneration.md` |
| Localization | Locales, overrides, client lang | `08 - Localization.md` |
| PlaceholderAPI | `%iris_…%` keys and migration | `09 - PlaceholderAPI.md` |
| Studio and schemas | Studio worlds, VSCode workspace, hotload | `10 - Studio & VSCode Schemas.md` |
| Dimensions | Dimension JSON, modes, height, imports | `11 - Dimensions.md` |
| Regions | Region-level content | `12 - Regions.md` |
| Biomes | Biome JSON, layers, custom biomes, spawns | `13 - Biomes.md` |
| Generators and noise | Generators, styles, expressions, images | `14 - Generators & Noise.md` |
| Caves and carving | Cave profiles, field modules | `15 - Caves & Carving.md` |
| Surfaces | Decorators, deposits, palettes | `16 - Surfaces, Decorators & Deposits.md` |
| Procedural decoration | Trees, fungi, coral, crystals, formations, ruins | `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md` |
| Structures overview | Objects vs jigsaw vs native | `18 - Structures Overview.md` |
| Objects | Creating and importing `.iob` | `19 - Objects.md` |
| Object placement | Placing objects in biomes and regions | `20 - Object Placement.md` |
| Jigsaw | Iris multi-piece structures | `21 - Jigsaw Structures.md` |
| Native structures | Vanilla / datapack structures on Iris | `22 - Native Structures & Datapacks.md` |
| Loot and entities | Pack entities, loot, spawners, markers | `23 - Loot, Entities, Spawners, Markers.md` |
| Pack extensions | Reusable snippets and the inactive pack-mod schema | `24 - Pack Mods & Snippets.md` |
| Pack management | Download, validate, cleanup, package, update-world | `25 - Pack Management.md` |
| Minimal pack example | Walkthrough | `26 - Example - Minimal Dimension.md` |
| Overworld example | Editing the shipping overworld | `27 - Example - Configuring Overworld.md` |
| Integrations | WorldEdit, Multiverse, Mythic, item plugins, tree feller | `28 - Integrations.md` |
| Client HUD | Client mod HUD and protocol channel | `29 - Client HUD & Protocol.md` |
| Platform matrix | Bukkit vs Fabric / Forge / NeoForge differences | `30 - Platform Differences.md` |
| Operator checks | Manual verification | `31 - Operator Runbooks & Smoke Tests.md` |
| Determinism | Goldenhash cross-platform gate | `32 - Determinism & Goldenhash.md` |
| Performance | Threads, mantle, SIMD, pregen caps | `33 - Performance Tuning.md` |
| Maintainer — MC version bump | Version bump procedure | `85 - Maintainer - MC Version Bump.md` |
| Maintainer — release | Release steps | `86 - Maintainer - Release Checklist.md` |
| Maintainer — readiness | Living readiness tracker | `87 - Maintainer - Release Readiness.md` |
| API — setup | Bukkit public API dependency | `90 - API - Getting Started.md` |
| API — terrain | Terrain query service | `91 - API - Terrain.md` |
| API — events | Engine and pregen events | `92 - API - World Events.md` |
| API — tree feller | Tree feller service | `93 - API - Tree Feller.md` |
| API — modded | Modded public API (`art.arcane.iris.modded.api`) | `94 - API - Modded.md` |
Docs `00``33` are for operators and pack authors in reading order. `85``87` are maintainer checklists. `90``94` are for plugin and mod developers.
## Content model (brief)
| Term | Meaning |
|---|---|
| Pack | Directory of JSON and `.iob` under `packs/<key>/` with at least `dimensions/*.json` |
| Dimension | Root config for a world type (height, modes, regions, imports) |
| Region / biome / generator | Spatial and terrain authoring units |
| Object / structure | Placed content (`.iob`, Iris jigsaw, native or datapack structures) |
| Studio | Transient authoring world with live pack hotload and VSCode schemas; deleted on close and purged at startup |
| World pack snapshot | Production worlds copy the pack into `<world>/iris/pack` and read that copy (see `05 - Concepts & Pack Layout.md`) |
## Project layout
| Path | Role |
|---|---|
| `core/` | Pure-JVM engine, pack loader, pregen, studio services, localization catalogs |
| `core/agent/` | Agent helper module used by the core build |
| `spi/` | Platform SPI and pure-JVM contracts (`IrisPlatform`, protocol types) |
| `adapters/bukkit/plugin/` | Bukkit plugin main, commands, public Bukkit API, Paper plugin descriptor |
| `adapters/bukkit/nms/v26_2_R1/` | NMS bindings for the current Minecraft line |
| `adapters/minecraft-common/` | Shared adapter code used by Bukkit and mod loaders |
| `adapters/modded-common/` | Shared Fabric / Forge / NeoForge worldgen, commands, services |
| `adapters/client-common/` | Client HUD and world-type screens |
| `adapters/fabric/`, `adapters/forge/`, `adapters/neoforge/` | Standalone loader builds (own `settings.gradle`) |
| `probe/` | Offline tooling and stub platform |
| `buildSrc/` | Shared Gradle helpers (artifact verification, API generation) |
| `dist/` | Built consumer jars after `buildAllToOut` |
| `docs/` | Authoritative product and API documentation |
## Building
Requirements: JDK 25 (`JAVA_HOME` set). From the Iris repo root:
```
./gradlew build
./gradlew test
./gradlew buildAllToOut
```
`buildAllToOut` writes every platform jar into `dist/`:
```
Iris v<version> [CraftBukkit] <mc>.jar
Iris v<version> [Fabric] <mc>+<loader>.jar
Iris v<version> [Forge] <mc>+<loader>.jar
Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Per-platform: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. SPI jar: `./gradlew :spi:jar``spi/build/libs/`.
Modded adapters are driven with their own project root when developing:
```
./gradlew -p adapters/fabric runServer
./gradlew -p adapters/forge runServer
./gradlew -p adapters/neoforge runServer
```
`-PincludeModdedAdapters=true` can surface those builds in the root composite for IDE import only; it is off by default because each adapter includes the root build back for `core`/`spi` substitution.
Current version property: `irisVersion=4.0.0-26.2` in `gradle.properties`.
+143
View File
@@ -0,0 +1,143 @@
# 01 - Installation & Platforms
Iris installs as either a Bukkit-family plugin jar or a self-contained Fabric, Forge, or NeoForge mod jar. Java 25 is required on every platform. On first boot the default `overworld` pack is downloaded when missing; packs live under each platforms data directory.
## Requirements
| Requirement | Value |
|---|---|
| Java | 25 (`--release 25` / `java >= 25` on mod loaders) |
| Minecraft (plugin) | 26.1.2 26.2 (`api-version` 26.1) |
| Minecraft (mod) | 26.2 |
| Fabric Loader | 0.19.3+ |
| Forge | 65.0.4+ |
| NeoForge | 26.2.0.12-beta+ |
| Network | Outbound HTTPS on first boot for default pack download (GitHub IrisDimensions overworld release / pack install) |
## Plugin install (Paper / Purpur / Leaf / Canvas / Folia / Spigot)
1. Place the CraftBukkit-labelled plugin jar into `plugins/`.
2. Start the server. Iris loads at `STARTUP` (`plugin.yml` / `paper-plugin.yml`).
3. On first boot Iris provisions the default `overworld` pack into `plugins/Iris/packs/overworld` when missing (source: IrisDimensions overworld `beta` release zip).
4. Settings are written at `plugins/Iris/settings.json` if absent (`IrisSettings.read()`).
Command root: `/iris` (aliases `/ir`, `/irs`). Explicit permission in the descriptor: `iris.treefeller` (default op). Command access uses the Director permission model rooted at `iris.all` (see `04 - Commands & Permissions.md`).
Soft dependencies (optional, not bundled): PlaceholderAPI, CraftEngine, Nexo, ItemsAdder, SCore, ExecutableItems, MythicLib, MMOItems, eco, EcoItems, MythicMobs, MythicCrucible, KGenerators, WorldEdit. Multiverse-Core is ordered after Iris so Multiverse sees Iris generators after Iris is up.
### Folia note
`folia-supported: true`. Engine work uses region-safe scheduling. Runtime `/iris create` does **not** hot-create a live world on Folia: Iris stages world files, pack snapshot, and `bukkit.yml` registration, then requires a server restart before the world generates and loads. After restart, use `/iris load` or rely on the registered world entry as appropriate. See `06 - Worlds & Lifecycle.md`.
## Mod install (Fabric / Forge / NeoForge)
1. Place the matching mod jar into `mods/`.
2. Start the dedicated server (or a client for singleplayer; see below).
3. The jar is self-contained: core, SPI, and required Fabric API modules are bundled where applicable. Mod id: `irisworldgen`.
4. On first boot, if `config/irisworldgen/modded.json` has `autoDownloadDefaultPack` true (default) and `defaultPack` (default `overworld`) is missing, Iris downloads `IrisDimensions/<pack>` (branch `master` for the auto-prefetch path) into the packs folder before the forced worldgen datapack is written.
Packs installed later register custom dimension types (height ranges) and custom biomes through the forced datapack at server start. **Restart once after adding a pack** so worlds get full heights and biomes. Worlds created before that restart run with fallback heights.
### Singleplayer (modded clients)
Installed Iris packs appear as selectable World Types on the Create New World screen (`IRIS:<Pack>` style presets from the forced datapack). The integrated server runs the same engine as dedicated servers.
### Client HUD
Installing the mod jar on a client adds a pregeneration HUD (progress bar, chunks done/total, percent, chunks/s, ETA; yellow while paused). Key `H` (rebindable, category “Iris”) toggles it. The HUD talks to modded Iris servers and Bukkit/Paper Iris over channel `irisworldgen:main` (custom payloads on modded, plugin messaging on Bukkit). Vanilla clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client mod is inert.
## Data directories
### Plugin (`plugins/Iris/`)
| Path | Role |
|---|---|
| `plugins/Iris/settings.json` | Engine settings (`IrisSettings`); created with defaults on first read |
| `plugins/Iris/packs/<key>/` | Installed packs (workspace name `packs`) |
| `plugins/Iris/bootstrap/` | Default-pack provision marker and related bootstrap state |
| `plugins/Iris/datapacks/` | Datapack download cache / staging (Bukkit datapack tooling) |
| `plugins/Iris/languages/overrides/<locale>.json` | Optional server message overrides |
| `<world>/iris/pack/` | Per-world pack **snapshot** used by production engines |
World dimension roots for managed Iris worlds are under the servers world container (Iris managed dimension storage); see `06 - Worlds & Lifecycle.md`.
### Mod (`config/` relative to the game instance)
| Path | Role |
|---|---|
| `config/irisworldgen/packs/<pack>/` | Installed packs; valid when `dimensions/<dimension>.json` exists |
| `config/irisworldgen/generated/datapack/iris/` | Generated forced datapack (owned by Iris; do not edit) |
| `config/irisworldgen/modded.json` | Mod-side config: `defaultPack`, `autoDownloadDefaultPack`, primary world routing, main-world override |
| `config/iris/` | Engine data directory: `settings.json` and per-world engine state via `dataFile` |
Pack resolution for engines, commands, and the forced datapack uses `config/irisworldgen/packs`. The engine data folder is `config/iris` — different roots.
### Default `modded.json` keys
| Key | Default | Effect |
|---|---|---|
| `defaultPack` | `overworld` | Pack auto-download and default create pack name |
| `autoDownloadDefaultPack` | `true` | Async prefetch at boot when pack missing |
| `primaryWorld` | `""` | Primary-world router target dimension id |
| `routePlayersToPrimaryWorld` | `true` | Route players from vanilla overworld when primary is set |
| `mainWorldPack` | `""` | Main-world generator override pack ref |
| `mainWorldSeed` | `0` | Seed for main-world override |
| `mainWorldAutoRestart` | `false` | Auto-restart related to main-world override |
## Platform defaults (settings)
`IrisSettings` is shared across platforms. Generator default relevant to install and first world:
| Key path | Default | Effect |
|---|---|---|
| `generator.defaultWorldType` | `overworld` | Bukkit `/iris create` resolves `type=default` to this pack/dimension key |
| `general.language` | `en_US` | Server locale selection |
| `studio.openVSCode` | `true` | Whether studio may launch VSCode |
| `studio.autoStartDefaultStudio` | `false` | Do not auto-open studio on boot |
Full key list: `03 - Configuration.md`.
## First boot pack download
| Platform | Behavior |
|---|---|
| Plugin | `DefaultPackBootstrapProvisioner` downloads `https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip` into `packs/overworld` when not already provisioned |
| Mod | If `autoDownloadDefaultPack` and pack missing, async install of configured `defaultPack` into `config/irisworldgen/packs` |
Manual install: `/iris download <pack>` (alias `dl`). Default overworld uses the beta-release path; other packs use `IrisDimensions/<pack>/<branch>` (plugin default branch `stable` for non-default; mod download defaults branch `stable` unless auto-prefetch uses `master` — see `25 - Pack Management.md`).
## Native worldgen over Iris terrain
Iris replaces the chunk generator. Vanilla and mod worldgen only runs where Iris runs it. Identical on every platform:
| Vanilla / mod worldgen | Over Iris terrain | Control |
|---|---|---|
| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies individual keys |
| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters |
| Carvers (caves, canyons, mod carvers) | Never | No `NoiseGeneratorSettings` for a carver to sample; use pack `caves` / `carvings` |
| Surface builders and surface rules | Never | Iris builds surfaces from pack palettes |
| Mod biomes | Only as `derivative`, `vanillaDerivative`, `biomeScatter`, or `biomeSkyScatter` target | Iris chooses biomes from the pack |
| Mob spawning, including mod mobs | Yes | Biome spawn tables merged with the vanilla derivatives |
With `importedFeatures` off (default), chunk output is the pure Iris result. Full control reference: `94 - API - Modded.md` (also applies conceptually on Bukkit for imported native stages).
Independently of that flag, Iris custom biomes inherit biome tags of their vanilla derivative on every platform, so tag-driven content (`#minecraft:is_overworld`, mod spawn rules, etc.) applies to Iris custom biomes.
## Build artifacts
From repo root with JDK 25:
```
./gradlew buildAllToOut
```
Output under `dist/`:
| Pattern | Platform |
|---|---|
| `Iris v… [CraftBukkit] ….jar` | Plugin |
| `Iris v… [Fabric] ….jar` | Fabric |
| `Iris v… [Forge] ….jar` | Forge |
| `Iris v… [NeoForge] ….jar` | NeoForge |
Next: create a world and open studio in `02 - Getting Started.md`. Settings detail in `03 - Configuration.md`.
+260
View File
@@ -0,0 +1,260 @@
# 02 - Getting Started
This page walks through creating an Iris world, teleporting into it, running a short pregeneration, and opening a studio pack workspace. Command argument style differs by platform: Bukkit uses Director keyed optional parameters; modded uses Brigadier positional arguments and flag literals.
Full command trees and permissions: `04 - Commands & Permissions.md`. World lifecycle detail: `06 - Worlds & Lifecycle.md`. Studio detail: `10 - Studio & VSCode Schemas.md`.
## Prerequisites
- Iris installed per `01 - Installation & Platforms.md`
- Java 25 server or mod instance running
- Operator / gamemaster access (`iris` commands; modded mutating commands require permission level 2 / gamemasters)
- Default pack present (auto-downloaded on first boot) or an installed pack under the platform packs directory
## Argument style
| Platform | Required args | Optional args | Example |
|---|---|---|---|
| Plugin (Bukkit) | Positional in declaration order | Must be `key=value` | `/iris create myworld type=overworld seed=1337` |
| Mod (Fabric / Forge / NeoForge) | Positional | Further positional tokens or literal flags | `/iris create myworld overworld 1337` |
On Bukkit, a bare extra token that is not a known key is a hard error. On modded, pregen flags are combinable literals (`gui`, `sync`, `nocache`) after the radius / dimension / center.
## 1. Create a world
### Plugin
```
/iris create <name> [type=…] [seed=…] [main=true|false]
```
| Parameter | Aliases | Default | Meaning |
|---|---|---|---|
| `name` | `world-name` | (required) | World name |
| `type` | `dimension`, `pack` | `default``generator.defaultWorldType` (`overworld`) | Pack/dimension load key |
| `seed` | — | `1337` | World seed |
| `main` | `main-world` | `false` | If true, register a shutdown hook to promote this world as `level-name` in `server.properties` |
Aliases for the create command itself: `c`.
**Reserved names (plugin):** `iris` and `benchmark` are rejected (case-insensitive). Iris suggests using another name (for example `irisworld`).
**Already exists:** if the managed dimension root already exists, create aborts.
**Folia:** runtime create is disabled. Iris stages world files, installs the pack snapshot, registers `bukkit.yml`, and tells you to **restart** the server. After restart the world can load. See `01 - Installation & Platforms.md`.
**Non-Folia:** create builds the world immediately via `IrisToolbelt.createWorld()` (production, not studio).
```
/iris create myworld type=overworld seed=1337
```
### Mod
```
/iris create <name> [pack] [seed]
```
| Parameter | Default | Meaning |
|---|---|---|
| `name` | (required) | Dimension id fragment; normalized under namespace `irisworldgen` when not fully qualified |
| `pack` | `overworld` | Pack key (optional `pack:dimension` form when the packs dimension key differs) |
| `seed` | `1337` | Long seed |
Aliases: `c`. Equivalent world management lives under `/iris world create|enable` with the same enable path.
If the pack is not installed, create starts an async download of `IrisDimensions/<pack>` then injects the dimension. On success the dimension is live and re-injected on later startups.
```
/iris create myworld overworld 1337
```
There is no separate “load” step on modded after a successful create.
## 2. Load a world (plugin only)
```
/iris load <world>
```
Aliases: `import`. Requires an existing managed dimension directory on disk. Origin: player (Director `PLAYER`). Loads through `BukkitWorldReconciler` and registers the world with the server.
Modded worlds created with `/iris create` or `/iris world enable` are already injected; use teleport instead of load.
## 3. Teleport
### Plugin
```
/iris teleport <world> [player=…]
```
Aliases: `tp`. Teleports the target (or the executing player) to the world spawn asynchronously when possible.
```
/iris tp myworld
```
### Mod
```
/iris teleport <dimension> [player]
/iris tp <dimension> [player]
```
Dimension is a loaded level argument (tab-completes Iris dimensions). Console must name a player. Teleport target is a fixed spawn-like position in the Iris dimension (engine-managed placement).
```
/iris tp irisworldgen:myworld
```
## 4. Pregenerate
Radius is in **blocks**. One pregeneration job runs server-wide.
### Plugin
```
/iris pregen start <radius> [world=…] [center=x,z|me] [gui=true|false] [serial=true|false]
```
| Parameter | Default | Notes |
|---|---|---|
| `radius` | (required) | Blocks; must be > 0 |
| `world` | contextual (senders world) | Target world |
| `center` | `0,0` | Or `me` for player position; aliases `middle` |
| `gui` | `true` | Open pregen GUI when available |
| `serial` | `false` | One chunk at a time; requires Paper-compatible server |
Control:
```
/iris pregen stop
/iris pregen pause
/iris pregen status
```
Example:
```
/iris pregen start 352 world=myworld center=0,0 gui=false
```
### Mod
```
/iris pregen start <radius> [dimension] [at <x> <z>] [gui] [sync] [nocache]
```
| Piece | Meaning |
|---|---|
| `radius` | 1100000 blocks |
| `dimension` | Optional level; defaults to current dimension |
| `at x z` | Optional center (default 0, 0) |
| `gui` | Request progress map window on the server display when GUI is launchable |
| `sync` | Synchronous chunk writes |
| `nocache` | Disable resumable checkpoint cache (default is cached / resumable) |
Flags are optional and combinable in any order after the radius/dimension/center prefix.
```
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
```
Control: `/iris pregen stop`, `pause` / `resume`, `status`. Progress: client mod HUD when present, otherwise boss bar / console.
## 5. Studio (first authoring steps)
Studio worlds are transient: closed on command, purged at startup. They read the **live** pack and hotload JSON/object edits into newly generated chunks. Production worlds do not (see pitfalls below).
### Plugin
```
/iris studio create [name=studio] [template=…]
/iris studio open <dimension> [seed=1337]
/iris studio vscode [dimension=default]
/iris studio close
```
| Command | Aliases | Notes |
|---|---|---|
| `create` | `+` | Omitting template scaffolds a **starter** pack (minimal dimension/region/biome/generator). Providing a template copies an existing packs entry (or downloads it) |
| `open` | `o` | Temporary studio world for the pack |
| `vscode` | `vsc` | Write / open a `.code-workspace` with live registry schemas |
| `close` | `x` | Discard studio world |
Default create name is `studio`; if that folder already exists, Iris picks the next free name.
### Mod
```
/iris studio create [name] [template]
/iris studio open <pack> [seed]
/iris studio vscode [pack]
/iris studio update [pack]
/iris studio close
```
| Command | Notes |
|---|---|
| `create` / `+` | Defaults: name `studio`, template **`example`** (differs from Bukkit starter-pack path when template omitted) |
| `open` / `o` | Pack required; seed default `1337` |
| `vscode` / `vsc` | Generate workspace |
| `update` | Regenerate schemas only |
| `close` / `x` | Discard studio |
Some Bukkit studio tools (importvanilla feature capture, loot GUI, profile, etc.) refuse or redirect on modded with an explicit message; capture vanilla features on Bukkit and copy the pack folder if needed.
```
/iris studio open overworld
/iris studio vscode dimension=overworld
```
## Suggested first-session flow
1. Confirm pack: ensure `overworld` (or your pack) exists under the platform packs directory.
2. Create world (plugin or mod forms above).
3. On Folia plugin: restart after staging, then load if needed.
4. Teleport into the world.
5. Optional: `/iris pregen start 352 …` for a small square (~704×704 blocks).
6. Optional: `/iris studio open <pack>` to edit live; use VSCode schemas for autocomplete of blocks/items/entities (mod content included on mod loaders).
## Common pitfalls
| Pitfall | What happens | What to do |
|---|---|---|
| World name `iris` or `benchmark` (plugin) | Create rejected | Use another name |
| Editing `packs/<pack>` after production create | **No effect** on existing worlds | Production engines read `<world>/iris/pack` snapshot. Push with `/iris developer update-world world=<world> pack=<dimension> confirm=true` (Bukkit, all keyed) and restart; or only new chunks after update. Studio reads live packs |
| Expecting pack edits in old chunks | Only new chunks use new config | Fly to unexplored terrain, pregen fresh radius, or use studio |
| Folia: create then teleport immediately | World not live yet | Restart after staging message, then load/teleport |
| Mod: new pack heights/biomes missing | Forced datapack not yet applied | Restart after installing pack |
| `/iris load` on modded | No equivalent subcommand | Use create/enable + teleport |
| Bukkit optional args without `key=` | Parse error | Use `seed=1337`, not a bare second number for optional params |
| Mod pregen while another job runs | Start fails | `/iris pregen stop` then start again |
| Studio closed mid-edit | World discarded | Edits on disk in `packs/` remain; reopen studio |
| Default pack download blocked | Create/open fails missing pack | Allow HTTPS or `/iris download overworld` offline install of a pack tree |
| `type=default` vs pack key | Resolves via `generator.defaultWorldType` | Prefer explicit `type=overworld` or your pack key |
## Quick reference
**Plugin**
```
/iris create myworld type=overworld seed=1337
/iris tp myworld
/iris pregen start 352 world=myworld center=0,0 gui=false
/iris studio open overworld seed=1337
/iris studio close
```
**Mod**
```
/iris create myworld overworld 1337
/iris tp irisworldgen:myworld
/iris pregen start 352 irisworldgen:myworld at 0 0
/iris studio open overworld 1337
/iris studio close
```
Next: pack structure in `05 - Concepts & Pack Layout.md`, configuration in `03 - Configuration.md`.
+237
View File
@@ -0,0 +1,237 @@
# 03 - Configuration
Iris stores shared runtime settings in `settings.json` under the platform data folder. On first boot Iris writes a full defaults file if missing; every successful load rewrites the file so new keys appear with defaults. See `01 - Installation & Platforms.md` for data paths and `33 - Performance Tuning.md` for tuning guidance.
## File locations
| Platform | Shared settings | Packs root | Modded-only config |
|----------|-----------------|------------|--------------------|
| Bukkit / Paper / Folia | `plugins/Iris/settings.json` | `plugins/Iris/packs/` | — |
| Fabric / Forge / NeoForge | `<configDir>/iris/settings.json` | `<configDir>/irisworldgen/packs/` | `<configDir>/irisworldgen/modded.json` |
`<configDir>` is the loader config directory (game `config/` for Fabric/Forge/NeoForge). Both surfaces use the same `IrisSettings` schema for `settings.json`.
## Load, save, hotload
| Action | Behavior |
|--------|----------|
| First boot | Create `settings.json` with current defaults if the file is absent |
| Load | Parse with Gson into `IrisSettings`; on parse failure log and keep empty defaults for that boot |
| After load | Rewrite `settings.json` (pretty JSON) so new keys and migrated values persist |
| `/iris reload` | Invalidate cached settings, re-read `settings.json`, reload locale |
| Hotload (Bukkit) | `SettingsHotloadWatch` via VolmLib `ConfigHotloadEngine`; on content change invalidates, reloads, reloads language, logs `Hotloaded settings.json` |
| Hotload (modded) | `ModdedSettingsHotloadService` polls every 3s; on `lastModified` change invalidates, reloads, reloads language, logs `Hotloaded settings.json` |
| Locale-only tick | Hotload paths also call `IrisLanguage.update()` when the file is unchanged |
| `forceSave()` | Used by `/iris debug` and similar toggles that mutate settings in memory |
Legacy migration: if raw JSON still has `world.anbientEntitySpawningSystem`, it is copied to `world.ambientEntitySpawningSystem` and logged once.
## Root object
Top-level Gson fields on `IrisSettings` (all nested objects are created with defaults when missing):
| Field | Nested class | Purpose |
|-------|--------------|---------|
| `general` | `IrisSettingsGeneral` | Language, debug, colors, datapack ingest, strict keys, splash |
| `world` | `IrisSettingsWorld` | Entity systems, async tick, WorldEdit CUI, pregen cache |
| `gui` | `IrisSettingsGUI` | Server-launched GUIs and pregen GUI options |
| `autoConfiguration` | `IrisSettingsAutoconfiguration` | Spigot/Paper timeout autoconfig, custom-biome restart |
| `generator` | `IrisSettingsGenerator` | Default world type, leaf decay |
| `concurrency` | `IrisSettingsConcurrency` | Runtime thread helpers only (no persisted fields) |
| `studio` | `IrisSettingsStudio` | Studio open/VSCode/weather/spawn defaults |
| `performance` | `IrisSettingsPerformance` | Mantle, caches, SIMD, nested engine SVC |
| `pregen` | `IrisSettingsPregen` | Pregen scheduler, mantle residency, timeouts |
| `sentry` | `IrisSettingsSentry` | Error reporter options |
| `treeFeller` | `IrisSettingsTreeFeller` | Survival tree feller enable and axe durability |
Static helper `IrisSettings.getThreadCount(int c)`: for `c` in `{-1,-2,-4}` returns `max(availableProcessors / -c, 1)`; otherwise `max(c, 2)` floored to at least 1.
## `general`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `language` | string | `"en_US"` | Active locale key; reloaded by `/iris reload` and hotload |
| `commandSounds` | boolean | `true` | Tab-complete amethyst chime on Bukkit when true |
| `debug` | boolean | `false` | Toggled by `/iris debug`; saved immediately |
| `dumpMantleOnError` | boolean | `false` | Dump mantle plates when tectonic errors occur |
| `disableNMS` | boolean | `false` | Disable NMS bindings when true |
| `pluginMetrics` | boolean | `true` | Plugin metrics reporting |
| `splashLogoStartup` | boolean | `true` | Console splash on enable |
| `useConsoleCustomColors` | boolean | `true` | Custom colors for console senders |
| `useCustomColorsIngame` | boolean | `true` | Custom colors for player senders |
| `adjustVanillaHeight` | boolean | `false` | Adjust vanilla height handling |
| `autoIngestDatapacks` | boolean | `true` | Auto-ingest configured datapack imports (Bukkit datapack pipeline) |
| `autoImportDatapackStructures` | boolean | `false` | Opt-in bulk write of every registered datapack structure as editable Iris resources; prefer `/iris structure import <dimension>` |
| `strictContentKeys` | boolean | `false` | Unresolved pack content keys and bad block-state properties become blocking pack errors; system property `-Diris.strictContent` overrides when set |
| `spinh` | int | `-20` | Splash / spin color H |
| `spins` | int | `7` | Splash / spin color S |
| `spinb` | int | `8` | Splash / spin color B |
## `world`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `postLoadBlockUpdates` | boolean | `true` | Post-load block updates |
| `forcePersistEntities` | boolean | `true` | Force entity persistence |
| `ambientEntitySpawningSystem` | boolean | `true` | Ambient entity spawning (legacy key `anbientEntitySpawningSystem` migrated) |
| `asyncTickIntervalMS` | long | `700` | World manager async tick interval ms |
| `targetSpawnEntitiesPerChunk` | double | `0.95` | Target entity density per chunk |
| `markerEntitySpawningSystem` | boolean | `true` | Marker-driven entity spawning |
| `effectSystem` | boolean | `true` | Engine effects |
| `worldEditWandCUI` | boolean | `true` | WorldEdit wand CUI integration (Bukkit) |
| `globalPregenCache` | boolean | `false` | Global pregen cache |
If both `markerEntitySpawningSystem` and `ambientEntitySpawningSystem` are false, the world manager skips related entity work.
## `gui`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `useServerLaunchedGuis` | boolean | `true` | Allow server-side GUI hosts (noise map, vision, pregen UI) |
| `maximumPregenGuiFPS` | boolean | `false` | Cap pregen GUI at max FPS when true |
| `colorMode` | boolean | `true` | Colored GUI mode |
## `autoConfiguration`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `configureSpigotTimeoutTime` | boolean | `true` | Raise Spigot timeout on Bukkit family when supported |
| `configurePaperWatchdogDelay` | boolean | `true` | Adjust Paper watchdog delay when supported |
| `autoRestartOnCustomBiomeInstall` | boolean | `true` | Auto-restart path after custom biome datapack install when required |
Bukkit-oriented; no-op or unused on mod loaders.
## `generator`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `defaultWorldType` | string | `"overworld"` | Default pack/dimension type key for world create when not overridden by command defaults |
| `preventLeafDecay` | boolean | `true` | Prevent leaf decay on Iris-managed leaves when true |
## `concurrency`
This section has **no public fields** serialized to JSON. Gson writes an empty object `{}`. Methods used at runtime:
| Method | Result |
|--------|--------|
| `getParallelism()` | `max(2, availableProcessors)` |
| `getIoParallelism()` | `max(2, availableProcessors / 2)` |
| `getWorldGenThreads()` | `max(2, availableProcessors)` |
## `studio`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `openVSCode` | boolean | `true` | Open VS Code / workspace on studio open paths |
| `disableTimeAndWeather` | boolean | `true` | Freeze time/weather in studio worlds |
| `entitySpawning` | boolean | `true` | Allow entity spawning in studio |
| `autoStartDefaultStudio` | boolean | `false` | Auto-open default studio on enable |
## `performance`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `engineSVC` | object | see below | Nested engine service thread pool |
| `trimMantleInStudio` | boolean | `false` | Trim mantle while in studio |
| `mantleKeepAlive` | int | `30` | Mantle keep-alive window |
| `noiseCacheSize` | int | `1024` | Noise cache capacity |
| `resourceLoaderCacheSize` | int | `1024` | Resource loader cache |
| `objectLoaderCacheSize` | int | `4096` | Object loader cache |
| `mantleCleanupDelay` | int | `200` | Cleanup delay ticks; world manager uses `max(mantleCleanupDelay * 50, 0)` ms |
| `simdKernels` | boolean | `true` | SIMD-accelerated noise kernels when available |
### `performance.engineSVC`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `useVirtualThreads` | boolean | `true` | Prefer virtual threads when available |
| `forceMulticoreWrite` | boolean | `false` | Force multicore write path |
| `priority` | int | `Thread.NORM_PRIORITY` (5) | Clamped to `[MIN_PRIORITY, MAX_PRIORITY]` |
| `parallelism` | int | `-1` | `>0`: min of configured and `processors * 2`; `≤0`: `ceil(sqrt(processors))` at least 1 |
## `pregen`
| Key | Type | Default | Effective clamp / resolve |
|-----|------|---------|---------------------------|
| `runtimeSchedulerMode` | enum | `AUTO` | `AUTO`, `PAPER_LIKE`, `FOLIA`. Regionized (Folia) always resolves to `FOLIA`. On non-regionized, configured `FOLIA` is forced to `PAPER_LIKE`. `AUTO` probes server name/version/class for Folia vs Paper-like family |
| `paperLikeBackendMode` | enum | `AUTO` | `AUTO`, `TICKET`, `SERVICE`. Non-`AUTO` uses the configured value; `AUTO` resolves to `TICKET` |
| `chunkLoadTimeoutSeconds` | int | `15` | Clamped `[5, 120]` |
| `timeoutWarnIntervalMs` | int | `500` | Minimum 250 |
| `saveIntervalMs` | int | `30000` | Clamped `[5000, 900000]` |
| `maxResidentTectonicPlates` | int | `96` | Minimum 16 via getter; effective residency also scales by world height and ~60% heap budget (~48 MB reference plate at height 384) with floor 16 |
| `mantleBackpressureWaitMs` | int | `25` | Clamped `[5, 1000]` |
| `mantleBackpressureTimeoutMs` | int | `60000` | Clamped `[5000, 600000]` |
| `moddedPregenInFlight` | int | `0` | `>0`: clamped to max 512; `≤0`: `max(16, min(48, cpu * 2))` for modded pregen concurrency |
`runtimeSchedulerMode` and Paper-like backend modes apply to Bukkit-family pregen routing. `moddedPregenInFlight` is the modded in-flight chunk budget.
## `sentry`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `includeServerId` | boolean | `true` | Include server id in reports |
| `disableAutoReporting` | boolean | `false` | Disable automatic Sentry reporting when true |
| `debug` | boolean | `false` | Sentry debug logging |
## `treeFeller`
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `enabled` | boolean | `false` | Master switch for survival tree feller |
| `durabilityPreservationChance` | int | `0` | Percent chance to preserve axe durability; clamped `[0, 100]` |
Requires permission `iris.treefeller` on Bukkit (and the platform tree-feller permission node on mod loaders). See `04 - Commands & Permissions.md` and `28 - Integrations.md`.
## Bukkit-only: `compat.json`
On Bukkit, Iris loads `plugins/Iris/compat.json` at startup and writes the built-in table to `compat.default.json`. Built-in compatibility mappings remain active; entries from `compat.json` are appended so operators can add fallback blocks and items for content that is unavailable on the running server.
```json
{
"blockFilters": [
{ "when": "example:missing_block", "supplement": "minecraft:stone", "exact": false }
],
"itemFilters": [
{ "when": "example:missing_item", "supplement": "minecraft:stick" }
]
}
```
| Field | Applies to | Behavior |
|-------|------------|----------|
| `when` | block and item filters | Unsupported source key to match |
| `supplement` | block and item filters | Replacement key; block replacement can continue through further mappings |
| `exact` | block filters only | When true, match complete block data instead of material only |
Invalid JSON logs the failure and leaves the built-in mappings active. These files are runtime compatibility configuration, not pack resources, and are not used by the modded adapters.
## Modded-only: `modded.json`
Path: `<configDir>/irisworldgen/modded.json`. Written with defaults on first load if missing. Not used by the Bukkit plugin.
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `defaultPack` | string | `"overworld"` | Default pack for bootstrap download/install |
| `autoDownloadDefaultPack` | boolean | `true` | Download default pack when missing |
| `primaryWorld` | string | `""` | Primary Iris dimension id for player routing |
| `routePlayersToPrimaryWorld` | boolean | `true` | Route players to primary when set |
| `mainWorldPack` | string | `""` | Pack (or `pack:dimensionKey`) for main-world preset |
| `mainWorldSeed` | long | `0` | Seed for main-world preset |
| `mainWorldAutoRestart` | boolean | `false` | Auto-restart after main-world inject when true |
Updated by `/iris world mainworld`, `/iris world replace-overworld`, primary-world clear paths, and related world commands. See `06 - Worlds & Lifecycle.md` and `30 - Platform Differences.md`.
## What is not in these files
- Pack JSON (dimensions, biomes, objects) lives under `packs/<key>/` — see `05 - Concepts & Pack Layout.md`.
- Per-world studio/workspace files are generated under pack roots — see `10 - Studio & VSCode Schemas.md`.
- Locale files and overrides — see `08 - Localization.md`.
## Related
- `01 - Installation & Platforms.md`
- `04 - Commands & Permissions.md`
- `07 - Pregeneration.md`
- `25 - Pack Management.md`
- `30 - Platform Differences.md`
- `33 - Performance Tuning.md`
+304
View File
@@ -0,0 +1,304 @@
# 04 - Commands & Permissions
Iris exposes one root command: `/iris` (aliases `/ir`, `/irs`). Bukkit uses VolmLib Director (named parameters, optional `key=value`). Fabric, Forge, and NeoForge register a Brigadier tree with the same root aliases. This is the complete command reference; platform gaps are marked **Bukkit-only** or **modded-only**. See `30 - Platform Differences.md` for a matrix and `03 - Configuration.md` for `/iris reload` targets.
## Syntax
### Bukkit (Director)
- Root: `/iris` / `/ir` / `/irs`.
- Subcommands and nested groups use method names (or `@Director(name=...)`) and aliases.
- Required parameters appear as positionals; optional parameters with defaults accept `name=value` (or short aliases from `@Param`).
- Help uses Director mini-menu: required shown as `<name>`, optional with default as `[name=default]`.
- Example: `/iris create myworld type=overworld seed=42 main=false`
- Example: `/iris pregen start 5000 world=world center=me gui=true serial=false`
- Contextual params (world, dimension, location) often resolve from the senders current world or look target when omitted.
### Modded (Brigadier)
- Same root and most names; arguments are ordered literals/arguments, not free-form `key=value`.
- `/iris` and `/iris help [section]` print the help browser (player paginated UI; console text list).
- Flags are literals where used (e.g. pregen `gui`, `sync`, `nocache`; download `force`).
## Permissions
### Bukkit
| Permission | Declared in `plugin.yml` / `paper-plugin.yml` | Default | Gate |
|------------|-----------------------------------------------|---------|------|
| `iris.all` | Declared in `plugin.yml` / `paper-plugin.yml` (`default: op`) | Operators receive it by default; otherwise grant explicitly | `CommandSVC` rejects every `/iris` execution without `iris.all` |
| `iris.treefeller` | Yes | `op` | Survival tree feller only (`TreeFellerSVC`); requires `treeFeller.enabled` in settings |
`iris.all` is code-gated as `ROOT_PERMISSION` in `CommandSVC` and declared on the plugin with `default: op`. Without it, the sender gets a permission-denied message and no subcommand runs.
Custom-biome restart warnings also notify online players who are op **or** hold `iris.all`.
### Modded
| Gate | Brigadier level | Applies to |
|------|-----------------|------------|
| Gamemaster | `Commands.LEVEL_GAMEMASTERS` | Mutating commands: create/world, studio, object tools, pregen, download, debug, reload, evacuate, seed, structure place, edit, developer, etc. |
| Read-only | `Commands.LEVEL_ALL` | `version`, `info`/`worlds` (seed field omitted unless gamemaster), `height`, `metrics`, `what` (relaxed at root), `help` |
Tree feller on mod loaders uses platform permission APIs (`irisworldgen:treefeller` on Fabric; PermissionAPI nodes on Forge/NeoForge), not Bukkit permission strings.
---
## Root: `/iris`
| Command | Aliases | Platforms | Params (Bukkit-style) | Description |
|---------|---------|-----------|------------------------|-------------|
| (empty) / help | | Both | `[section]` (modded) | Open help; modded supports section path |
| `version` | | Both | — | Print Iris/platform/Minecraft version and engine count |
| `info` | | **Modded** (see `worlds`) | `[dimension]` | List Iris dimensions and pack details; seed only for gamemasters |
| `create` | `c` | Both | **Bukkit:** `<name> [type=default] [seed=1337] [main=false]` (`type` aliases `dimension`,`pack`). **Modded:** `<name> [pack=overworld] [seed=1337]` | Create Iris world/dimension |
| `teleport` | `tp` | Both | **Bukkit:** `<world> [player=<name>]`. **Modded:** `<dimension> [player]` | Teleport self or named player into Iris world/dimension |
| `evacuate` | | Both | **Bukkit:** `<world>` (player origin). **Modded:** `[dimension]` | Move players out of Iris world to fallback/primary |
| `height` | | Both | — | Print world height (player on Bukkit) |
| `worlds` | `accesslist` | Both | — | **Bukkit:** access list of worlds. **Modded:** same as `info` (read-only); `accesslist` requires gamemaster |
| `remove` | `rm` | **Bukkit** | `<world> [delete=true]` | Remove managed Iris world; disk deletion defaults to true |
| `load` | `import` | **Bukkit** | `<world>` | Load managed Iris world |
| `unload` | | **Bukkit** | `<world>` | Unload Iris world |
| `debug` | | Both | — | Toggle `general.debug` and save settings |
| `download` | `dl` | Both | `<pack> [branch=stable] [overwrite=false]` (`overwrite` alias `force`) | Download pack project |
| `metrics` | `measure` | Both | — | Generation metrics (player / current Iris level) |
| `reload` | | Both | — | Reload `settings.json` and locale; modded also schedules forced datapack regeneration |
| `seed` | | **Modded** | — | Print world/engine seeds (gamemaster) |
| `regen` | `rg` | **Modded** root; Bukkit under `developer` | `[radius]` | Delete/regenerate nearby chunks |
| `goldenhash` | `gold` | **Modded** root; Bukkit under `developer` | `[radius] [threads] [capture\|verify]` | Deterministic buffer hashes |
| `wand` | | **Modded** root (+ object) | — | Give object wand |
| `dust` | `d` | **Modded** root (+ object) | — | Give reveal dust |
| `find` | `goto` | Both | see Find | Locate biome/region/object/structure/POI |
| `what` | | Both | see What | Inspect context |
| `edit` | | Both | see Edit | Open JSON in desktop editor |
| `pregen` | `pregenerate` | Both | see Pregen | Pregeneration control |
| `object` | `o` | Both | see Object | Object tools |
| `studio` | `std`, `s` | Both | see Studio | Studio / pack authoring |
| `pack` | `pk` | Both | see Pack | Validate/cleanup/restore/status |
| `structure` | `struct`, `str` | Both | see Structure | Structure index/import/place |
| `datapack` | `datapacks`, `dp` | Both | see Datapack | Datapack helpers |
| `developer` | `dev` | Both | see Developer | Diagnostics |
| `world` | `w` | **Modded** | see World | Runtime dimension enable/disable |
---
## Find: `/iris find` (`goto`)
**Origin:** player (Bukkit). **Modded:** gamemaster gate.
| Command | Params | Description |
|---------|--------|-------------|
| `biome` | **Bukkit:** `<biome> [teleport=true]`. **Modded:** `<key>` | Find Iris biome; teleport default true on Bukkit |
| `region` | **Bukkit:** `<region> [teleport=true]`. **Modded:** `<key>` | Find Iris region |
| `object` | **Bukkit:** `<object> [teleport=true]`. **Modded:** `<key>` | Find object placement (Bukkit may teleport to object studio first) |
| `structure` | **Bukkit:** `<structure>` (sync). **Modded:** `<key>` | Find vanilla/datapack/Iris structure |
| `poi` | **Bukkit:** `<type> [teleport=true]`. **Modded:** `<type>` | Find supported point of interest |
| `unregistered` | — | Print structures excluded from goto completion and rejection reasons to console |
---
## What: `/iris what`
**Bukkit origin:** player only. No bare `here` command on Bukkit.
| Command | Platforms | Params | Description |
|---------|-----------|--------|-------------|
| (empty) / `here` | **Modded** | — | Full inspect at player position |
| `biome` | Both | — | Current Iris biome |
| `region` | Both | — | Current Iris region |
| `block` | Both | — | Target block |
| `hand` | Both | — | Held item |
| `markers` | Both | `<marker>` | Reveal nearby markers (e.g. `cave_floor`, `cave_ceiling`, `object`) |
---
## Edit: `/iris edit`
**Bukkit origin:** player. Opens pack JSON in the desktop editor.
| Command | Aliases | Params | Description |
|---------|---------|--------|-------------|
| `biome` | `b` | **Bukkit:** `<biome>`. **Modded:** `[key]` | Open biome JSON (modded: omit key for current) |
| `region` | `r` | **Bukkit:** `<region>`. **Modded:** `[key]` | Open region JSON |
| `dimension` | `d` | **Bukkit:** `<dimension>`. **Modded:** — | Open dimension JSON (modded: current pack) |
---
## Pregen: `/iris pregen` (`pregenerate`)
| Command | Aliases | Params | Description |
|---------|---------|--------|-------------|
| `start` | | **Bukkit:** `<radius> [world=<world>] [center=0,0] [gui=true] [serial=false]` (`radius` alias `size`, `center` alias `middle`, use `me` for player). **Modded:** `<radius> [dimension] [at <x> <z>] [gui] [sync] [nocache]` | Start pregen; radius in **blocks**; resumable checkpoint cache on by default on modded unless `nocache` |
| `stop` | `x` | — | Stop active pregen |
| `pause` | `resume` | — | Toggle pause/resume |
| `status` | — | — | Progress, CPS, ETA, method, failures |
**Bukkit:** `serial=true` requires a Paper-compatible server (strict serial chunk generation). **Modded:** `sync` is the serial-like flag; `gui` opens boss-bar/GUI path when available.
See `07 - Pregeneration.md`.
---
## Object: `/iris object` (`o`)
**Bukkit:** group origin player. Root `wand`/`dust` also exist on modded.
| Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------|
| `wand` | | Both | — | Give Iris object wand |
| `dust` | `d` | Both | — | Give reveal dust |
| `save` | | Both | **Bukkit:** `[dimension=<key>] <name> [overwrite=false] [legacy=true]`. **Modded:** `[overwrite] <name>` | Save wand selection as `.iob` |
| `paste` | | Both | **Bukkit:** `<object> [edit=false] [rotate=0] [scale=1]`. **Modded:** `[at x y z] [rotate degrees] <key>` | Paste object |
| `expand` | | **Modded** | `[amount]` (default `1`) | Expand selection along look |
| `contract` | `-` | Both | `[amount=1]` | Contract selection along look |
| `shift` | | Both | `[amount=1]` | Shift selection along look |
| `position1` | `p1` | Both | **Bukkit:** `[here=true]` (look vs feet). **Modded:** `[look]` | Set selection point 1 |
| `position2` | `p2` | Both | same | Set selection point 2 |
| `x+y` | `xpy` (modded) | Both | — | Autoselect up and out |
| `x&y` | `xay` (modded) | Both | — | Autoselect up, down, and out |
| `analyze` | | Both | `<object\|key>` | Composition stats |
| `shrink` | | Both | `<object\|key>` | Shrink object to minimum bounds |
| `plausibilize` | | Both | **Bukkit:** `<key\|prefix/> [dryrun=false] [reach=12]`. **Modded:** greedy args `key [dryrun=true] [reach=N]` | Grow branches so leaves survive vanilla decay |
| `undo` | `u` | Both | `[amount=1]` | Undo pastes |
| `we` | | **Bukkit**; modded stub | — | Wand + import WorldEdit selection |
| `studio` | | **Bukkit**; modded stub | `[dimension=null] [seed=1337]` | Object studio grid world |
| `convert` | | **Bukkit**; modded stub | — | Convert `convert/` folder `.schem``.iob` |
---
## Studio: `/iris studio` (`std`, `s`)
| Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------|
| `open` | `o` | Both | **Bukkit:** `<dimension> [seed=1337]`. **Modded:** `<pack> [seed]` | Open temporary studio dimension |
| `close` | `x` | Both | — | Close studio and discard world |
| `tpstudio` | `stp` | Both | — | Teleport into open studio |
| `status` | | **Modded** (Bukkit uses other paths) | — | Show open studio and pack |
| `create` | `+` | Both | **Bukkit:** `[name=studio] [template=<dimension>]`. **Modded:** `[name] [template=example]` | Create pack project |
| `package` | `pkg` (Bukkit method `pkg`, alias `package`) | Both | **Bukkit:** `[dimension=default] [obfuscate=false] [minify=true]`. **Modded:** `[pack]` | Zip/package pack |
| `version` | | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Pack version |
| `regions` | | Both | **Bukkit:** `[radius=500]` (player). **Modded:** `[radius]` default 500 | Nearby region distribution |
| `noise` | `nmap` | Both | **Bukkit:** `[generator=<key>] [seed=12345]`. **Modded:** `[generator] [seed]` | Noise explorer GUI |
| `map` | `render` | Both | **Bukkit:** `[world=<world>]`. **Modded:** — | Vision map GUI |
| `vscode` | `vsc` | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Generate/open code workspace |
| `update` | | Both | same as vscode pack | Regenerate workspace only |
| `importvanilla` | `importv`, `iv` | **Bukkit** functional; **modded message** | `<dimension> [variants=3] [structures=true]` | Import vanilla trees/objects/structures into pack |
| `scoreboard` | `board`, `sidebar`, `sb` | **Bukkit** | — | Toggle studio debug scoreboard |
| `loot` | | **Bukkit**; modded stub | `[fast=false] [add=true]` | Simulate chest loot GUI |
| `profile` | | **Bukkit**; modded stub | `[dimension=default]` | Pack performance profile |
| `spawn` | `summon` | **Bukkit**; modded stub | `<entity> [location=<x,y,z>]` | Spawn Iris entity |
| `objects` | `find-objects` | **Bukkit**; modded stub | — | IGenData chunk report for nearby chunks |
See `10 - Studio & VSCode Schemas.md`.
---
## Pack: `/iris pack` (`pk`)
| Command | Aliases | Params | Description |
|---------|---------|--------|-------------|
| `validate` | `v` | **Bukkit:** `[pack=<key>]`. **Modded:** `[pack]`; empty = all | Validate pack(s) and publish results |
| `cleanup` | `c` | **Bukkit:** `<pack> [mode=preview]`. **Modded:** `<pack> [apply]` | Preview/quarantine unused resources |
| `restore` | `r` | same pattern | Preview/restore latest quarantine |
| `status` | `s` | **Bukkit:** `[pack=<key>]`. **Modded:** `[pack]` | Cached validation status |
See `25 - Pack Management.md`.
---
## Structure: `/iris structure` (`struct`, `str`)
| Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------|
| `list` | `ls` | Both | **Bukkit:** `<dimension>`. **Modded:** current engine pack | Write `structure-index.json` |
| `info` | | Both | **Bukkit:** `<dimension> <structure>`. **Modded:** `<key>` | Resolve jigsaw graph bounds |
| `place` | `p` | Both | **Bukkit:** `<dimension> <structure>` (player). **Modded:** `<key>` | Assemble and place structure at player |
| `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded message | `<dimension>` | Import all vanilla/datapack structures as editable Iris resources (overwrites) |
| `capture` | `cap` | **Bukkit**; modded message | `<dimension>` | Capture code-only structures via scratch world |
| `verify` | `locateall` | Both | **Bukkit:** `<dimension> [radius=48]`. **Modded:** `[key]` | Native/Iris structure reachability report |
See `18 - Structures Overview.md`, `21 - Jigsaw Structures.md`, `22 - Native Structures & Datapacks.md`.
---
## Datapack: `/iris datapack` (`datapacks`, `dp`)
| Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------|
| `ingest` | `pull` | **Bukkit**; modded message | `[restart=false]` | Download/install Modrinth `datapackImports` into world datapacks |
| `list` | `ls` | Both | — | **Bukkit:** configured imports + installed. **Modded:** configured/installed world datapacks |
| `remove` | `rm` | **Bukkit**; modded message | `<id>` | Remove installed datapack by id |
| `status` | | **Modded** | — | Check Iris dimension-type overrides vs pack heights |
| `install` | | **Modded** | — | Install dimension-type override datapack for loaded Iris dimensions |
See `22 - Native Structures & Datapacks.md`.
---
## World (modded-only group): `/iris world` (`w`)
Bukkit uses root `create` / `load` / `unload` / `remove` / `evacuate` instead.
| Command | Aliases | Params | Description |
|---------|---------|--------|-------------|
| `enable` | `create` | `<dimension> <pack\|pack:dimensionKey> [seed\|random]` | Create/inject persistent Iris dimension (downloads pack if missing) |
| `replace-overworld` | | `<pack\|pack:dimensionKey> [seed\|random]` | Inject primary world routing |
| `mainworld` | | `<pack\|pack:dimensionKey\|off> [seed\|random]` | Configure main-world preset in `modded.json` |
| `disable` | | `<dimension>` | Evacuate and unload; keep disk data |
| `delete` | `remove`, `rm` | `<dimension>` | Disable and wipe chunk/mantle data |
| `list` | `ls` | — | List loaded Iris dimensions |
| `status` | | — | Loaded dimensions + primary world config |
---
## Developer: `/iris developer` (`dev`)
| Command | Aliases | Platforms | Params | Description |
|---------|---------|-----------|--------|-------------|
| `EngineStatus` | | **Bukkit** | — | Loaded tectonic plate count |
| `Sentry` | `sentry` (modded) | Both | — | Send test exception to error reporter |
| `genhash` | | **Bukkit** | `[world] [radius=4] [center-x=0] [center-z=0]` | Hash generated blocks in fixed area |
| `update-world` | `^world` | **Bukkit** | `[world=<world>] [pack=<dimension>] [confirm=false] [fresh-download=false]` | Unsafe pack swap into world |
| `mantle` | | **Bukkit** | `[plate=false] [name=…]` | Dump mantle section/plate under dump folder |
| `packBenchmark` | | **Bukkit** | `[pack=overworld] [radius=2048] [gui=false]` | Pack benchmark |
| `upgrade` | | **Bukkit** | `[version=latest]` | Data version upgrade helper |
| `mca` | | **Bukkit** | `<world folder>` | Scan MCA region files |
| `delete-chunk` | `dc` | **Bukkit** | `[radius=0]` | Delete nearby chunk blocks (regen testing) |
| `network` | `ip` | Both | — | List network interfaces |
| `regen` | `rg` | **Bukkit** (modded root) | `[radius=5]` | Delete and regenerate nearby chunks |
| `goldenhash` | `gold` | **Bukkit** (modded root) | `[world] [radius=8] [center-x=0] [center-z=0] [reset-mantle=true] [threads=8] [deep=false]` | Buffer golden hash capture/verify |
Modded developer group currently implements only `sentry` and `network`/`ip`.
---
## Platform gap summary
| Feature | Bukkit | Modded |
|---------|--------|--------|
| Root permission node | `iris.all` (code) | Gamemaster / all levels |
| World lifecycle | `create`, `load`, `unload`, `remove` | `world enable/disable/delete`, `create`, `mainworld` |
| Seed print | — | `/iris seed` |
| Object expand | — | `/iris object expand` |
| Object WE / studio / convert | yes | help stubs only |
| Studio loot/profile/spawn/objects/scoreboard/importvanilla | yes | stubs or messages |
| Structure import/capture | yes | messages (run on Bukkit, copy pack) |
| Datapack Modrinth ingest/remove | yes | messages |
| Datapack status/install (dimension types) | — | yes |
| `regen` / `goldenhash` | under `developer` | root |
| Pregen flags | `serial`, `gui`, center string | `sync`, `gui`, `nocache`, `at x z` |
| Tree feller permission | `iris.treefeller` | loader-specific node |
---
## Related
- `03 - Configuration.md`
- `02 - Getting Started.md`
- `06 - Worlds & Lifecycle.md`
- `07 - Pregeneration.md`
- `10 - Studio & VSCode Schemas.md`
- `25 - Pack Management.md`
- `28 - Integrations.md`
- `30 - Platform Differences.md`
- `32 - Determinism & Goldenhash.md`
+142
View File
@@ -0,0 +1,142 @@
# 05 - Concepts & Pack Layout
An Iris pack is a directory of JSON, binary objects, and optional assets under the platform packs root (`packs/<key>/` on Bukkit-family; `config/irisworldgen/packs/<key>/` on Fabric/Forge/NeoForge). `IrisData` is the pack loader: it registers one `ResourceLoader` per registrant type, resolves keys to files, caches loads, and expands snippet references during JSON parse. Production worlds use a copied pack snapshot under the world folder; studio worlds load the live pack directory with hotload.
See also: `00 - Overview.md`, `01 - Installation & Platforms.md`, `10 - Studio & VSCode Schemas.md`, `11 - Dimensions.md`, `24 - Pack Mods & Snippets.md`, `25 - Pack Management.md`.
## Content model
| Concept | Role |
|---------|------|
| Pack | Directory under the packs root; pack folder name is the pack key |
| Dimension | Root world type under `dimensions/`; at least one is required |
| Region | Spatial zone listing biomes and region content |
| Biome | Terrain layers, surface, decorations, objects, structures, spawns |
| Generator | Height / noise generator definitions |
| Object | Placed block models (`.iob`) |
| Structure | Iris multi-piece / native structure graphs under `structures/` |
| Jigsaw pool / piece | Pool and piece JSON for Iris jigsaw assembly |
| Entity / spawner / marker / loot | Entity definitions, spawn rules, markers, loot tables |
| Mod schema | Inactive injector/replacer documents under `mods/`; loaded for schema/tooling but not applied by the engine |
| Expression / block / image | Expressions, custom blocks, PNG sampling maps |
| Snippet | Reusable JSON fragments under `snippet/<type>/` |
| Studio | Transient authoring world bound to the live pack with file hotload |
| World pack snapshot | Frozen copy at `<world>/iris/pack` used by non-studio worlds |
## Pack roots
| Context | Path |
|---------|------|
| Authoring / download target | Platform data dir `packs/<packKey>/` |
| Production world (non-studio) | World dimension root `iris/pack/` (see `06 - Worlds & Lifecycle.md`) |
| Studio world | Same as authoring pack path; no world copy unless benchmark |
| Prefetch cache | Platform `prefetch/` (loader key indexes) |
| Schemas (studio) | Pack-local `.iris/schema/` |
Pack folder names listed by Iris must be visible directories (not hidden names starting with `.`). Symbolic-link pack trees are rejected for download/replace and validation safety checks.
## Registrant folders
Every pack type that `IrisData` registers maps to a folder name returned by the registrants `getFolderName()`. Nested subfolders under a type root are allowed; the load key is the path relative to that root without the file extension.
| Folder | Type | Files | Required |
|--------|------|-------|----------|
| `dimensions/` | Dimension | `*.json` | **Yes** — pack is not loadable without at least one |
| `regions/` | Region | `*.json` | Optional |
| `biomes/` | Biome | `*.json` | Optional |
| `generators/` | Generator | `*.json` | Optional |
| `objects/` | Object | `*.iob` | Optional |
| `matter/` | Matter object | matter binary (loader-specific) | Optional; loader only, no runtime consumer |
| `structures/` | Structure | `*.json` | Optional |
| `jigsaw-pools/` | Jigsaw pool | `*.json` | Optional |
| `jigsaw-pieces/` | Jigsaw piece | `*.json` | Optional |
| `entities/` | Entity | `*.json` | Optional |
| `spawners/` | Spawner | `*.json` | Optional |
| `markers/` | Marker | `*.json` | Optional |
| `loot/` | Loot table | `*.json` | Optional |
| `mods/` | Pack mod schema | `*.json` | Optional; no runtime application path |
| `blocks/` | Block data | `*.json` | Optional |
| `expressions/` | Expression | `*.json` | Optional |
| `images/` | Image | `*.png` | Optional |
| `snippet/` | Snippet library | `snippet/<type>/**.json` | Optional (not a registrant loader) |
Live overworld also contains authoring-only or empty trees that loaders do not register as types (for example empty `caves/`, `ravines/`, `jigsaw-structures/`, plus pack-local `README.md`, workspace files, and `Schema.json`). Those names are not keys in `IrisData`.
## Key rules
- **Load key** is the path under the type folder without extension. Example: `biomes/temperate/plains.json` → key `temperate/plains`.
- Exact `name + extension` wins over dotted variants (`plains.json` beats `plains.disabled.json` when both match the base name rule).
- Ambiguous same-base-name matches log a warning and pick the sorted first file.
- Literal key `"null"` is refused.
- Cross-references between resources use these load keys (region biome lists, structure placements, spawner entity ids, and similar).
- Pack dimension selectors for world create accept `pack`, `pack:dimensionKey`, or `default` (resolves to `settings.generator.defaultWorldType`, default `overworld`). See `04 - Commands & Permissions.md` and `06 - Worlds & Lifecycle.md`.
- Download destination pack keys must match `[a-z0-9_-]+` and are taken from the single dimension load key in the archive.
## Dimensions required
`PackValidator` fails a pack as not loadable when:
1. The pack folder is missing or not a directory.
2. `dimensions/` is missing.
3. `dimensions/` has no `*.json` files.
A downloaded archive is also rejected unless it contains exactly one loadable dimension (install key becomes that dimensions load key). Presence of a pack on disk is defined as a safe pack directory with at least one non-symlink `dimensions/*.json` file.
## Snippets
Types annotated with `@Snippet("<type>")` may be written inline as JSON objects **or** as a string reference to a snippet file.
| Rule | Behavior |
|------|----------|
| Path form | `"snippet/<type>/<name>"` (optional shorter forms are normalized onto `snippet/<type>/`) |
| On-disk file | `snippet/<type>/<name>.json` under the pack root |
| Nested snippet keys | Subfolders under `snippet/<type>/` are allowed; listed keys keep the `snippet/` prefix |
| Schema | Studio schema builder exposes `snippet/<type>/…` enums and pattern matches under `.iris/schema/snippet/` |
| Shipping example | Overworld uses `snippet/decorator/*` and `snippet/style/*` |
Snippet parse failures log errors and yield `null` for that field; full pack validation treats broken graphs as blocking or warning depending on the validator.
## Studio pack vs world pack snapshot
| Mode | Pack path used by engine | Hotload | Copy on create |
|------|--------------------------|---------|----------------|
| Studio (`studio=true`) | Live `packs/<key>/` (or studio project path) | Yes — polls pack for `.json`/`.iob` changes (~1s latch), excludes `.iris` | No pack install into world (unless benchmark) |
| Production create | Installs full pack tree into `<world>/iris/pack` | No | Atomic copy via `StudioSVC.installIntoWorld` |
| Benchmark | Installs into world pack path | Studio flag still drives transient cleanup rules | Yes when `benchmark` |
Hotload opens a new `IrisData` runtime from the same folder, reloads the dimension key, rebuilds engine runtime under a lifecycle lock, retires the previous data, and refreshes workspace/datapacks. Production engines load only the world snapshot; editing `packs/` does not affect existing non-studio worlds until the snapshot is replaced (see `25 - Pack Management.md` `update-world`).
## Minimal pack layout
```text
packs/myworld/
dimensions/
myworld.json
regions/
main.json
biomes/
plains.json
generators/
plain.json
```
A pack with only `dimensions/*.json` is structurally valid for presence and basic validation; generation quality depends on the dimensions referenced regions/biomes/generators. For a walkthrough see `26 - Example - Minimal Dimension.md`.
## Live overworld folder map (shipping pack)
Adapter run configurations use these additional paths (`…/packs/overworld/`):
| Path | Contents (summary) |
|------|--------------------|
| `dimensions/overworld.json` | Root dimension |
| `regions/*.json` | Climate / biome zones |
| `biomes/**` | Nested biome sets (temperate, hot, frozen, ocean, …) |
| `generators/**` | Terrain generators |
| `objects/**` | `.iob` trees, structures, clutter, vanilla imports |
| `structures/*.json` | Structure graphs (including minecraft_* graphs) |
| `jigsaw-pieces/**`, `jigsaw-pools/**` | Jigsaw assembly data |
| `entities/standard/**`, `spawners/**`, `loot/**` | Entities, spawners, loot |
| `images/*.png` | Noise / map images |
| `snippet/decorator/**`, `snippet/style/**` | Shared snippets |
Related feature docs: `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `18 - Structures Overview.md`, `19 - Objects.md`, `21 - Jigsaw Structures.md`, `23 - Loot, Entities, Spawners, Markers.md`.
+157
View File
@@ -0,0 +1,157 @@
# 06 - Worlds & Lifecycle
Iris manages world identity, storage paths, pack installation, create/load/unload/remove/evacuate, and main-world promotion through Bukkit-family lifecycle services. Managed Iris worlds live under the level root as `dimensions/iris/<key>/` with namespace `iris`. Non-studio worlds carry a frozen pack at `iris/pack`; studio worlds bind the live packs directory.
See also: `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`.
## Identity and storage
| Item | Rule |
|------|------|
| Managed namespace | `iris` only for Iris-managed create/load/remove targets |
| Logical name | For `iris:foo` the logical name is `foo` |
| Storage root | Level root (`Server#getLevelDirectory` on Paper; else `world-container/level-name`) |
| Dimension folder | `<levelRoot>/dimensions/iris/<key>/` |
| Pack snapshot | `<dimensionRoot>/iris/pack/` |
| Pregen cache dir | `<dimensionRoot>/iris/pregen/` |
| Registry | `worlds.json` in Iris data + `bukkit.yml` worlds section for production worlds |
| Name constraints | Safe single path segment `[a-z0-9_-]+`; no `/`, `\`, `..`; reserved create names `iris` and `benchmark` rejected |
Vanilla main/nether/end map to minecraft keys from `level-name` / `level-name_nether` / `level-name_the_end` and are not Iris-managed dimension folders.
### Modded persistent-dimension registry
Fabric, Forge, and NeoForge persist dynamic Iris worlds in `<world-root>/iris/iris-dimensions.json`:
```json
{
"dimensions": [
{ "id": "irisworldgen:myworld", "pack": "overworld", "dimension": "overworld", "seed": 1337 }
]
}
```
`id` is the registered dimension id, `pack` is the installed pack folder, `dimension` is its dimension load key, and `seed` is the generation seed. Writes use a temporary file plus atomic replacement when the filesystem supports it. Invalid individual entries are logged and preserved verbatim during ordinary updates; duplicate ids keep the first valid entry.
If the whole registry cannot be parsed during startup, Iris moves it to `iris-dimensions.json.broken-<timestamp>`, logs any ids it can recover from the raw text, and continues with no persistent Iris dimensions. Keep the quarantined file, repair or recreate each reported world with `/iris world create`, and verify pack/dimension/seed values before deleting the backup.
## Command surface (Bukkit)
| Command | Effect |
|---------|--------|
| `/iris create <name> [type=default] [seed=1337] [main=false]` | Create or Folia-stage a managed world |
| `/iris load <name>` / `/iris import <name>` | Load a disk Iris world via reconciler |
| `/iris unload <world>` | Evacuate → unload → close generator |
| `/iris remove <name> [delete=true]` | Unregister / delete managed world |
| `/iris evacuate <world>` | Move players out of the Iris world |
| `/iris tp <world> [player=<name>]` | Teleport to world spawn |
| `/iris worlds` | List Iris vs non-Iris loaded worlds |
Full permission table: `04 - Commands & Permissions.md`.
### Create parameters
| Param | Default | Notes |
|-------|---------|-------|
| `name` | required | Becomes `iris:<logical>`; folder must not already exist |
| `type` | `default` | Pack/dimension selector: `default``settings.generator.defaultWorldType` (`overworld`); else pack name or `pack:dimensionKey` |
| `seed` | `1337` | World seed |
| `main` | `false` | Schedule main-world promotion on JVM shutdown (Paper path) or promote during Folia staging |
Create refuses the primary Bukkit thread. Lifecycle domain `WORLD_MUTATION` / kind `WORLD_CREATE` must be free or create fails busy.
## Production create flow (non-Folia)
1. Resolve managed key and empty dimension root.
2. Resolve dimension via `IrisToolbelt.getDimension` (may download pack if missing).
3. Ensure datapacks for the dimension types are installed; queue restart if types not yet loaded.
4. Copy pack into `<world>/iris/pack` (`StudioSVC.installIntoWorld`) — atomic stage → publish; refuses primary thread.
5. Build `WorldCreator` with Iris generator (`studio=false`).
6. Create world through `WorldLifecycleService` / NMS async create (timeout 120s; timeout triggers server restart).
7. Register world in `bukkit.yml` with generator `Iris` dimension key and seed; Multiverse link update when present.
8. Optional creation-time pregen if a `PregenTask` was attached by the creator API.
## Folia staging
Runtime world creation is disabled on Folia. `/iris create` instead:
1. Acquires `WORLD_CREATE` lease.
2. Installs datapacks if changed.
3. Stages pack into the managed dimension root via `installIntoWorld`.
4. Registers the world in `bukkit.yml` (`BukkitWorldConfiguration.register`).
5. If `main=true`, promotes main-world files immediately under lease (failure rolls back bukkit.yml + deletes staged folder).
6. Instructs operator to restart; generation/load happens on next startup.
`WorldLifecycleStaging` holds staged generators/biome providers for the backend that consumes them at load.
## Studio create
Studio uses `IrisCreator.studio(true)`:
- Does **not** copy the pack into the world folder (except benchmark).
- Engine data folder is the live pack path; hotloader starts after engine setup.
- Studio worlds are transient: unloaded studio worlds are cleaned; `bukkit.yml` studio entries are removed on shutdown cleanup paths.
- Studio open/close uses `StudioSVC` transition queue (see `10 - Studio & VSCode Schemas.md`).
## Load
`/iris load` / `/iris import`:
1. Parses managed key; requires dimension root directory on disk.
2. `BukkitWorldReconciler.loadWorld(bukkit.yml, worldKey)`.
3. Reports success, busy, restart-required, or failure.
Load does not re-download packs; the world must already have `iris/pack` content and registration data consistent with Iris.
## Unload
`/iris unload` (player origin, sync):
1. Requires Iris world; acquires `WORLD_UNLOAD` lease.
2. Marks world maintenance.
3. `IrisToolbelt.evacuateAsync``WorldLifecycleService.unloadAsync(world, true)``generator.closeAsync()`.
4. Terminal timeout **150 seconds**: if unload has not settled, marks timeout, requests server restart (`ServerConfigurator.restart`), and fails the future.
## Evacuate
`/iris evacuate` moves all players out of the Iris world into another loaded world (or kicks if none). Used as a step inside unload and removal.
## Remove
`/iris remove <name> [delete=true]` delegates to `IrisWorldRemovalService`:
| Status | Meaning |
|--------|---------|
| `UNREGISTERED` | Unloaded/unregistered; files kept (`delete=false`) |
| `DELETED` | Files deleted |
| `DELETE_QUEUED` | Quarantined for delete at next startup |
| `BUSY` | Another world/pack mutation holds the coordinator |
| `INVALID_IDENTIFIER` / `PROTECTED_WORLD` / `NOT_IRIS_WORLD` / `UNSAFE_PATH` / `NOT_FOUND` | Refused |
| Other failure statuses | Partial registry change without delete; quarantine path may remain |
Only safe `iris` namespace dimension paths are mutable. Phase timeouts use 120s and can request restart on stuck phases.
## Main world promotion
When create sets `main=true` (non-Folia), a shutdown hook rewrites `server.properties` `level-name` / `level-seed` and publishes files:
1. Stage temp directory under world container.
2. Copy shared `data`, `datapacks`, `players` from current level root.
3. Copy Iris dimension tree into staged overworld dimension path.
4. Atomic move stage → new level root; write `server.properties`.
Promotion requires absent target level folder and refuses symlink world data. Folia with `main=true` performs the same publish during staging instead of deferring to shutdown.
## Pack snapshot vs studio (lifecycle view)
| Operation | Pack effect |
|-----------|-------------|
| Production create | Full pack tree installed under world `iris/pack` |
| Studio open | Engine reads live packs root; no world pack install |
| `/iris studio package` | Export only; does not change world |
| `/iris dev update-world` | Replaces world `iris/pack` (unsafe; restart if engine active) |
| Hotload | Studio only; production snapshot stays fixed |
## Concurrent lifecycle guards
`LifecycleOperationCoordinator` serializes domains including `WORLD_MUTATION` and `PACK_MUTATION`. Overlapping create/load/unload/remove/pack-publish returns busy to the operator. World create also refuses if the dimension root already exists or the world is already loaded.
+116
View File
@@ -0,0 +1,116 @@
# 07 - Pregeneration
Pregeneration walks a rectangular (by default square) block radius around a center and forces chunk generation so players do not trigger generation on first visit. Bukkit command `/iris pregen` (alias `pregenerate`) drives a single active `PregeneratorJob` backed by `IrisPregenerator` and a `PregeneratorMethod`. Settings under `settings.json``pregen` and `world.globalPregenCache` control timeouts, mantle residency, scheduler mode, and optional durable skip-cache.
See also: `03 - Configuration.md`, `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `06 - Worlds & Lifecycle.md`, `29 - Client HUD & Protocol.md`, `33 - Performance Tuning.md`.
## Commands
| Command | Behavior |
|---------|----------|
| `/iris pregen start <radius> [world=<world>] [center=0,0] [gui=true] [serial=false]` | Start job |
| `/iris pregen stop` / `x` | Request stop; finishes in-flight work then cancels |
| `/iris pregen pause` / `resume` | Toggle pause on the active job |
| `/iris pregen status` | Print progress snapshot (chunks, %, speed, ETA, method, failed) |
Only one pregen job instance is active. Starting a new job closes the previous instance.
### `start` parameters
| Param | Default | Notes |
|-------|---------|-------|
| `radius` | required | Blocks from center on X and Z (`radiusX` = `radiusZ`). Must be `> 0`. Reported span is `(radius * 2)` by `(radius * 2)` blocks |
| `world` | contextual | Target world (Iris preferred; non-Iris uses hybrid method without engine cache wrapper when no access) |
| `center` | `0,0` | Block X/Z center; `me` uses player location when supported by director parsing |
| `gui` | `true` | Open desktop pregen GUI when host supports it; headless servers log and continue |
| `serial` | `false` | One chunk at a time via strict serial hybrid method; **requires Paper** (`supportsStrictSerialPregeneration`) |
If the sender is a player without engine access, Iris warns that the world may not be fully loaded.
## Area model
`PregenTask` builds saturating block bounds `center ± radius`, converts to chunk and region ranges, and iterates regions in spiral order with per-region chunk order pulled toward the center.
| Limit | Value |
|-------|-------|
| Max region span per axis | `117189` regions (~±30M blocks Minecraft world limit) |
| Oversized request | `IllegalArgumentException` at construction (does not hang) |
## Generation methods
| Path | Method |
|------|--------|
| Iris world, parallel | `HybridPregenMethod(world, threadCount)` with concurrency from settings parallelism |
| Iris world, `serial=true` | `HybridPregenMethod.strictSerial(world)` |
| Non-Iris world | Hybrid without engine |
| Cached wrapper | `CachedPregenMethod` around method when caching enabled and runtime scheduler mode is **not** Folia |
Other method classes (`AsyncPregenMethod`, `MedievalPregenMethod`, `AsyncOrMedievalPregenMethod`) exist for specialized/API paths; the command path uses hybrid.
## Cache
| Setting | Location | Behavior |
|---------|----------|----------|
| Per-job skip cache | World `iris/pregen/` via `GlobalCacheSVC.createDefault` | Records generated chunks/regions so restarts can skip work when wrapper is active |
| `world.globalPregenCache` | `settings.json` | When true, maintains global per-world caches on world init/chunk load; when false, service stays idle after enable |
| Folia | Runtime scheduler resolved as Folia | Cached wrapper **disabled** for pregen |
Cache write happens on world unload and service disable. Empty cache is used when the service is disabled.
## Mantle and heap caps
Pregen applies mantle backpressure and heap high-water checks so tectonic plates do not exhaust memory.
| Control | Default / rule |
|---------|----------------|
| `pregen.maxResidentTectonicPlates` | Default `96`, minimum effective floor `16` |
| Effective plate cap | `min(baseCap, heightScaledCap, heapBudgetCap)` using world height vs 384 and ~60% of process heap / estimated plate size |
| Backpressure wait | `mantleBackpressureWaitMs` default `25` (clamped 51000) |
| Backpressure timeout | `mantleBackpressureTimeoutMs` default `60000` (clamped 5s600s) |
| Hard cap trigger | Loaded plates `> effectiveCap * 2` forces wait/evict |
| Heap high water | Pause generation while heap used ≥ **92%**; release at **82%** |
| Heap panic | ≥ **96%** requests panic reclaim / GC (throttled) |
| Save interval | `saveIntervalMs` default `30000` (clamped 5s900s) during pregen loop |
Raising `maxResidentTectonicPlates` increases memory headroom for speed; lowering reduces peak RAM. See `33 - Performance Tuning.md`.
## Other `pregen` settings
| Key | Default | Role |
|-----|---------|------|
| `runtimeSchedulerMode` | `AUTO` | Influences Folia vs paper-like scheduling for pregen cache and related paths |
| `paperLikeBackendMode` | `AUTO` | Paper-like lifecycle backend selection |
| `chunkLoadTimeoutSeconds` | `15` (5120) | Chunk load timeout during pregen |
| `timeoutWarnIntervalMs` | `500` (≥250) | Warning interval for stalled loads |
| `moddedPregenInFlight` | `0` → auto `max(16, min(48, cpu*2))` | In-flight cap for modded pregen adapters |
## Pause / stop / status
| Action | Behavior |
|--------|----------|
| Pause | `PregeneratorJob.pauseResume()` flips pause; generator loop spins while paused or heap high-water |
| Stop | `shutdownInstance()` closes pregenerator and interrupts worker asynchronously |
| Status | `progressSnapshot()`: percent, generated, total, chunks/s, ETA, elapsed, method name, paused flag, failed count, world name |
Failed chunks are counted separately and shown in status when non-zero.
## HUD / GUI / protocol
| Surface | Behavior |
|---------|----------|
| Desktop GUI | `PregenRenderer` when `gui=true` and GUI host available; colors mark existing, generating, network, generated, cleaned, mantle states |
| Boss bar / loader HUD | Create and some pregen attach paths use HUD slot claims for progress (creation pregen and studio progress reporters) |
| Client protocol | `IrisProtocolServer.broadcastPregenProgress` sends progress to connected Iris client sessions |
Client HUD details: `29 - Client HUD & Protocol.md`. GUI toggles: `settings.gui.useServerLaunchedGuis`, `maximumPregenGuiFPS`.
## Performance profile
Starting pregen applies `PregenPerformanceProfile` to the engine (or global) before the job runs. Studio `profile` command can also apply the pregen performance profile while measuring pack cost.
## Operator notes
- Radius is in **blocks**, not chunks or regions.
- Re-running pregen over the same area is faster when the chunk cache wrapper is active and cache files under `iris/pregen` remain.
- Unload/remove of a world with active pregen should stop the job for that world identity when lifecycle hooks call shutdown-for-world.
- Serial mode is for diagnosis/stability on Paper, not peak throughput.
+111
View File
@@ -0,0 +1,111 @@
# 08 - Localization
Iris localizes command, Studio, runtime, HUD, and UI strings through typed Java message catalogs and optional locale overlays. Server locale is selected by `general.language` in `settings.json`. Client keybind labels use Minecraft lang assets under `assets/irisworldgen/lang/`. See also `03 - Configuration.md`, `04 - Commands & Permissions.md`, and `29 - Client HUD & Protocol.md`.
## English and catalogs
Canonical English is code-owned in `core/.../localization` (`IrisMessages` and the surface catalogs it assembles). Iris does not ship an English server translation file. English locale id is `en_US` (`VolmitLocales.ENGLISH`).
Catalog surfaces:
| Catalog | Surface |
|---|---|
| `IrisMessages` | Shared command deny / reload / modded help keys |
| `BukkitCommandMessages`, `BukkitCommandMessagesExtended` | Bukkit `/iris` feedback |
| `DirectorCommandMessages` | Director parameter/help copy (Bukkit command tree) |
| `ModdedCommandMessages`, `ModdedHelpMessages` | Fabric/Forge/NeoForge command and help |
| `RuntimeUiMessages`, `RuntimeProgressMessages`, `BukkitRuntimeMessages` | Pregen, chunk jobs, runtime status |
| `PackDownloadMessages` | Pack download progress |
| `ClientUiMessages` | Client Vision, What overlay, pregen HUD, toasts, create-world gates |
| `BukkitUiMessages`, `DesktopUiMessages` | Bukkit/desktop UI strings |
Resolution entry points: `IrisLanguage.text(...)` (color codes allowed) and `IrisLanguage.plain(...)` (legacy section colors stripped). Argument-free `plain` results are memoized per locale snapshot for hot UI paths.
## Selecting a locale
| Setting | Default | Location |
|---|---|---|
| `general.language` | `en_US` | `plugins/Iris/settings.json` (plugin) or Iris data-folder `settings.json` (mod) |
Locale names must match `[A-Za-z0-9_-]+`. Invalid values are rejected and the previous active locale continues. `/iris reload` (and settings hotload) reloads settings and locale; success/failure messages report the requested and active locale ids.
## Bundled server locales
Complete non-English server bundles ship as jar resources under `/languages/<locale>.json`. Bundled locale ids:
| Locale id | Language |
|---|---|
| `de_DE` | German |
| `es_ES` | Spanish |
| `fi_FI` | Finnish |
| `fr_FR` | French |
| `he_IL` | Hebrew |
| `it_IT` | Italian |
| `ja-JP` | Japanese (hyphen in the server locale id) |
| `ko_KR` | Korean |
| `lt_LT` | Lithuanian |
| `nl_NL` | Dutch |
| `pl_PL` | Polish |
| `pt_PT` | Portuguese |
| `ru_RU` | Russian |
| `tr_TR` | Turkish |
| `vi_VI` | Vietnamese |
| `zh_CN` | Simplified Chinese |
| `zh_TW` | Traditional Chinese |
Bundled file size is capped at 2 MiB. A missing bundle for a locale listed in `VolmitLocales` is a hard load failure; an unknown locale with no bundle falls through to English catalog text (with fallback warnings counted at load).
## Override files
Path: `<Iris data folder>/languages/overrides/<locale>.json`.
Iris creates `languages/overrides/` on locale load. Overrides are optional partial files: omitted keys resolve from the bundled overlay (if any), then from code-owned English.
Shape:
```json
{
"locale": "de_DE",
"messages": {
"iris.command.unknown": "Unbekannter Iris-Befehl"
}
}
```
Rules:
| Rule | Behavior |
|---|---|
| Root keys | Only `locale` and `messages` are allowed |
| `locale` | If present, must equal the file's locale id after normalize |
| Values | String (text), string array (lines), or object of plural forms for plural keys |
| Nesting | Objects nest into dotted keys; keys must exist in the message catalog |
| Size | Max 2 MiB |
| Hotload | Override file mtime/size is watched; change triggers locale reload without a full restart when settings hotload runs |
Rejected reloads leave the previous locale active and log up to 12 validation errors.
## Resolution order
For non-`en_US` locales: operator override overlay → bundled `/languages/<locale>.json` → English catalog defaults. For `en_US`: override overlay only (no English server bundle).
Template placeholders use `{name}` tokens. Trusted arguments may contain color codes; untrusted arguments strip legacy section codes and rewrite `&`, `<`, `>`.
`&` color codes in templates are translated to section-sign codes before send (`0-9a-f`, `k-o`, `r`, `x`).
## Client language assets
Minecraft client assets live at `assets/irisworldgen/lang/<mc_code>.json` inside the mod jar. `en_us.json` is required and currently holds keybind category and key names only:
| Key | English |
|---|---|
| `key.categories.irisworldgen.iris` | Iris |
| `key.irisworldgen.toggle_pregen_hud` | Toggle Pregen HUD |
| `key.irisworldgen.open_vision_map` | Open Iris Vision Map |
| `key.irisworldgen.toggle_what_overlay` | Toggle Iris What Overlay |
Minecraft codes are derived from server locale ids by replacing `-` with `_` and lowercasing (`ja-JP``ja_jp`). Matching translated client assets ship for every non-English bundled locale. Server HUD/Vision/toast strings still resolve through `IrisLanguage` / `ClientUiMessages` on the process that renders them, not through these four Minecraft keys.
## Platforms
Localization runs on Bukkit-family and modded (Fabric/Forge/NeoForge). Client keybind lang assets apply only where the client mod is installed. PlaceholderAPI and Bukkit-only command catalogs do not affect mod command trees; modded uses the modded catalogs. See `30 - Platform Differences.md`.
+155
View File
@@ -0,0 +1,155 @@
# 09 - PlaceholderAPI
Iris registers the `iris` PlaceholderAPI expansion on Bukkit-family servers when PlaceholderAPI is enabled at Iris enable time. It publishes sixteen keys: seven world-family readings for the player and nine global pregeneration keys. This is an operator board contract, not a Java API; plugins that need the same data with more precision use `90 - API - Getting Started.md`, `91 - API - Terrain.md`, and `92 - API - World Events.md`. PlaceholderAPI is not available on Fabric/Forge/NeoForge. See also `07 - Pregeneration.md` and `28 - Integrations.md`.
## Registration
| Item | Value |
|---|---|
| Expansion id | `iris` |
| Expansion version | `2.0.0` |
| Author string | `Volmit Software` |
| Required plugin | `Iris` |
| Soft-depend | `PlaceholderAPI` in `plugin.yml` |
| `persist()` | `true` — survives `/papi reload` without Iris restart |
Iris schedules setup after enable. If PlaceholderAPI is not enabled then, the expansion is not registered and there is no late `PluginEnableEvent` re-attempt. Soft-depend alone does not load PlaceholderAPI.
List published paths with `/papi info iris`.
## Value grammar
| Rule | Detail |
|---|---|
| Path form | Dot-separated, lowercase, no underscores. Iris lowercases the path before resolve, so `%iris_WORLD.BIOME%` works, but write lowercase |
| Plain text only | No color codes, no unit suffixes, no `%` in values, `.` as decimal separator, no thousands grouping |
| Pack name scrubbing | Section-sign sequences and `%` characters inside pack-authored names are stripped before return |
| Real zero | `0` (or `0.00` for two-decimal numbers), never `---` |
Three answers:
| Answer | When | Board shows |
|---|---|---|
| A value | Known key with data | The value |
| `---` | Known key with no data right now | `---` |
| Nothing (null to PAPI) | Unknown path | Literal `%iris_...%` |
Unknown paths stay visible on purpose. There is no catch-all blank fallback.
## Full key table
### World family
| Placeholder | Value |
|---|---|
| `%iris_available%` | `true` when the Iris terrain service is live, `false` otherwise |
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
| `%iris_world.biome%` | Surface biome display name at the player column (example: `Hot Desert Dunes`) |
| `%iris_world.biome-key%` | Surface biome load key (example: `desert/hot-dunes`) |
| `%iris_world.region%` | Region display name at the player column |
| `%iris_world.region-key%` | Region load key |
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
`%iris_available%` does not need a player. Every other `world.*` key needs a tracked online player. Console, offline player, or untracked position: `world.available` is `false` and the rest are `---`.
### Pregeneration family
| Placeholder | Value |
|---|---|
| `%iris_pregen.available%` | `true` while a pregeneration job is running |
| `%iris_pregen.world%` | World name the running job is pregenerating |
| `%iris_pregen.percent%` | Completion `0.00``100.00`, no `%` character |
| `%iris_pregen.eta%` | Estimated seconds remaining, whole number |
| `%iris_pregen.eta-text%` | Same estimate as `45s`, `2m 5s`, or `1h 30m` |
| `%iris_pregen.chunks%` | Chunks generated so far |
| `%iris_pregen.total%` | Chunks in the job |
| `%iris_pregen.chunks-per-second%` | Current rate, two decimal places |
| `%iris_pregen.paused%` | `true` while the job is paused |
`pregen.*` is global (one job per server). Values match for every player and the console. Snapshot is published on pregen events (`STARTED`, `TICK`, `PAUSED`, `RESUMED`, `SAVING`) and cleared on `COMPLETED` or `CANCELLED`. After clear, `pregen.available` is `false` and other `pregen.*` keys are `---`. Before enough chunks exist for an ETA, `eta`/`eta-text` read `0` / `0s`.
### Paths as reported by `/papi info iris`
```
available
pregen.available
pregen.chunks
pregen.chunks-per-second
pregen.eta
pregen.eta-text
pregen.paused
pregen.percent
pregen.total
pregen.world
world.available
world.biome
world.biome-key
world.dimension
world.region
world.region-key
```
Prefix each with `%iris_` and suffix with `%`.
## Surface readings and cache
`world.biome`, `world.biome-key`, `world.region`, and `world.region-key` are **surface** column readings: the biome/region the generator places at ground level for that X/Z. A player in a cave under an overhang still reads the surface biome above, not the cave biome.
### Position tracking
| Event | Publish |
|---|---|
| Walking (`PlayerMoveEvent`) | At most once per second per player; skipped while the player stays in the same block column |
| Join, respawn, world change, portal, any teleport (including `/iris goto`, `/tp`, ender pearl, random TP) | Immediate |
Standing still never keeps a stale column from a previous place after an immediate publish. Quit releases the player's position and world view.
### View rebuild TTL
World views rebuild at most **once per second per player** (`VIEW_TTL_MS = 1000`), and only when something reads a `world.*` key that needs the view. Consequences:
- A board full of `world.*` keys costs one rebuild per player per second
- Values can lag a sprinting player by up to one second
- An unread board costs no terrain queries
### Pregen snapshot
Pregen values come from a single global snapshot updated by `IrisPregenerationEvent`, not per-player polling.
## Permissions
Iris never gates a placeholder on a permission. Values that should not be public (for example world seed) are not published.
## Failure policy
| Situation | Shown |
|---|---|
| Unknown path | Nothing (literal `%iris_...%`) |
| Known path, no data | `---` |
| No player context on `world.*` | `---` and `world.available` = `false` |
| Player not in an Iris world | `---` and `world.available` = `false` |
| Terrain service not registered | `---` / `world.available` = `false` / `%iris_available%` = `false` |
| No pregen job | `---` / `pregen.available` = `false` |
| Resolver throws | `---`; one warning per distinct path, max 64 distinct paths |
Failed keys are not quarantined; they keep answering `---`.
## Migration from pre-2.0 keys
Pre-2.0 underscore keys are gone. No alias and no dual-accept window. Old keys render literally.
| Old key | New key | Notes |
|---|---|---|
| `%iris_biome_name%` | `%iris_world.biome%` | Dot grammar |
| `%iris_biome_id%` | `%iris_world.biome-key%` | `id` was always the load key |
| `%iris_region_name%` | `%iris_world.region%` | Dot grammar |
| `%iris_region_id%` | `%iris_world.region-key%` | `id` was always the load key |
| `%iris_biome_file%` | removed | Exposed absolute server paths; threw without a backing file |
| `%iris_region_file%` | removed | Same as `biome_file` |
| `%iris_world_seed%` | removed | No permission context on scoreboards; use terrain API `IrisWorldInfo.seed()` when a plugin needs seed |
| `%iris_terrain_height%` | removed | Generated height before objects/edits; disagreed with the block underfoot |
| `%iris_terrain_slope%` | removed | Expensive pack-authoring diagnostic |
| `%iris_world_mode%` | removed | Studio vs production is not a live-board concern |
| `%iris_world_speed%` | removed | Mutated engine rate-window state on read; use `%iris_pregen.chunks-per-second%` for pregen rate |
Behavior change inside the renames: old keys sampled two blocks above the player's feet (cave/overhang Y). New keys are always surface for the column. `%iris_world.dimension%` is new and has no pre-2.0 equivalent.
+179
View File
@@ -0,0 +1,179 @@
# 10 - Studio & VSCode Schemas
Studio is Iriss live pack-authoring workflow: open a pack as a transient world, edit JSON under `packs/<key>/`, and hotload changes without a full server restart. VSCode (or IntelliJ) gets JSON Schema bindings generated from the Java models so field names, enums, and pack resource keys autocomplete against the real loaders.
Related: see `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `02 - Getting Started.md`, `25 - Pack Management.md`, `30 - Platform Differences.md`.
## What Studio Is
| Concept | Behavior |
|---------|----------|
| Pack workspace | Packs live under the platform data directory folder named `packs` (`StudioSVC.WORKSPACE_NAME`). |
| Studio world | Opened from a pack dimension key; uses a studio chunk generator with live file watching. |
| Hotload | On studio worlds only: a low-priority looper polls pack files; when content changes, `EngineHotloader` reloads the pack data and rebuilds engine runtime under a lifecycle lock. |
| Hotload contract | `IrisDimensionRuntimeContract` refuses hotload if dimension type key, min height, total height, or logical height change. Restart the world after those edits. |
| Non-studio worlds | No pack file watcher looper; production worlds keep the pack snapshot installed at create/update time. |
Studio settings in `settings.json``studio` (`IrisSettings.IrisSettingsStudio`):
| Key | Default | Meaning |
|-----|---------|---------|
| `openVSCode` | `true` | When true and the JVM is not headless, `open` / `vscode` may launch the desktop opener on the packs `*.code-workspace` file. |
| `disableTimeAndWeather` | `true` | Studio world time/weather lock preference. |
| `entitySpawning` | `true` | Whether studio entity spawning is allowed. |
| `autoStartDefaultStudio` | `false` | Auto-open default studio on boot when enabled. |
## Commands (Bukkit)
Root: `/iris studio` (aliases `std`, `s`). Implemented by `CommandStudio` + `StudioSVC`.
| Subcommand | Aliases | What it does |
|------------|---------|--------------|
| `open <dimension> [seed=1337]` | `o` | Close any open studio, open pack as studio world. Blocks if pack validation has blocking errors. |
| `close` | `x` | Close the active studio project/world. |
| `create [name=studio] [template=<dimension>]` | `+` | Create a new pack under `packs/<name>`. Optional template is another pack dimension key; without template, writes the starter skeleton (see below). |
| `vscode [dimension=default]` | `vsc` | Open the packs VSCode workspace (generates it if missing). |
| `update [dimension=default]` | | Rewrite `<pack>/<name>.code-workspace` and regenerate `.iris/schema/*` mappings. |
| `version [dimension=default]` | | Print dimension `version` field. |
| `package [dimension=default] [obfuscate=false] [minify=true]` | `pkg` | Compile pack into a distributable archive. |
| `importvanilla <dimension> [variants=3] [structures=true]` | `importv`, `iv` | Capture vanilla features/structures into the pack (Bukkit NMS). |
| `scoreboard` | `board`, `sidebar`, `sb` | Toggle studio debug scoreboard (player, must be in studio world). |
| `noise [generator=<key>] [seed=12345]` | `nmap` | External noise explorer GUI. |
| `map [world=<world>]` | `render` | External biome/terrain map GUI for an Iris world. |
| `regions [radius=500]` | | Sample region rarity over a chunk spiral (player in Iris world). |
| `loot [fast=false] [add=true]` | | Open a virtual chest with loot tables for the block under the player (studio). |
| `profile [dimension=default]` | | Write a pack performance profile report. |
| `spawn` / `summon` | | Spawn a pack entity definition at the player. |
| `stp` | | Teleport to the active studio world spawn in creative. |
| `objects` / `find-objects` | | Capture nearby chunk object placement report. |
Permissions and the full `/iris` tree: see `04 - Commands & Permissions.md`.
## Commands (Modded)
`/iris studio` on Fabric/Forge/NeoForge is implemented by `ModdedStudioCommands`. Supported: `create`/`+`, `open`/`o`, `close`/`x`, `tpstudio`/`stp`, `status`, `vscode`/`vsc`, `update`, `version`, `package`/`pkg`, `regions`, `noise`/`nmap`, `map`/`render`.
Bukkit-only (modded replies with a fixed message): `importvanilla`, `loot`, `profile`, `spawn`/`summon`, `objects`/`find-objects`.
## Creating a Pack (Starter Skeleton)
`/iris studio create name=mypack` (no template) writes:
```
packs/mypack/
dimensions/mypack.json
regions/starter.json
biomes/starter.json
generators/flat.json
mypack.code-workspace
```
Starter dimension JSON (from `StudioSVC.createStarterProject`):
```json
{
"name": "mypack",
"version": 1,
"regions": ["starter"],
"logicalHeight": 384,
"dimensionHeight": {"min": -64, "max": 320}
}
```
Starter region lists the same biome for land/sea/shore. Starter biome uses generator `flat`, layers with `minecraft:grass_block`, and derivatives `minecraft:plains`. Project names must normalize to safe pack folder names; reserved name `studio` is auto-renamed to a free suffix.
With a template: `/iris studio create name=mypack template=overworld` copies that pack tree (after optional download if missing).
## Studio Open Workflow
1. Resolve pack folder `packs/<dimensionKey>/` with a loadable `dimensions/<key>.json`.
2. Pack validation must not report blocking errors (`PackValidationRegistry`).
3. Close existing studio if open.
4. `IrisProject.open` creates a studio world bound to that pack folder (not a permanent production install copy for authoring).
5. Optional VSCode launch when `studio.openVSCode` is true.
6. Datapack install may require restart after create; message tells you to re-run `open` after restart when needed.
## Hotload Details
- Watcher runs only when `PlatformChunkGenerator.isStudio()` is true (`BukkitChunkGenerator` looper).
- On change: load a new `IrisData` from the same folder, reload the dimension key, validate hotload contract, build new engine runtime, retire previous data, refresh workspace/schemas, reload datapacks when a platform world is bound, broadcast client studio-hotload toast on failure/success.
- Complex-only rebuild (`hotloadComplex`) rebuilds `IrisComplex` without full pack reopen.
- Failed hotload rolls runtime back when possible and reports the error.
Do not change `dimensionHeight`, `logicalHeight`, or the dimension load/type key mid-session if you need live reload; restart the studio world after those edits.
## VSCode / JSON Schemas
`IrisCodeWorkspace` writes `<pack>/<packName>.code-workspace` with:
| Workspace setting | Value / purpose |
|-------------------|-----------------|
| `folders` | `[{ "path": "." }]` — pack root |
| `workbench.colorTheme` | `Monokai` |
| `files.autoSave` | `onFocusChange` |
| `[json]` editor options | bracket indent, smart enter, trim whitespace, string quick suggestions |
| `json.maxItemsComputed` | `30000` |
| `json.schemas` | Array of `{ fileMatch, url }` entries |
### Schema generation
`SchemaBuilder` reflects a registrant or snippet class and emits JSON Schema draft-07:
- `$schema`: `http://json-schema.org/draft-07/schema#`
- `$id`: `https://volmit.com/iris-schema/<classname>.json`
- Field docs from `@Desc`, ranges from `@MinNumber`/`@MaxNumber`, arrays from `@ArrayType`, required from `@Required`
- Enumerations for platform registries (blocks, biomes, entities, structures, …) and pack resource lists from `@RegistryListResource` / related annotations
- Snippet types (classes annotated `@Snippet`) get schemas under `.iris/schema/snippet/<snippet>-schema.json`
`ResourceLoader.buildSchema()` for each loader that `supportsSchemas()`:
| Pack folder pattern | Schema URL (relative to pack) |
|---------------------|--------------------------------|
| `/<folder>/**/*.json` (up to 7 depth levels) | `./.iris/schema/<folder>-schema.json` |
Example folders with schemas (from loaders / workspace sample): `dimensions`, `regions`, `biomes`, `generators`, `loot`, `entities`, `spawners`, `structures`, `jigsaw-pieces`, `jigsaw-pools`, `expressions`, `blocks`, and others registered on `IrisData`. Object/image/matter loaders may disable schemas.
Snippet paths: `/snippet/<type>/**/*.json``./.iris/schema/snippet/<type>-schema.json`.
IntelliJ: workspace update also merges mappings into `.idea/jsonSchemas.xml` when that project file exists.
### Commands that refresh schemas
| Command | Effect |
|---------|--------|
| `/iris studio update dimension=<dim>` | Rewrite workspace + queue schema writes |
| Studio open / create | Builds workspace config including schemas |
| Hotload workspace refresh | Platform hook may refresh workspace after successful hotload |
Schema files under `.iris/schema/` are generated artifacts for editors; pack content is the JSON under type folders, not the schema files.
## How To: Edit a Pack in Studio
1. Ensure the pack is under the Iris data `packs/` directory (shipping overworld is typically downloaded as pack key `overworld`).
2. Run `/iris studio open overworld` (or your pack key). Enter the studio world.
3. Run `/iris studio vscode dimension=overworld` (or open the pack folders `*.code-workspace` in VSCode/Cursor with JSON schema support).
4. Edit `dimensions/`, `regions/`, `biomes/`, etc. Save. Studio hotloads when the file watcher detects the change.
5. Use `/iris studio map` or the debug scoreboard to inspect regions/biomes. Use `focus` / `focusRegion` on the dimension JSON for isolation while testing (see `11 - Dimensions.md`).
6. `/iris studio close` when finished. Promote pack changes into production worlds with pack install / world update flows (`06 - Worlds & Lifecycle.md`, `25 - Pack Management.md`).
## Studio Dimension Modes (author testing)
Dimension field `studioMode` (`StudioMode` enum) can force special studio generators:
| Value | Effect |
|-------|--------|
| `NORMAL` | Default generation |
| `BIOME_BUFFET_1x1``BIOME_BUFFET_36x36` | Biome buffet grid of given cell size |
| `REGION_BUFFET` | Region buffet |
| `OBJECT_BUFFET` | Object studio generator |
These are dimension JSON fields for studio testing, not production world modes (production engine mode is `mode.type`; see `11 - Dimensions.md`).
## Platform Notes
| Platform | Studio |
|----------|--------|
| Paper/Purpur/Folia (Bukkit plugin) | Full `CommandStudio` + file-watch hotload on studio worlds |
| Fabric / Forge / NeoForge | Studio open/create/workspace/package; subset of tooling; no Bukkit-only importers/GUIs that need Bukkit inventory |
Pack JSON contracts are shared across platforms. Schemas are built from the same core models.
+258
View File
@@ -0,0 +1,258 @@
# 11 - Dimensions
A dimension is the root pack object for a world type. File location is `dimensions/<loadKey>.json` (folder name from `IrisDimension.getFolderName()`). Regions, height, fluid, noise zooms, ores, caves, structures, and engine mode all hang off this object.
Related: see `05 - Concepts & Pack Layout.md`, `10 - Studio & VSCode Schemas.md`, `12 - Regions.md`, `14 - Generators & Noise.md`, `15 - Caves & Carving.md`, `22 - Native Structures & Datapacks.md`, `26 - Example - Minimal Dimension.md`.
## Role in the Pack Graph
```
Dimension → regions[] → Region → land/sea/shore/cave biomes[] → Biome
→ generators (via biomes)
→ objects / structures / ores / deposits / caves
```
World create/open selects a pack dimension load key. Studio and production both load `IrisDimension` from that file.
## Load Key and File Name
| Rule | Detail |
|------|--------|
| Load key | Path relative to `dimensions/` without `.json` |
| Typical | `dimensions/overworld.json` → key `overworld` |
| Nested | `dimensions/foo/bar.json` → key `foo/bar` if used (shipping overworld uses a single top-level file matching the pack name) |
| Starter create | Writes `dimensions/<projectName>.json` with matching `name` |
## Engine Modes (`mode`)
Object type: `IrisDimensionMode` (`@Snippet("dimension-mode")`).
| JSON path | Type | Default | Meaning |
|-----------|------|---------|---------|
| `mode.type` | `IrisDimensionModeType` | `OVERWORLD` | Selects the engine mode factory |
Enum `IrisDimensionModeType`:
| Type | Annotation summary | Runtime stages (code) |
|------|--------------------|------------------------|
| `OVERWORLD` | Full biome world with fluid height | Biomes, mantle matter, terrain, carving, post, floating child biomes, deposits, matter insert, decoration, perfection, custom modifiers (`ModeOverworld`) |
| `SUPERFLAT` | Ultra fast; terrain & biomes only | Terrain + biome actuators only (`ModeSuperFlat`); no decoration/mantle/carve/deposit stages registered |
| `ENCLOSURE` | Ceiling & floor carved style (nether-like intent) | Same stage registration as SuperFlat today (`ModeEnclosure`): terrain + biome only |
| `ISLANDS` | Floating islands intent | Same stage registration as SuperFlat today (`ModeIslands`): terrain + biome only |
JSON shape:
```json
{
"mode": {
"type": "OVERWORLD"
}
}
```
The field is annotated `@Required`. Gson still applies the default `IrisDimensionMode` when omitted. The shipping overworld pack does not set `mode` and therefore runs as `OVERWORLD`.
If mode construction fails, the engine logs a warning and falls back to `OVERWORLD`.
## Height, Fluid, and Environment
| Field | Type | Default | Constraints / notes |
|-------|------|---------|---------------------|
| `name` | string | `"A Dimension"` | Required human-readable name (`@MinNumber(2)` on length via annotation min) |
| `version` | int | `1` | Bump to discourage accidental pack upgrades on existing worlds |
| `logicalHeight` | int | `256` | 12032; player teleport height budget; part of hotload contract |
| `dimensionHeight` | `IrisRange` | min `-64`, max `320` | World min/max Y. Iris generates internal height `max - min`, then shifts by min on output |
| `fluidHeight` | int | `63` | Required; 01024. Fluid column top in **internal** Y (0 = bottom of dimension height). World Y ≈ `fluidHeight + dimensionHeight.min` |
| `environment` | `IrisEnvironment` | `NORMAL` | `NORMAL`, `NETHER`, `THE_END`, `CUSTOM` — selects base datapack dimension template (overworld/nether/end) |
| `fullbright` | boolean | `false` | Forces maximum ambient lighting when true |
| `bedrock` | boolean | `true` | Places bedrock at internal Y 0 when true |
| `caveLavaHeight` | int | `8` | Subterrain fluid layer height (0318) |
### Environment enum
| Value | Base dimension type mapping |
|-------|-----------------------------|
| `NORMAL` | Overworld-style |
| `NETHER` | Nether-style |
| `THE_END` | End-style |
| `CUSTOM` | Treated as overworld base in `getBaseDimension()` default branch |
### Height contract (hotload / world bind)
`IrisDimensionRuntimeContract` locks: namespaced type key, min height, total height (`max - min`), and `logicalHeight`. Changing these on a running world requires restart, not hotload.
## Regions and Spatial Zoom
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `regions` | string[] | empty | **Required.** Region load keys under `regions/` |
| `regionStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Region placement noise |
| `continentalStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Land/sea placement |
| `landBiomeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Land biome placement |
| `shoreBiomeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Shore biome placement |
| `seaBiomeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Sea biome placement |
| `caveBiomeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Cave biome placement |
| `landChance` | double | `0.625` | 01 land vs sea chance |
| `regionZoom` | double | `1` | Region size multiplier |
| `landZoom` | double | `1` | Land space zoom |
| `seaZoom` | double | `1` | Ocean biome zoom |
| `continentZoom` | double | `1` | Continent zoom |
| `biomeZoom` | double | `1` | Global biome size (higher = bigger) |
| `coordFractureDistance` | double | `20` | Coordinate warping distance (blocks) |
| `coordFractureZoom` | double | `8` | Coordinate warping frequency |
| `dimensionAngleDeg` | double | `0` | Rotate entire dimension input coordinates (degrees) |
| `focus` | string | `""` | Force a single biome load key (testing) |
| `focusRegion` | string | `""` | Force a single region load key (testing) |
## Materials, Ores, Deposits
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `rockPalette` | `IrisMaterialPalette` | stone | Subsurface “stone” fill palette |
| `fluidPalette` | `IrisMaterialPalette` | water | Fluid block palette |
| `rockZoom` | double | `5` | Rock palette noise zoom |
| `ores` | `IrisOreGenerator[]` | empty | Dimension-wide ore generators (surface vs underground via generator flags) |
| `deposits` | `IrisDepositGenerator[]` | empty | Global deposit blobs |
| `depositVariants` | `IrisDepositVariant[]` | empty | Source→replacement ore remaps; applied after biome/region rules |
| `overlayNoise` | `IrisShapedGeneratorStyle[]` | empty | Extra height overlay noise |
| `hideOresForHiddenOre` | boolean | `false` | Replace ore placements with base rock for drop-control plugins |
## Caves, Carving, Mantle, Decoration
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `carvingEnabled` | boolean | `true` | Master carving toggle |
| `carving` | `IrisDimensionCarvingEntry[]` | empty | Absolute-Y cave biome carving overrides |
| `caveProfile` | `IrisCaveProfile` | default object | Profile-driven 3D caves (see `15 - Caves & Carving.md`) |
| `requireObjectSurfaceSupport` | boolean | `true` | Refuse surface objects over carved openings |
| `objectSurfaceSupportBuffer` | int | `2` | Min surface-support buffer (016) |
| `useMantle` | boolean | `true` | Objects, entities, features, updates |
| `decorate` | boolean | `true` | Decorators |
| `postProcessing` | boolean | `true` | Post-process pass |
| `postProcessingSlabs` | boolean | `true` | Slab painting |
| `postProcessingWalls` | boolean | `true` | Wall painting |
| `preventLeafDecay` | boolean | `true`/`false` default `false` | Creative-like leaf persistence |
| `treeSettings` | `IrisTreeSettings` | default | Tree growth overrides |
| `disabledComponents` | mantle flag strings | empty | Disable mantle components by flag |
## Upper Dimension (canopy)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `upperDimension` | string | `"none"` | Dimension load key for inverted ceiling terrain; `"none"` or empty disables |
| `upperDimensionGap` | int | `32` | Min air gap between lower and upper surfaces (0256) |
| `upperDimensionCarving` | boolean | `false` | Allow carving through upper terrain |
| `upperDimensionObjects` | boolean | `false` | Allow mantle objects in upper zone |
| `upperObjectsForcePlace` | boolean | `false` | Force-place upper objects ignoring normal restrictions |
## Structures, Features, Datapacks
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `structures` | `IrisStructurePlacement[]` | empty | Dimension-level Iris structure placements |
| `importedStructures` | `IrisImportedStructureControl` | default | Vanilla/mod/datapack structure allow/deny and adjustments |
| `importedFeatures` | `IrisImportedFeatureControl` | default off | Optional vanilla feature decoration pass |
| `datapackImports` | string[] | empty | External datapack URLs requested by this pack |
Structure placement and native control details: see `18 - Structures Overview.md`, `22 - Native Structures & Datapacks.md`.
## Loot, Spawns, Drops, Studio Debug
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `loot` | `IrisLootReference` | empty | Dimension loot tables |
| `entitySpawners` | string[] | empty | `IrisSpawner` load keys |
| `blockDrops` | `IrisBlockDrops[]` | empty | Custom block drops |
| `studioMode` | `StudioMode` | `NORMAL` | Studio-only generator overrides |
| `debugChunkCrossSections` | boolean | `false` | Cut chunks for cross-section viewing |
| `debugCrossSectionsMod` | int | `3` | X/Z modulus for cross-section cuts |
| `explodeBiomePalettes` | boolean | `false` | Vertical palette visualization |
| `explodeBiomePaletteSize` | int | `3` | Palette explosion spacing |
| `debugSmartBore` | boolean | `false` | Fill object voids with cobweb |
| `forceConvertTo320Height` | boolean | `false` | Height conversion flag |
| `disableExplorerMaps` | boolean | `false` | Disable cartographer explorer maps |
| `dimensionOptions` | `IrisDimensionTypeOptions` | defaults | Minecraft dimension-type datapack options (ultrawarm, natural, skylight, coordinate scale, …) |
### `dimensionOptions` fields (`IrisDimensionTypeOptions`)
Tri-state fields use `DEFAULT` | `TRUE` | `FALSE` (follow base dimension when `DEFAULT`):
| Field | Default | Role |
|-------|---------|------|
| `ultrawarm` | `DEFAULT` | Nether-like water/lava behavior |
| `natural` | `DEFAULT` | Beds/compasses/portal piglins |
| `piglinSafe` | `DEFAULT` | Piglin zombification |
| `respawnAnchorWorks` | `DEFAULT` | Respawn anchor |
| `bedWorks` | `DEFAULT` | Beds |
| `raids` | `DEFAULT` | Bad Omen raids |
| `skylight` | `DEFAULT` | Has skylight |
| `ceiling` | `DEFAULT` | Logical bedrock ceiling |
| `coordinateScale` | `-1` (unset) | Portal scale |
| `ambientLight` | `-1` (unset) | 01 ambient |
| `fixedTime` | `-1` sentinel | Fixed day time when set |
| `cloudHeight` | `-1` sentinel | Cloud Y or null to disable |
| `monsterSpawnBlockLightLimit` | `-1` (unset) | 015 |
## Overworld Pack Sample
Path: `adapters/fabric/run/config/irisworldgen/packs/overworld/dimensions/overworld.json` (also present under forge/neoforge run configs).
Selected values from that file:
| Field | Overworld value |
|-------|-----------------|
| `name` | `"Overworld"` |
| `version` | `4000` |
| `environment` | `"NORMAL"` |
| `fluidHeight` | `50` |
| `logicalHeight` | `512` |
| `dimensionHeight` | `{ "min": -256, "max": 512 }` |
| `landChance` | `0.69` |
| `regionZoom` | `16.15` |
| `dimensionAngleDeg` | `69` |
| `regions` | `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics` |
| `mode` | omitted → `OVERWORLD` |
| `ores` | bedrock + deepslate band generators |
| `deposits` | granite/andesite/diorite/gravel + ore deposits |
| `importedStructures` | adjustments for stronghold, trial chambers, mineshaft, villages |
| `structures` | e.g. ancient city native placement with `REPLACE_SOURCE` |
| `caveProfile` | enabled 3D cave profile |
## Minimal Dimension JSON
Matches studio starter plus an explicit mode (recommended):
```json
{
"name": "mypack",
"version": 1,
"mode": { "type": "OVERWORLD" },
"regions": ["starter"],
"fluidHeight": 63,
"logicalHeight": 384,
"dimensionHeight": { "min": -64, "max": 320 },
"environment": "NORMAL"
}
```
## How To: Make a Dimension
1. Create a pack (`/iris studio create name=mypack`) or copy a template.
2. Edit `dimensions/<key>.json`: set `name`, `regions`, `dimensionHeight`, `fluidHeight`, `mode.type`.
3. Ensure every region key exists under `regions/` (see `12 - Regions.md`).
4. Set land/sea styles and zooms only after basic terrain generates.
5. Add `ores` / `deposits` / `caveProfile` / structures after biomes render correctly.
6. Open studio: `/iris studio open mypack`. Iterate with hotload.
7. For isolation: set `"focusRegion": "starter"` or `"focus": "starter"` while authoring one biome.
8. When ready for a permanent world, create a world from the pack key (`06 - Worlds & Lifecycle.md`). Do not change height/logicalHeight without recreating the world dimension type.
## Common Author Mistakes
| Mistake | Result |
|---------|--------|
| Empty `regions` | Dimension cannot place content |
| Region keys that do not resolve | Missing regions at runtime |
| Changing `dimensionHeight` / `logicalHeight` mid-studio | Hotload rejected; restart studio |
| Expecting SuperFlat/Islands/Enclosure decoration | Those modes currently register only terrain+biome stages |
| Treating `fluidHeight` as world Y | It is internal Y; world Y = fluidHeight + min height |
| Forgetting to reference regions listed in dimension | Orphan region files never spawn |
+153
View File
@@ -0,0 +1,153 @@
# 12 - Regions
A region is a mid-level spatial unit inside a dimension. File location is `regions/<loadKey>.json`. Each region lists root biomes for land, sea, shore, and optional cave roles, plus regional rarity, zooms, shores, ores, objects, and caves.
Related: see `05 - Concepts & Pack Layout.md`, `11 - Dimensions.md`, `13 - Biomes.md`, `16 - Surfaces, Decorators & Deposits.md`, `20 - Object Placement.md`, `15 - Caves & Carving.md`.
## Role
Dimensions pick regions by noise (`regionStyle` / `regionZoom` / region `rarity`). Within a region, land/sea/shore/cave biome lists pick biomes (also rarity-weighted). Child biomes are **not** listed on the region; only root parents go in the region arrays. Children are declared on the parent biome (`children` field).
Inferred surface roles (`InferredType`): `LAND`, `SEA`, `SHORE`, `CAVE`.
## Load Key
| Rule | Detail |
|------|--------|
| Folder | `regions/` |
| Key | Path relative to `regions/` without `.json` |
| Shipping overworld | Flat files: `temperate.json` → key `temperate` |
| Dimension reference | Dimension `regions` array uses those keys |
## Field Reference (`IrisRegion`)
### Identity and rarity
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `name` | string | `"A Region"` | Required display name |
| `rarity` | int | `1` | 1128; higher = rarer when competing among dimension regions |
| `color` | string | `null` | Map visualization color, e.g. `#9BEE61` |
### Biome lists
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `landBiomes` | string[] | **Yes** | Root land biome load keys |
| `seaBiomes` | string[] | No | Root sea biomes; empty allowed for land-only worlds |
| `shoreBiomes` | string[] | No | Root shore biomes; empty allowed for land-only worlds |
| `caveBiomes` | string[] | No (array type allows empty) | Root cave biomes for carving/cave selection |
Keys are biome load keys under `biomes/` (e.g. `temperate/plains`, `carving/drip`).
### Biome and shore zooms
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `landBiomeZoom` | double | `1` | Land biome size in this region |
| `shoreBiomeZoom` | double | `1` | Shore biome size |
| `seaBiomeZoom` | double | `1` | Sea biome size |
| `caveBiomeZoom` | double | `1` | Cave biome size |
| `shoreHeightMin` | double | `1.2` | Min shore height contribution |
| `shoreHeightMax` | double | `3.2` | Max shore height contribution |
| `shoreHeightZoom` | double | `3.14` | Shore height noise zoom |
### Rivers and lakes (style)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `riverStyle` | `IrisGeneratorStyle` | `VASCULAR_THIN` zoomed `7.77` | River placement style |
| `lakeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_THICK` | Lake placement style |
### Content attachments
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `objects` | `IrisObjectPlacement[]` | empty | Region-wide `.iob` placements |
| `proceduralObjects` | `IrisProceduralObjects` | empty | Trees/ruins/formations/coral/fungi/crystals generated procedurally |
| `structures` | `IrisStructurePlacement[]` | empty | Jigsaw / native structure placements |
| `entitySpawners` | string[] | empty | `IrisSpawner` load keys |
| `effects` | `IrisEffect[]` | empty | Packet ambient effects (potions, sounds, particles) |
| `loot` | `IrisLootReference` | empty | Region loot |
| `blockDrops` | `IrisBlockDrops[]` | empty | Custom drops |
| `deposits` | `IrisDepositGenerator[]` | empty | Regional deposits added to global |
| `depositVariants` | `IrisDepositVariant[]` | empty | Ore remaps after biome, before dimension |
| `ores` | `IrisOreGenerator[]` | empty | Regional ores (surface vs underground flags) |
| `caveProfile` | `IrisCaveProfile` | default | Region cave profile |
Deposit precedence (documented on fields): biome variants → region variants → dimension variants; first match wins at each tier.
## Overworld Sample: Temperate
Path: `…/packs/overworld/regions/temperate.json`
| Field | Value |
|-------|-------|
| `name` | `Temperate` |
| `color` | `#9BEE61` |
| `rarity` | `1` |
| `landBiomes` | Many temperate + mountain + vanilla roots (e.g. `temperate/plains`, `vanilla/cherry_grove`) |
| `shoreBiomes` | Beaches including `vanilla/stony_shore` |
| `seaBiomes` | Oceans/rivers (`ocean/deep`, `temperate/sea/river`, …) |
| `caveBiomes` | `carving/rocky-cavebiome`, `carving/deep`, `carving/drip`, … |
| `landBiomeZoom` | `3.5` |
| `seaBiomeZoom` | `6` |
| `shoreBiomeZoom` | `0.15` |
| `caveBiomeZoom` | `3.3` |
| `shoreHeightMin` / `Max` / `Zoom` | `1` / `5.2` / `1.14` |
| `deposits` | Iron/coal band example |
| `loot` | `FALLBACK` mode, temperate tables |
| `caveProfile` | Enabled with density/threshold/surface settings |
Shipping overworld region keys (from dimension `regions` list): `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics`.
## Minimal Region JSON
```json
{
"name": "Starter",
"rarity": 1,
"landBiomes": ["starter"],
"seaBiomes": ["starter"],
"shoreBiomes": ["starter"]
}
```
Land-only dimension (no ocean shoreline generated):
```json
{
"name": "Highlands",
"rarity": 2,
"landBiomes": ["highlands/plateau"],
"seaBiomes": [],
"shoreBiomes": []
}
```
## How To: Make a Region
1. Create `regions/<key>.json`.
2. Set `name` and `rarity`.
3. List **root** biomes only under `landBiomes` (and sea/shore/cave as needed). Keys must match files under `biomes/` (subfolders become path segments in the key).
4. Add the region key to the dimensions `regions` array.
5. Tune `landBiomeZoom` / `seaBiomeZoom` / `shoreBiomeZoom` after biomes look right.
6. Optionally add regional `deposits`, `ores`, `objects`, `structures`, `caveProfile`.
7. Studio: set dimension `"focusRegion": "<key>"` to generate only that region while authoring.
## Resolution Notes
- `getAllBiomeIds()` unions land, cave, sea, and shore lists.
- Child expansion walks each biomes `children` and `carvingBiome` through the pack loader (cyclic graphs stop after depth limit on biomes; region walks keep collecting until the name set empties).
- Shore height at a column uses noise fitted between `shoreHeightMin` and `shoreHeightMax` with `shoreHeightZoom`.
- Object lists are filtered into surface vs carving support by placement `carvingSupport`.
## Common Author Mistakes
| Mistake | Result |
|---------|--------|
| Listing child biomes on the region | Children should be on the parent biome; listing children as roots duplicates or skips intended nesting |
| Region not listed on dimension | Never selected |
| Empty `landBiomes` | Invalid region for normal overworld generation |
| Wrong biome key path | `temperate/plains` must match `biomes/temperate/plains.json` |
| Relying on region rarity alone | Dimension also uses noise style/zoom; sample with `/iris studio regions` |
+416
View File
@@ -0,0 +1,416 @@
# 13 - Biomes
A biome is the primary surface/authoring unit for terrain height, block layers, decorations, objects, and Minecraft biome derivatives. Files live under `biomes/<loadKey>.json`. Regions reference root biomes; biomes may nest children and optional custom datapack biomes.
Related: see `12 - Regions.md`, `14 - Generators & Noise.md`, `16 - Surfaces, Decorators & Deposits.md`, `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`, `19 - Objects.md`, `20 - Object Placement.md`, `23 - Loot, Entities, Spawners, Markers.md`.
## Role
| Layer | Responsibility |
|-------|----------------|
| Region lists | Choose which root biomes can appear |
| Biome `generators` | Height relative to dimension `fluidHeight` |
| Biome `layers` | Surface and subsurface material stacks |
| `derivative` / `vanillaDerivative` | Minecraft biome for colors and structure eligibility |
| `customDerivitives` | Optional custom datapack biomes (field spelling is intentional in code) |
| Objects / structures / decorators | Placement and decoration on this biome |
`InferredType` (`LAND`, `SEA`, `SHORE`, `CAVE`) is assigned from which region list selected the biome, not from a JSON field on the biome itself.
## Load Key
| Rule | Detail |
|------|--------|
| Folder | `biomes/` |
| Key | Relative path without `.json` |
| Examples | `starter``biomes/starter.json`; `temperate/plains``biomes/temperate/plains.json`; `carving/drip``biomes/carving/drip.json` |
## Core Fields (`IrisBiome`)
### Identity
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `name` | string | `"Subterranean Land"` | Required human-readable name (not the load key) |
| `rarity` | int | `1` | 1512; rarity among sibling biomes in a region list |
| `color` | string | `null` | Map color, e.g. `#42A616` |
### Minecraft derivatives (required for generation)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `derivative` | string (biome key) | `"minecraft:the_void"` | **Required.** Vanilla/mod biome used for Iris terrain/color resolution |
| `vanillaDerivative` | string | `null` → falls back to `derivative` | Structure selection derivative; land/sea/shore eligibility rules apply for vanilla namespaces |
| `biomeScatter` | string[] | empty | Extra derivatives for color scatter |
| `biomeSkyScatter` | string[] | empty | Derivatives above terrain (3D biome colors) |
| `biomeStyle` | `IrisGeneratorStyle` | `SIMPLEX` | Scatter dispersion when multiple derivatives |
Use namespaced keys (`minecraft:plains`) or bare vanilla paths accepted by `NamespacedKey` resolution.
### Children and carving
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `children` | string[] | empty | Child biome load keys; portions of this biome morph into children |
| `childShrinkFactor` | double | `1.5` | Child size vs parent (docs suggest ~13) |
| `childStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Child shape noise |
| `carvingBiome` | string | `""` | Biome used under carving instead of this one when set |
| `caveMinDepthBelowSurface` | int | `0` | Min depth below surface before this cave biome can be picked |
Cyclic child graphs are supported; Iris stops walking children after a depth limit (annotation: nine biomes down the tree).
### Generators (height)
Type: `IrisBiomeGeneratorLink` (`@Snippet("generator-layer")`).
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `generator` | string | `"default"` | Load key under `generators/` |
| `min` | int | `0` | Height offset min relative to fluid height (2032…2032) |
| `max` | int | `0` | Height offset max relative to fluid height |
Height is lerped from generator noise in \[0,1\] into \[min, max\], then added relative to fluid height. Negative min/max produce ocean floors.
Multiple generator links mix with other biomes generators as expected when interpolation sizes differ.
### Layers (block palettes)
Type: `IrisBiomePaletteLayer` (`@Snippet("biome-palette")`).
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `palette` | `IrisBlockData[]` | grass_block | **Required.** Weighted blocks |
| `minHeight` | int | `1` | Min layer thickness (02032) |
| `maxHeight` | int | `1` | Max layer thickness (12032) |
| `style` | `IrisGeneratorStyle` | `STATIC` | Multi-block palette noise |
| `zoom` | double | `5` | Palette noise zoom |
| `slopeCondition` | `IrisSlopeClip` | empty | Optional slope gate/growth |
`IrisBlockData` entries:
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `block` | string | `"air"` | Block id, e.g. `minecraft:grass_block` |
| `weight` | int | `1` | Relative pick weight |
| `data` | map | empty | Block state properties |
| `backup` | block data | optional | Fallback if block missing |
| `debug` | boolean | false | Console debug when Iris debug enabled |
Biome layer stacks:
| Field | Role |
|-------|------|
| `layers` | Surface-down stack (required; default one empty grass layer) |
| `seaLayers` | Underwater surface layers |
| `caveCeilingLayers` | Cave ceiling material stack |
| `slab` | Default slab layer for post slabs (default empty/zero palette) |
| `wall` | Steep-face wall palette (default empty/zero) |
| `lockLayers` | When true, layers descend from max biome height (mesa style) |
| `lockLayersMax` | Max layers when locked (default `7`) |
Below authored layers, Iris fills with the dimension rock palette.
### Custom biomes (`customDerivitives`)
**JSON field name is `customDerivitives`** (misspelling of “derivatives” preserved in `IrisBiome`).
Type: `IrisBiomeCustom` (`@Snippet("custom-biome")`). Installed via datapack compilation.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `id` | string | `""` | **Required.** Resource path id (lowercased), e.g. `oak_forest` |
| `category` | `IrisBiomeCustomCategory` | `plains` | **Required.** Vanilla category enum |
| `temperature` | double | `0.8` | 3…3 |
| `humidity` | double | `0.4` | 3…3 (downfall amount) |
| `downfallType` | `IrisBiomeCustomPrecipType` | `rain` | `none`, `rain`, `snow` |
| `spawnRarity` | int | `0` | 020 creature spawn probability |
| `spawns` | `IrisBiomeCustomSpawn[]` | empty | Custom mob spawns |
| `tags` | string[] | empty | Explicit biome tags |
| `ambientParticle` | `IrisBiomeCustomParticle` | null | Client particle |
| `skyColor` | hex string | `#79a8e1` | |
| `fogColor` | hex string | `#c0d8e1` | |
| `waterColor` | hex string | `#3f76e4` | |
| `waterFogColor` | hex string | `#050533` | |
| `grassColor` | hex string | `""` (omit if empty) | |
| `foliageColor` | hex string | `""` | |
Tag inheritance: effective tags = authored `tags` plus non-structure tags of the vanilla derivative. Structure tags (`has_structure/*`) are **not** inherited so native structures are not double-placed.
#### Custom spawn entry (`IrisBiomeCustomSpawn`)
| Field | Type | Default |
|-------|------|---------|
| `type` | entity key | `minecraft:cow` |
| `minCount` | int | `2` |
| `maxCount` | int | `5` |
| `weight` | int | `1` |
| `group` | `IrisBiomeCustomSpawnType` | `MISC` |
Spawn groups: `MONSTER`, `CREATURE`, `AMBIENT`, `AXOLOTLS`, `UNDERGROUND_WATER_CREATURE`, `WATER_CREATURE`, `WATER_AMBIENT`, `MISC`.
#### Custom categories (`IrisBiomeCustomCategory`)
`beach`, `desert`, `extreme_hills`, `forest`, `icy`, `jungle`, `mesa`, `mushroom`, `nether`, `none`, `ocean`, `plains`, `river`, `savanna`, `swamp`, `taiga`, `the_end`.
#### Ambient particle (`IrisBiomeCustomParticle`)
| Field | Default |
|-------|---------|
| `particle` | `minecraft:flash` |
| `rarity` | `35` (higher = rarer; probability `1/rarity` in datapack JSON) |
### Decorators, objects, structures, ores
| Field | Type | Notes |
|-------|------|-------|
| `decorators` | `IrisDecorator[]` | Tall grass, cactus, kelp-style placements (see `16 - Surfaces, Decorators & Deposits.md`) |
| `objects` | `IrisObjectPlacement[]` | `.iob` placements |
| `proceduralObjects` | `IrisProceduralObjects` | Procedural trees/coral/etc. |
| `structures` | `IrisStructurePlacement[]` | Jigsaw / native structures |
| `floatingChildBiomes` | `IrisFloatingChildBiomes[]` | Floating islands using another biomes visuals |
| `mergeFloatingChildBiomes` | boolean | When true, all floating entries sample independently |
| `deposits` | `IrisDepositGenerator[]` | Biome deposits |
| `depositVariants` | `IrisDepositVariant[]` | Ore remaps (first of biome tier) |
| `oreDepositFrequencyMultiplier` | double | 01 scale ore vein frequency (default `1`) |
| `oreDepositSizeMultiplier` | double | 0.0116 scale ore size (default `1`) |
| `ores` | `IrisOreGenerator[]` | Biome ores |
| `entitySpawners` | string[] | Spawner keys |
| `effects` | `IrisEffect[]` | Ambient effects |
| `loot` | `IrisLootReference` | Biome loot |
| `blockDrops` | `IrisBlockDrops[]` | Custom drops |
| `caveProfile` | `IrisCaveProfile` | Biome cave profile override |
## Floating child biomes (`IrisFloatingChildBiomes`)
`floatingChildBiomes` builds floating terrain above columns owned by the parent biome. Each entry can reuse the parent or reference another biome for its generators, layers, derivative, decorators, and surface objects. With `mergeFloatingChildBiomes: false` (default), `pickerStyle` and `rarity` select one entry per column; with it true, every entry samples independently and islands may overlap.
### Target, footprint, and altitude
| Field | Default / range | Behavior |
|-------|-----------------|----------|
| `biome` | `""` | Target biome key; empty, missing, or the parent key falls back to the parent biome |
| `rarity` | `1` (1512) | Relative selection rarity; lower values are more common |
| `footprintStyle` | `SIMPLEX` | 2D island-outline noise; style zoom and fracture control scale and warping |
| `footprintThreshold` | `0.5` (01) | Minimum footprint sample; higher values produce less coverage |
| `pickerStyle` | `SIMPLEX` | Coherent per-column entry selection when entries are not merged |
| `altitudeStyle` | `SIMPLEX` | Varies the island base between the configured heights |
| `minHeightAboveSurface` / `maxHeightAboveSurface` | `160` / `210` (02032) | Absolute world-Y range for the base despite the historical field names |
| `minAbsoluteY` | `null` | Optional lower clamp for the base/tail |
| `maxAbsoluteY` | `null` | Optional upper clamp for the island top |
### Edge, top, and underside shape
| Field | Default / range | Behavior |
|-------|-----------------|----------|
| `edgeTaperWidth` | runtime default (232) | Width of the rounded contour-to-full-thickness transition |
| `edgeTaperExponent` | runtime default (0.254) | Below 1 makes a fuller edge; above 1 keeps the rim thinner |
| `edgeTaperVariationStyle` | broad `SIMPLEX` | Coherently varies taper width without changing the footprint |
| `edgeTaperVariationAmplitude` | `0` (08) | Local widening/narrowing; runtime clamps the resulting width to 232 |
| `topShapeMode` | `BIOME` | `BIOME` uses target generators; `NOISE` uses `topShapeStyle`; `FLAT` uses a fixed top |
| `maxTopHeight` | `40` (0512) | Maximum height above the island base |
| `topShapeStyle` | `SIMPLEX` | Top heightmap when mode is `NOISE` |
| `topShapeAmp` | `1` (01) | Multiplier for the noise-driven top profile |
| `bottomStyle` | `SIMPLEX` | 2D noise for the hanging underside/tail |
| `bottomDepthMin` / `bottomDepthMax` | `4` / `20` (0512) | Tail depth range below the base |
| `bottomExponent` | `1` (0.18) | Power curve for tail depth; above 1 makes deep tails sparser |
| `maxThickness` | `96` (1512) | Hard cap on top-to-bottom column thickness |
| `wallWarpStyle` | `null` | Optional 3D noise that shifts X/Z footprint samples by Y layer |
| `wallWarpAmplitude` | `6` (064) | Maximum wall-warp displacement; ignored without `wallWarpStyle` |
### Materials, fluids, and carving
| Field | Default | Behavior |
|-------|---------|----------|
| `bottomPaletteMode` | `DEPTH` | `DEPTH` uses normal top-down layers; `MIRROR_TOP` mirrors the shallow palette; `CUSTOM` uses `bottomPalette` near the underside |
| `bottomPalette` | `[]` | `IrisBiomePaletteLayer[]` used only by `CUSTOM` |
| `localFluidHeight` | `null` | Fluid surface relative to the island base; null disables internal pools |
| `fluidBlock` | `minecraft:water` | Block used for internal pools |
| `carveStyle` | `null` | Optional direct 3D pocket noise |
| `carving` | `""` | Optional dimension carving-entry id or biome key; dimension entries resolve first and their cave profile overrides `carveStyle` |
| `carveThreshold` | `1` (01) | Direct noise above this value becomes air; with `carving`, tunes the referenced cave profile |
### Decoration and objects
| Field | Default | Behavior |
|-------|---------|----------|
| `inheritDecorators` | `true` | Apply target-biome decorators to the island top |
| `inheritObjects` | `true` | Allow target-biome surface objects on the island top |
| `objectShrinkFactor` | `1` (0.011) | Uniform scale for inherited, extra, and free-floating objects |
| `extraObjects` | `[]` | Additional `IrisObjectPlacement` entries anchored to the island top |
| `floatingObjects` | `[]` | Additional placements generated independently in air with floating placement mode |
| `topObjectMode` | `INHERIT_ONLY` | `INHERIT_ONLY`, `MERGE`, or `REPLACE` for inherited top objects versus overrides |
| `topObjectOverrides` | `[]` | Top placements consumed according to `topObjectMode` |
| `bottomObjectMode` | `INHERIT_ONLY` | Enables `bottomObjectOverrides`; `MERGE` and `REPLACE` are equivalent because there is no inherited bottom set |
| `bottomObjectOverrides` | `[]` | Placements attached upside-down to the lowest solid face; directional blocks may not survive the flip correctly |
| `color` | `null` | Iris Studio visualization color |
Example:
```json
{
"floatingChildBiomes": [{
"biome": "temperate/plains",
"rarity": 2,
"footprintStyle": { "style": "SIMPLEX", "zoom": 0.8 },
"footprintThreshold": 0.7,
"minHeightAboveSurface": 160,
"maxHeightAboveSurface": 210,
"topShapeMode": "BIOME",
"bottomDepthMin": 6,
"bottomDepthMax": 28,
"inheritDecorators": true,
"inheritObjects": true
}]
}
```
## Overworld Samples
### Land plains — `biomes/temperate/plains.json`
```json
{
"name": "Plains",
"color": "#42A616",
"rarity": 2,
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"generators": [{ "min": 4, "max": 10, "generator": "plain" }],
"biomeStyle": { "style": "SIMPLEX" },
"wall": { "palette": [{ "block": "minecraft:stone" }, { "block": "minecraft:andesite" }] },
"layers": [
{ "palette": [{ "block": "minecraft:grass_block" }] },
{ "minHeight": 2, "maxHeight": 2, "palette": [{ "block": "minecraft:dirt" }] }
]
}
```
(File continues with more layers, objects, and placements.)
### Parent with children and custom biome — `biomes/temperate/oak-forest.json`
```json
{
"name": "Oak Forest",
"derivative": "minecraft:forest",
"vanillaDerivative": "minecraft:forest",
"customDerivitives": [{
"id": "oak_forest",
"foliageColor": "#64B233",
"grassColor": "#77A620",
"category": "forest"
}],
"children": ["temperate/oak-forest-extended"],
"generators": [
{ "generator": "smooth-dunes", "max": 12, "min": 5 },
{ "generator": "rare-hills", "max": 40, "min": 0 }
]
}
```
### Sea biome heights — `biomes/temperate/sea/ocean.json` (excerpt)
```json
{
"name": "Temperate Ocean",
"derivative": "minecraft:lukewarm_ocean",
"vanillaDerivative": "minecraft:ocean",
"generators": [{ "min": -32, "max": -10, "generator": "mountain" }]
}
```
Negative generator min/max place the surface below fluid height.
### Custom-only colors — `biomes/vanilla/sunflower_plains.json` (excerpt)
```json
{
"customDerivitives": [{
"category": "plains",
"id": "sunflower_plains",
"grassColor": "#91BD59",
"foliageColor": "#77AB2F",
"waterColor": "#44AFF5",
"downfallType": "none"
}]
}
```
## Minimal Biome JSON
Studio starter:
```json
{
"name": "Starter Plains",
"layers": [{ "palette": [{ "block": "minecraft:grass_block" }] }],
"generators": [{ "generator": "flat", "min": 96, "max": 96 }],
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains"
}
```
Requires a matching generator file under `generators/` (starter uses `generators/flat.json`).
## How To: Make a Biome
1. Add `biomes/<path>/<name>.json`. Choose load key path carefully; regions will reference it exactly.
2. Set `name`, `derivative`, `vanillaDerivative`.
3. Add at least one `generators` link and a generator JSON under `generators/`.
4. Define `layers` from topsoil down (grass → dirt → stone blend).
5. Optionally set `wall` for cliffs, `decorators` for grass, `objects` for trees/clutter.
6. For variants inside a parent, create a child biome file and list its key in the parents `children`.
7. For custom colors/tags/mobs, add `customDerivitives` with a unique `id` and `category`.
8. Attach the biome to a region: land → `landBiomes`, ocean floor → `seaBiomes`, beach → `shoreBiomes`, cave → `caveBiomes`.
9. Studio test: dimension `"focus": "temperate/plains"` forces only that biome.
## Generator Link How-To
1. Create or reuse `generators/<id>.json` (noise composite + interpolator; see `14 - Generators & Noise.md`).
2. On the biome:
```json
{
"generators": [
{ "generator": "plain", "min": 4, "max": 10 }
]
}
```
3. Land: positive min/max above fluid. Sea: negative min/max. Flat plateaus: min == max.
## Custom Biome How-To
1. Add:
```json
{
"customDerivitives": [
{
"id": "my_plains",
"category": "plains",
"temperature": 0.8,
"humidity": 0.4,
"downfallType": "rain",
"grassColor": "#91BD59",
"foliageColor": "#77AB2F"
}
]
}
```
2. Keep `derivative` / `vanillaDerivative` set to a close vanilla biome for structure eligibility and tag inheritance.
3. Open studio or recreate the world so datapack custom biomes install (create/open may require restart when datapacks change).
4. Do not invent field names like `customDerivatives` — the engine field is `customDerivitives`.
## Common Author Mistakes
| Mistake | Result |
|---------|--------|
| Missing `derivative` | Terrain/biome resolution fails or voids |
| Wrong generator key | Falls back to empty default generator behavior |
| Listing child biomes on the region | Breaks parent/child hierarchy intent |
| `customDerivatives` spelling | Field ignored; use `customDerivitives` |
| Sea biome with positive generators | “Ocean” generates as land relative to fluid |
| Empty `layers` palette | Missing surface blocks |
| Expecting biome `type` field | Role comes from region list membership (`InferredType`) |
+240
View File
@@ -0,0 +1,240 @@
# 14 - Generators & Noise
Generators are pack-level noise definitions that biomes link for terrain height. Each generator composites one or more noise layers, optionally applies cliffs and cell fracture, and interpolates across biome boundaries. Styles, expressions, and image maps supply the raw noise signal.
Related: `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `15 - Caves & Carving.md`, `16 - Surfaces, Decorators & Deposits.md`, `05 - Concepts & Pack Layout.md`, `10 - Studio & VSCode Schemas.md`.
## Where files live
| Path | Registrant | Role |
|------|------------|------|
| `generators/<key>.json` | `IrisGenerator` | Height-map composite used by biomes |
| `expressions/<key>.json` | `IrisExpression` | Math expression used as a style source |
| `images/<key>.png` | `IrisImage` | PNG sampled by `IrisImageMap` |
Biome JSON does not embed generators. It references them by key:
```json
{
"generators": [
{ "generator": "plain", "min": 4, "max": 14 }
]
}
```
`IrisBiomeGeneratorLink` loads `generators/<generator>.json`, samples height in 0..1, then lerps to `min`..`max` relative to fluid height. Negative ranges produce ocean floors.
## Authoring workflow
1. Create `generators/<name>.json` with `seed`, `interpolator`, and at least one `composite` entry.
2. Reference that key from every biome that should share the shape (`generators[].generator`).
3. Tune `min`/`max` per biome for local relief; leave the generator file for global shape and frequency.
4. Hotload in studio; regenerate nearby chunks to verify blending across biome edges (`interpolator.horizontalScale`).
5. Optional: replace a style's built-in `NoiseStyle` with `expression` or `imageMap` for custom fields.
## Generator file (`IrisGenerator`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `zoom` | double ≥ 0.001 | `1` | Frequency; higher sample coords are divided by zoom |
| `opacity` | double ≥ 0 | `1` | Multiplier on composite output |
| `multiplicitive` | boolean | `false` | When true, multiplies composite layers instead of averaging by opacity sum (field spelling is code-authoritative) |
| `seed` | long | `1` | Required base seed |
| `offsetX` / `offsetZ` | double | `0` | Shifts sample coordinates |
| `interpolator` | `IrisInterpolator` | bilinear starcast | Cross-biome height smoothing |
| `composite` | `IrisNoiseGenerator[]` | `[]` | Required layers; empty → height 0 |
| `cliffHeightMin` / `cliffHeightMax` | double 0..8192 | `0` | Both 0 disables cliffs |
| `cliffHeightGenerator` | `IrisNoiseGenerator` | default | Picks cliff step height between min/max |
| `cellFractureZoom` | double ≥ 0.001 | `1` | Cell crack scale |
| `cellFractureShuffle` | double ≥ 0 | `12` | Cell coordinate shuffle |
| `cellFractureHeight` | double | `0` | `0` disables cell cracks; non-zero multiplies height outside cell cores |
| `cellPercentSize` | double 0..1 | `0.75` | Relative cell core size vs veins |
### Interpolator (`IrisInterpolator`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `function` | `InterpolationMethod` | `BILINEAR_STARCAST_6` | Smoothing kernel |
| `horizontalScale` | double 1..8192 | `7` | Sample radius; smaller = more detail, less smooth |
Common `InterpolationMethod` values: `NONE`, `BILINEAR`, `STARCAST_3/6/9/12`, `BILINEAR_STARCAST_3/6/9/12`, `HERMITE_STARCAST_3/6/9/12`. Overworld generators typically use `BILINEAR_STARCAST_9` with `horizontalScale` 1252.
### Noise layer (`IrisNoiseGenerator`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `enabled` | boolean | `true` | Disabled layers return `offsetY` only |
| `zoom` | double ≥ 0.0001 | `1` | Layer frequency |
| `opacity` | double 0..1 | `1` | Layer weight / amplitude |
| `negative` | boolean | `false` | Output becomes `-noise + opacity` |
| `offsetX` / `offsetY` / `offsetZ` | double | `0` | Coordinate / output offsets; avoid `offsetY` for terrain |
| `seed` | long | `0` | Required |
| `style` | `IrisGeneratorStyle` | `IRIS` | Noise source |
| `octaves` | int ≥ 1 | `1` | Multi-octave CNG |
| `exponent` | double | `1` | Power curve on output |
| `parametric` / `bezier` / `sinCentered` | boolean | `false` | Output remaps |
| `fracture` | `IrisNoiseGenerator[]` | `[]` | Child noise warps this layer's input coordinates |
Composite evaluation (add mode): sum each layer's noise, divide by total opacity, multiply generator `opacity`. Multiplicative mode starts at 1 and multiplies each layer.
### Cliff and cell post-process
- Cliffs quantize height: `(round((v*255)/cliffHeight) * cliffHeight) / 255` when `cliffHeightMax > 0`.
- Cell fracture uses a cell distance field; outside the cell core, height is scaled by `cellFractureHeight`.
## Generator style (`IrisGeneratorStyle`)
Used everywhere noise is configured: generator layers, decorators, deposits palettes, cave profiles, dimension biome styles.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `style` | `NoiseStyle` | `FLAT` | Built-in algorithm when expression/image unset |
| `zoom` | double ≥ 0.00001 | `1` | Style scale (`1/zoom` applied to CNG) |
| `exponent` | double 0.01562..64 | `1` | Power on style output |
| `multiplier` | double ≥ 0.00001 | `1` | Fracture strength when this style is a `fracture` child |
| `fracture` | nested `IrisGeneratorStyle` | null | Distorts parent coordinates |
| `axialFracturing` | boolean | `false` | Different axis order per dimension (slower) |
| `cellularFrequency` | double | `0` | `>0` cellularizes style |
| `cellularZoom` | double | `1` | Cell scale after cellularize |
| `expression` | string key | null | Load `expressions/<key>.json` instead of `style` |
| `imageMap` | `IrisImageMap` | null | Sample PNG instead of `style` |
| `cacheSize` | int 0..8192 | `0` | Disk-backed CNG cache size when >0 |
Priority when building CNG: `expression` if set and loadable, else `imageMap` if set, else `NoiseStyle`.
### Common `NoiseStyle` values
Terrain / large forms: `IRIS`, `IRIS_DOUBLE`, `IRIS_THICK`, `IRIS_HALF`, `SIMPLEX`, `PERLIN`, `PERLIN_IRIS`, `FRACTAL_SMOKE`, `FRACTAL_WATER`, `FRACTAL_FBM_SIMPLEX`, `FRACTAL_BILLOW_PERLIN`, `NOWHERE`, `NOWHERE_CELLULAR`, `GLOB`, `CELLULAR_HEIGHT`.
Scatter / decoration: `STATIC` (white noise), `STATIC_BILINEAR`, `FLAT` (always 0.5).
Cells / veins: `CELLULAR`, `CELLULAR_IRIS_DOUBLE`, `VASCULAR`, `VASCULAR_THIN`, `SIMPLEX_VASCULAR`, `CLOVER` and starcast variants, hex family (`HEXAGON`, `HEX_JAMES`, …).
Full enum is large; Studio schemas list every constant.
## Expressions (`IrisExpression`)
| Field | Type | Notes |
|-------|------|-------|
| `expression` | string | Required. Inherited vars: `x`, `y`, `z` (do not redeclare) |
| `variables` | `IrisExpressionLoad[]` | Named variables bound before evaluate |
| `functions` | `IrisExpressionFunction[]` | Named dynamic functions (style or engine stream) |
### Variable (`IrisExpressionLoad`)
| Field | Notes |
|-------|-------|
| `name` | Variable name (not `x`/`y`/`z`) |
| `staticValue` | Used when no other source |
| `styleValue` | Nested `IrisGeneratorStyle` sampled at coords |
| `engineStreamValue` | Engine procedural stream enum |
| `engineValue` | Engine scalar enum |
### Function (`IrisExpressionFunction`)
| Field | Notes |
|-------|-------|
| `name` | Function name in expression text |
| `styleValue` | Style-backed noise function |
| `engineStreamValue` | Engine stream function |
| `args` | Argument count (≥2); engine streams force 2 |
Parser: Paralithic. Load failures log and return null CNG fallback paths.
## Image maps (`IrisImageMap` + `IrisImage`)
PNG files under `images/` load as `IrisImage`. Styles reference them:
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `image` | string key | `""` | Image registrant key |
| `coordinateScale` | double ≥ 1 | `32` | Blocks per pixel (before style zoom) |
| `interpolationMethod` | `InterpolationMethod` | `BILINEAR_STARCAST_6` | Use `NONE` for nearest |
| `channel` | `IrisImageChannel` | `COMPOSITE_ADD_HSB` | Pixel → 0..1 |
| `inverted` | boolean | `false` | `1 - value` |
| `tiled` | boolean | `false` | Modulo wrap |
| `centered` | boolean | `true` | Origin at image center |
`IrisImageChannel`: `RED`, `GREEN`, `BLUE`, `SATURATION`, `HUE`, `BRIGHTNESS`, `COMPOSITE_ADD_RGB`, `COMPOSITE_MUL_RGB`, `COMPOSITE_MAX_RGB`, `COMPOSITE_ADD_HSB`, `COMPOSITE_MUL_HSB`, `COMPOSITE_MAX_HSB`, `RAW`.
Out-of-bounds pixels (non-tiled) return 0. Missing images log and yield 0.
## Dimension-level noise (related)
Dimensions also use styles and shaped styles for placement, not height generators:
| Dimension field | Role |
|-----------------|------|
| `landBiomeStyle` / `seaBiomeStyle` / `shoreBiomeStyle` / `caveBiomeStyle` / `regionStyle` / `continentalStyle` | Biome/region placement noise |
| `overlayNoise` | `IrisShapedGeneratorStyle[]` height overlays (`generator` style + `min`/`max`) |
| `coordFractureDistance` / `coordFractureZoom` | Global coordinate warp |
| `rockZoom` / `rockPalette` / `fluidPalette` | Default fill materials (see `16 - Surfaces, Decorators & Deposits.md`) |
## Overworld examples
`generators/plain.json` — smooth lowland:
```json
{
"interpolator": { "function": "BILINEAR_STARCAST_9", "horizontalScale": 12 },
"seed": 7246661,
"composite": [{
"style": {
"style": "IRIS_DOUBLE",
"zoom": 0.87,
"fracture": { "style": "NOWHERE", "zoom": 0.195, "multiplier": 18 }
},
"seed": 10056,
"bezier": true
}]
}
```
`generators/mountain.json` — large-scale smoke:
```json
{
"interpolator": { "horizontalScale": 52, "function": "BILINEAR_STARCAST_9" },
"seed": 53551,
"composite": [{
"style": { "style": "FRACTAL_SMOKE", "zoom": 1 },
"seed": 3356
}]
}
```
`generators/cracked-cliffs.json` — cliffs + inverted glob:
```json
{
"interpolator": { "function": "BILINEAR_STARCAST_9", "horizontalScale": 12 },
"seed": 7246661,
"composite": [{
"style": {
"style": "GLOB",
"zoom": 0.4,
"exponent": 0.7,
"fracture": { "style": "NOWHERE", "zoom": 0.1, "multiplier": 5 }
},
"negative": true,
"seed": 10056
}],
"cliffHeightMax": 80,
"cliffHeightMin": 35,
"cliffHeightGenerator": {
"seed": 2348,
"style": { "style": "CELLULAR_HEIGHT" },
"zoom": 0.4
}
}
```
Shipping overworld generators do not use `expression` or `imageMap`. Pack ships `images/prototype-rivers.png` and `images/vascularcliffs.png` for optional author use. Snippet styles under `snippet/style/` (e.g. `bedrock.json` with `"style": "STATIC"`) are reusable style fragments.
## Practical notes
- Prefer sharing one generator across many biomes; vary `min`/`max` per biome for height bands.
- Match interpolator `horizontalScale` between neighboring biomes that should blend smoothly.
- Nested `fracture` multiplies cost; keep fracture chains short on hot terrain paths.
- `STATIC` is for scatter, not terrain relief.
- Field names `multiplicitive` and deposit `varience` are intentional code spellings; JSON must match.
+227
View File
@@ -0,0 +1,227 @@
# 15 - Caves & Carving
Iris carves caves itself during mantle generation via `MantleCarvingComponent` and `IrisCaveCarver3D`. Density fields from `IrisCaveProfile` decide solid vs air/water/lava. Cave biomes paint floors, ceilings, decorators, and objects inside carved space. Vanilla and mod noise carvers never run over Iris terrain.
Related: `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `16 - Surfaces, Decorators & Deposits.md`, `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`, `20 - Object Placement.md`, `22 - Native Structures & Datapacks.md`.
## Architecture (author-relevant)
1. Dimension `carvingEnabled` must be true (default).
2. Per column, Iris resolves a cave profile from biome → region → dimension (`enabled` profiles only).
3. Profiles blend across neighbors; `IrisCaveCarver3D` samples 3D density and writes carve flags into the mantle.
4. Cave biomes (region `caveBiomes`, dimension `carving` Y-band overrides, surface biome `carvingBiome`) supply materials and content for carved voxels.
5. Fluid placement inside caves follows profile water/lava rules and surface-clearance guards.
Empty pack folders such as `caves/` or `ravines/` are not separate registrant types. Carving is profile-driven JSON on dimensions/biomes/regions, not standalone cave files.
## Vanilla carvers never run
Iris does not implement Minecraft `NoiseGeneratorSettings` carver sampling. Generated biome JSON keeps empty `carvers` arrays. `applyCarvers` on the Iris chunk generator is a no-op for Iris-owned terrain. Pack authors must use `caveProfile` (and related cave biomes), not vanilla carver JSON or datapack carver features. See also platform notes in `30 - Platform Differences.md` / API matrix.
## Dimension gates
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `carvingEnabled` | boolean | `true` | Master switch for all profile carving |
| `caveProfile` | `IrisCaveProfile` | disabled defaults | Global/default profile |
| `carving` | `IrisDimensionCarvingEntry[]` | `[]` | Absolute world-Y cave biome bands |
| `caveBiomeStyle` | `IrisGeneratorStyle` | cellular | Picks among region cave biomes |
| `requireObjectSurfaceSupport` | boolean | `true` | Refuse surface objects over carve openings |
| `objectSurfaceSupportBuffer` | int 0..16 | `2` | Minimum solid buffer for surface objects |
| `upperDimensionCarving` | boolean | `false` | Carve through ceiling/upper terrain when set |
| `useMantle` | boolean | `true` | Mantle required for carving/objects |
## Cave profile (`IrisCaveProfile`)
Snippet key: `cave-profile`. Appears on **dimension**, **region**, and **biome**. Resolution prefers the most specific enabled profile in the mantle path (biome/region/dimension blend).
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `enabled` | boolean | `false` | Must be true to carve |
| `verticalRange` | `IrisRange` | `0..384` | Global carve Y band for the profile |
| `verticalEdgeFade` | int 0..128 | `20` | Soft edge near min/max |
| `verticalEdgeFadeStrength` | double 0..1 | `0.18` | Fade strength |
| `baseDensityStyle` | `IrisGeneratorStyle` | cellular iris double | Primary density field |
| `detailDensityStyle` | `IrisGeneratorStyle` | simplex | Detail field |
| `warpStyle` | `IrisGeneratorStyle` | flat | Coordinate warp |
| `baseWeight` | double ≥ 0 | `1` | Base field multiplier |
| `detailWeight` | double ≥ 0 | `0.35` | Detail multiplier |
| `warpStrength` | double ≥ 0 | `0` | Warp amount |
| `densityThreshold` | `IrisStyledRange` | ±0.2 cellular | Carve cutoff band |
| `thresholdBias` | double 0..1 | `0.16` | Extra bias subtracted before tests |
| `sampleStep` | int 1..8 | `1` | Vertical density step |
| `adaptiveSampling` | boolean | `true` | Coarse predictor then refine |
| `adaptiveSampleStep` | int 2..4 | `2` | Horizontal predictor grid |
| `adaptiveThresholdMargin` | double 0..1 | `0.04` | Ambiguity margin |
| `surfaceClearance` | int 0..64 | `4` | Min solid below terrain before carve |
| `allowSurfaceBreak` | boolean | `true` | Permit selected surface openings |
| `surfaceBreakStyle` | style | simplex zoomed | Where openings may occur |
| `surfaceBreakNoiseThreshold` | double -1..1 | `0.62` | Min noise for break columns |
| `surfaceBreakDepth` | int 0..64 | `18` | Depth window for break logic |
| `surfaceBreakThresholdBoost` | double 0..1 | `0.2` | Easier carve near surface break |
| `objectMinDepthBelowSurface` | int 0..64 | `6` | Cave-object depth gate |
| `modules` | `IrisCaveFieldModule[]` | `[]` | Extra density layers |
| `defaultObjectAnchor` | `IrisCaveAnchorMode` | `FLOOR` | Cave object anchor default |
| `defaultObjectPlaceMode` | `ObjectPlaceMode` | null | Prefer stilt modes for cave props |
| `anchorScanStep` | int 1..8 | `1` | Vertical anchor search step |
| `anchorSearchAttempts` | int 1..64 | `6` | Random column retries per chunk |
| `allowWater` | boolean | `true` | Cave water below fluid height |
| `waterMinDepthBelowSurface` | int 0..64 | `12` | Depth before cave water |
| `waterRequiresFloor` | boolean | `true` | Solid floor under water |
| `allowLava` | boolean | `true` | Cave lava by lava height rules |
### Density module (`IrisCaveFieldModule`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `style` | `IrisGeneratorStyle` | cellular | Module density |
| `weight` | double ≥ 0 | `1` | Contribution |
| `threshold` | double -1..1 | `0` | Pre-blend offset |
| `verticalRange` | `IrisRange` | `0..384` | Module Y window |
| `invert` | boolean | `false` | Invert before weighting |
### Anchor modes (`IrisCaveAnchorMode`)
| Value | Meaning |
|-------|---------|
| `PROFILE_DEFAULT` | Use profile default |
| `FLOOR` | Solid support below carved cell |
| `CEILING` | Solid support above |
| `CENTER` | No immediate floor/ceiling support |
| `ANY` | Any carved anchor |
## Dimension carving entries (`IrisDimensionCarvingEntry`)
Absolute world-Y cave biome overrides independent of surface biome.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `id` | string | `""` | Stable id (child references) |
| `enabled` | boolean | `true` | Toggle |
| `biome` | biome key | `""` | Cave biome applied in band |
| `worldYRange` | `IrisRange` | `-64..320` | Absolute world Y |
| `children` | string[] | `[]` | Child entry ids (cycles allowed, depth-limited) |
| `childShrinkFactor` | double | `1.5` | Child patch scale |
| `childStyle` | style | cellular | Child patch shape |
| `childRecursionDepth` | int | `3` | Max child resolve depth |
## Cave biomes (content)
Cave biomes are normal biome JSON used only underground:
| Mechanism | Location | Role |
|-----------|----------|------|
| Region `caveBiomes` | region JSON | Pool selected by `caveBiomeStyle` |
| Biome `carvingBiome` | surface biome | Optional fixed carve biome under that surface |
| Biome `caveMinDepthBelowSurface` | surface biome | Min depth before that carve biome applies |
| Dimension `carving[]` | dimension | Y-band force biomes |
| Biome `caveProfile` | any biome | Local carve density override when enabled |
| `layers` / `caveCeilingLayers` / `wall` | cave biome | Floor / ceiling / wall materials |
| `decorators` with `partOf: CEILING` | cave biome | Hang from ceilings |
| `objects` / `proceduralObjects` | cave biome | Cave props (`carvingSupport: CARVING_ONLY`) |
Surface biomes still provide height generators; cave biomes typically omit height generators or use fillers—the carve step removes solid first.
## Overworld examples
Dimension switch and deepdark band (`dimensions/overworld.json`):
```json
{
"carvingEnabled": true,
"caveProfile": {
"enabled": true,
"verticalRange": { "min": 6, "max": 700 },
"baseDensityStyle": { "style": "PERLIN_IRIS", "zoom": 0.72 },
"detailDensityStyle": { "style": "SIMPLEX", "zoom": 0.54 },
"warpStyle": { "style": "FRACTAL_WATER", "zoom": 0.5 },
"baseWeight": 0.9,
"detailWeight": 0.11,
"warpStrength": 0.24,
"densityThreshold": {
"min": -0.14,
"max": -0.06,
"style": { "style": "SIMPLEX", "zoom": 0.74 }
},
"thresholdBias": 0.14,
"sampleStep": 3,
"surfaceClearance": 5,
"allowSurfaceBreak": true,
"surfaceBreakStyle": { "style": "SIMPLEX", "zoom": 0.88 },
"surfaceBreakNoiseThreshold": 0.6,
"surfaceBreakDepth": 16,
"surfaceBreakThresholdBoost": 0.1,
"objectMinDepthBelowSurface": 14,
"defaultObjectAnchor": "FLOOR",
"defaultObjectPlaceMode": "ORGANIC_STILT",
"anchorSearchAttempts": 12,
"allowWater": true,
"waterMinDepthBelowSurface": 20,
"waterRequiresFloor": true,
"allowLava": true,
"modules": [
{
"style": { "style": "SIMPLEX_VASCULAR", "zoom": 1.08 },
"weight": 0.08,
"threshold": 0.03,
"verticalRange": { "min": 24, "max": 660 },
"invert": false
}
],
"verticalEdgeFade": 24,
"verticalEdgeFadeStrength": 0.18
},
"carving": [
{
"id": "global-deepdark-band",
"enabled": true,
"biome": "carving/standard-deepdark",
"worldYRange": { "min": -250, "max": -175 }
}
]
}
```
Region cave pool (`regions/temperate.json`):
```json
{
"caveBiomes": [
"carving/rocky-cavebiome",
"carving/deep",
"carving/drip",
"carving/chalk-gardens",
"carving/moss-pillars"
]
}
```
Cave biome content (`biomes/carving/amethyst.json` excerpt): floor/wall amethyst, floor buds, ceiling-facing clusters via `"partOf": "CEILING"`, `caveCeilingLayers` for roof materials.
## Authoring workflow
1. Enable dimension `caveProfile` with a vertical range covering playable Y.
2. Add `modules` for tunnels/rooms instead of raising `detailWeight` alone.
3. List themed biomes under each region's `caveBiomes` (and optional dimension `carving` bands).
4. Paint cave biomes with `layers`, `caveCeilingLayers`, `wall`, ceiling/floor decorators, and cave-only objects.
5. For surface sinkholes, keep `allowSurfaceBreak` true and tune `surfaceBreak*` noise; for sealed caves raise `surfaceClearance` and disable surface break.
6. Place cave objects with `carvingSupport: CARVING_ONLY` and stilt place modes (`FAST_MIN_STILT` / `ORGANIC_STILT`) to avoid floating props.
7. Verify: studio regen, check openings, waterfalls (`waterRequiresFloor`), and lava depth.
## Tuning knobs (quick)
| Goal | Adjust |
|------|--------|
| Larger caverns | Lower `densityThreshold` band / raise bias toward carve |
| Thinner tunnels | Raise threshold, lower `detailWeight`, add inverted modules |
| Fewer surface holes | Raise `surfaceBreakNoiseThreshold`, lower `surfaceBreakDepth`, or `allowSurfaceBreak: false` |
| Safer cave props | Raise `objectMinDepthBelowSurface`, set place mode + anchor |
| Dry caves | `allowWater: false` |
| Performance | Higher `sampleStep`, keep adaptive sampling on, simpler styles |
## Practical notes
- Profile `enabled: false` (the Java default) produces no profile carving even if cave biomes are listed.
- Cave biome layers still need solid carve first; they do not create voids alone.
- Upper-dimension carving is optional and off in overworld.
- Pack JSON may contain unknown keys; only fields on `IrisCaveProfile` apply.
@@ -0,0 +1,336 @@
# 16 - Surfaces, Decorators & Deposits
Surface composition is layered block palettes on biomes (and default rock/fluid palettes on the dimension). Decorators scatter plants and props on surfaces, shores, sea floor/ceiling, and cave ceilings. Deposits place underground clumps of ores and stone; variants remap ore ids by Y band.
Related: `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `15 - Caves & Carving.md`, `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`, `20 - Object Placement.md`, `24 - Pack Mods & Snippets.md`.
## Surfaces and material layers
### Biome palette layer (`IrisBiomePaletteLayer`)
Biome fields:
| Field | Role |
|-------|------|
| `layers` | Surface-down material stack (grass → dirt → stone). Iris fills remaining depth with dimension rock |
| `seaLayers` | Underwater column layers |
| `caveCeilingLayers` | Materials for cave ceilings |
| `slab` | Post-process slab palette (empty = none) |
| `wall` | Steep wall palette when height jump is large |
| `lockLayers` | Mesa mode: layers descend from max biome height, not surface |
| `lockLayersMax` | Max layer iterations when locked |
Layer fields:
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `palette` | `IrisBlockData[]` | grass | Weighted block list |
| `minHeight` / `maxHeight` | int | `1` | Thickness range |
| `style` | `IrisGeneratorStyle` | `STATIC` | Picks among palette when size > 1 |
| `zoom` | double ≥ 0.0001 | `5` | Palette noise zoom |
| `slopeCondition` | `IrisSlopeClip` | 0..10 | Layer only on slopes in range; thickness grows toward clip center |
`IrisSlopeClip`: `minimumSlope` (default 0), `maximumSlope` (default 10). Default clip accepts all slopes.
### Material palette (`IrisMaterialPalette`)
Shared palette type for rock/fluid, tree trunks, deposits-adjacent systems, procedural accents.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `palette` | `IrisBlockData[]` | stone | Weighted blocks |
| `style` | `IrisGeneratorStyle` | `STATIC` | Spatial pick |
| `zoom` | double ≥ 0.0001 | `5` | Sample scale |
Dimension defaults:
| Field | Default | Role |
|-------|---------|------|
| `rockPalette` | stone | Fill below biome layers |
| `fluidPalette` | water | Ocean/fluid column |
| `rockZoom` | `5` | Rock palette zoom |
`IrisBlockData` entries use `block` (id), optional `weight`, optional `data` blockstate map. They can also reference reusable block aliases as described below.
### Block data and reusable `blocks/*.json`
`IrisBlockData` is used by palettes, decorators, deposits, drop filters, and object replacement rules. A normal inline entry can use a namespaced block id or a vanilla id without the `minecraft:` prefix:
```json
{
"block": "minecraft:oak_log",
"weight": 2,
"data": { "axis": "y" },
"backup": { "block": "minecraft:spruce_log" },
"debug": false
}
```
| Field | Default | Behavior |
|-------|---------|----------|
| `block` | `air` | Block id or load key under `blocks/`; required by schema |
| `weight` | `1` (11000) | Relative selection weight when the containing palette has multiple entries |
| `data` | `{}` | Block-state property map such as `axis`, `waterlogged`, or `facing` |
| `backup` | `null` | Recursive fallback when the requested state cannot resolve; unresolved entries without a backup become air |
| `debug` | `false` | Logs the resolved state when general debug logging is enabled |
| `tileData` | `{}` | Block-entity data applied only when the resolved state supports a tile entity |
Files under `blocks/<key>.json` use the same shape and act as reusable block-state aliases. Refer to one with `"block": "<key>"`; the referencing entry's `data` properties override properties from the alias. Alias resolution can chain, but aliases must not form cycles.
`tileData` is read from the actual palette entry being placed. It does not inherit from a referenced alias file, so put block-entity data on the referencing entry. For spawners, legacy `data.entitySpawn` is converted into `tileData.SpawnData.entity.id` before placement. Invalid tile data or tile data on a non-tile block produces no tile payload.
### Overworld surface example
`biomes/ocean/shore/beach.json` layers:
```json
{
"layers": [
{
"minHeight": 1,
"maxHeight": 1,
"palette": [{ "block": "minecraft:grass_block" }]
},
{
"minHeight": 1,
"maxHeight": 3,
"palette": [
{ "block": "minecraft:dirt" },
{ "block": "minecraft:coarse_dirt" }
]
}
],
"wall": {
"palette": [
{ "block": "minecraft:stone" },
{ "block": "minecraft:andesite" }
]
}
}
```
## Decorators (`IrisDecorator`)
Biome field: `decorators` (array). Snippet type `decorator`. Evaluated per surface column (chance tested densely; description notes 256 surface hits per chunk). Dimension `decorate` must be true (default).
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `chance` | double 0..1 | `0.1` | Placement probability gate |
| `palette` | `IrisBlockData[]` | grass | Required blocks to place |
| `topPalette` | `IrisBlockData[]` | `[]` | Top of tall stacks (bamboo tip) |
| `topThreshold` | double 0.01..1 | `1` | Stack fraction where top palette begins |
| `style` | style | `STATIC` | Dispersion noise for chance |
| `variance` | style | `STATIC` | Multi-block palette scatter |
| `heightVariance` | style | `STATIC` | Stack height noise |
| `stackMin` / `stackMax` | int 1..2032 | `1` | Vertical stack height |
| `scaleStack` | boolean | `false` | Treat stack min/max as % of cave height |
| `absoluteMaxStack` | int | `30` | Cap when scaleStack is on |
| `partOf` | `IrisDecorationPart` | `NONE` | Surface context filter |
| `forcePlace` | boolean | `false` | Ignore surface block type rules |
| `forceBlock` | `IrisBlockData` | null | Force surface block (implies forcePlace) |
| `whitelist` / `blacklist` | `IrisBlockData[]` | null | Surface allow/deny lists |
| `slopeCondition` | `IrisSlopeClip` | default | Slope gate |
### Decoration parts (`IrisDecorationPart`)
| Value | Placement target |
|-------|------------------|
| `NONE` | Default surface |
| `SHORE_LINE` | Shore (sugar cane) |
| `SEA_SURFACE` | Water surface (lily pads) |
| `SEA_FLOOR` | Entire placement below sea level |
| `CEILING` | Cave/overhang ceilings |
Stacking: `stackMax > 1` enables multi-block height. Height is noise-fit between min/max (+1 in height resolver).
### Overworld decorator examples
Flower scatter (`biomes/ocean/shore/beach.json`):
```json
{
"chance": 0.2,
"variance": {
"style": "CELLULAR",
"zoom": 0.25,
"fracture": { "style": "SIMPLEX", "zoom": 0.2, "multiplier": 10 }
},
"style": {
"style": "SIMPLEX",
"zoom": 0.2,
"fracture": { "style": "STATIC", "multiplier": 8 }
},
"palette": [
{ "block": "minecraft:dandelion" },
{ "block": "minecraft:poppy" }
]
}
```
Shore cane:
```json
{
"partOf": "SHORE_LINE",
"chance": 0.18,
"stackMin": 1,
"stackMax": 4,
"palette": [{ "block": "minecraft:sugar_cane" }],
"style": { "style": "NOWHERE", "zoom": 0.65 }
}
```
Snippet `snippet/decorator/bush.json` — reusable bush with slope limit and air-weighted palette:
```json
{
"chance": 0.03,
"style": {
"style": "CLOVER_HERMITE",
"zoom": 0.52,
"exponent": 2.5,
"axialFracturing": true
},
"slopeCondition": { "maximumSlope": 5 },
"palette": [
{ "block": "minecraft:bush", "weight": 1 },
{ "block": "minecraft:air", "weight": 4 }
]
}
```
Cave ceiling (`biomes/carving/amethyst.json`): `"partOf": "CEILING"` with downward-facing amethyst buds.
## Deposits (`IrisDepositGenerator`)
Clump-based underground replacements. Defined at **dimension**, **region**, and **biome** (`deposits` arrays stack). Biome ore multipliers only affect deposits whose palette resolves as ore blocks.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `minHeight` / `maxHeight` | int 0..8192 | `1` / `75` | Absolute placement Y band |
| `minSize` / `maxSize` | int 0..8192 | `0` / `128` | Blocks per clump |
| `minPerChunk` / `maxPerChunk` | int 0..2048 | `0` / `3` | Clumps per chunk |
| `spawnChance` | double 0..1 | `1` | Chunk spawn gate |
| `perClumpSpawnChance` | double 0..1 | `1` | Per-clump gate |
| `palette` | `IrisBlockData[]` | required | Clump materials |
| `varience` | int 1..64 | `3` | Pre-baked clump shape count (spelling is code field name) |
| `replaceBedrock` | boolean | `false` | Allow replacing bedrock |
Clumps are deterministic objects up to 11³ bounding cube. Ore detection: any palette block with ore property. Biome fields:
| Field | Default | Notes |
|-------|---------|-------|
| `oreDepositFrequencyMultiplier` | `1` | Scales ore deposit frequency only |
| `oreDepositSizeMultiplier` | `1` | Scales ore clump size only |
### Deposit variants (`IrisDepositVariant`)
Remap ore block ids after placement. Order: biome rules first, then region, then dimension; first matching Y-band wins.
| Field | Type | Notes |
|-------|------|-------|
| `minHeight` / `maxHeight` | int | Inclusive world Y |
| `remap` | map string→string | source material id → replacement id |
Dimension also supports `hideOresForHiddenOre`: replace all generator ore with base stone/deepslate/netherrack for drop-control plugins.
### Dimension ores (`IrisOreGenerator`)
Separate from deposits. Dimension `ores` array:
| Field | Notes |
|-------|-------|
| `palette` | Ore material palette |
| `chanceStyle` | Noise gate style |
| `threshold` | Noise must be ≤ threshold to place (default 0.5) |
| `range` | Y range |
| `generateSurface` | true = surface ore pass, false = underground |
### Overworld deposit examples
Dimension stone blobs (`dimensions/overworld.json`):
```json
{
"deposits": [
{
"minHeight": 19,
"maxHeight": 390,
"minPerChunk": 1,
"maxPerChunk": 8,
"minSize": 25,
"maxSize": 25,
"palette": [{ "block": "minecraft:granite" }],
"varience": 2
}
]
}
```
Region iron/coal (`regions/temperate.json`):
```json
{
"deposits": [
{
"minHeight": 15,
"maxHeight": 410,
"minPerChunk": 4,
"maxPerChunk": 26,
"minSize": 3,
"maxSize": 8,
"palette": [
{ "block": "minecraft:iron_ore" },
{ "block": "minecraft:coal_ore" }
],
"varience": 4
}
]
}
```
Deepslate remap (`dimensions/overworld.json`):
```json
{
"depositVariants": [
{
"minHeight": -64,
"maxHeight": 0,
"remap": {
"minecraft:iron_ore": "minecraft:deepslate_iron_ore",
"minecraft:diamond_ore": "minecraft:deepslate_diamond_ore"
}
}
]
}
```
## Authoring workflows
### Surface
1. Define 13 `layers` from top soil to subsoil; leave stone to `rockPalette`.
2. Set `wall` for cliff biomes; set `seaLayers` for oceans.
3. Use `lockLayers` only for mesa stripes.
### Decorators
1. Start with low `chance` and `STATIC` style; switch to wispy styles for patches.
2. Use `partOf` for shore/sea/ceiling-only content.
3. For cactus/bamboo, set `stackMin`/`stackMax` and optional `topPalette`.
4. Extract repeated decorators into `snippet/decorator/*.json` and reference via pack snippets.
### Deposits
1. Put global stone/ore veins on the dimension.
2. Region/biome deposits add local minerals.
3. Use `depositVariants` for deepslate or mod ore remaps by Y.
4. Tune `varience` for clump shape diversity; keep sizes moderate for performance.
## Practical notes
- Decorators do not replace object placements or procedural trees; they are block scatter only.
- Empty decorator palette → no place. Explicit empty whitelist blocks all placement.
- Deposit `varience` is not `variance`; generators use `multiplicitive`, not `multiplicative`.
- Post-processing slabs/walls (`postProcessingSlabs`, `postProcessingWalls` on dimension) are separate from biome `slab`/`wall` palettes but use related surface logic.
@@ -0,0 +1,317 @@
# 17 - Trees, Fungi, Coral, Crystals, Formations, Ruins
Procedural objects are baked from JSON settings into deterministic block blobs and scattered like `.iob` placements. They live under biome or region `proceduralObjects` and do not require object files. Separate systems: sapling tree overrides (`IrisTree` on object placements) and dimension `treeSettings` for growth replacement mode.
Related: `13 - Biomes.md`, `12 - Regions.md`, `15 - Caves & Carving.md`, `16 - Surfaces, Decorators & Deposits.md`, `18 - Structures Overview.md`, `19 - Objects.md`, `20 - Object Placement.md`.
## Container (`IrisProceduralObjects`)
| Field | Type | Contents |
|-------|------|----------|
| `trees` | `IrisProceduralTree[]` | Procedural trees |
| `fungi` | `IrisFungus[]` | Mushrooms / shelf fungi |
| `coral` | `IrisCoral[]` | Underwater coral forms |
| `crystals` | `IrisCrystal[]` | Cave crystal clusters |
| `formations` | `IrisFormation[]` | Rock landmarks |
| `ruins` | `IrisRuin[]` | Crumbling man-made shapes |
Attach on biomes and regions:
```json
{
"proceduralObjects": {
"trees": [ { "...": "..." } ],
"crystals": [ { "...": "..." } ]
}
}
```
Shipping overworld content currently relies on `.iob` tree objects more than procedural trees; the fields and generators are fully available for pack authors.
## Shared placement fields
All procedural types implement placement via `asPlacement()``IrisObjectPlacement`-compatible settings:
| Field | Typical default | Notes |
|-------|-----------------|-------|
| `name` | type-specific | Log key / variant key prefix |
| `chance` | 0.020.4 | Per-chunk attempt probability |
| `density` | `1` | Attempts if chance passes |
| `variants` | 68 | Pre-baked shape count (1..64) |
| `seed` | `1337` | Deterministic bake seed |
| `mode` | `CENTER_HEIGHT` (ruins: `MIN_HEIGHT`) | Terrain anchor mode |
| `rotation` | object rotation | Placement rotation |
| `clamp` | height limits | Min/max place height |
| `carvingSupport` | `SURFACE_ONLY` (crystals: `CARVING_ONLY`) | Surface vs cave |
| `underwater` | type-specific | Coral default true |
| `translate` | zero | XYZ offset |
| `stiltSettings` / `vacuumSettings` | optional | For stilt/vacuum modes |
Variant objects load as keys like `procedural/<name>#<i>` (trees: `procedural/tree/<name>#<i>`). Same seed + settings always bake the same variants.
`CarvingMode`: `SURFACE_ONLY`, `CARVING_ONLY`, `ANYWHERE`.
Common `ObjectPlaceMode` values: `CENTER_HEIGHT`, `MAX_HEIGHT`, `MIN_HEIGHT`, `FAST_*` variants, `STILT`, `MIN_STILT`, `FAST_STILT`, `CENTER_STILT`, `ERODE_STILT`, `ORGANIC_STILT`, vacuum modes. Cave defaults often use organic/fast stilt to prevent floating.
## Procedural trees (`IrisProceduralTree`)
Baked by `ProceduralTreeGenerator`.
### Core materials and size
| Field | Default | Notes |
|-------|---------|-------|
| `trunk` / `trunkPalette` | oak log | Palette wins |
| `leaves` / `leavesPalette` | oak leaves | Palette wins; real leaves get decay distances when `plausible` |
| `plausible` | `true` | Vanilla leaf distance / non-persistent |
| `heightMin` / `heightMax` | 8 / 12 | Trunk height range across variants |
| `trunkWidth` | `1` | Base trunk thickness |
| `profile` | `OAK` | Named canopy silhouette |
### Profiles (`IrisTreeProfile`)
`OAK`, `BIRCH`, `SPRUCE`, `JUNGLE`, `ACACIA`, `DARK_OAK`, `DARK_OAK_FLAT`, `DARK_OAK_FLAT_WIDE`, `CHERRY`, `PALM`, `WILLOW`, `COLUMNAR`, `BUSH`, `MEGA_SPRUCE`.
### Trunk shaping
| Field | Default | Notes |
|-------|---------|-------|
| `trunkShape` | `CONSTANT` | `IrisTreeFunction` over trunk width by height |
| `shapeStart` / `shapeEnd` | `1` / `1` | Linear width multipliers |
| `shapeSteepness` | `5` | Sigmoid steepness |
| `shapeBase` | `2.718281828` | Logarithmic base |
| `shapePeriod` / `shapeAmplitude` | `1` / `0.2` | Sine width controls |
| `shapePeakOffset` / `shapeFloor` | `0.5` / `0.5` | Parabolic waist position and minimum fraction |
| `leanAzimuth` / `leanAngle` | `0` / `0` | Compass direction and degrees from vertical |
| `trunkCurve` / `curveSteepness` | `LINEAR` / `8` | Lean accumulation and sigmoid steepness |
| `leanAzimuthMode` | `CONSTANT` | How lean direction changes over height |
| `azimuthStart` / `azimuthEnd` | `0` / `0` | Linear azimuth endpoints |
| `azimuthTurns` | `1` | Full turns for `SPIRAL` |
| `azimuthAmplitude` / `azimuthPeriod` / `azimuthOffset` | `90` / `1` / `0` | Sine azimuth controls |
| `azimuthScale` | `1` | Noise azimuth scale |
| `azimuthWhorlCount` | `5` | Branches per ring for `WHORL` |
| `trunkForks` / `forkHeight` / `forkAngle` | `1` / `0.5` / `25` | Fork count, normalized split height, and outward angle |
| `secondaryTrunk` / `secondaryTrunkPalette` | null | Optional band material; palette wins |
| `secondaryTrunkStart` / `secondaryTrunkEnd` | `0.5` / `1` | Normalized secondary band bounds |
| `roots` / `rootStyle` | `true` / `BUTTRESS` | Enable roots and select `TAPROOT`, `BUTTRESS`, or `STILT` |
| `rootDepth` / `rootFlare` | `0` / `0` | Explicit values; zero uses automatic scaling |
`IrisTreeFunction`: `CONSTANT`, `LINEAR`, `SIGMOID`, `LOG`, `SINE`, `PARABOLIC`, `EXPONENTIAL`, `SQRT`, `STEP`, `BELL`, `EASE_IN_OUT`.
`IrisTreeAzimuthMode`: `CONSTANT`, `LINEAR`, `SPIRAL`, `SINE`, `NOISE`, `RANDOM`, `GOLDEN_ANGLE`, `ALTERNATING`, `WHORL`, `ZIGZAG`.
### Canopy (`IrisTreeCanopy`)
| Field | Default | Notes |
|-------|---------|-------|
| `startAngle` | `90` | Disc elevation (90 flat, lower = dome, higher = umbrella) |
| `squish` | `1` | Vertical scale |
| `mode` | `TRIMMED` | Leaf fill mode |
| `leafDensity` | `0.85` | For density/noise modes |
| `crownStretchX` / `crownStretchZ` | `1` | Ellipse crown |
| `layers` | `[]` | Explicit discs override profile |
| `branches` | null | Branch system drives canopy when set |
`IrisTreeLayer`: `yOffset`, `radius`.
`IrisTreeLeafMode`: `TRIMMED`, `FILLED`, `DENSITY`, `NOISE`, `HOLLOW`, `GRADIENT`, `CLUMPED`, `TATTERED`, `SPARSE`.
### Branches (`IrisTreeBranches`)
Probability functions (`IrisTreeBranchProbability`): `CONSTANT`, `LINEAR`, `SIGMOID`, `TOP_HEAVY`, `GAUSSIAN`, `NOISE`, `BOTTOM_HEAVY`, `PERIODIC`, `BAND`, `INVERSE_GAUSSIAN`, `EXPONENTIAL_DECAY`.
| Field | Default | Role |
|-------|---------|------|
| `probabilityFunction` | `TOP_HEAVY` | Branch chance curve over normalized trunk height |
| `probabilityConstant` | `0.5` | `CONSTANT` chance |
| `probabilityBase` / `probabilityCrown` | `0` / `1` | `LINEAR` endpoints |
| `probabilitySteepness` / `probabilityMidpoint` | `10` / `0.7` | `SIGMOID` controls |
| `probabilityExponent` | `2` | `TOP_HEAVY` exponent |
| `probabilityMean` / `probabilityStd` | `0.7` / `0.15` | Gaussian center and deviation |
| `probabilityScale` | `1` | Noise scale |
| `probabilityPeriods` | `5` | Periodic whorl count |
| `lengthFunction` | `LINEAR` | Branch-length curve over trunk height |
| `lengthBase` / `lengthCrown` | `1` / `4` | Linear endpoints |
| `lengthConstant` / `lengthMax` | `3` / `4` | Constant value and nonlinear maximum |
| `lengthSteepness` | `5` | Sigmoid length steepness |
| `azimuthMode` / `azimuth` | `RANDOM` / `0` | Direction selection and fixed direction for `CONSTANT` |
| `elevation` / `sag` | `0` / `0` | Initial elevation and catenary droop |
| `branchDepth` | `1` | Recursive branch levels, 06 |
| `leafStartUp` | `false` | Clamp primary branches so they do not droop below horizontal |
| `clusterRadius` / `clusterMode` / `clusterDensity` | `2` / `TRIMMED` / `0.85` | Primary tip leaf cluster |
| `subBranches` | `null` | Optional `IrisTreeSubBranches` configuration |
`subBranches` fields: `count` (`1`), `pitchDelta` (`0`), `yawDelta` (`45`), `lengthScale` (`0.5`), `sag` (`0`), `clusterRadius` (`1`), `clusterMode` (`TRIMMED`), and `clusterDensity` (`0.85`).
### Secondary leaves and accents
| Field | Notes |
|-------|-------|
| `secondaryLeaves` | Single accent block |
| `weightedSecondaryLeaves` | Weighted list (`block`, `weight`) |
| `secondaryLeavesPalette` | Wins over both |
| `secondaryLeafFraction` | Fraction replaced |
| `decorators` | `IrisTreeDecorator[]` post accents |
### Tree decorator (`IrisTreeDecorator`)
| Field | Default | Notes |
|-------|---------|-------|
| `target` | `BRANCH_TIP` | Placement locus |
| `block` / `palette` | required block | Palette wins |
| `chance` | `0.5` | Per candidate |
| `length` | `1` | Max hang for `CANOPY_HANG` |
| `axisAware` | `false` | Orient facing away from trunk |
Targets: `BRANCH_TIP`, `TRUNK_SURFACE`, `CANOPY_TOP`, `CANOPY_BOTTOM`, `TRUNK_BASE`, `LEAF_SURFACE`, `CANOPY_HANG`, `BRANCH_SURFACE`, `TRUNK_TOP`, `GROUND_SCATTER`.
### Minimal tree example
```json
{
"name": "oak-plains",
"chance": 0.35,
"density": 2,
"variants": 8,
"seed": 9001,
"trunk": "minecraft:oak_log",
"leaves": "minecraft:oak_leaves",
"profile": "OAK",
"heightMin": 7,
"heightMax": 11,
"plausible": true,
"roots": true,
"rootStyle": "BUTTRESS",
"canopy": {
"mode": "TRIMMED",
"startAngle": 88,
"squish": 0.9
}
}
```
## Sapling overrides (`IrisTree` + `IrisTreeSettings`) — not procedural trees
`IrisTree` on **object placements** (`IrisObjectPlacement.trees`) maps grown sapling types/sizes to that object:
| Field | Notes |
|-------|-------|
| `treeTypes` | Bukkit `TreeType` names |
| `anyTree` | Match any type |
| `sizes` | `IrisTreeSize` width×depth sapling footprints |
| `anySize` | Match any size |
Dimension `treeSettings`: `enabled`, `mode` (`FIRST` biome→region→dimension, or `ALL` pool). This is growth replacement, not worldgen scatter.
## Fungi (`IrisFungus`)
| Field | Default | Notes |
|-------|---------|-------|
| `stem` / `stemPalette` | mushroom_stem | Palette wins |
| `cap` / `capPalette` | red_mushroom_block | Palette wins |
| `stemHeightMin` / `Max` | 5 / 9 | Stem height |
| `stemWidth` | `1` | 13 column |
| `stemCurve` / `stemLeanAzimuth` / `stemWaveAmplitude` / `stemWavePeriods` | lean/wave | Organic stem |
| `capShape` | `DOME` | `DOME`, `FLAT`, `FUNNEL`, `CONICAL`, `FLAT_WIDE` |
| `capRadiusMin` / `Max` | 3 / 5 | Cap size |
| `capThickness` / `capSquish` / `capDroop` / `capOverhang` | shape controls | |
| `gillBlock` / `gillPalette` / `gillChance` | underside | |
| `spotBlock` / `spotPalette` / `spotChance` | top speckles | |
| `shelf` / `shelfRadius` | false / 3 | Sideways polypore mode |
## Coral (`IrisCoral`)
Defaults: `underwater: true`, `waterlogged: true`, `mode: CENTER_HEIGHT`.
| Field | Notes |
|-------|-------|
| `form` | `BRANCHING`, `FAN`, `BRAIN`, `PILLAR`, `TENDRIL` |
| `block` / `blockPalette` | Structure body |
| `tipBlock` / `tipPalette` / `tipChance` | Fans/pickles |
| `heightMin` / `heightMax` | Overall height |
| `spread` / `sway` | Horizontal reach / wobble |
| Branching-only | `branchCount`, `branchLength`, `branchElevation`, `branchAzimuth` (`GOLDEN_ANGLE`/`EVEN`/`RANDOM`), `subBranches`, `subBranchCount`, `subBranchScale`, `tipClusterRadius` |
| Brain-only | `brainRadius`, `brainRoughness` |
| Pillar-only | `pillarRadius` |
| Fan-only | `fanWidth` |
| Tendril-only | `tendrilCount` |
## Crystals (`IrisCrystal`)
Defaults: `carvingSupport: CARVING_ONLY` (cave-first).
| Field | Notes |
|-------|-------|
| `growthSurface` | `FLOOR`, `CEILING`, `WALL` (orients bake only) |
| `block` / `blockPalette` | Shard body |
| `tipBlock` / `tipPalette` / `tipChance` | Tip accents |
| `glow` / `glowBlock` | Auto glow tips if no tip block |
| `baseBlock` / `basePalette` / `baseRadius` / `baseNoise` | Budding base blob |
| `shardCountMin` / `Max` | Shard count |
| `shardLengthMin` / `Max` | Shard length |
| `shardBaseRadius` / `shardTaper` | Taper geometry |
| `spreadAngle` | Cone half-angle degrees |
| `distribution` | `RANDOM` or `GOLDEN_ANGLE` |
| `jitter` | Angular noise |
## Formations (`IrisFormation`)
Natural landmarks. Extra field `surfaceSupportBuffer` (default 3) for foundation solidity.
| Field | Notes |
|-------|-------|
| `form` | `SPIRE`, `HOODOO`, `ARCH`, `SEA_STACK`, `BOULDER`, `BASALT_COLUMN` |
| `block` / `blockPalette` | Body |
| `capBlock` / `capPalette` | Caprock |
| `strataPalette` / `strataThickness` | Horizontal bands |
| `heightMin` / `heightMax` | Height |
| `baseWidthMin` / `baseWidthMax` / `topWidth` | Radii |
| `profile` | `CONSTANT`, `LINEAR`, `TAPER`, `PARABOLIC`, `BULGE` |
| `profileWaist` / `profileWaistFloor` | Parabolic waist |
| `lean` / `leanAzimuth` | Tilt |
| `roughness` / `jitter` | Surface erosion |
| Hoodoo | `hoodooCapRadius`, `hoodooCapHeight` |
| Arch | `archSpan`, `archThickness` |
| Basalt | `basaltColumns`, `basaltColumnRadius`, `basaltHeightVariance` |
## Ruins (`IrisRuin`)
| Field | Default | Notes |
|-------|---------|-------|
| `form` | `PILLAR` | `PILLAR`, `WALL`, `ARCH`, `FLOOR_SLAB`, `RUBBLE` |
| `block` / `blockPalette` | cobblestone | Structure body |
| `heightMin` / `Max` | 4 / 9 | Height band |
| `widthMin` / `Max` | 1 / 3 | Footprint X |
| `lengthMin` / `Max` | 3 / 7 | Footprint Z |
| `weatheredBlock` / `weatheringPalette` | mossy_cobblestone | Weather remap |
| `mossiness` / `weatheringScale` | 0.45 / 1 | Weather intensity/scale |
| `erosion` / `erosionScale` | 0.25 / 1.5 | Missing-block noise |
| `buriedFraction` | 0.2 | Fraction sunk below surface |
| `accents` | `[]` | `IrisRuinDecorator` list |
### Ruin decorator (`IrisRuinDecorator`)
| Field | Default | Notes |
|-------|---------|-------|
| `target` | `TOP` | `TOP`, `SURFACE`, `BASE_SCATTER` |
| `block` / `palette` | required | Palette wins |
| `chance` | `0.4` | Per candidate |
| `scatterRadius` | `2` | BASE_SCATTER extent |
## Authoring workflow
1. Choose system: trees for forests, fungi for mushroom biomes, coral for warm oceans, crystals for cave biomes, formations for deserts/coasts, ruins for sparse land.
2. Set `chance`/`density` low first; raise after silhouette looks correct.
3. Keep `variants` 412; each variant is baked at first use/cache warm.
4. Use palettes for material mix; single `block` strings for simple packs.
5. Match `carvingSupport` to environment; crystals and cave props use `CARVING_ONLY`.
6. For cave props, prefer stilt place modes and profile `defaultObjectPlaceMode`.
7. Prefer procedural systems for infinite variety; use `.iob` objects when you need hand-authored geometry (overworld trees mostly use objects today).
## Practical notes
- Procedural content is separate from `objects` placements and from structure jigsaws.
- `plausible: true` on trees enables natural leaf decay; false forces persistent leaves.
- Formation `surfaceSupportBuffer` interacts with dimension `objectSurfaceSupportBuffer` (max of requirements).
- Do not confuse `IrisTree` (sapling→object) with `IrisProceduralTree` (worldgen bake).
- Empty variant bake (invalid sizes) skips that entry; check logs if nothing places.
+65
View File
@@ -0,0 +1,65 @@
# 18 - Structures Overview
Iris places built content through three systems: single `.iob` objects, multi-piece Iris jigsaw assemblies, and native (vanilla/datapack/mod) structures. They share pack folders and some placement JSON, but each system has different fidelity, terrain tools, and commands. This page chooses the system; the linked guides are the field and workflow references.
Command listings assume the Bukkit/Paper plugin. Fabric/Forge/NeoForge expose a reduced command surface.
| Guide | Covers |
|---|---|
| `19 - Objects.md` | `.iob` format, wand/studio authoring, `.schem` import, editing |
| `20 - Object Placement.md` | Biome/region `objects[]`: chance, density, modes, loot, surface support |
| `21 - Jigsaw Structures.md` | Iris multi-piece structures: pieces, pools, connectors, grids |
| `22 - Native Structures & Datapacks.md` | Vanilla/datapack structures, ingest, adjustments, `nativeStructures` |
## The three systems
**1. Objects** (`objects/*.iob` + `objects[]` on a biome or region).
A single build stamped by chance and density: trees, rocks, ruins, camps. Object placements have the richest terrain-fitting tools (stilts, vacuum, paint, cave anchors, snow, edit, loot, markers). Dimensions do not carry `objects[]`.
**2. Iris jigsaw structures** (`structures/`, `jigsaw-pools/`, `jigsaw-pieces/` + `structures[]` placement).
Multi-piece assemblies in the style of villages: pieces connect through named connectors drawn from weighted pools, on a deterministic placement grid. Every piece is an Iris object, so the assembly is fully editable.
**3. Native structures** (vanilla, datapack, and mod structures controlled from the dimension file).
Generated by Minecraft's machinery with full native fidelity (processors, entities, spawners, loot). Iris can disable them, adjust them, ingest datapacks, and place specific registered keys on Iris grids.
## Which system
| Goal | Use |
|---|---|
| Scatter a build across a biome (trees, rocks, ruins, clutter) | Object + `objects[]` placement |
| Pack loot tables on chests | Object placement `loot` / `vanillaLoot` |
| Procedural multi-room structure you fully author | Iris jigsaw structure |
| Move/stilt/encase vanilla structures for Iris terrain | `importedStructures.adjustments` |
| Remove vanilla villages or other families | `importedStructures.disabled` |
| Datapack structures generating natively | `datapackImports` + ingest |
| Datapack structures only where you choose | Disable namespace + `nativeStructures` placement |
| Replace a vanilla structure with Iris-positioned native starts | Dimension placement with `nativeSuppression: REPLACE_SOURCE` |
| Edit vanilla/datapack blocks, pieces, or pools | `/iris structure import`, then edit Iris copies |
| Builds from vanilla structure blocks | Ship `.nbt` in a datapack, or import as objects — see `22 - Native Structures & Datapacks.md` |
Systems compose. Shipping packs commonly use objects for decoration, Iris or imported jigsaws for multi-piece content, and native placements with suppression for selected vanilla keys.
## How the pieces relate
```
build in world ──wand──> object (.iob) ──objects[] placement──> generates in biome/region
└──jigsaw-piece JSON──> pool ──> structure ──structures[] placement──> assembled in world
registered structure (vanilla / datapack / mod)
│ generates natively, controlled by importedStructures (disabled / adjustments)
├──nativeStructures placement──> vanilla machinery at Iris-chosen points
└──/iris structure import──> objects + pieces + pools + structure (editable Iris copies)
```
## Shared rules
**Command shape.** Director optional parameters are always `key=value` (`/iris structure verify overworld radius=200`, never a bare trailing number).
**Keys are relative paths.** Pack resources are keyed by path under their folder without extension: `objects/trees/oak/big1.iob` → object `trees/oak/big1`; `jigsaw-pools/fort/starts.json` → pool `fort/starts`.
**Worlds snapshot the pack.** On world create, the pack is copied to `<world>/iris/pack` and that copy is authoritative. Editing `plugins/Iris/packs/<pack>` does not change an existing world until `/iris developer update-world world=<world> pack=<dimension> confirm=true` (all arguments keyed) and a restart. Studio worlds read the live pack and hotload JSON/object edits into newly generated chunks about once per second.
**Only new chunks change.** Placement and structure-control edits affect chunks generated after the config existed.
**Validate before shipping.** On Bukkit, `/iris pack validate pack=<pack>` runs full pack validation, including jigsaw-graph and structure placement checks. Modded uses `/iris pack validate <pack>`.
+218
View File
@@ -0,0 +1,218 @@
# 19 - Objects
An Iris object is a sparse voxel volume (block states plus block-entity data) stored as `.iob` under a pack's `objects/` folder. This guide covers creating, importing, and editing objects. Generation wiring is `20 - Object Placement.md`; jigsaw pieces are `21 - Jigsaw Structures.md`.
## 1. What an object is
An object stores bounding box (`w × h × d`), a sparse block map, and a sparse tile-data map. Origin is always the **center** of the bounding box (`w/2, h/2, d/2`, integer division) — derived from dimensions, never stored, recomputed on load.
Stored:
- Every block except plain `minecraft:air`. `cave_air` and `void_air` **are** stored (use them to author carve-outs that clear terrain when placed).
- Full block-entity NBT for tiles: chest inventories, sign text, spawners, furnaces, banners, decorated pots. A chest with a vanilla loot table keeps the table key and loot seed.
Not stored:
- **Entities** (armor stands, item frames, paintings, mobs) — dropped on save. Runtime entity spawns use placement markers (`20 - Object Placement.md`).
- **Biomes.**
- Jigsaw blocks, structure blocks, and structure voids — stripped when the `.iob` is **read**. Iris jigsaw connectors are JSON metadata, not blocks (`21 - Jigsaw Structures.md`).
### Format limits
`.iob` V2 uses short-typed centered coordinates (±32,767 per axis) and a short-counted palette capped at 32,767 distinct block states. Pre-V2 files still load via a legacy reader. No explicit size cap on save; practical limits are memory and wand scan budget (~30 ms/tick).
### Where objects live
```
plugins/Iris/packs/<pack>/objects/**/*.iob
```
Object key = path relative to `objects/`, `/`-separated, no extension:
```
objects/light.iob -> "light"
objects/trees/oak/big_oak_1.iob -> "trees/oak/big_oak_1"
```
Keys resolve within the current pack. Outside an Iris world, lookups scan every visible pack — bare keys shared across packs are ambiguous; use distinctive subfolder paths.
## 2. Making objects in-game
### 2.1 Build surfaces
- **Pack studio** — world generated from your pack: `/iris studio open <dimension> [seed=1337]`. Close with `/iris studio close`.
- **Object studio** — flat gallery with every object in the pack on a grid and click-to-save editing (2.6): `/iris object studio [dimension=<dim>] [seed=1337]`. Omitting `dimension` aggregates objects from every visible pack.
You can also build in any flat world. `/iris object save` resolves `dimension` automatically inside an Iris world; outside one, pass `dimension=<pack>`.
### 2.2 The wand
```
/iris object wand
```
Blaze Rod named "Wand of Iris". Selection corners live on the item; dropping or replacing the wand loses the selection. Two wands are two independent selections.
| Action | Effect |
|---|---|
| **Left click** a block | Set corner 1 |
| **Right click** a block | Set corner 2 |
Main hand only; clicks cancel so you do not break or place. Selection particles draw when corners are within 64 blocks of each other. Setting a corner in a different world clears the other corner.
**WorldEdit interop.** With `worldEditWandCUI` true (default in settings), a WorldEdit selection is accepted anywhere Iris wants a selection. `/iris object we` converts the current WorldEdit selection into an Iris wand. `position2` does not work with a WorldEdit-only selection — run `/iris object we` first.
### 2.3 Adjusting the selection
| Command | Alias | Effect |
|---|---|---|
| `/iris object position1 [here=true]` | `p1` | Corner to feet (`here=true`) or look-at block (`here=false`) |
| `/iris object position2 [here=true]` | `p2` | Other corner |
| `/iris object shift [amount=1]` | | Move whole selection opposite facing |
| `/iris object contract [amount=1]` | `-` | Inset on the face you look at |
| `/iris object expand [amount]` | modded only | Expand the face you look at |
| `/iris object x&y` | | Expand up **and** down until clear, then contract sides inward |
| `/iris object x+y` | | Expand up only, then contract sides |
`x&y` / `x+y` wrap a build tightly: rough-select the base, then run one of them.
### 2.4 Saving
```
/iris object save [dimension=<pack>] <name> [overwrite=false] [legacy=true]
```
- `name` is required and positional — path under `objects/`, `/` allowed for subfolders.
- `dimension` resolves from the Iris world; pass `dimension=<pack>` otherwise.
- Without `overwrite=true` (alias `force=true`), an existing file aborts. No backup on overwrite.
- Saved volume is the **full selection box** — no shrinkwrap. Deliberate air padding shifts the center.
File path: `plugins/Iris/packs/<dimension load key>/objects/<name>.iob`.
**Footgun:** the target pack folder is the **dimension's load key**, not necessarily the pack folder the dimension came from. A pack in `packs/mypack/` with `dimensions/overworld.json` saves objects into `packs/overworld/` if that load key differs. Keep the dimension JSON filename equal to the pack folder name.
Example:
```
/iris object wand
# left-click one corner, right-click the other
/iris object x+y
/iris object save trees/birch/tall_birch_1
# -> plugins/Iris/packs/<dimension load key>/objects/trees/birch/tall_birch_1.iob
```
### 2.5 Pasting, previewing, editing
```
/iris object paste <object> [edit=false] [rotate=0] [scale=1]
```
Pastes at the block you look at. `rotate` is Y degrees; `scale` rescales with tricubic interpolation (clamped for large objects). Pastes are undoable:
```
/iris object undo [amount=1] (alias: u — reverts pastes, not hand edits)
```
**Edit existing:** `paste` with `edit=true` hands a wand fitted to the pasted bounds; modify, then re-save with `overwrite=true`.
Inspection and maintenance:
- `/iris object analyze <object>` — dimensions, block count, top materials. Read-only.
- `/iris object shrink <object>` — shrinkwraps and **overwrites in place**, no confirmation. Re-centers; deliberate off-center padding shifts.
- `/iris object plausibilize <target> [dryrun=false] [reach=12]` — tree-specific: organic branch connections so leaves survive vanilla decay. `target` accepts a key, a `prefix/` ending in `/`, or a path.
- `/iris object dust` (alias `d`) — "Dust of Revealing". Right-click a block in an Iris world to highlight the placement that owns it.
- `/iris find object <object> [teleport=true]` (also `/iris goto object`) — teleports to the grid cell in object studio, or locates a generated instance in a normal Iris world.
### 2.6 Object studio: click-to-save
Inside `/iris object studio`, left- or right-clicking a block in a grid cell writes that cell back to its `.iob`:
- Saved volume is the cell's original bounding box (center preserved; no shrinkwrap).
- Content hash per cell; no-op if unchanged ("no changes").
- Aborts if any covering chunk is unloaded — walk closer and click again.
## 3. Importing `.schem` files
### 3.1 `/iris object convert`
```
/iris object convert
```
1. Drop `.schem` files into `plugins/Iris/convert/` (created on first use).
2. Run the command. Each `foo.schem` becomes `foo.iob` **in that folder**, shrinkwrapped.
3. Move the `.iob` into a pack yourself — convert never writes into a pack.
Support and losses:
- **Sponge Schematic v2 and v3 only.** MCEdit `.schematic`, `.litematic`, and Sponge v1 are rejected. No WorldEdit/FAWE required — Iris parses NBT itself.
- Block palette and indices only. **Block entities, entities, and biomes are not carried**: chests empty, signs blank, spawners default.
- Source `.schem` is **deleted** after successful conversion.
- Empty folder or wrong extension produces no output.
### 3.2 High-fidelity path: paste, then wand
For schematics with chests, signs, or spawners:
1. Open a build world (`/iris studio open <pack>` or flat world).
2. `//schem load <name>` and `//paste` with WorldEdit/FAWE.
3. Keep the WorldEdit selection (accepted directly) or `/iris object we`, or select with the wand.
4. Tighten with `/iris object x+y` or `contract`.
5. `/iris object save <name> [overwrite=true]`.
Wand save reads live blocks with full block-entity NBT. This is the only import route that preserves block entities.
### 3.3 Vanilla `.nbt` templates
- `/iris structure import <dimension>` — imports registered structures and templates into the pack as objects plus jigsaw graphs.
- `/iris studio importvanilla <dimension> [variants=3] [structures=true]` — imports vanilla trees/features under `objects/vanilla/`, plus structure passes when `structures=true`.
Details: `22 - Native Structures & Datapacks.md`.
## 4. Practical details
**Center and anchoring.** Center is always bounding-box middle. Wand saves keep selection box (asymmetric padding survives); `convert` and `shrink` shrinkwrap and re-center. At placement, the object is centered on the column in X/Z and lifted so its bottom sits on the anchor; beyond that, placement `mode` applies (`20 - Object Placement.md`).
**Rotation is runtime, never baked.** A `.iob` stores one orientation; rotation ranges live on the placement. `paste rotate=90` rotates a throwaway copy.
**Loot: two mechanisms.** A chest saved with a vanilla loot table keeps that table inside the `.iob`. Pack loot tables attach at placement (`loot`, `vanillaLoot`, `overrideGlobalLoot`) — `20 - Object Placement.md`.
**Stale caches.** Objects are cached per pack. Non-studio worlds may not pick up on-disk `.iob` edits until pack reload; studio worlds hotload within about a second (new chunks only).
## 5. Common failure modes
1. **"You need to hold your wand!"** — no wand selection and no WorldEdit selection.
2. **"File already exists."** — pass `overwrite=true`.
3. **Save says missing `dimension`** — not in a loaded Iris world; pass `dimension=<pack>`.
4. **Objects saved into the wrong pack folder** — dimension load key ≠ pack folder name (2.4).
5. **`convert` does nothing** — files not in `plugins/Iris/convert/` or not ending in `.schem`.
6. **Converted objects lack tile data** — converter never reads block entities; use paste-then-wand.
7. **Converter deleted the schematic** — by design after success; keep a copy elsewhere.
8. **Selection lost** — corners live on the wand item; `paste edit=true` overwrites the held wand selection.
9. **`position2` does nothing** — WorldEdit-only selection; run `/iris object we` first.
10. **Entities vanished** — never stored in objects.
11. **Jigsaw/structure-void blocks vanished** — stripped on read; connectors are JSON.
## Command reference
All under `/iris object` (alias `/iris o`). Optional parameters must be `key=value`.
| Command | Aliases | Parameters |
|---|---|---|
| `studio` | | `dimension=<dim>` (optional), `seed=1337` |
| `wand` | | |
| `we` | | |
| `position1` / `position2` | `p1` / `p2` | `here=true` |
| `x&y` / `x+y` | | |
| `shift` | | `amount=1` |
| `contract` | `-` | `amount=1` |
| `save` | | `dimension` (contextual), `<name>`, `overwrite=false` (alias `force`), `legacy=true` |
| `paste` | | `<object>`, `edit=false`, `rotate=0`, `scale=1` |
| `undo` | `u` | `amount=1` |
| `analyze` | | `<object>` |
| `shrink` | | `<object>` |
| `plausibilize` | | `<target>`, `dryrun=false`, `reach=12` |
| `convert` | | |
| `dust` | `d` | |
Related: `/iris studio open <dimension> [seed=]`, `/iris studio close`, `/iris object studio`, `/iris find object <object> [teleport=true]`.
+369
View File
@@ -0,0 +1,369 @@
# 20 - Object Placement
Object placements wire a saved object (`objects/<key>.iob`) into biome or region JSON so the generator stamps it. Creating objects is `19 - Objects.md`; multi-piece assemblies are `21 - Jigsaw Structures.md`.
## 1. Where placements go
`objects[]` exists on exactly two resource types:
- **Biome** (`biomes/**.json`) — where that biome generates.
- **Region** (`regions/**.json`) — every biome in the region.
**Dimensions do not have `objects[]`.** An `objects` array on a dimension is ignored at generation time. Dimensions carry surface-support knobs (`requireObjectSurfaceSupport`, `objectSurfaceSupportBuffer`, section 7) and upper-dimension object controls (`upperDimensionObjects`, `upperObjectsForcePlace`), but no placements of their own.
### Scope and frequency
Per chunk, biome and region are sampled **once at the chunk center** (block 8,8), then every placement entry in the biome list, then the region list, rolls independently.
- Biome vs region differ only in breadth; chance/density math is identical; both can fire in the same chunk.
- A biome that only occupies a corner contributes nothing to that chunk; the center biome's objects can spill slightly into neighbors.
- Each entry belongs to the surface list, the cave list, or both via `carvingSupport` (`SURFACE_ONLY` default, `CARVING_ONLY`, `ANYWHERE`). `ANYWHERE` is in both lists and rolls chance twice per chunk. Cave placements probe for a cave biome below the surface and fall back to the surface biome.
### Minimal entry
```json
{
"objects": [
{
"place": ["clutter/boulder1"],
"chance": 0.05
}
]
}
```
`place` entries are object keys (path under `objects/` without `.iob`). A whole placement can also be a snippet reference (`"snippet/object-placer/<name>"`).
## 2. Frequency: `chance`, `density`, `densityStyle`
| Field | Default | Meaning |
|---|---|---|
| `chance` | `1` | Rolled **once per chunk per entry** (0..1). Surface entries add ±0.005 jitter, so use omission — not `chance: 0` — to disable a surface entry. Cave entries use the raw chance (no jitter). |
| `density` | `1` | Placement **attempts** in the chunk once chance passes. Each attempt picks a random object from `place` and a random column. Attempts can still be vetoed — density is not a guarantee. |
| `densityStyle` | unset | Noise-driven range that **replaces** `density` when present. Class defaults are min 16 / max 32, so an empty `densityStyle: {}` means 1632 attempts per chunk. |
Expected objects per chunk ≈ `chance × density`, before rejections. `chance: 0.002` is roughly one per 500 chunks.
**`place` has no weights** — uniform pick. To bias one object, list its key multiple times or split entries. An unresolvable key skips the attempt; `ObjectResourceLoader` logs a warning for each failed lookup.
## 3. Placement modes
`mode` (default `CENTER_HEIGHT`) decides how the object meets terrain. All 21 values:
**Height sampling**
| Mode | Meaning |
|---|---|
| `CENTER_HEIGHT` | One height sample at the center; whole object uses it. Default. |
| `MAX_HEIGHT` / `FAST_MAX_HEIGHT` | Highest sample across footprint (FAST = 4 samples). Never buried; floats off cliffs. |
| `MIN_HEIGHT` / `FAST_MIN_HEIGHT` | Lowest sample. Never overhangs; buries into slopes. |
| `PAINT` | Every column pushed to terrain surface — melts the object over terrain. Common for ground clutter. |
**Stilts** (extend bottom blocks down; tuned by `stiltSettings`)
| Mode | Meaning |
|---|---|
| `STILT` / `FAST_STILT` | `MAX_HEIGHT` + stilt columns (FAST cheaper, less accurate — combine with `overStilt`). |
| `MIN_STILT` / `FAST_MIN_STILT` | `MIN_HEIGHT` + stilts. |
| `CENTER_STILT` | `CENTER_HEIGHT` + stilts. Cheapest generally useful stilt mode. |
| `ERODE_STILT` | Cone-tapered stilts: center deepest, edges drop off, lower parts randomly broken. |
| `ORGANIC_STILT` | Scans down to first solid (cave floor or terrain); fills with object's bottom blocks, noise-varied. For cave floor connection. Tune with `organicMaxScan`, `organicJitter`, `organicScratch`. |
| `CEILING_HANG` | Flips object and anchors to cave ceiling with organic stilt into the roof. |
**Terrain shaping** (modify terrain; tuned by `vacuumSettings`)
| Mode | Meaning |
|---|---|
| `VACUUM` | Anchors like `CENTER_HEIGHT`, bends surrounding terrain flush with base (default radius 12, parabolic). |
| `VACUUM_HIGH` | Radius 20, finest blend. More expensive. |
| `VACUUM_FAST` | Radius 8, coarser sampling. |
| `VACUUM_ORGANIC` | Falloff radius perturbed per column. |
| `VACUUM_WAVY` | Bend modulated by simplex noise (`waveAmplitude`, `waveScale`). |
**Special**
| Mode | Meaning |
|---|---|
| `FLOATING` | Pure-air placement: Y from rotated object center height plus `translate.y` (+ `yRandom`). Skips terrain, water, and cave checks. Floating islands, sky structures. |
| `STRUCTURE_PIECE` | Raw stamp at caller coordinates. **Internal** for structure pieces. Do not author in `objects[]`. |
## 4. Field reference
### Anchoring and water
| Field | Default | Meaning |
|---|---|---|
| `carvingSupport` | `SURFACE_ONLY` | `SURFACE_ONLY` / `CARVING_ONLY` / `ANYWHERE`. |
| `caveAnchorMode` | `PROFILE_DEFAULT` | Cave: `FLOOR`, `CEILING`, `CENTER`, `ANY`, or carving profile default. |
| `heightmap` | unset | Noise generator used as a **virtual** heightmap instead of terrain. |
| `bottom` | `false` | With explicit Y (cave/structure/tree paths), place from ground up. Incompatible with X/Z rotation. |
| `fromBottom` | `false` | Place from world bottom up. Incomplete path — avoid. |
| `underwater` | `false` | Place on terrain height, ignoring water surface. Without it (or `onwater`), submerged surface placements are rejected. |
| `onwater` | `false` | Place on fluid surface (boats). |
| `waterloggable` | `false` | Waterlog placed blocks that sit in water. |
| `isDolphinTarget` | `false` | With `underwater`, marks placed storage chests as buried-treasure POI for dolphins. |
### Terrain interaction
| Field | Default | Meaning |
|---|---|---|
| `bore` | `false` | Clear bounding cuboid to air before placing. `boreExtendMaxY` / `boreExtendMinY` expand the box. |
| `smartBore` | `false` | Raytraced interior fill — clears rooms/pockets. Slows object loading, not warmed-up generation. |
| `meld` | `false` | Place only where blocks already exist. Expensive. |
| `warp` | flat | Generator style warping placement coordinates per block. |
| `snow` | `0` | 0..1 — snow layers on top of placed columns. |
| `edit` | `[]` | Find-and-replace at placement: `find[]`, `replace` (palette), `exact`, `chance`. |
### Collisions and force
| Field | Default | Meaning |
|---|---|---|
| `forbiddenCollisions` | `[]` | Object keys this object may not intersect; match inside bounding box rejects (unless also in `allowedCollisions`). |
| `allowedCollisions` | `[]` | Exceptions to the above. |
| `forcePlace` | `false` | JSON also accepts `"force"`. Bypasses slope, surface-support, underwater, clamp, bedrock, and collision guards. Does **not** bypass native-structure veto (objects never overwrite native structure pieces). |
### Slope
| Field | Default | Meaning |
|---|---|---|
| `slopeCondition` | `{minimumSlope: 0, maximumSlope: 10}` | Slope gate, 3-block radius. Defaults mean no condition; tighten `maximumSlope` (e.g. 2) to keep buildings off hills. |
| `rotateTowardsSlope` | `false` | Adds downhill direction (rounded to 90°) to Y rotation. |
### `rotation`
Default already enables Y free 90° steps — objects get random cardinal rotation with no config. Common pack form:
```json
{
"rotation": { "enabled": true, "yAxis": { "enabled": true, "min": 0, "max": 270, "interval": 90 } }
}
```
Per-axis (`xAxis` / `yAxis` / `zAxis`, each `{enabled, min, max, interval}`):
- `min == max == 0` — any multiple of `interval` (free spin).
- `min == max != 0` — locked to that angle.
- otherwise — multiple of `interval` clipped into `[min, max]`.
- `interval: 0` fully free only in free-spin case; in a clipped range always set non-zero `interval`. Non-90° intervals usually look bad at block resolution.
Disable with `"rotation": {"enabled": false}`. X/Z rotation is incompatible with `bottom: true`.
### `translate` and `scale`
```json
{
"translate": { "x": 0, "y": -1, "z": 0, "yRandom": 0 }
}
```
`translate.y: -1` seats clutter into the ground. Translate rotates with the object. `yRandom` adds `rand(0..yRandom)` height per placement.
```json
{
"scale": { "size": 1, "minimumScale": 0.75, "maximumScale": 1.25, "variations": 7, "interpolation": "TRILINEAR" }
}
```
Defaults: `size`, `minimumScale`, `maximumScale` all `1`; `interpolation` `NONE` — inert until you set `size` or widen min/max. `size != 1` is a fixed multiplier and overrides the range; at `size: 1` a random scale is picked from the range, quantized into `variations` (default 7) cached variants. Upscale interpolators: `NONE`, `TRILINEAR`, `TRICUBIC`, `TRIHERMITE`.
### `clamp`
```json
{
"clamp": { "minimumHeight": 40, "maximumHeight": 225 }
}
```
Rejects placements whose resolved bottom/top fall outside the band. Field defaults `-2048` / `2048`. Heights are engine-internal Y (0..dimension height), not necessarily world Y.
### `stiltSettings` (all `*_STILT` modes and `CEILING_HANG`)
| Field | Default | Meaning |
|---|---|---|
| `yMax` | `0` | Max stilt height before overstilt/random range. |
| `yRand` | `0` | Extra random stilt depth. |
| `overStilt` | `0` | Extra depth into ground (useful with FAST_STILT). |
| `palette` | unset | Column palette; default repeats object's bottom block (grass-family bottoms substitute dirt). Stops at first fluid. |
| `organicMaxScan` | `48` | ORGANIC_STILT / CEILING_HANG max scan. |
| `organicJitter` | `3` | Random per-column shortening. |
| `organicScratch` | `0.55` | Fraction of deepest part randomly broken. |
### `vacuumSettings` (all `VACUUM*` modes)
| Field | Default | Meaning |
|---|---|---|
| `radius` | `0` (auto: 12 / 20 high / 8 fast) | Deformation extent past footprint. |
| `falloff` | `2.0` | Easing: 1 = cone, 2 = parabolic, higher = flatter near object. |
| `organicJitter` | `4` | VACUUM_ORGANIC radius perturbation. |
| `waveAmplitude` | `3` | VACUUM_WAVY wave height. |
| `waveScale` | `5.0` | VACUUM_WAVY frequency (~100/waveScale blocks wavelength). |
## 5. Loot
Two arrays, matched against container blocks inside the placed object. Loot injects lazily when a chest is first opened, from the placement recorded in the mantle — **only storage chests** receive it.
```json
{
"loot": [
{ "name": "global-treasure", "filter": [{ "block": "minecraft:chest" }], "exact": false, "weight": 3 },
{ "name": "global-tools", "weight": 1 }
],
"vanillaLoot": [
{ "name": "minecraft:chests/simple_dungeon", "weight": 1 }
],
"overrideGlobalLoot": false
}
```
- `loot[].name` is a pack `loot/` key; `vanillaLoot[].name` is a vanilla or datapack loot-table key.
- Per chest: `exact: true` full block-data match beats material match beats no `filter`. One table picked by `weight`.
- `overrideGlobalLoot: true` suppresses dimension/region/biome loot for containers this placement matched.
- Unresolvable loot name logs `Couldn't find loot table <name>`.
Independent of this, a chest saved into the `.iob` with a vanilla loot table already on it keeps that table (`19 - Objects.md`).
## 6. Markers: entity spawns on placed objects
Placements have no direct entity field. `markers[]` tags matching blocks with a marker resource; the marker (`markers/`) carries `spawners[]`:
```json
{
"markers": [
{ "mark": [{ "block": "minecraft:mossy_cobblestone" }], "marker": "camp-spawns", "maximumMarkers": 4, "exact": false }
]
}
```
`markers/camp-spawns.json` references spawner resources; `emptyAbove` (default true) requires two air blocks above the marked block.
## 7. Surface support
Iris refuses surface objects that roof over, bridge, or overhang a carved opening. It rasterizes the object's lowest solid layer, dilates by `surfaceSupportBuffer`, and requires every column in that stencil to have `surfaceSupportDepth` blocks of un-carved, surface-solid ground. Failure rejects **with no log line** — common cause of "object never appears" near caves and canyon rims.
| Field | Where | Default | Meaning |
|---|---|---|---|
| `requireSurfaceSupport` | placement | `true` | Guard off for this placement. |
| `surfaceSupportBuffer` | placement | `2` (0..16) | Extra solid ring around footprint. |
| `surfaceSupportDepth` | placement | `2` (1..16) | Required un-carved ground thickness. |
| `requireObjectSurfaceSupport` | dimension | `true` | `false` disables guard pack-wide. |
| `objectSurfaceSupportBuffer` | dimension | `2` | Floor for every placement's buffer — can widen, never narrow. |
Skipped for: `force`, `fromBottom`, `mode: FLOATING`, `STRUCTURE_PIECE`, `underwater`, `onwater`, cave-anchored placements, and `requireSurfaceSupport: false`.
Escape hatches: `surfaceSupportBuffer: 0``surfaceSupportDepth: 1``requireSurfaceSupport: false``force: true` → dimension-wide off.
A related guard rejects surface placements resolving to y ≤ 1 in bedrock dimensions and **does** log (throttled): `Implausible object placement rejected`.
## 8. Worked examples
Rare surface camp:
```json
{
"place": ["clutter/camp1"],
"chance": 0.00175,
"rotation": { "enabled": true, "yAxis": { "enabled": true, "min": 0, "max": 270, "interval": 90 } }
}
```
Trees with slope gate, warp, snow:
```json
{
"place": ["trees/spruce/pine1", "trees/spruce/pine2", "trees/spruce/pine3"],
"chance": 0.6,
"density": 1,
"slopeCondition": { "maximumSlope": 2 },
"rotation": { "enabled": true, "yAxis": { "enabled": true, "interval": 90, "min": 0, "max": 270 } },
"warp": { "style": "IRIS_DOUBLE", "exponent": 1.2, "zoom": 0.4 },
"snow": 0.1
}
```
Ruins on stilts with loot:
```json
{
"place": ["structures/ruin-small-a", "structures/ruin-small-b"],
"chance": 0.08,
"density": 2,
"mode": "CENTER_STILT",
"stiltSettings": { "yMax": 4, "yRand": 1, "overStilt": 1 },
"translate": { "y": -1 },
"slopeCondition": { "maximumSlope": 3 },
"loot": [
{ "name": "global-treasure", "filter": [{ "block": "minecraft:chest" }], "weight": 2 },
{ "name": "global-clutter", "weight": 1 }
]
}
```
Cave floor clutter:
```json
{
"chance": 0.21,
"density": 6,
"carvingSupport": "CARVING_ONLY",
"caveAnchorMode": "PROFILE_DEFAULT",
"translate": { "y": -1 },
"place": ["clutter/stoneclutt4", "clutter/stoneclutt5"],
"bottom": true,
"force": true,
"scale": { "size": 0.75 }
}
```
Ground-hugging carpet:
```json
{
"chance": 0.15,
"density": 2,
"mode": "PAINT",
"translate": { "y": -1 },
"rotation": { "enabled": true, "yAxis": { "enabled": true, "min": 0, "max": 270, "interval": 90 } },
"place": ["trees/mushroom/mushclut1", "trees/mushroom/mushclut2"]
}
```
## 9. Trees
When a sapling grows, Iris scans the biome's (then region's) `objects[]` for placements whose `trees[]` matches the grown tree type and sapling square size, picks one, and stamps it. A placement can serve both generation and sapling override. Procedural tree generation (`proceduralObjects`) is separate.
## 10. Troubleshooting
**Never appears**
1. Wrong key — `place` paths are case-sensitive. Missing keys skip the placement attempt and log a loader warning.
2. Chance too low — prove wiring with `"chance": 1, "density": 4`, then dial back.
3. Not actually in that biome — chunk-center sample decides.
4. Wrong list — `CARVING_ONLY` never places on surface; `SURFACE_ONLY` (default) never places in caves.
5. Surface support rejection (section 7) — if it appears with `force: true` but not without, a guard is the cause.
6. Underwater — submerged surface placement needs `underwater` / `onwater`.
7. `clamp` too tight — internal Y, not world Y.
8. `slopeCondition` too strict — `maximumSlope` below ~1 excludes most terrain.
9. Native structure veto — objects never place into native structure pieces, even with `force`.
10. Console: `Implausible object placement rejected` (bedrock y≤1); loot failures: `Couldn't find loot table`.
**Floats or clips**
- Floats off cliffs → `MAX_HEIGHT`; use a stilt mode or `VACUUM`.
- Buried → `MIN_HEIGHT` or too much negative `translate.y`.
- Floating in a cave → `ORGANIC_STILT` (floor) or `CEILING_HANG` (roof) with `carvingSupport: "CARVING_ONLY"`.
- Flat stilt disc → `VACUUM` / `VACUUM_ORGANIC` / `VACUUM_WAVY`.
- Rides on grass → `translate.y: -1` or `PAINT`.
- Interior filled with terrain → `bore` or `smartBore`.
**Loot not filling**
- Only storage chests receive placement loot.
- Console: `Couldn't find loot table`.
- `exact: true` with mismatched block data — drop to `exact: false`.
**Iterating quickly**
- Studio worlds hotload pack edits (JSON and `.iob`) within about a second — newly generated chunks only.
- Non-studio worlds do not hotload.
- Separate object vs placement: `/iris object paste` first.
- Deep forensics: write `chunkX,chunkZ[,radius]` into `plugins/Iris/goldendebug.txt` — logs every attempt, pick, and rejection for those chunks. Extremely verbose; use radius 0.
+309
View File
@@ -0,0 +1,309 @@
# 21 - Jigsaw Structures
Iris multi-piece structures: pieces connect through named connectors drawn from weighted pools, assembled deterministically per start chunk. Every piece is an Iris object (`.iob`). Authoring pieces, pools, and structures and placing them is covered here. Vanilla/datapack structures and import are `22 - Native Structures & Datapacks.md`.
## 0. Resource model
| Resource | Pack folder | Role |
|---|---|---|
| Structure | `structures/` | Entry point: start pool and assembly caps |
| Jigsaw pool (`IrisJigsawPool`) | `jigsaw-pools/` | Weighted pieces + one fallback pool |
| Jigsaw piece (`IrisJigsawPiece`) | `jigsaw-pieces/` | One object + connectors |
| Connector | inline in a piece | Position, facing, target pool, name |
Keys are file paths under the folder minus `.json`. A structure attaches to the world via an `IrisStructurePlacement` in `structures[]` on a biome, region, or dimension — placement is separate from the structure resource.
Older packs may still have a `jigsaw-structures/` folder. Nothing reads it; live folders are the three above.
## 1. Resources
### 1.1 `structures/<key>.json`
| Field | Default | Meaning |
|---|---|---|
| `startPool` | required | Pool the assembler draws the start piece from. |
| `maxDepth` | `7` (1..30) | Maximum recursion depth. |
| `maxSizeChunks` | `8` (1..32) | Hard radius in chunks around start: candidate whose box leaves `maxSizeChunks * 16` blocks from start origin is rejected. Y is not bounded by it. |
| `placeMode` | `STRUCTURE_PIECE` | Object place mode when stamping pieces. Other modes change structure-piece anchoring (3.6). |
| `edit` | `[]` | Find-and-replace on every piece (same syntax as object placements). |
| `loot` | `[]` | Loot-table keys applied to piece containers (each weight 1, non-overriding). |
| `vanillaSource` | `""` | Provenance key from import. Locate alias for `/iris goto structure` and `verify`. Empty for hand-authored structures. |
No terrain adaptation on the structure — terrain lives on the placement.
Example (imported graph shape):
```json
{
"startPool": "minecraft_village_plains/pool/minecraft_village_plains_town_centers",
"maxDepth": 6,
"maxSizeChunks": 8,
"placeMode": "STRUCTURE_PIECE",
"vanillaSource": "minecraft:village_plains"
}
```
### 1.2 `jigsaw-pools/<key>.json` (`IrisJigsawPool`)
| Field | Default | Meaning |
|---|---|---|
| `pieces` | min 1 entry | Weighted entries: `{ "piece": "<key>", "weight": 1 }` or `{ "empty": true, "weight": 3 }`. |
| `fallback` | `""` | Pool tried after this one — and used **alone** once `maxDepth` is reached. Exactly one level deep: a fallback's own fallback is never consulted. Empty = stop expanding at max depth. |
An `empty: true` entry terminates the branch without placing. Both `empty` and `piece` in one entry is a validation error; weights must be ≥ 1; fallback cycles are blocked.
```json
{
"pieces": [
{ "piece": "mypack/watchtower/corridor", "weight": 5 },
{ "piece": "mypack/watchtower/corridor_short", "weight": 2 },
{ "weight": 3, "empty": true }
],
"fallback": "mypack/watchtower/pool/terminators"
}
```
### 1.3 `jigsaw-pieces/<key>.json` (`IrisJigsawPiece`)
| Field | Default | Meaning |
|---|---|---|
| `object` | required | Object (`objects/<key>.iob`) for this piece. |
| `connectors` | optional | Connection points. For a terminal cap, **omit the key** — runtime and pack validation accept an empty list; generated VSCode schema requires at least one entry when the key is present. |
| `rotatable` | `true` | Whether the assembler may Y-rotate the piece. |
### 1.4 Connectors
| Field | Default | Meaning |
|---|---|---|
| `position` | required | Block coordinate **inside the object**, 0-based from lowest corner: `0..W-1 / 0..H-1 / 0..D-1`. |
| `direction` | required | Face this connector points out of. |
| `top` | `UP_POSITIVE_Y` | Authored "up" for `ALIGNED` roll lock. Write it explicitly. |
| `pool` | required | Pool for the connecting piece. |
| `name` | `""` | Identity this connector **exposes**. |
| `targetName` | `""` | `name` this connector wants on the other piece. |
| `joint` | `ROLLABLE` | `ROLLABLE` (free roll) or `ALIGNED` (roll locked — doorways/streets). |
`direction` / `top` values:
```
UP_POSITIVE_Y DOWN_NEGATIVE_Y NORTH_NEGATIVE_Z SOUTH_POSITIVE_Z EAST_POSITIVE_X WEST_NEGATIVE_X
```
Unlike vanilla jigsaw blocks:
- Connectors are **JSON metadata, not blocks**. The block at the connector cell stays — no `final_state`. Jigsaw blocks inside an object are stripped on load.
- Matching is target-to-name: candidate attaches when its `name` equals the source's `targetName`, directions oppose after trial rotation, and for `ALIGNED` sources rotated `top` matches. Names are exact, case-sensitive.
## 2. Assembly
Constants: hard piece cap **512**, max depth **30**, max size **32 chunks**.
1. **Start.** Weighted-pick from `startPool` (`empty` → nothing), random cardinal rotation if `rotatable`, place at origin, queue connectors at depth 0.
2. **BFS.** Pop open connector; resolve its `pool`. Below `maxDepth` try primary then direct fallback; at `maxDepth` only fallback.
3. **Candidates** in weighted-random order. For each piece, test connectors (name match, opposed direction) across rotations — `{0,90,180,270}` shuffled for `ROLLABLE`, fixed for `ALIGNED`, `{0}` when `rotatable: false`.
4. **Geometry.** New piece positioned so connector cells are adjacent along source facing.
5. **Rejection.** Discard if box leaves `maxSizeChunks * 16` radius or intersects a placed piece (boxes may touch, not overlap).
6. **Success** places the piece and queues remaining connectors at depth + 1.
**Authoring rule:** a connector at depth **below** `maxDepth` that cannot be satisfied drops the **whole** assembly for that chunk, silently, with no retry. At `maxDepth` and beyond, unsatisfied connectors are tolerated.
Give every pool a terminating option — `empty: true` or a `fallback` of connector-less caps.
Loud failures (missing pool/piece/object, malformed connector, non-positive weight, 512-piece cap with connectors still open) indicate a broken graph and are caught by pack validation.
**Determinism.** Assembly is a pure function of `(mantle seed, chunk X, chunk Z, placement identity)`.
## 3. Placement
`structures[]` on a **biome**, **region**, or **dimension**. The same array hosts native placements (`nativeStructures``22 - Native Structures & Datapacks.md`); each placement must declare exactly one of `structures` / `nativeStructures`.
### 3.1 Fields (Iris backend)
| Field | Default | Meaning |
|---|---|---|
| `structures` | `[]` | Iris structure keys. One picked uniformly per start chunk. Duplicates are a validation error. |
| `placementId` | `""` | Stable identity. Empty derives identity from content — reordering does not move structures; changing settings does. Set when retuning spacing/heights without re-rolling positions, or when two placements would otherwise be identical. |
| `distribution` | `RANDOM_SPREAD` | `RANDOM_SPREAD` / `DENSITY` / `CONCENTRIC_RINGS`. |
| `spacing` | `32` (1..4096) | RANDOM_SPREAD grid cell size in chunks. |
| `separation` | `8` | RANDOM_SPREAD minimum chunk separation; must be **smaller than** `spacing`. |
| `salt` | `165745296` | Mixed into placement RNG. |
| `density` | `0.02` (0..1) | DENSITY per-chunk start probability. |
| `ringCount` / `ringDistance` / `ringSpread` | `128` / `32` / `3` | CONCENTRIC_RINGS around origin. |
| `minHeight` / `maxHeight` | `-2032` / `2032` | Surface: pass/fail gate on surface Y. Underground: Y band. |
| `underground` | `false` | Start at deterministic random Y in band, then shift down under terrain across footprint. |
| `underwater` | `false` | If false, submerged origins skipped. |
| `terrain` | `{mode: SOURCE}` | Iris backend: `SOURCE`, `PRESERVE`, `BORE`, `FORCE_CARVE`. **`VACUUM` and `ENCASE` are rejected** for Iris assemblies (native only). |
| `stilt` | unset | Foundation columns under assembly bottom cells: `maxDepth` (default 64), `palette` (default cobblestone), `supportNonOccluding`. `spacing` is honored on the **native** backend only. Placed only when every piece succeeded. |
| `nativeSuppression` | `NONE` | `REPLACE_SOURCE` suppresses each structure's `vanillaSource` native generation. Dimension-level only; failures then throw (no native fallback). |
`terrain` sub-fields for `BORE` / `FORCE_CARVE`: `horizontalPadding` (0..128), `ceilingPadding` (0..128), `floorPadding` (0..64; 0 preserves floor). `FORCE_CARVE`: `shape` (`BOX`/`ROUNDED`/`ERODED`) and for `ERODED` the erosion/lobe knobs. `BORE` always clears a box.
### 3.2 Scoping
Per chunk, Iris samples biome and region at chunk center and collects that biome's placements, that region's placements, and all dimension placements. Biome-level placements fire only where the center lands in that biome.
### 3.3 Height
- Surface: surface height at origin is anchor Y, gated by `minHeight`/`maxHeight`. With default `STRUCTURE_PIECE`, pieces stamp **centered** on that Y (start piece midpoint at surface). Assemblies with non-`STRUCTURE_PIECE`, non-`FLOATING` place mode re-anchor so base sits at surface.
- Underground: seeded random Y in band, then burial shift so envelope (including carve padding) stays under surface; if it cannot fit, chunk skipped.
- No `yBand` on this path — that belongs to native adjustments.
### 3.4 Worked example
```json
{
"structures": [
{
"placementId": "ruined-watchtower",
"structures": ["mypack/watchtower"],
"distribution": "RANDOM_SPREAD",
"spacing": 48,
"separation": 12,
"salt": 918273645,
"minHeight": 62,
"maxHeight": 140,
"terrain": {
"mode": "FORCE_CARVE",
"shape": "ROUNDED",
"horizontalPadding": 4,
"ceilingPadding": 6,
"floorPadding": 0
},
"stilt": {
"maxDepth": 48,
"palette": { "palette": [ { "block": "minecraft:cobblestone" } ] }
}
}
]
}
```
### 3.5 Distribution modes
- **`RANDOM_SPREAD`** — world cut into `spacing`-chunk cells, one candidate per cell, offset by up to `spacing - separation`.
- **`DENSITY`** — independent per-chunk roll. Low densities make `/iris goto` searches expensive.
- **`CONCENTRIC_RINGS`** — `ringCount` placements total, rings `ringDistance` chunks apart, `ringSpread` per ring. Chunk 0,0 is never a start.
### 3.6 Place modes for pieces
Structure `placeMode` matters most for single-piece structures: a terrain-following mode (e.g. `PAINT`, stilts) places through the normal object placer at the surface. Multi-piece assemblies stamp as `STRUCTURE_PIECE` at assembled coordinates; `FLOATING` is downgraded to `STRUCTURE_PIECE`. `underground` placements always stamp `STRUCTURE_PIECE`, with `ORGANIC_STILT` / `CEILING_HANG` as the exception. If in doubt, leave `STRUCTURE_PIECE`.
## 4. Authoring workflow
### 4.1 No in-game jigsaw editor
There is no `/iris jigsaw` command or connector wand. Build piece objects in-game and author piece, pool, and structure JSON directly. `/iris studio vscode [dimension=<pack>]` (alias `vsc`) provides JSON-schema autocomplete for all three folders.
### 4.2 End to end
**1. Studio and build**
```
/iris studio open <pack>
/iris object wand
```
Note object-local coordinates of connector cells — local `(0,0,0)` is the selection corner with lowest X, Y, Z. Leave connector cells as air (or the keep block).
**2. Save object** (`19 - Objects.md`):
```
/iris object save dimension=<pack> mypack/watchtower/base overwrite=true
```
**3. Piece JSON**`jigsaw-pieces/mypack/watchtower/base.json`:
```json
{
"object": "mypack/watchtower/base",
"rotatable": true,
"connectors": [
{
"position": { "x": 4, "y": 1, "z": 0 },
"direction": "NORTH_NEGATIVE_Z",
"top": "UP_POSITIVE_Y",
"pool": "mypack/watchtower/pool/corridors",
"name": "mypack:tower_side",
"targetName": "mypack:corridor_end",
"joint": "ALIGNED"
}
]
}
```
Matching corridor connector faces the opposite way and names itself `mypack:corridor_end`.
Connector geometry:
- Connector cell is **inside** the object, on the outermost layer the neighbor butts against. Neighbor matching cell lands adjacent along `direction`.
- Vertical connectors with `joint: ROLLABLE` suit toppers; horizontal doorways/streets want `ALIGNED`.
**4. Pools** — every reachable pool needs a terminating option.
**5. Structure**`structures/mypack/watchtower.json`:
```json
{
"startPool": "mypack/watchtower/pool/starts",
"maxDepth": 5,
"maxSizeChunks": 4,
"placeMode": "STRUCTURE_PIECE",
"loot": ["mypack/watchtower_chest"]
}
```
**6. Placement** in biome, region, or dimension (3.4).
**7. Validate, inspect, place, iterate** (section 5).
### 4.3 Reload
Studio hotloads into newly generated chunks. Outside studio, close and reopen the world after changes. Files written by `/iris structure import` are ownership-tracked; hand edits make later imports refuse to overwrite those files.
## 5. Testing and debugging
**Pack validation** — use `/iris pack validate pack=<pack>` on Bukkit or `/iris pack validate <pack>` on modded. It resolves references, connector bounds, weight/enum/range, `separation``spacing`, duplicate `placementId`s, VACUUM/ENCASE on Iris backend; compiles graphs with reachability; and runs 16 seeded sample assemblies per structure. Common hand-author diagnostic: `NO_COMPATIBLE_CONNECTOR`.
**`/iris structure info <dimension> <structure>`** — compile + one sample assembly (piece count, footprint). No world needed. "assembled 0 pieces" means start pool resolved empty.
**`/iris structure place <dimension> <structure>`** (player) — stamp at your location (raw geometry; no carve/stilts/loot).
**`/iris structure verify <dimension> [radius=48]`** — Iris placement plans in range (`[iris-planned]` / `[iris-not-found]`) plus native statuses.
**`/iris goto structure <key>`** (`/iris find structure`) — locate nearest planned instance; Iris keys and `vanillaSource` aliases. Iris search up to 1024 chunks; native locate 100 chunks. `/iris goto unregistered` dumps excluded keys and reasons.
At world load, graph diagnostics log with `[StructureGraph:<key>]`. A structure without a runtime-viable graph refuses to place.
### Failure modes
| Symptom | Likely cause |
|---|---|
| Only start piece | Every connector drew `empty` first, or all candidates rejected at max depth with no fallback. |
| Nothing, no error | Start pool rolled `empty`; submerged with `underwater: false`; surface Y outside band; grid did not select chunk. |
| Appears some seeds, vanishes others | Connector below `maxDepth` unsatisfiable for that seed. Add `empty` / terminator fallbacks. |
| `references missing connector pool` | Pool key typo or file not under `jigsaw-pools/`. |
| `exceeded the hard piece cap of 512` | Runaway recursion: always more connectors, no empty weight, depth too high. |
| Pieces visually clip | Bounding-box collision; decorative overhang inside box still "fits". `/iris object shrink` pieces. |
| VACUUM/ENCASE validate or runtime error | Native-only modes; use BORE / FORCE_CARVE, or place via `nativeStructures`. |
| `REPLACE_SOURCE ... failed in chunk` | Suppression without guaranteed output; dimension-level placement, valid `vanillaSource`, graph must guarantee output. |
## 6. Mapping from vanilla datapack jigsaws
| Vanilla | Iris |
|---|---|
| `worldgen/structure` type jigsaw | `structures/*.json` |
| `start_pool` | `startPool` |
| `size` | `maxDepth` |
| `max_distance_from_center` | `maxSizeChunks` (chunks, hard bound) |
| `worldgen/template_pool` | `jigsaw-pools/*.json` |
| `elements[]` | `pieces[]` |
| `minecraft:empty_pool_element` | `"empty": true` |
| `fallback` | same idea, one level deep |
| template `.nbt` | object + `jigsaw-pieces/*.json` |
| jigsaw block pool/name/target/joint/facing/top | connector `pool`/`name`/`targetName`/`joint`/`direction`/`top` |
| `final_state` | none — block at cell stays |
| structure set `random_spread` | `RANDOM_SPREAD` |
| structure set `concentric_rings` | `CONCENTRIC_RINGS` |
| (none) | `DENSITY` |
`terrain_adaptation`, `start_height`, `projection`, and per-element settings have no direct equivalents; nearest are placement height/underground fields and `terrain.mode`.
+407
View File
@@ -0,0 +1,407 @@
# 22 - Native Structures & Datapacks
Structures that originate outside Iris packs: vanilla structures in Iris worlds, datapack structures, the Minecraft structure-block / `.nbt` system, and converting native structures into editable Iris resources. Objects and Iris jigsaws are `19 - Objects.md`, `20 - Object Placement.md`, and `21 - Jigsaw Structures.md`.
Terminology:
- **registered / native structure** — anything in Minecraft's live structure registry (vanilla, mod, datapack). Keys are namespaced (`minecraft:village_plains`, `towns_and_towers:village_ocean`).
- **Iris structure** — editable `structures/<key>.json` inside a pack.
Command listings are Bukkit/Paper; modded loaders expose a reduced set.
## 1. Vanilla structures in Iris worlds
### 1.1 Default: everything generates
Every registered structure generates through its own native placement unless its key is disabled or a dimension-level Iris placement replaces its source. Changes only affect newly generated chunks.
### 1.2 Biome mapping for structure filters
Vanilla tests biomes against structure biome filters. Iris answers per Iris biome:
| Field on biome | Default | Purpose |
|---|---|---|
| `derivative` | `minecraft:the_void` | Vanilla biome this Iris biome reports generally. |
| `vanillaDerivative` | unset | Optional override for structure selection, spawn tables, imported features, biome tags. Wins when set. |
Refinements: a sea-role biome whose derivative is not ocean/river-like resolves to `minecraft:the_void`; shore-role falls back to `minecraft:beach`. **Non-`minecraft:` namespaces pass through** — point `vanillaDerivative` at a datapack/mod biome key that exists in the live registry.
A datapack structure whose filter lists only its own biomes never generates until an Iris biome reports one of those keys via `vanillaDerivative`. `/iris structure verify` reports `[unreachable] <key> needs <biomes>`.
```json
{
"derivative": "minecraft:plains",
"vanillaDerivative": "towns_and_towers:some_custom_biome"
}
```
### 1.3 `importedStructures` (dimension)
| Field | Default | Meaning |
|---|---|---|
| `disabled` | `[]` | Structure keys/prefixes to deny. |
| `undergroundYShift` | `0` (-512..512) | Vertical offset for underground-step structures only. Surface structures never use it. |
| `datapackOverrides` | `true` | Whether ingested datapacks may replace `minecraft:`-namespaced structure content (2.5). |
| `adjustments` | `[]` | Per-structure adjustments for structures still generating natively (1.4). |
#### Prefix matching
Used by `disabled` and `adjustments[].match`. Both sides trimmed and lowercased; key must start with pattern; then:
- Equal length → match.
- Pattern ends with `:`, `/`, or `_` → match (e.g. `"nova_structures:"` disables a namespace).
- Otherwise next character after pattern must be `/` or `_`.
`"minecraft:village"` matches village variants; `"nova_structures"` without trailing colon does **not** match the namespace.
```json
{
"importedStructures": {
"disabled": ["minecraft:village", "minecraft:pillager_outpost"]
}
}
```
### 1.4 `adjustments[]`
Each entry (`match` selects targets by the same prefix rule):
| Field | Default | Meaning |
|---|---|---|
| `match` | `[]` | Keys/prefixes. Empty matches nothing. |
| `yShift` | `0` (-512..512) | Vertical offset; stacks across matches; clamped to build bounds. |
| `yBand` | unset | Absolute world-Y band `{min, max}`: structure midpoint lands in band, deterministic per start chunk. |
| `preserveSourceY` | `false` | Skip Iris burial repositioning; keep vanilla Y. `undergroundYShift` and `yShift` still apply on top. |
| `stilt` | unset | Foundation columns: `maxDepth` (default 64), `palette` (default cobblestone), `spacing`. (`supportNonOccluding` applies to Iris-assembled structures.) |
| `terrain` | unset (= `SOURCE`) | Terrain-integration override. |
Vegetation clearing is automatic (trees intersecting piece envelopes removed).
**Merge:** `yShift` adds; `preserveSourceY` OR-ed; `stilt`, `terrain`, `yBand` last-match-wins. Broad prefix first, specific overrides after.
**Vertical precedence:**
```
preserveSourceY > yBand > burial (underground steps) > plain yShift
```
Three structures honor only `yShift` among these controls: `minecraft:monument` (aligned 24 below sea level), `minecraft:desert_pyramid` (one block above lowest surface Y of footprint), `minecraft:jungle_pyramid` (one block above average surface Y).
#### Terrain modes
| Mode | Behavior |
|---|---|
| `SOURCE` (default) | Replay structure's registered terrain adaptation, including vanilla BURY/ENCAPSULATE fill reimplemented with surrounding terrain material. |
| `PRESERVE` | Disable terrain integration. |
| `BORE` | Clear padded piece volume (box) before placement. |
| `FORCE_CARVE` | Clear padded envelope with `shape`: `BOX` / `ROUNDED` / `ERODED`. |
| `VACUUM` | Raise surface terrain to structure ground planes with fixed 12-block falloff. Never lowers ground. |
| `ENCASE` | Fill padded volume with solid blocks before placement (air/liquid only). Structure carves interiors. `encasePalette` optional (defaults: stone/deepslate overworld, netherrack nether, end stone end). |
Padding: `horizontalPadding` (0..128), `ceilingPadding` (0..128), `floorPadding` (0..64; 0 preserves floor). ERODED: `erosionStrength` (default 0.8), `erosionFrequency` (0.07), `lobeFrequency`, `lobeStrength` (0.85).
#### Examples
Stronghold deep band + encase:
```json
{
"match": ["minecraft:stronghold"],
"yBand": { "min": -120, "max": -20 },
"terrain": {
"mode": "ENCASE",
"horizontalPadding": 4,
"ceilingPadding": 4,
"floorPadding": 4,
"encasePalette": {
"zoom": 1,
"palette": [
{ "block": "minecraft:stone_bricks", "weight": 6 },
{ "block": "minecraft:mossy_stone_bricks", "weight": 2 },
{ "block": "minecraft:cracked_stone_bricks", "weight": 2 },
{ "block": "minecraft:cobblestone", "weight": 1 }
]
}
}
}
```
Shift trial chambers; preserve mineshaft Y; stilt villages:
```json
[
{ "match": ["minecraft:trial_chambers"], "yShift": -64 },
{ "match": ["minecraft:mineshaft"], "preserveSourceY": true },
{
"match": ["minecraft:village"],
"stilt": { "maxDepth": 768, "palette": { "palette": [ { "block": "minecraft:cobblestone" } ] } }
}
]
```
Broad then specific (last-match-wins):
```json
[
{ "match": ["towns_and_towers:"], "terrain": { "mode": "VACUUM" } },
{
"match": [
"towns_and_towers:mimic_desert",
"towns_and_towers:pillager_outpost_ocean",
"towns_and_towers:village_ocean",
"towns_and_towers:wreckage_ocean"
],
"terrain": { "mode": "PRESERVE" }
}
]
```
## 2. Datapack structures
### 2.1 `datapackImports`
Dimension-file list of datapack sources Iris downloads and installs:
```json
{
"datapackImports": [
"https://modrinth.com/datapack/towns-and-towers",
"https://modrinth.com/datapack/dungeons-and-taverns"
]
}
```
Accepted URL forms:
- **Modrinth project page** — latest datapack version for the server's Minecraft version.
- **Pinned Modrinth version** — any `.../version/<token>` URL.
- **Any other URL** — direct zip download, tracked by ETag/hash.
Checksum-verified when Modrinth publishes a hash; size-capped.
### 2.2 Where files land; when ingest runs
Installed datapacks are real Minecraft datapacks at `<level root>/datapacks/<id>/`, each with `.iris-managed.json`. Unmanaged datapacks are never touched; id `iris` is reserved. Cache/staging/manifest under `plugins/Iris/datapacks/`.
Ingest runs shortly after plugin enable when `general.autoIngestDatapacks` is true (default). Minecraft builds worldgen registries at server start, so a **newly installed** datapack is not registered on the boot that installed it — auto-ingest **restarts the server** when anything changed. After that restart, keys are live. A repair path reinstalls staged datapacks that went missing without re-downloading.
### 2.3 Manual commands
```
/iris datapack ingest [restart=false] (alias: pull)
/iris datapack list (alias: ls)
/iris datapack remove <id> (alias: rm)
```
`ingest` aggregates `datapackImports` from every dimension of every loaded pack. `restart` defaults false (Iris tells you a restart is required). `remove` refuses unmanaged datapacks — also delete the URL or next ingest reinstalls it.
### 2.4 Usage patterns
**(a) Natural generation.** Import, restart. Check with `/iris structure list <dimension>` (writes `<pack>/.iris/structure-index.json`) and `/iris structure verify <dimension>` (`[native-eligible]` vs `[unreachable]`). Fix unreachable biomes via `vanillaDerivative`, or use (c).
**(b) Replace vanilla.** Disable vanilla families; datapack replacements keep generating:
```json
{
"importedStructures": {
"datapackOverrides": true,
"disabled": [
"minecraft:village",
"minecraft:pillager_outpost",
"nova_structures:"
],
"adjustments": [
{ "match": ["towns_and_towers:"], "terrain": { "mode": "VACUUM" } },
{ "match": ["towns_and_towers:mimic_desert", "towns_and_towers:pillager_outpost_ocean",
"towns_and_towers:village_ocean", "towns_and_towers:wreckage_ocean"],
"terrain": { "mode": "PRESERVE" } }
]
}
}
```
**(c) Manual placement only.** Disable the datapack namespace, then place specific keys with `nativeStructures``disabled` never blocks explicit placements (3.2):
```json
{
"structures": [
{
"placementId": "dnt-taverns-temperate",
"nativeStructures": [
{ "structure": "nova_structures:tavern_oak", "weight": 4 },
{ "structure": "nova_structures:tavern_birch", "weight": 3 },
{ "structure": "nova_structures:tavern_cherry", "weight": 2 },
{ "structure": "nova_structures:shrine_tower", "weight": 1 }
],
"distribution": "RANDOM_SPREAD",
"spacing": 24,
"separation": 6,
"salt": 776215551
}
]
}
```
### 2.5 `datapackOverrides`
When `false`, Iris strips `data/minecraft/worldgen/structure_set|structure|template_pool/` and `data/minecraft/structure/` from every installed copy. Resolves **globally** — one dimension setting `false` strips for all. Non-`minecraft:` content is unaffected (disable those keys explicitly).
## 3. Placing specific native structures (`nativeStructures`)
`structures[]` on dimension/region/biome hosts two backends — exactly one per placement:
- `structures: ["<iris key>"]` — Iris assemblies (`21 - Jigsaw Structures.md`).
- `nativeStructures: [{ structure, weight, jigsaw }]` — registered structures via Minecraft machinery at Iris-chosen points, full native fidelity.
### 3.1 Entry fields
| Field | Default | Meaning |
|---|---|---|
| `structure` | required | Registered structure key (must exist live). |
| `weight` | `1` (min 1) | Weighted selection among sources. |
| `jigsaw` | unset | Overrides for registered **jigsaw** structures only: `startPool`, `startJigsawName`, `maxDepth` (0..20), `maxDistanceHorizontal` (1..128), `maxDistanceVertical` (1..4064), `useExpansionHack`, `projectStartToHeightmap` (`SOURCE`/`NONE`/heightmap types), `dimensionPaddingBottom` and `dimensionPaddingTop` (nonnegative distance from floor/ceiling), and `liquidSettings`. Null/unset values preserve the registered definition. |
Placement grid fields (`distribution`, `spacing`/`separation`/`salt`, `density`, rings, heights, `underground`, `underwater`, `placementId`) match `21 - Jigsaw Structures.md` section 3.1, except the native backend supports **every** terrain mode including `VACUUM` and `ENCASE`, plus `stilt` (including `spacing`).
Scoping matches Iris placements. Validation requires the structure's effective assembly span stay inside Minecraft's 128-block (8-chunk) structure reference range.
### 3.2 `disabled` never blocks an explicit placement
The placement injector generates planned starts without consulting `disabled` and bypasses the structure's own biome filter. "Disable namespace, re-place explicitly" is supported.
### 3.3 `nativeSuppression: REPLACE_SOURCE`
- **Dimension-level placements only** — blocking pack error elsewhere.
- With `nativeStructures`: suppresses that key's natural generation so it exists only where the placement puts it.
- With Iris `structures`: suppresses each referenced structure's `vanillaSource`; pack validation demands the graph guarantees output — no native fallback. Iris-backend `REPLACE_SOURCE` that produces nothing throws at runtime. Native-backend unusable starts are recorded invalid and skipped silently (still suppressed).
Example — ancient cities replaced by Iris-positioned native starts:
```json
{
"nativeStructures": [ { "structure": "minecraft:ancient_city" } ],
"placementId": "ancient-city-native",
"nativeSuppression": "REPLACE_SOURCE",
"underground": true,
"minHeight": -220,
"maxHeight": -220,
"distribution": "RANDOM_SPREAD",
"spacing": 64,
"separation": 5,
"salt": 42069,
"terrain": {
"mode": "FORCE_CARVE",
"horizontalPadding": 14,
"ceilingPadding": 12,
"shape": "ERODED",
"erosionStrength": 1.0,
"erosionFrequency": 0.05
},
"stilt": {
"maxDepth": 768,
"palette": {
"palette": [
{ "block": "minecraft:deepslate_bricks", "weight": 6 },
{ "block": "minecraft:cracked_deepslate_bricks", "weight": 1 },
{ "block": "minecraft:deepslate_tiles", "weight": 2 },
{ "block": "minecraft:cracked_deepslate_tiles", "weight": 1 }
]
}
}
}
```
### 3.4 Tool reporting
| Configuration | `/iris structure verify` | `/iris goto structure <key>` |
|---|---|---|
| Registered, not disabled, not placed | `[native-eligible]` or `[unreachable] ... needs <biomes>` | Vanilla locate |
| Placed via `nativeStructures` (even if also disabled) | `[iris-planned] <key> @ x,y,z` / `[iris-not-found]` | Iris grid search |
| Disabled, not placed | `[disabled]` | "disabled by this dimension's importedStructures settings" |
A key that is both disabled and placed reports as Iris-placed.
## 4. Minecraft structure-block system
Structure blocks save/load `.nbt` templates; jigsaw blocks wire pools. Iris does not re-teach vanilla tools — see Minecraft wiki Structure Block and datapack tutorials.
How an authored `.nbt` reaches an Iris world:
**(a) Through a datapack (native generation).** Ship under `data/<ns>/structure/`, add `worldgen/template_pool`, `worldgen/structure`, `worldgen/structure_set`, zip, host or Modrinth, add URL to `datapackImports`, `/iris datapack ingest restart=true`. Then natural generation, `adjustments`, or `nativeStructures`.
**(b) Import into Iris resources.** `/iris structure import <dimension>` (section 5). Template pass enumerates **registered** templates only — loose saves in `<world>/generated/` are not enumerated; package them into a datapack first.
Template-import fidelity (lossy by design): first palette only; structure voids and structure blocks dropped; jigsaw blocks resolved to `final_state` (graph rebuilt by separate jigsaw pass); entities not converted; block entities captured.
## 5. Importing native structures into Iris resources
You do not need import just to place — `nativeStructures` places any registered key with full fidelity. Import when you want to **edit** blocks, pools, pieces.
### 5.1 `/iris structure import <dimension>`
Four passes, always overwriting its own previous output:
1. **Jigsaw rebuild** — registered jigsaw structures → editable pools/pieces/objects.
2. **Template import** — registered `.nbt` templates → `objects/<name>.iob` + single-piece `jigsaw-pieces/<name>.json`.
3. **Template groups** — fixed multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils) → one Iris structure each with every variant in the pool.
4. **Capture** — code-generated structures without templates (swamp huts, igloos, ...) via scratch world (also alone as `/iris structure capture <dimension>`). Structures spanning more than **48 blocks** on any axis are skipped (strongholds, mansions, monuments stay native-only).
Naming: `minecraft:village_plains``minecraft_village_plains`. Generated structures carry `vanillaSource` for locate and `REPLACE_SOURCE`.
`/iris studio importvanilla <dimension> [variants=3] [structures=true]` also imports vanilla trees/features as objects, plus structure passes when `structures=true`.
### 5.2 Ownership and `unowned_resource`
Imports use per-bundle ownership manifests (`<pack>/.iris/structure-manifests/`). Failure:
```
Import conflict for '<name>': <path> is unowned_resource. Existing authored files were preserved.
```
Iris found a file it did not write and refused to clobber it. `modified_resource` means Iris wrote it, you edited it, hash no longer matches. Rename or leave the key native.
### 5.3 Automatic datapack import
`general.autoImportDatapackStructures` (default **false**) converts each ingested datapack's structures into editable pack resources on ingest. Off by default because native generation and `nativeStructures` never need the copies, and conversion can write thousands of files. If auto-import fails (often `unowned_resource`), the manifest stays pending and import **retries every boot** until resolved or the setting is disabled. Removing a URL from `datapackImports` cleans bundles that import wrote for it.
## 6. Verification and debugging
```
/iris structure list <dimension> # write + print key index
/iris structure verify <dimension> [radius=48] # eligibility + placement (alias: locateall)
/iris structure info <dimension> <structure> # Iris: compile + sample assembly
/iris structure place <dimension> <structure> # Iris: stamp at feet (player)
/iris goto structure <key> # locate + teleport
/iris goto unregistered # excluded keys + reasons
```
`verify` tags: `[iris-planned]`, `[iris-not-found]`, `[iris-search-limit]`, `[disabled]`, `[unreachable]`, `[native-eligible]`, `[error]`. Placements checked first — disabled-but-placed shows as `[iris-planned]`.
### Traps
- Worlds snapshot the pack — push with `/iris developer update-world world=<w> pack=<dim> confirm=true`, then restart. Backup first.
- Keyed optional args: `radius=200`, not bare `200`.
- New datapack structures need a restart for registry registration.
- Only new chunks change.
- Namespace disables need the colon: `"nova_structures:"`.
- `REPLACE_SOURCE` has no fallback — validate the graph before shipping.
- `datapackOverrides: false` anywhere strips `minecraft:` overrides server-wide.
## Command reference
| Command | Aliases | Parameters |
|---|---|---|
| `/iris datapack ingest` | `pull` | `restart=false` |
| `/iris datapack list` | `ls` | |
| `/iris datapack remove <id>` | `rm` | |
| `/iris structure list <dimension>` | `ls` | |
| `/iris structure import <dimension>` | `import-all`, `reimport`, `imp`, `all` | |
| `/iris structure capture <dimension>` | `cap` | |
| `/iris structure verify <dimension>` | `locateall` | `radius=48` (1..1000 chunks) |
| `/iris structure info <dimension> <structure>` | | |
| `/iris structure place <dimension> <structure>` | `p` | player only |
| `/iris goto structure <key>` | `/iris find structure` | |
| `/iris goto unregistered` | | |
| `/iris developer update-world` | | `world=<w> pack=<dim> confirm=true [fresh-download=false]` — all keyed |
Related dimension fields: `datapackImports`, `importedStructures`, `structures[]`. Settings (`plugins/Iris/settings.json`): `general.autoIngestDatapacks` (default true), `general.autoImportDatapackStructures` (default false).
@@ -0,0 +1,385 @@
# 23 - Loot, Entities, Spawners, Markers
Loot tables fill containers and entity drop inventories. Entities describe what to spawn. Spawners schedule ambient and initial spawns. Markers pin spawners to placed objects. Together they form the pack-side entity and loot systems.
Related: `05 - Concepts & Pack Layout.md`, `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `19 - Objects.md`, `20 - Object Placement.md`, `03 - Configuration.md`, `10 - Studio & VSCode Schemas.md`.
## Where files live
| Path | Registrant | Role |
|------|------------|------|
| `loot/<key>.json` | `IrisLootTable` | Weighted item tables |
| `entities/<key>.json` | `IrisEntity` | Entity type, gear, drops, flags |
| `spawners/<key>.json` | `IrisSpawner` | When/where entity keys spawn |
| `markers/<key>.json` | `IrisMarker` | Marker definition that lists spawner keys |
Keys use pack-relative paths without `.json` (for example `standard/hostile/zombie`, `temperate/hostile`, `global-treasure`).
## Wiring
| Source | Field | Loads |
|--------|-------|-------|
| Dimension | `loot` (`IrisLootReference`) | Global tables for the dimension |
| Region | `loot` | Regional tables |
| Biome | `loot` | Biome tables |
| Entity | `loot` | Drop tables when entity is `Lootable` |
| Object placement | `loot` / `vanillaLoot` | Containers in placed objects |
| Dimension / region / biome | `entitySpawners` | Spawner keys for ambient spawning |
| Object placement | `markers` (`IrisObjectMarker[]`) | Places markers on matching blocks |
| Marker | `spawners` | Spawner keys at that marker |
Ambient spawning merges dimension, then region, then surface-biome `entitySpawners` when `settings.json` has `world.ambientEntitySpawningSystem` true (default). Marker spawning uses mantle markers when `world.markerEntitySpawningSystem` is true (default).
The shipping overworld pack includes `entities/standard/...`, `spawners/<climate>/...`, and `loot/...` resources, but its dimensions, regions, and biomes do not currently reference those spawner keys through `entitySpawners`. Ambient Iris spawners only fire after a parent lists them.
## Loot tables (`IrisLootTable`)
Folder: `loot/`.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `name` | string | `""` | Required display name (min length 2) |
| `rarity` | int ≥ 1 | `1` | Table 1-in-N chance factor |
| `minPicked` | int 0..64 | `1` | Min successful picks per roll |
| `maxPicked` | int 1..64 | `5` | Max successful picks per roll |
| `maxTries` | int 1..256 | `10` | Cap on pick attempts |
| `loot` | `IrisLoot[]` | `[]` | Entries; empty table yields no items |
Roll procedure: RNG picks a random entry each try, keeps items that pass entry rarity and match the requested `InventorySlotType`, stops at `maxPicked` successes or `maxTries`.
### Loot entry (`IrisLoot`, snippet type `loot`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `type` | string | `""` | Required. Vanilla material name such as `diamond` or `DIAMOND_SWORD` (not `minecraft:*` except for namespaced third-party items) |
| `slotTypes` | `InventorySlotType` | `STORAGE` | `STORAGE`, `FUEL`, `FURNACE`, `BLAST_FURNACE`, `SMOKER` |
| `rarity` | int ≥ 1 | `1` | Sub-rarity after table pick |
| `minAmount` / `maxAmount` | int 1..64 | `1` | Stack size |
| `displayName` | string | null | Color codes via `&` |
| `lore` | string[] | `[]` | Wrapped lore lines |
| `minDurability` / `maxDurability` | 0..1 | `0` / `1` | Remaining durability percent |
| `customModel` | int | null | Custom model data float |
| `unbreakable` | boolean | `false` | |
| `itemFlags` | string[] | `[]` | Bukkit `ItemFlag` names |
| `enchantments` | `IrisEnchantment[]` | `[]` | |
| `attributes` | `IrisAttributeModifier[]` | `[]` | |
| `dyeColor` | string | null | `DyeColor` name for colorable items |
| `leatherColor` | string | null | `#RRGGBB` for leather armor |
| `customNbt` | object | null | Platform NBT map; also used for third-party items |
Combined rarity is table rarity × entry rarity. Studio debug lore can show table name and combined chance.
### Loot reference (`IrisLootReference`, snippet type `loot-registry`)
Used on dimensions, regions, biomes, and entities:
```json
{
"loot": {
"mode": "FALLBACK",
"multiplier": 0.5,
"tables": ["temperate/clutter", "temperate/food"]
}
}
```
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `mode` | `IrisLootMode` | `ADD` | `ADD`, `CLEAR`, `REPLACE`, `FALLBACK` |
| `tables` | string[] | `[]` | Loot table keys |
| `multiplier` | double 0..16 | `1` | Scales loot source counts |
- `ADD` — append to parent tables.
- `CLEAR` / `REPLACE` — drop parent tables then add these (same effective clear-then-add).
- `FALLBACK` — use only when no object-level loot already defined.
Overworld dimension example:
```json
{
"loot": {
"mode": "FALLBACK",
"tables": ["global-clutter"]
}
}
```
### Object loot
On `IrisObjectPlacement`:
| Field | Notes |
|-------|-------|
| `loot` | `IrisObjectLoot[]` — pack table name + optional block filter + weight |
| `vanillaLoot` | Vanilla loot table bindings |
| `overrideGlobalLoot` | When true, object tables replace dim/region/biome tables for that placement |
`IrisObjectLoot` fields: `name` (loot table key), `weight` (default 1), `filter` (block list, empty = all containers), `exact` (exact block-data match).
### Real overworld loot sample
From `loot/global-treasure.json` (abbreviated):
```json
{
"name": "Global Treasure",
"rarity": 1,
"maxPicked": 2,
"minPicked": 0,
"maxTries": 20,
"loot": [
{ "type": "diamond", "minAmount": 1, "maxAmount": 3, "rarity": 4, "slotTypes": "STORAGE" },
{ "type": "emerald", "minAmount": 1, "maxAmount": 4, "rarity": 3, "slotTypes": "STORAGE" },
{
"type": "enchanted_book",
"minAmount": 1,
"maxAmount": 1,
"rarity": 14,
"slotTypes": "STORAGE",
"enchantments": [{ "enchantment": "mending", "minLevel": 1, "maxLevel": 1, "chance": 1.0 }]
}
]
}
```
## Entities (`IrisEntity`)
Folder: `entities/`.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `type` | string | null | Required namespaced entity type (`minecraft:zombie` or bare keys resolved by platform). Use `unknown` with `specialType` for external plugins |
| `specialType` | string | `""` | `PluginName:MobName` via external data service |
| `applySettingsToCustomMobAnyways` | boolean | `false` | Apply Iris gear/flags on custom mobs |
| `reason` | string | null | `CreatureSpawnEvent.SpawnReason` name; default `NATURAL` |
| `customName` / `customNameVisible` | string / bool | `""` / `false` | |
| `aware` / `ai` | boolean | `true` | Mob awareness / AI |
| `glowing` / `gravity` / `invulnerable` / `silent` | boolean | false / true / false / false | |
| `pickupItems` / `removable` / `keepEntity` / `baby` | boolean | false | |
| `helmet` / `chestplate` / `leggings` / `boots` / `mainHand` / `offHand` | `IrisLoot` | null | Gear; each entry uses its own `rarity` |
| `passengers` | `IrisEntity[]` | `[]` | Nested riders |
| `attributes` | `IrisAttributeModifier[]` | `[]` | |
| `loot` | `IrisLootReference` | empty ADD | Drop tables |
| `leashHolder` | `IrisEntity` | null | Spawns holder and leashes |
| `spawnEffect` | `IrisEffect` | null | On-spawn effect |
| `spawnEffectRiseOutOfGround` | boolean | `false` | Rise-from-ground VFX |
| `pandaMainGene` / `pandaHiddenGene` | string | null | Panda genes |
| `surface` | `IrisSurface` | `LAND` | Spawn surface check: `LAND`, `ANIMAL`, `WATER`, `OVERWORLD`, `LAVA` |
| `rawCommands` | `IrisCommand[]` | `[]` | Run with `{x}` `{y}` `{z}` |
Minimal overworld entities are often one field:
```json
{"type": "ZOMBIE"}
```
Path key: `entities/standard/hostile/zombie.json` → load key `standard/hostile/zombie`.
### Entity commands (`IrisCommand`)
`rawCommands` executes console commands after an entity spawns. The same command object is also used by ambient effect command registries.
| Field | Default | Behavior |
|-------|---------|----------|
| `commands` | `[]` | Required command strings; a leading `/` is removed and `{x}`, `{y}`, `{z}` are replaced with block coordinates |
| `delay` | `0` | Delay before first execution, in server ticks; negative values clamp to zero |
| `repeat` | `false` | Repeats indefinitely after the first delay; no cancellation handle is exposed and repeats stop on server restart |
| `repeatDelay` | `100` | Ticks between repeats; values below one clamp to one |
| `timeBlock` | any time | Required world-time window |
| `weather` | `ANY` | Required weather: `NONE`, `DOWNFALL`, `DOWNFALL_WITH_THUNDER`, or `ANY` |
Bukkit and modded entity spawning both apply AI/awareness flags, spawn effects, and raw commands. Spawner time and weather gates are also enforced on both platform families.
## Ambient effects (`IrisEffect`)
Biomes and regions accept `effects[]`. Each entry performs a 1-in-`chance` check no more often than `interval` milliseconds and can combine a potion, sound, particle, and command registry.
| Field | Default / range | Behavior |
|-------|-----------------|----------|
| `interval` | `150` ms, ≥0 | Minimum interval between effect attempts |
| `chance` | `50`, ≥1 | One successful attempt in this many intervals |
| `potionEffect` | `""` | Potion-effect registry key |
| `potionStrength` | `-1` (11024) | Amplifier; `-1` disables potion application |
| `potionTicksMin` / `potionTicksMax` | `75` / `155` | Random potion duration in ticks |
| `sound` | `null` | Sound registry key |
| `soundDistance` | `12` (0512) | Maximum random sound offset from the player |
| `minPitch` / `maxPitch` | `0.5` / `1.5` | Random pitch interval |
| `volume` | `1.5` | Sound volume |
| `particleEffect` | `null` | Particle registry key; modded supports simple particle types that need no extra particle data |
| `particleOffset` | `0` (3232) | Random vertical surface offset |
| `particleCount` | `0` (0512) | Count; zero lets the alternate XYZ values behave as motion on Bukkit |
| `particleDistance` | `20` (064) | Forward sampling distance |
| `particleDistanceWidth` | `24` (0128) | Side-to-side sampling radius |
| `particleAway` | `5` (016) | Minimum forward offset |
| `particleAltX` / `particleAltY` / `particleAltZ` | `0` (88) | Particle offset or motion components |
| `randomAltX` / `randomAltY` / `randomAltZ` | `true` / `false` / `true` | Randomize each alternate component between its negative and positive value |
| `extra` | `0` | Particle-specific extra value |
| `commandRegistry` | `null` | Optional command casting configuration |
`IrisCommandRegistry` fields:
| Field | Default | Behavior |
|-------|---------|----------|
| `rawCommands` | `[]` | `IrisCommand[]` to run |
| `commandOffsetX` / `commandOffsetY` / `commandOffsetZ` | `0` (88) | Coordinate offsets from the player |
| `commandRandomAltX` / `commandRandomAltY` / `commandRandomAltZ` | `true` / `false` / `true` | Randomize each coordinate within the signed offset |
| `commandAllRandomLocations` | `true` | Pick a new random coordinate for each command object; false reuses one coordinate |
Example:
```json
{
"particleEffect": "minecraft:ash",
"particleCount": 8,
"sound": "minecraft:ambient.cave",
"interval": 1000,
"chance": 8,
"commandRegistry": {
"commandOffsetX": 4,
"commandOffsetZ": 4,
"rawCommands": [{ "commands": ["particle minecraft:smoke {x} {y} {z}"] }]
}
}
```
## Spawners (`IrisSpawner`)
Folder: `spawners/`.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `spawns` | `IrisEntitySpawn[]` | `[]` | Ongoing spawns |
| `initialSpawns` | `IrisEntitySpawn[]` | `[]` | Per-chunk initial pass (`EXECUTES PER CHUNK`) |
| `maxEntitiesPerChunk` | int | `1` | Skip if living entities exceed |
| `timeBlock` | `IrisTimeBlock` | any time | `startHour` / `endHour` in 24h (024) |
| `weather` | `IrisWeather` | `ANY` | `NONE`, `DOWNFALL`, `DOWNFALL_WITH_THUNDER`, `ANY` |
| `maximumRate` | `IrisRate` | infinite | Global rate limit |
| `maximumRatePerChunk` | `IrisRate` | infinite | Per-chunk rate limit |
| `allowedLightLevels` | `IrisRange` | `0`..`15` | Inclusive block light |
| `group` | `IrisSpawnGroup` | `NORMAL` | Placement band |
`IrisSpawnGroup`:
| Value | Placement |
|-------|-----------|
| `NORMAL` | Surface land biomes only |
| `CAVE` | Valid for all biome inferred types; uses cave-floor markers when available |
| `UNDERWATER` | Sea biomes, between seafloor and surface |
| `BEACH` | Shore biomes |
`IrisRate`: `amount` (0 = infinite) and `per` (`IrisDuration`). Empty duration means an infinite rate and is always allowed.
`IrisDuration` fields are additive:
| Field | Runtime unit |
|-------|--------------|
| `milliseconds` | 1 ms |
| `minecraftTicks` | 50 ms |
| `seconds` / `minutes` / `hours` / `days` | Real-time units |
| `minecraftHours` | 50 real seconds |
| `minecraftDays` | 20 real minutes |
| `minecraftWeeks` | 7 Minecraft days (2 h 20 min) |
| `minecraftLunarCycles` | 8 Minecraft days (2 h 40 min) |
### Entity spawn entry (`IrisEntitySpawn`, snippet `entity-spawn`)
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `entity` | string | `""` | Required entity key |
| `rarity` | int ≥ 1 | `1` | 1-in-N for some spawn paths |
| `minSpawns` / `maxSpawns` | int ≥ 1 | `1` | Count range when selected |
Overworld sample `spawners/temperate/hostile.json`:
```json
{
"group": "NORMAL",
"maximumRate": { "amount": 8, "per": { "seconds": 15 } },
"timeBlock": { "startHour": 20, "endHour": 3 },
"maxEntitiesPerChunk": 2,
"weather": "ANY",
"spawns": [
{ "entity": "standard/hostile/zombie", "rarity": 5, "maxSpawns": 4, "minSpawns": 2 },
{ "entity": "standard/hostile/skeleton", "rarity": 30, "maxSpawns": 2, "minSpawns": 1 }
]
}
```
To activate ambient spawning, list the spawner key on a parent:
```json
{
"entitySpawners": ["temperate/hostile", "temperate/passive"]
}
```
## Markers (`IrisMarker`)
Folder: `markers/`.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `spawners` | string[] | `[]` | Spawner keys attached at this marker |
| `removeOnChange` | boolean | `true` | Drop marker when assigned block changes |
| `emptyAbove` | boolean | `true` | Require two air blocks above when placing |
| `exhaustionChance` | double | `0` | Chance to remove marker on use; `<0` never; `≥1` always on first use |
### Placing markers from objects (`IrisObjectMarker`, snippet `object-marker`)
On object placement `markers[]`:
| Field | Type | Notes |
|-------|------|-------|
| `mark` | `IrisBlockData[]` | Required block types to tag |
| `marker` | string | Required marker key |
| `maximumMarkers` | int 1..16 | Default 8 |
| `exact` | boolean | Exact block-data match |
When marker entity spawning is enabled, `MarkerSpawnScanner` loads the marker tag, resolves its spawners, and `WorldEntitySpawner` fires those spawners at the marker position. Marker exhaustion can remove the mantle marker after use.
## Custom block drops (`IrisBlockDrops`)
Dimensions, regions, and biomes accept `blockDrops[]`. On Bukkit, a matching biome provider runs first; unless any matching biome provider has `skipParents: true`, matching region and dimension providers are appended in that order.
| Field | Default | Behavior |
|-------|---------|----------|
| `blocks` | `[]` | Required `IrisBlockData[]` to match |
| `exactBlocks` | `false` | False compares material only; true compares the complete block state |
| `drops` | `[]` | `IrisLoot[]`; each entry's rarity is rolled independently |
| `skipParents` | `false` | On a matching biome provider, prevents region and dimension drop providers from running for that break |
| `replaceVanillaDrops` | `false` | If any matching provider enables it, suppresses vanilla block drops while retaining Iris drops from all selected providers |
```json
{
"blocks": [{ "block": "minecraft:stone" }],
"exactBlocks": false,
"drops": [{ "type": "flint", "rarity": 4 }],
"skipParents": false,
"replaceVanillaDrops": false
}
```
The custom block-drop router is a Bukkit runtime feature; modded pack loading preserves the schema but does not install the Bukkit block-break event router.
## Runtime settings that gate spawning
From `settings.json``world`:
| Key | Default | Effect |
|-----|---------|--------|
| `ambientEntitySpawningSystem` | `true` | Dim/region/biome `entitySpawners` |
| `markerEntitySpawningSystem` | `true` | Marker-driven spawners |
| `targetSpawnEntitiesPerChunk` | `0.95` | Ambient density target |
| `asyncTickIntervalMS` | `700` | Spawn tick interval |
| `forcePersistEntities` | `true` | Force entity persistence globally |
Studio command `/iris studio loot` previews chest loot at the player position (see `10 - Studio & VSCode Schemas.md`, `04 - Commands & Permissions.md`).
## Authoring checklist
1. Write `loot/<key>.json` tables; reference them from dim/region/biome `loot.tables` or object `loot[].name`.
2. Write `entities/<key>.json` with at least `type`.
3. Write `spawners/<key>.json` with `spawns` entity keys and rates.
4. Add spawner keys to `entitySpawners` on dimension, region, and/or biome — or attach via markers on object placements.
5. Optional markers under `markers/` plus placement `markers` arrays for structure-bound spawns.
6. Validate with studio open + hotload; use studio loot preview for chests.
+291
View File
@@ -0,0 +1,291 @@
# 24 - Pack Mods & Snippets
Snippets are active reusable JSON fragments for types annotated `@Snippet`; fields accept either an inline object or a path under `snippet/<type>/`. Iris also loads the legacy `IrisMod` JSON schema from `mods/`, but no engine path applies those injector or replacer fields at runtime.
Related: `05 - Concepts & Pack Layout.md`, `10 - Studio & VSCode Schemas.md`, `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `20 - Object Placement.md`, `25 - Pack Management.md`.
## Pack mod schema (`IrisMod`, inactive)
Folder: `mods/`. The loader key is the path under `mods/` without `.json`. `IrisData` can parse and expose these registrants to schema and tooling paths, but engine creation and Studio hotload do not consume them. Treat the fields below as an inactive schema, not a supported way to modify a dimension.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `name` | string | `"A Pack Modification"` | Required human name (min length 2) |
| `forDimension` | string | `""` | Optional dimension load key; empty = any dimension |
| `overrideFluidHeight` | int -1..512 | `-1` | `-1` leaves fluid height unchanged |
| `removeBiomes` | string[] | `[]` | Biome keys to remove |
| `removeObjects` | string[] | `[]` | Object keys to remove |
| `removeRegions` | string[] | `[]` | Region keys to remove |
| `injectRegions` | string[] | `[]` | Region keys to inject into the dimension |
| `biomeInjectors` | `IrisModBiomeInjector[]` | `[]` | Inject biomes into a region |
| `biomeReplacers` | `IrisModBiomeReplacer[]` | `[]` | Swap biomes |
| `objectReplacers` | `IrisModObjectReplacer[]` | `[]` | Swap object keys |
| `biomeObjectPlacementInjectors` | `IrisModObjectPlacementBiomeInjector[]` | `[]` | Inject object placements into a biome |
| `regionObjectPlacementInjectors` | `IrisModObjectPlacementRegionInjector[]` | `[]` | Inject object placements into a region |
| `regionReplacers` | `IrisModRegionReplacer[]` | `[]` | Swap regions |
| `blockReplacers` | `IrisObjectReplace[]` | `[]` | Block find/replace rules (same shape as object material replacers) |
| `styleReplacers` | `IrisModNoiseStyleReplacer[]` | `[]` | Replace `NoiseStyle` usages |
### Injector and replacer shapes
**Biome injector** (`@Snippet("biome-injector")`):
```json
{ "region": "temperate", "inject": ["temperate/meadows"] }
```
**Biome replacer** (`biome-replacer`):
```json
{ "find": ["temperate/plains"], "replace": "temperate/lush-plains" }
```
**Region replacer** (`region-replacer`):
```json
{ "find": ["temperate"], "replace": "forests" }
```
**Object replacer** (`object-replacer`):
```json
{ "find": ["clutter/camp1"], "replace": "clutter/camp3" }
```
**Object placement biome injector** (`object-placement-biome-injector`):
```json
{
"biome": "temperate/plains",
"place": [{ "chance": 0.01, "place": ["clutter/camp1"] }]
}
```
**Object placement region injector** (`object-placement-region-injector`): field name is `biome` in code but the registry type is `IrisRegion` (region load key):
```json
{
"biome": "temperate",
"place": [{ "chance": 0.01, "place": ["clutter/camp1"] }]
}
```
**Noise style replacer** (`noise-style-replacer`):
| Field | Notes |
|-------|-------|
| `find` | `NoiseStyle` enum value to match |
| `replaceTypeOnly` | When true, only swap the style type and keep other style fields |
| `replace` | Full `IrisGeneratorStyle` replacement |
**Block replacer** (reuses `IrisObjectReplace`, snippet `object-block-replacer`): `find` block list, `replace` palette, optional `exact`, `chance` 0..1.
### Schema example
`mods/example-swap.json`:
```json
{
"name": "Example Temperate Swap",
"forDimension": "overworld",
"biomeReplacers": [
{
"find": ["temperate/plains"],
"replace": "temperate/meadows"
}
],
"biomeInjectors": [
{
"region": "temperate",
"inject": ["temperate/shattered-plains"]
}
]
}
```
The example is parseable as `IrisMod`, but it has no effect on generated terrain. Apply equivalent changes directly to the target dimension, region, biome, generator, or object-placement JSON.
## Snippets
### Mechanism
1. Many nested pack types carry `@Snippet("type-name")`.
2. Gson type adapters in `IrisData` intercept those types on read.
3. A field may be either:
- an inline JSON object of that type, or
- a **string** `"snippet/<type-name>/<path>"` that loads `snippet/<type-name>/<path>.json` from the pack root.
4. If the string starts with `snippet/` but uses a different type folder, the loader rewrites to the expected `snippet/<type-name>/` prefix for that field.
5. Missing snippet files log an error and yield null for that value.
Studio schemas (`SchemaBuilder`) expose every snippet as `anyOf` object-or-string and list files under `snippet/<type>/` in the workspace enum.
### Disk layout
```
pack/
snippet/
decorator/
bush.json
dry_grass.json
...
style/
bedrock.json
deepslate.json
```
Folder is singular `snippet/`, not `snippets/`. Subfolders match the `@Snippet` value exactly.
### Overworld usage
Dimension ores reference style snippets:
```json
{
"chanceStyle": "snippet/style/bedrock"
}
```
`snippet/style/bedrock.json`:
```json
{ "style": "STATIC" }
```
Biome decorators accept snippet strings in arrays:
```json
{
"decorators": [
"snippet/decorator/wildflowers",
"snippet/decorator/bush"
]
}
```
`snippet/decorator/bush.json`:
```json
{
"chance": 0.03,
"style": {
"style": "CLOVER_HERMITE",
"zoom": 0.52,
"exponent": 2.5,
"axialFracturing": true
},
"slopeCondition": { "maximumSlope": 5 },
"palette": [
{ "block": "minecraft:bush", "weight": 1 },
{ "block": "minecraft:air", "weight": 4 }
]
}
```
`biomes/dev.json` uses the same pattern for a minimal decorator list.
### `@Snippet` type names (engine/object)
Each value is the folder name under `snippet/` and the string prefix after `snippet/`:
| Snippet value | Class (representative) |
|---------------|------------------------|
| `attribute-modifier` | `IrisAttributeModifier` |
| `axis-rotation` | `IrisAxisRotationClamp` |
| `biome-injector` | `IrisModBiomeInjector` |
| `biome-palette` | `IrisBiomePaletteLayer` |
| `biome-replacer` | `IrisModBiomeReplacer` |
| `block-drops` | `IrisBlockDrops` |
| `cave-field-module` | `IrisCaveFieldModule` |
| `cave-profile` | `IrisCaveProfile` |
| `color` | `IrisColor` |
| `command` | `IrisCommand` |
| `command-registry` | `IrisCommandRegistry` |
| `coral` | `IrisCoral` |
| `crystal` | `IrisCrystal` |
| `custom-biome` | `IrisBiomeCustom` |
| `custom-biome-particle` | `IrisBiomeCustomParticle` |
| `custom-biome-spawn` | `IrisBiomeCustomSpawn` |
| `decorator` | `IrisDecorator` |
| `deposit` | `IrisDepositGenerator` |
| `deposit-variant` | `IrisDepositVariant` |
| `dimension-carving-entry` | `IrisDimensionCarvingEntry` |
| `dimension-mode` | `IrisDimensionMode` |
| `duration` | `IrisDuration` |
| `effect` | `IrisEffect` |
| `enchantment` | `IrisEnchantment` |
| `entity-spawn` | `IrisEntitySpawn` |
| `expression-function` | `IrisExpressionFunction` |
| `expression-load` | `IrisExpressionLoad` |
| `floating-child-biome` | `IrisFloatingChildBiomes` |
| `formation` | `IrisFormation` |
| `fungus` | `IrisFungus` |
| `generator` | `IrisNoiseGenerator` |
| `generator-layer` | `IrisBiomeGeneratorLink` |
| `image-map` | `IrisImageMap` |
| `loot` | `IrisLoot` |
| `loot-registry` | `IrisLootReference` |
| `noise-style-replacer` | `IrisModNoiseStyleReplacer` |
| `object-block-replacer` | `IrisObjectReplace` |
| `object-limit` | `IrisObjectLimit` |
| `object-loot` | `IrisObjectLoot` |
| `object-marker` | `IrisObjectMarker` |
| `object-placement-biome-injector` | `IrisModObjectPlacementBiomeInjector` |
| `object-placement-region-injector` | `IrisModObjectPlacementRegionInjector` |
| `object-placer` | `IrisObjectPlacement` |
| `object-replacer` | `IrisModObjectReplacer` |
| `object-rotator` | `IrisObjectRotation` |
| `object-scale` | `IrisObjectScale` |
| `object-translator` | `IrisObjectTranslate` |
| `object-vanilla-loot` | `IrisObjectVanillaLoot` |
| `palette` | `IrisMaterialPalette` |
| `position-3d` | `IrisPosition` |
| `potion-effect` | `IrisPotionEffect` |
| `procedural-objects` | `IrisProceduralObjects` |
| `procedural-tree` | `IrisProceduralTree` |
| `range` | `IrisRange` |
| `rate` | `IrisRate` |
| `region-replacer` | `IrisModRegionReplacer` |
| `ruin` | `IrisRuin` |
| `ruin-decorator` | `IrisRuinDecorator` |
| `shaped-style` | `IrisShapedGeneratorStyle` |
| `slope-clip` | `IrisSlopeClip` |
| `stilt-settings` | `IrisStiltSettings` |
| `style` | `IrisGeneratorStyle` |
| `style-range` | `IrisStyledRange` |
| `time-block` | `IrisTimeBlock` |
| `tree` | `IrisTree` |
| `tree-branches` | `IrisTreeBranches` |
| `tree-canopy` | `IrisTreeCanopy` |
| `tree-decorator` | `IrisTreeDecorator` |
| `tree-layer` | `IrisTreeLayer` |
| `tree-secondary-leaf` | `IrisTreeSecondaryLeaf` |
| `tree-settings` | `IrisTreeSettings` |
| `tree-size` | `IrisTreeSize` |
| `tree-sub-branches` | `IrisTreeSubBranches` |
| `vacuum-settings` | `IrisVacuumSettings` |
Registrants that are whole files (dimensions, regions, biomes, generators, loot tables, entities, spawners, markers, mods, objects, structures) are not snippet types; only nested field types listed above are.
### Registered schemas without a production authoring path
Schema registration alone does not prove a runtime consumer. The following types are discoverable by loaders or Studio schema generation but are not supported pack features:
| Surface | Current status |
|---------|----------------|
| `potion-effect` / `IrisPotionEffect` | Snippet schema exists, but no production field consumes this type; use the potion fields on `IrisEffect` instead |
| `matter/` resources | A loader exists for Matter binaries, but generation and runtime code do not consume pack `matter/` resources |
| `IrisObjectPlacement.translateCenter` | Serialized and copied by `toPlacement`, but no placement path reads the value |
The `mods/*.json` family is likewise schema/tooling-only as documented above.
### Authoring snippets
1. Create `snippet/<type>/<name>.json` matching the field type shape.
2. Reference as `"snippet/<type>/<name>"` (no `.json` suffix in the string).
3. Prefer snippets for values reused across many biomes (decorators, styles, palettes).
4. Open studio so VSCode schemas list available snippet paths under `.iris/schema/snippet/`.
## Related commands
- Pack validation: `/iris pack validate` — see `25 - Pack Management.md`, `04 - Commands & Permissions.md`.
- Studio open/hotload: `10 - Studio & VSCode Schemas.md`.
+153
View File
@@ -0,0 +1,153 @@
# 25 - Pack Management
Pack management covers download/install into the packs workspace, validation, unused-resource cleanup and restore, packaging for distribution, and unsafe replacement of a live worlds pack snapshot. Authoring packs live under the platform packs root; production worlds copy that tree into `<world>/iris/pack` (see `05 - Concepts & Pack Layout.md` and `06 - Worlds & Lifecycle.md`).
See also: `03 - Configuration.md`, `04 - Commands & Permissions.md`, `10 - Studio & VSCode Schemas.md`, `24 - Pack Mods & Snippets.md`, `27 - Example - Configuring Overworld.md`.
## Pack workspace
| Item | Path / rule |
|------|-------------|
| Packs root | Bukkit: plugin data `packs/`; modded: `config/irisworldgen/packs/` (platform data folder) |
| Visible packs | Non-hidden directories listed by `PackDirectoryResolver` |
| Presence | Pack exists if it has safe tree + at least one `dimensions/*.json` (parse failures do not trigger redownload) |
| Safe key | Download destination keys: `[a-z0-9_-]+` |
## Download
### Commands
| Command | Behavior |
|---------|----------|
| Bukkit: `/iris download <pack> [branch=stable] [overwrite=false]`; modded: `/iris download <pack> [branch] [force]` | Download into packs root |
| Default overworld special case | Pack name `overworld` uses IrisDimensions overworld **beta release zip** (`…/releases/download/beta/overworld.zip`), not an arbitrary branch zip |
| Other packs | `IrisDimensions/<pack>/<branch>` GitHub archive search via `StudioSVC.downloadSearch` |
| Param | Default | Notes |
|-------|---------|-------|
| `pack` | required | Folder/key or repo short name |
| `branch` | `stable` | GitHub ref when not default overworld |
| `overwrite` | `false` | Force replace existing present pack |
### Install pipeline (`PackDownloader`)
1. Per-key/ref download lock (concurrent startup and commands do not double-fetch).
2. If pack present and not force → skip network.
3. Download zip (size/entry limits: archive ≤512MiB, ≤100k entries, total uncompressed budget, per-file cap).
4. Unpack to temp; require single pack home directory.
5. Open as datapack-compiler `IrisData`; require **exactly one** dimension; key = that dimension load key.
6. Run `PackValidator.validate`; blocking errors abort install.
7. Publish into `packs/<key>/` with conflict checks (refuses symlink targets; detects dimension-key conflicts with other folders).
Default overworld repository constant: `IrisDimensions/overworld`.
## Validate
| Command | Behavior |
|---------|----------|
| Bukkit: `/iris pack validate [pack=<key>]`; modded: `/iris pack validate [pack]` | Validate one pack or all visible packs; publish into `PackValidationRegistry` |
| Bukkit: `/iris pack status [pack=<key>]`; modded: `/iris pack status [pack]` | Show cached registry results (run validate first) |
### Checks performed (`PackValidator`)
| Check | Blocking vs warning |
|-------|---------------------|
| Missing pack / missing `dimensions/` / no dimension JSON | Blocking |
| Dimension JSON integrity (`PackDimensionValidator`) | Blocking / warnings as emitted |
| Loot graph (`PackLootValidator`) | Blocking |
| Removed worldgen fields (e.g. `fluidBodies`) | Blocking |
| Object surface support | Blocking |
| Unsupported structure transforms (`rotation` / `translate` / `scale` on forbidden surfaces) | Blocking |
| Structure graph + compiled graph validator | Errors blocking; warnings advisory |
| Native structure replacement envelopes | Blocking |
| Spawner → entity references | Blocking |
| Custom biome spawns category resolution | Blocking |
| Content keys / bad block properties (`ContentKeyValidator`) | Blocking when `general.strictContentKeys` or `-Diris.strictContent`; else warnings (palette-sourced stay advisory) |
`isLoadable()` is false when any blocking error exists. Status reports blocking count and up to 10 warnings (plus “more” count).
## Cleanup (unused resources)
| Command | Mode | Behavior |
|---------|------|----------|
| Bukkit: `/iris pack cleanup <pack> [mode=preview]`; modded: `/iris pack cleanup <pack> [apply]` | `preview` (default) | List unused candidates; no writes |
| | `apply` | Quarantine candidates under pack `.iris-trash/<timestamp>/` |
Managed folders scanned for unreferenced JSON resources: `biomes`, `regions`, `entities`, `spawners`, `loot`, `generators`, `expressions`, `markers`, `blocks`, `mods`.
Excluded from cleanup corpus: `.iris-trash`, `datapack-imports`, `externaldatapacks`, `internaldatapacks`, `datapacks`, `cache`, `objects`, `.iris`.
Cleanup re-scans on apply (not a blind apply of an old preview). Failed apply may leave paths still quarantined and reports them.
## Restore
| Command | Mode | Behavior |
|---------|------|----------|
| Bukkit: `/iris pack restore <pack> [mode=preview]`; modded: `/iris pack restore <pack> [apply]` | `preview` | List latest quarantine dump files and conflicts |
| | `apply` | Move files back from latest dump if destinations free |
Restore **refuses** when destination paths already exist (conflict list). Nothing restored when no quarantine dump exists.
## Package (export)
| Command | Behavior |
|---------|----------|
| Bukkit: `/iris studio package [dimension=default] [obfuscate=false] [minify=true]`; modded: `/iris studio package [pack]` | Compile dimension closure to a zip |
| Param | Default | Notes |
|-------|---------|-------|
| `dimension` | contextual / `default` | Dimension in packs |
| `obfuscate` | `false` | Obfuscate packaged content when true |
| `minify` | `true` | Compact JSON (indent 0) |
Pipeline (`IrisPackageCompiler`):
1. Load dimension and walk regions → biomes → generators, loot, entities, spawners, structures/objects closure.
2. Stage under Iris data `exports/<dimensionKey>/`.
3. Write `package.json` with hash, time, version.
4. Zip to `exports/<dimensionKey>.iris` (compression level 9); delete staging folder.
Does not modify the source pack or any world snapshot.
## Developer update-world (unsafe)
| Command | Behavior |
|---------|----------|
| `/iris developer update-world world=<world> pack=<dimension> confirm=true [fresh-download=false]` | Replace the worlds pack snapshot |
| Param | Default | Notes |
|-------|---------|-------|
| `world` | contextual | Target world folder |
| `pack` / dimension | contextual | Source dimension (live packs root) |
| `confirm` | `false` | Required true; otherwise prints warning only |
| `fresh-download` | `false` | Re-download pack before install |
Implementation:
1. Requires `confirm=true`.
2. Optional `StudioSVC.downloadSearch` when `fresh-download`.
3. Acquires `PACK_MUTATION` / `PACK_PUBLISH` lease.
4. `StudioSVC.replaceIntoWorld` → install into `worldFolder/iris/pack` with `replaceExisting=true` (atomic stage/publish).
5. If an engine still holds that pack data, Iris **restarts the server** after commit (`"An active Iris world pack was replaced."`).
This is intentionally unsafe for production without backups: existing chunks keep old terrain; only future generation and pack-driven systems see new content. Prefer staging a new world when pack contracts change.
## Related operations
| Task | Where |
|------|-------|
| Create studio project from template | `/iris studio create` (`10 - Studio & VSCode Schemas.md`) |
| Open VSCode + schemas | `/iris studio vscode` |
| Import vanilla objects/structures into pack | `/iris studio importvanilla` |
| Structure import | `/iris structure …` |
| Strict content keys | `settings.general.strictContentKeys` (`03 - Configuration.md`) |
| Datapack bootstrap / install | Server configurator + `/iris datapack` (see platform docs) |
## Operator checklist
1. Download or place pack under `packs/<key>/` with `dimensions/*.json`.
2. On Bukkit, run `/iris pack validate pack=<key>` until loadable. Modded uses `/iris pack validate <key>`.
3. On Bukkit, optionally run `/iris pack cleanup <key> mode=preview`, then `mode=apply` after review; restore if needed. Modded uses the `apply` literal.
4. Create world with `/iris create …` (copies pack) or open studio for live edit.
5. To ship on Bukkit: `/iris studio package dimension=<dimension>`.
6. To refresh an existing world pack only after backup: `/iris dev update-world world=<world> pack=<dimension> confirm=true`.
+176
View File
@@ -0,0 +1,176 @@
# 26 - Example - Minimal Dimension
This walkthrough builds a loadable pack with one dimension, one region, one biome, and one generator using real field names from `IrisDimension`, `IrisRegion`, `IrisBiome`, and `IrisGenerator`. The skeleton matches `StudioSVC.createStarterProject` and is expanded with required mode and fluid height for explicit authoring.
Related: `05 - Concepts & Pack Layout.md`, `02 - Getting Started.md`, `10 - Studio & VSCode Schemas.md`, `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `25 - Pack Management.md`, `04 - Commands & Permissions.md`.
## Goal pack layout
```
packs/minimal/
dimensions/minimal.json
regions/starter.json
biomes/starter.json
generators/flat.json
```
Pack folder name is the pack key. Dimension file name without `.json` is the dimension load key (`minimal`).
## Create options
| Method | Command / action |
|--------|------------------|
| Studio create (code template) | `/iris studio create name=minimal` — writes starter files under `packs/` |
| Studio create from template | `/iris studio create name=minimal template=overworld` — copies existing pack |
| Manual | Create folders and JSON under the platform packs directory |
Studio create without a template writes the starter project shown below (dimension/region/biome/generator only). After create, open studio: `/iris studio open minimal`.
Platform packs roots (same layout):
- Bukkit-family: `plugins/Iris/packs/`
- Fabric / Forge / NeoForge: `config/irisworldgen/packs/`
## File contents
### `dimensions/minimal.json`
```json
{
"name": "minimal",
"version": 1,
"mode": { "type": "OVERWORLD" },
"regions": ["starter"],
"fluidHeight": 63,
"logicalHeight": 384,
"dimensionHeight": { "min": -64, "max": 320 }
}
```
Required / load-bearing fields:
| Field | Why |
|-------|-----|
| `name` | Human-readable name (`@Required`, min length 2) |
| `regions` | At least one region load key |
| `mode` | `IrisDimensionMode` (`type`: `OVERWORLD`, `SUPERFLAT`, `ENCLOSURE`, `ISLANDS`) |
| `fluidHeight` | Sea level relative to dimension min (default 63 if omitted) |
| `dimensionHeight` | World Y bounds; default `-64`..`320` if omitted |
| `version` | Pack version stamp; change to discourage accidental upgrades |
Optional but useful for testing: `"focus": "starter"` forces a single biome; `"focusRegion": "starter"` forces one region.
### `regions/starter.json`
```json
{
"name": "Starter",
"landBiomes": ["starter"],
"seaBiomes": ["starter"],
"shoreBiomes": ["starter"]
}
```
| Field | Why |
|-------|-----|
| `name` | Required region name |
| `landBiomes` | Required root land biome keys |
| `seaBiomes` / `shoreBiomes` | Optional for land-only packs; starter includes them for full land/sea/shore coverage |
| `caveBiomes` | Optional list for cave biomes |
Do not list child biomes here — only root parents.
### `biomes/starter.json`
```json
{
"name": "Starter Plains",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{
"palette": [{ "block": "minecraft:grass_block" }]
}
],
"generators": [
{
"generator": "flat",
"min": 96,
"max": 96
}
]
}
```
| Field | Why |
|-------|-----|
| `name` | Required display name |
| `derivative` | Required vanilla biome key for coloring / vanilla structure eligibility |
| `vanillaDerivative` | Structure selection derivative; falls back to `derivative` when null |
| `layers` | Surface material stack; remaining depth fills with stone |
| `generators` | Links to `generators/<key>.json` with height relative to fluid height |
`min`/`max` of 96 with fluid height 63 produce high flat land. For near-sea plains use smaller values (overworld plains use roughly `min` 4 / `max` 10 on generator `plain`).
### `generators/flat.json`
```json
{
"interpolator": { "function": "NONE", "horizontalScale": 1 },
"seed": 310,
"composite": [
{
"seed": 310,
"style": { "style": "FLAT" }
}
]
}
```
| Field | Why |
|-------|-----|
| `seed` | Required generator seed |
| `interpolator` | Cross-biome height blend; `NONE` for hard flat |
| `composite` | Noise layers; `FLAT` style yields constant mid-value height |
This matches shipping overworld `generators/flat.json` and the studio starter.
## Studio create vs this skeleton
`StudioSVC.createStarterProject` writes the same four files with pack name substituted for the dimension file/name. It omits explicit `mode` and `fluidHeight` (code defaults: mode `OVERWORLD`, fluid height `63`). The JSON above adds those fields so authors see the required contract.
## Run the pack
1. Ensure the pack sits under `packs/minimal/` with `dimensions/minimal.json`.
2. Validate: `/iris pack validate pack=minimal` (Bukkit).
3. Create a world: `/iris create myworld type=minimal` (Bukkit) or `/iris create myworld minimal` (modded).
4. Or open studio: `/iris studio open minimal` for hotload editing.
World create copies the pack into the world folder at `iris/pack/` (see `06 - Worlds & Lifecycle.md`). Studio worlds hotload the live pack under `packs/` — prefer studio for authoring.
## Extend without breaking the minimal set
| Add | Where |
|-----|-------|
| Second biome | New `biomes/*.json`, append key to `regions/starter.json` `landBiomes` |
| Sea variety | Distinct biome keys on `seaBiomes` / `shoreBiomes` |
| Loot | `loot/*.json` + dimension/region/biome `loot` reference (`23 - Loot, Entities, Spawners, Markers.md`) |
| Decorators | Biome `decorators` array (inline or `snippet/decorator/...`) |
| Objects | Biome/region `objects` placements + `objects/*.iob` (`19 - Objects.md`, `20 - Object Placement.md`) |
| Entity spawn | `entities/`, `spawners/`, then `entitySpawners` on dim/region/biome |
## Validation notes
- Dimension load key must match a file under `dimensions/`.
- Every region key in `regions` must load.
- Every biome key listed on a region must load.
- Every `generators[].generator` key must load or the engine falls back to an empty default generator.
- `derivative` must be a known biome registry key such as `minecraft:plains`.
## Cross-links for next steps
- Full dimension options: `11 - Dimensions.md`
- Region zooms, deposits, caves: `12 - Regions.md`
- Layers, decorators, structures: `13 - Biomes.md`
- Noise composite detail: `14 - Generators & Noise.md`
- Editing the full overworld pack: `27 - Example - Configuring Overworld.md`
@@ -0,0 +1,194 @@
# 27 - Example - Configuring Overworld
The shipping overworld pack is the default Iris dimension pack. This guide shows where it lives, how worlds snapshot it, how to edit safely with studio, and how to push changes into production worlds with `update-world`.
Related: `05 - Concepts & Pack Layout.md`, `06 - Worlds & Lifecycle.md`, `10 - Studio & VSCode Schemas.md`, `11 - Dimensions.md`, `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `23 - Loot, Entities, Spawners, Markers.md`, `24 - Pack Mods & Snippets.md`, `25 - Pack Management.md`, `04 - Commands & Permissions.md`, `02 - Getting Started.md`.
## Pack locations
| Platform | Authoritative packs root |
|----------|--------------------------|
| Bukkit / Paper / Folia / Purpur | `plugins/Iris/packs/overworld/` |
| Fabric | `config/irisworldgen/packs/overworld/` |
| Forge / NeoForge | `config/irisworldgen/packs/overworld/` |
Worlds created from a pack store a **copy** at:
```
<world>/iris/pack/
```
`StudioSVC.installIntoWorld` and `replaceIntoWorld` copy the source pack tree into that directory. Runtime generation for a normal world reads the world copy, not the global `packs/` tree. Studio worlds hotload the pack under `packs/` directly.
First install often downloads the default overworld release into `packs/` (`downloadDefaultOverworld` / `/iris download` flows — see `02 - Getting Started.md`, `25 - Pack Management.md`).
## High-level layout (shipping overworld)
```
overworld/
dimensions/overworld.json # root dimension (load key: overworld)
regions/*.json # frozen, hot, temperate, tropical, ...
biomes/<folder>/*.json # temperate/, hot/, carving/, vanilla/, ...
generators/*.json # plain, mountain, ocean, flat, ...
loot/... # global-clutter, temperate/food, ...
entities/standard/...
spawners/<climate>/...
objects/... # .iob schematics
structures/, jigsaw-*, ...
snippet/decorator/, snippet/style/
```
Dimension load key is `overworld` (`dimensions/overworld.json`).
## Dimension snapshot (real keys)
From `dimensions/overworld.json` (selected fields):
| Field | Shipping value (probe) |
|-------|------------------------|
| `name` | `"Overworld"` |
| `version` | `4000` |
| `fluidHeight` | `50` |
| `logicalHeight` | `512` |
| `dimensionHeight` | `min` -256, `max` 512 |
| `landChance` | `0.69` |
| `regionZoom` | `16.15` |
| `environment` | `NORMAL` |
| `regions` | `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics` |
| `loot` | mode `FALLBACK`, tables `["global-clutter"]` |
| `preventLeafDecay` | `true` |
| `useMantle` | `true` |
| `carvingEnabled` / `decorate` | `true` |
Also present: continental/region/biome styles, deposits, depositVariants, caveProfile, carving band entries, imported structure controls, structure placements. Do not invent biome or region keys; list directories under `regions/` and `biomes/` when adding content.
## Region and biome paths
Example region: `regions/temperate.json`
- `landBiomes` includes keys such as `temperate/plains`, `temperate/oak-forest`, `vanilla/cherry_grove`, `mountain/plains`
- `shoreBiomes` e.g. `temperate/shore/beach`
- `seaBiomes` e.g. `ocean/deep`, `temperate/sea/river`
- `caveBiomes` e.g. `carving/rocky-cavebiome`, `carving/drip`
- `loot`: mode `FALLBACK`, tables `temperate/clutter`, `temperate/food`
Example biome: `biomes/temperate/plains.json`
- `derivative` / `vanillaDerivative`: `minecraft:plains`
- `generators`: `[{ "generator": "plain", "min": 4, "max": 10 }]`
- `layers`: grass → dirt → stone stack
- `objects`: placements referencing `clutter/...` object keys
Generator referenced by that biome: `generators/plain.json` (composite IRIS_DOUBLE noise + bilinear starcast interpolator).
## Safe editing workflow
### Prefer studio for authoring
1. Ensure overworld exists under `packs/overworld/`.
2. Open studio: `/iris studio open overworld` (optional seed).
3. Edit files under `packs/overworld/` with VSCode workspace / schemas (`10 - Studio & VSCode Schemas.md`).
4. Hotload picks up JSON changes in the studio world. Regenerate or move to see new terrain.
5. Use focus fields on the dimension for isolation:
- `"focus": "temperate/plains"` — only that biome
- `"focusRegion": "temperate"` — only that region
6. Close studio when finished: `/iris studio close`.
Studio is the live pack. Production worlds still run on their `iris/pack` snapshot until updated.
### Do not edit the world copy as the source of truth
Editing `<world>/iris/pack/` only affects that world and is overwritten by pack install/update. Keep authoring in `packs/overworld/` (or a forked pack folder).
### Fork if you will diverge permanently
```
/iris studio create name=my-overworld template=overworld
```
Copies the overworld pack into a new pack key. Create worlds with `my-overworld` so upstream overworld updates do not clobber custom work.
## Applying changes to production worlds
World create installs a pack copy once. Changing `packs/overworld/` does **not** automatically update existing worlds.
### Bukkit: `/iris dev update-world`
```
/iris dev update-world world=<world> pack=overworld confirm=true
```
Optional: `fresh-download` re-downloads the pack before install.
Behavior (`CommandDeveloper.updateWorld``StudioSVC.replaceIntoWorld`):
1. Requires `confirm=true` (otherwise prints warning only).
2. Optionally re-downloads the pack.
3. Replaces `<world>/iris/pack/` with a fresh copy of the source pack.
4. Marked **UNSAFE** in the command description — already-generated chunks keep old terrain; only newly generated chunks use the new pack content for most features. Backup the world first.
### When to use update-world vs new world
| Goal | Approach |
|------|----------|
| Live design iteration | Studio open on `packs/` |
| Ship pack changes to existing survival world | Backup → `update-world ... confirm` |
| Guaranteed clean terrain | New world with the updated pack |
| Partial experimental changes | Fork pack (`studio create`) |
## Practical edit recipes
### Change sea level
In `dimensions/overworld.json` set `fluidHeight` (shipping `50`). Height is relative to `dimensionHeight.min` as documented on `IrisDimension`. Restart or hotload; expect shoreline shifts on new chunks only.
### Add a biome to temperate
1. Create `biomes/temperate/my-biome.json` with required `name`, `derivative`, `layers`, `generators` (see `26 - Example - Minimal Dimension.md`, `13 - Biomes.md`).
2. Append `"temperate/my-biome"` to `regions/temperate.json``landBiomes` (or sea/shore/cave lists as appropriate).
3. Studio hotload; sample locations with what/teleport tools.
Never invent keys that do not exist as files. Region lists must match real biome load keys.
### Tweak plains height
Edit `biomes/temperate/plains.json` generators min/max, or edit shared `generators/plain.json` (affects every biome using `plain`).
### Loot
- Dimension fallback: `dimensions/overworld.json``loot.tables`
- Region: e.g. `regions/temperate.json``loot`
- Tables live under `loot/` (`global-clutter`, `global-treasure`, `temperate/food`, …)
### Decorators via snippets
Reuse `snippet/decorator/*` and `snippet/style/*` as in `24 - Pack Mods & Snippets.md`. Example references already appear in `biomes/vanilla/old_growth_birch_forest.json` and dimension ore `chanceStyle` fields.
### Entities and spawners
Overworld ships `entities/standard/**` and `spawners/**`. Ambient Iris spawning requires listing keys on `entitySpawners` of dimension, region, or biome. Marker-based spawning requires markers + object placement `markers` arrays. See `23 - Loot, Entities, Spawners, Markers.md`.
## Validation and packaging
| Task | Command |
|------|---------|
| Validate pack | Bukkit: `/iris pack validate pack=overworld`; modded: `/iris pack validate overworld` |
| Cleanup unused resources | Bukkit: `/iris pack cleanup overworld mode=preview`, then `mode=apply`; modded uses `preview`/`apply` literals |
| Package for distribution | Bukkit: `/iris studio package dimension=overworld`; modded: `/iris studio package overworld` |
| Version stamp | Dimension `version` field (overworld uses large ints such as `4000`) |
## Checklist before production update
1. Edit and verify in studio, not only by reading JSON.
2. Run pack validate; fix broken keys.
3. Backup the target world folder.
4. Run `update-world` with `confirm` (and optional fresh download).
5. Explore **new** chunks for expected results; do not expect wholesale remesh of old chunks.
6. Record operator-facing changes in workspace changelog when releasing.
## Cross-links
- Minimal greenfield pack: `26 - Example - Minimal Dimension.md`
- Dimension field reference: `11 - Dimensions.md`
- Commands matrix: `04 - Commands & Permissions.md`
- Pack download/validate/package: `25 - Pack Management.md`
+123
View File
@@ -0,0 +1,123 @@
# 28 - Integrations
Iris integrates with selected Bukkit plugins for world management, selections, external blocks/items/entities, Mythic skill conditions, PlaceholderAPI, and tree felling. Soft-depends declare load order only; Iris still checks `isPluginEnabled` / readiness before use. Integrations are Bukkit-family unless noted. See also `04 - Commands & Permissions.md`, `06 - Worlds & Lifecycle.md`, `09 - PlaceholderAPI.md`, `19 - Objects.md`, and `93 - API - Tree Feller.md`.
## Soft-depends and load order (`plugin.yml`)
| Plugin | Relation | Role |
|---|---|---|
| PlaceholderAPI | softdepend | `%iris_...%` expansion |
| CraftEngine | softdepend | External blocks/items |
| Nexo | softdepend | External blocks/items |
| ItemsAdder | softdepend | External blocks/items |
| SCore | softdepend | Dependency of ExecutableItems ecosystems; no Iris provider class |
| ExecutableItems | softdepend | External items |
| MythicLib | softdepend | Dependency of MMOItems ecosystems; no Iris provider class |
| MMOItems | softdepend | External blocks/items |
| eco | softdepend | Dependency of EcoItems; no Iris provider class |
| EcoItems | softdepend | External items |
| MythicMobs | softdepend | External entities + skill conditions |
| MythicCrucible | softdepend | External blocks/items |
| KGenerators | softdepend | External blocks/items |
| WorldEdit | softdepend | Selection for object/wand workflows |
| Multiverse-Core | `loadbefore` (Iris loads first) | World import/generator sync and remove |
Multiverse is listed under `loadbefore`, not softdepend: Iris enables before Multiverse-Core so Multiverse can see Iris as a generator plugin.
## WorldEdit
`WorldEditLink` reflects into WorldEdit when the plugin is enabled.
| Use | Behavior |
|---|---|
| Selection read | Returns an Iris `Cuboid` for the player's current WorldEdit selection in their world, or `null` if none |
| Wand / object tools | With `world.worldEditWandCUI` default `true`, a WorldEdit selection is accepted where Iris wants a selection without holding the Iris wand |
| `/iris object we` | Converts the current WorldEdit selection into a real Iris wand selection |
| Limit | `position2` does not work on a WorldEdit-only selection until `/iris object we` runs |
WorldEdit is not required for `.schem` import: Iris parses schematic NBT itself. See `19 - Objects.md`.
## Multiverse-Core
`MultiverseCoreLink` uses the Multiverse Core API when Multiverse-Core is enabled.
| Operation | Behavior |
|---|---|
| World create/update | Imports or updates the Multiverse world with generator `Iris:<pack>`, `autoLoad=false`, environment from the Bukkit world, spawn adjust off |
| World remove | Removes the Multiverse world config entry and saves worlds config; throws if Multiverse refuses removal |
| Inactive Multiverse | Calls no-op / return `false` |
Studio open/close and world lifecycle paths use the same link when Multiverse is present. See `06 - Worlds & Lifecycle.md`.
## External data providers (item / block / entity plugins)
`ExternalDataSVC` activates built-in providers when their plugins are ready, including late enable via `PluginEnableEvent`. Packs reference external content by namespaced ids resolved through the active provider for that namespace and `DataType` (`ITEM`, `BLOCK`, `ENTITY`).
| Plugin id | Provider | Namespaces / match | Types |
|---|---|---|---|
| CraftEngine | `CraftEngineDataProvider` | Any CraftEngine item/block/furniture key that exists | ITEM, BLOCK |
| Nexo | `NexoDataProvider` | `nexo` | ITEM, BLOCK |
| ItemsAdder | `ItemAdderDataProvider` | Namespaces reported by ItemsAdder for items/blocks | ITEM, BLOCK |
| ExecutableItems | `ExecutableItemsDataProvider` | `executable_items` | ITEM |
| MMOItems | `MMOItemsDataProvider` | Blocks: `mmoitems`; items: two-part type namespace (`type_subtype:id`) | ITEM, BLOCK |
| EcoItems | `EcoItemsDataProvider` | `ecoitems` | ITEM |
| MythicMobs | `MythicMobsDataProvider` | `mythicmobs` | ENTITY |
| MythicCrucible | `MythicCrucibleDataProvider` | `crucible` | ITEM, BLOCK |
| KGenerators | `KGeneratorsDataProvider` | `kgenerators` | ITEM, BLOCK |
Third parties may register additional providers with `ExternalDataSVC#registerProvider` if the plugin id is not already taken. Missing resources log and resolve empty rather than crashing generation.
## MythicMobs skill conditions
When MythicMobs is active, Iris registers location conditions:
| Condition | Fields | Check |
|---|---|---|
| `irisbiome` | `biome`/`b` (comma list of load keys), `surface`/`s` (boolean, default false) | Surface biome or full column biome via Iris engine |
| `irisregion` | `region`/`r` (comma list of load keys) | Region load key at X/Z |
Both return false outside Iris worlds or when engine access is missing.
## PlaceholderAPI
Soft-depend + expansion id `iris`. Full key list and migration: `09 - PlaceholderAPI.md`.
## Tree feller (operator)
Standalone tree felling is Bukkit-only. It removes whole Iris-generated trees when a sneaking survival player breaks a provenanced log with an axe.
### Settings (`settings.json`)
| Key | Default | Meaning |
|---|---|---|
| `treeFeller.enabled` | `false` | Master switch for the **standalone** path only |
| `treeFeller.durabilityPreservationChance` | `0` | Percent chance a log costs no axe durability (standalone path); clamped `0..100` on read |
### Permission
| Node | Default | Meaning |
|---|---|---|
| `iris.treefeller` | `op` | Required for standalone felling |
### Standalone requirements (all required)
- `treeFeller.enabled` is `true`
- Player has `iris.treefeller`
- `GameMode.SURVIVAL`
- Player is sneaking
- Broken block is tagged as a log
- Main-hand item is an axe
- Block has Iris tree provenance in the mantle (Iris-placed tree, not player-planted saplings or hand-placed logs)
Iris listens at `EventPriority.HIGHEST` for its own standalone request. Other plugins can drive felling with `INTEGRATION_OVERRIDE` (bypasses enabled switch and permission only) through `IrisTreeFellerService`; that API is documented in `93 - API - Tree Feller.md`.
### Runtime notes for operators
- Discovery walks mantle provenance (bounds: 131072 members, 1e6 visits, 256-block axis distance); oversize trees fall back to removing only the broken block
- Removal is paced across ticks
- Run ends if the player stops sneaking, changes hotbar slot/hand, leaves survival/world, breaks the axe, or swaps the axe item
- Leaves never cost durability/cost hooks; logs do
## Platforms
WorldEdit, Multiverse, external data providers, Mythic conditions, PlaceholderAPI, and the tree feller are Bukkit-family. Modded loaders do not use these plugin soft-depends. See `30 - Platform Differences.md`.
+128
View File
@@ -0,0 +1,128 @@
# 29 - Client HUD & Protocol
The Iris client mod (Fabric/Forge/NeoForge jar on the client) adds a native pregeneration HUD, Vision map, What overlay, studio toasts, and singleplayer world-type entries. It talks to Iris servers over the shared channel `irisworldgen:main`. Vanilla clients ignore the channel and use server-side fallbacks. See also `07 - Pregeneration.md`, `08 - Localization.md`, `10 - Studio & VSCode Schemas.md`, and `30 - Platform Differences.md`.
## When the client mod does something
| Server | Client without Iris | Client with Iris mod |
|---|---|---|
| Modded Iris | Boss bar / status for pregen | Native HUD over custom payloads |
| Bukkit/Paper Iris | Console/status and any Bukkit HUD lanes; no Iris client protocol features | Same native HUD/Vision/What over plugin messaging on `irisworldgen:main` |
| Non-Iris server | N/A | Client mod is inert after hello fails / no Iris |
Singleplayer: installed packs appear as selectable World Types; the integrated server runs the same engine.
## Keybinds
Category: **Iris** (`key.categories.irisworldgen.iris`). Defaults:
| Action | Default key | Translation key |
|---|---|---|
| Toggle pregen HUD | `H` | `key.irisworldgen.toggle_pregen_hud` |
| Open Iris Vision map | `M` | `key.irisworldgen.open_vision_map` |
| Toggle Iris What overlay | `J` | `key.irisworldgen.toggle_what_overlay` |
Keys are rebindable in Controls under the Iris category. HUD visibility defaults on (`hudVisible = true`). What overlay defaults off. F1 hide-gui still advances toasts via a separate tick so they are not stranded.
## Pregen HUD
Top-left panel (`ORIGIN` 6,6) while a live pregen job is tracked and not expired:
| Element | Content |
|---|---|
| Title | Localized pregen header |
| Stats | `done / total (percent%)` |
| Bar | Green while running, yellow while paused, muted gray when stale |
| Tail | Rate, optional ETA, `PAUSED`, or “no updates for Ns” when stale |
| Minimap | Optional region grid when region deltas exist (pending / generating / done cells) |
Stale and expire timers (client-side, from last received progress frame):
| Threshold | Value | Effect |
|---|---|---|
| Stale | 5 s | Panel mutes colors and shows stale label |
| Expire | 30 s | Panel stops drawing |
`PregenEnd` clears the job immediately. Hide-gui (F1) skips layered HUD draw; toasts still pump.
## Boss bar fallback
Modded servers: `ModdedPregenBossBar` shows a green (running) / yellow (paused) boss bar to the player who started pregen **only if** that player does not already have a ready protocol session with `CAPABILITY_PREGEN`. Clients with a working pregen HUD skip the boss bar. Bar title uses localized `iris.runtime.pregen.bossbar.*` strings; progress updates every 10 ticks.
Bukkit-family servers: players without the client mod do not get this modded boss-bar path; use `/iris pregen status`, logging, and any server HUD lanes. Clients with the mod still receive protocol pregen frames over plugin messaging.
## Vision map and What overlay
| Feature | Capability | Notes |
|---|---|---|
| Vision map (`M`) | `CAPABILITY_VISION` | Full-screen map; drag pan, scroll zoom, Esc close; needs ready session + Iris dimension |
| What overlay (`J`) | `CAPABILITY_CURSOR` | Cursor column query: biome, region, cave biome, height |
| Studio toasts | Client advertises `CAPABILITY_STUDIO` | Hotload/toast frames when the server sends them |
| Dimension status | Always after hello | Pack/dimension/height bounds; non-Iris worlds clear tiles/markers |
Vision tiles arrive chunked (max payload per chunk 24000 bytes; header 25 bytes). Markers capped at 256 per frame.
## Protocol channel
| Constant | Value |
|---|---|
| Channel | `irisworldgen:main` |
| Protocol version | `1` |
| Transport (modded) | Custom payloads on the play channel |
| Transport (Bukkit) | Plugin messaging in/out on the same channel name |
| Max frame | 24576 bytes |
| Max inbound frames / client / s | 32 |
| Max vision tile requests / s | 8 |
| Max cursor info requests / s | 4 |
| Max query `|block|` coordinate | 29_999_999 |
Internal wire types (`IrisProtocol.TYPE_*`):
| Id | Direction | Message |
|---|---|---|
| 1 | C→S | `ClientHello` (version, capabilities) |
| 2 | S→C | `ServerHello` (version, capabilities, brand, irisActive) |
| 3 | S→C | `PregenProgress` |
| 4 | S→C | `PregenEnd` |
| 5 | S→C | `DimensionStatus` |
| 6 | C→S | `CursorInfoRequest` |
| 7 | S→C | `CursorInfo` |
| 8 | C→S | `VisionTileRequest` |
| 9 | S→C | `VisionTile` (chunked) |
| 10 | S→C | `VisionMarkers` |
| 11 | S→C | `PregenRegionDelta` |
| 12 | S→C | `StudioHotload` |
| 13 | S→C | `Toast` |
Capability bits:
| Bit | Name | Meaning |
|---|---|---|
| `1 << 0` | `CAPABILITY_PREGEN` | Pregen progress / end / region deltas |
| `1 << 1` | `CAPABILITY_VISION` | Vision tiles and markers |
| `1 << 2` | `CAPABILITY_CURSOR` | Cursor column lookups |
| `1 << 3` | `CAPABILITY_STUDIO` | Studio hotload notifications |
Client hello advertises all four. Bukkit and modded servers grant `PREGEN | VISION | CURSOR | STUDIO`. Negotiated capabilities are the intersection of what the client advertises and what the server grants.
## Handshake
1. Client joins world → sends `ClientHello` with protocol version `1` and client capabilities.
2. Retries every 2 s, max 5 attempts; failure → session `UNSUPPORTED`.
3. Version mismatch → session `INCOMPATIBLE` (UI can report mismatch).
4. Match → session `READY`; dimension status and feature frames follow.
5. Disconnect clears session and world-local client state (pregen, tiles, markers, cursor, toasts).
Server drops frames before hello, rate-limits, rejects oversized/malformed frames, and rejects out-of-bounds cursor queries without clamping.
## Localization touchpoints
Server-side locale (`general.language`) drives boss bar and many shared UI strings. Client keybind labels use `assets/irisworldgen/lang/*.json`. Vision/What/pregen HUD strings use `ClientUiMessages` through `IrisLanguage`. See `08 - Localization.md`.
## Operator verification
- Modded server + Iris client: pregen progress on HUD; boss bar absent for that player when protocol pregen capability is ready
- Bukkit Iris + Iris client: same HUD over plugin messaging
- Vanilla client on either server: no protocol traffic effects; modded boss bar path as above
- Non-Iris server + Iris client: mod inert
- H toggles HUD; M opens Vision when available; J toggles What when available
+140
View File
@@ -0,0 +1,140 @@
# 30 - Platform Differences
Iris runs the same generation core on Bukkit-family servers and on Fabric, Forge, and NeoForge. Adapters differ in world lifecycle, command surface, permissions, datapacks, and optional tools. Shared config is `settings.json`; mod loaders add `modded.json`. See `01 - Installation & Platforms.md`, `03 - Configuration.md`, and `04 - Commands & Permissions.md`.
## Artifacts and entry points
| Surface | Artifact | Bootstrap |
|---------|----------|-----------|
| Bukkit / Paper / Folia | CraftBukkit-shaded plugin jar | `plugin.yml` / `paper-plugin.yml`, `folia-supported: true`, load `STARTUP` |
| Fabric | Fabric mod jar | `IrisFabricBootstrap` registers commands and services |
| Forge | Forge mod jar | `IrisForgeBootstrap` |
| NeoForge | NeoForge mod jar | `IrisNeoForgeBootstrap` |
Core engine: `core/`. Shared modded logic: `adapters/modded-common/`. SPI: `spi/`.
## Data directories
| Item | Bukkit | Fabric / Forge / NeoForge |
|------|--------|---------------------------|
| Settings | `plugins/Iris/settings.json` | `<configDir>/iris/settings.json` |
| Packs | `plugins/Iris/packs/` | `<configDir>/irisworldgen/packs/` |
| Mod config | — | `<configDir>/irisworldgen/modded.json` |
| World datapacks | world `datapacks/` + Iris ingest | world `datapacks/`; dimension-type pack name `iris` under `data/irisworldgen/dimension_type/` |
| Dump / developer files | under plugin data folder | under mod data folder |
| Persistent dynamic-world registry | `worlds.json` in plugin data | `<world-root>/iris/iris-dimensions.json` |
Hotload: Bukkit file-watch engine; modded 3s poll. Same invalidate/reload/locale path.
## World model
| Concern | Bukkit | Modded |
|---------|--------|--------|
| Create | `/iris create` → managed world name, generator Iris, optional main-world | `/iris create` or `/iris world enable` → dimension id + pack injection |
| Load / unload | `/iris load` (`import`), `/iris unload` | `/iris world disable` unloads; no separate load command |
| Remove / delete | `/iris remove` optional folder delete | `/iris world delete` wipes chunk/mantle data |
| Primary / main world | create `main=true` and Bukkit yml registration paths | `modded.json` primary + `routePlayersToPrimaryWorld`; `/iris world mainworld`, `replace-overworld` |
| Evacuate | `/iris evacuate <world>` | `/iris evacuate [dimension]` → primary/overworld fallback |
| Studio world | Transient studio world via StudioSVC | Studio dimension under `irisworldgen:studio_*` |
| Folia | Regionized schedulers; pregen `runtimeSchedulerMode` forces `FOLIA` when regionized | N/A (not Bukkit Folia) |
Default pack bootstrap still downloads the IrisDimensions overworld release into `packs/overworld` when missing (shared provisioner).
Modded startup quarantines a corrupt persistent-dimension registry as `iris-dimensions.json.broken-<timestamp>` and continues without those dynamic worlds. Recovery details are in `06 - Worlds & Lifecycle.md`.
## Commands and permissions
| Concern | Bukkit | Modded |
|---------|--------|--------|
| Parser | VolmLib Director; `key=value` optionals | Brigadier; ordered args and flag literals |
| Root aliases | `iris`, `ir`, `irs` | same + redirects |
| Staff gate | `iris.all` (declared in `plugin.yml` and `paper-plugin.yml`, default `op`) | `LEVEL_GAMEMASTERS` for mutations |
| Public inspect | same gate as staff (`iris.all` required for all `/iris`) | `LEVEL_ALL` for version/info/height/metrics/what/help |
| Tree feller | `iris.treefeller` (plugin.yml, default op) | `irisworldgen:treefeller` (Fabric); Forge/NeoForge PermissionAPI node |
| Help | Director mini-menu | `ModdedCommandHelp` sections + clickable pages |
Full command tables and stubs: `04 - Commands & Permissions.md`.
## Feature matrix
| Feature | Bukkit | Fabric | Forge | NeoForge |
|---------|--------|--------|-------|----------|
| Core terrain / biomes / objects / jigsaw | yes | yes | yes | yes |
| Pack validate / cleanup / download | yes | yes | yes | yes |
| Pregen | yes (Paper-like / Folia modes) | yes (`moddedPregenInFlight`) | yes | yes |
| Studio open/close/vscode/package | yes | yes | yes | yes |
| Object wand / paste / save / undo | yes | yes | yes | yes |
| Object expand selection | no | yes | yes | yes |
| Object WorldEdit import | yes (WorldEdit soft depend) | no (stub) | no | no |
| Object studio world | yes | no (stub) | no | no |
| Schematic convert | yes | no (stub) | no | no |
| Structure import / capture | yes (NMS) | message only | message only | message only |
| Structure list / info / place / verify | yes | yes | yes | yes |
| Datapack Modrinth ingest/remove | yes | message only | message only | message only |
| Dimension-type datapack install/status | N/A / different path | yes | yes | yes |
| Studio loot GUI / entity spawn / profile / objects report | yes | no (stub) | no | no |
| Studio importvanilla | yes | message (run on Bukkit) | message | message |
| PlaceholderAPI | soft depend | no | no | no |
| Multiverse-Core | soft depend / loadbefore | no | no | no |
| Item plugins (ItemsAdder, Mythic, etc.) | paper soft deps | loader-specific / limited | limited | limited |
| Public API package `art.arcane.iris.api` | plugin jar | see `94 - API - Modded.md` | same | same |
| Client HUD / protocol | optional client mod | optional client mod | optional | optional |
| Tree feller | settings + `iris.treefeller` | settings + platform permission | same | same |
| Auto Spigot/Paper timeout config | yes | no | no | no |
| Custom biome restart prompts | yes (`iris.all` / op) | different datapack flow | same | same |
## Settings that are platform-sensitive
| Setting | Notes |
|---------|-------|
| `pregen.runtimeSchedulerMode` | Resolves using Bukkit/Folia detection; regionized always Folia |
| `pregen.paperLikeBackendMode` | Bukkit Paper-like pregen ticket vs service |
| `pregen.moddedPregenInFlight` | Modded pregen concurrency budget |
| `autoConfiguration.*` | Spigot/Paper server.properties/watchdog (Bukkit) |
| `world.worldEditWandCUI` | WorldEdit present on Bukkit |
| `general.autoIngestDatapacks` / `autoImportDatapackStructures` | Bukkit datapack ingest pipeline primary consumer |
| `gui.useServerLaunchedGuis` | Both; host implementation differs (`BukkitGuiHost` vs `ModdedGuiHost`) |
`modded.json` keys exist only on mod loaders.
## Integrations
| Integration | Bukkit | Modded |
|-------------|--------|--------|
| WorldEdit | soft depend; object `we` | not wired |
| Multiverse-Core | load order / link | not used |
| PlaceholderAPI | `%iris_…%` | no |
| MythicMobs / item plugins | paper-plugin optional deps | not the Bukkit pipeline |
| Tree feller | plugin permission | loader permission node |
See `28 - Integrations.md`, `09 - PlaceholderAPI.md`.
## NMS / version binding
- Bukkit plugin binds to a specific Paper/CraftBukkit revision (v26 NMS module in-tree).
- Structure import/capture and vanilla import studio paths require that NMS binding.
- Mod adapters use Minecraft mappings for the same game version line without the Bukkit plugin APIs.
## Determinism and parity
Goldenhash and genhash exist on both surfaces (command placement differs: Bukkit under `developer`, modded often at root). Use disposable worlds; mantle reset options exist on Bukkit goldenhash. See `32 - Determinism & Goldenhash.md`.
## Operator checklist when moving packs between platforms
1. Copy `packs/<key>/` between data folders.
2. Structure/vanilla imports that need Bukkit: run import on Bukkit, then copy the pack to the mod server.
3. Align `settings.json` keys that matter for generation (`generator`, `performance`, `treeFeller`); ignore Bukkit-only autoConfiguration if unused.
4. On modded, set `modded.json` primary/main-world if you need overworld replacement.
5. Re-run `/iris pack validate` and `/iris datapack status` (modded) or ingest (Bukkit) after moves.
## Related
- `01 - Installation & Platforms.md`
- `03 - Configuration.md`
- `04 - Commands & Permissions.md`
- `06 - Worlds & Lifecycle.md`
- `07 - Pregeneration.md`
- `22 - Native Structures & Datapacks.md`
- `28 - Integrations.md`
- `29 - Client HUD & Protocol.md`
- `94 - API - Modded.md`
@@ -0,0 +1,177 @@
# 31 - Operator Runbooks & Smoke Tests
Manual verification sequences for operators and maintainers after install, upgrade, pack change, or release candidate build. Each runbook ends when the stated gate passes. Full command trees and permissions live in `04 - Commands & Permissions.md`; pregen options in `07 - Pregeneration.md`; platform differences in `30 - Platform Differences.md`.
## Fixed inputs for parity smoke
Use the same inputs whenever comparing platforms or runs:
| Input | Typical value | Notes |
|-------|---------------|--------|
| Pack | Shipping default `overworld` (or a frozen pack copy) | Byte-identical pack on every platform under test |
| Seed | `1337` | World seed on Bukkit; Iris engine seed on modded |
| GoldenHash radius | `22` chunks (optional smaller `8` for quick smoke) | Chunk count = `(2r+1)²`; radius 22 = 2,025 chunks |
| GoldenHash threads | `1` for strict serial; `8` default for multi-thread smoke | `threads=1` catches order-dependence |
| Pregen radius | `352` blocks for 2,025-chunk square when centered at 0,0 | Radius is in **blocks**, not chunks |
GoldenHash details and file layout: `32 - Determinism & Goldenhash.md`.
## A. Fresh install and first world (Bukkit-family)
1. Install the CraftBukkit-family jar into `plugins/` (Paper, Purpur, Folia, Spigot, Leaf, Canvas as advertised). Require Java 25. See `01 - Installation & Platforms.md`.
2. Start the server once. Confirm Iris enables, default pack download completes when no pack is present, and `settings.json` is written under the Iris data directory.
3. Create a world with a fixed seed and teleport into it:
```
/iris create smoke-ow type=overworld seed=1337
/iris tp smoke-ow
```
4. Join or teleport into the world. Confirm non-empty terrain, surface biomes, and no repeating console stack traces on first chunks.
5. Gate: world is loaded as an Iris world; chunks generate without enable-time crash; console shows no fatal engine init failure.
## B. Fresh install and first world (Fabric / Forge / NeoForge)
1. Install the matching mod jar into `mods/`. Fabric requires Loader ≥ declared floor; Forge/NeoForge require their declared floors. See `01 - Installation & Platforms.md` and `30 - Platform Differences.md`.
2. Start dedicated server (or integrated singleplayer for client-mod smoke). Confirm Iris boots, default pack installs, and datapack/biome registration completes.
3. Create a world with fixed seed (positional mod syntax):
```
/iris create smoke-ow overworld 1337
```
4. Enter the dimension. Confirm non-empty generation and custom-biome registration where the pack defines custom biomes.
5. Gate: same as Bukkit section A for generation health; document intentional capability gaps only via `30 - Platform Differences.md`.
## C. Pack validation smoke
1. With packs installed:
```
/iris pack validate
```
Or a single pack: `/iris pack validate pack=<pack>` on Bukkit, `/iris pack validate <pack>` on modded.
2. Review blocking errors vs warnings. Blocking errors must be fixed before treating the pack as production-ready.
3. Optional: `/iris pack status` replays the last recorded validation result for the session.
4. Gate: target pack is loadable; no unexpected blocking errors on the shipping default pack. Cleanup/restore flows are separate and opt-in (`25 - Pack Management.md`).
## D. Pregeneration control smoke
Radius is always in **blocks**. Prefer a disposable test world.
**Bukkit (keyed optional args):**
```
/iris pregen start 352 world=smoke-ow center=0,0 gui=false
/iris pregen status
/iris pregen pause
/iris pregen status
/iris pregen pause
/iris pregen stop
```
Strict one-chunk-at-a-time mode (Paper-compatible only; not Folia serial gate):
```
/iris pregen start 352 world=smoke-ow center=0,0 gui=false serial=true
```
**Modded (positional / flag composition):**
```
/iris pregen start 352 irisworldgen:smoke-ow at 0 0 sync
/iris pregen status
```
Use `sync` / in-flight caps as documented in `07 - Pregeneration.md`. Pause/resume/stop availability follows the modded pregen command surface.
Gates:
- Start reports the correct world, center, and size.
- Status shows generated/total, percent, speed, and failed count when any.
- Pause freezes progress; second pause resumes.
- Stop cancels without claiming full success when work remains.
- A full serial/sync 2,025-chunk run (radius 352 at 0,0) completes with zero failed chunks for release-level evidence.
Client HUD: with the Iris client mod, pregen progress arrives on channel `irisworldgen:main`; vanilla clients use boss bar / console only (`29 - Client HUD & Protocol.md`).
## E. GoldenHash determinism smoke
Run on a **disposable** Iris world. GoldenHash generates into buffers (does not write world blocks) but **resets mantle** by default — treat the world as expendable.
**Bukkit** (`AUTO`: capture if golden file missing, verify if present):
```
/iris developer goldenhash world=smoke-ow radius=22 threads=1
```
Optional: `center-x=0 center-z=0 reset-mantle=true deep=false`. Defaults: radius `8`, threads `8`, reset-mantle `true`, center `0,0`.
**Modded** (center fixed at chunk 0,0; mantle always reset):
```
/iris goldenhash 22 1 capture
/iris goldenhash 22 1 verify
```
Alias: `/iris gold …`. Defaults without args: radius `8`, threads `8`, mode `AUTO`.
Gates:
- Capture writes a `.hashes` file under the platform golden directory.
- Second run with the same pack/seed/radius/center reports **MATCH** and the same combined hash.
- The same pack+seed+radius+center hash matches across Bukkit, Fabric, Forge, and NeoForge when comparing identical artifacts and pack bytes. Cross-platform rule: `32 - Determinism & Goldenhash.md`.
## F. Restart and existing-world smoke
1. After some pregen or free exploration, stop the server cleanly.
2. Start again without deleting world data.
3. Load the same Iris world; generate new chunks outside the pregenerated area.
4. Gate: world loads; new chunks generate; no blank-chunk regression on restart; pregen cache resume behaves as documented when a job is resumed (`07 - Pregeneration.md`).
## G. Studio smoke (authoring path)
```
/iris studio open overworld seed=1337
```
Edit a pack file on disk (or via the VSCode workspace from `/iris studio vscode dimension=overworld` on Bukkit). Confirm hotload applies without server restart when supported. Close with `/iris studio close` (studio worlds are transient and discarded).
Gate: studio world opens; hotload either applies successfully or fails closed without poisoning the live engine for non-studio worlds. Studio details: `10 - Studio & VSCode Schemas.md`.
## H. Offline probe module (no live server)
From the Iris project root (JDK 25). These are CI-oriented gates, not in-game commands.
| Task | Purpose |
|------|---------|
| `./gradlew :probe:run` (ClassloadProbe) | Loads compiled `core` classes without `org.bukkit` on the runtime classpath; fails on purity violations outside the allowlist |
| `./gradlew :probe:deserializationProbe` | Deserializes fixture entity/spawner/loot JSON through real Iris loaders on a Bukkit-free JVM |
| `./gradlew :probe:genProbe -PprobePack=/path/to/packs/overworld` | Builds a real engine for dimension `overworld`, seed `1337`, generates a chunk spiral into buffers |
`genProbe` properties: `probePack` (required usable pack path), `probeRadius` (default `2`), `probeCenterChunkX` / `probeCenterChunkZ` (default `0`). The task clones the pack into a temp directory, runs `PackValidator`, then generates.
Gate: each probe exits 0. Classload and deserialization probes are part of the release verify job when CI is green (`86 - Maintainer - Release Checklist.md`).
## I. Minimal post-upgrade checklist
After replacing the jar/mod only:
1. Boot on the same world data.
2. `/iris pack validate` on production packs.
3. Generate a few new chunks in an existing Iris world.
4. Optional short GoldenHash verify against a stored baseline if the pack and seed are unchanged (`32 - Determinism & Goldenhash.md`).
5. If pregen was mid-job, confirm status/resume or cancel cleanly (`07 - Pregeneration.md`).
Gate: no enable crash, packs still loadable, generation continues.
## J. Failure triage order
1. Confirm Java 25 and correct platform artifact (`01 - Installation & Platforms.md`).
2. Confirm pack validates and dimension key exists (`25 - Pack Management.md`, `05 - Concepts & Pack Layout.md`).
3. Confirm target is an Iris world/engine (`06 - Worlds & Lifecycle.md`).
4. Capture GoldenHash with `threads=1` and `reset-mantle=true`; if mismatch, read the written `.new` / `.diag-…` files (`32 - Determinism & Goldenhash.md`).
5. For throughput or memory issues, tune settings before changing packs (`33 - Performance Tuning.md`).
6. For release candidates, escalate to maintainer gates (`87 - Maintainer - Release Readiness.md`).
+112
View File
@@ -0,0 +1,112 @@
# 32 - Determinism & Goldenhash
GoldenHash is the cross-platform determinism gate: it generates chunks into in-memory buffers (no world block writes), hashes blocks and biomes, and either captures a baseline file or verifies against one. Identical pack bytes, Iris seed, radius, center, and height range must produce the same combined hash on Bukkit-family and every mod loader. Operator smoke sequences that use this gate are in `31 - Operator Runbooks & Smoke Tests.md`.
## What it measures
- **Blocks:** every local column `x,z ∈ [0,15]` and every `y` from engine/world min height (inclusive) to max height (exclusive). Each block state key (for example `minecraft:stone`) is fed into a per-chunk SHA-256 digest.
- **Biomes:** same height span sampled on a 4-block step in x, y, and z (`BIOME_STEP = 4`). Null biome samples hash as `minecraft:plains` (constant `GoldenHashEngine.FALLBACK_BIOME_KEY`).
- **Combined hash:** SHA-256 over the ordered per-chunk lines for the full spiral; stored as `#combined=<hex>` in the golden file.
- **World disk:** buffers only — the Minecraft region files are not written by the scan. **Mantle** (Iris multi-chunk structure/carving cache) **is reset** when `reset-mantle` is true (Bukkit default) or always on modded.
Use disposable test worlds. Mantle reset deletes mantle files under the engine mantle data folder so regeneration starts from a clean mantle state.
## File location and name
| Platform | Golden directory |
|----------|------------------|
| Bukkit-family plugin | Iris data folder `golden/` (for example `plugins/Iris/golden/`) |
| Fabric / Forge / NeoForge | Loader config dir `irisworldgen/golden/` |
Filename pattern:
```
<dimensionLoadKey>-s<seed>-c<centerChunkX>x<centerChunkZ>-r<radius>.hashes
```
Example: `overworld-s1337-c0x0-r22.hashes`.
Format header: `iris-goldenhash v1`. Metadata lines include `#world`, `#dim`, `#seed`, `#mc`, `#minY`/`maxY`, `#center`, `#radius`. Body lines are `chunkX chunkZ <blockSha256> <biomeSha256>`.
On mismatch, the engine also writes:
- `<file>.new` — current scan body + combined hash
- `<file>.diag-c{x}x{z}.txt` — first mismatched chunk diagnosis (repeat-generation stability, mantle-reset comparison, non-air dump)
- Optional deep dumps when `deep=true` (Bukkit only): non-air blockstate listings under a `.deep` / `.deep-verify` sibling directory
## Modes
Shared engine modes: `AUTO`, `CAPTURE`, `VERIFY`.
| Mode | Behavior |
|------|----------|
| `CAPTURE` | Always write a new golden file |
| `VERIFY` | Fail if no golden file; compare and report MATCH/MISMATCH |
| `AUTO` | Capture when the file is missing; verify when it exists |
Bukkit command always uses `AUTO`. Modded commands expose `capture`, `verify`, and default `AUTO`.
Verify rejects seed or dimension mismatches as hard failures. Minecraft version metadata mismatch is a **warning** only (hash comparison still runs).
## Commands
### Bukkit
```
/iris developer goldenhash world=<world> radius=<chunks> threads=<n> center-x=<cx> center-z=<cz> reset-mantle=<bool> deep=<bool>
```
Alias: `gold`. Defaults: `radius=8`, `threads=8`, `center-x=0`, `center-z=0`, `reset-mantle=true`, `deep=false`. Radius must be ≥ 0. Target must be a loaded Iris world with a live engine.
- Seed recorded and used for filename: `World.getSeed()`.
- MC version string: `Bukkit.getBukkitVersion()`.
- Generation path: `engine.generate` into a `TerrainChunk` buffer.
### Modded (Fabric / Forge / NeoForge)
```
/iris goldenhash [radius] [threads] [capture|verify]
```
Alias: `gold`. Defaults: radius `8`, threads `8`, mode `AUTO`. Radius range on the command tree: `0..256`. Threads: `1..64`. Center is always chunk `0,0`. Mantle is always reset. Deep dump is not exposed.
- Seed: Iris engine seed (`engine.getSeedManager().getSeed()`), not a vanilla level-seed quirk.
- MC version: loader-reported Minecraft version string.
- Generation path: `engine.generate` into modded block/biome buffers.
- Only one modded GoldenHash scan may run at a time.
Only one shared-engine scan may be active process-wide (`GoldenHashEngine.isActive()`).
## Determinism rules operators must follow
1. **Same pack bytes** on every platform (same pack key, same files; freeze default overworld downloads to a commit/tag for release baselines).
2. **Same Iris seed** (create with `seed=1337` / positional `1337` as in `02 - Getting Started.md`).
3. **Same radius and center** (modded center is fixed at 0,0 — use `center-x=0 center-z=0` on Bukkit for parity).
4. **Same height range** (dimension min/max Y). Height is part of the hash window.
5. **Prefer `threads=1` for order-dependence checks.** Multi-thread scans must still match the serial result when the engine is deterministic; a multi-thread-only mismatch is a bug.
6. **Reset mantle** between capture and verify when comparing regeneration purity (`reset-mantle=true` / modded always).
7. **Do not treat world pregen order as the hash source** — GoldenHash re-generates into buffers. Pregen is still useful as a stress gate before or after hashing (`07 - Pregeneration.md`, `31 - Operator Runbooks & Smoke Tests.md`).
## Interpreting results
| Result | Meaning |
|--------|---------|
| Captured | New baseline written; path printed |
| MATCH | All chunk lines equal; combined hash short form shown |
| MISMATCH | One or more chunks differ; up to 10 chunk keys listed; `.new` written; diagnosis on first mismatch |
| Aborted | Not all chunks generated successfully; no golden write |
| Wrong world | Golden `#seed` / `#dim` does not match current engine |
Diagnosis labels:
- **Repeat-generation STABLE** — two back-to-back generations of the same chunk agree; divergence is order/state-dependent relative to the golden or mantle, not pure non-determinism per call.
- **Repeat-generation UNSTABLE** — same chunk differs between two consecutive generations without intervening work.
- **Mantle-reset** section compares the first generation to a generation after deleting mantle chunks in the structure radius.
## Offline generation probe (not GoldenHash files)
`./gradlew :probe:genProbe -PprobePack=…` builds an offline engine (dimension key `overworld`, seed `1337`) and prints per-chunk hashes to stdout. It validates the pack and exercises generation without a server; it does **not** read or write `iris-goldenhash v1` files. Use in-game GoldenHash for cross-platform golden files. Probe overview: `31 - Operator Runbooks & Smoke Tests.md`.
## Release gate expectation
Release procedure requires GoldenHash **VERIFY** to pass on all four platforms with the **same** combined hash for the shared pack and seed (`86 - Maintainer - Release Checklist.md`). An unexplained deterministic output change is a release blocker (`87 - Maintainer - Release Readiness.md`).
+114
View File
@@ -0,0 +1,114 @@
# 33 - Performance Tuning
Iris throughput is dominated by generation threads, mantle residency, pregen in-flight limits, cache sizes, and optional SIMD kernels. All knobs below live in `settings.json` under the Iris data directory unless noted. Settings overview: `03 - Configuration.md`. Pregen operations: `07 - Pregeneration.md`. Determinism must stay intact after tuning — verify with GoldenHash (`32 - Determinism & Goldenhash.md`).
## Where settings live
| Platform | Data directory | Settings file |
|----------|----------------|---------------|
| Bukkit-family | `plugins/Iris/` | `settings.json` |
| Fabric / Forge / NeoForge | loader config `iris/` | `settings.json` |
Hotload behavior for settings follows `03 - Configuration.md`. After changing SIMD or thread-pool related keys, restart if values are read once at kernel install / pool creation.
## Performance section (`performance`)
| Key | Default | Role |
|-----|---------|------|
| `performance.simdKernels` | `true` | When true and `jdk.incubator.vector` is available, use vector kernels; otherwise scalar fallbacks |
| `performance.mantleKeepAlive` | `30` | Mantle plate keep-alive window (seconds-scale residency control used by mantle lifecycle) |
| `performance.mantleCleanupDelay` | `200` | Delay before mantle cleanup work |
| `performance.trimMantleInStudio` | `false` | When true, studio worlds trim mantle more aggressively |
| `performance.noiseCacheSize` | `1024` | Noise sample cache capacity |
| `performance.resourceLoaderCacheSize` | `1024` | Pack resource loader cache capacity |
| `performance.objectLoaderCacheSize` | `4096` | Object (`.iob`) loader cache capacity |
| `performance.engineSVC.useVirtualThreads` | `true` | Engine service uses virtual threads when true |
| `performance.engineSVC.forceMulticoreWrite` | `false` | Force multicore write path |
| `performance.engineSVC.priority` | `Thread.NORM_PRIORITY` | Clamped to valid Java thread priorities |
| `performance.engineSVC.parallelism` | `-1` | `>0` caps at `2 * CPU`; `≤0` uses `ceil(sqrt(CPU))` |
Larger loader caches trade heap for fewer pack disk/JSON reloads during generation. Raise `objectLoaderCacheSize` when pregen is object-heavy and the pack is large; lower caches if heap pressure shows retained pack data.
## Pregen section (`pregen`)
| Key | Default | Role |
|-----|---------|------|
| `pregen.runtimeSchedulerMode` | `AUTO` | Bukkit pregen scheduler mode: `AUTO`, `PAPER_LIKE`, `FOLIA` (Folia runtime always resolves to Folia scheduling) |
| `pregen.paperLikeBackendMode` | `AUTO` | Paper-like backend: `AUTO`, `TICKET`, `SERVICE` |
| `pregen.chunkLoadTimeoutSeconds` | `15` | Clamped 5120 |
| `pregen.timeoutWarnIntervalMs` | `500` | Minimum 250 ms between timeout warnings |
| `pregen.saveIntervalMs` | `30000` | Clamped 5_000900_000 |
| `pregen.maxResidentTectonicPlates` | `96` | Soft cap (effective floor 16) on resident mantle tectonic plates |
| `pregen.mantleBackpressureWaitMs` | `25` | Clamped 51000; wait when mantle backpressure engages |
| `pregen.mantleBackpressureTimeoutMs` | `60000` | Clamped 5_000600_000 |
| `pregen.moddedPregenInFlight` | `0` | `0` = auto `clamp(16, cpu*2, 48)`; positive values clamp to 1512 |
Effective resident plates also scale with world height and process heap: higher worlds and smaller heaps reduce the effective plate count (minimum 16). If pregen stalls with mantle pressure, lower concurrency first, then reduce `maxResidentTectonicPlates`, or raise heap so the byte budget allows more plates.
Related world flag: `world.globalPregenCache` (default `false`) — global pregen cache behavior; see `03 - Configuration.md` / `07 - Pregeneration.md`.
## Concurrency helpers (`concurrency`)
`IrisSettingsConcurrency` exposes derived counts (not all are free-form JSON knobs with independent storage in every build path):
- World-gen style parallelism floors at `max(2, availableProcessors)`.
- IO parallelism floors at `max(2, availableProcessors / 2)`.
Prefer pregen in-flight limits and `engineSVC.parallelism` for production tuning rather than inventing extra thread pools outside settings.
## SIMD
**Note:** Broader SIMD coverage (including full noise-kernel wiring through production worldgen) is actively being worked on. Array kernels used on some hot paths already honor `performance.simdKernels` when the incubator Vector API is available; treat noise SIMD as incomplete until that work lands.
Runtime selection (`SimdSupport`) today:
1. If `performance.simdKernels` is false → scalar kernels.
2. Else if module `jdk.incubator.vector` is present and vector kernel classes load → vector kernels for **array ops** used in some generation hot paths (for example `roundToInt` via `ChunkedDoubleDataCache`, carving paths via `MantleCarvingComponent`).
3. Else → scalar kernels; startup log tells the operator to add `--add-modules jdk.incubator.vector`.
2D fractal noise vector kernels exist (`VectorNoiseKernels2D`) and are gated to CPUs where `double` vector width is profitable (≥ 4 lanes). That path is selected by `SimdSupport.noiseKernels2D()` but is **not** the primary wired worldgen path yet.
JVM flag (required for vector API incubator):
```
--add-modules jdk.incubator.vector
```
Server start scripts and Gradle run configs for Iris already pass this where Iris launches the JVM. Standalone microbench: `tools/simd-bench/` (`./run.sh` or `run.bat`). That tool force-measures kernels even when Iris would gate noise SIMD off (for example Apple Silicon 2-lane NEON). Microbench speedups do not guarantee end-to-end pregen gains.
To A/B SIMD on a full server: set `performance.simdKernels` false, restart, measure pregen chunks/s, re-enable, restart, remeasure. Confirm GoldenHash unchanged (`32 - Determinism & Goldenhash.md`).
## Operator pregen modes that affect load
| Mode | Platform | Effect |
|------|----------|--------|
| Default pregen | All | Concurrent generation within platform scheduler limits |
| `serial=true` | Bukkit Paper-compatible only | Strict one-in-flight chunk pregen; rejected on non-Paper serial support |
| `sync` / in-flight flags | Modded | Synchronous or capped async pregen; see `07 - Pregeneration.md` |
| `moddedPregenInFlight` | Modded | Caps concurrent pregen chunk work |
For profiling and determinism isolation, prefer serial/sync one-in-flight runs. For production throughput, use default concurrency and raise heap before raising mantle plate caps.
## Practical tuning order
1. **Heap and GC** — give the process enough heap for pack caches + mantle plates (release smoke used 8 GiB heap on large pregens; size to hardware).
2. **Confirm SIMD module** — check startup log for `SIMD: vector kernels enabled` vs scalar message.
3. **Pregen concurrency** — use default; only lower in-flight / use serial when CPU saturated or region scheduling warns.
4. **Mantle residency** — if backpressure timeouts appear, reduce `maxResidentTectonicPlates` or pregen speed; increase heap if plates thrash.
5. **Caches** — raise object/resource caches when the same objects reload repeatedly; lower if heap retains too much after pregen.
6. **engineSVC.parallelism** — set an explicit positive value only after measuring; `-1` already scales with CPU via `ceil(sqrt(n))`.
7. **Never “tune” by changing pack content** for performance without a GoldenHash re-baseline — pack edits change terrain.
## Measurement checklist
Record for each experiment: pack identity, seed, radius, serial/sync flags, JVM version/flags, heap, CPU, `settings.json` performance/pregen excerpts, chunks/second, duration, failed chunks, peak heap, and GoldenHash combined value. Reject optimizations that change hashes unless the behavior change is intentional and documented. Larger release-style baselines (5k10k chunks, JProfiler) are tracked in `87 - Maintainer - Release Readiness.md`.
## Offline tools
| Tool | Command | Use |
|------|---------|-----|
| Generation probe | `./gradlew :probe:genProbe -PprobePack=…` | Headless engine generate; not a throughput benchmark |
| Classload probe | `./gradlew :probe:run` | Purity/classload gate |
| SIMD microbench | `tools/simd-bench/./run.sh` | Kernel-only scalar vs vector timing |
Smoke procedures that combine pregen and GoldenHash: `31 - Operator Runbooks & Smoke Tests.md`.
+113
View File
@@ -0,0 +1,113 @@
# 85 - Maintainer — Minecraft Version Bump
`gradle.properties` `minecraftVersion` is the single source of truth for the target Minecraft version. Most build outputs derive from it. This document lists every edit required to move Iris to a new Minecraft version, in order.
## Source of truth
`gradle.properties`:
- `minecraftVersion` — target MC version (e.g. `26.2`). Drives `BuildConstants.MINECRAFT_VERSION`, the `com.mojang:minecraft` coordinate, all mod-metadata minecraft ranges, and every dist/jar artifact name.
- `apiVersion` — Bukkit plugin `api-version` (e.g. `26.1`). Deliberately decoupled from `minecraftVersion`: it is the lowest Minecraft release line the single plugin artifact loads on (currently `26.1` so one jar serves 26.1.2 and 26.2). Bump it only when dropping support for the older line.
- `fabricLoaderVersion` — Fabric Loader version.
- `forgeVersion` — Forge version (`<mc>-<forge>`).
- `neoForgeVersion` — NeoForge version.
- `irisVersion` — bump the trailing `-<mc>` suffix to match (e.g. `4.0.0-26.2` -> `4.0.0-27.0`).
## Ordered steps
1. Edit `gradle.properties`: update `minecraftVersion`, `fabricLoaderVersion`, `forgeVersion`, `neoForgeVersion`, and the `irisVersion` suffix. Revisit `apiVersion` only if the bump drops support for the oldest Minecraft line the plugin artifact still loads on.
2. Edit `gradle/libs.versions.toml`:
- `spigot` — the Spigot/Paper API pin used to compile against (`<mc>-R0.1-SNAPSHOT`).
- `fabricApi-*` — the ten Fabric API module versions, if the new MC requires different Fabric API builds. Each module is versioned independently (`<version>+<build-hash>`). The ten are `base`, `registrySync`, `resourceLoader`, `lifecycleEvents`, `commandApi`, `eventsInteraction`, `networking`, `rendering`, `keyMapping`, `permission`. Every one of them is bundled jar-in-jar and must be declared in `fabric.mod.json` `jars` — see step 7.
3. Edit `core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java` (manual, structural):
- Append a new enum constant `V<major>_<minor>("<mc>", <packFormat>, <DataFixer>::new)`.
- `packFormat` comes from https://minecraft.wiki/w/Pack_format.
- `getLatest()` returns the last enum constant, so append; do not reorder.
- Add a matching `IDataFixer` implementation under `core/src/main/java/art/arcane/iris/core/nms/datapack/` if the datapack format changed.
4. Register the new Bukkit NMS binding module:
- `settings.gradle` — add `include(':adapters:bukkit:nms:v<major>_<minor>_R<rev>')`.
- `build.gradle` — add the binding to the `nmsBindings` map: `v<major>_<minor>_R<rev>: '<spigot-nms-build-version>'` (e.g. `'26.2.build.25-alpha'`).
- Create the binding sources under `adapters/bukkit/nms/v<major>_<minor>_R<rev>/`.
5. Update loader version-range metadata (manual floors/ranges only; the `minecraft` ranges are templated from `minecraftVersion` and need no edit):
- `adapters/fabric/src/main/resources/fabric.mod.json``minecraft` is `~${minecraftVersion}` (auto). Update the `fabricloader` floor (currently `>=0.19.3`) if the loader minimum changes, and the `jars` list if the bundled Fabric API modules change.
- `adapters/forge/src/main/resources/META-INF/mods.toml``minecraft` versionRange is `[${minecraftVersion}]` (auto). Update `loaderVersion` (currently `[65,)`) and the `forge` dependency versionRange (also `[65,)`) for the new Forge line. Both are hand-maintained.
- `adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml``minecraft` versionRange is `[${minecraftVersion}]` (auto). `loaderVersion` (currently `[3,)`) is the javafml specification version, not the NeoForge version, and rarely moves. The `neoforge` dependency `versionRange` is **hardcoded** (currently `[26.2,)`) and is *not* templated from `minecraftVersion` — hand-edit it on every bump or the mod will load on the wrong NeoForge line.
6. Re-verify the mapping-coupled files. Six files name Mojang-mapped classes, fields, and method descriptors directly. Nothing templates them, nothing fails fast at build time if a name moved, and a stale entry surfaces as a silent no-op or a load-time crash. Check every one against the new MC jar.
Access widener (Fabric) — `accessWidener v2 official`, so the names are Mojang-mapped:
- `adapters/fabric/src/main/resources/irisworldgen.accesswidener`
- `MinecraftServer.levels` `Ljava/util/Map;`
- `MinecraftServer.executor` `Ljava/util/concurrent/Executor;`
- `MinecraftServer.storageSource` `Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;`
- `PackRepository.sources` `Ljava/util/Set;` (accessible **and** mutable)
Verify: each field still exists with that exact descriptor. Loom fails the build on an unresolvable AW entry, so a rename shows up as an AW error — read it, do not delete the line.
Access transformers (Forge and NeoForge) — must stay in sync with each other and with the AW:
- `adapters/forge/src/main/resources/META-INF/accesstransformer.cfg`
- `adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg`
- both: `public net.minecraft.server.MinecraftServer levels` / `executor` / `storageSource`
Verify: the three ATs match the first three AW entries. Note the ATs have no `PackRepository` entry — Forge/NeoForge reach the pack sources through their own hooks, so do not add one without a reason. Wired via `minecraft { accessTransformer.from(...) }` (Forge) and `neoForge { accessTransformers.from(...) }` (NeoForge).
Mixin configs — three JSONs, eight mixin classes, all targeting Mojang-mapped members:
- `adapters/fabric/src/main/resources/irisworldgen.mixins.json`
(package `art.arcane.iris.fabric.mixin`, `compatibilityLevel` `JAVA_25`, Fabric only)
- `BlockItemMixin` -> `BlockItem.placeBlock`, `@At("RETURN")`
- `BlockMixin` -> `Block.getDrops(...)` with a **full descriptor** (`BlockState, ServerLevel, BlockPos, BlockEntity, Entity, ItemInstance`) — the highest-churn entry in the repo; the parameter list changes across MC versions
- `PackRepositoryMixin` -> `PackRepository.<init>`, `@At("RETURN")`
- `adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json`
(package `art.arcane.iris.modded.mixin`, `compatibilityLevel` `JAVA_21`, all three loaders)
- `EntityPersistenceMixin` -> `Entity.shouldBeSaved`
- `LivingEntityLootMixin` -> `LivingEntity.dropFromLootTable(ServerLevel, DamageSource, boolean)` — full descriptor
- `MobAwarenessMixin` -> `Mob.serverAiStep`, injecting at a **field target** (`Lnet/minecraft/world/entity/Mob;noActionTime:I`) — verify the field, not just the method
- `adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json`
(package `art.arcane.iris.client.mixin`, client-only)
- `IrisWorldOpenFlowsMixin` -> `WorldOpenFlows.confirmWorldCreation` and `WorldOpenFlows.openWorldCheckWorldStemCompatibility`
- `IrisWorldTypeEntryMixin` -> `WorldCreationUiState.WorldTypeEntry.describePreset`, plus a `@Shadow` member — shadows break silently if the field is renamed
The client mixin *config* lives in `modded-common/src/main/resources` but the classes live in `adapters/client-common/src/main/java/art/arcane/iris/client/mixin/`; the modded mixin classes live in `adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/`. All three adapters add both shared source dirs, so one edit hits every loader.
Registration differs per loader and each place must list the same configs:
- Fabric — `fabric.mod.json` `mixins` (all three; the client one gated on `"environment": "client"`).
- NeoForge — `[[mixins]]` blocks in `neoforge.mods.toml` (entity + client).
- Forge — no toml entry. The jar manifest attribute `MixinConfigs` in `adapters/forge/build.gradle` plus `--mixin.config` args on the `runClient`/`runServer` configurations (entity + client). Adding a mixin config on Forge means editing the manifest attribute *and* the run args.
`injectors.defaultRequire` is `1` in all three configs, so a mixin that no longer applies fails the run instead of degrading quietly. Treat any "mixin apply failed" line as a bump blocker, and run both `runClient` and `runServer` per loader — client-only mixins are not exercised by a server run.
7. Reconcile the Fabric jar-in-jar list. `adapters/fabric/build.gradle` adds every Fabric API module to the `jij` configuration, which the `shadowJar` copies into `META-INF/jars` with the version stripped from the filename. The `jij` configuration is `transitive = false`, so the bundled set is exactly the declared set, and `fabric.mod.json` `jars` must list exactly those filenames. After changing the module list, confirm the jar agrees:
```
unzip -l "dist/Iris v<version> [Fabric] <mc>+<loader>.jar" | grep META-INF/jars
```
An entry in `jars` with no matching nested jar makes the loader refuse the mod; a nested jar missing from `jars` is dead weight the loader never mounts.
8. Build and verify:
- `./gradlew :core:check`
- `./gradlew buildBukkit`
- `./gradlew buildFabric`
- `./gradlew buildForge`
- `./gradlew buildNeoforge`
9. After a successful bump, re-run operator smoke and GoldenHash parity on all platforms (`31 - Operator Runbooks & Smoke Tests.md`, `32 - Determinism & Goldenhash.md`) and continue with `86 - Maintainer - Release Checklist.md` only after `87 - Maintainer - Release Readiness.md` allows GO or GO-WARN.
## Derived automatically (do not hand-edit on a version bump)
- Bukkit plugin `api-version` — `adapters/bukkit/plugin/build.gradle` reads `apiVersion`.
- `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in `core/build.gradle` from `minecraftVersion`; consumed by `Tasks.supportedVersions`.
- Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`.
- Dist/jar artifact names and the `com.mojang:minecraft` coordinate — composed from `minecraftVersion` in the build scripts.
## Notes
- `build.gradle`, the adapter `build.gradle` files, and `settings.gradle` carry `.getOrElse('26.2')` defensive defaults for the version properties. `gradle.properties` always overrides them, so a bump does not require touching those fallbacks; refresh them only if the checked-in default should track the current release.
- The Java literal `"26.2"` intentionally remains in `DataVersion.java` (structural enum constant), `core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java`, and `core/src/test/java/art/arcane/iris/core/lifecycle/PaperLibBootstrapTest.java`. The test files use MC version strings as parser fixtures, not as a version source; update them only when the version string formats they exercise change.
@@ -0,0 +1,62 @@
# 86 - Maintainer — Release Checklist
Manual release procedure. There is no release automation by design: every step below is run by a person and verified by eye. Work top to bottom; do not skip the verify gates.
Before starting this publication procedure, complete `87 - Maintainer - Release Readiness.md`. It contains the engineering remediation, determinism, performance, CI, and full platform-acceptance gates. This checklist starts only after those gates produce GO or an explicitly accepted GO-WARN decision.
Reference values below assume the current `gradle.properties`: `irisVersion=4.0.0-26.2`, `minecraftVersion=26.2`, `fabricLoaderVersion=0.19.3`, `forgeVersion=26.2-65.0.4`, `neoForgeVersion=26.2.0.12-beta`. For a Minecraft version bump, do `85 - Maintainer - MC Version Bump.md` first, then start this checklist.
## a. Preflight
- [ ] Working tree clean on the exact commit you intend to tag (`git status` shows nothing to commit).
- [ ] CI is green on that commit. The `verify` job (`.github/workflows/ci.yml`) runs core checks, Bukkit and shared modded tests, the SPI build, the deserialization probe, the modded artifact-verifier tests, and guarded Fabric, Forge, and NeoForge artifact builds on JDK 25. Do not release on a red or stale run.
- [ ] `MasterChangelog.MD` Iris section is coherent: one consolidated entry set, deduplicated, no date-sliced headers, and it describes the current shipped state (not superseded intermediate work).
- [ ] Version fields correct in `gradle.properties`: `irisVersion` is the release version and its trailing `-<mc>` suffix matches `minecraftVersion`. For a Minecraft bump, confirm every step in `85 - Maintainer - MC Version Bump.md` is done (loader ranges, `DataVersion`, NMS binding).
- [ ] JDK 25 is the active toolchain locally (`java -version` reports 25).
## b. Build
- [ ] From the Iris project root: `./build-all.sh`. This disables local VolmLib substitution, uses the immutable coordinate from `gradle.properties`, and serializes the all-platform build.
- [ ] `dist/` contains the four platform jars (exact names for this release):
- [ ] `Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar` (Bukkit/Paper/Purpur/Spigot/Folia plugin)
- [ ] `Iris v4.0.0-26.2 [Fabric] 26.2+0.19.3.jar`
- [ ] `Iris v4.0.0-26.2 [Forge] 26.2+65.0.4.jar`
- [ ] `Iris v4.0.0-26.2 [NeoForge] 26.2+26.2.0.12-beta.jar`
- Naming pattern: `Iris v<irisVersion> [<Platform>] <mc>[+<loaderDisplay>].jar`.
- [ ] The SPI jar is built by the same run at `spi/build/libs/iris-spi-4.0.0-26.2.jar`. It is the adapter/platform contract, not the stable downstream plugin API; it is not copied into `dist/` or uploaded to mod portals.
- [ ] Each mod jar bundles Iris core, SPI, and Iris-owned shaded libraries. LZ4, OSHI, JNA, and JNA Platform are supplied by the Minecraft 26.2 runtime and must not be bundled or relocated.
## c. Verify (release gates)
- [ ] `:core:check` and `:probe:deserializationProbe` passed in CI on the tag commit (a. covers this).
- [ ] Golden-hash determinism VERIFY passes on all four platforms and matches the same hash (see `32 - Determinism & Goldenhash.md`):
- [ ] Bukkit plugin: `/iris developer goldenhash world=<world> radius=<radius> threads=<threads>` (automatically verifies when the matching capture already exists)
- [ ] Fabric mod: `/iris goldenhash verify <radius> <threads>`
- [ ] Forge mod: `/iris goldenhash verify <radius> <threads>`
- [ ] NeoForge mod: `/iris goldenhash verify <radius> <threads>`
- The hash is interchangeable across platforms: all four MUST report identical output for the same pack and seed. Any mismatch blocks the release.
- [ ] Live modded content-mod gate: on each loader, boot the mod jar alongside a real content mod (e.g. Create) and generate an Iris world. Confirm no load-time rejection, no class-loader crash, and that modded blocks/items/entities author and generate.
- [ ] Fabric + content mod
- [ ] Forge + content mod
- [ ] NeoForge + content mod
- [ ] Client-mod matrix: install the mod on the client (keybind `H` toggles the pregen HUD) and confirm:
- [ ] Modded server + modded client: HUD receives pregen progress over `irisworldgen:main`.
- [ ] Modded server + vanilla client: server generates normally; vanilla client is unaffected.
- [ ] Paper (Bukkit) server + modded client: HUD receives pregen progress over vanilla plugin messaging.
- [ ] Folia smoke: plugin loads and an Iris world generates on Folia.
- [ ] Non-Iris server + modded client: client is inert, no errors.
Operator-oriented sequences that support these gates: `31 - Operator Runbooks & Smoke Tests.md`. Client channel details: `29 - Client HUD & Protocol.md`.
## d. Publish (all manual, no automation)
- [ ] Modrinth: upload the three mod jars and the plugin jar. Tag loaders `fabric` / `forge` / `neoforge` on the mod files; mark the environment server + client; set game version 26.2.
- [ ] CurseForge: upload the three mod jars with the matching loader tags and game version 26.2.
- [ ] Existing plugin distribution channels: publish the plugin jar (`Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar`) where the plugin already ships.
- [ ] Sentry: add a release note / mark the release so incoming reports map to this version (the mod version string is the Sentry release tag).
- [ ] Storepage / `listing.json` staleness review: check the listing copy for pre-4.0 content (Bukkit-only framing, old feature lists, screenshots). Flag anything stale for update before or right after launch. (Review only; this checklist does not change store copy.)
## e. Post
- [ ] Tag the release commit (`v<irisVersion>`) and push the tag. Archive the already verified `dist/` bundle with the release record; no tag-triggered bundle automation is configured.
- [ ] Announce the release on the community channels once the portals show the new files live.
@@ -1,16 +1,12 @@
# Iris All-Platform Release Readiness Checklist
# 87 - Maintainer — Release Readiness
Engineering checklist for preparing Iris for a public release on Bukkit-family servers, Fabric,
Forge, and NeoForge. Complete this checklist before running `docs/release-checklist.md`.
**Internal living tracker.** Engineering checklist for preparing Iris for a public release on Bukkit-family servers, Fabric, Forge, and NeoForge. Complete this checklist before running `86 - Maintainer - Release Checklist.md`. Checkbox state and historical evidence below are maintained as work proceeds; they are not a frozen product manual for operators.
The goal is to correct confirmed defects without silently changing valid pack output, public behavior,
or platform parity. A behavior change is acceptable when it fixes a documented defect, is covered by
a regression test, and is recorded in `MasterChangelog.MD`.
The goal is to correct confirmed defects without silently changing valid pack output, public behavior, or platform parity. A behavior change is acceptable when it fixes a documented defect, is covered by a regression test, and is recorded in `MasterChangelog.MD`.
The current runtime pass prioritizes isolated world creation, deterministic generation, pregeneration,
and profiling. Hotload, reload, and shutdown refinement remains in the later lifecycle gates.
Automated release builds, tagged bundles, and publishing infrastructure are deferred; public-beta work
uses manually built artifacts and focuses on plugin/mod correctness and stability.
The current runtime pass prioritizes isolated world creation, deterministic generation, pregeneration, and profiling. Hotload, reload, and shutdown refinement remains in the later lifecycle gates. Automated release builds, tagged bundles, and publishing infrastructure are deferred; public-beta work uses manually built artifacts and focuses on plugin/mod correctness and stability.
Cross-links: GoldenHash (`32 - Determinism & Goldenhash.md`), operator smokes (`31 - Operator Runbooks & Smoke Tests.md`), performance knobs (`33 - Performance Tuning.md`), MC bump (`85 - Maintainer - MC Version Bump.md`).
## Completion rules
@@ -42,47 +38,17 @@ uses manually built artifacts and focuses on plugin/mod correctness and stabilit
- [x] Capture baseline golden hashes for the same pack, seed, radius, and thread counts on all platforms.
- [ ] Preserve a copy of the baseline performance results described in section 8.
Gate: the source, dependencies, generated terrain baseline, and test evidence are reproducible on a
second clean checkout.
Gate: the source, dependencies, generated terrain baseline, and test evidence are reproducible on a second clean checkout.
## 1. P0 - Make concurrent generation deterministic
- [x] Fix the reproducible order/state-dependent generation defect. Cave painting relabeled shared,
loader-cached biome objects as `CAVE`; shallow cave resolution can return the surface biome, so later
height and biome decisions changed until `IrisComplex` was rebuilt. Carving now passes explicit cave
context to surface and ceiling decorators without mutating the shared biome, preserving cave fluid
behavior. Focused isolation and decorator tests plus a 2,025-chunk warm-sequence reproducer pass.
- [x] Scope the confirmed height-bounds cache to its owning `IrisComplex`. The previous static thread-local
cache keyed entries only by grid coordinates and interpolator index, allowing another engine or a
hotloaded complex to reuse bounds from a different generator set. Focused coverage protects both
cross-complex isolation and same-complex cache reuse.
- [x] Scope the cave carver's scratch cache to each `IrisCaveCarver3D`. The warp cache was thread-local but
shared by every cave profile and keyed only by sample coordinates, so a second profile on the same
worker could reuse warp values from the first profile's noise generator. Focused same-thread coverage
now proves distinct carvers retain their own warp samples while preserving per-carver scratch reuse.
- [x] Make cross-chunk cave-wall painting independent of adjacent mantle load order. All 37 block differences
in the two focused mantle-reset diagnoses were on local chunk edges (`x=0`, `x=15`, or `z=15`), where
`IrisCarveModifier` paints the neighboring cave wall only when that neighbor's mantle chunk contains
carving data. The carving component now declares a minimal one-block radius, which schedules the full
adjacent chunk pass through the mantle radius conversion; focused coverage protects that contract.
Packaged-runtime regeneration now retains the same fixed-seed hash on every available platform.
- [x] Repeat the fixed-seed, reset-mantle GoldenHash sequence from clean startup and after a complete pregen
on every available platform. Paper 26.2-56, Fabric Loader 0.19.3, Forge 65.0.3, and NeoForge
26.2.0.8-beta all produced the exact combined hash
`783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4`; every strict single-thread
2,025-chunk pregen completed with zero failures and every post-pregen verification matched. Fabric also
matched after a controlled restart. Folia 26.2 remains unavailable from its upstream build endpoint.
- [x] Explain and fix the separate 50-chunk Paper-versus-modded biome-hash difference for byte-identical
packs. All 131 differing sampled columns were exactly `minecraft:forest` versus `minecraft:plains`:
Bukkit's NMS biome source seeded the shared scatter generator from its first coordinate-derived RNG,
while modded generation seeded it from the engine biome seed. Every runtime path now passes its owning
engine explicitly, shared registrants cache by canonical engine biome seed in a bounded eight-entry
cache, and engine-less tooling preserves supplied-seed behavior. Concurrent interleaved-engine coverage
protects exact engine ownership, seed isolation, same-seed reuse, and bounded eviction.
- [x] Sample direct Bukkit/modded biome derivatives at each world column, matching Bukkit NMS resolution.
The actuator previously reused the chunk origin for every local column, so scatter selection could
differ even after both platforms used the same generator seed. Focused actuator coverage verifies all
four coordinates in a two-by-two chunk section are distinct world positions.
- [x] Fix the reproducible order/state-dependent generation defect. Cave painting relabeled shared, loader-cached biome objects as `CAVE`; shallow cave resolution can return the surface biome, so later height and biome decisions changed until `IrisComplex` was rebuilt. Carving now passes explicit cave context to surface and ceiling decorators without mutating the shared biome, preserving cave fluid behavior. Focused isolation and decorator tests plus a 2,025-chunk warm-sequence reproducer pass.
- [x] Scope the confirmed height-bounds cache to its owning `IrisComplex`. The previous static thread-local cache keyed entries only by grid coordinates and interpolator index, allowing another engine or a hotloaded complex to reuse bounds from a different generator set. Focused coverage protects both cross-complex isolation and same-complex cache reuse.
- [x] Scope the cave carver's scratch cache to each `IrisCaveCarver3D`. The warp cache was thread-local but shared by every cave profile and keyed only by sample coordinates, so a second profile on the same worker could reuse warp values from the first profile's noise generator. Focused same-thread coverage now proves distinct carvers retain their own warp samples while preserving per-carver scratch reuse.
- [x] Make cross-chunk cave-wall painting independent of adjacent mantle load order. All 37 block differences in the two focused mantle-reset diagnoses were on local chunk edges (`x=0`, `x=15`, or `z=15`), where `IrisCarveModifier` paints the neighboring cave wall only when that neighbor's mantle chunk contains carving data. The carving component now declares a minimal one-block radius, which schedules the full adjacent chunk pass through the mantle radius conversion; focused coverage protects that contract. Packaged-runtime regeneration now retains the same fixed-seed hash on every available platform.
- [x] Repeat the fixed-seed, reset-mantle GoldenHash sequence from clean startup and after a complete pregen on every available platform. Paper 26.2-56, Fabric Loader 0.19.3, Forge 65.0.3, and NeoForge 26.2.0.8-beta all produced the exact combined hash `783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4`; every strict single-thread 2,025-chunk pregen completed with zero failures and every post-pregen verification matched. Fabric also matched after a controlled restart. Folia 26.2 remains unavailable from its upstream build endpoint.
- [x] Explain and fix the separate 50-chunk Paper-versus-modded biome-hash difference for byte-identical packs. All 131 differing sampled columns were exactly `minecraft:forest` versus `minecraft:plains`: Bukkit's NMS biome source seeded the shared scatter generator from its first coordinate-derived RNG, while modded generation seeded it from the engine biome seed. Every runtime path now passes its owning engine explicitly, shared registrants cache by canonical engine biome seed in a bounded eight-entry cache, and engine-less tooling preserves supplied-seed behavior. Concurrent interleaved-engine coverage protects exact engine ownership, seed isolation, same-seed reuse, and bounded eviction.
- [x] Sample direct Bukkit/modded biome derivatives at each world column, matching Bukkit NMS resolution. The actuator previously reused the chunk origin for every local column, so scatter selection could differ even after both platforms used the same generator seed. Focused actuator coverage verifies all four coordinates in a two-by-two chunk section are distinct world positions.
- [ ] Add a two-thread barrier test that generates two chunks through the same `IrisEngine` concurrently.
- [ ] Assert each generation observes its own chunk coordinates, `ChunkContext`, and generation session.
- [ ] Add a repeated parallel golden-hash test that fails on any cross-run difference.
@@ -92,13 +58,11 @@ second clean checkout.
- [ ] Run sequential and parallel generation for the same seed and assert identical hashes.
- [ ] Run the test under high concurrency and with generation-session close/hotload activity.
Gate: repeated concurrent generation is deterministic, context-isolated, and hash-identical to the
single-threaded result.
Gate: repeated concurrent generation is deterministic, context-isolated, and hash-identical to the single-threaded result.
## 2. P0 - Make hotload and shutdown transactional
This section is retained for the later lifecycle refinement pass and is not part of the current public-beta
runtime gate. Controlled restarts remain in scope only for existing-world persistence verification.
This section is retained for the later lifecycle refinement pass and is not part of the current public-beta runtime gate. Controlled restarts remain in scope only for existing-world persistence verification.
- [ ] Add a regression test: malformed dimension edit -> failed hotload -> old engine remains usable.
- [ ] Extend the test: corrected edit -> next hotload succeeds without restarting the server.
@@ -113,17 +77,14 @@ runtime gate. Controlled restarts remain in scope only for existing-world persis
- [ ] Restructure `IrisEngine.close()` so every cleanup stage runs even when lease draining times out.
- [ ] Add startup, failed-hotload recovery, successful-hotload, close, and restart tests.
Gate: no failed hotload can poison the live engine, admit generation into partial state, leak permits,
or skip shutdown cleanup.
Gate: no failed hotload can poison the live engine, admit generation into partial state, leak permits, or skip shutdown cleanup.
## 3. P0 - Make `.iris` packaging complete and lossless
- [ ] Define the complete pack resource graph in one shared traversal used by Bukkit and modded Studio.
- [ ] Traverse dimensions, regions, biomes, generators, blocks, objects, entities, spawners, loot,
structures, jigsaw pools/pieces, snippets, and every other referenced registrant.
- [ ] Traverse dimensions, regions, biomes, generators, blocks, objects, entities, spawners, loot, structures, jigsaw pools/pieces, snippets, and every other referenced registrant.
- [ ] Include objects referenced directly by regions, not only objects reached through biomes.
- [x] Include entity resources referenced only by spawner `initialSpawns` entries, alongside normal `spawns`
dependencies, with focused export dependency regression coverage.
- [x] Include entity resources referenced only by spawner `initialSpawns` entries, alongside normal `spawns` dependencies, with focused export dependency regression coverage.
- [ ] Fail packaging when any required resource is missing or malformed; never report partial success.
- [ ] Stop obfuscation/export from mutating loader-cached biome or placement objects.
- [ ] Give Bukkit and modded packaging the same graph, validation, and error semantics.
@@ -133,8 +94,7 @@ or skip shutdown cleanup.
- [ ] Add modded export -> import -> export round-trip tests.
- [ ] Compare normalized JSON, binary objects, dependency counts, and final resource hashes.
Gate: a complete fixture survives round-trip packaging without missing resources, mutated source state,
or unexplained byte/content changes.
Gate: a complete fixture survives round-trip packaging without missing resources, mutated source state, or unexplained byte/content changes.
## 4. P0 - Make Object Studio Folia-safe and atomic
@@ -148,8 +108,7 @@ or unexplained byte/content changes.
- [ ] Confirm a failed write is retried on the next save rather than reported as “no changes.”
- [ ] Test empty cells, unchanged cells, partial chunk availability, failure recovery, and concurrent saves.
Gate: Object Studio performs no cross-region Bukkit access, never exposes a partial file, and can always
retry a failed save.
Gate: Object Studio performs no cross-region Bukkit access, never exposes a partial file, and can always retry a failed save.
## 5. P1 - Make validation and schemas trustworthy
@@ -159,8 +118,7 @@ retry a failed save.
- [ ] Discover nested dimensions and resources using the same key rules as `ResourceLoader`.
- [ ] Parse and validate every referenced dependency rather than checking only file existence.
- [ ] Promote malformed referenced JSON to a blocking validation error.
- [x] Validate nested spawner `spawns` and `initialSpawns` entries against same-pack entity resources,
blocking malformed containers and entries, missing files, unsafe paths, and malformed referenced JSON.
- [x] Validate nested spawner `spawns` and `initialSpawns` entries against same-pack entity resources, blocking malformed containers and entries, missing files, unsafe paths, and malformed referenced JSON.
- [ ] Validate nested unknown properties where the schema disallows them.
- [ ] Preserve namespaces for non-Minecraft enchantments and potion effects in generated schemas.
- [ ] Add deliberate cross-namespace collision fixtures.
@@ -168,8 +126,7 @@ retry a failed save.
- [ ] Add validator tests for nested resources, malformed dependencies, cleanup preview, and restore conflicts.
- [ ] Add schema tests for vanilla shorthand and fully namespaced modded values.
Gate: validation is read-only by default, rejects broken dependency graphs, accepts valid nested packs,
and schema completion never changes registry identity.
Gate: validation is read-only by default, rejects broken dependency graphs, accepts valid nested packs, and schema completion never changes registry identity.
## 6. P1 - Harden modded generation and lifecycle
@@ -184,10 +141,9 @@ and schema completion never changes registry identity.
- [ ] Make engine-data persistence synchronized and atomic.
- [ ] Make persisted statistics safe under parallel generation.
- [ ] Test dedicated-server start/stop, integrated-server start/stop/start, and world unload/reload.
- [ ] Decide and document parity for modded entity time/weather gates, awareness, and spawn effects.
- [x] Modded entity spawners enforce time/weather gates and apply AI awareness, spawn effects, and raw commands; parity tests cover these paths and `23 - Loot, Entities, Spawners, Markers.md` documents them.
Gate: modded shutdown/hotload cannot save blank chunks, strand futures, race maintenance, or retain stale
world state across a second server lifecycle.
Gate: modded shutdown/hotload cannot save blank chunks, strand futures, race maintenance, or retain stale world state across a second server lifecycle.
## 7. P1 - Harden pregeneration, Folia, and scheduling
@@ -197,12 +153,8 @@ world state across a second server lifecycle.
- [ ] Ensure every shutdown step runs even when `generator.close()` fails.
- [ ] Clear `regionPending` and related bookkeeping on every load/generation callback failure.
- [ ] Make the pregen cache executor restartable in the same JVM.
- [x] Distinguish cancelled or aborted partial pregeneration from full completion after the generator drains.
Cancellation now reports generated, total, failed, and remaining counts without emitting a successful
`Pregen finished` summary; focused tests cover cancellation after the first chunk, normal async-close
completion, and completion with a failed chunk.
- [ ] Replace modded scheduler `CallerRunsPolicy` with explicit backpressure that cannot move async work
onto the server thread.
- [x] Distinguish cancelled or aborted partial pregeneration from full completion after the generator drains. Cancellation now reports generated, total, failed, and remaining counts without emitting a successful `Pregen finished` summary; focused tests cover cancellation after the first chunk, normal async-close completion, and completion with a failed chunk.
- [ ] Replace modded scheduler `CallerRunsPolicy` with explicit backpressure that cannot move async work onto the server thread.
- [ ] Add a bounded per-tick main-thread drain budget.
- [ ] Replace full delayed-task scans with a due-time queue or equivalent bounded scheduler.
- [ ] Stress cancellation, pause/resume, failure, shutdown, and restart under Paper and Folia.
@@ -214,36 +166,18 @@ Gate: pregeneration remains thread-correct and bounded under saturation, cancell
### Current isolated smoke evidence
This evidence validates packaged-artifact generation and establishes a profiling candidate. It is not the
final 5,000-10,000-chunk performance baseline required by this section.
This evidence validates packaged-artifact generation and establishes a profiling candidate. It is not the final 5,000-10,000-chunk performance baseline required by this section.
- [x] Fixed inputs: Iris seed `1337`, GoldenHash radius `22`, one hash thread, and a 352-block serial/sync
pregeneration radius covering exactly 2,025 chunks.
- [x] Fixed inputs: Iris seed `1337`, GoldenHash radius `22`, one hash thread, and a 352-block serial/sync pregeneration radius covering exactly 2,025 chunks.
- [x] Fixed host: Apple M3 Max, 128 GiB RAM, Temurin 25.0.2, 8 GiB instance heap.
- [x] Paper 26.2-56: serial pregen completed 2,025/2,025 with zero failed chunks; cancellation,
pause/status/resume, cache resume, restart persistence, and untouched far-chunk generation passed.
- [x] Fabric Loader 0.19.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict
`peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and far generation passed.
- [x] Forge 26.2-65.0.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict
`peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and untouched far-chunk
generation passed.
- [x] NeoForge 26.2.0.8-beta: sync pregen completed 2,025/2,025 with zero failed chunks; pause/cancel,
checkpoint resume, fresh generation, and GoldenHash capture completed against the corrected pack.
- [x] GoldenHash parity/determinism: Paper, Fabric, Forge, and NeoForge all captured the exact block+biome
hash `783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4` from the manually built
candidate artifacts. Every platform then completed a strict single-thread 2,025-chunk pregen with zero
failures and retained that hash; Fabric retained it across restart. The historical divergent hashes are
superseded by fixes for cross-complex height bounds, cross-profile cave warp, cave-boundary scheduling,
engine-owned biome generation, per-column biome sampling, and shared-biome cave relabeling.
- [x] Paper JProfiler CPU, heap, and GC snapshots captured; explicit post-run GC reduced used heap from a
sampled peak near 5.94 GiB to approximately 475 MiB, with no retained-heap leak indicated by this run.
- [x] Fabric JProfiler sampled-allocation snapshot captured; profiling overhead made that run unsuitable
for throughput comparison.
- [x] Real content-mod fixture: Fabric, Forge, and NeoForge loaded Nerospace beta.7 with Neroland Core 1.4.0
(plus Fabric API 0.154.2 on Fabric), resolved a custom entity/item/block through Iris, generated the
exact named structure chest item, performed once-per-chunk initial spawning with zero players, replaced
the entity's death loot, generated seven custom ore blocks in the forced test area, and completed strict
synchronous 2,025/2,025 pregeneration with zero failed chunks on every loader.
- [x] Paper 26.2-56: serial pregen completed 2,025/2,025 with zero failed chunks; cancellation, pause/status/resume, cache resume, restart persistence, and untouched far-chunk generation passed.
- [x] Fabric Loader 0.19.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict `peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and far generation passed.
- [x] Forge 26.2-65.0.3: sync pregen completed 2,025/2,025 with zero failed chunks and strict `peakInFlight=1 finalLimit=1`; controls, cache resume, restart persistence, and untouched far-chunk generation passed.
- [x] NeoForge 26.2.0.8-beta: sync pregen completed 2,025/2,025 with zero failed chunks; pause/cancel, checkpoint resume, fresh generation, and GoldenHash capture completed against the corrected pack.
- [x] GoldenHash parity/determinism: Paper, Fabric, Forge, and NeoForge all captured the exact block+biome hash `783cf831486858129a3730e93c2823b773a40af78442ba3ebe373425eb80fab4` from the manually built candidate artifacts. Every platform then completed a strict single-thread 2,025-chunk pregen with zero failures and retained that hash; Fabric retained it across restart. The historical divergent hashes are superseded by fixes for cross-complex height bounds, cross-profile cave warp, cave-boundary scheduling, engine-owned biome generation, per-column biome sampling, and shared-biome cave relabeling.
- [x] Paper JProfiler CPU, heap, and GC snapshots captured; explicit post-run GC reduced used heap from a sampled peak near 5.94 GiB to approximately 475 MiB, with no retained-heap leak indicated by this run.
- [x] Fabric JProfiler sampled-allocation snapshot captured; profiling overhead made that run unsuitable for throughput comparison.
- [x] Real content-mod fixture: Fabric, Forge, and NeoForge loaded Nerospace beta.7 with Neroland Core 1.4.0 (plus Fabric API 0.154.2 on Fabric), resolved a custom entity/item/block through Iris, generated the exact named structure chest item, performed once-per-chunk initial spawning with zero players, replaced the entity's death loot, generated seven custom ore blocks in the forced test area, and completed strict synchronous 2,025/2,025 pregeneration with zero failed chunks on every loader.
- [ ] Choose one fixed release pack, seed, world height, radius, JVM configuration, and hardware profile.
- [ ] Warm at least 256 chunks before measuring.
@@ -255,43 +189,32 @@ final 5,000-10,000-chunk performance baseline required by this section.
- [ ] Profile modded block/biome buffer allocation before pooling or changing representation.
- [ ] Profile height-bound sampling, custom biome caches, mantle tasks, and pregen region-drain complexity.
- [ ] Benchmark each optimization against the unchanged baseline with the same inputs.
- [ ] Reject or revise changes that regress median throughput by more than 5% or p95 latency/allocations by
more than 10%, unless the correctness benefit and accepted tradeoff are documented.
- [ ] Reject or revise changes that regress median throughput by more than 5% or p95 latency/allocations by more than 10%, unless the correctness benefit and accepted tradeoff are documented.
- [ ] Confirm optimized and baseline runs produce identical golden hashes where behavior should be unchanged.
Gate: representative generation and pregen have repeatable baselines, no unexplained regression, and no
unbounded queue, allocation, or retained-memory growth.
Gate: representative generation and pregen have repeatable baselines, no unexplained regression, and no unbounded queue, allocation, or retained-memory growth.
## 9. CI and deterministic test infrastructure
Automated build and release-pipeline work in this section is deferred. The current beta pass uses manual
artifacts; only correctness tests and deterministic reproducers that directly protect runtime behavior apply.
Automated build and release-pipeline work in this section is deferred. The current beta pass uses manual artifacts; only correctness tests and deterministic reproducers that directly protect runtime behavior apply.
- [x] Add `:adapters:bukkit:plugin:test` to CI.
- [x] Expand the broad classload probe across all top-level and nested core classfiles, with an exact reviewed
class and dependency-category allowlist that rejects new classes, changed dependency namespaces,
non-missing-class failures, and stale entries.
- [x] Expand the broad classload probe across all top-level and nested core classfiles, with an exact reviewed class and dependency-category allowlist that rejects new classes, changed dependency namespaces, non-missing-class failures, and stale entries.
- [ ] Move the core Bukkit purity ratchet below its current 182-file ceiling.
- [ ] Give `genProbe` a repository fixture or require an explicit portable pack path.
- [ ] Add a deterministic fixed-seed Iris-world task for Fabric, Forge, and NeoForge.
- [x] Make worldcheck return a failing process result when its internal result is FAIL.
- [x] Prevent `buildAllToOut` nested builds from racing root tasks over `core/build`.
- [x] Verify nested adapter builds honor the selected VolmLib source/coordinate.
- [x] Add packaged-jar server boots; manually assembled Bukkit, Fabric, Forge, and NeoForge artifacts all
reached their runtime-ready state in isolated instances, including a real multi-mod classpath.
- [x] Add packaged-jar server boots; manually assembled Bukkit, Fabric, Forge, and NeoForge artifacts all reached their runtime-ready state in isolated instances, including a real multi-mod classpath.
Gate: a clean CI run proves tests, deterministic generation, packaging, and server startup from the actual
release artifacts.
Gate: a clean CI run proves tests, deterministic generation, packaging, and server startup from the actual release artifacts.
## 10. Full platform acceptance matrix
Use the exact packaged release jars, not development classes.
The current isolated smoke proves fresh non-empty generation, exact fixed-seed block-and-biome parity, and
complete serial/sync 2,025-chunk pregeneration on Paper, Fabric, Forge, and NeoForge. A second real content-mod
fixture also passes entity, item, block, structure loot, death loot, headless initial-spawn, and 2,025-chunk
pregeneration gates on all three mod loaders. It does not yet satisfy the minimum/latest loader, complete
Bukkit-family, client, lifecycle, or full pregen-control matrix below.
The current isolated smoke proves fresh non-empty generation, exact fixed-seed block-and-biome parity, and complete serial/sync 2,025-chunk pregeneration on Paper, Fabric, Forge, and NeoForge. A second real content-mod fixture also passes entity, item, block, structure loot, death loot, headless initial-spawn, and 2,025-chunk pregeneration gates on all three mod loaders. It does not yet satisfy the minimum/latest loader, complete Bukkit-family, client, lifecycle, or full pregen-control matrix below.
- [ ] Bukkit-family server matrix:
- [ ] Paper current target
@@ -314,8 +237,7 @@ Bukkit-family, client, lifecycle, or full pregen-control matrix below.
- [ ] Pregeneration start, pause, resume, cancel, restart, and shutdown
- [ ] Studio validation, hotload failure recovery, and successful hotload where supported
- [ ] Clean startup and shutdown without leaked threads or incomplete futures
- [x] Content-mod gate on Fabric, Forge, and NeoForge using Nerospace beta.7 and Neroland Core 1.4.0 with
authored `nerospace:meadow_loper`, `nerospace:raw_nerosium`, and `nerospace:nerosium_ore` resources.
- [x] Content-mod gate on Fabric, Forge, and NeoForge using Nerospace beta.7 and Neroland Core 1.4.0 with authored `nerospace:meadow_loper`, `nerospace:raw_nerosium`, and `nerospace:nerosium_ore` resources.
- [ ] Client matrix:
- [ ] Modded Iris server + Iris client mod
- [ ] Modded Iris server + client without Iris where loader rules permit
@@ -324,8 +246,7 @@ Bukkit-family, client, lifecycle, or full pregen-control matrix below.
- [ ] Integrated singleplayer create, leave, and create/join again in the same client process
- [ ] Pregen HUD, Vision map, cursor overlay, keybinds, and Studio toasts
Gate: every advertised server, loader, client, and content path completes the same acceptance scenario or
has a clearly documented intentional capability difference.
Gate: every advertised server, loader, client, and content path completes the same acceptance scenario or has a clearly documented intentional capability difference.
## 11. Documentation and repository hygiene
@@ -343,61 +264,21 @@ has a clearly documented intentional capability difference.
### Confirmed release blockers and follow-ups
- [ ] Freeze the default overworld pack to an immutable release input. The runtime downloader currently
follows the mutable `master` branch, so any recorded tree checksum remains reproducible only while
that upstream content is unchanged. Immutable branch/tag/commit URL resolution is implemented, but
published commit `8e32852ee6ecd039fae27a36f701f57cdc02e83f` predates the five local slime-category
and biome-tag corrections, the dormant standard entity resource restoration, and removal of the
legacy default ambient-spawner attachments; publish those pack edits under a new commit/tag before
pinning automatic installs.
- [x] Make modded GoldenHash metadata use the active Iris engine seed. Fabric, Forge, and NeoForge generated
identical output from Iris seed `1337`, but filenames and headers recorded each vanilla level seed,
preventing one captured baseline file from being reused directly across loaders.
- [x] Correct the default overworld pack's slime spawn category from implicit `MISC` to explicit `MONSTER`
in `biomes/vanilla/mangrove_swamp.json`, `biomes/swamp/cambian-drift.json`,
`biomes/swamp/cambian-drift-extended.json`, `biomes/swamp/marsh.json`, and
`biomes/swamp/marsh-rotten.json`. NeoForge exposes the bad category at startup; all loaders generate
the same bad datapack entry, which can affect natural slime spawning and mob-cap accounting.
- [x] Extend `PackValidator` to reject authored custom-biome spawn categories that disagree with the live
entity category instead of allowing the bad datapack to reach loader validation.
- [x] Restore exactly the 36 standard entity resources required by the overworld's retained spawner library
from their last authored revision, preserving their type/surface values without restoring deleted
unique entities, while detaching every regional and spider-infestation ambient spawner so the library
remains dormant unless a pack author explicitly references it.
- [x] Delegate ongoing natural spawn tables in custom Iris biomes to each `vanillaDerivative` on Bukkit,
Fabric, Forge, and NeoForge; explicit custom entries replace the same native entity type and extend
the rest, while structure overrides remain authoritative and cached tables avoid hot-path allocation.
- [x] Add validated custom-biome tag opt-ins and put all five explicit overworld slime biomes in
`minecraft:allows_surface_slime_spawns`, allowing Minecraft's native surface-slime checks to succeed.
- [x] Add Minecraft 26.2 default-clock metadata to generated Iris overworld and End dimension types so
`/time set`, `/time add`, time queries, and clock controls work in Iris overworld dimensions. An
isolated Paper 26.2 runtime loaded a dimension using `iris:overworld`, reported the
`minecraft:overworld` clock, accepted day and night time markers, and returned the clock time.
- [x] Make synchronous modded pregen completion diagnostics report meaningful concurrency values. The
successful runs reported `peakInFlight=0 finalLimit=32` despite a strict `inFlightCap=1` sync mode.
- [ ] Pin or fix the isolated test harness behavior before treating it as release evidence: setting an
instance isolated currently leaves consumer-content symlinks in place. This pass used a fresh,
dedicated harness root, so those links pointed only to test-local content and did not contaminate
the test, but the isolation flag alone is insufficient.
- [x] Resolve the fixed-seed order/state-dependent block generation and Paper-versus-modded biome-hash
difference. The manually built candidate produced one exact full hash before and after 2,025-chunk
pregeneration on Paper, Fabric, Forge, and NeoForge; Fabric also retained it after restart.
- [ ] Re-run Folia when an upstream 26.2 server build becomes available. The official 26.2 build endpoint
currently returns `version_not_found`; an incompatible 26.1.2 runtime is not acceptable beta evidence.
- [ ] Nerospace beta.7's bundled `nerospace:guide/new_life` advancement uses the obsolete
`minecraft:entity_sub_predicate_type`/`minecraft:type` shape and logs one datapack parse error on Fabric,
Forge, and NeoForge 26.2. Iris's custom block, item, entity, chest loot, death loot, and pregeneration
integration all pass despite that independent content-mod error; update Nerospace before using it as a
clean-log beta recommendation.
- [x] Preserve structure-level loot through placement persistence. Newly placed structure containers receive a
versioned, delimiter-safe marker containing the piece object, deterministic placement id, and owning
structure; `Engine.getObjectPlacement()` reconstructs authored loot in order at weight 1 for the existing
Bukkit and modded application paths without overriding global loot. Legacy `object@id` markers remain
readable, malformed and unknown-version markers fail safely, and marker writes are storage-container-only.
- [x] Remove the unsupported `IrisStructurePlacement` `rotation`, `translate`, and `scale` fields from beta
authoring and generated schemas. Read-only pack validation now blocks those keys specifically inside
dimension, region, and biome `structures[]` entries instead of accepting settings with no runtime effect;
ordinary object-placement transforms remain valid and are not inspected by this check.
- [ ] Freeze the default overworld pack to an immutable release input. The runtime downloader currently follows the mutable `master` branch, so any recorded tree checksum remains reproducible only while that upstream content is unchanged. Immutable branch/tag/commit URL resolution is implemented, but published commit `8e32852ee6ecd039fae27a36f701f57cdc02e83f` predates the five local slime-category and biome-tag corrections, the dormant standard entity resource restoration, and removal of the legacy default ambient-spawner attachments; publish those pack edits under a new commit/tag before pinning automatic installs.
- [x] Make modded GoldenHash metadata use the active Iris engine seed. Fabric, Forge, and NeoForge generated identical output from Iris seed `1337`, but filenames and headers recorded each vanilla level seed, preventing one captured baseline file from being reused directly across loaders.
- [x] Correct the default overworld pack's slime spawn category from implicit `MISC` to explicit `MONSTER` in `biomes/vanilla/mangrove_swamp.json`, `biomes/swamp/cambian-drift.json`, `biomes/swamp/cambian-drift-extended.json`, `biomes/swamp/marsh.json`, and `biomes/swamp/marsh-rotten.json`. NeoForge exposes the bad category at startup; all loaders generate the same bad datapack entry, which can affect natural slime spawning and mob-cap accounting.
- [x] Extend `PackValidator` to reject authored custom-biome spawn categories that disagree with the live entity category instead of allowing the bad datapack to reach loader validation.
- [x] Restore exactly the 36 standard entity resources required by the overworld's retained spawner library from their last authored revision, preserving their type/surface values without restoring deleted unique entities, while detaching every regional and spider-infestation ambient spawner so the library remains dormant unless a pack author explicitly references it.
- [x] Delegate ongoing natural spawn tables in custom Iris biomes to each `vanillaDerivative` on Bukkit, Fabric, Forge, and NeoForge; explicit custom entries replace the same native entity type and extend the rest, while structure overrides remain authoritative and cached tables avoid hot-path allocation.
- [x] Add validated custom-biome tag opt-ins and put all five explicit overworld slime biomes in `minecraft:allows_surface_slime_spawns`, allowing Minecraft's native surface-slime checks to succeed.
- [x] Add Minecraft 26.2 default-clock metadata to generated Iris overworld and End dimension types so `/time set`, `/time add`, time queries, and clock controls work in Iris overworld dimensions. An isolated Paper 26.2 runtime loaded a dimension using `iris:overworld`, reported the `minecraft:overworld` clock, accepted day and night time markers, and returned the clock time.
- [x] Make synchronous modded pregen completion diagnostics report meaningful concurrency values. The successful runs reported `peakInFlight=0 finalLimit=32` despite a strict `inFlightCap=1` sync mode.
- [ ] Pin or fix the isolated test harness behavior before treating it as release evidence: setting an instance isolated currently leaves consumer-content symlinks in place. This pass used a fresh, dedicated harness root, so those links pointed only to test-local content and did not contaminate the test, but the isolation flag alone is insufficient.
- [x] Resolve the fixed-seed order/state-dependent block generation and Paper-versus-modded biome-hash difference. The manually built candidate produced one exact full hash before and after 2,025-chunk pregeneration on Paper, Fabric, Forge, and NeoForge; Fabric also retained it after restart.
- [ ] Re-run Folia when an upstream 26.2 server build becomes available. The official 26.2 build endpoint currently returns `version_not_found`; an incompatible 26.1.2 runtime is not acceptable beta evidence.
- [ ] Nerospace beta.7's bundled `nerospace:guide/new_life` advancement uses the obsolete `minecraft:entity_sub_predicate_type`/`minecraft:type` shape and logs one datapack parse error on Fabric, Forge, and NeoForge 26.2. Iris's custom block, item, entity, chest loot, death loot, and pregeneration integration all pass despite that independent content-mod error; update Nerospace before using it as a clean-log beta recommendation.
- [x] Preserve structure-level loot through placement persistence. Newly placed structure containers receive a versioned, delimiter-safe marker containing the piece object, deterministic placement id, and owning structure; `Engine.getObjectPlacement()` reconstructs authored loot in order at weight 1 for the existing Bukkit and modded application paths without overriding global loot. Legacy `object@id` markers remain readable, malformed and unknown-version markers fail safely, and marker writes are storage-container-only.
- [x] Remove the unsupported `IrisStructurePlacement` `rotation`, `translate`, and `scale` fields from beta authoring and generated schemas. Read-only pack validation now blocks those keys specifically inside dimension, region, and biome `structures[]` entries instead of accepting settings with no runtime effect; ordinary object-placement transforms remain valid and are not inspected by this check.
Gate: documentation and distribution metadata describe the behavior users will actually receive.
@@ -414,7 +295,7 @@ Gate: documentation and distribution metadata describe the behavior users will a
- [ ] `known-issues-reviewed`: pass.
- [ ] Working tree is clean on the exact release commit.
- [ ] CI is green on that commit and all evidence artifacts are retained.
- [ ] Complete every item in `docs/release-checklist.md` without rebuilding from different source.
- [ ] Complete every item in `86 - Maintainer - Release Checklist.md` without rebuilding from different source.
Release decision:
@@ -424,67 +305,37 @@ Release decision:
## Fixes already completed in the current working tree
- [x] Concurrent generation binds immutable engine/session/chunk context per worker scope and restores or
removes that binding at scope close.
- [x] Context-backed stream caches reject the wrong engine, a stale generation session, and coordinates
outside the bound chunk.
- [x] Registry-backed mantle and `.mat` reads bind the owning pack data explicitly, and heightmap object
placement no longer depends on ambient generation context.
- [x] Concurrent generation binds immutable engine/session/chunk context per worker scope and restores or removes that binding at scope close.
- [x] Context-backed stream caches reject the wrong engine, a stale generation session, and coordinates outside the bound chunk.
- [x] Registry-backed mantle and `.mat` reads bind the owning pack data explicitly, and heightmap object placement no longer depends on ambient generation context.
- [x] Configured Matter placements use the initialized canonical Matter loader instead of a duplicate null field.
- [x] Deterministic barrier, worker-reuse, nested-scope, close-order, and context-cache regression tests pass.
- [x] Bukkit/Paper pregeneration accepts small positive radii and a strict one-in-flight `serial=true` mode
without changing normal Paper/Folia concurrency.
- [x] Pregeneration drains the final backend callback before reporting completion, eliminating the observed
2,024/2,025 success summary; delayed final success and failure paths have regression coverage.
- [x] Modded synchronous and asynchronous completion counters count only successful chunks, and final
summaries include generated, total, failed, and duration values.
- [x] Bukkit/Paper pregeneration accepts small positive radii and a strict one-in-flight `serial=true` mode without changing normal Paper/Folia concurrency.
- [x] Pregeneration drains the final backend callback before reporting completion, eliminating the observed 2,024/2,025 success summary; delayed final success and failure paths have regression coverage.
- [x] Modded synchronous and asynchronous completion counters count only successful chunks, and final summaries include generated, total, failed, and duration values.
- [x] GoldenHash null-biome fallback is explicitly `minecraft:plains` on Bukkit and modded adapters.
- [x] GoldenHash metadata uses the active Iris seed across every platform, and GitHub pack downloads accept
validated immutable commit and tag references in preparation for freezing the default pack.
- [x] GoldenHash metadata uses the active Iris seed across every platform, and GitHub pack downloads accept validated immutable commit and tag references in preparation for freezing the default pack.
- [x] Runtime splash identity derives from the packaged artifact version instead of a stale release label.
- [x] Pack validation is read-only; cleanup and restore require explicit preview/apply flows with fresh scans,
direct-child containment, conflict refusal, per-pack serialization, truthful rollback reporting, and
no-overwrite quarantine handling.
- [x] Custom-biome spawn groups validate against live platform entity categories, including `AXOLOTLS`, and
the default overworld slime records are explicitly `MONSTER` with isolated NeoForge proof.
- [x] Spawner entity dependency validation covers both runtime spawn lists, malformed entry/container shapes,
missing or malformed referenced entities, nested resource keys, and path containment; the default pack's
dormant spawner library resolves to exactly 36 standard entities and no restored unique entities.
- [x] `.iris` packaging collects entity dependencies from both normal and initial spawner lists, so an entity
used exclusively during initial chunk spawning remains present after export.
- [x] Newly placed structure containers persist versioned structure ownership and resolve the structure's authored
loot through the shared Bukkit/modded placement path without replacing global loot or consuming generation RNG.
- [x] VolmLib is pinned to commit `d9026a7c8ebc391c8109f401ce79a0ce65df3969`; local-development and
clean remote-resolution modes propagate through every nested platform build.
- [x] Headless classload validation scans all 1,166 compiled core classes, including all 353 nested classfiles;
331 nested classes initialize without server APIs and the remaining 22 match exact reviewed class and
dependency-namespace entries.
- [x] Modded worldcheck uses a daemon coordinator with bounded waits on every server task, stops the server
before exiting, and returns nonzero for internal failure, timeout, interruption, thrown checks, and
shutdown failure; its exit contract is covered by the Fabric shared-source test gate.
- [x] Pack validation is read-only; cleanup and restore require explicit preview/apply flows with fresh scans, direct-child containment, conflict refusal, per-pack serialization, truthful rollback reporting, and no-overwrite quarantine handling.
- [x] Custom-biome spawn groups validate against live platform entity categories, including `AXOLOTLS`, and the default overworld slime records are explicitly `MONSTER` with isolated NeoForge proof.
- [x] Spawner entity dependency validation covers both runtime spawn lists, malformed entry/container shapes, missing or malformed referenced entities, nested resource keys, and path containment; the default pack's dormant spawner library resolves to exactly 36 standard entities and no restored unique entities.
- [x] `.iris` packaging collects entity dependencies from both normal and initial spawner lists, so an entity used exclusively during initial chunk spawning remains present after export.
- [x] Newly placed structure containers persist versioned structure ownership and resolve the structure's authored loot through the shared Bukkit/modded placement path without replacing global loot or consuming generation RNG.
- [x] VolmLib is pinned to commit `d9026a7c8ebc391c8109f401ce79a0ce65df3969`; local-development and clean remote-resolution modes propagate through every nested platform build.
- [x] Headless classload validation scans all 1,166 compiled core classes, including all 353 nested classfiles; 331 nested classes initialize without server APIs and the remaining 22 match exact reviewed class and dependency-namespace entries.
- [x] Modded worldcheck uses a daemon coordinator with bounded waits on every server task, stops the server before exiting, and returns nonzero for internal failure, timeout, interruption, thrown checks, and shutdown failure; its exit contract is covered by the Fabric shared-source test gate.
- [x] Fabric protocol startup tolerates the pre-player-list server phase.
- [x] NeoForge registers the shared payload once as bidirectional.
- [x] Fabric distributable metadata declares the bundled transitive access-widener.
- [x] Fabric, Forge, and NeoForge relocate Iris's embedded Sentry runtime so another mod can bundle Sentry
without a duplicate-package module-resolution failure; the corrected Forge and NeoForge artifacts boot
alongside Neroland Core's jar-in-jar Sentry dependency.
- [x] Fabric, Forge, and NeoForge resolve Minecraft 26.2's supplied OSHI, JNA, JNA Platform, and LZ4
implementations without embedding or relocating them. The distribution gate scans outer classes and
nested jars for private rewritten references or duplicate runtime libraries before accepting each artifact.
- [x] Headless force-loaded chunks receive structure loot and initial entity spawning on Bukkit and every mod
loader without requiring a player to enter the world. Bukkit target collection is global/region-safe,
bounded, rotating, and deduplicated; modded initial-spawn requests retry and recover without caller-runs
disk work on the server tick.
- [x] Bukkit world creation preserves explicit `pack:dimensionKey` selection through pack installation and
engine creation, matching the modded command behavior and preventing same-key cross-pack collisions.
- [x] Bukkit/Folia world-manager snapshots keep world, player, entity, chunk, and force-load API access on the
appropriate global, entity, or region scheduler and refresh saturation before its early-return gate.
- [x] Fabric, Forge, and NeoForge relocate Iris's embedded Sentry runtime so another mod can bundle Sentry without a duplicate-package module-resolution failure; the corrected Forge and NeoForge artifacts boot alongside Neroland Core's jar-in-jar Sentry dependency.
- [x] Fabric, Forge, and NeoForge resolve Minecraft 26.2's supplied OSHI, JNA, JNA Platform, and LZ4 implementations without embedding or relocating them. The distribution gate scans outer classes and nested jars for private rewritten references or duplicate runtime libraries before accepting each artifact.
- [x] Headless force-loaded chunks receive structure loot and initial entity spawning on Bukkit and every mod loader without requiring a player to enter the world. Bukkit target collection is global/region-safe, bounded, rotating, and deduplicated; modded initial-spawn requests retry and recover without caller-runs disk work on the server tick.
- [x] Bukkit world creation preserves explicit `pack:dimensionKey` selection through pack installation and engine creation, matching the modded command behavior and preventing same-key cross-pack collisions.
- [x] Bukkit/Folia world-manager snapshots keep world, player, entity, chunk, and force-load API access on the appropriate global, entity, or region scheduler and refresh saturation before its early-return gate.
- [x] Multicore Perfection waits for isolated worker completion.
- [x] Bukkit exclusive-control permits release after failures and interruptions.
- [x] Modded sea-level/base-column calculations use absolute world Y.
- [x] Low-risk map drawing, post-processing, base-column, and block-buffer loop costs were reduced.
- [x] Core tests, Bukkit plugin tests, all-platform assembly, archive integrity, and fresh Iris-world checks
on Fabric, Forge, and NeoForge passed for this fix set.
- [x] Core tests, Bukkit plugin tests, all-platform assembly, archive integrity, and fresh Iris-world checks on Fabric, Forge, and NeoForge passed for this fix set.
These completed items remain subject to the final packaged-artifact, Bukkit/Folia, concurrency, and
performance gates above.
These completed items remain subject to the final packaged-artifact, Bukkit/Folia, concurrency, and performance gates above.
+149
View File
@@ -0,0 +1,149 @@
# 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 Bukkit types, `java.*`, 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`.
| 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.
```gradle
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.
```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; 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.
```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 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:
```java
String label = switch (kind) {
case LAND -> "land";
case OCEAN -> "water";
default -> "";
};
```
+435
View File
@@ -0,0 +1,435 @@
# 91 - API - Terrain
`art.arcane.iris.api.terrain` answers what the Iris generator says about a coordinate: whether a world is Iris-generated, which biome and region the pack places, surface height, and whether that surface is land, shore, ocean, or void. It reads the **generator**, not the world: no chunk load, no forced generation, no placed-block read, and no knowledge of player edits. Reads are non-blocking noise evaluation over a shared per-chunk cache.
Build and service acquisition: `90 - API - Getting Started.md`. Service: `IrisTerrainService`, registered at `ServicePriority.Normal` for Iris's enabled lifetime.
```java
package com.example.integration;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class TerrainAccess {
private TerrainAccess() {
}
public static IrisTerrainService service() {
RegisteredServiceProvider<IrisTerrainService> provider =
Bukkit.getServicesManager().getRegistration(IrisTerrainService.class);
return provider == null ? null : provider.getProvider();
}
}
```
Missing registration means Iris is absent or not enabled yet — `null`, not an exception. There is no static `Iris` accessor for this surface.
---
## The read surface
```java
public interface IrisTerrainService {
boolean isIrisWorld(World world);
Optional<IrisWorldInfo> worldInfo(World world);
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
Optional<String> regionName(World world, int blockX, int blockZ);
int maxSampleColumns();
int maxSampleChunks();
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
}
```
All coordinates are **absolute block coordinates in world space**, including `blockY` and `surfaceHeight`. There is no engine-space offset for the caller.
`*Key` returns a pack load key (`desert/hot-dunes`, `overworld`) — stable, lowercase, store this. `*Name` returns the author's display string (`Hot Desert Dunes`) — for display; it can change when the pack author edits it. Both are empty when the value is absent or the empty string.
---
## Cost and blocking
Iris's generator is procedural noise. Each read evaluates the stack for one column and memoises in a shared per-chunk noise cache. Cold columns run pack noise; warm columns are array reads. Nothing here reads chunk storage, loads a region file, takes a contended lock, waits on a future, or asks the server to generate.
| Call | Cost when cold | Cost when warm | Forces generation | Can block | When data is absent |
|---|---|---|---|---|---|
| `isIrisWorld` | `World#getGenerator()` + `instanceof` | same | No | No | `false` |
| `worldInfo` | field reads off live engine/dimension | same | No | No | `Optional.empty()` |
| `surfaceHeight` | one height sample (region + base-biome streams) | array read | No | No | `OptionalInt.empty()` |
| `surfaceKind` | height sample; surface-biome only when column is above fluid and not void floor | array read | No | No | `IrisSurfaceKind.UNKNOWN` |
| `surfaceBiomeKey` / `surfaceBiomeName` | surface-biome sample (height, base biome, region) | array read | No | No | `Optional.empty()` |
| `biomeKey` at/near surface | as surface biome + height to choose surface vs cave | array read | No | No | `Optional.empty()` |
| `biomeKey` well below surface | above + cave-biome stream and carving resolution | array reads | No | No | `Optional.empty()` |
| `regionKey` / `regionName` | region sample (cheapest biome-family call) | array read | No | No | `Optional.empty()` |
| `maxSampleColumns` / `maxSampleChunks` | settings fields | same | No | No | positive number always |
| `sampleColumns` | one of the above per column, chunk-local order | array reads | No | No | `false`, sink untouched |
**Tight main-thread loops are non-blocking but wasteful.** They can evict the generator's noise cache working set shared with live chunk generation — chunk gen slows, not your loop. Use `sampleColumns` for anything wider than a handful of columns.
**Values are the generator's opinion, not the world's.** `surfaceHeight` is the topmost generated terrain block Y. It excludes objects, decorations, structures, trees, snow, and player edits. For real blocks use Bukkit `World#getHighestBlockYAt` (chunk load cost). For pack intent (pregen planners, map renderers, spawn pickers) use this API.
### Surface height, precisely
`surfaceHeight` returns absolute Y of the **topmost generated terrain block**. Standing height is `surfaceHeight + 1`. Fluid is ignored: under ocean you get the sea floor. Compare with `IrisWorldInfo.fluidHeight()` or use `surfaceKind`.
---
## Threading
**Every read may be called from any thread, including async.**
- Only Bukkit call on your behalf: `World#getGenerator()` on the world object. No chunk, block state, entity, or world-list walk.
- After that: engine-internal noise over concurrent caches; no region-owned state.
- No method takes a lock you can contend on, calls `join`, or schedules onto another thread.
Wide scans belong on your own async executor. On Folia there is no single correct region thread for a multi-region scan.
**`IrisColumnSink.accept` runs on the thread that called `sampleColumns`, inline, once per column.** If that thread is async, the sink must not touch Bukkit state. Collect locally, hop afterward.
---
## Column sampling
`sampleColumns` walks a rectangle at a stride, chunk by chunk, into your sink.
```java
public record IrisColumnQuery(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
public static IrisColumnQuery rect(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields);
public long columnCount();
public long chunkCount();
public EnumSet<IrisColumnField> fields();
}
```
Bounds are **inclusive on both ends**. Lattice anchors at `(minBlockX, minBlockZ)` and steps by `strideBlocks`.
Constructor rejects with `IllegalArgumentException`:
- empty `fields`,
- `maxBlockX < minBlockX` or `maxBlockZ < minBlockZ`,
- `strideBlocks < 1`.
`fields` is defensively copied on construction and on every `fields()` call. `fields()` allocates a fresh `EnumSet` each call — hoist it out of loops.
`columnCount()` and `chunkCount()` saturate at `Long.MAX_VALUE` on overflow.
### Hard limits
```
maxSampleChunks = max(64, noiseCacheSize / 4)
maxSampleColumns = maxSampleChunks * 256 (capped at Integer.MAX_VALUE)
```
Default `performance.noiseCacheSize` is `1024`**256 chunks and 65 536 columns**. One API query may not consume more than a quarter of the live generator cache.
**A query over either limit returns `false` and never calls the sink.** No partial answer, truncation, exception, or log line.
Limits are independent. Example: stride 64 over a 6400×6400 block rectangle can pass the column limit and fail the chunk limit. **`chunkCount()` is the chunk span of the rectangle, not sampled columns** — stride does not reduce it. Tile large areas.
Ask `maxSampleColumns()` / `maxSampleChunks()` every time; they change when the operator edits settings and reloads.
### The sink
```java
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
```
Every column produces one `accept`. Placeholders for unrequested fields are not distinguishable from real data by value alone:
| Field requested | Parameter | If requested | If not |
|---|---|---|---|
| `SURFACE_HEIGHT` | `surfaceHeight` | absolute world Y of topmost terrain | `-1` |
| `SURFACE_KIND` | `kind` | `LAND`, `SHORE`, `OCEAN`, or `VOID` | `IrisSurfaceKind.UNKNOWN` |
| `BIOME_KEY` | `biomeKey` | biome load key | `null` |
`-1` is a legal absolute Y in worlds with negative min height — **never treat `-1` as absent**. Branch on your field set. `biomeKey` may be `null` even when requested if the column has no biome.
Fewer fields cost less. `SURFACE_KIND` alone skips the biome stream for void-floor and at-or-below-fluid columns. `BIOME_KEY` pays for the biome stream every column.
### Visit order
Columns arrive **grouped by chunk**. Chunk walk: Z outer, X inner. Within a chunk: lattice Z outer, X inner. Deterministic for a given query; **not** a pure row-major sweep of the rectangle. Sort or index by `(blockX, blockZ)` if you need raster order.
### Return value
`true` iff every column was delivered. `false` when:
- `world`, `query`, or `sink` is null, or no live Iris engine — sink untouched;
- a limit was exceeded — sink untouched;
- **your sink threw** — walk stops at that column;
- **engine closed mid-walk** — walk stops at that column.
In the last two cases, already-delivered columns stay delivered. Treat `false` as incomplete; discard partial results if completeness is required.
---
## Worked example: flattest buildable spot
Async sample, then hop to the player's entity scheduler (correct on Paper and Folia).
```java
package com.example.settlement;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.util.EnumSet;
import java.util.Optional;
import java.util.concurrent.Executor;
public final class SettlementSiteFinder {
private static final int RADIUS_BLOCKS = 512;
private static final int STRIDE_BLOCKS = 8;
private final Plugin plugin;
private final Executor background;
public SettlementSiteFinder(Plugin plugin, Executor background) {
this.plugin = plugin;
this.background = background;
}
public void findFor(Player player) {
World world = player.getWorld();
Location origin = player.getLocation();
int centreX = origin.getBlockX();
int centreZ = origin.getBlockZ();
background.execute(() -> {
String result = search(world, centreX, centreZ);
player.getScheduler().run(plugin, task -> player.sendMessage(result), null);
});
}
private String search(World world, int centreX, int centreZ) {
IrisTerrainService terrain = service();
if (terrain == null || !terrain.isIrisWorld(world)) {
return "That world is not generated by Iris.";
}
Optional<IrisWorldInfo> info = terrain.worldInfo(world);
if (info.isEmpty()) {
return "The Iris engine for that world is not available right now.";
}
IrisColumnQuery query = IrisColumnQuery.rect(
centreX - RADIUS_BLOCKS,
centreZ - RADIUS_BLOCKS,
centreX + RADIUS_BLOCKS,
centreZ + RADIUS_BLOCKS,
STRIDE_BLOCKS,
EnumSet.of(IrisColumnField.SURFACE_HEIGHT, IrisColumnField.SURFACE_KIND));
if (query.columnCount() > terrain.maxSampleColumns()
|| query.chunkCount() > terrain.maxSampleChunks()) {
return "That search area is larger than this server allows.";
}
int fluidHeight = info.get().fluidHeight();
Best best = new Best();
IrisColumnSink sink = (int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey) -> {
if (kind != IrisSurfaceKind.LAND || surfaceHeight <= fluidHeight) {
return;
}
long score = (long) Math.abs(surfaceHeight - fluidHeight) * 1024L
+ Math.abs(blockX - centreX) + Math.abs(blockZ - centreZ);
if (score < best.score) {
best.score = score;
best.x = blockX;
best.y = surfaceHeight;
best.z = blockZ;
}
};
if (!terrain.sampleColumns(world, query, sink)) {
return "The terrain scan did not complete. Try again.";
}
if (best.score == Long.MAX_VALUE) {
return "No dry land within " + RADIUS_BLOCKS + " blocks.";
}
return "Best site: " + best.x + ", " + (best.y + 1) + ", " + best.z;
}
private IrisTerrainService service() {
RegisteredServiceProvider<IrisTerrainService> provider =
plugin.getServer().getServicesManager().getRegistration(IrisTerrainService.class);
return provider == null ? null : provider.getProvider();
}
private static final class Best {
private long score = Long.MAX_VALUE;
private int x;
private int y;
private int z;
}
}
```
`Best` needs no synchronisation: the sink runs inline on the `sampleColumns` caller thread.
---
## The minimum: one coordinate
```java
IrisTerrainService terrain = service();
String biome = terrain == null
? "unknown"
: terrain.surfaceBiomeName(player.getWorld(), player.getLocation().getBlockX(),
player.getLocation().getBlockZ()).orElse("unknown");
```
`surfaceBiomeName` returns empty for non-Iris worlds, null worlds, closing engines, or disabled Iris. Call `isIrisWorld` only when you need to distinguish "not Iris" from "Iris has no answer".
---
## What `IrisWorldInfo` tells you
```java
public record IrisWorldInfo(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeight,
boolean studio) {
public int height();
}
```
| Component | Meaning |
|---|---|
| `dimensionKey` | Pack load key of the dimension (e.g. `overworld`) |
| `worldIdentity` | World's namespaced key as string (e.g. `minecraft:overworld`) |
| `seed` | Raw generator seed |
| `minHeight` | Absolute world floor Y (e.g. `-64`) |
| `maxHeight` | Absolute world ceiling Y, exclusive (e.g. `320`) |
| `fluidHeight` | Absolute pack sea level Y (`pack fluid height + minHeight`) |
| `studio` | `true` only for a transient studio world |
| `height()` | `maxHeight - minHeight` |
All height fields are absolute world Y, comparable with `surfaceHeight` and `blockY`. Constructor rejects null `dimensionKey`/`worldIdentity` and non-positive height range.
`worldIdentity` is what Iris persists per-world state under. Outside the three vanilla dimensions the server derives the key from the world folder — renaming the folder changes `worldIdentity` and `World#getName()`.
`studio` worlds exist briefly for pack authoring; skip them for persistence.
`seed` reproduces the entire world offline. Iris does not expose it via PlaceholderAPI (`09 - PlaceholderAPI.md`). Do not put it where players can read it.
---
## Failure policy
| Situation | Behaviour |
|---|---|
| `world` is `null` | Queries answer absent; `sampleColumns` returns `false` |
| World has no Iris generator | Same |
| Iris disabled, or disabled between calls | Same; nothing throws |
| Generator closing, or engine closed | `isIrisWorld` still **`true`**; other queries absent |
| Query throws inside engine | Counted, logged with stack, answered absent |
| `query` or `sink` null | `sampleColumns` returns `false` |
| Query exceeds sample limits | `false`, sink never called, nothing logged |
| Sink throws | Walk aborts, fault logged (throttled), `false`; prior columns delivered |
| Engine closes mid-walk | Walk stops, `false` |
**`isIrisWorld` does not check liveness.** It answers "created by Iris", not "can answer right now". During unload/shutdown you can see `isIrisWorld == true` with empty `worldInfo`. Use `Optional` carefully.
**No caller quarantine.** Fault counters only throttle log lines to at most one report per minute per category; the count is cumulative.
No checked exceptions. Unchecked throws only from `IrisColumnQuery` / `IrisWorldInfo` construction validation.
---
## Configuration
`plugins/Iris/settings.json`:
| Key | Default | Effect |
|---|---|---|
| `performance.noiseCacheSize` | `1024` | Shared noise cache chunk capacity. `maxSampleChunks = max(64, this / 4)`; `maxSampleColumns = maxSampleChunks * 256` |
No on/off switch for the terrain API. Answers for every world with a live Iris engine; absent otherwise.
---
## Enum reference
### `IrisSurfaceKind`
| Constant | Meaning | Test applied (engine space, then reported in absolute terms) |
|---|---|---|
| `LAND` | Dry ground | Surface above fluid height; biome not shore |
| `SHORE` | Beach or bank | Surface above fluid; pack classifies biome as shore |
| `OCEAN` | Under water / sea floor at sea level | Surface at or below fluid height (and above void floor) |
| `VOID` | Nothing generated | Engine surface height ≤ 0 → absolute surface ≤ `minHeight()` |
| `UNKNOWN` | No answer | Not Iris / unavailable / fault / `SURFACE_KIND` not requested |
**`VOID` wins first.** Then fluid check, then shore vs land. Mutually exclusive.
`OCEAN` is inclusive at fluid height. Compare `surfaceHeight` to `fluidHeight` yourself if the one-block boundary matters.
### `IrisColumnField`
| Constant | Fills | Extra work |
|---|---|---|
| `SURFACE_HEIGHT` | `surfaceHeight` | one height sample per column |
| `SURFACE_KIND` | `kind` | height sample; biome only when above void floor and above fluid |
| `BIOME_KEY` | `biomeKey` | biome sample per column, always |
`SURFACE_HEIGHT` and `SURFACE_KIND` share the height sample when both are requested.
Write a `default` arm when switching enums: `90 - API - Getting Started.md`.
+365
View File
@@ -0,0 +1,365 @@
# 92 - API - World Events
`IrisWorldEngineEvent` reports when an Iris world's engine becomes usable, is rebuilt under you, or is about to stop being usable. `IrisPregenerationEvent` reports pregeneration job progress. Both are pure observation: not cancellable, and handlers cannot change Iris's next step. Prefer `IrisWorldEngineEvent` over `WorldLoadEvent` when you care about the **generator**: a world exists before its Iris engine can answer, and still exists after the engine is told to close.
Build setup: `90 - API - Getting Started.md`. No service lookup — register a `Listener` in `onEnable`; Bukkit unregisters on your disable.
Each event has its own `HandlerList`. No shared base class. Neither implements `Cancellable`; `ignoreCancelled = true` does nothing useful.
---
## World engine lifecycle
```java
public enum IrisWorldPhase {
ENGINE_READY,
ENGINE_HOTLOADED,
ENGINE_CLOSING
}
```
```
ENGINE_READY engine registered and answering; terrain queries work from here
|
+--> ENGINE_HOTLOADED pack reloaded; same world/engine object, pack contents may change
| any number of times, or never
v
ENGINE_CLOSING engine about to tear down; last call
```
Guarantees:
- `ENGINE_READY` fires **at most once per world registration**, keyed on world UUID. Unload + load again gets a new ready.
- `ENGINE_CLOSING` is **never delivered without a prior `ENGINE_READY`** for that world (ledger-gated).
- `ENGINE_CLOSING` is dispatched **before** Iris starts closing the generator.
- Engine replacement: `ENGINE_CLOSING` for the old, later `ENGINE_READY` for the new — never two consecutive ready without closing between them.
- On Iris shutdown, every announced-ready world gets closing before worker pool drain and generator close.
- `ENGINE_HOTLOADED` is not deduplicated and is not part of ready/closing pairing. Treat pack-derived caches from `ENGINE_READY` as stale when it arrives.
### What `ENGINE_CLOSING` does not promise
Closing fires before the generator closes, but during full plugin shutdown the terrain service may already be withdrawn. **Do not run terrain queries in a closing handler.** Capture state at `ENGINE_READY`; use closing only to drop it. Queries during closing return absent without throwing.
---
## The world engine event
```java
public class IrisWorldEngineEvent extends Event {
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info);
public static HandlerList getHandlerList();
public World getWorld();
public IrisWorldPhase getPhase();
public Optional<IrisWorldInfo> getInfo();
@Override
public HandlerList getHandlers();
}
```
`getWorld()` and `getPhase()` are never null (constructor rejects nulls).
`getInfo()` may be empty if Iris could not describe the engine at dispatch (generator already closing, engine closed, or describe threw — logged; event still delivered). Do not call `Optional#get()` unconditionally.
`IrisWorldInfo` fields: `91 - API - Terrain.md`.
### Threading
**Handlers always run on the main thread. On Folia, that is the global region thread.**
Dispatch:
- Raised on the primary thread: event called **inline** before the raiser continues.
- Raised off-thread (e.g. file-watcher hotload): scheduled to main/global region on a later tick via Iris's event path.
Blocking is forbidden on this thread: no I/O, no `CompletableFuture#join`, no waiting on another scheduler.
---
## Worked example: cache pack metadata per world
```java
package com.example.hud;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.api.world.IrisWorldEngineEvent;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisWorldRegistry implements Listener {
private final Map<UUID, String> dimensionKeys = new ConcurrentHashMap<>();
public String dimensionKeyOf(World world) {
return dimensionKeys.get(world.getUID());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEngine(IrisWorldEngineEvent event) {
UUID worldId = event.getWorld().getUID();
switch (event.getPhase()) {
case ENGINE_READY, ENGINE_HOTLOADED -> {
Optional<IrisWorldInfo> info = event.getInfo();
if (info.isEmpty()) {
dimensionKeys.remove(worldId);
return;
}
dimensionKeys.put(worldId, info.get().dimensionKey());
}
case ENGINE_CLOSING -> dimensionKeys.remove(worldId);
default -> {
}
}
}
}
```
`default` is required because enums can grow: `90 - API - Getting Started.md`. Map is concurrent because readers may be off the event thread.
---
## Pregeneration
```java
public enum IrisPregenPhase {
STARTED,
TICK,
PAUSED,
RESUMED,
SAVING,
COMPLETED,
CANCELLED
}
```
```java
public class IrisPregenerationEvent extends Event {
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress);
public static HandlerList getHandlerList();
public IrisPregenPhase getPhase();
public IrisPregenProgress getProgress();
@Override
public HandlerList getHandlers();
}
```
Both accessors never null; constructor rejects nulls.
### Phase order
```
STARTED -> TICK -> TICK -> ... -> COMPLETED
|
+-- PAUSED -> TICK -> ... -> RESUMED -> TICK -> ...
|
+-- SAVING (at most once, near end)
|
+-- CANCELLED (instead of COMPLETED if stopped early)
```
- **One job at a time, server-wide.** No job id on the event; `IrisPregenProgress` names the world.
- `STARTED` once per job, immediately before first `TICK`.
- `TICK` once per second while the job exists, including while paused.
- `PAUSED` / `RESUMED` on transition only, each followed by a `TICK`.
- `SAVING` at most once per job.
- Exactly one of `COMPLETED` or `CANCELLED` is terminal. No phase after the terminal.
### Threading
**Handlers always run on the main / Folia global region thread.** Pregen ticks on a worker; phases are scheduled (up to about one tick of skew). Fire-and-forget: a throwing handler is logged and skipped; the job does not wait. Do not block the tick thread.
### `IrisPregenProgress`
```java
public record IrisPregenProgress(
String worldName,
String worldIdentity,
double percent,
long generatedChunks,
long totalChunks,
long remainingChunks,
long failedChunks,
double chunksPerSecond,
long etaMillis,
long elapsedMillis,
String method,
boolean paused) {
}
```
| Component | Meaning |
|---|---|
| `worldName` | Never null; falls back to `worldIdentity` |
| `worldIdentity` | World's namespaced key string |
| `percent` | `0.0` .. `100.0` |
| `generatedChunks` | Finished chunks |
| `totalChunks` | Job total |
| `remainingChunks` | Still to do |
| `failedChunks` | Could not generate |
| `chunksPerSecond` | Current rate |
| `etaMillis` | Estimated remaining ms |
| `elapsedMillis` | Since job start |
| `method` | Never null; `""` if unknown |
| `paused` | Job paused |
Constructor sanitises:
- `percent` clamped to `0..100`; non-finite → `0`
- `chunksPerSecond` ≥ 0; non-finite → `0`
- chunk and time counters ≥ 0
- null `worldName``worldIdentity`; null `method``""`
- null `worldIdentity` throws `NullPointerException` at construction — delivered instances always identify a world
`etaMillis` is `0` until enough chunks complete for an estimate. Non-zero `failedChunks` on `COMPLETED` means holes remain.
Operator pregen surface: `07 - Pregeneration.md`.
---
## Worked example: boss bar
```java
package com.example.pregenbar;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public final class PregenBar implements Listener {
private BossBar bar;
@EventHandler(priority = EventPriority.MONITOR)
public void onPregen(IrisPregenerationEvent event) {
IrisPregenProgress progress = event.getProgress();
switch (event.getPhase()) {
case STARTED -> open(progress);
case TICK, PAUSED, RESUMED, SAVING -> update(progress);
case COMPLETED, CANCELLED -> close();
default -> {
}
}
}
private void open(IrisPregenProgress progress) {
close();
bar = Bukkit.createBossBar(
"Pregenerating " + progress.worldName(), BarColor.BLUE, BarStyle.SEGMENTED_10);
for (Player player : Bukkit.getOnlinePlayers()) {
bar.addPlayer(player);
}
update(progress);
}
private void update(IrisPregenProgress progress) {
if (bar == null) {
return;
}
bar.setProgress(progress.percent() / 100.0D);
bar.setColor(progress.paused() ? BarColor.YELLOW : BarColor.BLUE);
bar.setTitle(progress.worldName()
+ " " + progress.generatedChunks() + "/" + progress.totalChunks()
+ " at " + Math.round(progress.chunksPerSecond()) + "/s");
}
private void close() {
if (bar == null) {
return;
}
bar.removeAll();
bar = null;
}
}
```
---
## The minimum: world usable once
```java
@EventHandler
public void onEngine(IrisWorldEngineEvent event) {
if (event.getPhase() == IrisWorldPhase.ENGINE_READY) {
prepare(event.getWorld());
}
}
```
Do not set `ignoreCancelled = true`.
---
## Failure policy
| Situation | Behaviour |
|---|---|
| Your handler throws | Logged; remaining handlers run; Iris lifecycle continues |
| Iris cannot describe world for a phase | Logged; event still delivered with empty `getInfo()` |
| Event dispatch itself throws | Logged with phase and world; registration/teardown proceeds |
| Pregen sink not registered | No `IrisPregenerationEvent` (before enable completes / after disable starts) |
| Pregen handler throws | Logged; job not slowed/paused/stopped |
| Iris shuts down mid-pregen | Terminal phase `CANCELLED` |
| Iris shuts down with worlds registered | Every announced world gets `ENGINE_CLOSING` before worker drain |
No listener quarantine. Iris never silently stalls a lifecycle step because a third party failed.
---
## Configuration
No configuration keys. Events are always on while Iris is enabled; no per-world gate.
---
## Enum reference
### `IrisWorldPhase`
| Constant | Meaning | Fires |
|---|---|---|
| `ENGINE_READY` | Engine registered and answering | Once per world registration |
| `ENGINE_HOTLOADED` | Pack data reloaded in place | Any number of times, or never; not ledger-paired |
| `ENGINE_CLOSING` | Engine about to tear down | Once per registration, always after a ready |
### `IrisPregenPhase`
| Constant | Meaning | Fires |
|---|---|---|
| `STARTED` | Job began | Once, before first `TICK` |
| `TICK` | Progress sample | Once per second while job exists |
| `PAUSED` | Job paused | Transition only + following `TICK` |
| `RESUMED` | Job resumed | Transition only + following `TICK` |
| `SAVING` | Flushing to disk | At most once |
| `COMPLETED` | Reached chunk total | Terminal; exclusive with `CANCELLED` |
| `CANCELLED` | Stopped before total | Terminal; exclusive with `COMPLETED` |
Default arms: `90 - API - Getting Started.md`.
+395
View File
@@ -0,0 +1,395 @@
# 93 - API - Tree Feller
`art.arcane.iris.api.tree` lets another plugin **drive** the Iris tree feller and **charge** for it. The feller removes a whole Iris-generated tree when a sneaking survival player breaks one of its logs with an axe. Integrations can start a run Iris would not start, override durability rules, and reserve a cost per log with commit or refund. The feature is **off by default** (`treeFeller.enabled = false`); the standalone path also requires `iris.treefeller`. `INTEGRATION_OVERRIDE` bypasses both the enabled switch and the permission.
Build and service acquisition: `90 - API - Getting Started.md`. Service: `IrisTreeFellerService` at `ServicePriority.Normal`.
| Goal | Use |
|---|---|
| Start a felling run Iris would not start, or price it | `tryFell` with `TreeFellerOptions.integrationOverride(...)` |
| Avoid double-handling Iris-generated breaks during a run | `isManagedBreak` |
| Ask whether a block belongs to an Iris tree | `isTreeBlock` |
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.IrisTreeFellerService;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class FellerAccess {
private FellerAccess() {
}
public static IrisTreeFellerService service() {
RegisteredServiceProvider<IrisTreeFellerService> provider =
Bukkit.getServicesManager().getRegistration(IrisTreeFellerService.class);
return provider == null ? null : provider.getProvider();
}
}
```
```java
public interface IrisTreeFellerService {
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
boolean isManagedBreak(BlockBreakEvent event);
boolean isTreeBlock(Block block);
}
```
---
## Lifecycle
```
your BlockBreakEvent handler
|
v
tryFell(event, options) register a felling request against this break.
| true = YOUR access is pending (first writer or same-access re-call).
| Nothing removed yet; no hook fired.
|
| (Iris re-checks at EventPriority.MONITOR)
v
onActivationAccepted() run is real. At most once, if at all.
|
| (per LOG block, discovery order)
v
reserveLogCost() -> false refuse; run ends; nothing reserved to refund.
|
| true
v
+--> commitLogCost() log gone; charge final
+--> refundLogCost() log not removed; give cost back
```
Guarantees:
- `onActivationAccepted` fires **at most once per run**, only after MONITOR re-validation: event not cancelled, block still the same Iris tree, tree not already claimed.
- `reserveLogCost` once per **log** (not leaves). Leaves never reserve.
- Reserve runs **before** axe durability charge.
- Exactly one of `commitLogCost` or `refundLogCost` follows a true reserve, except the miss cases under Failure policy.
- **`commitLogCost` is final.** No later refund for that log.
- `reserveLogCost` false ends the **whole** run immediately.
- One tree, one run, server-wide. A second player on the same tree gets the break cancelled with drops suppressed; no hooks for them.
- **No terminal callback.** Count commits/refunds against activation if you need end-of-run accounting.
---
## Threading
| Call | Thread |
|---|---|
| `tryFell` | Thread delivering `BlockBreakEvent` (region thread owning the block) |
| `isManagedBreak` | Any thread (set lookup) |
| `isTreeBlock` | Region thread owning the block; can block on disk |
| `onActivationAccepted` | Region thread of the broken block, inline in MONITOR |
| `reserveLogCost` | Player entity path: entity scheduler on Folia; may run inline on Paper main when already primary |
| `commitLogCost` | Same entity path (scheduled onto the player when not already inline) |
| `refundLogCost` | Same entity path when a refund is delivered |
Cost hooks may touch the feller's inventory/XP/effects. Do **not** read/write blocks from cost hooks on Folia (entity thread ≠ region).
`onActivationAccepted` is inside event dispatch — return promptly.
**Do not block any of the four hooks.** No I/O, no `join`, no long locks. Iris does not time out hanging hooks. Cache remote data (e.g. on join).
### `isTreeBlock` is expensive
Reads Iris mantle for tree provenance. Cold mantle regions load from disk **synchronously on your thread**. Also reads block type/data — chunk must be loaded; call from the owning region thread.
Do not call per block in a loop, per tick, or over large areas. Use for blocks a player just interacted with.
---
## Worked example: stamina per log
### Hooks
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import java.util.UUID;
public final class StaminaFellHooks implements TreeFellerRunHooks {
private static final int COST_PER_LOG = 4;
private final StaminaPool pool;
private final UUID fellerId;
public StaminaFellHooks(StaminaPool pool, UUID fellerId) {
this.pool = pool;
this.fellerId = fellerId;
}
@Override
public void onActivationAccepted() {
pool.beginRun(fellerId);
}
@Override
public boolean reserveLogCost() {
return pool.withdraw(fellerId, COST_PER_LOG);
}
@Override
public void commitLogCost() {
pool.recordSpend(fellerId, COST_PER_LOG);
}
@Override
public void refundLogCost() {
pool.deposit(fellerId, COST_PER_LOG);
}
}
```
All four methods are required (no defaults). `TreeFellerRunHooks.NONE` is the shared no-op with `reserveLogCost` always true.
Hooks are **per run**, not per plugin. Build a new instance per `tryFell` and store feller identity in it. Iris holds the reference for the run and never inspects it beyond the four methods.
### Listener
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.IrisTreeFellerService;
import art.arcane.iris.api.tree.TreeFellerOptions;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
public final class WoodcuttingListener implements Listener {
private static final int PRESERVE_PERCENT = 50;
private final StaminaPool pool;
public WoodcuttingListener(StaminaPool pool) {
this.pool = pool;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBreak(BlockBreakEvent event) {
IrisTreeFellerService feller = FellerAccess.service();
if (feller == null || feller.isManagedBreak(event)) {
return;
}
Player player = event.getPlayer();
if (!pool.hasWoodcutting(player.getUniqueId())) {
return;
}
TreeFellerOptions options = TreeFellerOptions.integrationOverride(
PRESERVE_PERCENT, new StaminaFellHooks(pool, player.getUniqueId()));
feller.tryFell(event, options);
}
}
```
`isManagedBreak` is required: Iris fires a `BlockBreakEvent` for every block it removes during a run so protection plugins see removals. Without the guard you re-enter `tryFell` on every member.
`EventPriority.HIGH` is the usual choice: after typical protection cancels, before Iris's own standalone request at `HIGHEST`. See pending rules below.
```java
@Override
public void onEnable() {
getServer().getPluginManager().registerEvents(new WoodcuttingListener(pool), this);
}
```
---
## Minimum: override only, no charge
```java
IrisTreeFellerService feller = FellerAccess.service();
if (feller != null && !feller.isManagedBreak(event) && classes.isWoodcutter(event.getPlayer())) {
feller.tryFell(event, TreeFellerOptions.integrationOverride(0, TreeFellerRunHooks.NONE));
}
```
`durabilityPreservationChance` of `0` charges every log (vanilla-like). `100` never charges durability for logs. Unbreakable axes are never charged.
`TreeFellerOptions.standalone()` is Iris's own request shape; third parties almost never need it. It still requires `treeFeller.enabled` and `iris.treefeller`.
---
## What `tryFell` promises
```java
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
```
`true` means a pending request is associated with this break for your call path — **not** that a tree will fall. Iris re-validates at `MONITOR` and may drop the request with no hooks.
### Pending request precedence
Pending state is keyed by the `BlockBreakEvent` instance.
| Existing pending | New request | Result |
|---|---|---|
| none | any | Accept; store request; return `true` |
| `STANDALONE` | `STANDALONE` | Keep existing; return `true` |
| `STANDALONE` | `INTEGRATION_OVERRIDE` | Replace the pending standalone request; return `true` |
| `INTEGRATION_OVERRIDE` | `INTEGRATION_OVERRIDE` | Keep the first override; return `true` |
| `INTEGRATION_OVERRIDE` | `STANDALONE` | Keep the override; return `false` |
| managed internal probe (no pending, already managed) | any | return `false` |
`INTEGRATION_OVERRIDE` has precedence over Iris's pending standalone request. The first accepted override keeps its hooks and options; later overrides are idempotently accepted without replacing it. Iris registers its standalone request at `EventPriority.HIGHEST` and finalizes at `MONITOR`, so integrations should submit overrides no later than `HIGHEST` and avoid `MONITOR` ordering races.
Open run state in `onActivationAccepted`, not at `tryFell`.
`false` when:
- service disabled, or `event`/`options` null;
- event already cancelled;
- managed break with no pending (Iris probe / already finalizing path);
- `canUse` failed — standalone needs enabled + permission; override never fails this;
- break is not a fellable candidate;
- candidate resolve throws (logged).
### Candidate checks (no bypass)
`INTEGRATION_OVERRIDE` skips enabled switch and permission only. Still required:
- `GameMode.SURVIVAL`
- player sneaking
- broken block tagged `Tag.LOGS`
- main-hand item is an axe (`*_AXE` material name)
- Iris tree provenance in mantle: placed by an Iris tree, not replaced since, not structure-aware
Vanilla saplings and hand-placed logs never fell. Provenance clears when a block is broken or built over.
---
## How a run comes apart
Discovery walks mantle provenance outward from the broken block in 26 directions, BFS. Members remove in discovery order (trigger first; ties Y then X then Z).
Bounds — any hit marks discovery incomplete; Iris falls back to **only the trigger block**:
| Bound | Value |
|---|---|
| Members collected | 131 072 |
| Positions visited | 1 000 000 |
| Distance from trigger on any axis | 256 blocks |
Removal is paced in batches with tick yields. Batch size scales with tree size.
Run ends immediately (no further hooks) when the player:
- stops sneaking,
- changes held hotbar slot,
- swaps hands,
- goes offline, leaves survival, or changes world,
- breaks the axe (after that log's commit),
- or replaces the axe in that slot with a different item.
Each removed block fires a `BlockBreakEvent` with `isManagedBreak == true`. Cancelled **log** probes refund that log's reservation and end the run; cancelled **leaf** probes continue. Drops use the axe as it was before that block's durability charge (Silk Touch / Fortune apply). The original break is cancelled with drops and XP suppressed; Iris delivers per block.
---
## Options
```java
public record TreeFellerOptions(
TreeFellerAccess access,
int durabilityPreservationChance,
TreeFellerRunHooks runHooks) {
public static TreeFellerOptions standalone();
public static TreeFellerOptions integrationOverride(
int durabilityPreservationChance,
TreeFellerRunHooks runHooks);
}
```
Canonical constructor: null `access`/`runHooks``NullPointerException`; chance outside `0..100``IllegalArgumentException`.
`durabilityPreservationChance` is percent chance a log costs **no** durability. Rolled per log.
**Honoured only for `INTEGRATION_OVERRIDE`.** Standalone uses `treeFeller.durabilityPreservationChance` from settings; `standalone()` hard-codes `0` in the record for that reason.
```java
public interface TreeFellerRunHooks {
TreeFellerRunHooks NONE;
void onActivationAccepted();
boolean reserveLogCost();
void commitLogCost();
void refundLogCost();
}
```
Iris never calls `equals`/`hashCode`/`toString` on hooks.
---
## Failure policy
| Misbehaviour | Behaviour |
|---|---|
| `onActivationAccepted` throws | Logged; **run continues** (notification, not veto) |
| `reserveLogCost` throws | Logged as false; run ends; nothing refunded |
| `reserveLogCost` returns false | Run ends cleanly |
| `commitLogCost` throws | Logged; run ends; **block already gone** |
| `refundLogCost` throws | Logged; run ends |
| Hook blocks a long time | Nothing — no timeout |
| null event or options | `tryFell` → false |
| Two overrides for one break | First stored override's hooks stay; later override returns true without replacing |
| Candidate resolve throws | Logged; false |
| `isTreeBlock` throws | Logged; false |
| Iris disabled mid-run | Active runs finish immediately; **no refund for outstanding reserves** |
No integration quarantine.
### Missed refund
Refunds schedule onto the feller's entity path. If scheduling fails (logout/removal) or shutdown ends runs, **`refundLogCost` may not run**. Exposure is at most one log cost per run in the reserve→resolve window. For strict accounting, accumulate in your own state and reconcile on quit/`onDisable`.
---
## Configuration
`plugins/Iris/settings.json`:
| Key | Default | Meaning |
|---|---|---|
| `treeFeller.enabled` | `false` | Standalone path only. Override ignores it |
| `treeFeller.durabilityPreservationChance` | `0` | Standalone durability preserve %; clamped `0..100` on read |
| Permission | Default | Meaning |
|---|---|---|
| `iris.treefeller` | `op` | Standalone only. Override does not check it |
Related operator surface: `28 - Integrations.md`, `04 - Commands & Permissions.md`.
---
## Enum reference
### `TreeFellerAccess`
| Constant | Enabled switch | `iris.treefeller` | Durability chance source |
|---|---|---|---|
| `STANDALONE` | Required | Required | Settings; value in options ignored |
| `INTEGRATION_OVERRIDE` | Bypassed | Bypassed | Value in options |
Neither mode bypasses survival, sneak, axe, log tag, or mantle provenance.
Default arms: `90 - API - Getting Started.md`.
+349
View File
@@ -0,0 +1,349 @@
# 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.
Assumes Minecraft 26.2, Java 25, and Fabric, Forge, or NeoForge. Mod id is `irisworldgen` on all three.
| Goal | Entry |
|---|---|
| Detect Iris, read engine token, start pregen, mantle R/W | `IrisModdedAPI` |
| Have Iris place your blocks/items/mobs | `ModdedDataProvider` |
| Alias one custom key onto a fixed vanilla state | `IrisModdedAPI.registerCustomBlockData` |
---
## Depending on Iris
**No published Maven artifact for the mod jars.** Root build has no `maven-publish` for them; JitPack for this repo resolves Bukkit-oriented sources, not the modded adapters. Build from source:
```bash
./gradlew buildFabric # -> dist/Iris v<version> [Fabric] <mc>+<loader>.jar
./gradlew buildForge # -> dist/Iris v<version> [Forge] <mc>+<loader>.jar
./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:
```gradle
dependencies {
compileOnly(files('libs/Iris-fabric.jar'))
}
```
Adapters are not in root `settings.gradle` by default. `-PincludeModdedAdapters=true` is for IDE import only and can close a composite-build cycle Gradle rejects.
### Soft dependency
Fabric (`fabric.mod.json`) — `suggests`, not hard `depends`:
```json
{
"suggests": { "irisworldgen": "*" }
}
```
NeoForge (`META-INF/neoforge.mods.toml`):
```toml
[[dependencies.yourmod]]
modId = "irisworldgen"
type = "optional"
ordering = "AFTER"
side = "BOTH"
```
Forge (`META-INF/mods.toml`): same with `mandatory = false` instead of `type`.
Do not rely on load order for ServiceLoader discovery — Iris discovers providers itself.
### Detecting Iris
| Question | How |
|---|---|
| Is the mod present? | `FabricLoader.getInstance().isModLoaded("irisworldgen")` or `ModList.get().isLoaded("irisworldgen")` |
| Are Iris classes loadable? | Keep imports behind a presence check / separate class, or `Class.forName("art.arcane.iris.modded.api.IrisModdedAPI")` |
| Is **this** level Iris? | `IrisModdedAPI.isIrisLevel(level)` |
Do not gate on version strings. Probe for the class or method you need. Servers mix Iris and vanilla dimensions freely.
```java
private static final boolean IRIS_PRESENT = irisPresent();
private static boolean irisPresent() {
try {
Class.forName("art.arcane.iris.modded.api.IrisModdedAPI");
return true;
} catch (Throwable absent) {
return false;
}
}
```
---
## `IrisModdedAPI`
All static, null-tolerant: null or non-Iris `ServerLevel``false`, `null`, or no-op.
| Method | Behaviour |
|---|---|
| `isIrisLevel(ServerLevel)` | Chunk generator is `IrisModdedChunkGenerator`. Cheapest check |
| `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 |
| `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 |
| `registerProvider(ModdedDataProvider)` | Imperative provider registration |
| `registerCustomBlockData(String namespace, String key, String state)` | Static block-state alias |
### Threading (API entry)
| Method | Thread guidance |
|---|---|
| `isIrisLevel`, `isStudioLevel`, `getEngine` | Safe from any thread once the level is loaded |
| 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 |
### `Engine` is internal
`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.
### 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)`.
Operator pregen concepts: `07 - Pregeneration.md`. Platform matrix: `30 - Platform Differences.md`.
### Mantle data
Iris 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.**
```java
IrisModdedAPI.retainMantleDataForSlice(MyMarker.class);
```
Registration is by canonical class name, process-wide, irreversible. All three mantle accessors throw `IllegalStateException` if the engine mantle is already closed.
---
## `ModdedDataProvider`
```java
public interface ModdedDataProvider {
String modId();
default boolean isReady(); // default true
Collection<Identifier> getTypes(ModdedDataType type);
boolean isValidProvider(Identifier id, ModdedDataType type);
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state); // default null
default void processBlockPlacement(ModdedBlockPlacementContext context); // default no-op
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId); // default null
default void init(); // default no-op
}
```
`ModdedDataType`: `BLOCK`, `ITEM`, `ENTITY`. May gain constants — use `default` in switch expressions.
### Contract
- `modId()` — identity for de-duplication and logging. Non-null and stable; null aborts discovery.
- `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.
- `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.
`ModdedBlockData`: `direct(BlockState)` / `deferred(BlockState)`; null state rejected.
### Provider threading
| Callback | Thread | Notes |
|---|---|---|
| `isValidProvider`, `getBlockData` | Generation threads, concurrent | Fast; no world mutation |
| `processBlockPlacement` | Server thread, chunk loaded | Safe block/entity writes |
| `spawnMob` | Server thread | |
| `init` | Discovering/registering thread | No server/level assumed |
| `modId`, `isReady`, `getTypes` | Any | |
### ServiceLoader registration
`META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider` — binary names, one per line. Nested classes use `$`. Public no-arg constructor required.
Iris loads with **its** class loader: `ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader())`. If the registration log never appears, fall back to `registerProvider`.
### 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.
- ServiceLoader providers are available before any world resolves blocks.
- Discovery once per process; second `discover()` is no-op.
- `init()` must not assume a server, level, or full game registry. Gate late work with `isReady()`.
### Imperative registration
`IrisModdedAPI.registerProvider(provider)` any time during setup. Providers registered after Iris's ServiceLoader pass are fine; after a world has generated, already-resolved blocks are not revisited. Duplicate `modId()` logged and ignored. `init()` runs during the call; throwables from imperative `init` are logged (provider stays registered).
### Logging
Under the `Iris` logger:
```
Iris registered custom content provider 'yourmod'
```
Duplicates: `already registered; ignoring duplicate`.
Per-callback failures are caught, logged against `modId()`, and generation continues:
```
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 to initialize
```
ServiceLoader `init` failure is all-or-nothing: aborts discovery, rolls back that pass, rethrows:
```
Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider)
```
### Static aliases
```java
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.
Go through `IrisModdedAPI`, not `ModdedCustomContentRegistry` resolution methods (public only for Iris internal packages).
---
## Pack paths and forced datapack (modded)
Paths relative to the loader config directory (`config/`):
| Path | Role |
|---|---|
| `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 |
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).
Async default-pack prefetch at boot when `autoDownloadDefaultPack` is set and `defaultPack` is missing (`IrisDimensions/<pack>` from `master`). Failures log a pointer to `/iris download <pack>`.
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:
```
Iris forced datapack 'iris_worldgen' was never loaded by this server.
```
That is a loader/environment problem, not a pack problem.
### Commands (modded)
`/iris` (`/ir`, `/irs`), gamemaster level. Full tree: `04 - Commands & Permissions.md`. Notable:
| Command | Role |
|---|---|
| `/iris pack validate [pack]` | Validate packs on a worker |
| `/iris pack status [pack]` | Recorded validation results |
| `/iris pack cleanup` / `restore` | 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 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`.
---
## Native worldgen passthrough over Iris terrain
Iris replaces the chunk generator. Vanilla/mod worldgen runs only if Iris runs it:
| System | Over Iris? | Notes |
|---|---|---|
| Structures (vanilla, datapack, mod) | **Yes**, on by default | Vertical fit, stilts, vegetation clear. Deny: `importedStructures.disabled` |
| Placed features (ores, trees, plants, …) | **Yes**, **off** by default | Dimension `importedFeatures.enabled` |
| Carvers | **Never** | No noise router / 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 |
### `importedFeatures`
Disabled by default. Absent or `enabled: false` → no feature table; terrain matches long-standing Iris-only output. Biome tags of the vanilla derivative are always inherited on custom biomes (not gated on this flag). Structure tags `#minecraft:has_structure/*` are **not** inherited.
```json
{
"importedFeatures": {
"enabled": true,
"steps": ["UNDERGROUND_ORES"],
"disabledSteps": ["VEGETAL_DECORATION"],
"disabled": ["minecraft:ore_diamond", "minecraft:trees"]
}
}
```
| Field | Meaning |
|---|---|
| `enabled` | Master switch; default `false` |
| `steps` | Allow-list of decoration steps; empty = all |
| `disabledSteps` | Deny-list after `steps` |
| `disabled` | Placed-feature key deny-list; prefix match on namespace/`/`/`_` boundaries |
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.
### Carvers
`applyCarvers` is empty by design. Use pack `caves` / `carvings`: `15 - Caves & Carving.md`.
### 26.2 note: `pointed_dripstone` / `speleothem`
| Registry | 26.2 key |
|---|---|
| Block | `minecraft:pointed_dripstone` unchanged |
| Placed / configured feature | `minecraft:pointed_dripstone` unchanged |
| Feature **type** | `minecraft:speleothem` renamed |
Pack palette / object / `importedFeatures.disabled` keys using the block or placed-feature id remain correct.
### Biome tags
Custom biomes inherit derivative tag membership plus pack `tags`. Files use `"replace": false`. Details: `13 - Biomes.md`.
---
## What is not supported
- **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 PlaceholderAPI**`09 - PlaceholderAPI.md` is Bukkit-only.
- **Datapack ingest commands** Bukkit-only.
- **Vanilla carvers and surface rules never run.**
- **`ModdedCustomContentRegistry` resolution APIs** are Iris internals; use `IrisModdedAPI`.
Install and platform notes: `01 - Installation & Platforms.md`, `30 - Platform Differences.md`.
-199
View File
@@ -1,199 +0,0 @@
# 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).
Writing a **mod** rather than a plugin? The Fabric, Forge and NeoForge jars carry a different surface,
`art.arcane.iris.modded.api`: [modded.md](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 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.
The mod jars carry a separate surface instead: `art.arcane.iris.modded.api`, documented in
[modded.md](modded.md). It is where a mod detects Iris levels, drives pregeneration, reads and writes
mantle data, and registers a provider so an Iris pack can place the mod's own blocks, items and mobs.
It 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 -> "";
};
```
-530
View File
@@ -1,530 +0,0 @@
# Iris on Fabric, Forge and NeoForge
`art.arcane.iris.modded.api` is the surface a **mod** compiles against. It answers three questions: is this
level generated by Iris, how do I drive Iris from my mod, and how do I get my own blocks, items and mobs
placed by an Iris pack.
It ships in the Fabric, Forge and NeoForge jars only. It is absent from the Bukkit plugin jar, shares no types
with `art.arcane.iris.api`, and is not covered by [terrain.md](terrain.md), [world-events.md](world-events.md)
or [tree-feller.md](tree-feller.md) - those describe the Bukkit surface, which does not exist on a mod loader.
Everything here assumes Minecraft 26.2, Java 25, and one of Fabric, Forge or NeoForge. The mod id is
`irisworldgen` on all three.
| What you want | Where |
|---|---|
| Detect Iris, read the engine, start a pregeneration, read/write mantle data | `IrisModdedAPI` |
| Have Iris place *your* blocks, items and mobs | `ModdedDataProvider` |
| Alias one custom key onto a fixed vanilla state, with no provider class | `IrisModdedAPI.registerCustomBlockData` |
---
## Depending on Iris
**There is no published Maven artifact for the mod jars.** No module in this repository applies
`maven-publish`, and the JitPack route documented in [README.md](README.md) resolves the Bukkit sources, not
the modded adapter. Until that changes, building from source is the only path.
The three adapters are standalone Gradle builds - each `adapters/<loader>/settings.gradle` does
`includeBuild('../..')` to substitute `art.arcane:core` and `art.arcane:spi` from the root build, which is what
keeps Loom, ForgeGradle and ModDevGradle off one plugin classpath. The root build drives them through their own
wrappers:
```bash
./gradlew buildFabric # -> dist/Iris v<version> [Fabric] <mc>+<loader>.jar
./gradlew buildForge # -> dist/Iris v<version> [Forge] <mc>+<loader>.jar
./gradlew buildNeoforge # -> dist/Iris v<version> [NeoForge] <mc>+<loader>.jar
```
Then compile against the jar you will actually run:
```gradle
dependencies {
compileOnly(files('libs/Iris-fabric.jar'))
}
```
The adapters are **not** in the root `settings.gradle` by default. Add `-PincludeModdedAdapters=true` for IDE
import only; it closes a composite build cycle (root -> adapter -> root) and Gradle may reject it.
### Soft dependency
Declare the optional relationship, then do not rely on load order - the ServiceLoader path below works
regardless of it, because Iris does the discovering.
Fabric (`fabric.mod.json`) - `suggests`, not `depends`; a hard `depends` makes Iris mandatory:
```json
{
"suggests": { "irisworldgen": "*" }
}
```
NeoForge (`META-INF/neoforge.mods.toml`):
```toml
[[dependencies.yourmod]]
modId = "irisworldgen"
type = "optional"
ordering = "AFTER"
side = "BOTH"
```
Forge (`META-INF/mods.toml`) is the same with `mandatory = false` instead of `type`.
### Detecting Iris
Two checks, and they answer different questions.
**Is the mod present?** Ask the loader - `FabricLoader.getInstance().isModLoaded("irisworldgen")`, or
`ModList.get().isLoaded("irisworldgen")` on Forge and NeoForge. Cheap, but tells you nothing about whether
Iris actually generates anything.
**Are Iris classes on the classpath?** If your integration lives in a class that imports
`art.arcane.iris.modded.api.*`, that class must not load unless Iris is present. Keep the imports behind a
presence check and a separate class, or probe reflectively:
```java
private static final boolean IRIS_PRESENT = irisPresent();
private static boolean irisPresent() {
try {
Class.forName("art.arcane.iris.modded.api.IrisModdedAPI");
return true;
} catch (Throwable absent) {
return false;
}
}
```
Do not gate on a version string. Probe for the class or method you need.
**Is *this* level Iris?** `IrisModdedAPI.isIrisLevel(level)`. A server can mix Iris and vanilla dimensions
freely, so presence of the mod is not presence of an Iris world.
---
## `IrisModdedAPI`
All static, all null-tolerant: a null or non-Iris `ServerLevel` yields `false`, `null` or a no-op. You never
have to pre-check.
| Method | What it does |
|---|---|
| `isIrisLevel(ServerLevel)` | Whether the level's chunk generator is `IrisModdedChunkGenerator`. The cheapest check |
| `isStudioLevel(ServerLevel)` | Whether it is a throwaway pack-authoring world. Persist nothing against one |
| `getEngine(ServerLevel)` | The `Engine` behind the level, or null. See the stability warning below |
| `pregenerate(ServerLevel, int radiusBlocks)` | Starts a cached async pregeneration around the origin |
| `pregenerate(ServerLevel, int, int centerX, int centerZ, boolean sync, boolean cached)` | Same, with a centre and write mode |
| `getMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Reads Iris's per-block sidecar storage |
| `setMantleData(ServerLevel, int x, int y, int z, T)` | Writes it |
| `deleteMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Removes a value of that type |
| `retainMantleDataForSlice(Class<?>)` | Declares a mantle type Iris must keep rather than discard |
| `registerProvider(ModdedDataProvider)` | Registers a custom content provider imperatively |
| `registerCustomBlockData(String namespace, String key, String state)` | Aliases one key onto a fixed block state |
### `Engine` is internal
`getEngine` returns `art.arcane.iris.engine.framework.Engine`. That type is **internal to Iris** and changes
without a deprecation cycle - as do `art.arcane.iris.core.*`, `art.arcane.iris.util.*` and
`art.arcane.iris.spi.*`. Treat the returned `Engine` as an opaque token to hand back to Iris. Every method in
the table above that needs one resolves it for you; prefer those.
Never cache an `Engine`. A pack hotload or a level unload replaces it, and the old instance goes inert.
`getEngine` already returns null while the generator is binding and during shutdown.
### Pregeneration
`pregenerate` returns as soon as the job is queued. Progress goes to Iris's own logging and boss bar, not to
your caller. Only one job runs server-wide, so it returns `false` if one is already active - and `false` also
means "not an Iris level", so check `isIrisLevel` first if you need to tell those apart. Call it on the server
thread.
`cached = true` writes an on-disk pregeneration cache so an interrupted run resumes instead of regenerating.
`sync = true` writes chunks synchronously: slower, but it bypasses the async write queue.
### Mantle data
The mantle is Iris's own per-block storage, independent of chunk NBT, and it is how Iris carries data between
generation stages. Three things to know:
1. **Coordinates are world-space.** `y` is translated by the engine's minimum height internally. A `y` outside
the engine's height range reads as null and writes as a no-op - no exception.
2. **Reads never create storage; writes do.** `getMantleData` returns null when no mantle region exists for
that column yet. `setMantleData` and `deleteMantleData` create the region, which can touch disk - do not
call them per block in a tick loop on the server thread.
3. **Declare your types or lose them.** Iris discards mantle slices it does not need once a region's
generation data has served its purpose. Any type you write and expect to read back later must be declared
once during mod setup:
```java
IrisModdedAPI.retainMantleDataForSlice(MyMarker.class);
```
Registration is by canonical class name, process-wide across every Iris world, and cannot be undone. All three
mantle methods throw `IllegalStateException` if the engine's mantle has already been closed.
---
## `ModdedDataProvider`
The extension point. Implement it and a pack can name `yourmod:something` in a block palette, a loot table or a
spawn entry, and Iris will ask you to resolve it.
```java
public interface ModdedDataProvider {
String modId();
default boolean isReady(); // default true
Collection<Identifier> getTypes(ModdedDataType type);
boolean isValidProvider(Identifier id, ModdedDataType type);
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state); // default null
default void processBlockPlacement(ModdedBlockPlacementContext context); // default no-op
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId); // default null
default void init(); // default no-op
}
```
`ModdedDataType` is `BLOCK`, `ITEM` or `ENTITY`. Constants may be added - write a `default` arm in any switch
expression over it.
### The contract
`modId()` is your identity, not decoration. It de-duplicates registrations and labels every log line Iris emits
about your provider. It must be non-null and stable; returning null aborts discovery.
`isValidProvider(id, type)` is the gate. Iris calls it before every resolution callback, on generation threads,
for every key it could not resolve itself. Keep it to a namespace comparison or a set lookup.
`isReady()` is how a provider whose registries populate late excuses itself. Iris **skips** a provider that
returns false rather than treating it as absent, so returning false is strictly better than returning wrong
answers.
`getTypes(type)` feeds command suggestion and pack tooling. It is not the resolution path - return an empty
collection rather than null, and do not do work here that `isValidProvider` should do.
`getBlockData(blockId, state)` resolves a claimed block. `state` holds the `[prop=value]` pairs from the pack's
key, already parsed, possibly empty, never null. Return null to decline and Iris tries the next provider, then
falls back to air. Return `ModdedBlockData.direct(blockState)` when the state is final.
`processBlockPlacement(context)` finishes a **deferred** placement. Return
`ModdedBlockData.deferred(placeholder)` from `getBlockData` when the real block needs a loaded level - a block
entity, neighbour state, or mod registries not reachable from a generation thread. Iris writes your placeholder
during generation and calls you back later on the server thread with the chunk loaded. Pick a placeholder with
the same shape and occlusion as the final block so terrain around it generates correctly. Only the *first*
provider claiming the identifier is called for a given position.
`ModdedBlockPlacementContext` is an immutable record: `engine`, `level`, `position`, `blockId`, `state`,
`blockState`. `blockState` is what is currently at `position` - normally your placeholder, though a later
generation stage may have replaced it. `state` is defensively copied and unmodifiable.
`spawnMob(...)` spawns a claimed custom entity on the server thread. Return null to decline.
`init()` runs once, immediately after Iris accepts your provider.
### Threading
| Callback | Thread | Notes |
|---|---|---|
| `isValidProvider`, `getBlockData` | Generation threads, many at once | Must be fast. Must not touch world state |
| `processBlockPlacement` | Server thread, chunk loaded | Safe to write blocks and attach block entities |
| `spawnMob` | Server thread | |
| `init` | Whichever thread registered you | Mod init for ServiceLoader, your caller otherwise |
| `modId`, `isReady`, `getTypes` | Any | |
Implementations must be thread-safe. `getBlockData` in particular is called concurrently for every unresolved
key a pack names, which during a pregeneration is a lot.
### Registration by ServiceLoader
Iris discovers providers with `java.util.ServiceLoader`. Ship a service file naming your implementation's
binary name; the class needs a public no-argument constructor.
`src/main/resources/META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider`:
```
com.example.yourmod.iris.YourIrisProvider
```
One binary name per line. Nested classes use `$`, for example
`com.example.yourmod.YourMod$IrisProvider`.
Iris loads the service with **its own** class loader:
`ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader())`. Your service
file therefore has to be visible from Iris's loader, which is the normal case on all three loaders but is the
thing to suspect first if nothing happens. Confirm with the registration log line below; if it never appears,
fall back to `registerProvider`.
### When discovery runs
`ModdedCustomContentRegistry.discover()` runs inside `ModdedEngineBootstrap.bootCommon(...)`, which is the very
first thing each loader's entrypoint calls - `IrisFabricBootstrap.onInitialize`,
`IrisForgeBootstrap`/`IrisNeoForgeBootstrap` construction. That is **before** the Iris chunk generator is
registered and long before any server starts. Consequences:
- A ServiceLoader-declared provider is available before any world could resolve a block. This is the safe path.
- Discovery runs **once per process**. A second `discover()` is a no-op.
- Your `init()` must not assume a server, a level, or a fully populated game registry. Defer that work and gate
it behind `isReady()`.
### Registration imperatively
`IrisModdedAPI.registerProvider(provider)` works at any time and is the option if you would rather not ship a
service file, or need to build the provider from your own config.
Ordering is the catch: Iris only consults providers registered **before** a pack resolves the block in
question, and blocks already resolved are not revisited. Register during mod setup. Registering after Iris's
own ServiceLoader pass is fine; registering after a world has generated is not.
A second registration under a `modId()` already present is logged and ignored. `init()` runs during the call.
### How discovery and failures are reported
Everything below is logged under the `Iris` logger. One line per accepted provider confirms registration -
this is what to grep for when checking whether your service file was seen:
```
Iris registered custom content provider 'yourmod'
```
A duplicate `modId()` is rejected with `already registered; ignoring duplicate`.
Iris catches throwables from every provider callback, logs them against your `modId()`, and continues with the
remaining providers - one broken provider does not stop world generation:
```
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 to initialize # registerProvider path only
```
`init()` during ServiceLoader discovery is the one exception, and it is all-or-nothing: a throwable there
aborts the pass, restores the registry to its pre-discovery state, and rethrows. The log line names the
provider that failed, by mod id and class name:
```
Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider)
```
If your provider's own `modId()` throws while Iris is building that message, the class name alone is logged; if
the failure happened outside any provider, it reads `the provider service loader`. A `null` provider or a null
`modId()` from the ServiceLoader fails the pass with a message naming which.
### Static aliases, no provider class
For the common case of "my key is really this vanilla block", skip the provider:
```java
IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]");
```
The state string uses the same syntax packs use and is parsed **immediately** - a typo is logged at startup and
the registration dropped, rather than surfacing later as missing blocks. Aliases take precedence over provider
lookups for the same key. Null arguments are ignored.
---
## How pack resolution works on modded
Paths are relative to the loader's config directory (`config/` on a normal server install).
| Path | What it is |
|---|---|
| `config/irisworldgen/packs/<pack>/` | Installed packs. A pack is valid when `dimensions/<dimension>.json` exists |
| `config/irisworldgen/generated/datapack/iris/` | The generated forced datapack. Iris owns this - do not edit it |
| `config/irisworldgen/modded.json` | Mod-side config: default pack, auto-download, primary world routing |
| `config/iris/` | Engine data directory: settings and per-world engine state |
Note the split: the engine's data folder is `config/iris`, but every modded pack path - installer, validator,
command suggestions, forced datapack, engine creation - resolves under `config/irisworldgen/packs`. Install
packs there.
At `bootCommon`, Iris kicks off an async default-pack prefetch. If `modded.json` has
`autoDownloadDefaultPack` enabled and the configured `defaultPack` is missing, Iris downloads
`IrisDimensions/<pack>` from the `master` branch into the packs folder. If that fails it logs a pointer to
`/iris download <pack>`. A pack that is already installed is left alone.
When a level asks for its pack, `ModdedWorldEngines.packFolder(pack)` resolves
`config/irisworldgen/packs/<pack>`. A missing pack is a hard failure with the expected absolute path printed -
Iris does not silently generate vanilla terrain in its place.
### The forced datapack
Iris cannot register dimension types and per-pack biomes through a mod registry, because vanilla world creation
reads them from the datapack layer. So Iris **generates a datapack** from the installed packs and injects it as
a built-in, top-priority repository source:
- Fabric: a `PackRepository` mixin, via `FabricForcedDatapackSources`
- Forge and NeoForge: `event.addRepositorySource(ModdedForcedDatapack.repositorySource())`
It contributes world presets, dimension types and biomes under the `irisworldgen` namespace, with ids derived
from the pack and dimension names - `irisworldgen:packs/<pack>/dimensions/<dimension>/preset`,
`.../dimension_type`, `.../biomes/<biome>`. This is why an Iris dimension shows up in the vanilla world
creation screen as `IRIS:<Pack>`.
It is regenerated when the pack changes: a studio hotload calls `ModdedForcedDatapack.regenerate()`. If
regeneration fails and a previously published pack is still readable, Iris keeps the last known-good one and
logs the failure rather than starting with no dimension types.
**The failure you need to recognise.** If injection did not happen for your loader - a mixin that failed to
apply, an event that never fired - Iris logs this once at startup and world creation will fail no matter how
many times you restart:
```
Iris forced datapack 'iris_worldgen' was never loaded by this server.
N installed pack(s) at <path> contributed no dimension types or custom biomes.
Datapack source injection failed for this loader (mixin/event not applied), so world
creation will fail and restarting will not fix it.
```
That is a loader/environment problem, not a pack problem. Nothing an integrating mod does can fix it.
### Commands worth knowing
The modded command tree is `/iris`, aliased `/ir` and `/irs`, gated at gamemaster permission level.
| Command | What it tells you |
|---|---|
| `/iris pack validate [pack]` | Validates every installed pack, or one. Runs on a worker thread, reports per pack, and counts unloadable packs |
| `/iris pack status [pack]` | Replays the **recorded** validation results - blocking errors and warnings per pack. Says so and returns nothing if `validate` has not run this session |
| `/iris pack cleanup <pack> [apply]` | Previews unused pack resources; `apply` deletes them |
| `/iris pack restore <pack> [apply]` | Previews a restore of pack resources; `apply` performs it |
| `/iris datapack status` | Per Iris dimension: active dimension type, its min/max/logical height, what the pack wants, and whether they match |
| `/iris datapack install` | Writes the pack's dimension type into `<world>/datapacks/iris/data/irisworldgen/dimension_type/` as an override |
| `/iris datapack list` | Datapack URLs declared by installed pack dimensions, plus the datapacks actually present in `<world>/datapacks/` |
| `/iris download <pack> [branch] [overwrite]` | Installs a pack from `IrisDimensions/<pack>` into the packs folder. Aliased `dl` |
| `/iris version` | Iris version and loader |
`/iris datapack status` is the first thing to run when an Iris dimension generates at the wrong height. A
mismatch means the level's active dimension type disagrees with the pack, which happens when a world was
created before a pack's height range changed. `install` writes the override; the world still needs a restart.
`/iris datapack ingest`, `pull` and `remove` exist but refuse on modded, with a message explaining why: the
Modrinth ingest workflow is Bukkit tooling. Native vanilla and datapack structure placement **does** work on
modded - install the datapack into `<world>/datapacks/` and restart, and its registered structures generate.
---
## Native worldgen passthrough: what generates over Iris terrain
Iris replaces the chunk generator outright. Every piece of vanilla or mod worldgen therefore only runs if Iris
runs it. This is the honest matrix.
| Vanilla / mod worldgen | Runs over Iris terrain? | How |
|---|---|---|
| Structures (vanilla, datapack, mod) | **Yes**, on by default | Iris runs its own structure pass with vertical fitting, foundation stilts and vegetation clearing. Deny with `importedStructures.disabled` |
| Placed features: ores, trees, plants, springs, geodes (vanilla, datapack, mod) | **Yes**, off by default | Set `importedFeatures.enabled` on the dimension |
| Carvers (caves, canyons, mod carvers) | **Never** | Architectural. See below |
| Mod biomes | Only as a `derivative` / `vanillaDerivative` / `biomeScatter` target | Iris chooses biomes from the pack, not from a biome source |
| Mob spawning, including mod mobs | **Yes** | Iris merges the biome's own spawn table with the vanilla derivative's |
| Surface builders / surface rules | **Never** | Iris generates its own surface from the pack palettes |
### `importedFeatures`
A dimension-level control block, disabled by default. With it absent or `enabled: false` chunk output is
byte-for-byte what Iris has always produced, and no feature table is built.
Biome tags are not part of that guarantee, and are not gated on this flag at all: Iris custom biomes inherit
the biome tags of their vanilla derivative on every platform, so the emitted datapack tag files differ from
older Iris builds regardless of `importedFeatures`. Anything driven by biome tags - mob variants, spawn rules,
mod content selecting on `#minecraft:is_overworld` and friends - therefore applies to Iris custom biomes.
```json
{
"importedFeatures": {
"enabled": true,
"steps": ["UNDERGROUND_ORES"],
"disabledSteps": ["VEGETAL_DECORATION"],
"disabled": ["minecraft:ore_diamond", "minecraft:trees"]
}
}
```
| Field | Meaning |
|---|---|
| `enabled` | Master switch. Default `false` |
| `steps` | Allow-list of decoration steps. Empty (default) means every step |
| `disabledSteps` | Deny-list of decoration steps, applied after `steps` |
| `disabled` | Placed-feature key deny-list. A `namespace:path` prefix matches on namespace, slash and underscore boundaries, so `minecraft:ore` denies every vanilla ore |
Steps are the vanilla ones, in order: `RAW_GENERATION`, `LAKES`, `LOCAL_MODIFICATIONS`,
`UNDERGROUND_STRUCTURES`, `SURFACE_STRUCTURES`, `STRONGHOLDS`, `UNDERGROUND_ORES`, `UNDERGROUND_DECORATION`,
`FLUID_SPRINGS`, `VEGETAL_DECORATION`, `TOP_LAYER_MODIFICATION`. Ores live in `UNDERGROUND_ORES`; trees, grass
and flowers in `VEGETAL_DECORATION`.
What you get, and what it costs:
- Features are read from the biome's **vanilla derivative**. An Iris custom biome declares no features of its
own by design (its generated datapack JSON has empty `features` and `carvers` arrays); passthrough comes from
the chunk generator's generation-settings getter, which maps the custom biome onto the derivative.
- Iris terrain is not vanilla terrain. A feature that assumes a vanilla surface can land oddly - floating
sugar cane, ore veins in unexpected rock, trees on a slope Iris carved. Turn it on per dimension and look
before shipping.
- Feature seeds are derived exactly as vanilla derives them, so the same pack plus the same registries places
the same features. Denying one feature never shifts another: each takes its seed from its own global index.
- The pass runs on the worldgen thread that owns the chunk, never on the Iris generation pool. The vanilla
FEATURES step writes into neighbouring chunks and is not parallel-safe.
- The whole feature pass runs after Iris has placed its structures, not interleaved per step the way vanilla
orders them. An early-step feature - `RAW_GENERATION`, `LAKES`, `LOCAL_MODIFICATIONS` - therefore sees placed
structures and can cut into one, so a lake can open into a structure vanilla would have flooded before
placing it.
- Both platforms behave identically: the Bukkit plugin has the same control with the same semantics.
**Feature order cycles.** Vanilla topologically sorts every placed feature across every biome. Content that
declares mutually inconsistent orderings makes that sort fail with `Feature order cycle found`. Iris builds the
table at bind and catches that failure: `importedFeatures` degrades to off for the dimension and Iris logs an
ERROR naming the involved sources. It never becomes a chunk-generation crash.
### Why carvers can never be imported
A carver runs against `NoiseGeneratorSettings` - the noise router, aquifer state and surface rules of a
`NoiseBasedChunkGenerator`. Iris has none of those; its terrain comes from the pack's own generators and its
caves from the Iris carving system. There is nothing for a vanilla carver to sample, so `applyCarvers` is empty
by design and there is no flag to change that. Use Iris `caves` and `carvings` in the pack instead.
### 26.2 pack-content note: `pointed_dripstone` and `speleothem`
26.2 renamed the *feature type* `minecraft:pointed_dripstone` to `minecraft:speleothem`, and
`minecraft:dripstone_cluster` to `minecraft:speleothem_cluster`. Verified against the 26.2 built-in data:
| Registry | 26.2 key |
|---|---|
| Block (`minecraft:block`) | `minecraft:pointed_dripstone` - **unchanged** |
| Placed feature (`minecraft:worldgen/placed_feature`) | `minecraft:pointed_dripstone` - **unchanged** |
| Configured feature (`minecraft:worldgen/configured_feature`) | `minecraft:pointed_dripstone` - **unchanged** |
| Feature type (`minecraft:worldgen/feature`) | `minecraft:speleothem` - **renamed** |
So a pack that lists `minecraft:pointed_dripstone` in a palette, an object, or an
`importedFeatures.disabled` entry is still correct - those are block and placed-feature keys. Only content that
names the *feature type* directly, which is a datapack-authoring concern rather than an Iris pack concern, needs
updating.
### Biome tags
Generated Iris custom biomes inherit the **biome tag membership of their vanilla derivative**, on top of any
tags the pack declares in `tags`. That is what makes `#minecraft:is_overworld` and mod-authored tag selectors
resolve against Iris terrain; without it a custom biome sits in no tag at all. Structure tags
(`#minecraft:has_structure/*`) are deliberately **not** inherited - Iris resolves native structure placement
through the biome's structure derivative, so inheriting them would place a structure twice.
Tag files are written with `"replace": false`, so vanilla tags are extended, never replaced.
---
## What is not supported
- **No published artifact.** Build from source, as above. There is no Maven coordinate for the modded jars and
no `maven-publish` in this build.
- **Core types are internal.** `art.arcane.iris.engine.*`, `art.arcane.iris.core.*`,
`art.arcane.iris.util.*` and `art.arcane.iris.spi.*` change without notice. `Engine`, reachable through
`getEngine` and `ModdedBlockPlacementContext.engine()`, is the one internal type this surface exposes, and it
is exposed as a token to hand back rather than an API to call.
- **No event bus.** `IrisPlatform.callEvent` is a no-op on every mod loader adapter. There is no modded
equivalent of `IrisWorldEngineEvent` or `IrisPregenerationEvent`; poll `isIrisLevel`/`getEngine` instead.
- **No `art.arcane.iris.api`.** The Bukkit terrain, world-event, pregen and tree-feller interfaces are not in
the mod jars. There is no modded terrain-query surface yet.
- **No PlaceholderAPI.** [placeholders.md](placeholders.md) is Bukkit-only.
- **Datapack ingest is Bukkit-only**, per the command note above.
- **Vanilla carvers and surface rules never run.** See the passthrough matrix above.
- **`ModdedCustomContentRegistry`'s resolution methods are Iris internals.** They are public only because the
adapter's generation code lives in another package. Go through `IrisModdedAPI`.
-216
View File
@@ -1,216 +0,0 @@
# Iris placeholders
Iris registers the `iris` PlaceholderAPI expansion when PlaceholderAPI is enabled. It publishes
sixteen keys: seven in the world family, describing the generator around the reading player, and
nine in the pregeneration family, describing the server's running pregeneration job.
This is an operator-facing contract, not a compile surface. Nothing here needs a dependency, a
`softdepend`, or a line of Java. If you are writing a plugin rather than a scoreboard, the same data
is available with more precision through the [terrain API](terrain.md) and the
[pregeneration events](world-events.md).
**The pre-2.0 underscore keys are gone.** There is no alias and no dual-accept window. If you are
upgrading an existing board, go straight to the [migration table](#migration-from-the-pre-20-keys).
---
## The value grammar
Every key follows the same rules, so a board never has to special-case Iris:
- Paths are **dot-separated and lowercase** and never contain an underscore. Iris lowercases the
path before resolving it, so `%iris_WORLD.BIOME%` works, but write it lowercase.
- Values are **plain text**: no colour codes, no unit suffixes, no `%` character, `.` as the decimal
separator, no thousands grouping.
- Any section sign or `%` character that appears inside a pack-authored name — a biome display name,
a world name — is stripped before you see it, so a pack cannot inject formatting or a nested
placeholder into your board.
There are exactly three possible answers:
| Answer | When | What PlaceholderAPI shows |
|---|---|---|
| A value | The key is known and has data | The value |
| `---` | The key is known and has no data right now | `---` |
| Nothing | The key is not one Iris publishes | The literal `%iris_...%` |
The third row is deliberate. A typo stays visible on the board instead of quietly rendering as
blank, which is why there is no catch-all fallback.
A real zero is `0`, never `---`. `---` means "no reading", not "zero".
---
## World keys
| Placeholder | Value |
|---|---|
| `%iris_available%` | `true` when the Iris terrain service is live, `false` otherwise |
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
| `%iris_world.biome%` | Surface biome display name at the player, for example `Hot Desert Dunes` |
| `%iris_world.biome-key%` | Surface biome load key, for example `desert/hot-dunes` |
| `%iris_world.region%` | Region display name at the player |
| `%iris_world.region-key%` | Region load key |
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
`%iris_available%` is the only world-family key that does not need a player. It answers for the
console and for an offline player.
Every other `world.*` key needs a tracked online player. For the console, an offline player, or a
player Iris has no position for yet, `world.available` is `false` and the rest are `---`.
### They are surface readings, and they are cached
`world.biome`, `world.biome-key`, `world.region` and `world.region-key` describe the **surface** at
the player's block column — the biome and region the generator places at ground level. A player
standing in a cave under an overhang reads the biome of the sky above them, not the cave they are
in. That is what a board reader means by "what biome am I in".
The reading is rebuilt at most **once per second per player**, and only when something actually reads
one of these keys. Consequences:
- A whole board of `world.*` keys costs one rebuild per player per second, however many of them are
on it.
- A value can lag a sprinting player by up to a second.
- A board that nobody is looking at costs nothing.
Position tracking has two speeds. Walking republishes a player's column at most once per second, and
not at all while they stand still. Anything that is not walking — joining, respawning, changing
world, stepping through a portal, or **any** teleport including `/iris goto`, `/tp`, an ender pearl
and a random teleport — publishes immediately. A player who arrives somewhere and stops moving
therefore never keeps showing the biome of where they came from.
---
## Pregeneration keys
| Placeholder | Value |
|---|---|
| `%iris_pregen.available%` | `true` while a pregeneration job is running |
| `%iris_pregen.world%` | World name the running job is pregenerating |
| `%iris_pregen.percent%` | Completion, `0.00` to `100.00`, with no `%` character |
| `%iris_pregen.eta%` | Estimated seconds remaining, whole number |
| `%iris_pregen.eta-text%` | The same estimate as `45s`, `2m 5s` or `1h 30m` |
| `%iris_pregen.chunks%` | Chunks generated so far |
| `%iris_pregen.total%` | Chunks in the job |
| `%iris_pregen.chunks-per-second%` | Current rate, two decimal places |
| `%iris_pregen.paused%` | `true` while the job is paused |
`pregen.*` is **global**, not per player. Iris runs one pregeneration job per server, so these keys
read the same for everyone, including the console. `%iris_pregen.world%` says which world it is.
The snapshot is published when the job reports progress, once per second, and cleared the moment the
job completes or is cancelled. After that every `pregen.*` key except `pregen.available` reads `---`,
and `pregen.available` reads `false`. There is no lingering "last job" state to mistake for a running
one.
`pregen.eta` and `pregen.eta-text` are two renderings of the same estimate: use `eta` for arithmetic
and `eta-text` for display. Both read `0` and `0s` respectively before the job has generated enough
chunks to estimate from.
---
## Availability
The expansion is registered only if PlaceholderAPI is enabled when Iris starts. It sets `persist()`,
so it survives `/papi reload` without Iris restarting.
`%iris_available%` distinguishes "Iris is installed but its terrain service is not up" from "Iris is
not installed at all" — in the second case the expansion does not exist, no key resolves, and every
`%iris_...%` on the board renders literally. Gate a conditional board on `%iris_available%` if you
want it to disappear cleanly rather than show `---` rows on a server where Iris is present but still
starting.
Iris never gates a placeholder on a permission. A placeholder has no permission context to check
against — the player reading a scoreboard is not necessarily the player the value describes — so
values that should not be public are not published at all. That is why there is no seed key.
---
## Migration from the pre-2.0 keys
The old underscore keys are gone. There is no alias and no dual-accept window: an old key now
renders literally, so it is visible rather than silently wrong. This table is complete — every key
the old expansion published appears in it.
| Old key | New key | Why |
|---|---|---|
| `%iris_biome_name%` | `%iris_world.biome%` | Renamed onto the dot grammar |
| `%iris_biome_id%` | `%iris_world.biome-key%` | Renamed; `id` was always the load key |
| `%iris_region_name%` | `%iris_world.region%` | Renamed onto the dot grammar |
| `%iris_region_id%` | `%iris_world.region-key%` | Renamed; `id` was always the load key |
| `%iris_biome_file%` | removed | Rendered an absolute server path into player-visible text, and threw on packs with no backing file |
| `%iris_region_file%` | removed | Same as `biome_file` |
| `%iris_world_seed%` | removed | Handed the world seed to anyone who could read a scoreboard, and a placeholder has no permission context to gate on |
| `%iris_terrain_height%` | removed | Reported the *generated* height, before objects and player edits, so it disagreed with the block under the player's feet |
| `%iris_terrain_slope%` | removed | Three extra noise samples per read for an unformatted pack-authoring diagnostic |
| `%iris_world_mode%` | removed | Studio or Production; a studio world exists for seconds during authoring and is never on a live board |
| `%iris_world_speed%` | removed | Mutated engine rate-window state every time it was read. `%iris_pregen.chunks-per-second%` answers the same question from a snapshot |
There is one behaviour change inside the four renames, and it will be visible on a board that has
been in service for a while. The old keys sampled **two blocks above the player's feet** and asked
for the biome at that exact Y, which meant a player standing under an overhang or inside a cave read
the *cave* biome. `%iris_world.biome%` and `%iris_world.biome-key%` are always the surface biome for
the column. If your board is checked against a screenshot from before the rename, expect
underground readings to differ.
`%iris_world.dimension%` is new. It has no pre-2.0 equivalent.
The three removals worth a replacement plan:
- **`world_seed`** has no replacement and will not get one. A plugin that legitimately needs the seed
can read it from `IrisWorldInfo.seed()` through the [terrain API](terrain.md), where there is a
caller to hold responsible.
- **`terrain_height`** has no replacement. If you want the ground height for a coordinate, use
`IrisTerrainService#surfaceHeight`, which is the same number with its limitations documented. If
you want the block under the player, use the player's own Y.
- **`world_speed`** is replaced by `%iris_pregen.chunks-per-second%` for the pregeneration case,
which is what it was almost always used for. There is no per-world live generation rate key.
---
## Failure policy
| Situation | What Iris shows |
|---|---|
| An unknown path | Nothing. PlaceholderAPI re-emits the literal `%iris_...%` |
| A known path with no data | `---` |
| No player context, on a `world.*` key | `---`, and `world.available` is `false` |
| Player is not in an Iris world | `---`, and `world.available` is `false` |
| The terrain service is not registered | `---`, `world.available` is `false`, `%iris_available%` is `false` |
| No pregeneration job running | `---`, and `pregen.available` is `false` |
| A resolver throws | `---`, and one warning is logged naming the exact placeholder |
A resolver that throws is logged **once per distinct path**, up to 64 distinct paths, so a broken
key cannot flood the console from a scoreboard that re-renders every tick. The value shown is always
`---` — a failure never renders a stack trace, a class name, or an empty string.
Iris does not disable a placeholder after repeated failures. There is no fault limit and no
quarantine; a key that fails keeps being asked and keeps answering `---`.
---
## Key reference
The full published list, as PlaceholderAPI reports it under `/papi info iris`:
```
available
pregen.available
pregen.chunks
pregen.chunks-per-second
pregen.eta
pregen.eta-text
pregen.paused
pregen.percent
pregen.total
pregen.world
world.available
world.biome
world.biome-key
world.dimension
world.region
world.region-key
```
Prefix each with `%iris_` and suffix with `%`.
-584
View File
@@ -1,584 +0,0 @@
# Iris terrain query API
`art.arcane.iris.api.terrain` answers what the Iris generator says about a coordinate: whether a
world is an Iris world at all, what biome and region the pack places there, how high the terrain
generates and whether that surface is land, shore, ocean or nothing. It is a read of the
**generator**, not of the world. It never loads a chunk, never forces generation, never reads a
placed block, and never tells you what a player has since built.
Everything here is cheap and non-blocking, and this document says exactly how cheap and exactly why
non-blocking, because a terrain API where the reader has to guess is a terrain API that ends up in a
per-tick loop.
---
## Depending on Iris and acquiring the service
See [README.md](README.md#depending-on-iris) for the build and plugin-descriptor setup. The service
is registered with the Bukkit `ServicesManager` at `ServicePriority.Normal` for the duration of the
Iris plugin's enabled lifetime.
```java
package com.example.integration;
import art.arcane.iris.api.terrain.IrisTerrainService;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class TerrainAccess {
private TerrainAccess() {
}
public static IrisTerrainService service() {
RegisteredServiceProvider<IrisTerrainService> provider =
Bukkit.getServicesManager().getRegistration(IrisTerrainService.class);
return provider == null ? null : provider.getProvider();
}
}
```
There is no `Iris` class to import, no static accessor and no reflection. If the registration is
missing, Iris is absent or has not enabled yet; that is a `null` and not an exception.
---
## The read surface
```java
public interface IrisTerrainService {
boolean isIrisWorld(World world);
Optional<IrisWorldInfo> worldInfo(World world);
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
Optional<String> regionKey(World world, int blockX, int blockZ);
Optional<String> regionName(World world, int blockX, int blockZ);
int maxSampleColumns();
int maxSampleChunks();
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
}
```
All coordinates are **absolute block coordinates in world space**, including `blockY` and including
the value returned by `surfaceHeight`. There is no engine-space offset for a caller to apply.
`*Key` returns a pack load key — `desert/hot-dunes`, `overworld` — which is stable, lowercase and
what you store. `*Name` returns the author's display string — `Hot Desert Dunes` — which is what you
show and which can change when the pack author edits it. Both are `Optional` and both are empty when
the underlying value is absent or the empty string.
---
## Cost and blocking
This is the whole story. Read it before you write a loop.
Iris's generator is a stack of procedural noise streams. Every read below evaluates that stack for
one column and memoises the result in a shared per-chunk noise cache. A **cold** column runs the
pack's noise; a **warm** column is an array index. Nothing on this page reads chunk storage, reads a
block, loads a region file, takes a lock, waits on a future, or asks the server to generate
anything.
| Call | Cost when cold | Cost when warm | Forces generation | Can block | When the data is not there |
|---|---|---|---|---|---|
| `isIrisWorld` | one `World#getGenerator()` and an `instanceof` | same | No | No | `false` |
| `worldInfo` | field reads off the live engine and dimension | same | No | No | `Optional.empty()` |
| `surfaceHeight` | one height sample, which pulls the region and base-biome streams for that column | array read | No | No | `OptionalInt.empty()` |
| `surfaceKind` | one height sample, plus one surface-biome sample **only** for columns above fluid level | array read | No | No | `IrisSurfaceKind.UNKNOWN` |
| `surfaceBiomeKey` / `surfaceBiomeName` | one surface-biome sample, which pulls height, base biome and region | array read | No | No | `Optional.empty()` |
| `biomeKey` at or near the surface | as `surfaceBiomeKey`, plus one height sample to decide surface vs cave | array read | No | No | `Optional.empty()` |
| `biomeKey` well below the surface | the above, plus the cave-biome stream and the dimension's carving resolution | array reads | No | No | `Optional.empty()` |
| `regionKey` / `regionName` | one region sample — the cheapest of the biome family | array read | No | No | `Optional.empty()` |
| `maxSampleColumns` / `maxSampleChunks` | reads two settings fields | same | No | No | a positive number, always |
| `sampleColumns` | one of the above per column, in chunk-local order | array reads | No | No | `false`, sink untouched |
Two consequences that matter more than the per-call cost:
**Calling in a tight main-thread loop is survivable but wasteful.** Nothing will deadlock and
nothing will stall on I/O. What you will do is evict the generator's own working set: the noise
cache is shared with live chunk generation, and a scan across unrelated coordinates pushes out the
columns the generator was about to reuse. The visible symptom is chunk generation slowing down, not
your loop slowing down. Use `sampleColumns` for anything wider than a handful of columns — it walks
chunk by chunk so each cached chunk is filled and finished with before the next one starts.
**These values are the generator's opinion, not the world's.** `surfaceHeight` is the height of the
generated terrain column. It does not include objects, decorations, structures, trees, snow, or
anything a player has placed or broken since. In an already-generated world the block at that Y may
be different, and in a world that has never generated there you still get an answer, because the
answer comes from noise and not from storage. If you need the real block, use Bukkit's
`World#getHighestBlockYAt` and accept its chunk-loading cost. If you need to know where the pack
*intends* the ground to be — which is the useful question for a pregeneration planner, a map
renderer or a spawn picker — use this.
### Surface height, precisely
`surfaceHeight` returns the absolute Y of the **topmost generated terrain block**. A player stands
at `surfaceHeight + 1`. Fluid is ignored: under an ocean you get the sea floor, not the water
surface. Compare against `IrisWorldInfo.fluidHeight()` to tell the difference, or use
`surfaceKind`, which does exactly that comparison for you.
---
## Threading
**Every read on this interface may be called from any thread, including an async task.** That is an
unusual claim in a Folia-aware suite and it is made deliberately, so here is the justification:
- The only Bukkit call Iris makes on your behalf is `World#getGenerator()`, an accessor on the world
object itself. No chunk is touched, no block state is read, no entity is looked at, no world list
is walked.
- Everything after that is engine-internal noise evaluation over concurrent caches. There is no
region-owned state involved, so there is no region thread with a claim on it.
- No method here takes a lock you can contend on, calls `join`, or schedules onto another thread.
There is nothing to gain from hopping to a region thread first, and on Folia there is no region
thread that would be the *correct* one for a coordinate scan spanning many regions anyway. Run wide
scans on your own async executor.
The one rule: **`IrisColumnSink.accept` runs on the thread that called `sampleColumns`, inline,
once per column.** If you called from an async thread, your sink is on that async thread and must
not touch Bukkit state. If you called from a region thread, your sink is holding that region thread
for the entire walk. Collect into a local structure inside the sink and do the Bukkit work
afterwards.
---
## Column sampling
`sampleColumns` is the bulk read. It walks a rectangle at a stride, chunk by chunk, and pushes each
column into your sink.
```java
public record IrisColumnQuery(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields) {
public static IrisColumnQuery rect(
int minBlockX,
int minBlockZ,
int maxBlockX,
int maxBlockZ,
int strideBlocks,
EnumSet<IrisColumnField> fields);
public long columnCount();
public long chunkCount();
public EnumSet<IrisColumnField> fields();
}
```
The bounds are **inclusive on both ends**. The sampled lattice is anchored at
`(minBlockX, minBlockZ)` and steps by `strideBlocks`; a stride of `1` visits every column.
The constructor rejects, with `IllegalArgumentException`:
- an empty `fields` set,
- `maxBlockX < minBlockX` or `maxBlockZ < minBlockZ`,
- `strideBlocks < 1`.
`fields` is defensively copied on the way in and on every call to `fields()`, so a set you mutate
after construction does not change the query, and a set you get back and mutate does not either.
`fields()` allocates a fresh `EnumSet` each call — hoist it out of loops.
`columnCount()` and `chunkCount()` saturate at `Long.MAX_VALUE` instead of overflowing, so a query
over the whole coordinate space reports an absurd number rather than a negative one.
### The hard limits
```java
int maxSampleColumns();
int maxSampleChunks();
```
Both are derived from the generator's noise cache size, so a large-cache server permits larger
queries and a small-cache server permits smaller ones. The rule is fixed:
```
maxSampleChunks = max(64, noiseCacheSize / 4)
maxSampleColumns = maxSampleChunks * 256
```
With the default `noiseCacheSize` of 1024 that is **256 chunks and 65 536 columns**. The divisor of
four is the point of the whole mechanism: one API query may never consume more than a quarter of the
cache the live generator is using.
**A query that exceeds either limit returns `false` and never calls your sink — not once.** There is
no partial answer, no truncation, no exception, and no log line. If you get `false` before any
column arrives, check the counts.
The two limits are checked independently, and this is where callers get caught:
```java
IrisColumnQuery wide = IrisColumnQuery.rect(
0, 0, 6399, 6399, 64, EnumSet.of(IrisColumnField.SURFACE_KIND));
```
That query reports `columnCount() == 10_000`, well under the 65 536 column limit, and
`chunkCount() == 160_000`, far over the 256 chunk limit. It is refused.
**`chunkCount()` counts the chunk span of the rectangle, not the chunks you actually sample.**
Striding does not reduce it. A coarse sweep across a large area is refused on chunks even though it
touches very few columns. Split it into tiles, or accept a smaller rectangle:
```java
long maxColumns = terrain.maxSampleColumns();
long maxChunks = terrain.maxSampleChunks();
if (query.columnCount() > maxColumns || query.chunkCount() > maxChunks) {
return;
}
```
Ask the service every time. Both values change when an operator edits the setting and reloads.
### The sink
```java
@FunctionalInterface
public interface IrisColumnSink {
void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey);
}
```
Every column produces exactly one `accept`. What arrives depends on the `fields` you asked for, and
the placeholders for fields you did **not** ask for are not distinguishable from real data:
| Field requested | Parameter | If you asked for it | If you did not |
|---|---|---|---|
| `SURFACE_HEIGHT` | `surfaceHeight` | absolute world Y of the topmost terrain block | `-1` |
| `SURFACE_KIND` | `kind` | `LAND`, `SHORE`, `OCEAN` or `VOID` | `IrisSurfaceKind.UNKNOWN` |
| `BIOME_KEY` | `biomeKey` | the biome load key | `null` |
`-1` is a legal absolute Y in any world with a negative minimum height, so **never treat `-1` as
"absent"**. Branch on your own field set, which you already have. `biomeKey` may also be `null` when
you *did* ask for it, if the column resolves to no biome; test for `null` regardless.
Requesting fewer fields genuinely costs less. `SURFACE_KIND` alone does not evaluate the biome
stream for a column that is at or below fluid level, because the classification is already decided.
Ask for `BIOME_KEY` and every column pays for the biome stream.
### Visit order
Columns arrive grouped by chunk. The walk iterates chunks with Z as the outer loop and X as the
inner loop, and within each chunk iterates its lattice points the same way, Z outer and X inner.
Order is deterministic for a given query, but it is **not** a row-major sweep of the rectangle: you
receive all of one chunk's columns before any of the next chunk's. If your consumer needs raster
order, sort afterwards or index into an array by `(blockX, blockZ)`.
### The return value
`sampleColumns` returns `true` if and only if every column in the query was delivered. It returns
`false` when:
- `world`, `query` or `sink` is `null`, or the world has no live Iris engine — sink untouched;
- a limit was exceeded — sink untouched;
- **your sink threw** — the walk stops at that column;
- **the engine closed underneath the walk** — the walk stops at that column.
In the last two cases the columns already delivered were delivered. `false` does not mean "nothing
happened"; it means "do not trust this result set as complete". Treat a `false` as a signal to
discard the partial data, not as a signal that there is none.
---
## Worked example: finding the flattest buildable spot
A plugin that places a settlement wants the flattest patch of land inside a radius, and wants none
of that work on a region thread. It samples on an async task, then hands the answer to the player's
entity scheduler, which is the correct thread to touch a player on Folia and on Paper alike.
```java
package com.example.settlement;
import art.arcane.iris.api.terrain.IrisColumnField;
import art.arcane.iris.api.terrain.IrisColumnQuery;
import art.arcane.iris.api.terrain.IrisColumnSink;
import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.util.EnumSet;
import java.util.Optional;
import java.util.concurrent.Executor;
public final class SettlementSiteFinder {
private static final int RADIUS_BLOCKS = 512;
private static final int STRIDE_BLOCKS = 8;
private final Plugin plugin;
private final Executor background;
public SettlementSiteFinder(Plugin plugin, Executor background) {
this.plugin = plugin;
this.background = background;
}
public void findFor(Player player) {
World world = player.getWorld();
Location origin = player.getLocation();
int centreX = origin.getBlockX();
int centreZ = origin.getBlockZ();
background.execute(() -> {
String result = search(world, centreX, centreZ);
player.getScheduler().run(plugin, task -> player.sendMessage(result), null);
});
}
private String search(World world, int centreX, int centreZ) {
IrisTerrainService terrain = service();
if (terrain == null || !terrain.isIrisWorld(world)) {
return "That world is not generated by Iris.";
}
Optional<IrisWorldInfo> info = terrain.worldInfo(world);
if (info.isEmpty()) {
return "The Iris engine for that world is not available right now.";
}
IrisColumnQuery query = IrisColumnQuery.rect(
centreX - RADIUS_BLOCKS,
centreZ - RADIUS_BLOCKS,
centreX + RADIUS_BLOCKS,
centreZ + RADIUS_BLOCKS,
STRIDE_BLOCKS,
EnumSet.of(IrisColumnField.SURFACE_HEIGHT, IrisColumnField.SURFACE_KIND));
if (query.columnCount() > terrain.maxSampleColumns()
|| query.chunkCount() > terrain.maxSampleChunks()) {
return "That search area is larger than this server allows.";
}
int fluidHeight = info.get().fluidHeight();
Best best = new Best();
IrisColumnSink sink = (int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey) -> {
if (kind != IrisSurfaceKind.LAND || surfaceHeight <= fluidHeight) {
return;
}
long score = (long) Math.abs(surfaceHeight - fluidHeight) * 1024L
+ Math.abs(blockX - centreX) + Math.abs(blockZ - centreZ);
if (score < best.score) {
best.score = score;
best.x = blockX;
best.y = surfaceHeight;
best.z = blockZ;
}
};
if (!terrain.sampleColumns(world, query, sink)) {
return "The terrain scan did not complete. Try again.";
}
if (best.score == Long.MAX_VALUE) {
return "No dry land within " + RADIUS_BLOCKS + " blocks.";
}
return "Best site: " + best.x + ", " + (best.y + 1) + ", " + best.z;
}
private IrisTerrainService service() {
RegisteredServiceProvider<IrisTerrainService> provider =
plugin.getServer().getServicesManager().getRegistration(IrisTerrainService.class);
return provider == null ? null : provider.getProvider();
}
private static final class Best {
private long score = Long.MAX_VALUE;
private int x;
private int y;
private int z;
}
}
```
`Best` needs no synchronisation: the sink runs inline on the thread that called `sampleColumns`, so
every `accept` for this walk is on the background thread that started it, and no other thread reads
the holder until the walk has returned. The `+ 1` on the reported Y is the standing height, since
`surfaceHeight` is the topmost solid block. `player.getScheduler()` is Paper's entity scheduler and
is the correct hop on both Paper and Folia; on Folia it resumes on whichever region owns the player
at that moment, which may not be the region they were in when the scan started.
---
## The minimum: one coordinate
Most integrations want one biome name at one place. That is three lines and needs none of the above.
```java
IrisTerrainService terrain = service();
String biome = terrain == null
? "unknown"
: terrain.surfaceBiomeName(player.getWorld(), player.getLocation().getBlockX(),
player.getLocation().getBlockZ()).orElse("unknown");
```
`surfaceBiomeName` on a non-Iris world, a null world, a closing engine or a disabled Iris returns
`Optional.empty()`. You do not need to call `isIrisWorld` first unless you want to distinguish
"not an Iris world" from "Iris has nothing to say".
---
## What `IrisWorldInfo` tells you
```java
public record IrisWorldInfo(
String dimensionKey,
String worldIdentity,
long seed,
int minHeight,
int maxHeight,
int fluidHeight,
boolean studio) {
public int height();
}
```
| Component | What it is |
|---|---|
| `dimensionKey` | Pack load key of the dimension, for example `overworld` |
| `worldIdentity` | The world's namespaced key rendered as a string, for example `minecraft:overworld` |
| `seed` | The raw seed the engine was built with |
| `minHeight` | Absolute Y of the world floor, for example `-64` |
| `maxHeight` | Absolute Y of the world ceiling, exclusive, for example `320` |
| `fluidHeight` | Absolute Y of the pack's sea level |
| `studio` | `true` only for a transient studio world |
| `height()` | `maxHeight - minHeight` |
`minHeight`, `maxHeight` and `fluidHeight` are all absolute world Y, directly comparable with
`surfaceHeight` and with `blockY`. The record's own constructor rejects a null `dimensionKey` or
`worldIdentity` with `NullPointerException` and a non-positive height range with
`IllegalArgumentException`, so an instance you receive is always internally consistent.
`worldIdentity` is the string form of the world's `NamespacedKey`, and it is the key Iris itself
persists per-world state under. It is the right key for you to persist too, because it is namespaced
and unambiguous where a bare name is not. It is **not** independent of the world's name: outside the
three vanilla dimensions the server derives the key from the world folder, so renaming that folder
changes `worldIdentity` exactly as it changes `World#getName()`.
`studio` is `true` for a world Iris created for pack authoring — those exist for seconds and are
deleted, so a persistence layer should skip them.
`seed` is the generator seed. Treat it as privileged: it is enough to reproduce the entire world
offline, including every ore vein and structure. Iris deliberately does not expose it through
PlaceholderAPI for that reason. Do not put it anywhere a player can read.
---
## Failure policy
Iris assumes the caller will pass nulls, hand it a world it does not own, keep a stale service
reference, and throw from a sink.
| Situation | What Iris does |
|---|---|
| `world` is `null` | Every query answers absent; `sampleColumns` returns `false` |
| The world has no Iris generator | Same |
| Iris is disabled, or disabled between your two calls | Same. Nothing throws |
| The generator is closing, or the engine is closed | `isIrisWorld` still returns **`true`**; every other query answers absent |
| A query throws inside the engine | Counted, logged with the stack trace, answered as absent |
| `query` or `sink` is `null` | `sampleColumns` returns `false`, sink never called |
| The query exceeds `maxSampleColumns` or `maxSampleChunks` | `sampleColumns` returns `false`, sink never called, nothing logged |
| Your sink throws | Walk aborts at that column, fault counted and logged, `sampleColumns` returns `false`. Columns already delivered stay delivered |
| The engine closes mid-walk | Walk stops at that column, `sampleColumns` returns `false` |
Two deliberate asymmetries worth internalising:
**`isIrisWorld` does not check liveness.** It answers "was this world created by Iris", not "can
Iris answer questions about it right now". During world unload and during plugin shutdown you will
see `isIrisWorld(world) == true` alongside `worldInfo(world).isEmpty()`. That is correct behaviour,
not a race you can win. Code that branches on `isIrisWorld` and then dereferences an
`Optional#get()` will throw eventually; use `orElse` or check the `Optional`.
**Iris never quarantines a caller.** There is no fault limit and no disable-after-N. A sink that
throws on every column will be logged and refused on every call, forever, and will never be muted or
blacklisted. The internal fault counters exist only to throttle the log line to at most one report
per minute per category — the count in that line tells you how many faults have occurred in total,
so a "3 faults" line followed by a "9000 faults" line means you have a loop, not two incidents.
Nothing in this API ever throws a checked exception, and nothing throws an unchecked one except the
argument validation on `IrisColumnQuery` and `IrisWorldInfo` constructors described above.
---
## Configuration
`plugins/Iris/settings.json`:
| Key | Default | Effect on this API |
|---|---|---|
| `performance.noiseCacheSize` | `1024` | The chunk capacity of the shared noise cache. `maxSampleChunks` is `max(64, this / 4)` and `maxSampleColumns` is `maxSampleChunks * 256`. Raising it raises both limits and the memory the generator holds |
There is no on/off switch for the terrain API and no per-world gate. It answers for every world with
a live Iris engine, and answers absent for everything else.
---
## Enum reference
### `IrisSurfaceKind`
Returned by `surfaceKind` and delivered to `IrisColumnSink`.
| Constant | Meaning | Test Iris applies |
|---|---|---|
| `LAND` | Dry ground | Surface above sea level, and the biome is not a shore biome |
| `SHORE` | Beach or bank | Surface above sea level, and the pack classifies the biome as shore |
| `OCEAN` | Under water | Surface at or below `IrisWorldInfo.fluidHeight()` |
| `VOID` | Nothing generated | Surface at or below `IrisWorldInfo.minHeight()` |
| `UNKNOWN` | No answer | Not an Iris world, the engine is unavailable, a query faulted, or `SURFACE_KIND` was not requested |
**`VOID` is tested first and wins.** A column at or below `minHeight()` reports `VOID` whatever the
sea level is; only a column above the floor is then tested against the fluid level, and only a column
above the fluid level is then tested for a shore biome. The four are mutually exclusive.
`OCEAN` is inclusive at the boundary: a column whose topmost terrain block sits exactly at sea level
reports `OCEAN` even though no water block is generated above it. If that one-block distinction
matters, compare `surfaceHeight` against `fluidHeight` yourself.
`UNKNOWN` is overloaded on purpose — it is the single "no data" value, so a caller never has to
handle both a sentinel and an exception. Distinguish the causes with `isIrisWorld` and `worldInfo`
if you need to.
### `IrisColumnField`
Selects what `sampleColumns` computes and passes to the sink. At least one is required.
| Constant | Fills | Extra work |
|---|---|---|
| `SURFACE_HEIGHT` | the `surfaceHeight` parameter | one height sample per column |
| `SURFACE_KIND` | the `kind` parameter | one height sample, plus a biome sample only for columns above sea level |
| `BIOME_KEY` | the `biomeKey` parameter | one biome sample per column, unconditionally |
`SURFACE_HEIGHT` and `SURFACE_KIND` share their height sample — asking for both costs barely more
than asking for either.
-517
View File
@@ -1,517 +0,0 @@
# Iris tree feller API
`art.arcane.iris.api.tree` lets another plugin **drive** the Iris tree feller and **charge** for it.
The feller removes a whole Iris-generated tree, block by block, when a sneaking survival player
breaks one of its logs with an axe. This API lets you turn it on for a player who would not
otherwise be allowed it, override the durability rules, and take something from that player for each
log removed — with a reservation you can get back if the log turns out not to be removable.
There are two things you can do, and they are independent:
| You want to… | Use |
|---|---|
| start a felling run that Iris would not have started, or price it | `IrisTreeFellerService#tryFell` with `TreeFellerOptions.integrationOverride(...)` |
| avoid double-handling the block breaks Iris generates while felling | `IrisTreeFellerService#isManagedBreak` |
| ask whether a block belongs to an Iris tree at all | `IrisTreeFellerService#isTreeBlock` |
**The tree feller is off by default.** `treeFeller.enabled` in Iris's settings is `false` out of the
box, and the standalone path additionally requires the `iris.treefeller` permission. An
`INTEGRATION_OVERRIDE` request bypasses **both** — that is what the mode is for, and it means your
plugin is now the thing that decides who may fell trees.
---
## Depending on Iris and acquiring the service
See [README.md](README.md#depending-on-iris) for the build and plugin-descriptor setup. The service
is registered with the Bukkit `ServicesManager` at `ServicePriority.Normal`.
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.IrisTreeFellerService;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
public final class FellerAccess {
private FellerAccess() {
}
public static IrisTreeFellerService service() {
RegisteredServiceProvider<IrisTreeFellerService> provider =
Bukkit.getServicesManager().getRegistration(IrisTreeFellerService.class);
return provider == null ? null : provider.getProvider();
}
}
```
```java
public interface IrisTreeFellerService {
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
boolean isManagedBreak(BlockBreakEvent event);
boolean isTreeBlock(Block block);
}
```
---
## The lifecycle
```
your BlockBreakEvent handler
|
v
tryFell(event, options) register a felling request against this break.
| Returns true when YOUR request is pending. Nothing has
| happened yet and no hook has fired.
|
| (Iris re-checks everything at EventPriority.MONITOR)
v
onActivationAccepted() the run is real. Fires exactly once, if at all.
|
| (per LOG block, in the order the tree comes apart)
v
reserveLogCost() -> false you refuse. The run ends. Nothing to give back.
|
| true
v
+--> commitLogCost() the log is gone. The charge is yours. FINAL.
+--> refundLogCost() the log could not be removed. Give it back.
```
Rules Iris guarantees:
- `onActivationAccepted` fires **at most once per run**, and only after Iris has re-validated the
break at `MONITOR`: the event was not cancelled, the block still resolves to the same Iris tree,
and no other run already claims that tree.
- `reserveLogCost` is called **once per log block**, not once per run. A twelve-log tree calls it up
to twelve times. **Leaves never reserve** — they are removed without consulting you.
- `reserveLogCost` is called **before** Iris charges the axe's durability, so a refusal costs the
player nothing at all.
- Exactly **one** of `commitLogCost` or `refundLogCost` follows a `reserveLogCost` that returned
`true`, with the one exception described under [Failure policy](#failure-policy).
- **`commitLogCost` is final.** There is no reversal after it, and Iris will not call
`refundLogCost` for a block it has already committed.
- A `reserveLogCost` that returns `false` ends the whole run immediately. It does not skip that log
and continue.
- A tree can only be felled by one run at a time, server-wide. A second player breaking the same
tree while a run is in flight has their break cancelled with drops suppressed, and no hook of
yours is called for it.
There is **no terminal callback.** `TreeFellerRunHooks` has no "the run finished" method. If your
accounting needs to know when a run ended, count `commitLogCost` and `refundLogCost` calls against
the `onActivationAccepted` that opened the run, and treat a run with no activity as over.
---
## Threading
Three different threads are involved and the distinction matters, because two of them are region
threads on Folia and one is an entity scheduler.
| Call | Thread |
|---|---|
| `tryFell` | You call it. It must be the thread delivering the `BlockBreakEvent` — the region thread that owns the broken block |
| `isManagedBreak` | Any thread. It is a set lookup and touches nothing else |
| `isTreeBlock` | The region thread that owns the block. It reads block state **and** can block on disk — see below |
| `onActivationAccepted` | The region thread that owns the broken block, inline in the `MONITOR` dispatch |
| `reserveLogCost` | The **feller's entity scheduler thread** |
| `commitLogCost` | The feller's entity scheduler thread |
| `refundLogCost` | The feller's entity scheduler thread |
The three cost hooks run on the player's entity scheduler, which is the thread that owns that player
on Folia. Reading and mutating the feller's inventory, experience and effects is legal there. The
player's *world* is not yours on that thread — do not read or write blocks from a cost hook.
`onActivationAccepted` runs on the block's region thread, inline inside the `BlockBreakEvent`
dispatch at `MONITOR`. Blocks and the player are both legal to touch there, but you are inside event
dispatch: return promptly.
**Do not block, in any of the four.** No I/O, no `CompletableFuture#join`, no locks held across the
call. Iris does not interrupt a hook that hangs and does not time it out; the contract is the only
protection. If a cost decision needs remote data, cache it — prime it on `PlayerJoinEvent`.
### `isTreeBlock` is the expensive one
`isTreeBlock` reads Iris's mantle — the generator's persistent per-region metadata store — to find
out whether the block was placed by an Iris tree. If the mantle region covering that block is not
resident in memory, **this call loads it from disk, synchronously, on your thread.** It also reads
the block's type and block data, so the chunk must be loaded and you must be on the region thread
that owns it.
Concretely: on a first touch in a cold area it does a filesystem stat, and possibly a full region
load and decompress, before it answers. On a warm area it is a couple of map lookups.
Do not call it per block in a loop, per tick, or on a large area. Nothing else in this API touches
the mantle; if you are calling `isTreeBlock` speculatively rather than about a block a player just
interacted with, you are using it wrong.
---
## Worked example: charging stamina per log
A plugin with its own stamina pool. It lets players fell trees regardless of Iris's permission and
enabled switch, charges 4 stamina per log, gives it back when a log turns out not to be removable,
and preserves the axe 50% of the time.
### The hooks
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import java.util.UUID;
public final class StaminaFellHooks implements TreeFellerRunHooks {
private static final int COST_PER_LOG = 4;
private final StaminaPool pool;
private final UUID fellerId;
public StaminaFellHooks(StaminaPool pool, UUID fellerId) {
this.pool = pool;
this.fellerId = fellerId;
}
@Override
public void onActivationAccepted() {
pool.beginRun(fellerId);
}
@Override
public boolean reserveLogCost() {
return pool.withdraw(fellerId, COST_PER_LOG);
}
@Override
public void commitLogCost() {
pool.recordSpend(fellerId, COST_PER_LOG);
}
@Override
public void refundLogCost() {
pool.deposit(fellerId, COST_PER_LOG);
}
}
```
`TreeFellerRunHooks` declares all four methods and none of them has a default, so an implementation
must provide all four even when three are empty. `TreeFellerRunHooks.NONE` is the shared no-op
implementation whose `reserveLogCost` returns `true`; use it when you want the override behaviour
without a cost.
The hooks instance is **per run**, not per plugin. Build a new one for each `tryFell` call and put
the feller's identity in it — Iris hands the same instance back for every callback of that run and
never inspects it, so it is the natural place to carry run state.
### The listener
```java
package com.example.woodcutting;
import art.arcane.iris.api.tree.IrisTreeFellerService;
import art.arcane.iris.api.tree.TreeFellerOptions;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
public final class WoodcuttingListener implements Listener {
private static final int PRESERVE_PERCENT = 50;
private final StaminaPool pool;
public WoodcuttingListener(StaminaPool pool) {
this.pool = pool;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBreak(BlockBreakEvent event) {
IrisTreeFellerService feller = FellerAccess.service();
if (feller == null || feller.isManagedBreak(event)) {
return;
}
Player player = event.getPlayer();
if (!pool.hasWoodcutting(player.getUniqueId())) {
return;
}
TreeFellerOptions options = TreeFellerOptions.integrationOverride(
PRESERVE_PERCENT, new StaminaFellHooks(pool, player.getUniqueId()));
feller.tryFell(event, options);
}
}
```
The `isManagedBreak` guard is not optional. While a run is in progress Iris fires a
`BlockBreakEvent` for **every block it removes**, so that protection plugins and loggers see the
removals. Without the guard your listener would call `tryFell` on Iris's own break events and your
stamina check would run once per block of the tree.
`EventPriority.HIGH` is a deliberate choice, and it is load-bearing. It is after the priorities a
protection plugin normally uses to cancel, and — the part that matters — strictly before `HIGHEST`,
which is where Iris asks for its own standalone run. A break is claimed by the first `tryFell` that
succeeds against it, so a handler at `HIGHEST` or `MONITOR` can find that Iris has already taken it.
See [What `tryFell` actually promises](#what-tryfell-actually-promises).
Registration is ordinary:
```java
@Override
public void onEnable() {
getServer().getPluginManager().registerEvents(new WoodcuttingListener(pool), this);
}
```
---
## The minimum: turn the feller on, charge nothing
If you only want players in your woodcutting class to fell trees, with Iris's own durability
behaviour and no cost:
```java
IrisTreeFellerService feller = FellerAccess.service();
if (feller != null && !feller.isManagedBreak(event) && classes.isWoodcutter(event.getPlayer())) {
feller.tryFell(event, TreeFellerOptions.integrationOverride(0, TreeFellerRunHooks.NONE));
}
```
Put that in a `BlockBreakEvent` handler at a priority earlier than `HIGHEST` — Iris asks for its own
standalone run at `HIGHEST`, and the first request to succeed claims the break.
`TreeFellerRunHooks.NONE` never refuses and never charges. A `durabilityPreservationChance` of `0`
means every log costs one point of axe durability, which is vanilla-equivalent.
`TreeFellerOptions.standalone()` exists for completeness — it is the request Iris makes for itself —
and there is almost never a reason for a third party to pass it. It respects the enabled switch and
the permission, so it can only ever do what Iris would already have done.
---
## What `tryFell` actually promises
```java
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
```
`true` means **your felling request is pending against this break**. It does not mean a tree will
fall — Iris re-validates everything at `MONITOR` and can still drop the request there.
**A break is claimed by the first `tryFell` that succeeds against it.** The moment a request is
accepted, Iris marks that `BlockBreakEvent` as managed, and every later `tryFell` for the same event
returns `false` immediately, without looking at your `access` at all. There is no displacement and no
last-writer-wins: whoever asks first, in event-priority order, owns the break.
| State when you call | Your `access` | Result |
|---|---|---|
| Nothing pending | either | Your request becomes pending. Returns `true` |
| A request already accepted for this break | either | The existing one stays. Returns `false` |
That has one consequence you must design around. **Iris makes its own `STANDALONE` request from a
listener at `EventPriority.HIGHEST`.** If your handler runs at `HIGHEST` and happens to be registered
after Iris's, or at `MONITOR`, Iris has already claimed the break and your override is refused. Call
`tryFell` from a handler at a priority strictly earlier than `HIGHEST``LOWEST`, `LOW`, `NORMAL` or
`HIGH` — and your `INTEGRATION_OVERRIDE` is the one that lands. `HIGH` is the usual choice.
Two plugins that both want to override the same break resolve the same way: the earlier priority
wins, and the later one gets `false` and knows it lost. Nothing is silently discarded.
`false` means no request of yours is pending. Iris returns `false` when:
- the service is disabled, or `event` or `options` is `null`;
- the event is already cancelled;
- the event is one Iris is already managing — either a break another request has already claimed, or
one of the per-block probe events Iris fires during a run. `isManagedBreak` answers both;
- `canUse` failed — for `STANDALONE` that means `treeFeller.enabled` is `false` or the player lacks
`iris.treefeller`; an `INTEGRATION_OVERRIDE` never fails this check;
- the break is not a fellable candidate.
`true` is still not a run. The `MONITOR` re-validation drops the request if the event was cancelled
after you asked, if the block no longer resolves to the same Iris tree, or if another run already
claims that tree — and in none of those cases does a hook fire. Open your run state in
`onActivationAccepted`, not at `tryFell`.
### What makes a break a candidate
An `INTEGRATION_OVERRIDE` bypasses the enabled switch and the permission. It does **not** bypass any
of these, and there is no option to:
- the player is in `GameMode.SURVIVAL`;
- the player is sneaking;
- the broken block is tagged `Tag.LOGS`;
- the item in the player's main hand is an axe;
- the block carries Iris tree provenance in the mantle — it was placed by an Iris tree, has not been
replaced since, and is not part of a structure.
A tree the player planted with a vanilla sapling is not an Iris tree and will never fell. Neither is
a log a player placed by hand: Iris clears the provenance record for a block as soon as it is broken
or built over.
---
## How a run comes apart
Once activated, Iris discovers the tree by walking the mantle provenance markers outward from the
broken block in all 26 directions, breadth-first. Members are then removed in that discovery order —
the block the player broke first, then outward — with ties broken by Y, then X, then Z.
Discovery is bounded. If any bound is hit the discovery is **incomplete**, and Iris falls back to
removing only the block the player actually broke:
| Bound | Value |
|---|---|
| Members collected | 131 072 |
| Positions visited | 1 000 000 |
| Distance from the broken block on any axis | 256 blocks |
Removal is paced: Iris removes a batch of blocks, then yields for a tick before the next batch, so a
large tree takes several ticks and does not stall a region. Batch size scales with the tree.
A run ends immediately, with no further hooks, when the player:
- stops sneaking,
- changes their held hotbar slot,
- swaps their hands,
- goes offline, leaves survival mode, or changes world,
- breaks their axe (the run ends after the log that broke it is committed),
- or replaces the axe in that slot with a different item.
Each removed block fires its own `BlockBreakEvent`, marked so that `isManagedBreak` returns `true`
for it during dispatch. Other plugins can cancel that event to protect a block. A cancelled probe on
a **log** refunds that log's reservation and ends the run; a cancelled probe on a **leaf** has no
reservation to give back and the run simply carries on to the next member. Drops for each block are
computed with the axe **as it was before that block's durability charge**, so enchantments like Silk
Touch and Fortune apply normally.
The original break event is cancelled by Iris with drops and experience suppressed, because Iris
delivers them itself per block instead.
---
## What the options carry
```java
public record TreeFellerOptions(
TreeFellerAccess access,
int durabilityPreservationChance,
TreeFellerRunHooks runHooks) {
public static TreeFellerOptions standalone();
public static TreeFellerOptions integrationOverride(
int durabilityPreservationChance,
TreeFellerRunHooks runHooks);
}
```
The canonical constructor throws `NullPointerException` for a null `access` or `runHooks`, and
`IllegalArgumentException` for a `durabilityPreservationChance` outside `0 .. 100`. Both factory
methods go through it, so `TreeFellerOptions.integrationOverride(101, hooks)` throws at the call
site rather than clamping silently.
`durabilityPreservationChance` is a percentage: the chance that removing one log costs the axe no
durability at all. `0` charges every log; `100` never charges. It is rolled independently per log.
An unbreakable axe is never charged whatever the value.
**The value is only honoured for `INTEGRATION_OVERRIDE`.** A `STANDALONE` request ignores whatever
you passed and uses `treeFeller.durabilityPreservationChance` from Iris's settings —
`TreeFellerOptions.standalone()` hard-codes `0` in the record for exactly that reason.
```java
public interface TreeFellerRunHooks {
TreeFellerRunHooks NONE;
void onActivationAccepted();
boolean reserveLogCost();
void commitLogCost();
void refundLogCost();
}
```
Iris never calls anything else on your hooks object — not `equals`, not `hashCode`, not `toString`.
It holds the reference for the duration of the run and drops it when the run ends.
---
## Failure policy
Iris assumes a hooks implementation will throw, refuse late, or be handed a player who logs out
mid-run.
| Misbehaviour | What Iris does |
|---|---|
| `onActivationAccepted` throws | Logged with the stack trace. **The run continues** — activation is a notification, not a veto |
| `reserveLogCost` throws | Logged, treated as `false`. The run ends. Nothing is refunded, because nothing was reserved |
| `reserveLogCost` returns `false` | Not a fault. The run ends cleanly at that log |
| `commitLogCost` throws | Logged. The run ends. **The block is already gone and is not restored** |
| `refundLogCost` throws | Logged. The run ends |
| A hook blocks for a long time | Nothing. Iris does not time hooks out, does not warn, and cannot interrupt them |
| `tryFell` is passed a null event or options | Returns `false`. No hook is called |
| Two plugins request an override for one break | The one whose handler ran first wins. The other gets `false` and no hook of its own fires |
| Resolving the candidate throws | Logged. `tryFell` returns `false` |
| `isTreeBlock` throws | Logged. Returns `false` |
| Iris is disabled mid-run | Every active run is finished immediately. **No refund is issued for anything outstanding** |
**Iris does not quarantine a misbehaving integration.** There is no fault limit, no disable-after-N,
and no automatic unregistration. A hooks implementation that throws on every log will be logged on
every log, forever.
### The one place a refund can be missed
A refund is delivered by scheduling onto the feller's entity scheduler. If that scheduling fails —
the player has logged out, or been removed from the world, between the reservation and the failure
that triggers the refund — Iris finishes the run **without calling `refundLogCost`**. The same
applies to a plugin shutdown that ends runs in flight.
The exposure is at most one log's worth of cost per run, and only in the window between reserving a
log and resolving it, which is a single block removal. If a stricter guarantee matters to you, do
not settle the charge inside the hooks: accumulate reservations in your own per-run state keyed by
the feller, and reconcile on `PlayerQuitEvent` and on your own `onDisable`. The hooks tell you what
happened; they are not a transaction log you can rely on being complete across a disconnect.
---
## Configuration
`plugins/Iris/settings.json`:
| Key | Default | Meaning |
|---|---|---|
| `treeFeller.enabled` | `false` | Master switch for the **standalone** path only. When `false`, Iris never fells a tree on its own. An `INTEGRATION_OVERRIDE` request is unaffected |
| `treeFeller.durabilityPreservationChance` | `0` | Percentage chance a log costs no axe durability, for the standalone path only. Clamped to `0 .. 100` on read |
Permission, declared in the plugin descriptor:
| Node | Default | Meaning |
|---|---|---|
| `iris.treefeller` | `op` | Required for the standalone path. An `INTEGRATION_OVERRIDE` request does not check it |
---
## Enum reference
### `TreeFellerAccess`
| Constant | Enabled switch | `iris.treefeller` | `durabilityPreservationChance` source |
|---|---|---|---|
| `STANDALONE` | Required | Required | Iris settings; the value in your options is ignored |
| `INTEGRATION_OVERRIDE` | Bypassed | Bypassed | The value in your options |
Neither mode bypasses the candidate checks — survival, sneaking, an axe, a log, and Iris tree
provenance.
Write a `default` arm when switching over this enum; see
[README.md](README.md#switching-over-the-enums).
-459
View File
@@ -1,459 +0,0 @@
# Iris world engine and pregeneration events
Two Bukkit events tell you what Iris is doing over time. `IrisWorldEngineEvent` marks the points at
which an Iris world's engine becomes usable, is rebuilt under you, or is about to stop being usable.
`IrisPregenerationEvent` reports the progress of a pregeneration job. Both are pure observation:
neither is cancellable, and nothing you do in a handler changes what Iris does next.
Use `IrisWorldEngineEvent` instead of `WorldLoadEvent` if you care about the *generator* rather than
the world. A world exists before its Iris engine is ready to answer questions, and it still exists
after the engine has been told to close.
---
## Depending on Iris
See [README.md](README.md#depending-on-iris) for the build and plugin-descriptor setup. Events need
no service lookup — register a `Listener` in your `onEnable` as usual and Bukkit unregisters you
when your plugin disables.
Both events have their own `HandlerList`. There is no shared base class and no common interface;
`IrisWorldEngineEvent` and `IrisPregenerationEvent` extend `org.bukkit.event.Event` directly.
Neither implements `Cancellable`. `ignoreCancelled = true` on a handler for either is meaningless
and will not do what you expect.
---
## The world engine lifecycle
```java
public enum IrisWorldPhase {
ENGINE_READY,
ENGINE_HOTLOADED,
ENGINE_CLOSING
}
```
```
ENGINE_READY the engine for this world is registered and answering.
| Terrain queries work from here on.
|
+--> ENGINE_HOTLOADED the pack was edited and the engine rebuilt in place.
| Same world, same engine object, different pack contents.
| Can fire any number of times, or never.
|
v
ENGINE_CLOSING the engine is about to be torn down. Last call.
```
Guarantees Iris makes:
- `ENGINE_READY` fires **at most once per world** for a given registration. It is keyed on the
world's UUID, so a world that unloads and loads again gets a fresh `ENGINE_READY`.
- `ENGINE_CLOSING` is **never delivered without a preceding `ENGINE_READY`** for that world. If Iris
never announced a world ready, it never announces it closing.
- `ENGINE_CLOSING` is dispatched **before** Iris starts closing the generator, not after. When your
handler runs, the engine has not been shut down yet.
- If Iris replaces a world's engine — the generator was swapped out and a new one registered — you
get `ENGINE_CLOSING` for the old one, and a later `ENGINE_READY` when the replacement finishes
registering. You never get two consecutive `ENGINE_READY` without a `CLOSING` between them.
- On Iris shutdown, **every** world that was announced ready is announced closing, before Iris drains
its worker pool and before any generator is closed.
- `ENGINE_HOTLOADED` is not deduplicated and does not participate in the ready/closing pairing. It
is a notification that the pack data behind a live engine was reloaded and the engine rebuilt
around it. The world, the world object and the seed are unchanged; the pack contents may not be.
Treat any pack-derived value you cached at `ENGINE_READY` as stale when it arrives.
### The one thing `ENGINE_CLOSING` does not promise
`ENGINE_CLOSING` is fired before the *generator* closes, but during a full plugin shutdown the
terrain service may already have been withdrawn by the time your handler runs — Iris tears down its
services in an unspecified order. So:
> Do not treat `ENGINE_CLOSING` as a window in which to run terrain queries. Capture whatever you
> need at `ENGINE_READY` and use `ENGINE_CLOSING` only to drop it.
A terrain query in a closing handler does not throw. It returns absent, which is worse, because it
looks like data.
---
## The event
```java
public class IrisWorldEngineEvent extends Event {
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info);
public static HandlerList getHandlerList();
public World getWorld();
public IrisWorldPhase getPhase();
public Optional<IrisWorldInfo> getInfo();
@Override
public HandlerList getHandlers();
}
```
`getWorld()` and `getPhase()` are never `null` — the constructor rejects both.
`getInfo()` is `Optional` and can be empty. It is empty when Iris could not describe the engine at
dispatch time: the generator was already closing, the engine was already closed, or building the
description threw (which is logged with a stack trace, and does not suppress the event). Handle the
empty case; do not call `get()` unconditionally.
`IrisWorldInfo` is documented in [terrain.md](terrain.md#what-irisworldinfo-tells-you). The short
version is that it carries the dimension load key, the world's namespaced identity, the seed, the
world height bounds, the pack's sea level, and whether this is a transient studio world.
### Threading
**Handlers always run on the main thread. On Folia, that is the global region thread.**
Iris raises these phases from several places — the world load and unload handlers, its own enable
and disable, and a pack hotload that can originate from a file watcher thread. The dispatch
normalises all of them:
- Raised from the primary thread: the event is called **inline**, before the raising code continues.
A `WorldLoadEvent` handler of yours that registers state, and an `ENGINE_READY` handler that reads
it, will see a consistent picture.
- Raised from any other thread: the event is handed to the server scheduler and delivered on the
main or global region thread on a later tick.
So your handler is always on a thread where touching Bukkit is legal, and never on the file-watcher
or worker thread that caused the phase.
What is forbidden: blocking. These phases run on the thread the server ticks on. No I/O, no
`CompletableFuture#join`, no waiting on another scheduler. If you need to persist something, hand it
to your own executor.
---
## Worked example: caching pack metadata per world
A plugin that shows the dimension a player is in wants that string without asking Iris for it on
every render. It captures it once when the engine is ready and drops it when the engine closes.
```java
package com.example.hud;
import art.arcane.iris.api.terrain.IrisWorldInfo;
import art.arcane.iris.api.world.IrisWorldEngineEvent;
import art.arcane.iris.api.world.IrisWorldPhase;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class IrisWorldRegistry implements Listener {
private final Map<UUID, String> dimensionKeys = new ConcurrentHashMap<>();
public String dimensionKeyOf(World world) {
return dimensionKeys.get(world.getUID());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onEngine(IrisWorldEngineEvent event) {
UUID worldId = event.getWorld().getUID();
switch (event.getPhase()) {
case ENGINE_READY, ENGINE_HOTLOADED -> {
Optional<IrisWorldInfo> info = event.getInfo();
if (info.isEmpty()) {
dimensionKeys.remove(worldId);
return;
}
dimensionKeys.put(worldId, info.get().dimensionKey());
}
case ENGINE_CLOSING -> dimensionKeys.remove(worldId);
default -> {
}
}
}
}
```
`ENGINE_HOTLOADED` is handled alongside `ENGINE_READY` because a hotload can change the pack's
dimension key. The `default` arm is there because the enum can grow; see
[README.md](README.md#switching-over-the-enums).
The map is a `ConcurrentHashMap` even though the handler is single-threaded, because
`dimensionKeyOf` is read from wherever your HUD renders.
---
## Pregeneration
```java
public enum IrisPregenPhase {
STARTED,
TICK,
PAUSED,
RESUMED,
SAVING,
COMPLETED,
CANCELLED
}
```
```java
public class IrisPregenerationEvent extends Event {
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress);
public static HandlerList getHandlerList();
public IrisPregenPhase getPhase();
public IrisPregenProgress getProgress();
@Override
public HandlerList getHandlers();
}
```
Both accessors are never `null`; the constructor rejects both.
### The order phases arrive in
```
STARTED -> TICK -> TICK -> ... -> COMPLETED
|
+-- PAUSED -> TICK -> ... -> RESUMED -> TICK -> ...
|
+-- SAVING (once, near the end)
|
+-- CANCELLED (instead of COMPLETED, if the job was stopped early)
```
- **One job at a time, server-wide.** Iris runs a single pregeneration job per server. There is no
job identifier on the event because there is nothing to disambiguate; `IrisPregenProgress` names
the world the running job is working on.
- `STARTED` is dispatched exactly once per job, immediately before that job's first `TICK`, in that
order.
- `TICK` fires **once per second** while the job runs. It fires while paused too.
- `PAUSED` and `RESUMED` fire on the transition only, each immediately followed by a `TICK`. A job
that is never paused never emits either.
- `SAVING` fires at most once per job.
- Exactly one of `COMPLETED` or `CANCELLED` is dispatched, and it is terminal. `COMPLETED` means the
job reached its chunk total; `CANCELLED` means it stopped before that, whether by operator action
or by shutdown. **No phase is ever dispatched for a job after its terminal phase.**
### Threading
**Handlers always run on the main thread. On Folia, that is the global region thread.**
The pregenerator ticks on its own worker thread, so every pregeneration phase is scheduled rather
than called inline. It arrives on a later tick than the moment the numbers were sampled. For a
progress bar this is invisible; for anything that correlates pregeneration against another timeline,
assume up to one tick of skew.
Do not block. The job does not wait for your handler — the dispatch is fire-and-forget and a
throwing handler is logged and skipped — but you are on the server's tick thread and everything else
does wait for you.
### What `IrisPregenProgress` tells you
```java
public record IrisPregenProgress(
String worldName,
String worldIdentity,
double percent,
long generatedChunks,
long totalChunks,
long remainingChunks,
long failedChunks,
double chunksPerSecond,
long etaMillis,
long elapsedMillis,
String method,
boolean paused) {
}
```
| Component | What it is |
|---|---|
| `worldName` | Never null; falls back to `worldIdentity` |
| `worldIdentity` | The world's namespaced key rendered as a string |
| `percent` | `0.0` to `100.0` |
| `generatedChunks` | Chunks the job has finished |
| `totalChunks` | Chunks in the job |
| `remainingChunks` | Chunks still to do |
| `failedChunks` | Chunks the job could not generate |
| `chunksPerSecond` | Current rate |
| `etaMillis` | Estimated milliseconds remaining |
| `elapsedMillis` | Milliseconds since the job started |
| `method` | Never null; `""` when unknown |
| `paused` | `true` while the job is paused |
The record's constructor sanitises everything before you see it, so you never have to defend against
the generator's arithmetic:
- `percent` is clamped to `0.0 .. 100.0`. `NaN` and infinity become `0.0`.
- `chunksPerSecond` is clamped to at least `0.0`. `NaN` and infinity become `0.0`.
- `generatedChunks`, `totalChunks`, `remainingChunks`, `failedChunks`, `etaMillis` and
`elapsedMillis` are clamped to at least `0`.
- `worldName` falls back to `worldIdentity` when the world has no name.
- `method` becomes `""` rather than `null`.
The only rejection is a `null` `worldIdentity`, which throws `NullPointerException` at construction —
so an instance delivered to you always identifies a world.
`etaMillis` is an estimate derived from the running rate and is `0` before enough chunks have
completed to compute one. `failedChunks` counts chunks the job could not generate; a non-zero value
on `COMPLETED` means the job finished with holes.
---
## Worked example: mirroring pregeneration into a boss bar
```java
package com.example.pregenbar;
import art.arcane.iris.api.pregen.IrisPregenProgress;
import art.arcane.iris.api.pregen.IrisPregenerationEvent;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public final class PregenBar implements Listener {
private BossBar bar;
@EventHandler(priority = EventPriority.MONITOR)
public void onPregen(IrisPregenerationEvent event) {
IrisPregenProgress progress = event.getProgress();
switch (event.getPhase()) {
case STARTED -> open(progress);
case TICK, PAUSED, RESUMED, SAVING -> update(progress);
case COMPLETED, CANCELLED -> close();
default -> {
}
}
}
private void open(IrisPregenProgress progress) {
close();
bar = Bukkit.createBossBar(
"Pregenerating " + progress.worldName(), BarColor.BLUE, BarStyle.SEGMENTED_10);
for (Player player : Bukkit.getOnlinePlayers()) {
bar.addPlayer(player);
}
update(progress);
}
private void update(IrisPregenProgress progress) {
if (bar == null) {
return;
}
bar.setProgress(progress.percent() / 100.0D);
bar.setColor(progress.paused() ? BarColor.YELLOW : BarColor.BLUE);
bar.setTitle(progress.worldName()
+ " " + progress.generatedChunks() + "/" + progress.totalChunks()
+ " at " + Math.round(progress.chunksPerSecond()) + "/s");
}
private void close() {
if (bar == null) {
return;
}
bar.removeAll();
bar = null;
}
}
```
`bar` needs no synchronisation: every phase is delivered on the same thread.
`percent()` is already clamped, so dividing by 100 always yields a legal boss-bar progress value.
---
## The minimum: knowing a world is usable
If all you want is "run this once, when Iris can answer for this world":
```java
@EventHandler
public void onEngine(IrisWorldEngineEvent event) {
if (event.getPhase() == IrisWorldPhase.ENGINE_READY) {
prepare(event.getWorld());
}
}
```
No `switch`, no `Optional`, no service lookup. Do not add `ignoreCancelled = true`; the event is not
cancellable.
---
## Failure policy
| Situation | What Iris does |
|---|---|
| Your handler throws | Logged with the stack trace. The remaining handlers still run, and Iris's own lifecycle continues unaffected |
| Iris cannot describe a world for a phase | The failure is logged and the event is **still delivered**, with `getInfo()` empty |
| The event dispatch itself throws | Logged, naming the phase and world. The engine registration or teardown that raised it proceeds |
| The pregeneration sink is not registered | No `IrisPregenerationEvent` is fired at all. This is the state before Iris finishes enabling and after it starts disabling |
| A pregeneration handler throws | Logged, naming the phase. The job is not slowed, paused or stopped |
| Iris shuts down mid-pregeneration | The job's terminal phase is `CANCELLED` |
| Iris shuts down with worlds registered | Every announced world receives `ENGINE_CLOSING` before the worker pool drains |
Iris does not quarantine a listener. A handler that throws on every event will be logged on every
event, forever. There is no fault limit and no automatic unregistration.
Iris never suppresses a lifecycle phase because a third party misbehaved. A logged failure is always
accompanied by delivery, or by the lifecycle step proceeding without delivery — never by a silent
stall.
---
## Configuration
There are no configuration keys for either event. They are always on when Iris is enabled, cannot be
disabled, and have no per-world gate.
---
## Enum reference
### `IrisWorldPhase`
| Constant | Meaning | Fires |
|---|---|---|
| `ENGINE_READY` | The engine is registered and answering queries | Once per world registration |
| `ENGINE_HOTLOADED` | A live engine's pack data was reloaded in place | Any number of times, or never. It is dispatched straight from the hotload, not through the ready/closing bookkeeping, so it is not paired with either |
| `ENGINE_CLOSING` | The engine is about to be torn down | Once per world registration, always after a `READY` |
### `IrisPregenPhase`
| Constant | Meaning | Fires |
|---|---|---|
| `STARTED` | A job began | Once per job, immediately before its first `TICK` |
| `TICK` | Periodic progress sample | Once per second while the job exists, including while paused |
| `PAUSED` | The job was paused | On the transition only, followed by a `TICK` |
| `RESUMED` | The job was resumed | On the transition only, followed by a `TICK` |
| `SAVING` | The job is flushing to disk | At most once per job |
| `COMPLETED` | The job reached its chunk total | Terminal; mutually exclusive with `CANCELLED` |
| `CANCELLED` | The job stopped before its total | Terminal; mutually exclusive with `COMPLETED` |
Write a `default` arm when switching over either; see
[README.md](README.md#switching-over-the-enums).
-173
View File
@@ -1,173 +0,0 @@
# Minecraft Version Bump Checklist
`gradle.properties` `minecraftVersion` is the single source of truth for the target Minecraft
version. Most build outputs derive from it. This document lists every edit required to move Iris
to a new Minecraft version, in order.
## Source of truth
`gradle.properties`:
- `minecraftVersion` — target MC version (e.g. `26.2`). Drives
`BuildConstants.MINECRAFT_VERSION`, the `com.mojang:minecraft` coordinate, all mod-metadata
minecraft ranges, and every dist/jar artifact name.
- `apiVersion` — Bukkit plugin `api-version` (e.g. `26.1`). Deliberately decoupled from
`minecraftVersion`: it is the lowest Minecraft release line the single plugin artifact loads on
(currently `26.1` so one jar serves 26.1.2 and 26.2). Bump it only when dropping support for the
older line.
- `fabricLoaderVersion` — Fabric Loader version.
- `forgeVersion` — Forge version (`<mc>-<forge>`).
- `neoForgeVersion` — NeoForge version.
- `irisVersion` — bump the trailing `-<mc>` suffix to match (e.g. `4.0.0-26.2` -> `4.0.0-27.0`).
## Ordered steps
1. Edit `gradle.properties`: update `minecraftVersion`, `fabricLoaderVersion`, `forgeVersion`,
`neoForgeVersion`, and the `irisVersion` suffix. Revisit `apiVersion` only if the bump drops
support for the oldest Minecraft line the plugin artifact still loads on.
2. Edit `gradle/libs.versions.toml`:
- `spigot` — the Spigot/Paper API pin used to compile against (`<mc>-R0.1-SNAPSHOT`).
- `fabricApi-*` — the ten Fabric API module versions, if the new MC requires different
Fabric API builds. Each module is versioned independently (`<version>+<build-hash>`).
The ten are `base`, `registrySync`, `resourceLoader`, `lifecycleEvents`, `commandApi`,
`eventsInteraction`, `networking`, `rendering`, `keyMapping`, `permission`. Every one of them
is bundled jar-in-jar and must be declared in `fabric.mod.json` `jars` — see step 7.
3. Edit `core/src/main/java/art/arcane/iris/core/nms/datapack/DataVersion.java` (manual, structural):
- Append a new enum constant `V<major>_<minor>("<mc>", <packFormat>, <DataFixer>::new)`.
- `packFormat` comes from https://minecraft.wiki/w/Pack_format.
- `getLatest()` returns the last enum constant, so append; do not reorder.
- Add a matching `IDataFixer` implementation under `core/src/main/java/art/arcane/iris/core/nms/datapack/`
if the datapack format changed.
4. Register the new Bukkit NMS binding module:
- `settings.gradle` — add `include(':adapters:bukkit:nms:v<major>_<minor>_R<rev>')`.
- `build.gradle` — add the binding to the `nmsBindings` map:
`v<major>_<minor>_R<rev>: '<spigot-nms-build-version>'` (e.g. `'26.2.build.25-alpha'`).
- Create the binding sources under `adapters/bukkit/nms/v<major>_<minor>_R<rev>/`.
5. Update loader version-range metadata (manual floors/ranges only; the `minecraft` ranges are
templated from `minecraftVersion` and need no edit):
- `adapters/fabric/src/main/resources/fabric.mod.json``minecraft` is `~${minecraftVersion}`
(auto). Update the `fabricloader` floor (currently `>=0.19.3`) if the loader minimum changes,
and the `jars` list if the bundled Fabric API modules change.
- `adapters/forge/src/main/resources/META-INF/mods.toml``minecraft` versionRange is
`[${minecraftVersion}]` (auto). Update `loaderVersion` (currently `[65,)`) and the `forge`
dependency versionRange (also `[65,)`) for the new Forge line. Both are hand-maintained.
- `adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml``minecraft` versionRange
is `[${minecraftVersion}]` (auto). `loaderVersion` (currently `[3,)`) is the javafml
specification version, not the NeoForge version, and rarely moves. The `neoforge` dependency
`versionRange` is **hardcoded** (currently `[26.2,)`) and is *not* templated from
`minecraftVersion` — hand-edit it on every bump or the mod will load on the wrong NeoForge
line.
6. Re-verify the mapping-coupled files. Six files name Mojang-mapped classes, fields, and method
descriptors directly. Nothing templates them, nothing fails fast at build time if a name moved,
and a stale entry surfaces as a silent no-op or a load-time crash. Check every one against the
new MC jar.
Access widener (Fabric) — `accessWidener v2 official`, so the names are Mojang-mapped:
- `adapters/fabric/src/main/resources/irisworldgen.accesswidener`
- `MinecraftServer.levels` `Ljava/util/Map;`
- `MinecraftServer.executor` `Ljava/util/concurrent/Executor;`
- `MinecraftServer.storageSource` `Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;`
- `PackRepository.sources` `Ljava/util/Set;` (accessible **and** mutable)
Verify: each field still exists with that exact descriptor. Loom fails the build on an
unresolvable AW entry, so a rename shows up as an AW error — read it, do not delete the line.
Access transformers (Forge and NeoForge) — must stay in sync with each other and with the AW:
- `adapters/forge/src/main/resources/META-INF/accesstransformer.cfg`
- `adapters/neoforge/src/main/resources/META-INF/accesstransformer.cfg`
- both: `public net.minecraft.server.MinecraftServer levels` / `executor` / `storageSource`
Verify: the three ATs match the first three AW entries. Note the ATs have no `PackRepository`
entry — Forge/NeoForge reach the pack sources through their own hooks, so do not add one
without a reason. Wired via `minecraft { accessTransformer.from(...) }` (Forge) and
`neoForge { accessTransformers.from(...) }` (NeoForge).
Mixin configs — three JSONs, eight mixin classes, all targeting Mojang-mapped members:
- `adapters/fabric/src/main/resources/irisworldgen.mixins.json`
(package `art.arcane.iris.fabric.mixin`, `compatibilityLevel` `JAVA_25`, Fabric only)
- `BlockItemMixin` -> `BlockItem.placeBlock`, `@At("RETURN")`
- `BlockMixin` -> `Block.getDrops(...)` with a **full descriptor**
(`BlockState, ServerLevel, BlockPos, BlockEntity, Entity, ItemInstance`) — the highest-churn
entry in the repo; the parameter list changes across MC versions
- `PackRepositoryMixin` -> `PackRepository.<init>`, `@At("RETURN")`
- `adapters/modded-common/src/main/resources/irisworldgen.entity.mixins.json`
(package `art.arcane.iris.modded.mixin`, `compatibilityLevel` `JAVA_21`, all three loaders)
- `EntityPersistenceMixin` -> `Entity.shouldBeSaved`
- `LivingEntityLootMixin` -> `LivingEntity.dropFromLootTable(ServerLevel, DamageSource, boolean)`
— full descriptor
- `MobAwarenessMixin` -> `Mob.serverAiStep`, injecting at a **field target**
(`Lnet/minecraft/world/entity/Mob;noActionTime:I`) — verify the field, not just the method
- `adapters/modded-common/src/main/resources/irisworldgen.client.mixins.json`
(package `art.arcane.iris.client.mixin`, client-only)
- `IrisWorldOpenFlowsMixin` -> `WorldOpenFlows.confirmWorldCreation` and
`WorldOpenFlows.openWorldCheckWorldStemCompatibility`
- `IrisWorldTypeEntryMixin` -> `WorldCreationUiState.WorldTypeEntry.describePreset`, plus a
`@Shadow` member — shadows break silently if the field is renamed
The client mixin *config* lives in `modded-common/src/main/resources` but the classes live in
`adapters/client-common/src/main/java/art/arcane/iris/client/mixin/`; the modded mixin classes
live in `adapters/modded-common/src/main/java/art/arcane/iris/modded/mixin/`. All three
adapters add both shared source dirs, so one edit hits every loader.
Registration differs per loader and each place must list the same configs:
- Fabric — `fabric.mod.json` `mixins` (all three; the client one gated on
`"environment": "client"`).
- NeoForge — `[[mixins]]` blocks in `neoforge.mods.toml` (entity + client).
- Forge — no toml entry. The jar manifest attribute `MixinConfigs` in
`adapters/forge/build.gradle` plus `--mixin.config` args on the `runClient`/`runServer`
configurations (entity + client). Adding a mixin config on Forge means editing the manifest
attribute *and* the run args.
`injectors.defaultRequire` is `1` in all three configs, so a mixin that no longer applies
fails the run instead of degrading quietly. Treat any "mixin apply failed" line as a bump
blocker, and run both `runClient` and `runServer` per loader — client-only mixins are not
exercised by a server run.
7. Reconcile the Fabric jar-in-jar list. `adapters/fabric/build.gradle` adds every Fabric API
module to the `jij` configuration, which the `shadowJar` copies into `META-INF/jars` with the
version stripped from the filename. The `jij` configuration is `transitive = false`, so the
bundled set is exactly the declared set, and `fabric.mod.json` `jars` must list exactly those
filenames. After changing the module list, confirm the jar agrees:
```
unzip -l "dist/Iris v<version> [Fabric] <mc>+<loader>.jar" | grep META-INF/jars
```
An entry in `jars` with no matching nested jar makes the loader refuse the mod; a nested jar
missing from `jars` is dead weight the loader never mounts.
8. Build and verify:
- `./gradlew :core:check`
- `./gradlew buildBukkit`
- `./gradlew buildFabric`
- `./gradlew buildForge`
- `./gradlew buildNeoforge`
## Derived automatically (do not hand-edit on a version bump)
- Bukkit plugin `api-version``adapters/bukkit/plugin/build.gradle` reads `apiVersion`.
- `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in
`core/build.gradle` from `minecraftVersion`; consumed by `Tasks.supportedVersions`.
- Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`.
- Dist/jar artifact names and the `com.mojang:minecraft` coordinate — composed from
`minecraftVersion` in the build scripts.
## Notes
- `build.gradle`, the adapter `build.gradle` files, and `settings.gradle` carry `.getOrElse('26.2')`
defensive defaults for the version properties. `gradle.properties` always overrides them, so a
bump does not require touching those fallbacks; refresh them only if the checked-in default should
track the current release.
- The Java literal `"26.2"` intentionally remains in `DataVersion.java` (structural enum constant),
`core/src/test/java/art/arcane/iris/core/nms/MinecraftVersionTest.java`, and
`core/src/test/java/art/arcane/iris/core/lifecycle/PaperLibBootstrapTest.java`. The test files use
MC version strings as parser fixtures, not as a version source; update them only when the version
string formats they exercise change.
-87
View File
@@ -1,87 +0,0 @@
# Iris Release Checklist
Manual release procedure. There is no release automation by design: every step below is run by
a person and verified by eye. Work top to bottom; do not skip the verify gates.
Before starting this publication procedure, complete the
[all-platform release readiness checklist](release-readiness-checklist.md). It contains the engineering
remediation, determinism, performance, CI, and full platform-acceptance gates. This checklist starts
only after those gates produce GO or an explicitly accepted GO-WARN decision.
Reference values below assume the current `gradle.properties`: `irisVersion=4.0.0-26.2`,
`minecraftVersion=26.2`, `fabricLoaderVersion=0.19.3`, `forgeVersion=26.2-65.0.4`,
`neoForgeVersion=26.2.0.12-beta`. For a Minecraft version bump, do `docs/mc-version-bump.md` first,
then start this checklist.
## a. Preflight
- [ ] Working tree clean on the exact commit you intend to tag (`git status` shows nothing to commit).
- [ ] CI is green on that commit. The `verify` job (`.github/workflows/ci.yml`) runs
core checks, Bukkit and shared modded tests, the SPI build, the deserialization probe, the modded
artifact-verifier tests, and guarded Fabric, Forge, and NeoForge artifact builds on JDK 25.
Do not release on a red or stale run.
- [ ] `MasterChangelog.MD` Iris section is coherent: one consolidated entry set, deduplicated, no
date-sliced headers, and it describes the current shipped state (not superseded intermediate work).
- [ ] Version fields correct in `gradle.properties`: `irisVersion` is the release version and its
trailing `-<mc>` suffix matches `minecraftVersion`. For a Minecraft bump, confirm every step in
`docs/mc-version-bump.md` is done (loader ranges, `DataVersion`, NMS binding).
- [ ] JDK 25 is the active toolchain locally (`java -version` reports 25).
## b. Build
- [ ] From the Iris project root: `./build-all.sh`. This disables local VolmLib substitution, uses the
immutable coordinate from `gradle.properties`, and serializes the all-platform build.
- [ ] `dist/` contains the four platform jars (exact names for this release):
- [ ] `Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar` (Bukkit/Paper/Purpur/Spigot/Folia plugin)
- [ ] `Iris v4.0.0-26.2 [Fabric] 26.2+0.19.3.jar`
- [ ] `Iris v4.0.0-26.2 [Forge] 26.2+65.0.4.jar`
- [ ] `Iris v4.0.0-26.2 [NeoForge] 26.2+26.2.0.12-beta.jar`
- Naming pattern: `Iris v<irisVersion> [<Platform>] <mc>[+<loaderDisplay>].jar`.
- [ ] The developer SPI jar is built by the same run at `spi/build/libs/iris-spi-4.0.0-26.2.jar`.
It is the platform-API artifact for downstream developers and is not copied into `dist/`; it is
not uploaded to the mod portals (see publish).
- [ ] Each mod jar bundles Iris core, SPI, and Iris-owned shaded libraries. LZ4, OSHI, JNA, and
JNA Platform are supplied by the Minecraft 26.2 runtime and must not be bundled or relocated.
## c. Verify (release gates)
- [ ] `:core:check` and `:probe:deserializationProbe` passed in CI on the tag commit (a. covers this).
- [ ] Golden-hash determinism VERIFY passes on all four platforms and matches the same hash:
- [ ] Bukkit plugin: `/iris developer goldenhash world=<world> radius=<radius> threads=<threads>`
(automatically verifies when the matching capture already exists)
- [ ] Fabric mod: `/iris goldenhash verify <radius> <threads>`
- [ ] Forge mod: `/iris goldenhash verify <radius> <threads>`
- [ ] NeoForge mod: `/iris goldenhash verify <radius> <threads>`
- The hash is interchangeable across platforms: all four MUST report identical output for the same
pack and seed. Any mismatch blocks the release.
- [ ] Live modded content-mod gate: on each loader, boot the mod jar alongside a real content mod
(e.g. Create) and generate an Iris world. Confirm no load-time rejection, no class-loader crash,
and that modded blocks/items/entities author and generate.
- [ ] Fabric + content mod
- [ ] Forge + content mod
- [ ] NeoForge + content mod
- [ ] Client-mod matrix: install the mod on the client (keybind `H` toggles the pregen HUD) and confirm:
- [ ] Modded server + modded client: HUD receives pregen progress over `irisworldgen:main`.
- [ ] Modded server + vanilla client: server generates normally; vanilla client is unaffected.
- [ ] Paper (Bukkit) server + modded client: HUD receives pregen progress over vanilla plugin messaging.
- [ ] Folia smoke: plugin loads and an Iris world generates on Folia.
- [ ] Non-Iris server + modded client: client is inert, no errors.
## d. Publish (all manual, no automation)
- [ ] Modrinth: upload the three mod jars and the plugin jar. Tag loaders `fabric` / `forge` /
`neoforge` on the mod files; mark the environment server + client; set game version 26.2.
- [ ] CurseForge: upload the three mod jars with the matching loader tags and game version 26.2.
- [ ] Existing plugin distribution channels: publish the plugin jar
(`Iris v4.0.0-26.2 [CraftBukkit] 26.2.jar`) where the plugin already ships.
- [ ] Sentry: add a release note / mark the release so incoming reports map to this version
(the mod version string is the Sentry release tag).
- [ ] Storepage / `listing.json` staleness review: check the listing copy for pre-4.0 content
(Bukkit-only framing, old feature lists, screenshots). Flag anything stale for update before or
right after launch. (Review only; this checklist does not change store copy.)
## e. Post
- [ ] Tag the release commit (`v<irisVersion>`) and push the tag. Archive the already verified `dist/`
bundle with the release record; no tag-triggered bundle automation is configured.
- [ ] Announce the release on the community channels once the portals show the new files live.