mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
rmd
This commit is contained in:
@@ -6,9 +6,9 @@ studio authoring workflow. The same engine runs as a Bukkit-family plugin and as
|
||||
or NeoForge server mod. Cross-platform generation targets deterministic parity for identical
|
||||
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2.
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](docs/00%20-%20Overview.md) **|** [Git](https://github.com/IrisDimensions)
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
Authoritative docs live in [`docs/`](docs/00%20-%20Overview.md) (flat numbered files). Hosted external docs are not authority.
|
||||
Authoritative docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository, which is the source for the hosted wiki.
|
||||
|
||||
Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
@@ -50,8 +50,8 @@ is identical on every platform.
|
||||
| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's |
|
||||
|
||||
With `importedFeatures` off - the default - chunk output is byte-for-byte what Iris has always
|
||||
produced. See [`docs/94 - API - Modded.md`](docs/94%20-%20API%20-%20Modded.md) and
|
||||
[`docs/01 - Installation & Platforms.md`](docs/01%20-%20Installation%20%26%20Platforms.md) for the full control reference, including
|
||||
produced. See [94 - API - Modded](https://github.com/VolmitSoftware/docs/blob/master/iris/94-api-modded.md) and
|
||||
[01 - Installation & Platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) for the full control reference, including
|
||||
which `pointed_dripstone` keys the 26.2 `speleothem` rename does and does not affect.
|
||||
|
||||
Independently of that flag, Iris custom biomes now inherit the biome tags of their vanilla
|
||||
@@ -270,12 +270,12 @@ composite cycle. The build and release paths do not need it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full product docs are under [`docs/`](docs/00%20-%20Overview.md). Start with:
|
||||
Full product docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository. Start with:
|
||||
|
||||
- [Overview and index](docs/00%20-%20Overview.md)
|
||||
- [Installation and platforms](docs/01%20-%20Installation%20%26%20Platforms.md)
|
||||
- [Getting started](docs/02%20-%20Getting%20Started.md)
|
||||
- [Structures overview](docs/18%20-%20Structures%20Overview.md)
|
||||
- [API — Getting Started](docs/90%20-%20API%20-%20Getting%20Started.md)
|
||||
- [Maintainer — MC version bump](docs/85%20-%20Maintainer%20-%20MC%20Version%20Bump.md)
|
||||
- [Maintainer — release checklist](docs/86%20-%20Maintainer%20-%20Release%20Checklist.md)
|
||||
- [Overview and index](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md)
|
||||
- [Installation and platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md)
|
||||
- [Getting started](https://github.com/VolmitSoftware/docs/blob/master/iris/02-getting-started.md)
|
||||
- [Structures overview](https://github.com/VolmitSoftware/docs/blob/master/iris/18-structures-overview.md)
|
||||
- [API — Getting Started](https://github.com/VolmitSoftware/docs/blob/master/iris/90-api-getting-started.md)
|
||||
- [Maintainer — MC version bump](https://github.com/VolmitSoftware/docs/blob/master/iris/85-maintainer-mc-version-bump.md)
|
||||
- [Maintainer — release checklist](https://github.com/VolmitSoftware/docs/blob/master/iris/86-maintainer-release-checklist.md)
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
# 00 - Overview
|
||||
|
||||
Iris is a world generation engine for Minecraft: it replaces the vanilla chunk generator with terrain, biomes, caves, structures, objects, and entities built from editable JSON packs. The same engine ships as a Bukkit-family plugin and as a Fabric, Forge, or NeoForge server mod, and generates identical chunks on all four when artifacts, pack bytes, seed, and area match. This page is the map of the documentation set: read it to find the page you actually need, then leave. This branch targets Minecraft 26.2 and requires Java 25 everywhere.
|
||||
|
||||
## Who this documentation is for
|
||||
|
||||
There are three audiences and the numbering reflects them. Pages `00`–`33` are for **server operators** installing Iris and **pack authors** writing dimensions, in roughly the order a newcomer needs them. Pages `85`–`87` are **maintainer** checklists for cutting a release. Pages `90`–`94` are for **Java developers** consuming the Iris API from their own plugin or mod.
|
||||
|
||||
Reading the set front to back is a waste of time. Pick the outcome you want from the table below and follow only that row.
|
||||
|
||||
## Choose a learning path
|
||||
|
||||
| You want to | Read, in order |
|
||||
|---|---|
|
||||
| Get Iris running and make one world | `01 - Installation & Platforms.md` → `02 - Getting Started.md` → `31 - Operator Runbooks.md` |
|
||||
| Write a pack from scratch | `05 - Concepts & Pack Layout.md` → `10 - Studio & VSCode Schemas.md` → `26 - Example - Minimal Dimension.md` |
|
||||
| Shape terrain and lay out biomes | `11 - Dimensions.md` → `12 - Regions.md` → `13 - Biomes.md` → `14 - Generators & Noise.md` |
|
||||
| Add caves, surface detail, and vegetation | `15 - Caves & Carving.md` → `16 - Surfaces, Decorators & Deposits.md` → `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md` |
|
||||
| Place a building or structure | `18 - Structures Overview.md` first to pick an approach, then `19 - Objects.md` + `20 - Object Placement.md` for single `.iob` objects, `21 - Jigsaw Structures.md` for multi-piece Iris structures, or `22 - Native Structures & Datapacks.md` for vanilla/datapack ones |
|
||||
| Ship a pack to a production server | `25 - Pack Management.md` → `06 - Worlds & Lifecycle.md` → `07 - Pregeneration.md` → `31 - Operator Runbooks.md` |
|
||||
| Make another plugin or mod work with Iris | `28 - Integrations.md` → `30 - Platform Differences.md`; if you're writing Java against Iris, start at `90 - API - Getting Started.md` |
|
||||
|
||||
Each tutorial page ends with something you can observe — a world that loads, a chunk that generates, a hotload that lands. Confirm that before moving to the next page. Iris failures cascade misleadingly: a biome key you typo'd in step two shows up three systems later as a cave, decorator, or structure that "doesn't work," and you'll debug the wrong thing.
|
||||
|
||||
## Platforms
|
||||
|
||||
One plugin jar covers the whole Bukkit family; each mod loader gets its own jar. Pick by what your server runs, not by feature set — the generator is the same code on all of them.
|
||||
|
||||
| Platform | Artifact | Minecraft | What's different |
|
||||
|---|---|---|---|
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 – 26.2 | Nothing; this is the reference plugin target |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.1.2 – 26.2 | Nothing for generation. Paper-only APIs degrade gracefully |
|
||||
| Folia | plugin jar | 26.1.2 – 26.2 | Region-safe scheduling, and `/iris create` cannot build a live world at runtime — it stages the world and requires a restart. See `01 - Installation & Platforms.md` and `06 - Worlds & Lifecycle.md` |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen plus an optional client HUD; needs Fabric Loader 0.19.3+ and Java 25 |
|
||||
| Forge | mod jar | 26.2 | Same; needs Forge 65.x (built against 26.2-65.0.4) |
|
||||
| NeoForge | mod jar | 26.2 | Same; needs NeoForge 26.2.x (built against 26.2.0.12-beta) |
|
||||
|
||||
The plugin registers as `Iris` with command `/iris` (aliases `/ir`, `/irs`), `folia-supported: true`, `load: STARTUP`, and `api-version: 26.1` — the low api-version is deliberate so one jar loads on both 26.1.2 and 26.2. Its descriptor declares two permissions, `iris.all` (the whole command tree) and `iris.treefeller` (survival tree felling only), both defaulting to op. Optional soft-dependencies load before Iris; Multiverse-Core is ordered *after* Iris so Multiverse sees Iris generators once they exist. Full list in `01 - Installation & Platforms.md`.
|
||||
|
||||
All three mod loaders use mod id `irisworldgen`, and register `/ir` and `/irs` as command redirects the same way the plugin does.
|
||||
|
||||
## Feature map
|
||||
|
||||
Every feature of Iris is documented on exactly one page. Find the subject, go there.
|
||||
|
||||
| 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.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` |
|
||||
|
||||
## Content model
|
||||
|
||||
Six terms carry most of the documentation. Learn them here and the rest of the set reads much faster.
|
||||
|
||||
| Term | What it is |
|
||||
|---|---|
|
||||
| Pack | A folder of JSON and `.iob` files under `packs/<key>/`. It needs at least one `dimensions/*.json` to count as a pack at all — a folder without one is treated as absent and will be re-downloaded |
|
||||
| Dimension | The root config for one world type: height range, generation modes, which regions it uses, what native content it imports. One dimension file is one world's ruleset |
|
||||
| Region / biome / generator | The authoring units under a dimension. Regions divide the map, biomes fill regions, generators produce the actual heightmap noise |
|
||||
| Object / structure | Placed content. An object is a single saved build (`.iob`); a structure is either an Iris jigsaw of several objects, or a vanilla/datapack/mod structure Iris allows through |
|
||||
| Studio | A throwaway authoring world that reads the live pack folder and hotloads your edits into new chunks. Deleted when you close it, and any leftovers are purged at startup |
|
||||
| World pack snapshot | A production world copies the pack into `<world>/iris/pack` at creation and reads only that copy forever after. This is the single most common source of "my edits did nothing" — see `05 - Concepts & Pack Layout.md` |
|
||||
|
||||
## Project layout
|
||||
|
||||
Relevant if you're building Iris or filing a bug against a specific subsystem.
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `core/` | The engine: pack loader, generation pipeline, pregen, studio services, localization catalogs. Pure JVM, no platform types |
|
||||
| `core/agent/` | Java instrumentation agent (premain/agent-class jar) consumed by the core build |
|
||||
| `spi/` | The platform contract (`IrisPlatform`, protocol types) that lets `core/` stay platform-free |
|
||||
| `adapters/bukkit/plugin/` | Bukkit plugin main class, the Director command tree, the public Bukkit API, and both plugin descriptors |
|
||||
| `adapters/bukkit/nms/v26_2_R1/` | NMS bindings for the current Minecraft line |
|
||||
| `adapters/minecraft-common/` | Source shared by the Bukkit and mod-loader adapters |
|
||||
| `adapters/modded-common/` | Source shared by Fabric, Forge, and NeoForge: worldgen hooks, Brigadier commands, services |
|
||||
| `adapters/client-common/` | Client-dist source: HUD, keybinds, world-type screens |
|
||||
| `adapters/fabric/`, `adapters/forge/`, `adapters/neoforge/` | The three loader builds. Each is a standalone Gradle build with its own `settings.gradle` |
|
||||
| `probe/` | Offline tooling and a stub platform for running the engine without a server |
|
||||
| `buildSrc/` | Gradle helpers: artifact verification, NMS bindings, API generation |
|
||||
| `dist/` | Where `buildAllToOut` drops the finished consumer jars |
|
||||
| `docs/` | This documentation set, which is the authority over any hosted copy |
|
||||
|
||||
`minecraft-common`, `modded-common`, and `client-common` are source trees only — they have no `build.gradle` and are not Gradle projects. The loader builds pull them in as extra source directories.
|
||||
|
||||
## Developer build check
|
||||
|
||||
Set `JAVA_HOME` to a JDK 25, then from the Iris root:
|
||||
|
||||
```text
|
||||
java -version
|
||||
./gradlew build
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
The check passes when `build` finishes with no failed tasks and `buildAllToOut` leaves one current jar per platform in `dist/`. `build` already runs the tests, so use `./gradlew test` only when you want to rerun tests without reassembling every artifact.
|
||||
|
||||
At version `4.0.0-26.2` the four jars are named like this — the CraftBukkit one carries the supported Minecraft *range*, the loader jars carry `<mc>+<loader>`:
|
||||
|
||||
```text
|
||||
Iris v4.0.0-26.2 [CraftBukkit] 26.1.2-26.2.jar
|
||||
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
|
||||
```
|
||||
|
||||
Build one platform at a time with `./gradlew buildBukkit`, `buildFabric`, `buildForge`, or `buildNeoforge`. The SPI jar comes from `./gradlew :spi:jar` and lands in `spi/build/libs/`.
|
||||
|
||||
To iterate on a loader adapter, drive it from its own project root — these are separate Gradle builds, so a root-level invocation won't reach them:
|
||||
|
||||
```text
|
||||
./gradlew -p adapters/fabric runServer
|
||||
./gradlew -p adapters/forge runServer
|
||||
./gradlew -p adapters/neoforge runServer
|
||||
```
|
||||
|
||||
`-PincludeModdedAdapters=true` surfaces those builds in the root composite for IDE import. It's off by default because each adapter includes the root build back for `core`/`spi` substitution, which closes a composite cycle.
|
||||
|
||||
A passing Bukkit jar proves nothing about the loaders. Loom, ForgeGradle, and ModDevGradle each fail in their own ways, so if a loader build breaks while the root build is green, rerun that adapter from its own root and fix its first error rather than re-running the root build.
|
||||
|
||||
The current version lives in `gradle.properties` as `irisVersion=4.0.0-26.2`.
|
||||
|
||||
Next: install Iris with `01 - Installation & Platforms.md`.
|
||||
@@ -1,247 +0,0 @@
|
||||
# 01 - Installation & Platforms
|
||||
|
||||
Iris ships as one Bukkit-family plugin jar and three self-contained mod jars (Fabric, Forge, NeoForge). This page gets the right artifact onto your server, tells you how to prove the install actually worked, and documents where Iris puts its files on each platform. Java 25 is required everywhere. On first boot Iris downloads the managed `overworld` and `underworld` beta packs if they aren't already present.
|
||||
|
||||
Read this before `02 - Getting Started.md`. If Iris is already installed and you just want a world, skip ahead.
|
||||
|
||||
## What a good install looks like
|
||||
|
||||
Whichever path you take, you're done when all three of these are true:
|
||||
|
||||
1. Iris reached its enabled/ready state with no exception in the startup log.
|
||||
2. The data directory has a `settings.json` and loadable `packs/overworld/` and `packs/underworld/` directories.
|
||||
3. `/iris` prints help from the server console.
|
||||
|
||||
On a modded client the Iris keybind category showing up is a nice extra signal, but it proves the client mod loaded — not that the server can generate chunks. Always check server-side.
|
||||
|
||||
Keep the old jar and the entire Iris data directory until the new build passes these checks. Swapping the binary does **not** update pack snapshots already copied into existing worlds; that's a separate operation covered in `06 - Worlds & Lifecycle.md`.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Value |
|
||||
|---|---|
|
||||
| Java | 25. The mod jars declare `java >= 25` and refuse to load on anything older |
|
||||
| Minecraft (plugin) | 26.1.2 – 26.2. One jar covers both; `api-version` is pinned to 26.1 so it loads on the older line too |
|
||||
| Minecraft (mod) | 26.2 only |
|
||||
| Fabric Loader | 0.19.3+ |
|
||||
| Forge | 65.x (built and tested against 26.2-65.0.4) |
|
||||
| NeoForge | 26.2.x (built and tested against 26.2.0.12-beta) |
|
||||
| Network | Outbound HTTPS to `github.com` on first boot, to fetch the IrisDimensions Overworld and Underworld beta release assets |
|
||||
|
||||
Before replacing an existing installation:
|
||||
|
||||
1. Run `java -version` **on the server** and confirm it reports 25. Having Java 25 installed somewhere on the box is not the same as the server process using it.
|
||||
2. Match the jar label to the platform and Minecraft version you're running.
|
||||
3. Stop the server cleanly.
|
||||
4. Back up the Iris jar/mod, the Iris data directory, and every Iris world you intend to keep.
|
||||
|
||||
Never put two Iris platform jars in the same `plugins/` or `mods/` folder. That fails in confusing ways rather than picking a winner.
|
||||
|
||||
## Plugin install (Paper / Purpur / Leaf / Canvas / Folia / Spigot)
|
||||
|
||||
1. Drop the CraftBukkit-labelled plugin jar into `plugins/`.
|
||||
2. Start the server. Iris loads at `STARTUP`, before worlds are created, because it has to register generators first.
|
||||
3. First boot writes `plugins/Iris/settings.json` with defaults if it's absent, and provisions `overworld` and `underworld` into `plugins/Iris/packs/` from their IrisDimensions `beta` release ZIPs when missing.
|
||||
|
||||
Then verify from the server console:
|
||||
|
||||
```text
|
||||
/iris version
|
||||
/iris pack validate pack=overworld
|
||||
/iris pack validate pack=underworld
|
||||
```
|
||||
|
||||
`/iris version` prints exactly one line — `Iris v<version> by Volmit Software`. That's the whole output; it does not report platform or Minecraft version, so use it only as a "the command tree is alive" check. Each `pack validate` must resolve the downloaded pack and finish with no blocking errors.
|
||||
|
||||
Pass an explicit pack name. `/iris pack validate` with no argument fails with a missing-argument error rather than validating everything, because the parameter is required. To validate every installed pack, pass the key with an empty value: `/iris pack validate pack=`.
|
||||
|
||||
A command responding is not proof the generator can produce chunks. Finish with the disposable-world walkthrough in `02 - Getting Started.md`.
|
||||
|
||||
### Startup validation gates login
|
||||
|
||||
Iris blocks player login until external datapack validation and dimension-pack validation both complete. The kick message names the reason and tells you to check the console.
|
||||
|
||||
The two gates behave differently. A failed or restart-pending **external datapack** state keeps login locked and blocks all Iris world creation until you fix it and restart — Iris tells you when a restart is what's required. Unchanged, already-validated datapacks and packs reuse their persisted results, so this costs nothing on a normal boot. A **dimension pack** with blocking errors does *not* lock the server: that one pack is refused for world and Studio creation, an error listing the reasons is printed at startup, and every healthy pack stays usable.
|
||||
|
||||
### Permissions
|
||||
|
||||
The descriptor declares two permissions, both defaulting to op:
|
||||
|
||||
| Permission | Grants |
|
||||
|---|---|
|
||||
| `iris.all` | The entire `/iris` command tree — worlds, studio, pregen, packs, developer tools |
|
||||
| `iris.treefeller` | Survival tree felling with an axe. Nothing else |
|
||||
|
||||
There is exactly one permission check, at the command root, against `iris.all`. Subcommands do not derive their own permission nodes — there is no `iris.all.pregen`. If a non-op needs any Iris command, they need `iris.all`, and that is all of it. See `04 - Commands & Permissions.md`.
|
||||
|
||||
Command root is `/iris`, aliases `/ir` and `/irs`.
|
||||
|
||||
### Soft dependencies
|
||||
|
||||
None of these are bundled or required. When present they load before Iris so Iris can see them: PlaceholderAPI, CraftEngine, Nexo, ItemsAdder, SCore, ExecutableItems, MythicLib, MMOItems, eco, EcoItems, MythicMobs, MythicCrucible, KGenerators, WorldEdit. Multiverse-Core is deliberately ordered *after* Iris so that Multiverse sees Iris generators once they're registered. Integration details in `28 - Integrations.md`.
|
||||
|
||||
### Folia
|
||||
|
||||
`folia-supported: true`, and engine work uses region-safe scheduling. The one behavioral difference that matters at install time: `/iris create` cannot build a live world at runtime on Folia. Instead it stages the world files, installs the pack snapshot, registers the world in `bukkit.yml`, and prints a message telling you to restart. After the restart the world generates and loads on its own from that `bukkit.yml` entry — you do not need to run `/iris load`. See `06 - Worlds & Lifecycle.md`.
|
||||
|
||||
### If the first-boot download fails
|
||||
|
||||
Fix network access and restart. Do not create an empty folder named `overworld` to silence the error: a folder without a `dimensions/*.json` inside it is treated as absent (and re-downloaded), and a partial pack is not a usable dimension. To install by hand later, use `/iris download overworld` and `/iris download underworld`.
|
||||
|
||||
Before you create a world you care about, run the Bukkit fresh-install runbook in `31 - Operator Runbooks.md`.
|
||||
|
||||
## Mod install (Fabric / Forge / NeoForge)
|
||||
|
||||
1. Drop the matching mod jar into `mods/`.
|
||||
2. Start the dedicated server, or a client if you want singleplayer.
|
||||
3. The jar is self-contained — engine, SPI, and the required Fabric API modules are bundled. Mod id is `irisworldgen` on all three loaders.
|
||||
4. On first boot, if `autoDownloadDefaultPack` is true in `config/irisworldgen/modded.json` (it is by default), a daemon thread installs the managed `overworld` and `underworld` beta packs when missing, then a configured non-managed `defaultPack` if you've set one. Only after that does it write the forced worldgen datapack.
|
||||
|
||||
Verify server-side:
|
||||
|
||||
```text
|
||||
/iris version
|
||||
/iris pack validate overworld
|
||||
/iris pack validate underworld
|
||||
```
|
||||
|
||||
Modded `/iris version` prints more than the Bukkit one — mod version, platform, Minecraft version, and the count of loaded Iris dimensions. The install passes when that line looks right, both managed pack directories contain their primary dimension JSON, and validation reports no blocking errors.
|
||||
|
||||
### Restart once after installing a pack
|
||||
|
||||
This is the modded-specific gotcha. Packs register their custom dimension types (which set the world height range) and their custom biomes through the forced datapack, and that datapack is read when the server builds its registries at start. A pack installed during *this* boot may land after registries are already built.
|
||||
|
||||
So: **restart once after any pack is installed, before creating a world with it.** Worlds created before that restart run with fallback heights and will not have the pack's real height range or custom biomes. If a pack or its generated dimension-type datapack was installed during the current boot, restart before continuing.
|
||||
|
||||
### Singleplayer on a modded client
|
||||
|
||||
Installed Iris packs show up as selectable World Types on the Create New World screen, named `IRIS:<Pack>` (or `IRIS:<Pack> / <Dimension>` when a pack exposes more than one dimension). The integrated server runs the same engine as a dedicated one.
|
||||
|
||||
### Client HUD
|
||||
|
||||
Installing the mod jar on a client adds a pregeneration HUD showing a progress bar, chunks done and total, percent, chunks per second, and ETA, turning yellow while paused. `H` toggles it. The keybind category is "Iris" and also holds `M` (Iris Vision Map) and `J` (Iris What overlay); all three are rebindable. Details in `29 - Client HUD & Protocol.md`.
|
||||
|
||||
The HUD talks to both 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 a non-Iris server the client mod is inert.
|
||||
|
||||
## Data directories
|
||||
|
||||
### Plugin
|
||||
|
||||
| Path | What lives there |
|
||||
|---|---|
|
||||
| `plugins/Iris/settings.json` | Engine settings. Written with defaults if absent, and rewritten on every read, so keys added by a new Iris version appear automatically with their defaults and your edits survive |
|
||||
| `plugins/Iris/packs/<key>/` | Installed packs. This is the live tree the Studio reads and edits |
|
||||
| `plugins/Iris/bootstrap/` | First-boot provisioning marker (`provisioned.properties`) recording what was installed and against which compiler identity |
|
||||
| `plugins/Iris/datapacks/` | External datapack imports pulled from Modrinth by `/iris datapack`, plus a `staging/` subfolder used mid-download |
|
||||
| `plugins/Iris/languages/overrides/<locale>.json` | Optional server message overrides. See `08 - Localization.md` |
|
||||
| `<level-root>/datapacks/iris/` | The aggregate worldgen datapack Iris compiles from your installed packs. Iris owns this; do not hand-edit it |
|
||||
| `<level-root>/dimensions/<namespace>/<name>/` | Storage for a managed Iris world. Namespace is `iris` for worlds Iris creates |
|
||||
| `<world>/iris/pack/` | The per-world pack **snapshot**. A production engine reads only this copy, never `plugins/Iris/packs/` |
|
||||
|
||||
`<level-root>` is the server's level directory — the folder named by `level-name` in `server.properties`.
|
||||
|
||||
That last row is worth internalizing early: editing `plugins/Iris/packs/overworld/` has no effect on a world that already exists, because that world froze a copy of the pack at creation time. See `05 - Concepts & Pack Layout.md`.
|
||||
|
||||
### Mod
|
||||
|
||||
Paths are relative to the game instance's `config/` directory.
|
||||
|
||||
| Path | What lives there |
|
||||
|---|---|
|
||||
| `config/irisworldgen/packs/<pack>/` | Installed packs. A pack counts as installed when `dimensions/<dimension>.json` exists |
|
||||
| `config/irisworldgen/generated/datapack/iris/` | The generated forced datapack (datapack id `iris_worldgen`), plus a hash sidecar used to detect staleness. Iris owns this; do not edit it |
|
||||
| `config/irisworldgen/modded.json` | Mod-side config: default pack, auto-download, primary-world routing, main-world override |
|
||||
| `config/iris/` | Engine data directory — `settings.json` and per-world engine state |
|
||||
|
||||
Two different roots, and mixing them up is a common mistake. Packs, the generated datapack, and mod config live under `config/irisworldgen/`. The shared engine's own data lives under `config/iris/`.
|
||||
|
||||
### `modded.json`
|
||||
|
||||
Written with these defaults on first read. If the file is unparseable Iris logs an error and falls back to defaults without rewriting it, so a syntax error is silent apart from the log line — check the log if a setting seems ignored.
|
||||
|
||||
| Key | Default | What it does |
|
||||
|---|---|---|
|
||||
| `defaultPack` | `overworld` | The pack `/iris create` uses when you don't name one, and the extra pack auto-download will fetch beyond the two managed betas |
|
||||
| `autoDownloadDefaultPack` | `true` | Whether boot installs missing packs at all. Set false for air-gapped servers where you place pack folders by hand |
|
||||
| `primaryWorld` | `""` | Dimension id players get routed into. Empty means no routing. Set by `/iris world replace-overworld` rather than by hand |
|
||||
| `routePlayersToPrimaryWorld` | `true` | Whether the routing above actually happens. Set false to keep a primary world configured but stop moving players into it |
|
||||
| `mainWorldPack` | `""` | Pack whose generator replaces the vanilla main world. Empty means the vanilla overworld is untouched |
|
||||
| `mainWorldSeed` | `0` | Seed for that main-world override |
|
||||
| `mainWorldAutoRestart` | `false` | When true, `/iris world mainworld` halts the server immediately so the override takes effect. Leave false unless you have a supervisor that restarts the process |
|
||||
|
||||
Only `/iris world replace-overworld`, `/iris world disable|delete` on the primary, and `/iris world mainworld` rewrite this file at runtime.
|
||||
|
||||
## Settings that affect install and first world
|
||||
|
||||
`IrisSettings` is shared across every platform; only the file location differs. The three keys most likely to matter before your first world:
|
||||
|
||||
| Key path | Default | When you'd change it |
|
||||
|---|---|---|
|
||||
| `generator.defaultWorldType` | `overworld` | Bukkit `/iris create` resolves `type=default` through this. Point it at your own pack so plain `/iris create <name>` produces your world instead of the stock overworld |
|
||||
| `general.language` | `en_US` | Server-side message locale. See `08 - Localization.md` |
|
||||
| `studio.openVSCode` | `true` | Set false on a headless box so `/iris studio vscode` writes the workspace file without trying to launch an editor |
|
||||
|
||||
Full key list: `03 - Configuration.md`.
|
||||
|
||||
## First-boot pack download
|
||||
|
||||
| Platform | What happens |
|
||||
|---|---|
|
||||
| Plugin | `DefaultPackBootstrapProvisioner` installs the Overworld and Underworld beta release assets into `packs/overworld` and `packs/underworld` independently, then compiles the aggregate datapack once |
|
||||
| Mod | If `autoDownloadDefaultPack` is on, a daemon thread installs both managed beta packs plus any distinct configured `defaultPack` into `config/irisworldgen/packs`, then regenerates the forced datapack |
|
||||
|
||||
Manual install is `/iris download <pack>` (alias `dl`). The two managed packs are special-cased: `overworld` and `underworld` always come from their pinned beta release assets and **ignore the `branch` argument entirely**. Any other pack resolves as `IrisDimensions/<pack>/<branch>`, with `branch` defaulting to `stable`.
|
||||
|
||||
The managed-pack match is case-sensitive, so `/iris download Overworld` misses the special case and tries `IrisDimensions/Overworld/stable` instead. Use lowercase.
|
||||
|
||||
One more branch inconsistency to be aware of on modded: when `/iris create` auto-downloads a pack that isn't installed, and when boot fetches a non-managed `defaultPack`, both use the `master` branch — not `stable`. Only the explicit `/iris download` command defaults to `stable`. Pin the branch explicitly if it matters. See `25 - Pack Management.md`.
|
||||
|
||||
## When the install goes wrong
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `/iris version` does nothing | Wrong directory, wrong platform jar, a duplicate Iris jar, Java below 25, or an exception during enable | Stop the server, leave exactly one matching artifact in place, confirm Java 25, then fix the **first** Iris exception in the startup log — later ones are usually fallout |
|
||||
| `settings.json` exists but a managed pack is missing | The beta download failed or is still running | Restore outbound HTTPS or drop in the complete release pack, then restart. Do not create an empty pack folder to paper over it |
|
||||
| Players are kicked at login with an Iris message | Startup validation hasn't passed | Read the reason in the kick text and the console. External datapack failures lock login; fix the datapack state and restart |
|
||||
| Pack validates, but modded heights and biomes are wrong | The forced datapack was generated after registries had already loaded | Restart once with the pack already on disk, then create a fresh disposable world to confirm |
|
||||
| A non-op can't run any Iris command | `iris.all` isn't granted | Grant `iris.all`. `iris.treefeller` only covers survival tree felling and grants no commands |
|
||||
| Client HUD missing but server commands work | Client mod absent, keybind unbound, or capability not negotiated | Install the matching client mod, reconnect, check the Iris keybind category. Server-side generation never depends on the client HUD |
|
||||
| An existing world ignores a newly installed pack | The world is reading its frozen snapshot | Use the explicit snapshot update or create a new world — `06 - Worlds & Lifecycle.md` and `25 - Pack Management.md` |
|
||||
|
||||
## Native worldgen over Iris terrain
|
||||
|
||||
Iris replaces the chunk generator outright, so vanilla and mod worldgen only runs where Iris explicitly runs it. This behaves the same on every platform.
|
||||
|
||||
| Vanilla / mod worldgen | Runs over Iris terrain? | Control |
|
||||
|---|---|---|
|
||||
| Structures (vanilla, datapack, mod) | Yes, on by default | Deny families with `importedStructures.disabled`, or one complete key with `importedStructures.disabledExact` |
|
||||
| Placed features: ores, trees, plants, springs, geodes | Yes, but **off by default** | `importedFeatures.enabled` per dimension, with per-step (`steps` / `disabledSteps`) and per-key (`disabled`) filters |
|
||||
| Carvers (caves, canyons, mod carvers) | Never | There's no `NoiseGeneratorSettings` for a carver to sample against. Use the pack's `caves` and `carvings` instead |
|
||||
| Surface builders and surface rules | Never | Iris builds surfaces from pack palettes |
|
||||
| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter`, or `biomeSkyScatter` target | Iris always picks the biome from the pack |
|
||||
| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's |
|
||||
|
||||
With `importedFeatures` off — the default — chunk output is pure Iris. The full control reference is `94 - API - Modded.md`, which also applies conceptually on Bukkit for the imported native stages.
|
||||
|
||||
Separately from that flag, Iris custom biomes inherit the biome tags of their vanilla derivative on every platform. Tag-driven content such as `#minecraft:is_overworld` and mod spawn rules therefore applies to Iris custom biomes without any extra configuration.
|
||||
|
||||
## Build artifacts
|
||||
|
||||
If you're building rather than downloading, from the repo root with JDK 25:
|
||||
|
||||
```text
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
Four jars land in `dist/`. The CraftBukkit jar's version token is the supported Minecraft *range*; the loader jars use `<mc>+<loader>`:
|
||||
|
||||
| Pattern | Platform |
|
||||
|---|---|
|
||||
| `Iris v<version> [CraftBukkit] 26.1.2-26.2.jar` | Plugin (whole Bukkit family, including Folia) |
|
||||
| `Iris v<version> [Fabric] 26.2+<loader>.jar` | Fabric |
|
||||
| `Iris v<version> [Forge] 26.2+<loader>.jar` | Forge |
|
||||
| `Iris v<version> [NeoForge] 26.2+<loader>.jar` | NeoForge |
|
||||
|
||||
Per-platform build tasks and the developer build gate are in `00 - Overview.md`.
|
||||
|
||||
Next: create a world and open the Studio in `02 - Getting Started.md`. Every settings key is in `03 - Configuration.md`.
|
||||
@@ -1,307 +0,0 @@
|
||||
# 02 - Getting Started
|
||||
|
||||
This page takes you from a working Iris install to a world you can stand in, a small pregenerated area, and an open Studio session for editing packs. It's written for whoever is running the server, and assumes nothing about pack authoring. Command syntax differs between the plugin and the mods, so each step gives both forms.
|
||||
|
||||
Full command trees and permissions: `04 - Commands & Permissions.md`. World lifecycle detail: `06 - Worlds & Lifecycle.md`. Studio detail: `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
## What you'll end up with
|
||||
|
||||
One disposable Iris world built from the `overworld` pack, entered and generating chunks, with roughly a 45×45-chunk area pregenerated, plus a separate Studio session pointed at the live pack. Use seed `1337` throughout. Changing seeds while you're still diagnosing something makes every comparison meaningless.
|
||||
|
||||
Work through the sections in order and confirm each one before moving on. Confirm the world loaded before teleporting; confirm ordinary chunks generate around you before starting a pregen; confirm the Studio world is genuinely separate from your production world before editing files. Skipping a check doesn't save time here, because Iris failures surface late and in the wrong place.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Iris installed and verified per `01 - Installation & Platforms.md`
|
||||
- A Java 25 server or mod instance running
|
||||
- Operator access on Bukkit (the `iris.all` permission), or permission level 2 / gamemaster on modded for anything that mutates state
|
||||
- The managed `overworld` and `underworld` packs present, or your own pack installed under the platform's packs directory
|
||||
|
||||
## The one syntax rule that trips everyone up
|
||||
|
||||
On the plugin, Iris uses the Director command framework, and it has a hard rule: **only required parameters accept a bare positional value. Every optional parameter must be given as `key=value`.** A leftover positional token isn't ignored — it's an error, and the command fails.
|
||||
|
||||
```text
|
||||
/iris create myworld type=overworld seed=1337 correct
|
||||
/iris create myworld overworld 1337 fails: unexpected argument
|
||||
```
|
||||
|
||||
Parameters marked contextual (like `world` on pregen, which normally comes from where you're standing) also never take a positional, so name them with `key=` when you need to override them.
|
||||
|
||||
Modded is Brigadier and works the way you'd expect: everything is positional, in order, and pregen options are literal flag words.
|
||||
|
||||
| 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` |
|
||||
|
||||
Director also matches command and parameter names fuzzily, so shortenings and near-misses often resolve. That's convenient but don't rely on it in scripts — write the real names.
|
||||
|
||||
## 1. Create a world
|
||||
|
||||
### Plugin
|
||||
|
||||
```text
|
||||
/iris create <name> [type=…] [seed=…] [main=true|false] [overwrite=true|false]
|
||||
```
|
||||
|
||||
| Parameter | Aliases | Default | What it does |
|
||||
|---|---|---|---|
|
||||
| `name` | `world-name` | required | The world name. The only parameter that takes a positional value |
|
||||
| `type` | `dimension`, `pack` | `default` | Which pack/dimension to generate. The literal `default` is resolved at runtime through `generator.defaultWorldType` (stock value `overworld`), so it follows your config rather than being hardcoded |
|
||||
| `seed` | — | `1337` | World seed |
|
||||
| `main` | `main-world` | `false` | Promote this world to the server's main world. See below — it does not take effect until a restart |
|
||||
| `overwrite` | `force` | `false` | Replace an existing world in place instead of creating a new one. This is a staged, restart-to-publish operation, not something to reach for casually |
|
||||
|
||||
The command itself has alias `c`.
|
||||
|
||||
`main=true` on a non-Folia server registers a JVM shutdown hook. On shutdown it copies `data/`, `datapacks/`, and `players/` from the current level root along with the Iris dimension folder into a new level directory, then rewrites `server.properties` with the new `level-name` and `level-seed`. Nothing changes while the server is up. On Folia the promotion is applied inline during staging instead, and rolls back the `bukkit.yml` entry and the staged folder if it fails.
|
||||
|
||||
**Names Iris refuses.** `iris` and `benchmark` are rejected outright (case-insensitive) and Iris suggests something like `irisworld`. Before those checks, the name also has to be a safe single path segment matching `[a-z0-9_-]`, so anything containing `/`, `\`, or `..` is rejected, as is any name that would collide with a vanilla world slot — your server's `level-name`, `<level-name>_nether`, or `<level-name>_the_end`. Those produce a different message about only Iris-managed worlds being changeable.
|
||||
|
||||
**Already exists.** Without `overwrite=true`, create aborts if the managed dimension folder is already there. That folder lives at `<level-root>/dimensions/iris/<name>`, not next to your server jar.
|
||||
|
||||
**Folia.** Runtime creation is disabled. Iris stages the world files, installs the pack snapshot, registers the world in `bukkit.yml`, and tells you to restart. After the restart the world generates and loads on its own from that registration — you don't need `/iris load`.
|
||||
|
||||
**Everything else.** Create builds the world immediately through `IrisToolbelt.createWorld()`, as a production world (not a studio world).
|
||||
|
||||
```text
|
||||
/iris create myworld type=overworld seed=1337
|
||||
```
|
||||
|
||||
Now run `/iris worlds` (alias `accesslist`). It prints two lists — Iris worlds and plain Bukkit worlds. On a non-Folia server `myworld` must appear under Iris worlds. On Folia, success is the staging-and-restart message; restart before continuing.
|
||||
|
||||
### Mod
|
||||
|
||||
```text
|
||||
/iris create <name> [pack] [seed]
|
||||
```
|
||||
|
||||
| Parameter | Default | What it does |
|
||||
|---|---|---|
|
||||
| `name` | required | Dimension id. A bare name is normalized into the `irisworldgen` namespace, so `myworld` becomes `irisworldgen:myworld` |
|
||||
| `pack` | `overworld` | Pack key. Use the `pack:dimension` form when the pack's dimension key differs from its name |
|
||||
| `seed` | `1337` | Long seed |
|
||||
|
||||
Alias `c`. You can't pass `seed` without also passing `pack`.
|
||||
|
||||
The `pack:dimension` form has to be **quoted** — `"overworld:overworld"` — because Brigadier's unquoted string type doesn't accept a colon. Iris's own help text says the same thing.
|
||||
|
||||
If the pack isn't installed, create prints a message, downloads `IrisDimensions/<pack>` on a background thread (from the `master` branch), then injects the dimension. Once that succeeds the dimension is live and gets re-injected on later startups.
|
||||
|
||||
```text
|
||||
/iris create myworld overworld 1337
|
||||
```
|
||||
|
||||
There's no separate load step on modded. The same world management also lives under `/iris world create|enable`, where `create` is simply an alias of `enable`. That form requires the pack argument and has no `overworld` default.
|
||||
|
||||
Confirm with `/iris world status`, which lists each loaded Iris level with its pack and dimension key. Then run `/iris info` to check the seed. `/iris info` takes an optional greedy string that acts as a **substring filter** across dimension id, generator identity, and pack key — not a dimension selector — so `/iris info myworld` narrows the listing. The seed is only printed to gamemasters; at lower permission levels the line simply omits it.
|
||||
|
||||
## 2. Load a world (plugin only)
|
||||
|
||||
```text
|
||||
/iris load <world>
|
||||
```
|
||||
|
||||
The real command node is `loadWorld`, with alias `import`; `/iris load` reaches it through fuzzy matching. It requires the managed dimension directory to already exist on disk, then loads through `BukkitWorldReconciler` and registers the world with the server.
|
||||
|
||||
This one is **player-origin only** — the console cannot run it. On a headless server, load worlds by having them registered in `bukkit.yml` (which create already does) and restarting, or run it as a player.
|
||||
|
||||
Modded worlds created with `/iris create` or `/iris world enable` are already injected. Teleport instead.
|
||||
|
||||
## 3. Teleport
|
||||
|
||||
### Plugin
|
||||
|
||||
```text
|
||||
/iris teleport <world> [player=…]
|
||||
```
|
||||
|
||||
Alias `tp`. The world is positional; the player is optional and therefore keyed — `/iris tp myworld player=Notch`. Left out, it targets whoever ran the command, so console needs to name a player explicitly or it reports that the player doesn't exist. The teleport itself is performed asynchronously where the platform allows it.
|
||||
|
||||
```text
|
||||
/iris tp myworld
|
||||
```
|
||||
|
||||
You've passed this gate when the teleport completes and chunks generate normally around spawn. If the teleport target doesn't exist, go back to the create/load step — don't push on to pregen.
|
||||
|
||||
### Mod
|
||||
|
||||
```text
|
||||
/iris teleport <dimension> [player]
|
||||
/iris tp <dimension> [player]
|
||||
```
|
||||
|
||||
Dimension is a loaded-level argument and tab-completes Iris dimensions; a non-Iris dimension is rejected. Console must name a player. You land at x=8.5, z=8.5 in that dimension, at the `MOTION_BLOCKING` height for that column, and Iris force-loads the chunk with a ticket if it isn't loaded yet.
|
||||
|
||||
```text
|
||||
/iris tp irisworldgen:myworld
|
||||
```
|
||||
|
||||
Success is entry into `irisworldgen:myworld` with `/iris info myworld` still reporting the pack and seed you expect.
|
||||
|
||||
## 4. Pregenerate
|
||||
|
||||
Radius is in **blocks**, measured from the center outward, and one pregeneration job runs server-wide at a time.
|
||||
|
||||
The block radius is converted to an inclusive chunk box, so a 352-block radius at `0,0` covers chunks -22 through 22 on both axes: **45 × 45 = 2,025 chunks**, or 720 × 720 blocks of actual generated area. The command's own feedback describes the request as 704 × 704 blocks (radius × 2); the extra chunk on each edge is the inclusive rounding. Pick your radius knowing that chunk count, not the block number, is what determines how long this takes.
|
||||
|
||||
### Plugin
|
||||
|
||||
```text
|
||||
/iris pregen start <radius> [world=…] [center=x,z|me] [gui=true|false] [serial=true|false]
|
||||
```
|
||||
|
||||
| Parameter | Aliases | Default | What it does |
|
||||
|---|---|---|---|
|
||||
| `radius` | `size` | required | Radius in blocks; must be greater than 0. The only positional parameter |
|
||||
| `world` | — | your current world | Target world. Contextual, so it must be keyed when you override it — typically when running from console |
|
||||
| `center` | `middle` | `0,0` | Center point. `me` uses the running player's position |
|
||||
| `gui` | — | `true` | Open the pregen progress window. Set false on a headless server |
|
||||
| `serial` | — | `false` | Generate one chunk at a time. Much slower, but the safe option when parallel generation is destabilizing the server. Requires a Paper-compatible server |
|
||||
|
||||
```text
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
```
|
||||
|
||||
Immediately run `/iris pregen status`. It should report a 2,025-chunk job that advances without a growing failure count.
|
||||
|
||||
Control it with `/iris pregen stop` (alias `x`), `/iris pregen pause`, and `/iris pregen status`. **`resume` is an alias of `pause`, and `pause` is a toggle** — there's no distinct resume command, so running either one on a paused job resumes it and on a running job pauses it.
|
||||
|
||||
### Mod
|
||||
|
||||
```text
|
||||
/iris pregen start <radius> [dimension] [at <x> <z>] [gui] [sync] [nocache]
|
||||
```
|
||||
|
||||
| Piece | What it does |
|
||||
|---|---|
|
||||
| `radius` | Blocks, 1–100000 |
|
||||
| `dimension` | Optional target level; defaults to the dimension you're in |
|
||||
| `at x z` | Optional center; defaults to 0, 0 |
|
||||
| `gui` | Ask for the progress map window on the server display. Silently ignored when no GUI can be launched |
|
||||
| `sync` | Synchronous chunk writes |
|
||||
| `nocache` | Disable the resumable checkpoint cache. Caching is on by default, which is what lets a stopped job pick up where it left off |
|
||||
|
||||
The three flags are combinable in any order and each may appear once, but **`at <x> <z>` must come before any flag.** `/iris pregen start 100 gui at 0 0` is a syntax error; `/iris pregen start 100 at 0 0 gui` is fine.
|
||||
|
||||
```text
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
|
||||
```
|
||||
|
||||
Run `/iris pregen status` right away and confirm the target dimension, total, and generated count. Stop before retrying with different flags. As on Bukkit, `pause` and `resume` are the same toggle. Progress shows in the client mod HUD when present, otherwise a boss bar or the console.
|
||||
|
||||
## 5. Open a Studio
|
||||
|
||||
Studio worlds are transient. They're discarded when you close them and any leftovers are purged at startup. Crucially, a Studio world reads the **live** pack directory and hotloads JSON and object edits into newly generated chunks. Production worlds never do this — they read the frozen snapshot copied into the world at creation. That difference is the reason Studio exists, and the reason pack edits appear to do nothing on a production world.
|
||||
|
||||
### Plugin
|
||||
|
||||
```text
|
||||
/iris studio create [name=…] [template=…]
|
||||
/iris studio open <dimension> [seed=…]
|
||||
/iris studio vscode [dimension=…]
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
| Command | Aliases | Notes |
|
||||
|---|---|---|
|
||||
| `create` | `+` | Both parameters are optional, so **neither takes a positional value** — use `name=mypack`. With no template it scaffolds a minimal starter pack (`dimensions/`, `regions/`, `biomes/`, `generators/` with one of each). With a template it copies an existing packs entry, downloading it if needed |
|
||||
| `open` | `o` | Opens a temporary studio world for a pack. `dimension` is required and positional; `seed` is optional (alias `s`) and defaults to `1337` |
|
||||
| `vscode` | `vsc` | Writes and opens a `.code-workspace` with live registry schemas. `dimension` is optional, so keyed only, and defaults to `default` |
|
||||
| `close` | `x` | Discards the studio world |
|
||||
|
||||
The studio group itself has aliases `std` and `s`. Default create name is `studio`; if a project by that name already exists, Iris picks the next free name rather than failing.
|
||||
|
||||
```text
|
||||
/iris studio open overworld seed=1337
|
||||
/iris studio vscode dimension=overworld
|
||||
```
|
||||
|
||||
### Mod
|
||||
|
||||
```text
|
||||
/iris studio create [name] [template]
|
||||
/iris studio open <pack> [seed]
|
||||
/iris studio vscode [pack]
|
||||
/iris studio update [pack]
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
| Command | Aliases | Notes |
|
||||
|---|---|---|
|
||||
| `create` | `+` | Name defaults to `studio`, template defaults to **`example`**. This differs from the plugin, where omitting the template scaffolds a starter pack instead. The template pack is auto-downloaded (from `master`) if missing |
|
||||
| `open` | `o` | Pack is required; seed defaults to `1337` |
|
||||
| `vscode` | `vsc` | Writes the workspace and opens it |
|
||||
| `update` | — | Regenerates the workspace schemas without opening anything |
|
||||
| `close` | `x` | Discards the studio world |
|
||||
|
||||
Group aliases are `std` and `s`.
|
||||
|
||||
```text
|
||||
/iris studio open overworld 1337
|
||||
/iris studio vscode overworld
|
||||
```
|
||||
|
||||
A number of Bukkit studio and content tools deliberately refuse on modded and print an explanatory message rather than half-working: `importvanilla` (`importv`, `iv`), `loot`, `profile`, `spawn`/`summon`, `objects`/`find-objects`, the object `we`, `studio`, and `convert` subcommands, structure `import`/`import-all`/`reimport`, and datapack `ingest`/`pull`/`remove`. Do that work on a Bukkit server and copy the pack folder across.
|
||||
|
||||
The Studio gate passes when the transient world opens, the workspace points at the live `packs/overworld/` tree, and saving a valid JSON change produces a hotload result in-game. Close it with `/iris studio close` and confirm your production `myworld` is still there and unaffected.
|
||||
|
||||
## The whole first session
|
||||
|
||||
1. Confirm the pack: `overworld` (or yours) exists under the platform packs directory.
|
||||
2. Create the world using the form for your platform.
|
||||
3. On Folia only: restart after the staging message. The world comes back on its own.
|
||||
4. Teleport in and fly around a little to confirm chunks generate.
|
||||
5. Optional: `/iris pregen start 352 …` for a 45×45-chunk area.
|
||||
6. Optional: `/iris studio open <pack>` and use the VSCode schemas for block, item, and entity autocomplete — mod content is included in those schemas on mod loaders.
|
||||
|
||||
The session is genuinely finished when you restart the server cleanly, the production world loads again, and it generates new chunks from its copied pack snapshot. Remove a disposable world through the lifecycle command after evacuating players, never by deleting folders — see `06 - Worlds & Lifecycle.md`.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
| Pitfall | What actually happens | What to do |
|
||||
|---|---|---|
|
||||
| Bukkit optional args passed positionally | Hard parse error, command does nothing | Write `seed=1337`, not a bare second token |
|
||||
| `/iris studio create mypack` | Fails — both params are optional so neither is positional | `/iris studio create name=mypack` |
|
||||
| `/iris pack validate` with no argument | Missing-argument error, not "validate everything" | Name the pack, or pass an explicitly empty `pack=` to do all of them |
|
||||
| World named `iris` or `benchmark` | Create rejected | Pick another name, e.g. `irisworld` |
|
||||
| Editing `packs/<pack>` after creating a production world | **No effect** on that world, ever | Production engines read `<world>/iris/pack`. Push changes with `/iris developer update-world world=<world> pack=<dimension> confirm=true` and restart, or accept that only new chunks change. Studio reads the live pack |
|
||||
| Expecting pack edits to change existing chunks | Only newly generated chunks use the new config | Fly to unexplored terrain, pregen a fresh radius, or use a Studio world |
|
||||
| Folia: create then teleport immediately | The world isn't live yet | Restart after the staging message, then teleport |
|
||||
| Modded: new pack's heights or biomes missing | The forced datapack wasn't applied before registries loaded | Restart once with the pack already installed |
|
||||
| `/iris load` from console | Player-origin only; console can't run it | Rely on the `bukkit.yml` registration plus a restart, or run it as a player |
|
||||
| `/iris load` on modded | No such subcommand | Use create or `world enable`, then teleport |
|
||||
| Modded `pack:dimension` unquoted | Brigadier rejects the colon | Quote it: `"overworld:overworld"` |
|
||||
| Modded pregen flags before `at x z` | Syntax error | Put `at <x> <z>` before any flag |
|
||||
| Starting a pregen while one is running | Start fails | `/iris pregen stop` first |
|
||||
| `/iris pregen resume` expected to only resume | It's an alias of `pause`, which toggles | Check `/iris pregen status` instead of assuming |
|
||||
| Studio closed mid-edit | The studio world is discarded | Your edits are on disk in `packs/` and survive. Reopen the studio |
|
||||
| Managed pack download blocked | Startup, create, or studio open fails with a missing pack | Allow HTTPS or run `/iris download overworld` and `/iris download underworld`. An offline install must contain each complete pack tree |
|
||||
| Relying on `type=default` | Resolves through `generator.defaultWorldType`, which someone may have changed | Name the pack explicitly: `type=overworld` |
|
||||
|
||||
## Quick reference
|
||||
|
||||
**Plugin**
|
||||
|
||||
```text
|
||||
/iris create myworld type=overworld seed=1337
|
||||
/iris tp myworld
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
/iris pregen status
|
||||
/iris studio open overworld seed=1337
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
**Mod**
|
||||
|
||||
```text
|
||||
/iris create myworld overworld 1337
|
||||
/iris tp irisworldgen:myworld
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0
|
||||
/iris pregen status
|
||||
/iris studio open overworld 1337
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
Next: how packs are structured in `05 - Concepts & Pack Layout.md`, and every settings key in `03 - Configuration.md`.
|
||||
@@ -1,310 +0,0 @@
|
||||
# 03 - Configuration
|
||||
|
||||
Iris keeps its shared runtime settings in `settings.json` under the platform data folder. On first boot Iris writes a full defaults file if one is missing, and every successful load rewrites the file so new keys appear with defaults. Bukkit adds `compat.json`; mod loaders add `modded.json`. See `01 - Installation & Platforms.md` for data paths and `33 - Performance Tuning.md` for how to measure a tuning change.
|
||||
|
||||
## What you actually need to change
|
||||
|
||||
The shipped defaults are correct for almost every server. Most operators only ever touch a handful of keys:
|
||||
|
||||
| You want to | Change |
|
||||
|---|---|
|
||||
| Run the server in another language | `general.language` |
|
||||
| See why generation is behaving oddly | `general.debug`, or `/iris debug` |
|
||||
| Stop Iris opening desktop windows on the host | `gui.useServerLaunchedGuis`, `studio.openVSCode` |
|
||||
| Survive pregen on a memory-constrained box | `pregen.maxResidentTectonicPlates`, the `performance.*CacheSize` keys |
|
||||
| Turn on the survival tree feller | `treeFeller.enabled` |
|
||||
| Catch broken pack keys instead of silently ignoring them | `general.strictContentKeys` |
|
||||
|
||||
Everything else is either already right, only meaningful while diagnosing a specific problem, or inert on your platform. Each table below marks which is which.
|
||||
|
||||
## File locations
|
||||
|
||||
| Platform | Shared settings | Packs root | Platform-only config |
|
||||
|----------|-----------------|------------|----------------------|
|
||||
| Bukkit / Paper / Folia | `plugins/Iris/settings.json` | `plugins/Iris/packs/` | `plugins/Iris/compat.json` |
|
||||
| Fabric / Forge / NeoForge | `<configDir>/iris/settings.json` | `<configDir>/irisworldgen/packs/` | `<configDir>/irisworldgen/modded.json` |
|
||||
|
||||
`<configDir>` is the loader config directory (game `config/` on Fabric, Forge, and NeoForge). Both surfaces use the same `IrisSettings` schema for `settings.json`.
|
||||
|
||||
The modded split is real and easy to get wrong: the engine data folder is `<configDir>/iris`, but installed packs, the generated datapack, and `modded.json` live under `<configDir>/irisworldgen`. Iris also creates an empty `<configDir>/iris/packs` directory; that is not the pack root and putting a pack there will not load it.
|
||||
|
||||
## Changing a setting safely
|
||||
|
||||
1. Start Iris once so it writes the current schema and defaults.
|
||||
2. Copy `settings.json` outside the server directory as a rollback file.
|
||||
3. Change one key. Keep its JSON type: quoted values such as `"false"` are strings, not booleans.
|
||||
4. Save the file, then run `/iris reload` or wait for the hotload poll (about 3 seconds on both platforms).
|
||||
5. Confirm the console logs `Hotloaded settings.json` or the reload success message, with no parse error.
|
||||
6. Exercise the affected feature. If nothing changed, check the "Takes effect" column below — several keys are captured when a service, pool, or cache is constructed and need a restart.
|
||||
|
||||
If parsing fails, restore the saved file and restart. Do not delete `settings.json` unless resetting every setting to defaults is what you want.
|
||||
|
||||
To change only the server locale, edit the existing `general` object in place:
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"language": "de_DE"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That fragment shows the field location; do not replace a populated settings file with it. After `/iris reload`, run `/iris help` and confirm the selected locale is active. Iris rewrites the complete settings file after a successful load, including defaults for fields that were absent.
|
||||
|
||||
### Validation and rollback
|
||||
|
||||
| Result | Meaning | Action |
|
||||
|---|---|---|
|
||||
| Reload succeeds and the feature changes | The file parsed and the setting is read live | Keep the backup until the next clean restart |
|
||||
| Reload succeeds but behavior is unchanged | The value was captured when a service, pool, or cache was built | Restart, then retest the same workload |
|
||||
| Parse error in console, file unchanged | Gson threw before the rewrite, so your broken file is still on disk and Iris is running built-in defaults | Fix the JSON, reload; restore the backup if you cannot |
|
||||
| File is rewritten with defaults | Missing or unknown fields were normalized by `IrisSettings` | Reapply only intentional overrides; do not restore an obsolete full file over new defaults |
|
||||
| Modded and Bukkit paths differ | The wrong data root was edited | Use the path table above and confirm the file timestamp changed before reloading |
|
||||
|
||||
## 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 failure, log `Configuration Error in settings.json!` and run on built-in defaults for that boot — the bad file is left untouched, because the rewrite never runs |
|
||||
| After a successful load | Rewrite `settings.json` as pretty JSON so new keys and migrated values persist. Comments and hand formatting are lost |
|
||||
| `/iris reload` | Invalidate the cached settings, re-read the file, reload the locale. On modded it also schedules a forced datapack regeneration. It does not restart services, reload packs, or rebuild engines |
|
||||
| Hotload (Bukkit) | `SettingsHotloadWatch` polls every 60 ticks (about 3 s) through the VolmLib `ConfigHotloadEngine`. A `lastModified` or size change triggers a read; the reload only runs if the normalized file content actually differs. Logs `Hotloaded settings.json` |
|
||||
| Hotload (modded) | `ModdedSettingsHotloadService` polls `lastModified` every 3 s. Because a load rewrites the file, a touch with no edit still produces one reload (it does not loop). Logs `Hotloaded settings.json` |
|
||||
| Locale refresh | Bukkit calls `IrisLanguage.update()` on every poll; modded calls it only when the file is unchanged, and calls a full `IrisLanguage.reload()` when it did change |
|
||||
| `forceSave()` | Only `/iris debug` writes settings back from memory |
|
||||
|
||||
Legacy migration: if the raw JSON still contains `world.anbientEntitySpawningSystem`, the value is copied to `world.ambientEntitySpawningSystem` and logged once.
|
||||
|
||||
## Root object
|
||||
|
||||
Top-level Gson fields on `IrisSettings`. Every nested object is created with defaults when missing.
|
||||
|
||||
| Field | Nested class | Covers |
|
||||
|-------|--------------|--------|
|
||||
| `general` | `IrisSettingsGeneral` | Locale, debug output, console colors, datapack ingest, strict keys, splash |
|
||||
| `world` | `IrisSettingsWorld` | Entity systems, async world tick, WorldEdit CUI, pregen cache |
|
||||
| `gui` | `IrisSettingsGUI` | Server-launched desktop GUIs |
|
||||
| `autoConfiguration` | `IrisSettingsAutoconfiguration` | Spigot/Paper server-file fixups, custom-biome restart |
|
||||
| `generator` | `IrisSettingsGenerator` | Default pack for world creation, leaf decay |
|
||||
| `concurrency` | `IrisSettingsConcurrency` | Nothing configurable — see below |
|
||||
| `studio` | `IrisSettingsStudio` | Studio world behavior |
|
||||
| `performance` | `IrisSettingsPerformance` | Mantle residency, loader caches, SIMD, engine service pool |
|
||||
| `pregen` | `IrisSettingsPregen` | Pregen scheduling, mantle backpressure, timeouts |
|
||||
| `sentry` | `IrisSettingsSentry` | Error reporter |
|
||||
| `treeFeller` | `IrisSettingsTreeFeller` | Survival tree feller |
|
||||
|
||||
Static helper `IrisSettings.getThreadCount(int c)`: for `c` in `{-1, -2, -4}` it returns `max(availableProcessors / -c, 1)`; otherwise `max(c, 2)`, floored at 1.
|
||||
|
||||
## `general` — locale, diagnostics, and console output
|
||||
|
||||
This group decides what Iris says and how loudly. `language`, `debug`, and `strictContentKeys` are the ones worth touching; the colour and spin keys are cosmetic; the datapack keys change startup work on Bukkit only.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `language` | `"en_US"` | Live | Selects the locale catalog for all Iris messages. Reloaded by `/iris reload` and by both hotload watchers |
|
||||
| `commandSounds` | `true` | Live | **Bukkit only.** Plays the amethyst chime on `/iris` tab completion and success/failure sounds after a command. Turn off if the noise annoys staff |
|
||||
| `debug` | `false` | Live | Enables verbose engine tracing on the console and writes per-chunk crash dumps under `debug/chunk-errors/`. Toggle with `/iris debug` rather than editing by hand; leave off in production because it is loud |
|
||||
| `dumpMantleOnError` | `false` | Live | When a tectonic plate read reports an error, dump the decoded region to `dump/<name>.bin` instead of logging a timing line. Turn on only when investigating mantle corruption |
|
||||
| `disableNMS` | `false` | **Restart** | **Bukkit only.** Forces the no-op NMS binding. Iris logs a warning and world creation stops working entirely, so this is a diagnostic escape hatch, not a compatibility switch. Read in a class initializer, so a reload will not change it |
|
||||
| `pluginMetrics` | `true` | **Restart** | **Bukkit only.** Registers the bStats reporter at enable |
|
||||
| `splashLogoStartup` | `true` | **Restart** | Prints the ASCII logo and version block at startup. Set false for quieter console logs |
|
||||
| `useConsoleCustomColors` | `true` | Live | Gradient/hex colouring for console output. Set false if your log viewer mangles it — you still get legacy colour codes. Iris also forces both colour keys off in memory if Adventure fails to bind |
|
||||
| `useCustomColorsIngame` | `true` | Live | Same, for messages sent to players |
|
||||
| `adjustVanillaHeight` | `false` | **Restart** | **Bukkit only.** Overwrites the vanilla `overworld`/`the_nether`/`the_end` dimension-type JSON with Iris height when compiling the datapack. It is part of the datapack fingerprint, so flipping it forces a datapack rebuild |
|
||||
| `autoIngestDatapacks` | `true` | **Restart** | **Bukkit only.** Downloads and installs configured `datapackImports` during the startup admission gate. Unchanged committed content reuses its persisted result instead of revalidating; managed structures stay scoped to the declaring Iris dimensions |
|
||||
| `autoImportDatapackStructures` | `false` | Live (next ingest) | **Bukkit only.** Converts every registered datapack structure into editable Iris pools, pieces, and objects — thousands of files in your pack folder. Native generation never needs those copies, so leave it off and run `/iris structure import <dimension>` when you actually want them |
|
||||
| `strictContentKeys` | `false` | Live | Promotes unresolved pack content keys and bad block-state properties from warnings to blocking pack errors. Worth turning on while developing a pack. `-Diris.strictContent` overrides it in both directions, and the bare property with no value counts as true |
|
||||
| `spinh` | `-20` | Live | Hue factor of the animated "aura" gradient on Iris text |
|
||||
| `spins` | `7` | Live | Saturation factor of the same gradient |
|
||||
| `spinb` | `8` | Live | Brightness factor of the same gradient |
|
||||
|
||||
## `world` — entity systems and the async world tick
|
||||
|
||||
Iris runs its own spawning and effects pass on a background loop, separate from vanilla mob spawning. These keys decide whether that loop does anything and how often. Turning the spawn systems off makes Iris worlds feel emptier but removes an entire class of tick cost; the defaults are the intended experience.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `postLoadBlockUpdates` | `true` | Live | Runs a block-update pass over freshly generated chunks near players so placed objects settle (physics and waterlogging fixups). Turning it off is faster but leaves floating or unwatered blocks from some objects |
|
||||
| `forcePersistEntities` | `true` | Live | Marks every Iris-spawned entity persistent so vanilla mob-cap and distance rules do not despawn it. Turn off if pack-spawned mobs are accumulating |
|
||||
| `ambientEntitySpawningSystem` | `true` | Live | Enables the biome/region ambient spawn lists on the async tick (legacy key `anbientEntitySpawningSystem` is migrated automatically) |
|
||||
| `asyncTickIntervalMS` | `700` | Live (next tick) | Milliseconds between world-manager passes that handle spawning, effects, and cleanup. Raise it to cut background cost on a busy server; lower it only if pack spawns feel too sparse |
|
||||
| `targetSpawnEntitiesPerChunk` | `0.95` | Live | Entity saturation ceiling. Once entities per loaded chunk exceed this, Iris stops spawning (the Bukkit path also backs off for 5 seconds). Lower it on servers already near their entity budget |
|
||||
| `markerEntitySpawningSystem` | `true` | Live | Enables spawning driven by mantle marker blocks, which is how packs place specific mobs at specific generated features |
|
||||
| `effectSystem` | `true` | Live | Applies per-biome and per-region `IrisEffect`s (potion effects, particles, sounds) to players |
|
||||
| `worldEditWandCUI` | `true` | Live | **Bukkit only.** Lets a WorldEdit selection act as an Iris wand and draws the particle outline for it |
|
||||
| `globalPregenCache` | `false` | Live, one event late | **Bukkit only.** Maintains a persistent per-world bitmap of already-generated chunks so pregen can skip finished work across restarts. The enable/disable flip is observed on the following world-init or chunk-load event, not the current one |
|
||||
|
||||
With both `markerEntitySpawningSystem` and `ambientEntitySpawningSystem` false, the world manager skips all related entity work.
|
||||
|
||||
## `gui` — desktop windows launched by the server
|
||||
|
||||
Iris can open AWT windows on the machine running the server: the noise explorer, the vision map, and the pregen viewer. That is useful on a local dev box and wrong on a headless host, which is the only reason to touch this group.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `useServerLaunchedGuis` | `true` | Live | Allows server-side GUI hosts to open windows. Set false on any remote or headless server; the commands then report that GUIs are unavailable instead of trying |
|
||||
| `maximumPregenGuiFPS` | `false` | Live | Repaints the pregen map window as fast as possible instead of roughly four times a second. Only affects the local window, never generation throughput |
|
||||
| `colorMode` | `true` | Per window open | Colour rendering in the noise explorer instead of grayscale. It is captured when the window opens, so close and reopen the explorer to apply a change |
|
||||
|
||||
## `autoConfiguration` — Bukkit server-file fixups
|
||||
|
||||
Iris edits a couple of server config files at boot so long chunk generation does not look like a hang to the server's own watchdogs. Leave these on unless you manage those files yourself. They are all Bukkit-only and all read once during enable.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `configureSpigotTimeoutTime` | `true` | **Restart** | Raises `timeout-time` in `spigot.yml` so a long generation stall does not kill the server |
|
||||
| `configurePaperWatchdogDelay` | `true` | **Restart** | Raises Paper's watchdog early-warning and timeout for the same reason |
|
||||
| `autoRestartOnCustomBiomeInstall` | `true` | **Restart** | When a datapack install registers new custom biomes and reports that a restart is required, Iris restarts the server itself instead of waiting for an admin |
|
||||
|
||||
These keys are no-ops on mod loaders.
|
||||
|
||||
## `generator` — defaults for world creation
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `defaultWorldType` | `"overworld"` | Live | **Bukkit only.** The pack key used whenever a world, studio, or command omits one — including a bare `Iris` generator string in `bukkit.yml` and `/iris create name type=default`. Mod loaders use `defaultPack` in `modded.json` instead |
|
||||
| `preventLeafDecay` | `true` | Effectively **restart** | Marks generated leaves persistent so they do not decay. The flag is baked into resolved block data that is then cached, so already-resolved leaf blocks keep the old behavior after a reload. Unrelated to the per-dimension `preventLeafDecay` field in pack JSON |
|
||||
|
||||
## `concurrency` — nothing to configure
|
||||
|
||||
This object has no serialized fields. Gson writes `{}`, and anything you type inside it is silently discarded the next time Iris saves the file. The values are derived from CPU count at runtime:
|
||||
|
||||
| Method | Result | Used by |
|
||||
|--------|--------|---------|
|
||||
| `getParallelism()` | `max(2, availableProcessors)` | Default `MultiBurst` pools, hybrid pregen thread count, locator searches |
|
||||
| `getIoParallelism()` | `max(2, availableProcessors / 2)` | The shared IO burst pool |
|
||||
| `getWorldGenThreads()` | `max(2, availableProcessors)` | Async pregen concurrency cap and the Moonrise worker-pool adjustment |
|
||||
|
||||
## `performance` — caches, mantle residency, and the engine service pool
|
||||
|
||||
This is the memory-versus-rework group. Larger loader caches trade heap for fewer pack reloads; mantle keys decide how long generated region data stays resident before being written out. Most keys here are captured when a pool or cache is built, so plan on a restart. Use `33 - Performance Tuning.md` for the measurement procedure — changing these blind usually makes things worse.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `trimMantleInStudio` | `false` | Live | Lets the maintenance pass trim mantle in studio worlds. With the default `false`, studio engines skip the whole maintenance pass, which keeps edits responsive at the cost of growing memory during long authoring sessions |
|
||||
| `mantleKeepAlive` | `30` | Live | Seconds a mantle plate stays resident before it is eligible for trimming. Scaled down automatically as reclaim pressure rises. Lower it when heap is tight, raise it if the same regions are reloaded repeatedly |
|
||||
| `noiseCacheSize` | `1024` | Mixed | Capacity of the noise sample caches. The terrain query API picks it up live; the engine's own caches need an engine hotload or restart. Pregen temporarily raises it to at least 4096 in memory and does not lower it again or persist the change |
|
||||
| `resourceLoaderCacheSize` | `1024` | **Restart / pack reload** | How many loaded pack resources stay cached per loader. Captured when a pack's `IrisData` is opened |
|
||||
| `objectLoaderCacheSize` | `4096` | **Restart / pack reload** | Same, for `.iob` objects, matter objects, and images. Raise it for object-heavy packs when heap allows; lower it first when profiling shows retained pack data |
|
||||
| `mantleCleanupDelay` | `200` | Live | Delay in **ticks** before a loaded chunk's mantle cleanup runs — the default is 10 seconds. Read from the raw field with no clamping, so a negative value is floored at 0 ms and a huge value really does postpone cleanup |
|
||||
| `simdKernels` | `true` | **Restart** | Uses Vector API noise kernels when `jdk.incubator.vector` is on the module path, otherwise scalar fallbacks. Chosen once during class initialization, so toggling it and reloading does nothing, and it is silently inert without the JVM module flag |
|
||||
|
||||
### `performance.engineSVC`
|
||||
|
||||
The engine maintenance service is a small scheduled pool that trims and unloads mantle plates. Its three sizing keys are read once at enable, so a restart is required for any change to matter.
|
||||
|
||||
| Key | Default | Takes effect | What it does |
|
||||
|-----|---------|--------------|--------------|
|
||||
| `useVirtualThreads` | `true` | **Restart** | Builds the maintenance thread factory from virtual threads instead of platform threads |
|
||||
| `forceMulticoreWrite` | `false` | Live | Makes every maintenance pass unload all eligible tectonic plates instead of only unloading under heap pressure. Trades steadier memory for more write work; useful during long pregens on a small heap |
|
||||
| `priority` | `5` (`Thread.NORM_PRIORITY`) | **Restart** | Thread priority, clamped to `[MIN_PRIORITY, MAX_PRIORITY]`. It is applied only when `useVirtualThreads` is false, so with the shipped defaults this key does nothing |
|
||||
| `parallelism` | `-1` | **Restart** | Maintenance pool size. `>0` is capped at `processors * 2`; `<=0` uses `ceil(sqrt(processors))`, at least 1 |
|
||||
|
||||
## `pregen` — scheduling, timeouts, and mantle backpressure
|
||||
|
||||
These keys bound how aggressively pregeneration pushes the server. They are read when a pregen job is constructed, so a change applies to the *next* job, not a running one. The two that matter in practice are `maxResidentTectonicPlates` (the memory ceiling) and, on mod loaders, `moddedPregenInFlight` (the concurrency ceiling). The rest exist for diagnosing a specific failure mode.
|
||||
|
||||
| Key | Default | Applies to | What it does and how it resolves |
|
||||
|-----|---------|------------|----------------------------------|
|
||||
| `runtimeSchedulerMode` | `AUTO` | Bukkit | `AUTO`, `PAPER_LIKE`, `FOLIA`. A regionized (Folia) runtime resolves to `FOLIA` before the setting is consulted, and off Folia a configured `FOLIA` is downgraded to `PAPER_LIKE`. Since `AUTO` also lands on `PAPER_LIKE` for every recognized and unrecognized fork, this key changes nothing in practice — the one exception is a non-regionized server that still identifies itself as Folia by name or version, where `AUTO` picks `FOLIA` and an explicit `PAPER_LIKE` does not |
|
||||
| `paperLikeBackendMode` | `AUTO` | Bukkit, non-Folia | `AUTO`, `TICKET`, `SERVICE`. `SERVICE` uses the service executor (`paper-service`); `TICKET` and `AUTO` both use the ticket executor (`paper-ticket`). Ignored entirely on Folia. Try `SERVICE` only if ticket-based chunk loading is producing timeouts |
|
||||
| `chunkLoadTimeoutSeconds` | `15` | Both | Clamped to `[5, 120]`. How long pregen waits for one chunk load before it counts as timed out. **On mod loaders the effective value is floored at 120**, so any value below that is ignored there |
|
||||
| `timeoutWarnIntervalMs` | `500` | Bukkit | Minimum 250. Rate-limits the "timed out async pregen chunk load" and failed-release warnings so a bad run does not flood the log. Not read on mod loaders |
|
||||
| `saveIntervalMs` | `30000` | Both | Clamped to `[5000, 900000]`. How often a running pregen flushes progress. Lower it if you expect to lose the process and want a closer resume point; the cost is more IO |
|
||||
| `maxResidentTectonicPlates` | `96` | Both | Minimum 16. The mantle memory ceiling, and the first knob to lower on an out-of-memory pregen. The effective cap is also scaled by world height and by roughly 60% of the heap budget against a ~48 MB reference plate at height 384, with a floor of 16 — so on a small heap you may already be running below the configured number |
|
||||
| `mantleBackpressureWaitMs` | `25` | Both | Clamped to `[5, 1000]`. Sleep granularity while pregen waits for resident plates to drop below the cap |
|
||||
| `mantleBackpressureTimeoutMs` | `60000` | Both | Clamped to `[5000, 600000]`. How long that wait may last before Iris logs a backpressure warning and lowers its adaptive in-flight limit. Seeing this warning repeatedly means `maxResidentTectonicPlates` is too high for your heap, not too low |
|
||||
| `moddedPregenInFlight` | `0` | Modded | Concurrent chunk budget for modded pregen. `>0` is capped at 512; `<=0` derives `max(16, min(48, cpu * 2))`. Lower it when modded pregen causes chunk-load timeouts or memory growth. Inert on Bukkit |
|
||||
|
||||
## `sentry` — error reporting
|
||||
|
||||
Read once during boot on both platforms, so every change here needs a restart.
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `includeServerId` | `true` | **Bukkit only.** Attaches the server id to reports so recurring reports from one server can be grouped. Not read on mod loaders |
|
||||
| `disableAutoReporting` | `false` | Skips Sentry initialization entirely. Set true if you do not want automatic error reports leaving the machine |
|
||||
| `debug` | `false` | Turns on Sentry's own debug logging. Useful only when reports are not arriving |
|
||||
|
||||
## `treeFeller` — survival tree felling
|
||||
|
||||
Off by default because it changes survival gameplay. Both keys are read live, so `/iris reload` is enough.
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `enabled` | `false` | Master switch. With it on, a permitted player breaking one log fells the whole Iris-managed tree. Disabling it mid-run cancels an in-flight fell on mod loaders only; the Bukkit runner does not re-read the setting once a fell has started |
|
||||
| `durabilityPreservationChance` | `0` | Percent chance per block that the axe takes no durability, clamped to `[0, 100]`. An integration may override this per call |
|
||||
|
||||
Requires permission `iris.treefeller` on Bukkit, or the platform tree-feller node on mod loaders. See `04 - Commands & Permissions.md` and `28 - Integrations.md`.
|
||||
|
||||
## `studio` — authoring world behavior
|
||||
|
||||
Only two of the four keys in this section do anything today.
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `openVSCode` | `true` | Whether `/iris studio vscode` launches an editor after writing the workspace file. Set false on a headless box |
|
||||
| `entitySpawning` | `true` | Whether mobs spawn inside studio worlds. Has no effect on normal worlds |
|
||||
| `disableTimeAndWeather` | `true` | Nothing. Present in the settings model but not read by any code path today |
|
||||
| `autoStartDefaultStudio` | `false` | Nothing. Present in the settings model but not read by any code path today |
|
||||
|
||||
Studio workflow details: see `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
## Bukkit-only: `compat.json`
|
||||
|
||||
On Bukkit, Iris loads `plugins/Iris/compat.json` at startup and writes the complete built-in table to `compat.default.json` next to it for reference. This is how you keep a pack working on a server that lacks some block or item it references: Iris substitutes the replacement instead of failing.
|
||||
|
||||
Built-in mappings always stay active; entries read from `compat.json` are appended to them. Both files are read once at boot — `/iris reload` does not re-read them, and mod loaders do not use them at all.
|
||||
|
||||
```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 | The unsupported source key to match |
|
||||
| `supplement` | block and item filters | The replacement key. If the replacement is also unsupported, Iris re-runs the lookup on it, up to 16 hops, and falls back to `STONE` with an error |
|
||||
| `exact` | block filters only | When true, match the full key including namespace and block-state properties (`minecraft:some_log[axis=x]`). When false, match the bare material name. Item filters have no `exact` field |
|
||||
|
||||
A block substitution logs `Compat: Using '<supplement>' in place of '<when>' since this server doesnt support '<when>'` as a warning; item substitutions log the same at debug level. Invalid JSON logs the failure and leaves the built-in mappings active.
|
||||
|
||||
One quirk to know: when `compat.json` is absent, Iris seeds it with a copy of the entire built-in table. On the next boot those entries are appended to the built-ins again, so the runtime list holds every default twice. It is harmless because the first match wins, but if you are editing the file, delete the entries you did not add.
|
||||
|
||||
## Modded-only: `modded.json`
|
||||
|
||||
Path: `<configDir>/irisworldgen/modded.json`, written with defaults on first load if missing. Not used by the Bukkit plugin. Unlike `settings.json` this file is parsed by hand rather than Gson, is cached once, and has no hotload — a restart is required except for the keys that Iris rewrites itself. Malformed JSON logs `Iris modded config at … is invalid; using defaults` and runs on defaults **without** rewriting your file.
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `defaultPack` | `"overworld"` | Pack used by `/iris create` when none is given. A distinct non-managed value is also prefetched when auto-download is on |
|
||||
| `autoDownloadDefaultPack` | `true` | Downloads the managed Overworld and Underworld beta packs when missing, plus any distinct configured default. Set false on an air-gapped server and install packs by hand |
|
||||
| `primaryWorld` | `""` | Iris dimension id used for player routing |
|
||||
| `routePlayersToPrimaryWorld` | `true` | Sends players to the primary world when one is set |
|
||||
| `mainWorldPack` | `""` | Pack (or `pack:dimensionKey`) for the main-world preset |
|
||||
| `mainWorldSeed` | `0` | Seed for the main-world preset |
|
||||
| `mainWorldAutoRestart` | `false` | Restarts the server automatically after a main-world inject instead of telling you to |
|
||||
|
||||
`/iris world mainworld`, `/iris world replace-overworld`, and the primary-world clear paths write this file directly. 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 and 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`
|
||||
@@ -1,426 +0,0 @@
|
||||
# 04 - Commands & Permissions
|
||||
|
||||
Iris exposes one root command, `/iris` (aliases `/ir`, `/irs`), on every platform. Bukkit uses VolmLib Director, where optional arguments are always `key=value`; Fabric, Forge, and NeoForge register a Brigadier tree with positional arguments and literal flags. This page is the complete command reference; platform gaps are marked **Bukkit-only** or **modded-only**. See `30 - Platform Differences.md` for the platform matrix and `03 - Configuration.md` for what `/iris reload` re-reads.
|
||||
|
||||
## Everyday commands
|
||||
|
||||
Four workflows cover most operator use. The Bukkit and modded forms are separate command trees, not translations of each other, so do not port `key=value` tokens to a mod loader.
|
||||
|
||||
### Create a world and enter it
|
||||
|
||||
```
|
||||
/iris create tutorial type=overworld seed=1337 # Bukkit
|
||||
/iris create tutorial overworld 1337 # modded
|
||||
```
|
||||
|
||||
`type` (aliases `dimension`, `pack`) takes a pack key or `pack:dimensionKey`. Left at its default `default`, it resolves to `generator.defaultWorldType`. Bukkit refuses the names `iris` and `benchmark`, and refuses any name whose dimension folder already exists.
|
||||
|
||||
When it works, Bukkit prints `Successfully created your world!` and the world is immediately teleportable with `/iris tp tutorial`. On Folia the world is staged instead and the message tells you to restart before it exists. On mod loaders the dimension appears in `/iris world list`, and you enter it with `/iris tp irisworldgen:tutorial`.
|
||||
|
||||
If the pack is missing you get `Could not find or download dimension …` plus a `/iris download <pack>` hint — the world is not created.
|
||||
|
||||
### Pregenerate an area
|
||||
|
||||
Radius is in **blocks** and measured from the center, so `352` covers a 704x704 block square.
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=tutorial center=0,0 gui=false # Bukkit
|
||||
/iris pregen start 352 irisworldgen:tutorial at 0 0 # modded
|
||||
```
|
||||
|
||||
On Bukkit, `world` resolves from the sender's current world when omitted and is hidden from the in-game usage line, but `world=` still works — use it from console. `center=me` uses your own position. On modded, `at` must come after the dimension, never before it, and the flags `gui`, `sync`, `nocache` are literals in any order. The resumable checkpoint cache is on unless you pass `nocache`.
|
||||
|
||||
Confirm with `/iris pregen status`. A running job prints the target world, generated and total chunks, percent, chunks/s, ETA, elapsed time, the generation method, and a failure count. No output line for failures means nothing has failed. `/iris pregen pause` toggles, and `/iris pregen stop` finishes in-flight work before cancelling. Only one job runs server-wide. Detail: `07 - Pregeneration.md`.
|
||||
|
||||
### Open a studio for pack authoring
|
||||
|
||||
```
|
||||
/iris studio open overworld seed=1337 # Bukkit
|
||||
/iris studio open overworld 1337 # modded
|
||||
```
|
||||
|
||||
A transient studio world opens and you are teleported into it; saving any pack file hotloads the change into that world. `/iris studio close` (alias `x`) discards the world. `/iris studio vscode` regenerates the `.code-workspace` and JSON schemas, and opens it in the desktop editor unless `studio.openVSCode` is false. Detail: `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
### Check a pack before you rely on it
|
||||
|
||||
```
|
||||
/iris pack validate pack=overworld # Bukkit — pack is required, see the note below
|
||||
/iris pack validate # modded — empty means every pack
|
||||
```
|
||||
|
||||
A clean pack reports no blocking errors; the all-packs form finishes with a broken-pack count out of the total scanned. `/iris pack status` reprints the last recorded result without revalidating. Warnings do not block world creation, blocking errors do. Detail: `25 - Pack Management.md`.
|
||||
|
||||
**Bukkit quirk:** `/iris pack validate` and `/iris pack status` describe `pack` as optional ("leave empty for all"), but the parameter is declared with a blank default, which Director treats as *no* default. Both commands therefore reject a bare invocation with a missing-argument error, and the all-packs branch in the code is unreachable from Bukkit chat. Pass a pack name explicitly.
|
||||
|
||||
### Other common goals
|
||||
|
||||
| Goal | Bukkit-family | Fabric / Forge / NeoForge | Detailed guide |
|
||||
|---|---|---|---|
|
||||
| Replace the vanilla Nether slot | `/iris create world_nether type=underworld seed=1337 overwrite=true`, then restart | Not available | `06 - Worlds & Lifecycle.md` |
|
||||
| Create an in-game jigsaw project | `/iris jigsaw create overworld village/demo` | Not available; author on Bukkit and copy the saved pack | `21 - Jigsaw Structures.md` |
|
||||
| Inspect an Iris jigsaw graph | `/iris structure info overworld <structure>` | `/iris structure info <structure>` while in its Iris dimension | `21 - Jigsaw Structures.md` |
|
||||
| Remove a disposable Iris world | Evacuate players, `/iris unloadWorld <world>`, then `/iris remove <world>` | `/iris world delete <dimension>` | `06 - Worlds & Lifecycle.md` |
|
||||
|
||||
If a command fails before doing work, check in this order: platform syntax, permission, sender type (player versus console), exact pack/world key, then lifecycle busy state. A parse error is not evidence that the underlying feature failed.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Bukkit (Director)
|
||||
|
||||
- Root: `/iris` / `/ir` / `/irs`. Subcommand and group names come from the method or class name unless `@Director(name=…)` overrides it, so several commands read differently than you would guess — see the reference tables.
|
||||
- **Required parameters are positional; optional parameters are never positional.** `/iris pregen start 500 true` is a parse error; write `/iris pregen start 500 gui=true`.
|
||||
- A parameter declared with a blank default counts as required even when the description says otherwise.
|
||||
- Names and aliases match case-insensitively.
|
||||
- Help uses the Director mini-menu: required renders as `<name>`, optional as `[name=…]`.
|
||||
- **Contextual** parameters (world, dimension, pack, location, generator, template on many nodes) resolve from the sender's current world or look target. They are hidden from the usage line and from tab completion, but they still accept `name=value`, which is how you drive them from console.
|
||||
- Tab completion is not permission-gated; only execution is.
|
||||
|
||||
### Modded (Brigadier)
|
||||
|
||||
- Same root and aliases; `ir` and `irs` are redirect nodes onto `iris`.
|
||||
- Arguments are ordered literals and typed arguments, never free-form `key=value`.
|
||||
- `/iris` and `/iris help [section]` open the help browser: a paginated clickable UI for players (17 entries per page, trailing page number accepted in the section string) and a flat text list for console. Bare group nodes route into the same help.
|
||||
- Flags are literals where used (pregen `gui`, `sync`, `nocache`; download `force`).
|
||||
|
||||
## Permissions
|
||||
|
||||
### Bukkit
|
||||
|
||||
| Permission | Declared in | Default | Gate |
|
||||
|------------|-------------|---------|------|
|
||||
| `iris.all` | `plugin.yml` and `paper-plugin.yml` | `op` | `CommandSVC.executeRoot` rejects every `/iris` execution without it |
|
||||
| `iris.treefeller` | `plugin.yml` and `paper-plugin.yml` | `op` | Survival tree feller only (`TreeFellerSVC`); also requires `treeFeller.enabled` in settings |
|
||||
|
||||
`iris.all` is code-gated as `ROOT_PERMISSION` in `CommandSVC`. There are no per-subcommand permission nodes: a sender either has the whole tree or none of it. 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` (2) | Everything that mutates: create/world, studio, object tools, pregen, download, debug, reload, evacuate, teleport, seed, edit, find/goto, structure, datapack, pack, developer, regen, goldenhash, accesslist |
|
||||
| Read-only | `Commands.LEVEL_ALL` (0) | `help`, `version`, `info` (seed field omitted unless gamemaster), `worlds`, `height`, `metrics`, and the entire `what` subtree |
|
||||
|
||||
The root `iris` literal itself carries no requirement, so any player can run it and reach the help browser; the help marks itself as restricted when the source fails the gamemaster check.
|
||||
|
||||
Tree feller on mod loaders uses the platform permission API with node `irisworldgen:treefeller` (Fabric `Identifier`; Forge and NeoForge `PermissionNode`), defaulting to gamemaster level on all three. It is not a Bukkit permission string.
|
||||
|
||||
---
|
||||
|
||||
## Root: `/iris`
|
||||
|
||||
Bukkit names below are the names Director actually registers. Where that differs from what you would expect, the method name is what you type.
|
||||
|
||||
| Command | Aliases | Platforms | Params (Bukkit-style) | Description |
|
||||
|---------|---------|-----------|------------------------|-------------|
|
||||
| (empty) / help | | Both | `[section]` (modded) | Open help; modded supports a section path and page number |
|
||||
| `version` | | Both | — | Print Iris/platform/Minecraft version and engine count |
|
||||
| `info` | | **Modded** | `[dimension]` (substring filter) | List Iris dimensions and pack details; seed only for gamemasters |
|
||||
| `create` | `c` | Both | **Bukkit:** `<name> [type=default] [seed=1337] [main=false] [overwrite=false]` (`name` alias `world-name`; `type` aliases `dimension`,`pack`; `main` alias `main-world`; `overwrite` alias `force`). **Modded:** `<name> [pack=overworld] [seed=1337]` | Create an Iris world/dimension; Bukkit `overwrite=true` stages an exact slot replacement for restart |
|
||||
| `teleport` | `tp` | Both | **Bukkit:** `<world> [player]` (defaults to the sender). **Modded:** `<dimension> [player]` | Teleport self or a named player into an Iris world/dimension |
|
||||
| `evacuate` | | Both | **Bukkit:** `<world>`, player origin. **Modded:** `[dimension]` | Move players out of an Iris world to fallback/primary |
|
||||
| `height` | | Both | — | Print world height; player origin on Bukkit |
|
||||
| `worlds` | `accesslist` (Bukkit) | Both | — | **Bukkit:** access list of worlds. **Modded:** two separate nodes — `worlds` is read-only and takes no argument, `accesslist` needs gamemaster; both print the `info` listing |
|
||||
| `remove` | `rm` | **Bukkit** | `<world> [delete=true]` | Remove a managed Iris world; disk deletion defaults to true. `world` is a name, so worlds that exist only on disk are accepted |
|
||||
| `loadWorld` | `import` | **Bukkit** | `<world>`, player origin | Load a managed Iris world |
|
||||
| `unloadWorld` | | **Bukkit** | `<world>`, player origin | Unload an Iris world |
|
||||
| `debug` | | Both | — | Toggle `general.debug` and save settings |
|
||||
| `download` | `dl` | Both | `<pack> [branch=stable] [overwrite=false]` (`pack` alias `project`; `overwrite` alias `force`) | Download a pack; `overworld` and `underworld` resolve to managed beta release ZIPs |
|
||||
| `metrics` | `measure` | Both | — | Generation metrics; player origin on Bukkit |
|
||||
| `reload` | | Both | — | Reload `settings.json` and locale; modded also schedules forced datapack regeneration |
|
||||
| `seed` | | **Modded** | — | Print world and engine seeds |
|
||||
| `regen` | `rg` | **Modded** root; Bukkit under `Developer` | `[radius]` — modded default `0`, range `0..64` | Delete and regenerate nearby chunks |
|
||||
| `goldenhash` | `gold` | **Modded** root; Bukkit under `Developer` | `[radius=8] [threads=8] [capture\|verify]`, radius `0..256`, threads `1..64` | 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 pack JSON in the 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 |
|
||||
| `jigsaw` | `jig`, `jgs` | **Bukkit** | see Jigsaw | Transaction-owned planar/spatial Jigsaw Studio |
|
||||
| `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; the group name is registered with a capital `D`, but matching is case-insensitive |
|
||||
| `world` | `w` | **Modded** | see World | Runtime dimension enable/disable |
|
||||
|
||||
---
|
||||
|
||||
On Paper-family servers, `overwrite=true` is deliberately restart-only; Spigot rejects it because it has no pre-registry plugin bootstrap. The exact target dimension folder must already exist; use ordinary `/iris create` for a new world. The name may resolve to a safe `iris:*` world or exactly the configured main, `_nether`, or `_the_end` alias; arbitrary `minecraft:*` and foreign namespaces are rejected. Iris stages and validates a fresh pack snapshot, compare-and-swaps only that world's `bukkit.yml` generator and seed, and retains the existing dimension folder as a rollback backup until the restarted world proves its Iris identity, pack, dimension, environment, and seed. Multiple distinct slots may be staged before one restart. `main=true` is valid with overwrite only when the name is the configured main-world name. Exact vanilla slots preserve the authoritative seed shared by the existing level, regardless of the supplied `seed`; this keeps Overworld/Nether/End coordinate generation aligned, and Iris tells you which seed it used instead. Use ordinary new-main promotion when a new level seed is required.
|
||||
|
||||
---
|
||||
|
||||
## Find: `/iris find` (`goto`)
|
||||
|
||||
**Origin:** player (Bukkit). **Modded:** gamemaster gate.
|
||||
|
||||
| Command | Params | Description |
|
||||
|---------|--------|-------------|
|
||||
| `biome` | **Bukkit:** `<biome> [teleport=true]`. **Modded:** `<key>` | Find an Iris biome; teleport defaults to true on Bukkit |
|
||||
| `region` | **Bukkit:** `<region> [teleport=true]`. **Modded:** `<key>` | Find an Iris region |
|
||||
| `object` | **Bukkit:** `<object> [teleport=true]`. **Modded:** `<key>` | Find an object placement (Bukkit may teleport to the object studio first) |
|
||||
| `structure` | **Bukkit:** `<structure>`, runs sync. **Modded:** `<key>` | Find a vanilla/datapack/Iris structure |
|
||||
| `poi` | **Bukkit:** `<type> [teleport=true]`. **Modded:** `<type>` | Find a supported point of interest |
|
||||
| `unregistered` | — | Print structures excluded from goto completion, and the rejection reasons, to console |
|
||||
|
||||
---
|
||||
|
||||
## What: `/iris what`
|
||||
|
||||
**Bukkit origin:** player only, and there is no bare or `here` form. On modded the whole subtree is `LEVEL_ALL`, so any player can use it.
|
||||
|
||||
| Command | Platforms | Params | Description |
|
||||
|---------|-----------|--------|-------------|
|
||||
| (empty) / `here` | **Modded** | — | Full inspect at the 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 (for example `cave_floor`, `cave_ceiling`, `object`) |
|
||||
|
||||
---
|
||||
|
||||
## Edit: `/iris edit`
|
||||
|
||||
**Bukkit origin:** player. Opens pack JSON in the desktop editor. The same tree is also mounted at `/iris studio edit …` on Bukkit.
|
||||
|
||||
| Command | Aliases | Params | Description |
|
||||
|---------|---------|--------|-------------|
|
||||
| `biome` | `b` | **Bukkit:** `<biome>`. **Modded:** `[key]` | Open biome JSON (modded: omit the key for the current biome) |
|
||||
| `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] [center=0,0] [gui=true] [serial=false]` (`radius` alias `size`, `center` alias `middle`, `me` for the player position; `world` is contextual with no default). **Modded:** `<radius> [dimension] [at <x> <z>] [gui] [sync] [nocache]`, radius `1..100000` | Start pregen; radius in **blocks**; the resumable checkpoint cache is on by default on modded unless `nocache` |
|
||||
| `stop` | `x` | — | Stop the active pregen after in-flight work closes |
|
||||
| `pause` | `resume` | — | Toggle pause/resume |
|
||||
| `status` | | — | Progress, chunks/s, ETA, elapsed, method, failures |
|
||||
|
||||
**Bukkit:** `serial=true` requires a Paper-compatible server (strict serial chunk generation) and is rejected elsewhere. **Modded:** `sync` is the serial-like flag; `gui` opens the boss-bar/GUI path when available.
|
||||
|
||||
See `07 - Pregeneration.md`.
|
||||
|
||||
---
|
||||
|
||||
## Object: `/iris object` (`o`)
|
||||
|
||||
**Bukkit:** group origin is player. Root `wand`/`dust` also exist on modded.
|
||||
|
||||
| Command | Aliases | Platforms | Params | Description |
|
||||
|---------|---------|-----------|--------|-------------|
|
||||
| `wand` | | Both | — | Give the Iris object wand |
|
||||
| `dust` | `d` | Both | — | Give reveal dust |
|
||||
| `save` | | Both | **Bukkit:** `<name> [overwrite=false] [legacy=true]` (`overwrite` alias `force`; a contextual `dimension` is resolved from your world, or passed as `dimension=`). **Modded:** `[overwrite] <name>` | Save the wand selection as `.iob` |
|
||||
| `paste` | | Both | **Bukkit:** `<object> [edit=false] [rotate=0] [scale=1]`. **Modded:** `[at x y z] [rotate degrees] <key>` | Paste an object |
|
||||
| `expand` | | **Modded** | `[amount=1]`, range `1..256` | Expand the selection along your look direction |
|
||||
| `contract` | `-` | Both | `[amount=1]` | Contract the selection along your look direction |
|
||||
| `shift` | | Both | `[amount=1]` | Shift the selection along your look direction |
|
||||
| `position1` | `p1` | Both | **Bukkit:** `[here=true]` uses the block under your feet; `here=false` uses your look target. **Modded:** feet by default, `look` switches to the look target | Set one selection corner; requires the Iris wand in hand |
|
||||
| `position2` | `p2` | Both | same | Set the other selection corner |
|
||||
| `x+y` | `xpy` (modded) | Both | — | Autoselect up and out |
|
||||
| `x&y` | `xay` (modded) | Both | — | Autoselect up, down, and out |
|
||||
| `analyze` | | Both | `<object>` | Composition stats |
|
||||
| `shrink` | | Both | `<object>` | Shrink the object to its minimum bounds |
|
||||
| `plausibilize` | | Both | **Bukkit:** `<target> [dryrun=false] [reach=12]`; `target` accepts a key or a `prefix/`. **Modded:** greedy `<args>` parsed as `key [dryrun] [reach]`, same defaults | Grow branches so leaves survive vanilla decay |
|
||||
| `undo` | `u` | Both | `[amount=1]` | Undo pastes |
|
||||
| `we` | | **Bukkit**; modded stub | — | Wand plus import of the WorldEdit selection |
|
||||
| `studio` | | **Bukkit**; modded stub | `[dimension] [seed=1337]` | Object studio grid world |
|
||||
| `convert` | | **Bukkit**; modded stub | — | Convert `convert/` folder `.schem` to `.iob` |
|
||||
|
||||
---
|
||||
|
||||
## Studio: `/iris studio` (`std`, `s`)
|
||||
|
||||
| Command | Aliases | Platforms | Params | Description |
|
||||
|---------|---------|-----------|--------|-------------|
|
||||
| `open` | `o` | Both | **Bukkit:** `<dimension> [seed=1337]` (`dimension` alias `dim`, `seed` alias `s`). **Modded:** `<pack> [seed]` | Open a temporary studio dimension; the owning player may replace an active Jigsaw Studio, and Iris waits for its autosave and active-operation barriers before closing it |
|
||||
| `close` | `x` | Both | — | Close the studio and discard the world; Bukkit requires `/iris jigsaw close` for an active Jigsaw Studio |
|
||||
| `tpstudio` | `stp` | Both | — | Teleport into the open studio |
|
||||
| `status` | | **Modded** | — | Show the open studio and pack |
|
||||
| `create` | `+` | Both | **Bukkit:** `[name=studio] [template]`. **Modded:** `[name] [template=example]` | Create a pack project |
|
||||
| `pkg` | `package` | Both | **Bukkit:** `[dimension=default] [obfuscate=false] [minify=true]`. **Modded:** `[pack]` | Zip and package a pack. The Bukkit command name is `pkg`; `package` is the alias |
|
||||
| `version` | | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Pack version |
|
||||
| `regions` | | Both | **Bukkit:** `[radius=500]`, player origin. **Modded:** `[radius]`, default 500 | Nearby region distribution |
|
||||
| `noise` | `nmap` | Both | **Bukkit:** `[generator] [seed=12345]`. **Modded:** `[generator] [seed]` | Noise explorer GUI |
|
||||
| `map` | `render` | Both | **Bukkit:** contextual `world`, required. **Modded:** — | Vision map GUI |
|
||||
| `vscode` | `vsc` | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Generate and open the code workspace |
|
||||
| `update` | | Both | same pack argument as `vscode` | Regenerate the workspace only |
|
||||
| `importvanilla` | `importv`, `iv` | **Bukkit**; modded stub | `<dimension> [variants=3] [structures=true]` | Import vanilla trees/objects/structures into a pack |
|
||||
| `scoreboard` | `board`, `sidebar`, `sb` | **Bukkit** | — | Toggle the studio debug scoreboard |
|
||||
| `loot` | | **Bukkit**; modded stub | `[fast=false] [add=true]` | Simulate chest loot in a GUI |
|
||||
| `profile` | | **Bukkit**; modded stub | `[dimension=default]` | Pack performance profile |
|
||||
| `spawn` | `summon` | **Bukkit**; modded stub | `<entity> <location>` (`location` is contextual) | Spawn an Iris entity |
|
||||
| `objects` | `find-objects` | **Bukkit**; modded stub | — | IGenData chunk report for nearby chunks |
|
||||
|
||||
See `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## Jigsaw: `/iris jigsaw` (`jig`, `jgs`)
|
||||
|
||||
**Bukkit-only; player origin.** This opens a transient Jigsaw Studio through the same single active Studio lifecycle. Saved Iris jigsaw resources run through the shared core on every platform, but Fabric/Forge/NeoForge do not register this authoring command tree.
|
||||
|
||||
| Command | Params | Description |
|
||||
|---|---|---|
|
||||
| `create` | `<dimension> <key> [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic graph creation followed by open; `key` aliases `structure` and `name`; `mode` completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`; planar X/Z `3..128`, spatial X/Z `1..128`, Y `1..192`, volume `<=2,097,152` |
|
||||
| `convert` | `<dimension> <source> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, then open it; aliases `import`, `import-vanilla` |
|
||||
| `adopt inspect` | `<dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect an existing Iris closure and issue a hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, `clone` |
|
||||
| `adopt apply` | `<planId>` | Revalidate and atomically apply that player's unexpired plan, then open the target at seed `1337`; an active or opening Jigsaw Studio is rejected |
|
||||
| `open` | `<dimension> <key> [seed=1337]` | Open an existing graph in compact workcells; aliases `edit`, `reopen`; existing Iris structure keys tab-complete; owner, autosave, and operation barriers protect replacement |
|
||||
| `close` | `[discard=false]` | Close Studio; refuse active autosave/load/graph work or a pending dirty capture unless deliberately discarded |
|
||||
| `status` | — | Show project/workcell state and the current automatic seed-`1337` evaluation, theme, piece count, and diagnostic |
|
||||
| `menu` | — | Open the six-row controls also opened by the generated chest or three sneaks within 1.5 seconds |
|
||||
| `select` | — | Select the workcell containing the player |
|
||||
| `goto` | `<bay>` | Select and teleport above a stable workcell ID; alias `teleport` |
|
||||
| `particles` | `<visible>` | Toggle player-local workcell-bound, connector, and temporary assembly-preview particle trails |
|
||||
| `save` | `[bay=selected]` | Flush the selected dirty workcell's automatic capture now; normal block and container updates already autosave |
|
||||
| `connector channel` | `<channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position |
|
||||
| `bounds` | `<width> <height> <depth>` | Set the selected workcell capacity without resizing any variant object; every existing variant must fit, and the compact Studio layout regenerates in place; aliases `cell`, `resize` |
|
||||
| `workcell capacity` | `<width> <height> <depth>` | Explicit nested form of `bounds` (group alias `cell`); planar capacities are per canonical archetype and spatial capacity is the shared envelope for its one-row variant cells |
|
||||
| `workcell label` | `<displayName>` | Set the selected planar or spatial workcell's author label; quote spaces; solver identity remains canonical |
|
||||
| `workcell label-reset` | — | Reset the selected workcell to its canonical solver label; alias `reset-label` |
|
||||
| `pool create` | `<poolKey> [fallbackPoolKey=none]` | Atomically create an empty owned pool, optionally using an existing owned direct fallback |
|
||||
| `piece create` | `<poolKey> <pieceKey> [weight=1]` | Create and load an owned variant; planar derives connectors from the contextual canonical workcell |
|
||||
| `piece add` | `<poolKey> <pieceKey> [weight=1]` | Re-add and load an existing piece/object already owned by this project |
|
||||
| `piece remove` | `<poolKey>` | Remove the active variant from a pool without deleting its owned resources |
|
||||
| `piece rotatable` | `<rotatable>` | Persist cardinal rotation for the active variant; portable sessions reject `false` |
|
||||
| `piece expand` | — | Resize the active planar or spatial owned variant exactly to workcell capacity; planar canonical sockets move to the new faces |
|
||||
| `variant weight` | `<poolKey> <weight>` | Set the active variant's positive weight in an owned pool |
|
||||
| `variant resize` | `<width> <height> <depth>` | Resize only the active owned variant within its workcell capacity; safe shrink rejects cropped content and the active cell reloads in place |
|
||||
| `variant label` | `<displayName>` | Set the active variant's author label; quote spaces |
|
||||
| `variant label-reset` | — | Reset the active variant to its resource-key fallback; alias `reset-label` |
|
||||
| `variant duplicate` | — | Copy the active variant's object, metadata, and exact pool memberships into one new variant in this workcell |
|
||||
| `variant duplicate-family` | `[themeKey=next]` | Atomically clone every enabled workcell's active owned variant into one coherent Iris family and load the whole family; alias `family` |
|
||||
| `rules limits` | `<maxDepth> <maxSizeChunks>` | Set depth `1..30` and radius `1..32` (group alias `rule`); `VANILLA_PORTABLE` is restricted to `<=20` and `<=8` |
|
||||
| `rules fallback` | `<poolKey> <fallbackPoolKey>` | Set or clear one owned pool's direct fallback after compiling the complete graph; pass `none` to clear. Unlike `pool create`, this argument is required |
|
||||
| `preview goto` | — | Teleport above the permanent seed-`1337` block preview; alias `teleport` |
|
||||
| `preview assemble` | `[seed=1337]` | Compute a deterministic read-only assembly at the player, report its complete piece count, and show in-range bounds as purple particle boxes for 10 seconds within the shared particle budget; places no blocks |
|
||||
| `export` | `[namespace=iris] [output=jigsaw-export] [format=zip] [replace=false]` | Start a background strict Minecraft 26.2 vanilla datapack export as one direct artifact under the Studio packs `exports/` folder |
|
||||
| `delete` | `[confirm=false]` | With `confirm=true`, scan reverse references, close Studio, and hash-pinned-delete the complete owned project; alias `remove` |
|
||||
|
||||
There is no Jigsaw Studio undo command, adoption rollback command, or mod-loader authoring command. **Undo Last Autosave** in Workcell Settings restores the newest of five previous saved graph iterations retained in one `.iris/jigsaw-history/key-<sha256>.json` file. **Reset Connector Blocks** restores the selected workcell's saved connector blocks without replacing its other edited blocks. Planar Studio always has six independently capacitated/enabled canonical workcells. Spatial Studio places every variant in a dedicated one-row cell; a new project seeds seven 15×15×15 variants with 0 through 6 cumulative face-center connectors. All cells retain one clear block of separation and use physical white-concrete edge cages with player-local particle trails inside them; Jigsaw Studio spawns no display entities for workcell bounds. Every variant retains its own exact dimensions and optional display label. Workcell and variant rename tools are renamed in an anvil, right-clicked to apply, and sneak-right-clicked to reset. A catalog may contain at most 512 variants. Natural creature spawning is disabled in the transient Studio world. The seed-`1337` assembly is evaluated automatically and rendered as a permanent protected block preview; spatial previews form an elevated connected assembly, while `preview assemble` remains the separate temporary arbitrary-seed particle diagnostic. See `21 - Jigsaw Structures.md` for GUI/toolbox controls, whole-assembly theme chances, independent pool-entry chances, rules/caps, markers, ownership, placement, export, and recovery.
|
||||
|
||||
Bukkit has one global Studio project/world and the Jigsaw session belongs to one owning player. Only that owner can control, load, or mutate it; entering a workcell makes that physical cell the owner's next menu selection. Non-owner edits are cancelled and non-owner commands use a strict informational/communication allowlist. Block and inventory changes in loaded owned workcells autosave after a 40-tick quiet period. Duplicate-one and duplicate-family actions queue once behind pending autosave, expedite it, and continue automatically against the same request and source variants. The chest and live preview are protected; schema-1 or otherwise stale toolbox sticks are rejected. A later mutation after capture starts remains dirty for another capture. Plugins that bypass covered events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`.
|
||||
|
||||
---
|
||||
|
||||
## Pack: `/iris pack` (`pk`)
|
||||
|
||||
| Command | Aliases | Params | Description |
|
||||
|---------|---------|--------|-------------|
|
||||
| `validate` | `v` | **Bukkit:** `<pack>` (required despite the "leave empty for all" description). **Modded:** `[pack]`; empty means all | Validate pack(s) and publish results |
|
||||
| `cleanup` | `c` | **Bukkit:** `<pack> [mode=preview]`. **Modded:** `<pack> [apply]` | Preview or quarantine unused resources |
|
||||
| `restore` | `r` | same pattern as `cleanup` | Preview or restore the latest quarantine |
|
||||
| `status` | `s` | **Bukkit:** `<pack>` (required, same blank-default trap). **Modded:** `[pack]` | Startup-published validation status, including persisted unchanged results |
|
||||
|
||||
See `25 - Pack Management.md`.
|
||||
|
||||
---
|
||||
|
||||
## Structure: `/iris structure` (`struct`, `str`)
|
||||
|
||||
Bukkit `dimension` parameters all carry the alias `dim`.
|
||||
|
||||
| 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 origin. **Modded:** `<key>` | Assemble and place at the player; Bukkit reports the exact changed-block count and rejects air-only or already-identical no-op results |
|
||||
| `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded stub | `<dimension>` | Import all vanilla/datapack structures as editable Iris resources (overwrites) |
|
||||
| `capture` | `cap` | **Bukkit**; modded stub | `<dimension>` | Capture code-only structures via a scratch world |
|
||||
| `verify` | `locateall` | Both | **Bukkit:** `<dimension> [radius=48]`, clamped to `1..1000`. **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 stub | `[restart=false]` | Download and install Modrinth `datapackImports` into world datapacks |
|
||||
| `list` | `ls` | Both | — | **Bukkit:** configured imports plus installed. **Modded:** configured/installed world datapacks |
|
||||
| `remove` | `rm` | **Bukkit**; modded stub | `<id>` | Remove an installed datapack by id |
|
||||
| `status` | | **Modded** | — | Check Iris dimension-type overrides against pack heights |
|
||||
| `install` | | **Modded** | — | Install the 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` / `loadWorld` / `unloadWorld` / `remove` / `evacuate` instead. `[seed]` accepts a number or the literal `random`; blank means `1337`.
|
||||
|
||||
| Command | Aliases | Params | Description |
|
||||
|---------|---------|--------|-------------|
|
||||
| `enable` | `create` | `<dimension> <pack\|pack:dimensionKey> [seed\|random]` | Create/inject a persistent Iris dimension (downloads the pack if missing) |
|
||||
| `replace-overworld` | | `<pack\|pack:dimensionKey> [seed\|random]` | Inject primary world routing |
|
||||
| `mainworld` | | `<pack\|pack:dimensionKey\|off> [seed\|random]` | Configure the 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 plus primary world config |
|
||||
|
||||
---
|
||||
|
||||
## Developer: `/iris Developer` (`dev`)
|
||||
|
||||
| Command | Aliases | Platforms | Params | Description |
|
||||
|---------|---------|-----------|--------|-------------|
|
||||
| `EngineStatus` | | **Bukkit** | — | Loaded tectonic plate count |
|
||||
| `Sentry` | `sentry` (modded) | Both | — | Send a test exception to the error reporter |
|
||||
| `genhash` | | **Bukkit** | `[radius=4] [centerX=0] [centerZ=0]`, contextual `world` | Hash generated blocks in a fixed area. The center parameters are `centerX`/`centerZ` here, not the hyphenated `goldenhash` names |
|
||||
| `update-world` | `^world` | **Bukkit** | `[confirm=false] [fresh-download=false]`, contextual `world` and `pack` (`pack` alias `dimension`; `confirm` alias `c`; `fresh-download` aliases `fresh`, `new`) | Unsafe pack swap into a world |
|
||||
| `mantle` | | **Bukkit** | `[plate=false] [name=21474836474]` | Dump a mantle section or plate under the dump folder |
|
||||
| `packBenchmark` | | **Bukkit** | `[dimension=overworld] [radius=2048] [gui=false]` (`dimension` alias `pack`) | Pack benchmark |
|
||||
| `upgrade` | | **Bukkit** | `[version=latest]` | Data version upgrade helper |
|
||||
| `mca` | | **Bukkit** | `<world>` (a world folder path) | Scan MCA region files |
|
||||
| `delete-chunk` | `dc` | **Bukkit** | `[radius=0]`, player origin | Delete nearby chunk blocks for regen testing |
|
||||
| `network` | `ip` | Both | — | List network interfaces |
|
||||
| `regen` | `rg` | **Bukkit** (modded root) | `[radius=5]`, player origin | Delete and regenerate nearby chunks |
|
||||
| `goldenhash` | `gold` | **Bukkit** (modded root) | `[radius=8] [center-x=0] [center-z=0] [reset-mantle=true] [threads=8] [deep=false]`, contextual `world` | Buffer golden hash capture/verify |
|
||||
|
||||
The modded developer group implements only `sentry` and `network`/`ip`; its help section still advertises a region file scan that has no command node.
|
||||
|
||||
---
|
||||
|
||||
## Platform gap summary
|
||||
|
||||
| Feature | Bukkit | Modded |
|
||||
|---------|--------|--------|
|
||||
| Root permission node | `iris.all` (code-gated, whole tree) | `LEVEL_GAMEMASTERS` / `LEVEL_ALL` per node |
|
||||
| World lifecycle | `create`, `loadWorld`, `unloadWorld`, `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 |
|
||||
| Jigsaw Studio create/edit/autosave/export commands and GUI | yes | no; copy a Bukkit-authored Iris pack |
|
||||
| Structure import/capture | yes | messages (run on Bukkit, copy the 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` |
|
||||
| `pack validate` / `status` with no pack | rejected (blank default is required) | validates all packs |
|
||||
| Tree feller permission | `iris.treefeller` | `irisworldgen:treefeller` via the loader permission API |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- `03 - Configuration.md`
|
||||
- `02 - Getting Started.md`
|
||||
- `06 - Worlds & Lifecycle.md`
|
||||
- `07 - Pregeneration.md`
|
||||
- `10 - Studio & VSCode Schemas.md`
|
||||
- `21 - Jigsaw Structures.md`
|
||||
- `25 - Pack Management.md`
|
||||
- `28 - Integrations.md`
|
||||
- `30 - Platform Differences.md`
|
||||
- `32 - Determinism & Goldenhash.md`
|
||||
@@ -1,229 +0,0 @@
|
||||
# 05 - Concepts & Pack Layout
|
||||
|
||||
A pack is a folder of JSON files, binary objects, and images that fully describes one or more worlds. Iris loads it through `IrisData`, which registers one loader per resource type, turns short string keys into files on disk, and caches what it reads. Every world you create gets its own frozen copy of the pack; only Studio worlds read the folder you are editing.
|
||||
|
||||
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`.
|
||||
|
||||
## What a pack actually is
|
||||
|
||||
There is no manifest file, no registry, and no build step. A pack is a directory whose subfolder names tell Iris what type each file is. `biomes/plains.json` is a biome because it sits in `biomes/`. Move that same file to `regions/` and Iris will try to parse it as a region.
|
||||
|
||||
The pack folder's own name is the pack key. A folder called `packs/myworld/` is the pack `myworld`. Rename the folder and you have renamed the pack.
|
||||
|
||||
The only hard requirement is at least one `.json` file directly inside `dimensions/`. Everything else is optional, and a folder you never create simply has no resources of that type.
|
||||
|
||||
## A pack you can read in one screen
|
||||
|
||||
```text
|
||||
packs/myworld/
|
||||
dimensions/
|
||||
myworld.json -> key "myworld"
|
||||
regions/
|
||||
main.json -> key "main"
|
||||
biomes/
|
||||
plains.json -> key "plains"
|
||||
hills/
|
||||
rolling.json -> key "hills/rolling"
|
||||
generators/
|
||||
plain.json -> key "plain"
|
||||
```
|
||||
|
||||
Five files. `dimensions/myworld.json` lists `"main"` in its `regions` array. `regions/main.json` lists `"plains"` and `"hills/rolling"` in `landBiomes`. Each biome names `"plain"` as a generator. That chain is the whole pack.
|
||||
|
||||
Note `hills/rolling`. Subfolders are yours to organize however you like — they become part of the key, and nothing else changes.
|
||||
|
||||
## Keys: the one rule
|
||||
|
||||
**A key is the file's path under its type folder, with the extension removed.**
|
||||
|
||||
| File on disk | Type folder | Key you write in JSON |
|
||||
|---|---|---|
|
||||
| `biomes/plains.json` | `biomes/` | `plains` |
|
||||
| `biomes/temperate/plains.json` | `biomes/` | `temperate/plains` |
|
||||
| `objects/trees/oak/big.iob` | `objects/` | `trees/oak/big` |
|
||||
| `snippet/style/soft.json` | (snippets, see below) | `snippet/style/soft` |
|
||||
|
||||
There is no namespace and no type prefix. You never write `biomes/plains` or `iris:plains` — the field you are filling in already knows it wants a biome, so it searches `biomes/` for you. Cross-references everywhere (region biome lists, object placements, spawner entity ids, structure piece pools) use exactly these keys.
|
||||
|
||||
### What happens when the exact file is missing
|
||||
|
||||
Iris first tries `<typeFolder>/<key>.json` and returns it if it exists. That is the normal path and the only one that works for nested keys.
|
||||
|
||||
If there is no exact hit, Iris scans the type folder's own files (not subfolders) for any name whose first dot-segment equals the key. This is what makes `plains.disabled.json` still load for key `plains` — useful for parking a variant, surprising if you forgot you did it. If two files match, Iris logs `Ambiguous <type> <key> in <folder>: ...` and takes the alphabetically first one. Keep one canonical filename per key and this never bites you.
|
||||
|
||||
The literal string `"null"` is refused with a warning by direct file lookups and by warning-enabled loads. Silent loads (the cross-pack fallback search) do not refuse it and will look for `null.json`. Do not name a file `null.json`.
|
||||
|
||||
## How the pieces relate
|
||||
|
||||
Generation walks a graph, and the graph starts at exactly one place: the dimension you named when you created the world.
|
||||
|
||||
```text
|
||||
dimension -> regions -> biomes -> generators (terrain height/noise)
|
||||
-> objects (.iob models)
|
||||
-> decorators (surface clutter)
|
||||
-> structures (jigsaw / native)
|
||||
-> spawners -> entities
|
||||
-> loot
|
||||
```
|
||||
|
||||
- **Dimension** — the root. Sets world height, environment, seed behavior, and which regions exist. One dimension equals one world type.
|
||||
- **Region** — a spatial zone. Regions decide which biomes can appear where, and can carry their own objects and structures that span biome edges.
|
||||
- **Biome** — the workhorse. Block layers, surface treatment, decorations, object placements, structures, and mob spawns.
|
||||
- **Generator** — noise and height math. Biomes reference generators to get terrain shape; several biomes can share one.
|
||||
- **Object** — a `.iob` block model with its own placement rules.
|
||||
- **Structure / jigsaw pool / jigsaw piece** — multi-piece assemblies, either Iris-native or bridged to vanilla structures.
|
||||
|
||||
The practical consequence: **a file that nothing references is inert.** It parses, it validates, it never generates. When a resource you wrote is not showing up, the first question is not "is the JSON wrong" but "is it reachable from the dimension." Work forward from `dimensions/<key>.json` and find where the chain breaks.
|
||||
|
||||
## Trace one reference end to end
|
||||
|
||||
Do this once on a pack you did not write. It takes two minutes and makes everything above concrete.
|
||||
|
||||
Prerequisites: a loadable pack under the packs root, `iris.all` (Bukkit) or gamemaster (modded), and an editor that will not reformat your JSON.
|
||||
|
||||
1. Validate first, so you know a later failure is yours: `/iris pack validate pack=overworld` on Bukkit, `/iris pack validate overworld` on a mod loader.
|
||||
2. Open `dimensions/overworld.json`. Pick one key out of the `regions` array.
|
||||
3. Open `regions/<that key>.json`. Pick one key out of `landBiomes`.
|
||||
4. Open `biomes/<that key>.json`. Follow its first generator, object, decorator, or structure reference into the matching type folder.
|
||||
5. At each hop, confirm the key is the path under the type folder with the extension removed — nothing more.
|
||||
6. Open the pack in Studio, focus that biome, save one valid edit, and wait for hotload. Re-validate.
|
||||
|
||||
You are done when every reference resolved without guessing at a namespace or filename, hotload succeeded, and validation reports no blocking errors.
|
||||
|
||||
## Snippets
|
||||
|
||||
A snippet is a JSON fragment you write once and reference from many places. Types tagged `@Snippet("<type>")` in the engine accept either an inline object or a string pointing at a snippet file.
|
||||
|
||||
```json
|
||||
"style": "snippet/style/soft-hills"
|
||||
```
|
||||
|
||||
resolves to `<packRoot>/snippet/style/soft-hills.json`.
|
||||
|
||||
| Rule | Actual behavior |
|
||||
|---|---|
|
||||
| Trigger | Only a JSON **string** value. Inline objects parse normally and never touch the snippet path |
|
||||
| Required prefix | The string must start with `snippet/`. Anything else resolves to `null` **with no log line at all** — the most common silent snippet failure |
|
||||
| Re-rooting | If the string starts with `snippet/` but not `snippet/<thisType>/`, Iris strips `snippet/` and re-roots the remainder under this field's own type. So `snippet/decorator/foo` on a style field becomes `snippet/style/decorator/foo`, not an error |
|
||||
| On-disk path | `<packRoot>/snippet/<type>/<name>.json`, resolved from the pack root, not from the type folder |
|
||||
| Subfolders | Allowed; `<name>` may contain `/`. Discovery walks the tree recursively |
|
||||
| Missing file | Logs `Couldn't find snippet <path> in <file>` and yields `null` for that field |
|
||||
| Unreadable file | Logs `Couldn't read snippet <path> in <file> (<message>)` and yields `null` |
|
||||
| Inline parse failure | Different path: logs `Failed to read <type>... faking objects a little`, then substitutes a **default-constructed instance**, not `null` |
|
||||
| Schema | Studio writes `.iris/schema/snippet/<type>-schema.json` so the editor offers completions for `snippet/<type>/…` |
|
||||
|
||||
The shipping overworld uses `snippet/decorator/*` and `snippet/style/*`.
|
||||
|
||||
## Two copies of every pack
|
||||
|
||||
This is the concept that causes the most confusion, so it is worth being blunt about.
|
||||
|
||||
**The pack you edit and the pack a world generates from are different files.**
|
||||
|
||||
When you create a non-Studio world, Iris copies the entire pack tree into `<world>/iris/pack` and the world's engine reads only that copy for the rest of its life. Editing `packs/overworld/` afterwards changes nothing about that world. This is deliberate: a world's terrain must stay reproducible even if you keep authoring.
|
||||
|
||||
Studio worlds are the exception. A Studio world's engine points directly at the live pack folder and watches it for changes, which is what makes hotload possible.
|
||||
|
||||
| Mode | Pack the engine reads | Hotload | Copied into the world? |
|
||||
|---|---|---|---|
|
||||
| Studio (`studio=true`) | Live `packs/<key>/` (or the studio project path) | Yes | No |
|
||||
| Production create | `<world>/iris/pack` | No | Yes, atomic stage then publish via `StudioSVC.installIntoWorld` |
|
||||
| Benchmark | `<world>/iris/pack` | Studio flag still governs transient cleanup | Yes |
|
||||
|
||||
Hotload opens a fresh `IrisData` on the same folder, reloads the dimension by its key, builds a replacement engine runtime under the lifecycle lock, publishes it, retires the old `IrisData`, then refreshes the editor workspace and datapacks in the background. If any step fails it rolls back to the previous runtime and reports the error.
|
||||
|
||||
The watcher polls every 250 ms but only checks the folder about once per second, backing off to once per 4 s during maintenance or within 2 s of chunk generation. It watches `.json` and `.iob` and ignores anything under `.iris`. It runs only while the world is a Studio world that is not closing and not in jigsaw-studio mode.
|
||||
|
||||
To push pack edits into an existing production world, see `update-world` in `25 - Pack Management.md`, or just create a new world — which is the right answer for any change to height or dimension type.
|
||||
|
||||
## Where packs live
|
||||
|
||||
| What | Bukkit-family | Fabric / Forge / NeoForge |
|
||||
|---|---|---|
|
||||
| Packs you author and download into | `plugins/Iris/packs/<key>/` | `config/irisworldgen/packs/<key>/` |
|
||||
| Platform data dir (`settings.json`, languages, caches) | `plugins/Iris/` | `config/iris/` |
|
||||
| A world's frozen snapshot | `<dimensionRoot>/iris/pack/` | same, under the modded world root |
|
||||
| Prefetch key indexes | `<platform data dir>/prefetch/<dimId>/<hash>.ipfch` | same |
|
||||
| Studio schemas | `<packRoot>/.iris/schema/` | same |
|
||||
|
||||
On mod loaders the pack root and the platform data dir are two different folders — packs go under `config/irisworldgen/`, everything else under `config/iris/`. If you are hand-placing a pack on a modded server, `config/irisworldgen/packs/` is the one that matters.
|
||||
|
||||
Folders whose names start with `.` are skipped when Iris lists packs, which is why `.iris/` inside a pack is invisible to the pack listing. Pack listing itself follows symbolic links; the stricter check (`requireSafePackTree`, used when installing a pack into a world) refuses a symlinked root, any symlink anywhere in the tree, and any non-regular file, and skips hidden subtrees.
|
||||
|
||||
## Registrant folders
|
||||
|
||||
`IrisData` registers 17 loaders. Each one owns exactly one folder name and one file extension.
|
||||
|
||||
| Folder | Extension | What lives here and when you touch it |
|
||||
|---|---|---|
|
||||
| `dimensions/` | `.json` | World roots. Height, environment, region list, imports. **Required** — a pack with none is not loadable |
|
||||
| `regions/` | `.json` | Which biomes appear in which climate zone, plus region-wide objects and structures |
|
||||
| `biomes/` | `.json` | Where most authoring time goes: layers, surface, decorators, objects, structures, spawns |
|
||||
| `generators/` | `.json` | Reusable noise/height math that biomes point at. Edit here to change terrain shape across many biomes at once |
|
||||
| `objects/` | `.iob` | Binary block models saved from the wand or imported from schematics. Referenced by placements, never edited as text |
|
||||
| `structures/` | `.json` | Structure graphs, including the `minecraft_*` graphs that bridge vanilla structures |
|
||||
| `jigsaw-pools/` | `.json` | Weighted sets of pieces a jigsaw connector can pick from |
|
||||
| `jigsaw-pieces/` | `.json` | One placeable piece: its object, its connectors, its rules |
|
||||
| `entities/` | `.json` | Entity definitions with equipment, attributes, and custom data, used by spawners and markers |
|
||||
| `spawners/` | `.json` | When and where entities spawn — time, block, biome, and rate rules |
|
||||
| `markers/` | `.json` | Named points Iris records during generation so spawners and other systems can find them later |
|
||||
| `loot/` | `.json` | Iris loot tables applied to generated containers |
|
||||
| `blocks/` | `.json` | Named custom block states you can reference instead of repeating long block data strings |
|
||||
| `expressions/` | `.json` | Math expressions callable from generators and placement rules |
|
||||
| `images/` | `.png` | PNG maps sampled as noise or as direct biome/height input |
|
||||
| `matter/` | `.mat` | Matter binaries. The loader exists and resolves keys, but no runtime system consumes them |
|
||||
| `mods/` | `.json` | Injector/replacer documents. Loaded so schemas and tooling see them; the engine has no path that applies them |
|
||||
|
||||
Anything else in a pack directory is not a resource type. The shipping overworld ships empty `caves/`, `ravines/`, and `jigsaw-structures/` folders plus `README.md`, `Schema.json`, and a `.code-workspace` file — none of those names are keys, and none are loaded.
|
||||
|
||||
A reduced init path used by the datapack compiler registers only `biomes` and `dimensions`; that is internal and not something a pack author configures.
|
||||
|
||||
## What makes a pack loadable
|
||||
|
||||
`PackValidator` fails fast on three structural problems, in order:
|
||||
|
||||
1. The pack folder is missing or is not a directory.
|
||||
2. There is no `dimensions/` directory.
|
||||
3. There are no `*.json` files **directly inside** `dimensions/`. Nested dimension files do not count toward this check.
|
||||
|
||||
Passing those three does not mean the pack is loadable. `PackValidator` then runs roughly ten content validators — dimension, cave profile, loot, object/surface, structure graph, native structure, spawn, and content-key checks — and any blocking error from those also makes the pack not loadable. Content-key problems are blocking only under strict content mode. Read the first blocking error and fix that one; the rest are usually downstream.
|
||||
|
||||
Presence on disk is a weaker notion than loadability: a pack "exists" if its directory is safe and holds at least one non-symlink `dimensions/*.json`.
|
||||
|
||||
### Download key rules
|
||||
|
||||
Downloaded pack keys must match `[a-z0-9_-]+`, and the check applies both to a caller-supplied expected key and to the key Iris derives from the archive.
|
||||
|
||||
An archive with no expected key must contain exactly one dimension; its load key becomes the install folder name. Managed-release and listing downloads carry an exact expected key, so their archive may hold extra dimensions — the expected key picks the folder name, and the whole pack is validated before publication.
|
||||
|
||||
## When a resource does not resolve
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| File exists, key does not resolve | You included the extension or the type folder in the key, the case differs, or you counted the path from the wrong root | Rebuild the key as the exact relative path under the type folder, extension removed |
|
||||
| Nested dotted variant not found | The dotted-name fallback only scans the type folder's own files, never subfolders | Give nested files their exact key name, or move the variant to the type folder root |
|
||||
| File validates but never generates | Nothing in the dimension → region → biome chain references it, or a chance/filter excludes it | Trace forward from the dimension root; test with Studio focus or buffet mode |
|
||||
| Snippet silently becomes null | The string does not start with `snippet/` — this failure logs nothing | Write the full `snippet/<type>/<name>` form |
|
||||
| Snippet loaded the wrong file | A `snippet/<otherType>/…` string was re-rooted under this field's own type | Use the type that matches the field |
|
||||
| Studio does not offer a new resource in completions | Workspace schema enums are stale | `/iris studio update dimension=<pack>` on Bukkit, `/iris studio update <pack>` on modded |
|
||||
| Console warns "Ambiguous \<type\> \<key\>" | Two files share a base name before the first dot | Keep one canonical filename; Iris took the alphabetically first |
|
||||
| Production world ignores your fix | It is reading `<world>/iris/pack`, not your live pack | Validate in Studio, then run the explicit world-update workflow or create a new world |
|
||||
|
||||
## The shipping overworld pack
|
||||
|
||||
For orientation when reading `packs/overworld/`:
|
||||
|
||||
| Path | What is in it |
|
||||
|---|---|
|
||||
| `dimensions/overworld.json` | The single root dimension |
|
||||
| `regions/*.json` | Climate zones that partition the biome set |
|
||||
| `biomes/**` | Nested biome sets — temperate, hot, frozen, ocean, and so on |
|
||||
| `generators/**` | Shared terrain generators |
|
||||
| `objects/**` | `.iob` trees, structures, clutter, and vanilla imports |
|
||||
| `structures/*.json` | Structure graphs, including `minecraft_*` bridges |
|
||||
| `jigsaw-pieces/**`, `jigsaw-pools/**` | Jigsaw assembly data |
|
||||
| `entities/standard/**`, `spawners/**`, `loot/**` | Mob and loot content |
|
||||
| `images/*.png` | Noise and map images |
|
||||
| `snippet/decorator/**`, `snippet/style/**` | Shared fragments referenced across biomes |
|
||||
|
||||
Feature-level detail: `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`.
|
||||
@@ -1,318 +0,0 @@
|
||||
# 06 - Worlds & Lifecycle
|
||||
|
||||
Creating an Iris world copies the pack into the world folder, registers the world so the server rebuilds it on every boot, and hands generation to the Iris engine. This page covers the full lifecycle on Bukkit-family servers and on Fabric, Forge, and NeoForge: create, load, unload, remove, main-world promotion, and the exact-slot replacement path. Iris worlds are managed under the level root as `dimensions/iris/<key>/` on Bukkit; mod loaders keep theirs in `iris-dimensions.json`.
|
||||
|
||||
See also: `02 - Getting Started.md`, `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`.
|
||||
|
||||
## Create a world you intend to keep
|
||||
|
||||
The difference between a throwaway world and one you will still be running in six months is that you decide the pack, seed, and height **before** the first chunk generates. None of those are editable afterwards without regenerating terrain.
|
||||
|
||||
Before you start: a pack that validates, a seed you have written down, a current backup, and no other lifecycle command running.
|
||||
|
||||
### Bukkit-family
|
||||
|
||||
```text
|
||||
/iris pack validate pack=overworld
|
||||
/iris studio open overworld seed=1337
|
||||
```
|
||||
|
||||
Fly around, look at the terrain, then close the studio:
|
||||
|
||||
```text
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
Now create the real world. This is the step that freezes the pack:
|
||||
|
||||
```text
|
||||
/iris create release_candidate type=overworld seed=1337
|
||||
```
|
||||
|
||||
On Folia, this stages files and prints a restart instruction — stop the server, start it again, and the world loads on boot. On every other Bukkit-family server the world is created immediately.
|
||||
|
||||
```text
|
||||
/iris worlds
|
||||
/iris tp release_candidate
|
||||
```
|
||||
|
||||
**Success looks like:** `release_candidate` appears in `/iris worlds` as a loaded Iris world, you spawn in it, and chunks generate as you fly.
|
||||
|
||||
Now prove it survives a restart, because a world that only works in the session that created it is not actually created:
|
||||
|
||||
```text
|
||||
/iris pregen start 352 world=release_candidate center=0,0 gui=false
|
||||
```
|
||||
|
||||
Wait for it to finish (see `07 - Pregeneration.md`), restart the server cleanly, teleport back in, and fly past the pregenerated boundary. New terrain must still appear.
|
||||
|
||||
**The world is now committed.** It generates from `<world>/iris/pack`, its own frozen copy. Continuing to edit `packs/overworld/` affects Studio only. Never delete or replace that snapshot while the world is loaded. To ship pack changes into it later, use the deliberate path in `25 - Pack Management.md`; for anything that changes height or dimension type, create a new world instead.
|
||||
|
||||
### Fabric / Forge / NeoForge
|
||||
|
||||
```text
|
||||
/iris pack validate overworld
|
||||
/iris world enable irisworldgen:release_candidate overworld 1337
|
||||
/iris world status
|
||||
/iris tp irisworldgen:release_candidate
|
||||
```
|
||||
|
||||
The seed argument is optional and defaults to `1337`. `enable` also accepts the alias `create`, and the whole group is reachable as `/iris w`.
|
||||
|
||||
```text
|
||||
/iris pregen start 352 irisworldgen:release_candidate at 0 0
|
||||
```
|
||||
|
||||
Restart the server when it finishes.
|
||||
|
||||
**Success looks like:** `/iris world status` lists the same dimension with the same pack after restart, and `/iris info irisworldgen:release_candidate` as a gamemaster reports seed `1337` read back from `iris-dimensions.json`.
|
||||
|
||||
From here, `/iris world disable <dimension>` unloads it and keeps the files; `/iris world delete <dimension>` is the destructive path. Both require the dimension argument.
|
||||
|
||||
## Remove a world without losing anything else
|
||||
|
||||
Removal is the operation most likely to cost you data, so the order matters.
|
||||
|
||||
1. **Back up first.** Nothing below is undoable.
|
||||
2. **Get everyone out.** `/iris evacuate <world>` moves players to another loaded world, or kicks them if there is nowhere to go. Removal does this for you, but doing it deliberately means you see who was in there.
|
||||
3. **Unload it.** `/iris unload <world>`. This marks the world for maintenance, evacuates, unloads through the lifecycle service, and closes the generator.
|
||||
4. **Remove it.** `/iris remove <world>` deletes the files. `/iris remove <world> delete=false` keeps them and only unregisters — use this when you want the directory back later.
|
||||
5. **Read the status Iris prints.** It tells you what actually happened; see the status table below.
|
||||
|
||||
**Success looks like:** `UNREGISTERED` (files kept) or `DELETED` (files gone), the world is absent from `/iris worlds`, and its directory under `<levelRoot>/dimensions/iris/` matches what you asked for.
|
||||
|
||||
`DELETE_QUEUED` means the files could not be deleted now and were quarantined for deletion at next startup. Restart and confirm the target is gone before reusing that name.
|
||||
|
||||
On mod loaders the equivalent is `/iris world delete <dimension>`, which disables and then wipes chunk and mantle data.
|
||||
|
||||
### If unload hangs
|
||||
|
||||
Unload has a hard 150-second ceiling. If the world, generator, or scheduler work has not settled by then, Iris marks a terminal timeout, requests a server restart, and fails the command. Let the restart happen. Do not delete a live world directory to force the issue.
|
||||
|
||||
## Lifecycle recovery
|
||||
|
||||
| Symptom | What it means | What to do |
|
||||
|---|---|---|
|
||||
| "busy" response | Another lifecycle operation holds the coordinator. It is one global mutex, so a pack download or publish blocks world create just as much as another create does | Wait for the running operation. Retrying concurrently will not help |
|
||||
| Startup validation pending / failed / restart-required on login or create | External datapack ingestion or dimension-pack validation has not reached a safe state | Fix the first logged failure, or complete the requested restart. Do not hand-create world folders or hand-edit `bukkit.yml` |
|
||||
| Folia create succeeded but teleport says no such world | Folia create only stages files and registration | Restart, then load or teleport |
|
||||
| Load reports missing or inconsistent data | The dimension root, the `bukkit.yml` registration, or the `iris/pack` snapshot is incomplete | Keep the directory and restore from backup. Load never re-downloads a snapshot |
|
||||
| Unload hits its terminal timeout | Work did not drain in 150 s | Allow the restart. Do not force-delete the live directory |
|
||||
| Remove returns `DELETE_QUEUED` | Files were quarantined for startup deletion | Restart, confirm the target is gone, then reuse the name |
|
||||
| Modded registry renamed to `.broken-<timestamp>` | The whole `iris-dimensions.json` failed to parse | Keep the backup. Iris logs whatever ids it could salvage from the raw text; recreate each with its original pack, dimension, and seed, then verify with `/iris world status` |
|
||||
|
||||
## Identity and storage
|
||||
|
||||
| Item | Rule |
|
||||
|---|---|
|
||||
| Managed namespace | The safe/managed API accepts `iris` only, so create, load, and remove can never touch a `minecraft:` or third-party dimension folder |
|
||||
| Logical name | For `iris:foo` the logical name is `foo` — that is what you type in commands |
|
||||
| Storage root | `Server#getLevelDirectory` on Paper. If that method is missing, Iris latches a permanent fallback to `<world-container>/<level-name>` read from `server.properties` (default `world`) |
|
||||
| Dimension folder | `<levelRoot>/dimensions/iris/<key>/` |
|
||||
| Pack snapshot | `<dimensionRoot>/iris/pack/` |
|
||||
| Pregen cache | `<dimensionRoot>/iris/pregen/` |
|
||||
| Registry | `worlds.json` in the Iris data folder (a flat `worldIdentity → dimensionType` map, written atomically) plus the `worlds:` section of `bukkit.yml`, which stores `generator: "Iris:<dimension>"` and the seed |
|
||||
| Name normalization | The name is lowercased and spaces become `_` before validation, so `My World` becomes `my_world` rather than being rejected |
|
||||
| Name constraints | After normalization the key must match `[a-z0-9_-]+`; `/`, `\`, and `..` are rejected as unsafe path segments, and symlinks on any component of the dimension root are refused |
|
||||
| Reserved names | `/iris create` rejects `iris` and `benchmark` case-insensitively. This is a create-time check only; the storage layer does not enforce it |
|
||||
|
||||
The vanilla main, nether, and end worlds map to `minecraft:` keys derived from `level-name`, `level-name_nether`, and `level-name_the_end`. They are not Iris-managed dimension folders and are only reachable through the exact-slot replacement path below.
|
||||
|
||||
## Command surface (Bukkit)
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `/iris create <name> [type=default] [seed=1337] [main=false] [overwrite=false]` | Create a managed world now, stage one for Folia's next boot, or stage an exact-slot replacement |
|
||||
| `/iris load <name>` / `/iris import <name>` | Reconcile a world that already exists on disk back into the server. Never downloads anything |
|
||||
| `/iris unload <world>` | Evacuate, unload, close the generator. The safe first half of removal |
|
||||
| `/iris remove <name> [delete=true]` | Unregister the world, and by default delete its files |
|
||||
| `/iris evacuate <world>` | Move every player out of an Iris world, or kick them if no other world is loaded |
|
||||
| `/iris tp <world> [player=<name>]` | Teleport yourself, or a named player, to the world spawn |
|
||||
| `/iris worlds` | List which loaded worlds are Iris worlds and which are not |
|
||||
|
||||
Aliases and permissions: `04 - Commands & Permissions.md`.
|
||||
|
||||
### Create parameters
|
||||
|
||||
| Param | Default | What it controls |
|
||||
|---|---|---|
|
||||
| `name` (`world-name`) | required | Becomes `iris:<logical>`. With `overwrite=true` it may instead name the configured main world or its `_nether` / `_the_end` alias |
|
||||
| `type` (`dimension`, `pack`) | `default` | Which pack and dimension to generate from. `default` resolves to `settings.generator.defaultWorldType` (`overworld`); otherwise a pack name or `pack:dimensionKey` |
|
||||
| `seed` | `1337` | The world seed. Ignored for exact vanilla-slot overwrites, which must keep the level's existing authoritative seed |
|
||||
| `main` (`main-world`) | `false` | Promote this world to `level-name` in `server.properties`. Happens in a JVM shutdown hook on Paper-family, or inline during Folia staging |
|
||||
| `overwrite` (`force`) | `false` | Stage a validated replacement of an existing world slot for the next restart. Never touches a loaded world in place |
|
||||
|
||||
Create refuses to run on the primary thread. Before it takes a lifecycle lease it requires startup datapack validation to be ready and the chosen pack to have a loadable validation result; then the `WORLD_MUTATION` / `WORLD_CREATE` lease must be free or the command fails busy. A refusal at any of those gates leaves no dimension folder and no registration behind.
|
||||
|
||||
## What create actually does (non-Folia)
|
||||
|
||||
1. Resolve the managed key and dimension. No directory is created yet.
|
||||
2. Require startup datapack readiness and a loadable validation result for the owning pack.
|
||||
3. Install datapacks for the dimension types. If the types are not loaded yet, queue a restart.
|
||||
4. Copy the pack into `<world>/iris/pack` through `StudioSVC.installIntoWorld` — staged into a temp directory, published atomically, then validated at that exact root. A validation failure rolls the publication back.
|
||||
5. Build a `WorldCreator` with the Iris generator and `studio=false`.
|
||||
6. Create the world through `WorldLifecycleService` / NMS async create, with a 120-second timeout. A timeout triggers a server restart rather than leaving a half-created world.
|
||||
7. Register the world in `bukkit.yml` with the Iris generator, dimension key, and seed. Update the Multiverse link if Multiverse is present — that step has its own 30-second budget and also escalates to a restart.
|
||||
8. Run creation-time pregen if the caller attached a `PregenTask` through the API.
|
||||
|
||||
Rollback phases carry the same 120-second budget.
|
||||
|
||||
## Folia staging
|
||||
|
||||
Folia cannot create worlds at runtime, so `/iris create` becomes a staging operation:
|
||||
|
||||
1. Require startup datapack readiness and a loadable pack validation result.
|
||||
2. Acquire the `WORLD_CREATE` lease.
|
||||
3. Install datapacks if they changed.
|
||||
4. Abort if the dimension folder already exists.
|
||||
5. Stage the pack into the managed dimension root through `installIntoWorld`; the published snapshot must pass exact-root validation.
|
||||
6. Register the world in `bukkit.yml`.
|
||||
7. If `main=true`, promote the main-world files immediately under the same lease. A failure here rolls back `bukkit.yml` and deletes the staged folder.
|
||||
8. Tell the operator to restart. Generation and loading happen on the next boot.
|
||||
|
||||
`WorldLifecycleStaging` holds the staged generator and biome provider for the backend that picks them up at load.
|
||||
|
||||
## Exact world-slot replacement
|
||||
|
||||
`overwrite=true` is how you put Iris generation into a slot that already exists — including the vanilla overworld, nether, or end. It uses lifecycle kind `WORLD_REPLACE` and always stages for a full restart. There is no in-place variant.
|
||||
|
||||
It requires a Paper-family early bootstrap, which plain Spigot never runs; on Spigot the command fails closed. The target dimension folder must already exist — ordinary create is still the path for a new world.
|
||||
|
||||
Accepted targets are safe `iris:*` keys and exactly three vanilla slots resolved from the configured level name: `minecraft:overworld`, `minecraft:the_nether`, and `minecraft:the_end`. A vanilla slot additionally requires:
|
||||
|
||||
- a pack whose environment matches the slot (`NORMAL`, `NETHER`, or `THE_END`), checked both before staging and after install;
|
||||
- `allow-nether` or `allow-end` enabled in the server config for those two slots.
|
||||
|
||||
Foreign namespaces, other `minecraft:*` keys, path traversal, symlinks, and special filesystem entries all fail closed. `main=true` may accompany `overwrite` only when the target is `minecraft:overworld`.
|
||||
|
||||
Minecraft stores one authoritative seed for a level, so every vanilla-slot replacement keeps the loaded overworld's seed and Iris warns you when that differs from the `seed` you passed. Changing the level seed is the ordinary new-main promotion workflow, not this one.
|
||||
|
||||
### How the transaction is made safe
|
||||
|
||||
The stage copies and validates a fresh frozen pack on the same filesystem, fingerprints it, binds a journal to the canonical level root and logical world name, records the original target and the existing `bukkit.yml` generator and seed, then compare-and-swaps that one configuration entry. Several distinct slots can be queued before a single restart.
|
||||
|
||||
At the next boot, Paper's bootstrap reconciles each authorized transaction before Iris compiles its aggregate datapack and before Minecraft builds registries: it atomically moves the old dimension directory to a retained sibling backup and publishes the stage. The filesystem must support atomic replacement for the world directories, the journal, and `bukkit.yml`; without it Iris refuses rather than falling back to a destructive move.
|
||||
|
||||
Publication retains Paper's per-world `data/paper/metadata.dat`, `data/paper/level_overrides.dat`, and `data/minecraft/world_gen_settings.dat` so the slot keeps its metadata and authoritative seed. Old `region`, `entities`, `poi`, and Iris runtime data are never merged — they stay in the backup, and the replacement starts from the staged snapshot.
|
||||
|
||||
The backup is only eligible for deletion after `WorldLoad` proves the exact namespaced identity, Iris generator, selected dimension, seed, vanilla-slot environment, and an unchanged pack fingerprint. A failed check journals a rollback and requests another restart, after which cold bootstrap restores the retained directory and the prior `bukkit.yml` entry. A crash between any move, config write, or journal phase is retried idempotently. Conflicting manual configuration, changed roots or names, changed staged bytes, unsafe storage, or a duplicate or corrupt journal aborts early bootstrap and preserves the artifacts rather than guessing.
|
||||
|
||||
## Studio create
|
||||
|
||||
Studio worlds use `IrisCreator.studio(true)` and differ from production worlds in ways that matter:
|
||||
|
||||
- Startup datapack validation and the pack's own validation must both be loadable before any Studio folder, snapshot, generator, or Bukkit world is created. Missing validation fails closed.
|
||||
- The pack is **not** copied into the world folder, except for benchmark runs. The engine reads the live pack directly, which is what enables hotload.
|
||||
- Studio worlds are transient. Unloaded Studio worlds are cleaned up, and their `bukkit.yml` entries are removed during shutdown cleanup.
|
||||
- Open and close go through the `StudioSVC` transition queue (`10 - Studio & VSCode Schemas.md`).
|
||||
- Biome Buffet prepares a changed focus before opening the chunk generation session; its exclusive fair-stage admission downgrades straight to the retained chunk permit so no other transition can slip in between the focus hotload and that chunk.
|
||||
- Ordinary Studio suppresses native structure starts only while the initial FULL entry chunk loads, then restores them for later preview chunks.
|
||||
- A failed open never unloads or closes the generator while that asynchronous entry request is still active. Another Studio open is rejected in the meantime; cleanup starts once it settles, and if it is still active 120 seconds later the transient world is queued for deletion at the next clean startup.
|
||||
|
||||
## Load
|
||||
|
||||
`/iris load` (alias `/iris import`) reconciles a world that already exists on disk:
|
||||
|
||||
1. Parse the managed key and require the dimension root directory to exist.
|
||||
2. Run `BukkitWorldReconciler.loadWorld(bukkit.yml, worldKey)`.
|
||||
3. Report success, busy, restart-required, or failure.
|
||||
|
||||
Load never downloads a pack. The world must already have `iris/pack` content and registration data consistent with Iris. Reconciliation checks startup readiness and then lazily validates that world's exact snapshot root before it touches `bukkit.yml` or calls a world backend. Validation results are path-scoped, so two worlds whose snapshot folders are both named `pack` cannot authorize or reject one another.
|
||||
|
||||
## Unload
|
||||
|
||||
`/iris unload` runs synchronously from a player origin:
|
||||
|
||||
1. Require an Iris world and acquire the `WORLD_UNLOAD` lease.
|
||||
2. Mark the world for maintenance.
|
||||
3. `IrisToolbelt.evacuateAsync` → `WorldLifecycleService.unloadAsync(world, true)` → `generator.closeAsync()`.
|
||||
4. On a 150-second terminal timeout, mark the timeout, request a server restart, and fail the future.
|
||||
|
||||
There are two timers in play: the inner `WorldLifecycleService` unload has its own 120-second budget, and the command wraps the whole sequence in the 150-second ceiling.
|
||||
|
||||
`WorldUnloadEvent` stops Iris engine maintenance immediately, but Iris does not treat it as proof that Paper's chunk scheduler has drained. Generator close waits for the raw world-lifecycle backend to confirm a successful unload, and the 26.2 noise pipeline holds one generation lease through terrain generation and worldgen-heightmap priming.
|
||||
|
||||
## Evacuate
|
||||
|
||||
`/iris evacuate <world>` moves every player out of an Iris world into another loaded world, or kicks them when there is nowhere else to go. It runs as a step inside both unload and removal, and is worth running on its own first so you can see who was affected.
|
||||
|
||||
## Remove
|
||||
|
||||
`/iris remove <name> [delete=true]` delegates to `IrisWorldRemovalService`, which reports one of 18 statuses.
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| `UNREGISTERED` | Unloaded and unregistered; files kept. This is success for `delete=false` |
|
||||
| `DELETED` | Unregistered and files deleted. Success for the default |
|
||||
| `DELETE_QUEUED` | Files could not be deleted now and were quarantined for deletion at next startup. Restart and confirm before reusing the name |
|
||||
| `BUSY` | Another world or pack mutation holds the coordinator |
|
||||
| `INVALID_IDENTIFIER` | The name is not a parseable managed key |
|
||||
| `PROTECTED_WORLD` | The target is a world Iris refuses to remove |
|
||||
| `NOT_IRIS_WORLD` | The target exists but is not Iris-managed |
|
||||
| `UNSAFE_PATH` | The resolved directory failed a path-safety check (traversal, symlink, wrong namespace) |
|
||||
| `NOT_FOUND` | No such managed world |
|
||||
| `RESOLUTION_FAILED` | Iris could not resolve the world identity to a directory |
|
||||
| `TELEPORT_FAILED` | Players could not be evacuated, so removal stopped before touching files |
|
||||
| `GENERATOR_CLOSE_FAILED` | The Iris generator did not close cleanly; the world may still hold resources |
|
||||
| `UNLOAD_FAILED` | The server refused or failed to unload the world |
|
||||
| `CONFIGURATION_FAILED` | The `bukkit.yml` entry could not be updated |
|
||||
| `REGISTRY_FAILED` | The `worlds.json` registry could not be updated |
|
||||
| `QUARANTINE_FAILED` | The world directory could not be moved to the quarantine name |
|
||||
| `DELETE_FAILED` | Quarantine succeeded but deletion did not |
|
||||
| `INTERNAL_FAILURE` | An unexpected error; read the logged cause |
|
||||
|
||||
Any status other than `UNREGISTERED`, `DELETED`, or `DELETE_QUEUED` means the registry may have changed without the files being removed, and a quarantine directory may still exist. Check the world directory before retrying.
|
||||
|
||||
Only safe `iris` namespace dimension paths are mutable. Each phase has a 120-second timeout and can request a restart when it gets stuck.
|
||||
|
||||
With `delete=true`, Iris records the exact quarantine name in a durable startup queue **before** moving the directory, so a crash mid-delete still gets cleaned up on the next boot. Both immediate cleanup and the startup retry snapshot every directory's direct children before deleting, reject symlinks and special filesystem entries, and keep the queue entry with the full error when a concurrent writer or filesystem failure leaves content behind.
|
||||
|
||||
## Main world promotion
|
||||
|
||||
`main=true` on a non-Folia server installs a JVM shutdown hook. At shutdown it rewrites `level-name` and `level-seed` in `server.properties` and publishes files:
|
||||
|
||||
1. Stage a temp directory (`.<name>.promoting-`) under the world container.
|
||||
2. Copy the shared `data`, `datapacks`, and `players` folders from the current level root.
|
||||
3. Copy the Iris dimension tree into `<stage>/dimensions/minecraft/overworld`.
|
||||
4. Re-check that the target is absent, then move the stage into place with a plain rename.
|
||||
5. Write `server.properties` atomically, with an fsync and an `ATOMIC_MOVE` (falling back to a non-atomic write if the filesystem refuses).
|
||||
|
||||
Only step 5 is atomic; the directory move in step 4 is an ordinary rename. Promotion requires the target level folder to be absent and aborts if it finds a symlink anywhere in the copied tree. The whole sequence runs under a `WORLD_MUTATION` / `WORLD_PROMOTE` lease. Folia performs the same publish inline during staging rather than deferring to shutdown.
|
||||
|
||||
To put Iris into the currently configured main slot **without** creating a new level root, name that exact main world and use `overwrite=true`. That keeps the top-level level root, shared datapacks, player data, and non-target dimensions intact. Plain `main=true` remains the new-level-root workflow above.
|
||||
|
||||
## 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 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All four fields are required per entry: `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 go to a sibling `iris-dimensions.json.tmp`, get an fsync, and are moved into place with `ATOMIC_MOVE` where the filesystem supports it.
|
||||
|
||||
Entries that are individually invalid are logged, kept verbatim, and re-appended on the next write — Iris never silently drops one. Duplicate ids keep the first valid entry and warn.
|
||||
|
||||
If the whole file fails to parse, only the startup load path quarantines it as `iris-dimensions.json.broken-<timestamp>`, salvages whatever ids it can from the raw text into the log, and continues with no persistent Iris dimensions. Every other code path throws rather than discard persistent worlds. Keep the quarantined file, recreate each reported world with `/iris world create`, verify pack, dimension, and seed, then delete the backup.
|
||||
|
||||
## Pack snapshot vs studio
|
||||
|
||||
| Operation | Effect on the pack |
|
||||
|---|---|
|
||||
| Production create | Full pack tree installed under the world's `iris/pack` and frozen there |
|
||||
| Studio open | Engine reads the live packs root; nothing is installed into the world |
|
||||
| `/iris studio package` | Exports an archive; no world is touched |
|
||||
| `/iris dev update-world` | Replaces a world's `iris/pack`. Unsafe, and restarts the server if an engine still holds that pack |
|
||||
| Hotload | Studio only. A production snapshot never changes underneath a running world |
|
||||
|
||||
## Concurrent lifecycle guards
|
||||
|
||||
`LifecycleOperationCoordinator` is a **single global mutex** shared by the `WORLD_MUTATION` and `PACK_MUTATION` domains. It is not one lock per domain: a pack download or publish will make a world create report busy, and vice versa. A third domain, `SERVER_LIFECYCLE`, is reserved and cannot be acquired.
|
||||
|
||||
Thirteen operation kinds run under it: `WORLD_CREATE`, `WORLD_LOAD`, `WORLD_UNLOAD`, `WORLD_REMOVE`, `WORLD_REPLACE`, `WORLD_PROMOTE`, `STUDIO_OPEN`, `STUDIO_CLOSE`, `PACK_CREATE`, `PACK_DOWNLOAD`, `PACK_PUBLISH`, `DATAPACK_COMPILE`, and `SERVER_RESTART`.
|
||||
|
||||
Ordinary create also refuses when the dimension root already exists or the world is already loaded. Exact replacement runs as a separately journaled restart transaction and never relaxes the removal-path protections.
|
||||
@@ -1,207 +0,0 @@
|
||||
# 07 - Pregeneration
|
||||
|
||||
Pregeneration forces chunks to generate ahead of time so players never wait on terrain generation when they explore. You give it a block radius and a center; Iris walks the square area region by region in a spiral and generates every chunk in it. One pregen job runs at a time per server, driven by `/iris pregen` on Bukkit-family and by the same subcommand tree on mod loaders.
|
||||
|
||||
See also: `02 - Getting Started.md`, `03 - Configuration.md`, `04 - Commands & Permissions.md`, `06 - Worlds & Lifecycle.md`, `29 - Client HUD & Protocol.md`, `33 - Performance Tuning.md`.
|
||||
|
||||
## Pregenerate 10,000 blocks around spawn
|
||||
|
||||
This is the common production task: generate a large area once, up front, so the server never generates terrain during play.
|
||||
|
||||
Before you start you need a world whose ordinary chunk generation already works, free disk space for the area, a backup or a world you can afford to lose, and no other pregen job running.
|
||||
|
||||
**Radius is in blocks, not chunks and not regions.** A radius of `10000` covers 20,000 blocks across, which is 1,251 chunks per axis and **1,565,001 chunks total**. That is hours of work and tens of gigabytes. Do not type it first.
|
||||
|
||||
### 1. Prove the pipeline with a small run
|
||||
|
||||
```text
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
```
|
||||
|
||||
That is 2,025 chunks and finishes in a minute or two. Watch it:
|
||||
|
||||
```text
|
||||
/iris pregen status
|
||||
```
|
||||
|
||||
You should see the world name, `2,025` total chunks, a rising generated count, a chunks-per-second rate, an ETA, and the method name. **Failed count must stay at zero.** If failures accumulate, stop now — a big run will only produce more of them.
|
||||
|
||||
### 2. Run the real thing
|
||||
|
||||
```text
|
||||
/iris pregen start 10000 world=myworld center=0,0 gui=false
|
||||
```
|
||||
|
||||
If your spawn is not at `0,0`, stand at spawn and use `center=me` instead. That token only works for a player sender; from console, pass explicit coordinates.
|
||||
|
||||
Drop `gui=false` only if the server has a desktop and you want the visual renderer.
|
||||
|
||||
### 3. Know when it is done
|
||||
|
||||
Poll `/iris pregen status`. The job is finished when:
|
||||
|
||||
- generated equals total (1,565,001 for this run),
|
||||
- failed is still `0`,
|
||||
- and `/iris pregen status` reports no active task after it ends.
|
||||
|
||||
The last one is the real signal. While a job exists, status prints progress; once the job closes, status tells you there is no active pregeneration task. That transition is the completion condition — not the percentage, which can sit at 100% while in-flight chunks finish writing.
|
||||
|
||||
Then restart the server cleanly and fly to the edge of the generated area. Chunks inside must load without generating; chunks past the boundary must generate normally.
|
||||
|
||||
### Pausing and stopping
|
||||
|
||||
```text
|
||||
/iris pregen pause
|
||||
```
|
||||
|
||||
`pause` is a **toggle**, and `resume` is just an alias for the same command. Running `/iris pregen resume` on a job that is currently running will pause it. The command echoes the resulting state, so read the reply rather than assuming.
|
||||
|
||||
```text
|
||||
/iris pregen stop
|
||||
```
|
||||
|
||||
Stop lets in-flight chunks finish, then cancels. Wait for it to actually close before starting another job — starting a new one closes the previous instance, which is not the same as it having shut down cleanly.
|
||||
|
||||
Unloading or removing a world also stops a pregen targeting that world. That path blocks for up to 15 seconds waiting for the job to close and throws if it does not.
|
||||
|
||||
### Fabric / Forge / NeoForge
|
||||
|
||||
```text
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0
|
||||
/iris pregen start 10000 irisworldgen:myworld at 0 0 nocache
|
||||
```
|
||||
|
||||
The dimension argument comes after the radius, `at <x> <z>` after that, and `gui`, `sync`, and `nocache` are order-free literal flags you can combine. Radius accepts `1`–`100000`. Modded pregen shows a boss bar automatically unless the player is running the Iris client mod, which draws its own HUD instead.
|
||||
|
||||
## Recovery
|
||||
|
||||
| Symptom | Check | What to do |
|
||||
|---|---|---|
|
||||
| Start reports an active job | There is one pregen job server-wide, not one per world | Check `/iris pregen status`; finish or stop it, and wait for closure before retrying |
|
||||
| Total chunk count is not what you expected | Bounds are inclusive and round outward to whole chunks, so the area is slightly larger than `radius × 2` | Recompute: chunks per axis is `ceil(radius/16) - floor(-radius/16) + 1` centered on your center chunk |
|
||||
| Failed count climbing | Chunk load timeout, a generation exception, disk failure, or a lifecycle interruption | Stop, fix the first logged failure, confirm ordinary generation works, then retry the same small area |
|
||||
| `serial=true` rejected | Strict serial generation needs a Paper-compatible server | Use the normal method, or run the diagnostic on Paper |
|
||||
| Desktop GUI never opens | The server is headless, or `gui.useServerLaunchedGuis` is off | Use `gui=false` and watch status, console, or the client HUD |
|
||||
| Progress repeatedly stalls | Heap high-water or mantle plate backpressure is engaging | Stop the job before tuning. Lower resident plates and in-flight work before raising anything heap-sensitive |
|
||||
| Restart regenerates work you already did | The cache wrapper was off — Folia routing disables it, `nocache` was passed, the world has no engine access, or the files under `iris/pregen` were deleted | Treat the rerun as uncached. Regeneration alone is not evidence of corruption |
|
||||
| "world may not be fully loaded" warning | A player sender started pregen in a world Iris has no engine access to | Confirm the world is loaded and its engine initialized before trusting the run |
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `/iris pregen start <radius> [world=<world>] [center=0,0] [gui=true] [serial=false]` | Start a job. Closes any previous job instance first |
|
||||
| `/iris pregen stop` (alias `x`) | Request stop. In-flight chunks finish, then the job cancels asynchronously |
|
||||
| `/iris pregen pause` (alias `resume`) | Toggle pause. One command, two names — it flips whatever state the job is in |
|
||||
| `/iris pregen status` | Print a progress snapshot for the active job, or report that none exists |
|
||||
|
||||
The command root is `/iris pregen` with alias `/iris pregenerate`.
|
||||
|
||||
### `start` parameters
|
||||
|
||||
| Param | Default | What it controls |
|
||||
|---|---|---|
|
||||
| `radius` (`size`) | required | Blocks from center on both X and Z. Must be greater than zero. The chat confirmation reports the span as `radius × 2` blocks, which slightly understates the real area because bounds round outward to whole chunks |
|
||||
| `world` | contextual | Which world to generate. Falls back to your current world. A non-Iris world runs the hybrid method with no engine, so no engine-backed cache wrapper |
|
||||
| `center` (`middle`) | `0,0` | Block X/Z the square is centered on. Accepts `me`/`here`/`self` for your position, `look`/`cursor` for your look target, and `player:<name>` — all player-sender only |
|
||||
| `gui` | `true` | Open the desktop renderer when the host supports it. Headless servers log and carry on |
|
||||
| `serial` | `false` | Generate one chunk at a time through the strict serial hybrid method. **Requires Paper**; rejected elsewhere. For diagnosing instability, not for throughput |
|
||||
|
||||
## Area model
|
||||
|
||||
`PregenTask` builds saturating block bounds at `center ± radius`, converts them to chunk and region ranges, then iterates regions in a spiral from the center, ordering chunks within each region toward the center too. That ordering is why the area around your center becomes playable first.
|
||||
|
||||
Bounds are inclusive on both edges: the minimum block floors to a chunk, the maximum ceils. For radius 352 at `0,0` that gives chunks `-22..22` on each axis — 45 per axis, 2,025 total.
|
||||
|
||||
| Limit | Value |
|
||||
|---|---|
|
||||
| Maximum region span per axis | 117,189 regions, which is the ±30,000,000 block Minecraft world limit |
|
||||
| Oversized or non-positive request | `IllegalArgumentException` at construction, so the command fails immediately instead of hanging |
|
||||
| Modded radius argument range | 1 to 100,000 |
|
||||
|
||||
## Generation methods
|
||||
|
||||
| Situation | Method used |
|
||||
|---|---|
|
||||
| Iris world, parallel (default) | `HybridPregenMethod(world, threadCount)` |
|
||||
| Iris world, `serial=true` | `HybridPregenMethod.strictSerial(world)` |
|
||||
| Non-Iris world | The same hybrid method with a null engine |
|
||||
| Caching enabled, engine present, scheduler not Folia | `CachedPregenMethod` wrapped around whichever of the above applies |
|
||||
|
||||
`HybridPregenMethod` delegates to `AsyncOrMedievalPregenMethod`, which picks `AsyncPregenMethod` on Paper and `MedievalPregenMethod` elsewhere. Region-at-a-time generation is not supported on this path; it is always chunk by chunk.
|
||||
|
||||
The `threadCount` argument is vestigial — `AsyncPregenMethod` ignores it and recomputes concurrency from the server's worker pool, CPU count, and world-gen thread settings, and `MedievalPregenMethod` takes no thread count at all. Tune concurrency through the settings in `33 - Performance Tuning.md`, not by expecting that parameter to do something.
|
||||
|
||||
## Cache
|
||||
|
||||
The cache records which chunks are already generated so a restarted or repeated run can skip them.
|
||||
|
||||
| Piece | Where | Behavior |
|
||||
|---|---|---|
|
||||
| Per-world skip cache | `<dimensionRoot>/iris/pregen/` | Created through `GlobalCacheSVC.createDefault`. Records generated chunks and regions. Only consulted when the `CachedPregenMethod` wrapper is active |
|
||||
| `world.globalPregenCache` | `settings.json`, default `false` | When true, Iris also maintains the cache during ordinary play — creating it at world init and marking chunks on every `ChunkLoadEvent`, so normal exploration counts toward it. When false, the pregen job still gets a real on-disk cache; it just is not fed by ordinary chunk loads |
|
||||
| Folia | Resolved runtime scheduler is Folia | The cached wrapper is disabled entirely for pregen |
|
||||
| No engine | Non-Iris world | The wrapper is skipped, since the cache is keyed to the engine's world identity |
|
||||
|
||||
Cache contents are written on world unload, on service disable, when the setting is toggled off, and when the cached method closes or saves. If the Iris service itself is disabled, `createDefault` hands back an empty cache rather than touching disk.
|
||||
|
||||
Modded pregen keeps its cache in the equivalent `<worldFolder>/iris/pregen`.
|
||||
|
||||
## Mantle and heap caps
|
||||
|
||||
Pregen generates faster than chunks get saved, so Iris throttles itself against tectonic plate residency and heap use. These are the knobs that decide whether a large run finishes or thrashes.
|
||||
|
||||
| Control | Default and rule | Why you would change it |
|
||||
|---|---|---|
|
||||
| `pregen.maxResidentTectonicPlates` | `96`, floored at `16` | The headline speed/memory tradeoff. Raise it to keep more mantle in RAM and cut re-reads; lower it when the run is pushing the heap |
|
||||
| Effective plate cap | `max(16, min(baseCap, heightScaledCap, heapBudgetCap))` | Computed, not configured. `heightScaledCap` scales the base cap by `384 / worldHeight`, so tall worlds automatically hold fewer plates; `heapBudgetCap` allows about 60% of max heap against a 48 MB reference plate |
|
||||
| `mantleBackpressureWaitMs` | `25`, clamped 5–1000 | How long the generator sleeps per backpressure check. Rarely worth changing |
|
||||
| `mantleBackpressureTimeoutMs` | `60000`, clamped 5s–600s | How long backpressure waits before giving up. On timeout Iris logs and proceeds anyway — it never deadlocks the run |
|
||||
| Hard cap trigger | Loaded plates greater than `effectiveCap × 2` | Forces a wait-and-evict cycle. Seeing this in logs means the cap is too high for your heap |
|
||||
| Heap high water | Pause at 92% used, release at 82% | Deliberate hysteresis. Generation stalls at 92% and does not resume until it drops to 82%, so brief spikes do not cause flapping |
|
||||
| Heap panic | 96% requests a panic reclaim and GC | Throttled to once per 30 seconds. Repeated panic lines mean the heap is undersized for the settings |
|
||||
| `pregen.saveIntervalMs` | `30000`, clamped 5s–900s | How often the pregen loop flushes. Shorter means less lost work on a crash and more I/O |
|
||||
|
||||
Full settings reference: `03 - Configuration.md`. Tuning guidance: `33 - Performance Tuning.md`.
|
||||
|
||||
## Other `pregen` settings
|
||||
|
||||
| Key | Default | What it controls |
|
||||
|---|---|---|
|
||||
| `runtimeSchedulerMode` | `AUTO` | Whether Iris treats the server as Folia or Paper-like. This is what decides if the pregen cache wrapper is available. `AUTO` probes the server; a regionized server always resolves to `FOLIA`, and configuring `FOLIA` on a non-regionized server is forced back to `PAPER_LIKE` |
|
||||
| `paperLikeBackendMode` | `AUTO` | Which Paper-like lifecycle backend loads chunks. `AUTO` resolves to `TICKET` |
|
||||
| `chunkLoadTimeoutSeconds` | `15`, clamped 5–120 | How long a single chunk load may take before it counts as a failure. Raise it on slow storage; a rising failed count with a low value here is usually I/O, not corruption. Modded pregen floors this at 120 seconds regardless of the setting |
|
||||
| `timeoutWarnIntervalMs` | `500`, minimum 250 | How often stalled loads warn. Purely log volume |
|
||||
| `moddedPregenInFlight` | `0` | In-flight chunk budget for modded pregen. `0` auto-resolves to `max(16, min(48, cpu × 2))`; an explicit value is capped at 512 and floored at 8 |
|
||||
|
||||
## Pause, stop, and status
|
||||
|
||||
| Action | What happens |
|
||||
|---|---|
|
||||
| Pause | `PregeneratorJob.pauseResume()` flips the flag. The generator loop spins while paused, and also while heap high-water is engaged |
|
||||
| Stop | `shutdownInstance()` closes the pregenerator and interrupts the worker asynchronously, so the command returns before the job is fully closed |
|
||||
| Status | `progressSnapshot()` returns percent, generated, total chunks, chunks remaining, chunks per second, ETA, elapsed time, method name, paused flag, failed count, world name, and world identity |
|
||||
|
||||
Failed chunks are counted separately from generated ones and only appear in the status line when the count is above zero. A run can reach 100% with failures — check the failed count, not just the percentage.
|
||||
|
||||
## HUD, GUI, and protocol
|
||||
|
||||
| Surface | Behavior |
|
||||
|---|---|
|
||||
| Desktop GUI (Bukkit) | `PregenRenderer` opens when `gui=true` and a GUI host is available. It draws the progress text and a pause hint over a chunk map — there is no color legend on screen. Chunks being generated are muted green and network-sourced chunks purple. For an Iris world, finished and pre-existing chunks are painted with the engine's biome colors instead of flat status colors; the flat green and dark-green status colors only appear when there is no engine |
|
||||
| Boss bar | **`/iris pregen` on Bukkit shows no boss bar.** Only creation-time pregen and studio progress claim a HUD slot. Modded pregen does show a boss bar — green while running, yellow while paused — and skips it entirely for players running the Iris client mod |
|
||||
| Client HUD | `IrisProtocolServer.broadcastPregenProgress` sends progress every tick to connected Iris client sessions that hold the pregen capability, plus per-region deltas. This is the only path client HUDs are fed on any platform |
|
||||
|
||||
GUI toggles live at `settings.gui.useServerLaunchedGuis` and `settings.gui.maximumPregenGuiFPS`. Client HUD detail: `29 - Client HUD & Protocol.md`.
|
||||
|
||||
## Performance profile
|
||||
|
||||
Starting a pregen applies `PregenPerformanceProfile` to the engine before the job is constructed. It raises the noise cache to at least 4096 entries and enables the fast cache path, then rebuilds the biome complex if anything actually changed. The studio `profile` command applies the same profile while measuring pack cost, so pregen and profiling numbers are comparable.
|
||||
|
||||
## Operator notes
|
||||
|
||||
- Radius is in **blocks**. Every mistake in this area is someone typing a chunk count.
|
||||
- Re-running over the same area is fast only when the cache wrapper was active and the files under `iris/pregen` still exist.
|
||||
- Serial mode is a diagnostic. Use it to reproduce a generation failure deterministically, not to go faster.
|
||||
- Stop a job before tuning mantle or heap settings. Changing them mid-run makes the before/after meaningless.
|
||||
- Change one setting at a time against the 352-block baseline before scaling back up. See `33 - Performance Tuning.md`.
|
||||
@@ -1,151 +0,0 @@
|
||||
# 08 - Localization
|
||||
|
||||
Iris ships its command, Studio, runtime, HUD, and UI text as typed Java message catalogs, with translated overlays for seventeen languages and an operator-editable override file per locale. You pick the server language with `general.language` in `settings.json`; you change individual strings by dropping a partial JSON file into `languages/overrides/`. Client keybind labels are a separate surface and live in the mod jar's Minecraft lang assets. See also `03 - Configuration.md`, `04 - Commands & Permissions.md`, and `29 - Client HUD & Protocol.md`.
|
||||
|
||||
## Change one message
|
||||
|
||||
Say you want German, but you dislike the wording Iris uses when someone mistypes a subcommand. You need two things: the locale setting, and an override file that redefines exactly that one key.
|
||||
|
||||
Prerequisites: write access to the Iris data folder, a backup of `settings.json`, and an account that can run `/iris reload`.
|
||||
|
||||
1. Set `general.language` to `de_DE` in `settings.json` (`plugins/Iris/settings.json` on Bukkit-family, `<data folder>/settings.json` on a mod loader).
|
||||
2. Create `<Iris data folder>/languages/overrides/de_DE.json`. Iris creates the `languages/overrides/` folder itself on the first locale load, so it should already exist.
|
||||
|
||||
```json
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.unknown": "Kenn ich nicht. Probier /iris help"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Run `/iris reload`. A green `Hotloaded settings and locale de_DE.` means the settings and the locale both applied. A yellow `Settings were reloaded, but locale ... was rejected` means the overlay failed validation and the previous locale is still active — read the errors in the console before going further.
|
||||
4. Run `/iris` with a subcommand that does not exist, for example `/iris zzz`.
|
||||
|
||||
Success looks like your override text appearing verbatim. Everything else in the same session — help output, pregen status, Studio messages — should be in German from the bundled `de_DE` overlay, and any key that neither file defines falls back to the built-in English rather than printing a raw key id.
|
||||
|
||||
Edit the file again and save it. The settings hotload poll calls `IrisLanguage.update()`, which compares the override file's path, modification time, and length against what it last loaded, so an edit is picked up on the next poll with no command and no restart. Delete the test override when you are done.
|
||||
|
||||
If you are authoring a whole new locale, translate one command group first and confirm it loads before you translate the rest. A single bad key rejects the entire file.
|
||||
|
||||
### Recovery
|
||||
|
||||
| Symptom | What actually happened | Fix |
|
||||
|---|---|---|
|
||||
| `Rejected locale setting '...'` in the log | The value does not match `[A-Za-z0-9_-]+`, so it never reached the loader | Correct the string in `settings.json`; the previously active locale keeps running in the meantime |
|
||||
| `Rejected locale reload for <locale>` | The overlay failed validation. The console then lists up to 12 concrete errors and a count of any it omitted | Fix the listed keys and reload. Nothing partial is applied — the previous locale stays active in full |
|
||||
| `Locale overlay key is not declared by the message catalog` | You invented a key name. Overrides can only redefine keys that already exist in code | Copy the exact key id from the bundled locale file for your language |
|
||||
| `Expected [x, y] but found [x]` | Your text dropped or renamed a `{name}` placeholder | Match the English template's placeholder set exactly. Order and surrounding words are free; the set of names is not |
|
||||
| `Expected 5 lines but found 4` / `Expected plural forms [...]` | A lines key needs the same line count as English, and a plural key needs the same form names | Restore the missing entries |
|
||||
| Override edits do nothing | Wrong data folder, or the filename does not match the active locale id | The file must be `<data>/languages/overrides/<active locale>.json`, spelled exactly as `general.language`. Run `/iris reload` to force it |
|
||||
| Server text is translated but keybind labels are still English | Those labels come from the mod jar's client assets, not from the server locale | See "Client language assets" below |
|
||||
|
||||
## Where English comes from
|
||||
|
||||
Canonical English is owned by code in `core/.../localization`. `IrisMessages` assembles the catalog from every surface class plus the shared Director command keys from VolmLib. There is no English server translation file, and there does not need to be — English is the fallback layer under every locale.
|
||||
|
||||
| Catalog | Covers |
|
||||
|---|---|
|
||||
| `DirectorMessages` (VolmLib) | Shared command framework text: parameter errors, argument prompts, help chrome |
|
||||
| `IrisMessages` | Permission denials, unknown command, player-only, "not an Iris world", reload results, modded help chrome |
|
||||
| `BukkitCommandMessages`, `BukkitCommandMessagesExtended` | Feedback from the Bukkit `/iris` command tree |
|
||||
| `DirectorCommandMessages` | Per-command and per-parameter descriptions shown in `/iris help` |
|
||||
| `ModdedCommandMessages`, `ModdedHelpMessages` | The Fabric/Forge/NeoForge command tree and its help pages |
|
||||
| `RuntimeUiMessages`, `RuntimeProgressMessages`, `BukkitRuntimeMessages` | Pregen headers and boss bar titles, chunk job progress, runtime status lines |
|
||||
| `PackDownloadMessages` | Pack download progress and results |
|
||||
| `ClientUiMessages` | Client mod strings: Vision map, What overlay, pregen HUD stats, toasts, create-world gates |
|
||||
| `BukkitUiMessages`, `DesktopUiMessages` | Bukkit inventory UI and desktop pregen window strings |
|
||||
|
||||
Code resolves text through `IrisLanguage.text(...)` when color codes should survive, or `IrisLanguage.plain(...)` when they should be stripped. Argument-free `plain` calls are memoized per locale snapshot because HUD code calls them several times per frame; a locale reload publishes a new snapshot and throws the whole memo away.
|
||||
|
||||
## Selecting a locale
|
||||
|
||||
| Setting | Default | Location |
|
||||
|---|---|---|
|
||||
| `general.language` | `en_US` | `plugins/Iris/settings.json` (plugin) or `<data folder>/settings.json` (mod) |
|
||||
|
||||
The value must match `[A-Za-z0-9_-]+`. Anything else is rejected outright and the previously active locale keeps running. Both `/iris reload` and the automatic settings hotload re-read the setting and reload the locale; the command reports the requested and the active id so you can tell a successful switch from a silent no-op.
|
||||
|
||||
On a successful load Iris logs `Loaded locale <id> with N fallback entries.` That count is the number of catalog keys each overlay did not define, summed across overlays. A one-key override file therefore produces a very large number. It is informational, not an error.
|
||||
|
||||
## Bundled server locales
|
||||
|
||||
Complete translations ship inside the jar as `/languages/<locale>.json`.
|
||||
|
||||
| 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, not underscore — this one id is irregular) |
|
||||
| `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 |
|
||||
|
||||
A bundled file is capped at 2 MiB. If one of these ids is configured but its jar resource is missing, the load throws — that is a build defect, not an operator problem. An id outside this list with no bundle is not an error: Iris skips the bundled layer and every string falls through to English, which is what makes a fully custom locale possible from an override file alone.
|
||||
|
||||
## Override files
|
||||
|
||||
Path: `<Iris data folder>/languages/overrides/<locale>.json`, created as a folder on locale load.
|
||||
|
||||
Overrides are partial by design. Define only the keys you want to change; the rest resolve from the bundled overlay, then from English.
|
||||
|
||||
```json
|
||||
{
|
||||
"locale": "de_DE",
|
||||
"messages": {
|
||||
"iris.command.unknown": "Unbekannter Iris-Befehl"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Rule | Behavior |
|
||||
|---|---|
|
||||
| Root keys | Only `locale` and `messages`. Any other root key throws and the reload is rejected |
|
||||
| `locale` | Optional. If present it must equal the filename's locale id after trimming, otherwise the file is rejected |
|
||||
| Key existence | Every key must already exist in the catalog. An unknown key is an ERROR that rejects the whole file — you cannot mint new messages from an override |
|
||||
| Value shapes | A string for text keys, a string array for lines keys, an object of plural forms for plural keys. Using the wrong shape rejects the file |
|
||||
| Nesting | Nested objects flatten into dotted keys, so `{"iris": {"command": {"unknown": "..."}}}` is the same as `"iris.command.unknown"`. The exception is a plural key, where an object is read as the plural forms |
|
||||
| Placeholders | The set of `{name}` tokens must match the English template exactly. A lines key must also match the English line count, and each line's placeholder set |
|
||||
| Size | Max 2 MiB |
|
||||
| Hotload | The file's path, mtime, and length are watched. A change reloads the locale on the next settings-hotload poll |
|
||||
|
||||
Validation is all-or-nothing. A rejected reload leaves the previous locale fully intact and logs the first 12 errors plus a count of the remainder.
|
||||
|
||||
## Resolution order
|
||||
|
||||
For a non-`en_US` locale a key resolves as: operator override → bundled `/languages/<locale>.json` → English catalog default. For `en_US` the bundled layer is skipped entirely, so it is: operator override → English catalog default.
|
||||
|
||||
Templates use `{name}` tokens. Arguments are classified as trusted or untrusted at the call site. Trusted arguments may carry color codes. Untrusted arguments — player names, world names, pack-authored strings, exception text — have legacy section codes stripped and have `&`, `<`, and `>` rewritten to lookalike characters so they cannot inject formatting.
|
||||
|
||||
`&` codes in the template itself are translated to section-sign codes before send, for `0-9`, `a-f`, `k-o`, `r`, and `x`.
|
||||
|
||||
## Client language assets
|
||||
|
||||
The Minecraft client reads its own lang files from `assets/irisworldgen/lang/<mc_code>.json` inside the mod jar. These are not the server catalogs and are not affected by `general.language`. They currently define only the keybind category and the three key names:
|
||||
|
||||
| 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 |
|
||||
|
||||
`en_us.json` is required; a translated file ships for every bundled locale. The Minecraft code is the server locale id with `-` replaced by `_` and lowercased, so `ja-JP` becomes `ja_jp`.
|
||||
|
||||
Everything else the client draws — HUD stats, Vision map labels, What overlay rows, toasts — resolves through `IrisLanguage` and `ClientUiMessages` on whichever process renders it, not through these four keys. That is why a translated boss bar and an English keybind label can coexist.
|
||||
|
||||
## Platforms
|
||||
|
||||
Localization works the same on Bukkit-family and on Fabric/Forge/NeoForge, from the same catalogs and the same override file. Only the surfaces differ: the modded command tree uses `ModdedCommandMessages`/`ModdedHelpMessages`, the Bukkit tree uses the Bukkit catalogs, and each ignores the other's keys. Keybind lang assets apply only where the client mod is installed. See `30 - Platform Differences.md`.
|
||||
@@ -1,185 +0,0 @@
|
||||
# 09 - PlaceholderAPI
|
||||
|
||||
Iris registers a PlaceholderAPI expansion with id `iris` on Bukkit-family servers, publishing sixteen read-only values: one service flag, six terrain readings scoped to a player's position, and nine global pregeneration readings. It exists so scoreboard, chat, and HUD plugins can show Iris state without writing Java. Plugins that need the same data with real types and no string parsing should use `90 - API - Getting Started.md`, `91 - API - Terrain.md`, and `92 - API - World Events.md` instead. There is no PlaceholderAPI on Fabric/Forge/NeoForge; see `07 - Pregeneration.md` and `28 - Integrations.md` for the surrounding runtime.
|
||||
|
||||
## Put an Iris value on a scoreboard
|
||||
|
||||
Work outward: prove the placeholder resolves in PlaceholderAPI itself before you touch the plugin that will display it. Half of all "the placeholder is broken" reports are a formatting mistake in the consumer.
|
||||
|
||||
Prerequisites: Bukkit-family Iris, PlaceholderAPI installed *before* Iris starts, a full server restart, and a player standing in a loaded Iris world.
|
||||
|
||||
1. `/papi info iris` — the expansion must be listed with author `Volmit Software` and version `2.0.0`, along with all sixteen paths. If it is not listed at all, skip to the recovery table; nothing else will work.
|
||||
2. `/papi parse me %iris_available%` — expect `true`. This only means Iris registered its terrain service, not that you are in an Iris world.
|
||||
3. `/papi parse me %iris_world.available%` — expect `true`. This is the guard you will use in the board template.
|
||||
4. `/papi parse me %iris_world.biome-key%` — expect a load key such as `desert/hot-dunes`. A `---` here means Iris has no reading for you yet; see the recovery table.
|
||||
5. Start a job to test the pregen family: `/iris pregen start 352 center=0,0 gui=false`, then `/papi parse me %iris_pregen.percent%`. Expect a bare number between `0.00` and `100.00` with no percent sign.
|
||||
6. `/iris pregen stop`, then `/papi parse me %iris_pregen.available%`. Expect `false`, and every other `pregen.*` key to read `---`.
|
||||
7. Now paste the exact string you verified into the consumer plugin, wrap it in whatever guard that plugin offers, and reload it.
|
||||
|
||||
Success is the board showing the same text `/papi parse` showed. If step 7 disagrees with step 4, the bug is in the consumer's template or refresh interval, not in Iris.
|
||||
|
||||
### Recovery
|
||||
|
||||
| Symptom | What actually happened | Fix |
|
||||
|---|---|---|
|
||||
| `/papi info iris` reports no such expansion | PlaceholderAPI was not enabled at the moment Iris ran its registration task, one tick after enable | Full server restart with both plugins present. There is no late retry on `PluginEnableEvent`, and `/papi reload` will not make Iris try again |
|
||||
| The placeholder renders literally as `%iris_...%` | The path is not one Iris publishes. Unknown paths return null to PlaceholderAPI, which leaves the text alone | Copy an exact path from `/papi info iris` or the table below. Pre-2.0 underscore names are gone |
|
||||
| A `world.*` key reads `---` | No player context (console or offline player), the player has no tracked position yet, the player is not in an Iris world, or the terrain service returned nothing for that column | Parse as a named online player who is standing in a loaded Iris world |
|
||||
| `world.available` is `true` but the biome lags your movement | The per-player world view has a one-second TTL | Wait a second, or teleport — teleports publish immediately. Do not go looking for a bug in the consumer first |
|
||||
| A `pregen.*` key reads `---` | No job snapshot is currently published | Guard the board template on `pregen.available` rather than testing the value keys for `---` |
|
||||
| The board shows `47.5%%` or `47.5` with no sign | `pregen.percent` deliberately returns a bare number | Put the literal `%` in the consumer's format string |
|
||||
|
||||
## Registration
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Expansion id | `iris` |
|
||||
| Version | `2.0.0` |
|
||||
| Author | `Volmit Software` |
|
||||
| Required plugin | `Iris` |
|
||||
| Declared in `plugin.yml` | `softdepend: PlaceholderAPI` |
|
||||
| `persist()` | `true` — the expansion survives `/papi reload` without restarting Iris |
|
||||
|
||||
Iris checks `isPluginEnabled("PlaceholderAPI")` inside a task scheduled just after its own enable, and gives up silently if the answer is no. Registration also installs a listener; if that listener fails to attach, Iris unregisters the expansion again and logs a warning, so you never end up with an expansion publishing stale positions.
|
||||
|
||||
Soft-depend only affects load order. It does not install or load PlaceholderAPI.
|
||||
|
||||
## Value grammar
|
||||
|
||||
| Rule | Detail |
|
||||
|---|---|
|
||||
| Path form | Dot-separated, lowercase `a-z`, `0-9`, and `-`. The path is lowercased before lookup, so `%iris_WORLD.BIOME%` resolves, but write it lowercase |
|
||||
| Plain text only | No color codes, no unit suffixes, no `%` character in any value, `.` as the decimal separator, no thousands separators |
|
||||
| Pack-name scrubbing | Section-sign sequences and `%` characters inside pack-authored names are removed before the value is returned, so a mischievous biome name cannot inject formatting into a scoreboard |
|
||||
| Genuine zero | Returned as `0`, or `0.00` for two-decimal values. Never `---` |
|
||||
|
||||
Every key answers in one of three ways:
|
||||
|
||||
| Answer | When | What the board shows |
|
||||
|---|---|---|
|
||||
| The value | Known path, data available | The value |
|
||||
| `---` | Known path, nothing to report right now | `---` |
|
||||
| Null | Unknown path | The literal `%iris_...%` |
|
||||
|
||||
Unknown paths stay visible on purpose. There is no blanket empty-string fallback that would hide a typo.
|
||||
|
||||
## Key reference
|
||||
|
||||
### World family
|
||||
|
||||
Everything except `%iris_available%` needs an online player with a tracked position.
|
||||
|
||||
| Placeholder | What it reports |
|
||||
|---|---|
|
||||
| `%iris_available%` | `true` when Iris has registered its terrain service on this server. Works from the console. Says nothing about the player's world |
|
||||
| `%iris_world.available%` | `true` when the reading player's tracked position is in a world Iris generates. The guard for every other `world.*` key |
|
||||
| `%iris_world.biome%` | Display name of the surface biome at the player's X/Z column, for example `Hot Desert Dunes` |
|
||||
| `%iris_world.biome-key%` | Load key of that same biome, for example `desert/hot-dunes`. This is what a pack file is named after |
|
||||
| `%iris_world.region%` | Display name of the region covering the player's X/Z |
|
||||
| `%iris_world.region-key%` | Load key of that region |
|
||||
| `%iris_world.dimension%` | Load key of the dimension the player's world generates from, for example `overworld`. This is the dimension file's key, which is usually but not necessarily the pack folder name |
|
||||
|
||||
From the console, for an offline player, or before a player's first tracked position: `world.available` is `false` and the rest are `---`.
|
||||
|
||||
### Pregeneration family
|
||||
|
||||
One job runs per server, so these are global. Every player and the console see identical values.
|
||||
|
||||
| Placeholder | What it reports |
|
||||
|---|---|
|
||||
| `%iris_pregen.available%` | `true` while a job snapshot is published. Use this as the guard |
|
||||
| `%iris_pregen.world%` | Name of the world being pregenerated |
|
||||
| `%iris_pregen.percent%` | Completion from `0.00` to `100.00`, two decimals, no `%` character |
|
||||
| `%iris_pregen.eta%` | Whole seconds remaining |
|
||||
| `%iris_pregen.eta-text%` | The same estimate formatted for humans: `45s` under a minute, `2m 5s` under an hour, `1h 30m` above it |
|
||||
| `%iris_pregen.chunks%` | Chunks finished so far |
|
||||
| `%iris_pregen.total%` | Chunks the job will generate in total |
|
||||
| `%iris_pregen.chunks-per-second%` | Current generation rate, two decimals |
|
||||
| `%iris_pregen.paused%` | `true` while the job is paused |
|
||||
|
||||
The snapshot is republished on the `STARTED`, `TICK`, `PAUSED`, `RESUMED`, and `SAVING` pregen phases, and cleared on `COMPLETED` and `CANCELLED`. After the clear, `pregen.available` is `false` and every value key is `---`. Before the job has run long enough to estimate, `eta` reads `0` and `eta-text` reads `0s`.
|
||||
|
||||
### Paths as `/papi info iris` prints them
|
||||
|
||||
```
|
||||
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 `%`.
|
||||
|
||||
## What "surface" means, and what a board costs
|
||||
|
||||
`world.biome`, `world.biome-key`, `world.region`, and `world.region-key` are **surface column** readings: whatever the generator places at ground level for that X/Z. Y is not part of the query. A player 60 blocks down in a cave still reads the surface biome overhead, not the cave biome. If you need the biome at an actual Y, that is a terrain API call, not a placeholder — see `91 - API - Terrain.md`.
|
||||
|
||||
### When a position is published
|
||||
|
||||
| Event | Timing |
|
||||
|---|---|
|
||||
| Walking (`PlayerMoveEvent`) | At most once per second, and skipped entirely while the player stays inside the same block column |
|
||||
| Join, respawn, world change, portal, and every teleport — `/iris goto`, `/tp`, ender pearls, random-TP plugins | Immediately, bypassing the one-second interval |
|
||||
| Quit | The player's tracked position and cached view are released |
|
||||
|
||||
Because teleports publish immediately, a player who arrives somewhere and stands still never reads a stale column from where they came from.
|
||||
|
||||
### View rebuild cost
|
||||
|
||||
A player's world view is rebuilt at most once per second (`VIEW_TTL_MS = 1000`), and only when something actually reads a `world.*` key. Three consequences worth knowing before you design a board:
|
||||
|
||||
- A board with six `world.*` keys costs one rebuild per player per second, not six.
|
||||
- Values can trail a sprinting player by up to a second.
|
||||
- A board nobody is reading costs nothing. Iris does not poll terrain in the background for this.
|
||||
|
||||
Pregen values are not polled per player either — they come from one global snapshot updated by `IrisPregenerationEvent`.
|
||||
|
||||
## Permissions
|
||||
|
||||
No placeholder is permission-gated. Anything sensitive is simply not published: there is no seed key, no file path key, and no key that mutates engine state on read.
|
||||
|
||||
## Failure policy
|
||||
|
||||
| Situation | Result |
|
||||
|---|---|
|
||||
| Unknown path | Null to PlaceholderAPI, so the literal `%iris_...%` stays on screen |
|
||||
| Known path, no data | `---` |
|
||||
| No player context on a `world.*` key | `---`, and `world.available` is `false` |
|
||||
| Player outside an Iris world | `---`, and `world.available` is `false` |
|
||||
| Terrain service not registered | `---`, `world.available` is `false`, `%iris_available%` is `false` |
|
||||
| No pregen job | `---`, and `pregen.available` is `false` |
|
||||
| A resolver throws | `---`, plus one logged warning for that path. Logging stops after 64 distinct paths have warned |
|
||||
|
||||
A key that threw is not quarantined. It keeps being called and keeps answering `---` until whatever was wrong resolves itself.
|
||||
|
||||
## Migration from pre-2.0 keys
|
||||
|
||||
The old underscore keys are gone with no aliases and no dual-accept window. They now render literally, which is deliberate — a silently empty scoreboard line is worse than a visibly broken one.
|
||||
|
||||
| Old key | New key | Why |
|
||||
|---|---|---|
|
||||
| `%iris_biome_name%` | `%iris_world.biome%` | Dot grammar |
|
||||
| `%iris_biome_id%` | `%iris_world.biome-key%` | `id` was always the load key; the name now says so |
|
||||
| `%iris_region_name%` | `%iris_world.region%` | Dot grammar |
|
||||
| `%iris_region_id%` | `%iris_world.region-key%` | Same as `biome_id` |
|
||||
| `%iris_biome_file%` | removed | Leaked absolute server paths, and threw whenever the biome had no backing file |
|
||||
| `%iris_region_file%` | removed | Same problem |
|
||||
| `%iris_world_seed%` | removed | A scoreboard has no permission context. Read `IrisWorldInfo.seed()` from the terrain API if a plugin genuinely needs it |
|
||||
| `%iris_terrain_height%` | removed | Reported generated height before objects and player edits, so it regularly disagreed with the block under the player's feet |
|
||||
| `%iris_terrain_slope%` | removed | A pack-authoring diagnostic, far too expensive to run once per player per board refresh |
|
||||
| `%iris_world_mode%` | removed | Studio versus production is not something a live board needs |
|
||||
| `%iris_world_speed%` | removed | Mutated engine rate-window state as a side effect of being read. Use `%iris_pregen.chunks-per-second%` |
|
||||
|
||||
One behavior change hides inside the renames: the old biome and region keys sampled two blocks above the player's feet, so they picked up cave and overhang biomes. The new keys are always the surface column. `%iris_world.dimension%` has no pre-2.0 equivalent.
|
||||
@@ -1,279 +0,0 @@
|
||||
# 10 - Studio & VSCode Schemas
|
||||
|
||||
Studio is the live pack-authoring loop: open a pack as a throwaway world, edit its JSON in an editor that autocompletes against schemas generated from the Java models, save, and watch the running engine rebuild itself. This page walks the loop end to end first, then documents the commands, the hotload rules, and how the schemas are produced.
|
||||
|
||||
Related: see `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `02 - Getting Started.md`, `11 - Dimensions.md`, `21 - Jigsaw Structures.md`, `25 - Pack Management.md`, `30 - Platform Differences.md`.
|
||||
|
||||
## The edit loop
|
||||
|
||||
Prerequisites: a writable packs directory, operator access on Bukkit or gamemaster access on a mod loader, and VSCode/Cursor (or IntelliJ) on the machine that holds the pack folder. Keep the server console visible — hotload reports success and failure there.
|
||||
|
||||
### Bukkit-family
|
||||
|
||||
1. **Create a project.** `/iris studio create name=tutorial`
|
||||
Writes `packs/tutorial/` with a dimension, region, biome, generator, and a `tutorial.code-workspace`. The command reports the completed project path; creation runs asynchronously and may report that a restart is needed before the pack can be opened.
|
||||
2. **Open it as a world.** `/iris studio open tutorial seed=1337`
|
||||
You are teleported into a transient world generated from the live pack folder. A fixed seed matters — you will be comparing the same coordinates across reloads.
|
||||
3. **Open the editor workspace.** `/iris studio vscode dimension=tutorial`
|
||||
Generates `.iris/schema/*` if missing and opens the `*.code-workspace`. On a headless server nothing launches; copy the pack folder to your machine and open the workspace file yourself.
|
||||
*Success condition:* typing `"` inside any object in `biomes/starter.json` offers field names, and hovering a field shows its description, type, and default value. If it does not, the workspace was not opened or the schemas were never written — run `/iris studio update dimension=tutorial`.
|
||||
4. **Make one change.** Edit `packs/tutorial/biomes/starter.json` and change only its display `name`. Save once.
|
||||
5. **Wait for the hotload result** in console before saving anything else. A failed hotload leaves the previous runtime active and reports the error; stacking more edits on top makes the first failure hard to find.
|
||||
6. **Verify in fresh terrain.** Walk into chunks that have never generated and run `/iris what biome`. The new display name appears there. Hotload never rewrites blocks that already exist, so standing still and expecting the world to change is the usual false negative.
|
||||
7. **Validate.** `/iris pack validate pack=tutorial` — no blocking errors.
|
||||
8. **Close.** `/iris studio close`
|
||||
|
||||
### Fabric / Forge / NeoForge
|
||||
|
||||
Same loop, positional arguments, and the modded studio create always copies a template (`example` by default):
|
||||
|
||||
1. `/iris studio create tutorial example`
|
||||
2. `/iris studio open tutorial 1337`
|
||||
3. `/iris studio vscode tutorial`
|
||||
4. Trace the active dimension to one referenced biome, change one display or palette value, save once.
|
||||
5. Wait for the hotload result, then enter newly generated terrain and check it with `/iris what biome`.
|
||||
6. `/iris pack validate tutorial`, then `/iris studio close`.
|
||||
|
||||
The loop passes when the editor binds the generated schema, hotload succeeds, validation reports no blocking errors, and newly generated chunks show the change. Create a production world only after that gate.
|
||||
|
||||
A rejected height or dimension-type change is not evidence that hotload is broken — those are refused by design. See **Hotload rules** below.
|
||||
|
||||
### When something goes wrong
|
||||
|
||||
| Symptom | Meaning | Recovery |
|
||||
|---|---|---|
|
||||
| `open` reports startup validation pending, missing, failed, restart-required, or blocking pack errors | Datapacks or the pack graph cannot safely build an engine | Complete the requested restart or run the platform's `pack validate` form, fix the first blocking error, and retry; do not bypass validation |
|
||||
| Save reports hotload failure | The new data or runtime build failed; the previous runtime may remain active | Fix the first console error and save again before making unrelated edits |
|
||||
| Height, logical height, or dimension type change is rejected | The edit violates `IrisDimensionRuntimeContract` | Close Studio and reopen; on modded, restart when regenerated dimension-type datapacks require a registry reload |
|
||||
| A valid change is invisible | The chunks you are standing in are already materialized, or the edited resource is unreachable from the active dimension | Move to new chunks; trace dimension → region → biome to confirm the resource is actually referenced; use `focus`/`focusRegion` or a buffet studio mode to isolate |
|
||||
| No autocomplete, or resource keys are stale | Schemas were not generated or refreshed, or the editor never opened the workspace | Run `/iris studio update`, then open the pack's `.code-workspace`; on headless servers open it manually |
|
||||
| The Studio world disappears after a restart | Studio worlds are transient and purged on purpose | Reopen the pack; `packs/<key>/` is the source of truth, not the world folder |
|
||||
|
||||
## What Studio is
|
||||
|
||||
| Concept | Behavior |
|
||||
|---------|----------|
|
||||
| Pack workspace | Packs live under the platform data directory in the folder named `packs` (`StudioSVC.WORKSPACE_NAME`) |
|
||||
| Studio world | Opened from a pack dimension key; uses a studio chunk generator bound to the live pack folder with file watching |
|
||||
| Hotload | Studio worlds only. A low-priority looper polls pack files; on change, `EngineHotloader` waits for already-admitted top-level Bukkit chunk stages, reloads the pack data, and rebuilds the engine runtime under exclusive generator control. Fair stage admission keeps later chunk stages behind the waiting transition, and Studio close uses the same drain boundary. Biome Buffet resolves its chunk focus and completes any required complex hotload under exclusive admission before that noise stage opens a generation session, then downgrades directly to one ordinary stage permit |
|
||||
| Hotload contract | `IrisDimensionRuntimeContract` refuses hotload if the dimension type key, min height, total height, or logical height change |
|
||||
| Non-studio worlds | No pack file watcher. Production worlds keep the pack snapshot installed at create or update time |
|
||||
|
||||
Studio settings live in `settings.json` under `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 pack's `*.code-workspace`. Set false on servers where a desktop launch would be pointless or unwanted |
|
||||
| `entitySpawning` | `true` | Only affects Studio worlds. False stops Iris ambient entity spawning there; production worlds always spawn regardless of this key |
|
||||
| `disableTimeAndWeather` | `true` | Present in the settings model but not read by any code path today |
|
||||
| `autoStartDefaultStudio` | `false` | Present in the settings model but not read by any code path today |
|
||||
|
||||
## Hotload rules
|
||||
|
||||
- The watcher runs only when `PlatformChunkGenerator.isStudio()` is true, the world is not closing, and no Jigsaw Studio session is active. Jigsaw Studio deliberately suppresses ordinary pack-file hotload.
|
||||
- On change: load a new `IrisData` from the same folder, reload the dimension key, check the hotload contract, build a new engine runtime, retire the previous data, refresh the workspace and schemas, reload datapacks when a platform world is bound, and broadcast a client studio-hotload toast on success or failure.
|
||||
- `hotloadComplex` is a narrower rebuild that reconstructs `IrisComplex` without reopening the pack.
|
||||
- A failed hotload rolls the runtime back where possible and reports the error.
|
||||
|
||||
Four values are pinned for the life of the world and cannot hotload: the dimension type key (derived from the dimension load key), min height, total height, and logical height. Editing `dimensionHeight`, `logicalHeight`, `environment`, or the dimension file name means closing and reopening Studio. See `11 - Dimensions.md`.
|
||||
|
||||
## Commands (Bukkit)
|
||||
|
||||
Root: `/iris studio`, aliases `std` and `s`. Implemented by `CommandStudio` and `StudioSVC`. Keyed arguments; the first column shows the primary subcommand name.
|
||||
|
||||
| Subcommand | Aliases | What it does |
|
||||
|------------|---------|--------------|
|
||||
| `open <dimension> [seed=1337]` | `o` | Closes any open studio and opens the pack as a studio world. Blocked unless startup datapack validation is ready and the selected pack has a loadable validation result |
|
||||
| `close` | `x` | Closes the active studio project and world |
|
||||
| `create [name=studio] [template=<dimension>]` | `+` | Creates a pack under `packs/<name>` after startup validation is ready. With a template, that template must itself validate as loadable and is downloaded first if missing; without one, writes the starter skeleton below |
|
||||
| `vscode [dimension=default]` | `vsc` | Opens the pack's VSCode workspace, generating it if missing |
|
||||
| `update [dimension=default]` | | Rewrites `<pack>/<name>.code-workspace` and queues regeneration of `.iris/schema/*` |
|
||||
| `version [dimension=default]` | | Prints the dimension's `version` field |
|
||||
| `pkg [dimension=default] [obfuscate=false] [minify=true]` | `package` | Compiles the pack into a distributable archive |
|
||||
| `importvanilla <dimension> [variants=3] [structures=true]` | `importv`, `iv` | Captures vanilla features and structures into the pack through Bukkit NMS |
|
||||
| `scoreboard` | `board`, `sidebar`, `sb` | Toggles the studio debug scoreboard; player must be in the studio world |
|
||||
| `noise [generator=<key>] [seed=12345]` | `nmap` | Opens the external noise explorer GUI |
|
||||
| `map [world=<world>]` | `render` | Opens the external biome/terrain map GUI for an Iris world |
|
||||
| `regions [radius=500]` | | Samples region rarity over a chunk spiral; player must be in an Iris world |
|
||||
| `loot [fast=false] [add=true]` | | Opens a virtual chest showing loot tables for the block under the player |
|
||||
| `profile [dimension=default]` | | Writes a pack performance profile report |
|
||||
| `spawn` | `summon` | Spawns a pack entity definition at the player |
|
||||
| `tpstudio` | `stp` | Teleports to the active studio world spawn in creative |
|
||||
| `objects` | `find-objects` | Captures a nearby-chunk object placement report |
|
||||
|
||||
Permissions and the full `/iris` tree: see `04 - Commands & Permissions.md`.
|
||||
|
||||
## Commands (Modded)
|
||||
|
||||
`/iris studio` on Fabric, Forge and NeoForge is implemented by `ModdedStudioCommands` with positional arguments. Supported: `create`/`+`, `package`/`pkg`, `version`, `regions`, `open`/`o`, `close`/`x`, `tpstudio`/`stp`, `status`, `vscode`/`vsc`, `update`, `noise`/`nmap`, `map`/`render`.
|
||||
|
||||
`create` with no arguments creates a project named `studio` from the `example` template; `create <name>` uses the same template; `create <name> <template>` picks another. `open <pack> [seed]` defaults the seed to `1337`.
|
||||
|
||||
These subcommands are registered on modded but only report why they are unavailable: `importvanilla`/`importv`/`iv` (vanilla capture needs throwaway Bukkit worlds via NMS), `loot` (needs a Bukkit chest inventory GUI), `profile`, `spawn`/`summon` (Bukkit entity pipeline), and `objects`/`find-objects` (reads Bukkit chunk data).
|
||||
|
||||
## Creating a pack (starter skeleton)
|
||||
|
||||
`/iris studio create name=mypack` with no template writes:
|
||||
|
||||
```
|
||||
packs/mypack/
|
||||
dimensions/mypack.json
|
||||
regions/starter.json
|
||||
biomes/starter.json
|
||||
generators/flat.json
|
||||
mypack.code-workspace
|
||||
```
|
||||
|
||||
Starter dimension JSON, verbatim from `StudioSVC.createStarterProject`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mypack",
|
||||
"version": 1,
|
||||
"regions": ["starter"],
|
||||
"logicalHeight": 384,
|
||||
"dimensionHeight": {"min": -64, "max": 320}
|
||||
}
|
||||
```
|
||||
|
||||
The starter region lists the same `starter` biome for land, sea and shore. The starter biome uses generator `flat` at `min` 96 / `max` 96, one grass-block layer, and `minecraft:plains` as both `derivative` and `vanillaDerivative`. The full four-file listing is in `26 - Example - Minimal Dimension.md`.
|
||||
|
||||
Project names are lowercased and must match `a-z`, `0-9`, `_`, `-`, up to 64 characters. The name `studio` is reserved and auto-renamed to the next free suffix. If the target folder already exists, nothing is changed.
|
||||
|
||||
With a template — `/iris studio create name=mypack template=overworld` — Iris downloads the template pack if it is not present, requires it to validate as loadable, and copies its tree into the new pack key.
|
||||
|
||||
## Studio open workflow
|
||||
|
||||
1. Resolve pack folder `packs/<dimensionKey>/` with a loadable `dimensions/<key>.json`.
|
||||
2. Pack validation must not report blocking errors (`PackValidationRegistry`); an unvalidated pack fails closed.
|
||||
3. Close the existing studio if one is open.
|
||||
4. `IrisProject.open` creates a studio world bound to that pack folder — not to a production install copy.
|
||||
5. Optionally launch VSCode when `studio.openVSCode` is true.
|
||||
6. Datapack installation may require a restart after create; the message tells you to re-run `open` after restarting.
|
||||
|
||||
Both ordinary and Jigsaw Studio reuse Iris's startup-loaded datapack runtime only while its pinned compiler-input fingerprint still matches every live `dimensions`, `biomes`, and `snippet` JSON input, the compiler build, and the vanilla-height policy. A changed input, unavailable registry, failed startup recovery, or changed/failed external datapack ingest or removal invalidates reuse and falls back to recovery, compilation, publication, and the existing restart gate; a verified no-change ingest or recovery check restores the prior pin. Object, structure, jigsaw, pool, and ownership edits do not affect generated dimension types or custom biomes and therefore do not force that fallback.
|
||||
|
||||
Ordinary Studio still resolves and teleports through its standard safe entry, may launch the pack workspace, prepares the complete mantle radius, and preserves native structures for generation previews. On Paper 26.2, WorldInit publishes the filtered native-structure placement state once but leaves it uninitialized while native starts, locates, and object-collision volume queries are gated; injection verifies that Paper's canonical chunk-generator getter owns the new Iris generator before native structure state is published. After the exact FULL entry-chunk request and retention ticket settle and the standard safe-entry teleport step succeeds when applicable, the global scheduler claims that exact level, chunk map, generator, and state, starts its placement initialization, registers the exact concentric-ring futures, enables collision-volume queries, and then lowers the structure gate. The final Studio callback also returns to the server scheduler before applying game rules or committing a Jigsaw session. The ring searches finish in the background rather than delaying entry or extending Studio ready time, while normal close, full hotload, and complex hotload wait up to 120 seconds for their exact aggregate before mutating or sealing the engine; a synchronous partial-start failure permanently rejects those transitions for that engine because a complete drain cannot be proven.
|
||||
|
||||
Jigsaw Studio publishes an initialized empty native-structure state even when no managed datapack scope exists, never retains or activates the filtered full state, and keeps starts, references, locates, and native collision-volume queries disabled. Its dedicated open kind also skips the standard-entry teleport, workspace launch, procedural generation-cache warm, complete mantle-radius preparation, and ordinary pack-file hotloader before sending the owner once through the selected workcell destination. Jigsaw graph transactions directly invalidate, reload, evaluate, and rematerialize their owned resources; close and reopen Jigsaw Studio to apply unrelated external pack edits.
|
||||
|
||||
Paper-family entry chunks are requested through the urgent asynchronous chunk API before Iris retains them with a plugin ticket; Folia retains its nonblocking ticket bootstrap and confirms the owning region before entry resolution. If an open fails while that exact request remains active, Iris reports the failure without unloading or closing the generator, rejects another Studio open until cleanup succeeds, and queues the transient world for deletion at the next clean startup if it remains active for another 120 seconds. Closing a Studio stops Iris engine maintenance at `WorldUnloadEvent`, waits for the raw backend unload completion and any tracked native ring preparation before sealing the generator, and the 26.2 noise pipeline keeps its generation lease through terrain and heightmap completion. Forced process termination cannot drain in-process ring futures.
|
||||
|
||||
Every Bukkit Studio open writes one `[Studio timing]` line per lifecycle phase with the transient world, `standard` or `jigsaw` kind, phase duration, and cumulative duration where available. The measured phases separate loaded-runtime reuse, external-datapack recovery, compiler-input fingerprinting, datapack compilation/publication, generator preparation, Bukkit world creation, entry-chunk loading, safe-entry resolution, standard teleport, and finalization. Studio engine timing additionally separates prefetch loading, runtime construction, and the generation-cache warm or its Jigsaw-only skip so a slow open can be correlated with a profiler capture.
|
||||
|
||||
## VSCode workspace
|
||||
|
||||
`IrisCodeWorkspace` writes `<pack>/<packName>.code-workspace`:
|
||||
|
||||
| Workspace setting | Value / purpose |
|
||||
|-------------------|-----------------|
|
||||
| `folders` | `[{ "path": "." }]` — the pack root |
|
||||
| `workbench.colorTheme` | `Monokai` (dark preference `Solarized Dark`) |
|
||||
| `files.autoSave` | `onFocusChange` — so switching windows triggers a hotload |
|
||||
| `[json]` editor options | Bracket indent, smart accept-on-enter, trim trailing whitespace and final newlines, quick suggestions inside strings, replace-mode insert, keyword/snippet/word suggestions off so only schema entries are offered |
|
||||
| `json.maxItemsComputed` | `30000` — large enough that big registry enums still complete |
|
||||
| `json.schemas` | Array of `{ fileMatch, url }` entries, sorted by url |
|
||||
|
||||
The same call also merges the mappings into `<pack>/.idea/jsonSchemas.xml` so IntelliJ picks up the schemas, writing that file whenever there is a mapping it does not already contain. The workspace file is only rewritten when its rendered content changes; if it is unparseable, Iris deletes and recreates it, losing hand-edited workspace settings but never pack content.
|
||||
|
||||
## Schema generation
|
||||
|
||||
`SchemaBuilder` reflects a registrant or snippet class and emits JSON Schema draft-07:
|
||||
|
||||
- `$schema` is `http://json-schema.org/draft-07/schema#`.
|
||||
- `$id` is `https://volmit.com/iris-schema/<lowercased class simple name>.json`.
|
||||
- Every property's `description` is assembled from the field name, its `@Desc` text, the type name, the type's own `@Desc`, a snippet hint where applicable, and the field's **default value**, read by instantiating the owning class. That is why hovering a field in the editor tells you what it defaults to without opening the source.
|
||||
- `@MinNumber`/`@MaxNumber` become `minimum`/`maximum` on numeric fields and `minLength`/`maxLength` on string fields. `@Required` fills the `required` array. `@ArrayType` supplies the array item schema.
|
||||
- Registry annotations become `enum` lists: `@RegistryListResource` for pack resource keys of a given type, `@RegistryListFunction` for computed lists such as mantle component flags, and the platform registry annotations (`@RegistryListBlockType`, `@RegistryListBiome`, `@RegistryListEntityType`, `@RegistryListItemType`, `@RegistryListStructure`, `@RegistryListVanillaStructure`, `@RegistryListVanillaStructureSet`, `@RegistryListNativeJigsawPool`, `@RegistryListPotionEffect`, `@RegistryListEnchantment`, `@RegistryListSpecialEntity`, `@RegistryListFont`, `@RegistryMapBlockState`) for live server registries.
|
||||
- A field with no `@Desc` still emits, with the description `No Field Description`, and the builder logs a warning naming the field and class when the schema is generated. A type with no `@Desc` logs a similar warning.
|
||||
|
||||
**These annotations are editor hints only.** Nothing validates `@Required`, `@MinNumber` or `@MaxNumber` at load time. The schema will underline an out-of-range value in your editor; the engine will load it anyway. The dimension-type height rules in `11 - Dimensions.md` are the exception — those are enforced in code and throw.
|
||||
|
||||
### Snippets
|
||||
|
||||
Classes annotated `@Snippet("<type>")` get their own schema at `.iris/schema/snippet/<type>-schema.json`, and every field of a snippet type is emitted as an `anyOf` of the inline object and a string. The string alternative is itself an `anyOf` of an enum of the snippet files that currently exist and the pattern `^snippet/<type>/`, so both existing and not-yet-created snippet paths validate.
|
||||
|
||||
At load time a Gson type adapter resolves any string in a snippet-typed position: `"snippet/decorator/wildflowers"` reads `<pack>/snippet/decorator/wildflowers.json` and parses it in place. This works anywhere the type appears, including inside arrays. A string that does not start with `snippet/` resolves to null, and a missing snippet file logs an error naming the reference and the JSON path.
|
||||
|
||||
### File matching
|
||||
|
||||
`ResourceLoader.buildSchema()` runs for each loader that reports `supportsSchemas()`, and emits seven glob patterns per folder so nested resource keys are covered:
|
||||
|
||||
| Pack folder pattern | Schema URL (relative to pack) |
|
||||
|---------------------|--------------------------------|
|
||||
| `/<folder>/*.json` through `/<folder>/*/*/*/*/*/*/*.json` (7 depth levels) | `./.iris/schema/<folder>-schema.json` |
|
||||
| `/snippet/<type>/*.json` through 7 levels | `./.iris/schema/snippet/<type>-schema.json` |
|
||||
|
||||
Folders with schemas: `dimensions`, `regions`, `biomes`, `generators`, `loot`, `entities`, `spawners`, `markers`, `blocks`, `expressions`, `mods`, `structures`, `jigsaw-pools`, `jigsaw-pieces`. The object, image, and matter loaders return false from `supportsSchemas()` because their content is binary, so `objects/`, `images/` and `matter/` get no schema.
|
||||
|
||||
Files under `.iris/schema/` are generated editor artifacts. They are safe to delete and are rewritten on the next workspace update; pack content is the JSON under the type folders.
|
||||
|
||||
### What refreshes schemas
|
||||
|
||||
| Trigger | Effect |
|
||||
|---------|--------|
|
||||
| `/iris studio update dimension=<dim>` | Rewrites the workspace and queues schema writes |
|
||||
| `/iris studio open` or `create` | Builds the workspace config, including schemas |
|
||||
| Successful hotload | The platform hook may refresh the workspace |
|
||||
|
||||
Registry-backed enums are captured from the live server, so a schema generated on a server without a mod installed will not offer that mod's blocks. Regenerate after changing the server's mod or datapack set.
|
||||
|
||||
## Studio dimension modes (author testing)
|
||||
|
||||
The dimension field `studioMode` swaps in a debug generator. It is applied by the Bukkit chunk generator only; Fabric, Forge and NeoForge ignore it.
|
||||
|
||||
| Value | Effect |
|
||||
|-------|--------|
|
||||
| `NORMAL` | Default generation |
|
||||
| `BIOME_BUFFET_1x1`, `_3x3`, `_5x5`, `_9x9`, `_18x18`, `_36x36` | Lays every biome out in a grid of that cell size so palettes and decorators can be compared side by side |
|
||||
| `OBJECT_BUFFET` | Object studio generator; also forced automatically while an object studio session is active |
|
||||
| `REGION_BUFFET` | Installs no studio generator today and behaves exactly like `NORMAL` |
|
||||
|
||||
These are testing fields, not production world modes — the production engine mode is `mode.type` (see `11 - Dimensions.md`). Remove `studioMode` before packaging.
|
||||
|
||||
Jigsaw Studio does not add a `studioMode` value; `/iris jigsaw open` and `create` select its generator transiently for one Studio activation.
|
||||
|
||||
## Jigsaw Studio (Bukkit)
|
||||
|
||||
`/iris jigsaw` opens one selected structure graph through the transient Studio lifecycle, but chooses `JigsawStudioGenerator` for that activation without persisting a special dimension mode. The owner enters in creative, `spawn_mobs` is disabled, and natural creature-spawn events are cancelled. Planar Studio has six rotation-independent workcells in a compact three-column by two-row layout: Blank, End Cap, Hallway, L Junction, T Junction, and Cross Junction. Spatial Studio places every variant in its own dedicated cell in one horizontal row. New spatial projects seed seven 15×15×15 cells labeled 0 through 6 connectors, with one additional north, south, east, west, up, then down face-center connector in each successive cell. All seven pieces belong to the start pool; the 1-through-6 connector pieces also belong to the generated pieces pool with an explicit empty terminator and a 16-placement default per piece. Their seed-`1337` preview is a connected blob raised 48 blocks above the editing floor without making the intentionally connectorless piece an unreachable child. There is no orientation, permutation, or derived-rotation gallery.
|
||||
|
||||
Each planar floor is light-gray wool with a red canonical topology glyph and sea-lantern caps at its face-center connector positions. Every workcell has an independent width, height, depth, enabled state, and optional author label. Those dimensions are capacity only: changing one never rewrites a variant object, and the complete change is rejected if any existing variant would no longer fit. Each owned variant has its own exact width, height, depth, and optional label, so one End Cap can be a `16×3×3` longhouse while another End Cap in the same workcell remains `3×3×3`. Per-variant growth or lossless shrink preserves in-bounds canonical content and moves canonical connector payloads and sockets to the new face centers; cropped stored content, connector collisions, or shared/read-only objects reject the transaction. Workcell Settings stages width, height, and depth clicks in the open menu and performs one live regeneration only after **Apply Cell Size**; resizing the loaded variant still reloads that cell in place. Every enabled or disabled cell uses a physical white-concrete edge cage with particle trails drawn inside its editable bounds, and no workcell-bound display entity is spawned. Existing planar variants are rotated into the archetype's canonical display orientation and inverse-rotated during capture, while their piece resources, dimensions, labels, and pool entries remain distinct.
|
||||
|
||||
Create a default planar, Iris-native graph with:
|
||||
|
||||
```text
|
||||
/iris jigsaw create <dimension> <key>
|
||||
```
|
||||
|
||||
`key` is the structure's internal resource path: `village/demo` writes `structures/village/demo.json` and becomes the key used by Iris placements and later editing. Named arguments `structure=` and `name=` are aliases for `key=`; they do not select a separate vanilla structure or template. Omitted options default to `mode=planar`, `compatibility=iris`, `width=15`, `height=15`, `depth=15`, and `seed=1337`; `mode=` completes `planar` or `spatial`, while `compatibility=` completes `iris` or `vanilla`. Existing Iris keys tab-complete for `open`, `edit`, and `reopen`.
|
||||
|
||||
New Iris-compatible planar projects contain one owned piece for every archetype, assign every piece to the weighted `variant-1` structure theme, and mark the End piece terminal. New vanilla-compatible projects contain the same six owned pieces but omit Iris theme and terminal-rule metadata. Open an owned graph with `/iris jigsaw open <dimension> <key>` or the equivalent `edit`/`reopen` alias. Existing unowned Iris graphs use `adopt inspect` then `adopt apply`; managed datapack imports must be cloned. Registered vanilla or datapack jigsaws use `convert`, which creates a separate owned Iris graph.
|
||||
|
||||
The owner can open the six-row control GUI by right-clicking its protected chest, running `/iris jigsaw menu`, or starting three sneaks within 1.5 seconds. Walking into a workcell also makes that physical cell the owner's next menu selection, while left-clicking a workcell selects it and teleports the owner to its horizontal center. The GUI selects workcells, loads and creates variants, independently resizes variants, changes workcell capacity or enabled state, toggles per-workcell connector blocks, restores broken connector blocks from the saved variant, rewinds the latest autosave, adjusts exact pool-entry weights and chances, edits theme membership and piece rules, toggles mandatory caps, navigates to the live preview, and deletes inactive variants or the complete project. Destructive actions require a second confirmation within 10 seconds. **New Blank Variant** clones the active owned piece's complete metadata and every exact pool membership but creates an empty object with the same dimensions; **Duplicate This Cell's Variant** preserves the same metadata and memberships while copying only that source object's bytes. **Duplicate All Enabled Cells as Family** atomically clones the loaded owned variant in every enabled workcell and rebinds the complete family together. All duplication uses service-generated keys and requires active owned variants with owned pool memberships. A duplication clicked during dirty or in-flight autosave is queued once, expedites autosave, and continues automatically only while the request, session, and source variants still match. Iris never chooses a first or lexicographically sorted pool as a fallback; use `/iris jigsaw piece create <poolKey> <pieceKey>` for an empty or unassigned workcell.
|
||||
|
||||
The **Toolbox** page gives the player named stick items bound to the current Studio request and the selected workcell, variant, pool entry, or action. Variant/workcell rename sticks are renamed in an anvil, right-clicked to apply the 64-code-point label, and sneak-right-clicked to reset; control characters and section-sign formatting are rejected. Right-clicking another valid tool performs its action or opens the exact GUI context needed for capacity, per-variant size, themes, or rules. Bound tools use schema `2`; schema-`1` tools and sticks from a replaced or closed Studio are rejected. The active variant uses a jigsaw-block icon, valid evaluation uses an emerald, and lime dye is reserved for the explicitly labeled theme-membership toggle. Destructive stick tools also require a second right-click within 10 seconds.
|
||||
|
||||
Building, marker, container, and machine changes inside a loaded owned workcell autosave after a 40-tick quiet period. Before each changed graph commit, Iris retains the prior complete ownership-manifest closure; identical resource blobs are deduplicated and the newest five iterations remain in one atomic `.iris/jigsaw-history/key-<sha256>.json` sidecar. **Undo Last Autosave** restores the newest entry through the same ownership writer, then reloads the affected active variant; repeated clicks rewind until the five-entry stack is empty. Project creation clears stale same-key history and project deletion removes it. Fresh untouched workcells report **Autosaved**. Later edits replace the pending capture identity, and a busy autosave retries until the current save/load/graph barrier permits it. When the owning player runs `/iris studio open` while Jigsaw Studio is active, Iris expedites and waits for these barriers, claims the close, and continues opening the ordinary Studio; console and non-owner replacement remain blocked. Opening Mojang's jigsaw-block UI starts a persistent owning-region NBT watch; changed tile data marks the workcell dirty, and commands, tools, teleport/world changes, quit, graph operations, **Flush Autosave Now**, close, and enabled-world unload request a final tile snapshot before proceeding. Tracked events include block placement, breakage, buckets, growth, fluids, pistons, redstone, explosions, block-state interactions, recognized mutating commands, inventory click/drag/close plus internal move/pickup, and furnace, brewing-stand, dispenser, and crafter activity. **Flush Autosave Now** and `/iris jigsaw save` only request an immediate flush; if a barrier or scheduler prevents capture from starting, the same pending autosave remains queued for retry. They are not a required authoring step. Paper drains pending work synchronously during disable. A forced Folia plugin disable occurs after Folia rejects new region tasks, so close Studio or wait for `status` to report no pending autosave before a reload or server stop; that late disable hook cannot guarantee a new final cross-region capture. An external integration that bypasses Bukkit events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`.
|
||||
|
||||
Each committed graph is compiled and assembled automatically with seed `1337`. The GUI reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, the selected theme, piece count, and current detail. Iris keeps the assembled blocks on the negative-X side for the active Studio session, replaces them after later commits, and protects the complete preview bounds from edits, fluids, pistons, fire, growth, explosions, entities, and redstone. The live renderer accepts at most 250,000 explicit blocks; a larger result becomes `INVALID` with the render-limit diagnostic. **Go to Preview** or `/iris jigsaw preview goto` teleports above it. This live block preview is separate from `/iris jigsaw preview assemble`, which remains a temporary player-local particle diagnostic for an arbitrary seed.
|
||||
|
||||
Structure themes select one weighted family before assembly. **Duplicate All Enabled Cells as Family** allocates the next `variant-<n>` theme by default, clones the currently loaded owned variant from every enabled workcell with its exact object size and label, duplicates their pool memberships, assigns the new pieces to that family, and atomically loads the new family across those workcells. Individual loaded owned variants can join one or more declared themes; an empty theme list makes a piece available to every selected theme. Pool membership `chance` is an independent `0..1` eligibility gate applied before its positive relative weight. Piece rules constrain minimum/maximum depth, minimum/maximum placements, and terminal status. With mandatory caps enabled, an unresolved open connector must use its direct fallback to place a compatible terminal piece; failure rejects that assembly. Themes, chance gates, piece rules, and mandatory caps are Iris-only and block `VANILLA_PORTABLE` compilation or export when used.
|
||||
|
||||
Connector blocks are hidden per workcell by default and can be shown from **Workcell Settings**. **Reset Connector Blocks** rewrites every saved connector coordinate in the selected workcell from the active on-disk variant while leaving every other edited block unchanged; it restores jigsaw orientation and NBT while visible, or the exact final block and tile NBT while hidden. If an autosave already committed a deleted connector, use **Undo Last Autosave** first. Hidden capture retains each connector's pool, identity, orientation, priorities, channel, and authored order while the ordinary block and tile NBT at that coordinate become its exact final state; visible mode exposes Mojang's jigsaw UI for pool, name, target, joint, final state, and both signed priorities. `/iris jigsaw connector channel <channel|none>` changes the saved Iris-only channel for the exact targeted visible connector. Particle trails outline nearby and focused editable bounds inside the white-concrete cages; connector diagnostics remain red for incomplete identity, lime for a valid connector without a channel, and deterministic by channel otherwise. The Iris scoreboard switches automatically to Jigsaw context and shows the structure, author workcell label, canonical solver role when the label differs, loaded variant label, state, and `Triple-sneak for controls`; `/iris studio scoreboard` retains its session-only toggle behavior. Every successful atomic workcell save plays one owner-local bell.
|
||||
|
||||
Bukkit has one global Studio project/world and one owning Jigsaw session. Only that owner can control or mutate it. Non-owner edits are cancelled, non-owner commands use a strict informational/communication allowlist, and the control chest plus live preview are protected. Autosave, variant switching, graph changes, opening, closing, and deletion share operation barriers. Close waits for clean state unless `discard=true`; discard is only for deliberately losing pending work.
|
||||
|
||||
Dimensions are capped at 128 blocks on X/Z, 192 on Y, and 2,097,152 blocks in total; planar variant and capacity width/depth must each be at least 3. A workcell capacity change persists only structure metadata, verifies every variant fits, leaves all object bytes unchanged, and atomically regenerates the affected white-concrete cage, objects, connector view, and block-entity hydration before editing resumes. Workcell Settings batches all three staged axes into that one transaction; **Discard Size Changes** cancels the menu-local values without writing. `variant resize` and the **Variant Size** screen change only the selected owned object; lossless growth and shrink preserve in-bounds canonical content, reject cropped explicit air/blocks/tiles/connectors, and relocate planar canonical sockets. The loaded variant reloads in place; inactive planar variants remain untouched, while a spatial variant's dedicated cell moves with the live row. **Resize to Capacity** or `/iris jigsaw piece expand` is a convenience for setting one selected object exactly to its current capacity. On Folia, each intersecting object chunk is read on its owning region and one graph write begins only after the full snapshot validates.
|
||||
|
||||
Deleting a planar variant is limited to an owned, inactive variant when another variant remains in that workcell. A spatial variant is always active in its dedicated cell, so deletion removes that cell when at least one other spatial variant remains. Project deletion first verifies ownership hashes and scans the pack for external JSON or ownership-manifest references; any reverse reference blocks deletion. A clear result closes Studio and removes the complete owned resource set through a hash-pinned transaction. If the post-close delete fails, the project files remain on disk for recovery.
|
||||
|
||||
This command tree is Bukkit-only. Saved `PLANAR_JIGSAW` and `SPATIAL_JIGSAW` pack resources run in the shared core on Fabric, Forge, and NeoForge, and strict `VANILLA_PORTABLE` graphs can be exported as Minecraft 26.2 datapacks. The complete workflow, commands, marker rules, portability blockers, and recovery steps are in `21 - Jigsaw Structures.md`.
|
||||
|
||||
## Platform notes
|
||||
|
||||
| Platform | Studio |
|
||||
|----------|--------|
|
||||
| Paper / Purpur / Folia (Bukkit plugin) | Full `CommandStudio` plus file-watch hotload on studio worlds; `studioMode` honored; Jigsaw Studio available |
|
||||
| Fabric / Forge / NeoForge | Studio open/create/workspace/package and a subset of tooling; no Bukkit-only importers or inventory GUIs; `studioMode` ignored; no Jigsaw Studio authoring commands |
|
||||
|
||||
Pack JSON contracts are shared across every platform, and schemas are built from the same core models, so a pack authored on one platform loads on all of them.
|
||||
@@ -1,466 +0,0 @@
|
||||
# 11 - Dimensions
|
||||
|
||||
A dimension is the root object of a pack: it decides how tall the world is, where sea level sits, which regions supply biomes, and which engine stages run. It lives at `dimensions/<loadKey>.json` and everything else in the pack hangs off it. Some of its fields are a permanent contract with the world folder and some are free to change every save — this page separates those two groups and explains what each knob actually does to generated terrain.
|
||||
|
||||
Related: see `05 - Concepts & Pack Layout.md`, `10 - Studio & VSCode Schemas.md`, `12 - Regions.md`, `14 - Generators & Noise.md`, `15 - Caves & Carving.md`, `18 - Structures Overview.md`, `22 - Native Structures & Datapacks.md`, `26 - Example - Minimal Dimension.md`.
|
||||
|
||||
## Decide these before you create a world
|
||||
|
||||
Iris pins four values when a world binds to an engine: the dimension type key (derived from the dimension load key), the minimum Y, the total height (`max - min`), and `logicalHeight`. That record is `IrisDimensionRuntimeContract`. Hotload compares the record before and after every pack reload and refuses the reload if any of the four changed, so a height edit in a running Studio world is rejected until you close and reopen. On a production world the values are baked into the generated Minecraft dimension type, so changing them means recreating the world.
|
||||
|
||||
| Field | Why it is a contract |
|
||||
|-------|----------------------|
|
||||
| Dimension file name (load key) | Becomes the dimension type key `iris:<sanitized load key>`; renaming the file makes the world look for a dimension type that no longer exists |
|
||||
| `dimensionHeight` | Min Y and total height are written into the Minecraft dimension type and into every stored mantle/chunk coordinate |
|
||||
| `logicalHeight` | Written into the same dimension type |
|
||||
| `environment` | Selects which vanilla dimension template the generated type is built from, which changes the type's identity |
|
||||
|
||||
Everything else — regions, zooms, noise styles, palettes, ores, deposits, caves, structures, decoration, loot — reloads live in Studio and applies to newly generated chunks. Iterate on those freely; already-generated chunks keep whatever they were built with.
|
||||
|
||||
## File location and load key
|
||||
|
||||
The load key is the path under `dimensions/` with `.json` removed. `dimensions/overworld.json` is key `overworld`; `dimensions/foo/bar.json` is key `foo/bar`. The world create and studio open commands take that key.
|
||||
|
||||
The key is also sanitized into the dimension type key: lowercased, path separators and any character outside `a-z0-9_-./` replaced with `_`. Two dimensions whose keys sanitize to the same string collide on the same generated dimension type, so keep keys distinct in more than punctuation.
|
||||
|
||||
`/iris studio create name=mypack` writes `dimensions/mypack.json` with a matching `name`. Keep the file name and the load key stable once a world exists.
|
||||
|
||||
## Role in the pack graph
|
||||
|
||||
```
|
||||
Dimension → regions[] → Region → land/sea/shore/cave biomes[] → Biome → generators[]
|
||||
→ ores, deposits, depositVariants, overlayNoise
|
||||
→ caveProfile, carving[], structures[], importedStructures, importedFeatures
|
||||
→ loot, entitySpawners, blockDrops
|
||||
```
|
||||
|
||||
The dimension never names a biome directly (outside `focus` and carving entries). Biomes are reached through regions, so an unreferenced region file is dead weight and a biome missing from every region list never generates.
|
||||
|
||||
## Engine mode
|
||||
|
||||
`mode.type` picks which stages the engine registers. This is the single biggest behavioral switch in the file: three of the four modes register only a terrain pass and a biome pass, so caves, objects, decorations, deposits and post-processing do not run at all in them. Do not reach for `ISLANDS` or `ENCLOSURE` expecting a themed world — today they generate exactly what `SUPERFLAT` does.
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": { "type": "OVERWORLD" }
|
||||
}
|
||||
```
|
||||
|
||||
| Type | Stages actually registered | Use it when |
|
||||
|------|----------------------------|-------------|
|
||||
| `OVERWORLD` | Biome, mantle matter generation, terrain, carve, post-process, floating child biome solids, deposits, matter insert, decoration, floating child biome decoration, perfection, custom modifiers | Any world that needs caves, objects, structures, decorations or entities — this is the only complete mode |
|
||||
| `SUPERFLAT` | Terrain and biome only | Fast noise/biome-layout previews, or a genuinely bare world; nothing else generates |
|
||||
| `ENCLOSURE` | Terrain and biome only | Nothing yet; the nether-style ceiling/floor treatment is not implemented |
|
||||
| `ISLANDS` | Terrain and biome only | Nothing yet; floating-island terrain comes from biome `floatingChildBiomes` in `OVERWORLD` mode, not from this |
|
||||
|
||||
`mode` is marked required in the schema, but Gson supplies a default `IrisDimensionMode` when the field is absent, so an omitted `mode` runs `OVERWORLD`. The shipping overworld pack omits it. If the mode factory throws, the engine logs the failure once, warns that it is falling back, and builds `OVERWORLD` instead.
|
||||
|
||||
`IrisDimensionMode` is a snippet type (`dimension-mode`), so `"mode": "snippet/dimension-mode/overworld"` is also valid.
|
||||
|
||||
## Vertical layout: height, sea level, bedrock
|
||||
|
||||
Iris generates internally from `0` to `dimensionHeight.max - dimensionHeight.min`, then shifts the finished chunk down by `dimensionHeight.min` on output. Almost every Y number the engine handles internally is in that shifted space, but the numbers you write in the dimension JSON are not all in the same space, which is the most common source of confusion in this file:
|
||||
|
||||
- `dimensionHeight.min` / `dimensionHeight.max` are **world Y**.
|
||||
- `fluidHeight` is **world Y**. `IrisDimension.getFluidHeight()` returns `fluidHeight - dimensionHeight.min`, which is what the engine uses internally, so sea level ends up back at the world Y you wrote. The shipping overworld sets `fluidHeight` 50 with `min` -256, and its ocean surface is at world Y 50.
|
||||
- `caveLavaHeight` is **internal Y**. World Y = `caveLavaHeight + dimensionHeight.min`. The default 8 with a min of -64 puts the cave lava ceiling at world Y -56.
|
||||
- Bedrock is written at internal Y 0, which is world Y `dimensionHeight.min`.
|
||||
|
||||
Biome generator heights are relative to sea level: a biome generator entry with `min` 4 / `max` 10 produces terrain 4 to 10 blocks above `fluidHeight`, clamped to the dimension's height range.
|
||||
|
||||
```json
|
||||
{
|
||||
"dimensionHeight": { "min": -64, "max": 320 },
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 63,
|
||||
"bedrock": true,
|
||||
"caveLavaHeight": 8
|
||||
}
|
||||
```
|
||||
|
||||
That fragment is a vanilla-shaped world: build floor at -64, ceiling at 320, sea at 63, bedrock at -64, cave lava below -56.
|
||||
|
||||
Minecraft imposes hard rules on the generated dimension type, and Iris fails when they are broken rather than clamping:
|
||||
|
||||
- `dimensionHeight.max - dimensionHeight.min` must be a multiple of 16, and between 16 and 4064.
|
||||
- `dimensionHeight.min` must be a multiple of 16, and between -2032 and 2031.
|
||||
- `logicalHeight` must be between 0 and the total height.
|
||||
|
||||
`/iris pack validate` does not check these — it only checks that structure placements fit inside the declared range. A bad height passes validation and then fails when Iris compiles the dimension type, at boot or at studio open. Check the arithmetic yourself before creating the world.
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `dimensionHeight` | `IrisRange` | `{ "min": -64, "max": 320 }` | The world's build floor and ceiling in world Y. Raise `max` for tall mountain packs, lower `min` for deep-cave packs. Contract field: pick it once per world |
|
||||
| `logicalHeight` | int | `256` | The vanilla logical height of the generated dimension type — the ceiling gameplay teleports respect (nether portal search, chorus fruit). Usually set equal to the total height. Contract field |
|
||||
| `fluidHeight` | int | `63` | World Y of the ocean surface, and the baseline every biome generator height is measured from. Lowering it makes the same biome generators produce relatively taller land; raising it drowns low biomes. Not a contract field, but changing it on a live world leaves a visible seam between old and new chunks |
|
||||
| `bedrock` | boolean | `true` | Writes a bedrock layer at the build floor. Turn it off for void-bottom or stacked-dimension packs |
|
||||
| `caveLavaHeight` | int | `8` | Internal Y at or below which carved cave space fills with lava instead of air. Raise it to flood deep caves, set it to 0 for dry caves. Explicit fluid intent from a carver overrides this |
|
||||
| `name` | string | `"A Dimension"` | Display name shown by commands and the studio scoreboard. Cosmetic |
|
||||
| `version` | int | `1` | A stamp you control. Iris does not act on it; it exists so pack updates can be recognized and so operators do not silently swap incompatible pack generations under an existing world |
|
||||
|
||||
## Environment and dimension-type options
|
||||
|
||||
`environment` picks which vanilla dimension template Iris starts from when it generates this dimension's type; `dimensionOptions` then overrides individual attributes of that template. Together they control sky, fog, ambient light, portal scale, whether beds work, whether raids can start, and the rest of the dimension-type surface. They do not change terrain.
|
||||
|
||||
```json
|
||||
{
|
||||
"environment": "NETHER",
|
||||
"dimensionOptions": {
|
||||
"ultrawarm": "TRUE",
|
||||
"skylight": "FALSE",
|
||||
"ceiling": "TRUE",
|
||||
"coordinateScale": 8,
|
||||
"ambientLight": 0.1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| `environment` | Base template |
|
||||
|---------------|---------------|
|
||||
| `NORMAL` | Overworld |
|
||||
| `NETHER` | Nether |
|
||||
| `THE_END` | End |
|
||||
| `CUSTOM` | Overworld — the enum value exists but resolves through the same default branch as `NORMAL` |
|
||||
|
||||
`fullbright` is a shortcut: when true, Iris copies `dimensionOptions` and forces `ambientLight` to `1.0` before generating the type. On both supported Minecraft versions (26.1.2 and 26.2 share the same datapack fixer) a resolved ambient light of `1` also emits `minecraft:visual/ambient_light_color` `#ffffff`, so a fullbright world reads as flat white rather than merely bright.
|
||||
|
||||
Tri-state options are `DEFAULT`, `TRUE`, or `FALSE`; `DEFAULT` inherits from the base template. Numeric options use `-1` as "unset".
|
||||
|
||||
| `dimensionOptions` field | Default | Effect |
|
||||
|--------------------------|---------|--------|
|
||||
| `ultrawarm` | `DEFAULT` | Water evaporates, sponges dry, snow golems melt, lava spreads fast and thin, dripstone drips lava. Set `TRUE` for nether-like packs |
|
||||
| `natural` | `DEFAULT` | When false, compasses spin and beds cannot set spawn or be slept in; when true, nether portals spawn zombified piglins and creaking hearts activate |
|
||||
| `piglinSafe` | `DEFAULT` | When false, piglins and hoglins zombify here |
|
||||
| `respawnAnchorWorks` | `DEFAULT` | When false, a respawn anchor explodes instead of setting spawn |
|
||||
| `bedWorks` | `DEFAULT` | When false, beds explode instead of sleeping |
|
||||
| `raids` | `DEFAULT` | Whether Bad Omen can start a raid here |
|
||||
| `skylight` | `DEFAULT` | Whether the dimension receives sky light at all. `FALSE` makes a permanently dark world regardless of the terrain ceiling |
|
||||
| `ceiling` | `DEFAULT` | Declares a logical bedrock ceiling to the client and to gameplay rules. Independent of whether terrain actually has a roof |
|
||||
| `coordinateScale` | `-1` (unset) | Portal coordinate multiplier when travelling out of this dimension. `8` reproduces nether-style compression |
|
||||
| `ambientLight` | `-1` (unset) | 0 to 1. Raises the light floor so caves are never fully black. A resolved `1` also forces white ambient light color |
|
||||
| `fixedTime` | `-1` (unset) | Locks the time of day to this tick value. Leave unset for a normal day cycle |
|
||||
| `cloudHeight` | `-1` (unset) | Y of the cloud layer, between -2032 and 2031. `null` disables clouds |
|
||||
| `monsterSpawnBlockLightLimit` | `-1` (unset) | 0 to 15. Maximum block light at which hostile mobs may spawn |
|
||||
|
||||
## Regions and spatial layout
|
||||
|
||||
`regions` is the only mandatory content link in the file. Region selection happens first, per column: `regionStyle` picks the region, `continentalStyle` and `landChance` decide land versus sea inside it, then the land/sea/shore/cave biome styles pick a biome from that region's lists. The zoom fields scale the noise inputs — higher zoom means larger, slower-changing features.
|
||||
|
||||
```json
|
||||
{
|
||||
"regions": ["temperate", "frozen", "tropical"],
|
||||
"landChance": 0.69,
|
||||
"regionZoom": 16.15,
|
||||
"biomeZoom": 1.0,
|
||||
"coordFractureDistance": 20,
|
||||
"coordFractureZoom": 8,
|
||||
"dimensionAngleDeg": 12
|
||||
}
|
||||
```
|
||||
|
||||
Tune this group in Studio with a fixed seed and compare the same coordinates between reloads. Every field here is safe to hotload.
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `regions` | string[] | empty | The region load keys this dimension may place. Required — an empty list produces a world with no biomes to select. Add a region here after creating its file, or the file never generates |
|
||||
| `landChance` | double | `0.625` | Fraction of continental noise that becomes land. Push toward 1.0 for a continental world, toward 0.0 for an archipelago or ocean world |
|
||||
| `regionZoom` | double | `1` | Scales region cells. Small values give many small climate patches; the shipping overworld uses `16.15` for continent-sized climate bands |
|
||||
| `landZoom` | double | `1` | Scales the land-biome selection space independently of regions. Raise it to make each land biome patch bigger without changing where regions sit |
|
||||
| `seaZoom` | double | `1` | Same, for ocean biomes |
|
||||
| `continentZoom` | double | `1` | Scales the land/sea mask. Raise it for fewer, larger continents |
|
||||
| `biomeZoom` | double | `1` | Global multiplier over every biome selection stream. The quickest way to make all biomes uniformly bigger or smaller |
|
||||
| `regionStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Noise style that shapes region borders. Change it when region edges look too regular or too noisy |
|
||||
| `continentalStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Noise style for the land/sea mask — this is what coastlines look like |
|
||||
| `landBiomeStyle` / `seaBiomeStyle` / `shoreBiomeStyle` / `caveBiomeStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Per-category biome border shapes. Cellular styles give patchwork borders; simplex-family styles give organic blobs |
|
||||
| `coordFractureDistance` | double | `20` | How far, in blocks, coordinate warping can displace a sample. This is what produces Iris's characteristic swirls. Set to 0 for straight, unwarped borders |
|
||||
| `coordFractureZoom` | double | `8` | Frequency of that warping. Lower values warp more rapidly and more violently; the shipping overworld uses `0.15` |
|
||||
| `dimensionAngleDeg` | double | `0` | Rotates every input coordinate by this angle. Breaks up axis-aligned artifacts. Pick something off 45 and 90 — the shipping overworld uses 69 |
|
||||
| `focus` | string | `""` | Forces the whole world to one biome load key. Testing only; remove before packaging |
|
||||
| `focusRegion` | string | `""` | Forces the whole world to one region load key. Testing only; remove before packaging |
|
||||
|
||||
## Rock, fluid, and overlay noise
|
||||
|
||||
`rockPalette` is the material the terrain column is filled with below the biome's own surface layers, and `fluidPalette` is what fills ocean columns and any cave aquifer that allows fluid. Both are full weighted palettes, so "stone" can be a blend of stone, andesite and tuff, and "water" can be lava, or a custom mod fluid.
|
||||
|
||||
```json
|
||||
{
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{ "block": "minecraft:stone", "weight": 6 },
|
||||
{ "block": "minecraft:andesite", "weight": 2 },
|
||||
{ "block": "minecraft:tuff" }
|
||||
]
|
||||
},
|
||||
"fluidPalette": { "palette": [{ "block": "minecraft:water" }] }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `rockPalette` | `IrisMaterialPalette` | `stone` | Subsurface fill for every column that a biome layer does not claim. Change it for a themed world (deepslate planet, sandstone desert world). Biomes and regions can override it locally |
|
||||
| `fluidPalette` | `IrisMaterialPalette` | `water` | Blocks used for ocean columns and for cave aquifers that allow fluid. Set it to lava for a magma world |
|
||||
| `overlayNoise` | `IrisShapedGeneratorStyle[]` | empty | Extra height noise summed on top of the interpolated biome height, everywhere, ignoring biome boundaries. Use it for a global roughness or a world-wide swell that must not follow biome edges |
|
||||
| `rockZoom` | double | `5` | Declared but never read by the engine. Leave it alone; palette scaling comes from the palette's own `zoom` |
|
||||
|
||||
## Ores and deposits
|
||||
|
||||
Iris has two independent ways to put ore in the ground, and they behave differently.
|
||||
|
||||
**Ores** (`ores`) are noise-threshold generators evaluated per block while terrain is written. Each generator declares its own Y `range` and a `generateSurface` flag; Iris keeps two separate lists and only consults the matching one, so a generator with `generateSurface: false` never appears in the exposed surface layer. Ores also exist at region and biome scope; all three scopes are consulted, dimension last.
|
||||
|
||||
**Deposits** (`deposits`) are blob placements written through the mantle, closer to vanilla ore veins, with per-chunk counts and sizes.
|
||||
|
||||
**Deposit variants** (`depositVariants`) rewrite the block that any of the above would have placed, inside a world-Y band. This is how the shipping pack turns `minecraft:iron_ore` into `minecraft:deepslate_iron_ore` below Y 0 without duplicating every generator.
|
||||
|
||||
```json
|
||||
{
|
||||
"ores": [
|
||||
{
|
||||
"palette": { "palette": [{ "block": "minecraft:bedrock" }] },
|
||||
"chanceStyle": { "style": "FLAT" },
|
||||
"threshold": 1,
|
||||
"range": { "min": 0, "max": 0 }
|
||||
}
|
||||
],
|
||||
"depositVariants": [
|
||||
{
|
||||
"minHeight": -64,
|
||||
"maxHeight": 0,
|
||||
"remap": { "minecraft:iron_ore": "minecraft:deepslate_iron_ore" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `ores` | `IrisOreGenerator[]` | empty | Noise-driven ore placement across the whole dimension. Each entry has a palette, a `chanceStyle`, a `threshold`, a Y `range`, and `generateSurface`. Use dimension scope for ores that must exist everywhere regardless of biome |
|
||||
| `deposits` | `IrisDepositGenerator[]` | empty | Blob deposits with per-chunk min/max counts and blob sizes. The shipping pack uses these for granite, andesite, diorite, gravel and the classic ores |
|
||||
| `depositVariants` | `IrisDepositVariant[]` | empty | Source-to-replacement block remaps inside a world-Y band, applied after biome and region rules; the first matching dimension rule wins. Source matching ignores block properties |
|
||||
| `hideOresForHiddenOre` | boolean | `false` | Replaces every ore the generator would write — terrain ores, deposits, and ores baked into objects — with the surrounding base material. Turn it on only when a drop-control plugin such as HiddenOre is supplying ores at break time instead |
|
||||
|
||||
## Caves and carving
|
||||
|
||||
Three fields at dimension scope decide whether caves exist and what they look like.
|
||||
|
||||
`caveProfile` is the 3D cave configuration. The same object exists on regions and biomes, and the most specific enabled profile wins: dimension, then region, then surface biome, then cave biome. So a dimension-level profile is the default cave system and a region can replace it wholesale for its own climate.
|
||||
|
||||
`carving` maps absolute world-Y bands to cave biomes, which is how the shipping pack puts a deep-dark biome between Y -250 and -175 without touching surface biome selection. Entries can nest through `children` for patchy sub-regions, bounded by `childRecursionDepth`.
|
||||
|
||||
`carvingEnabled: false` is implemented by adding the `CARVED` mantle flag to the disabled set — it is exactly equivalent to listing `CARVED` in `disabledComponents`.
|
||||
|
||||
```json
|
||||
{
|
||||
"carvingEnabled": true,
|
||||
"caveProfile": { "enabled": true },
|
||||
"carving": [
|
||||
{
|
||||
"id": "global-deepdark-band",
|
||||
"enabled": true,
|
||||
"biome": "carving/standard-deepdark",
|
||||
"worldYRange": { "min": -250, "max": -175 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `carvingEnabled` | boolean | `true` | Master switch for carving. Setting it false disables the `CARVED` mantle component, so no cave, canyon or carver output is written anywhere. Use it to isolate a terrain problem from a cave problem |
|
||||
| `caveProfile` | `IrisCaveProfile` | disabled default object | The dimension's default 3D cave system: density styles, vertical range, threshold, surface clearance. Regions and biomes override it when they enable their own. Full field reference in `15 - Caves & Carving.md` |
|
||||
| `carving` | `IrisDimensionCarvingEntry[]` | empty | Cave-biome overrides keyed to absolute world-Y bands, each with a stable `id`, a `biome`, a `worldYRange`, optional `children`, `childStyle`, `childShrinkFactor`, and `childRecursionDepth`. Use it for depth-banded cave themes such as a deep dark layer |
|
||||
| `useMantle` | boolean | `true` | Disables the entire mantle when false: no objects, jigsaw structures, features, entities or deferred block updates. Terrain and decoration still run. Only useful for isolating mantle cost or debugging |
|
||||
| `disabledComponents` | mantle flag strings | empty | Turns off individual mantle components by flag. The registered components are `OBJECT`, `JIGSAW`, `CARVED`, and `FLOATING_OBJECT`. Cheaper than `useMantle: false` when you only need to silence one subsystem |
|
||||
|
||||
## Objects, decoration, and post-processing
|
||||
|
||||
These fields gate the passes that run after terrain and carving. All of them hotload.
|
||||
|
||||
```json
|
||||
{
|
||||
"decorate": true,
|
||||
"postProcessing": true,
|
||||
"postProcessingSlabs": true,
|
||||
"postProcessingWalls": true,
|
||||
"requireObjectSurfaceSupport": true,
|
||||
"objectSurfaceSupportBuffer": 2,
|
||||
"preventLeafDecay": false
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `decorate` | boolean | `true` | Runs the decorator pass (grass, flowers, seafloor clutter, ceiling growths). Turn it off to look at bare terrain shape |
|
||||
| `postProcessing` | boolean | `true` | Master switch for the post pass that smooths block-level artifacts |
|
||||
| `postProcessingSlabs` | boolean | `true` | Lets the post pass place slabs to soften one-block terrain steps |
|
||||
| `postProcessingWalls` | boolean | `true` | Lets the post pass paint wall blocks against exposed faces |
|
||||
| `requireObjectSurfaceSupport` | boolean | `true` | Refuses to place surface objects and trees that would hang over a carved opening. Turning it off produces floating buildings above caves; the per-placement flag can only opt out further, never override this on |
|
||||
| `objectSurfaceSupportBuffer` | int | `2` | Minimum solid blocks required beneath a surface placement, 0 to 16. The effective value is the larger of this and the placement's own buffer, so raising it hardens every placement in the dimension at once |
|
||||
| `preventLeafDecay` | boolean | `false` | Marks generated leaves persistent so they never decay when the supporting log is removed. Turn it on for packs whose custom trees have unusual leaf-to-log distances |
|
||||
| `treeSettings` | `IrisTreeSettings` | disabled default | Overrides vanilla sapling growth with pack objects. See `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md` |
|
||||
|
||||
## Structures and datapacks
|
||||
|
||||
Dimension-level `structures` entries are Iris placements considered everywhere in the dimension, independent of any biome or region placement. `importedStructures` and `importedFeatures` control what vanilla, mod, and ingested-datapack content is allowed to generate on top of Iris terrain.
|
||||
|
||||
`anchor` is what makes a placement's vertical contract explicit. `LEGACY` preserves the historical `underground` boolean; the named anchors do not depend on it. Cave anchors read Iris carved-space data, so they apply only to editable `structures` and never to the `nativeStructures` backend.
|
||||
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"placementId": "ancient-city-native",
|
||||
"nativeStructures": [{ "structure": "minecraft:ancient_city" }],
|
||||
"nativeSuppression": "REPLACE_SOURCE",
|
||||
"minHeight": -220,
|
||||
"maxHeight": -220
|
||||
}
|
||||
],
|
||||
"importedStructures": {
|
||||
"datapackOverrides": true,
|
||||
"disabled": [],
|
||||
"adjustments": [
|
||||
{ "match": ["minecraft:mineshaft"], "preserveSourceY": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `structures` | `IrisStructurePlacement[]` | empty | Iris structure placements at dimension scope. Use this for content that must exist regardless of biome, such as a global stronghold analogue or a native structure you are re-anchoring |
|
||||
| `importedStructures` | `IrisImportedStructureControl` | default | Allow/deny and Y-adjustment rules for every registered native structure. Every registered structure generates by default; `disabled` is the only deny list. `adjustments` can shift, band, encase, or stilt a structure into Iris terrain |
|
||||
| `importedFeatures` | `IrisImportedFeatureControl` | disabled | Off by default: leaving it out generates exactly the terrain Iris always has. Setting `enabled` true runs the vanilla placed-feature decoration pass (ores, trees, plants, springs, geodes) over Iris terrain, filterable by `disabled` keys, `steps`, and `disabledSteps`. Carvers are never imported |
|
||||
| `datapackImports` | string[] | empty | External datapack URLs this dimension owns. Their structure sets and definitions generate and locate only in dimensions that declare the same source. Replacing native generation still requires a placement with `nativeSuppression: REPLACE_SOURCE`; declaring the source alone never disables anything |
|
||||
|
||||
Anchor values for editable placements: `LEGACY`, `SURFACE`, `HEIGHT_BAND`, `CAVE_FLOOR`, `CAVE_CEILING`, `CAVE_CENTER`, `CAVE_ANY`. Details in `18 - Structures Overview.md`, `21 - Jigsaw Structures.md`, and `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
## Upper dimension (inverted ceiling terrain)
|
||||
|
||||
Set `upperDimension` to another dimension's load key (or this dimension's own key) and Iris generates that dimension's terrain upside-down against the world ceiling, nether-style. `"none"` or an empty string disables it; the shipping overworld ships with `""`.
|
||||
|
||||
```json
|
||||
{
|
||||
"upperDimension": "overworld",
|
||||
"upperDimensionGap": 32,
|
||||
"upperDimensionCarving": false,
|
||||
"upperDimensionObjects": false
|
||||
}
|
||||
```
|
||||
|
||||
If the referenced key cannot be loaded, Iris warns and skips upper terrain rather than failing the world.
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `upperDimension` | string | `"none"` | Load key of the dimension whose terrain becomes the ceiling. Self-reference is allowed and produces a mirrored world |
|
||||
| `upperDimensionGap` | int | `32` | Minimum air blocks kept between the lower surface and the upper surface, 0 to 256. Raise it if the two halves close up in high terrain |
|
||||
| `upperDimensionCarving` | boolean | `false` | Lets cave carving cut through the ceiling mass. False leaves it solid |
|
||||
| `upperDimensionObjects` | boolean | `false` | Lets mantle objects place in the upper zone. False protects the ceiling from trees and structures |
|
||||
| `upperObjectsForcePlace` | boolean | `false` | Upper objects ignore slope, underwater, clamp, collision, and carving restrictions. Lower-dimension objects always place first, so enabling this lets upper objects clip through them |
|
||||
|
||||
## Loot, spawners, and block drops
|
||||
|
||||
These are dimension-wide fallbacks. Regions and biomes layer on top of them.
|
||||
|
||||
```json
|
||||
{
|
||||
"loot": { "mode": "FALLBACK", "tables": ["global-clutter"] },
|
||||
"entitySpawners": ["swamp/passive"],
|
||||
"blockDrops": []
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `loot` | `IrisLootReference` | empty, mode `ADD`, multiplier `1` | Loot tables applied to containers generated anywhere in the dimension. `ADD` stacks this scope's tables onto the parent list; `CLEAR` and `REPLACE` both drop the parent tables first; `FALLBACK` supplies tables only when the object itself declared none. `multiplier` scales item counts 0 to 16. Use dimension scope for global clutter, region scope for climate flavor |
|
||||
| `entitySpawners` | string[] | empty | `IrisSpawner` load keys that continually replenish mobs like vanilla does. Dimension scope means "everywhere in this world" |
|
||||
| `blockDrops` | `IrisBlockDrops[]` | empty | Custom drop overrides for specific blocks in this dimension |
|
||||
|
||||
## Studio and debug fields
|
||||
|
||||
These exist to help you look at the generator, not to ship. `studioMode` is applied only by the Bukkit chunk generator; on Fabric, Forge and NeoForge the field is ignored.
|
||||
|
||||
| Field | Type | Default | What it does and when to change it |
|
||||
|-------|------|---------|------------------------------------|
|
||||
| `studioMode` | `StudioMode` | `NORMAL` | Swaps in a debug generator. `BIOME_BUFFET_1x1`, `_3x3`, `_5x5`, `_9x9`, `_18x18`, `_36x36` lay every biome out in a grid of that cell size; `OBJECT_BUFFET` lays out objects. `REGION_BUFFET` currently installs no generator and behaves exactly like `NORMAL`. Remove before packaging |
|
||||
| `debugChunkCrossSections` | boolean | `false` | Deletes whole chunks on a grid so you can walk up and read the terrain column like a diagram |
|
||||
| `debugCrossSectionsMod` | int | `3` | The X/Z modulus that decides which chunks get cut, 2 to 16. Larger values cut fewer chunks |
|
||||
| `explodeBiomePalettes` | boolean | `false` | Inserts air gaps between palette layers so you can count and identify them visually |
|
||||
| `explodeBiomePaletteSize` | int | `3` | Size of those gaps, 1 to 16 |
|
||||
| `debugSmartBore` | boolean | `false` | Fills the air volume objects carve for themselves with cobweb, making object footprints visible |
|
||||
| `disableExplorerMaps` | boolean | `false` | Declared but never read by the engine today |
|
||||
| `forceConvertTo320Height` | boolean | `false` | Declared but never read by the engine today |
|
||||
|
||||
## Annotations are editor hints, not runtime validation
|
||||
|
||||
`@Required`, `@MinNumber`, and `@MaxNumber` are consumed only by the schema generator. Nothing enforces them at load time, so a value outside the documented range loads without complaint and produces whatever the engine does with it. The exceptions are the dimension-type constraints listed under vertical layout, which throw during dimension-type compilation. Treat the ranges in the tables above as design guidance backed by editor warnings, and verify unusual values in Studio.
|
||||
|
||||
## A complete minimal dimension
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mypack",
|
||||
"version": 1,
|
||||
"mode": { "type": "OVERWORLD" },
|
||||
"regions": ["starter"],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": { "min": -64, "max": 320 },
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 63
|
||||
}
|
||||
```
|
||||
|
||||
This is the studio starter with `mode`, `environment` and `fluidHeight` written out. It needs `regions/starter.json`, a biome, and a generator to actually produce terrain; the four-file walkthrough is in `26 - Example - Minimal Dimension.md`.
|
||||
|
||||
## What the shipping overworld sets
|
||||
|
||||
Path: `packs/overworld/dimensions/overworld.json` under the platform data directory (the same file ships in the Fabric, Forge and NeoForge run configs).
|
||||
|
||||
| Field | Overworld value | Why it is interesting |
|
||||
|-------|-----------------|-----------------------|
|
||||
| `name` / `version` | `"Overworld"` / `4000` | Large version stamp so pack generations are distinguishable |
|
||||
| `environment` | `"NORMAL"` | Overworld base template |
|
||||
| `dimensionHeight` | `{ "min": -256, "max": 512 }` | 768 blocks tall, both bounds multiples of 16 |
|
||||
| `logicalHeight` | `512` | Below the 768 total, so it is a legal contract |
|
||||
| `fluidHeight` | `50` | Sea level at world Y 50, well below the vanilla 63 — biome generator values are measured from here |
|
||||
| `landChance` | `0.69` | Land-heavy world |
|
||||
| `regionZoom` | `16.15` | Continent-sized climate regions |
|
||||
| `coordFractureZoom` | `0.15` | Aggressive coordinate warping — this is where the swirl comes from |
|
||||
| `dimensionAngleDeg` | `69` | Off-axis rotation to break up straight borders |
|
||||
| `regions` | `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics` | Twelve climate regions |
|
||||
| `mode` | omitted | Runs `OVERWORLD` by default |
|
||||
| `preventLeafDecay` | `true` | Custom trees keep their canopies |
|
||||
| `caveProfile` | `enabled: true` | 3D caves on by default, overridden per region |
|
||||
| `carving` | one deep-dark band, world Y -250 to -175 | Depth-banded cave biome |
|
||||
| `ores` / `deposits` / `depositVariants` | 11 / 20 / 1 | Bedrock and band generators, classic ore blobs, one deepslate remap below Y 0 |
|
||||
| `importedStructures` | adjustments for stronghold, trial chambers, mineshaft, village | Native structures re-fitted to Iris terrain |
|
||||
| `structures` | ancient city with `nativeSuppression: REPLACE_SOURCE` | Vanilla placement replaced by an Iris-anchored one |
|
||||
|
||||
## Build a dimension, step by step
|
||||
|
||||
1. Create the pack: `/iris studio create name=mypack` on Bukkit writes a four-file starter skeleton; `/iris studio create mypack` on a mod loader copies the `example` template instead. Either way you get a loadable pack to edit.
|
||||
2. Open `dimensions/mypack.json` and set the contract fields deliberately: `dimensionHeight`, `logicalHeight`, `fluidHeight`, `environment`, and `mode.type`. Check the multiple-of-16 rules now, not after the world exists.
|
||||
3. Make sure every key in `regions` has a file under `regions/`, and that region has at least one land biome with at least one generator (`12 - Regions.md`, `13 - Biomes.md`).
|
||||
4. Validate: `/iris pack validate pack=mypack` on Bukkit, `/iris pack validate mypack` on modded. Fix blocking errors before opening Studio — Studio refuses to open a pack whose validation is not loadable.
|
||||
5. Open Studio on a fixed seed: `/iris studio open mypack seed=1337` on Bukkit, `/iris studio open mypack 1337` on modded.
|
||||
6. Walk into fresh chunks and check the baseline: solid terrain, the build floor where you put it, fluid at the right Y, the expected biome from `/iris what biome`, no unresolved-key errors in console. Fix this before touching noise.
|
||||
7. Isolate while tuning: set `"focusRegion": "starter"` or `"focus": "starter"`, save, and generate a new area. Remove both before packaging.
|
||||
8. Tune land/sea and zoom, then add subsystems one at a time — caves, then ores and deposits, then objects, then structures — validating after each new resource edge so a broken key is attributable.
|
||||
9. Close and reopen Studio after editing `dimensionHeight`, `logicalHeight`, `environment`, or the dimension file name. Hotload rejects those by design.
|
||||
10. Create the production world only after Studio is clean: `/iris create mypack-test type=mypack seed=1337`. Recreate the world rather than editing its height contract later.
|
||||
|
||||
The baseline passes when Studio opens clean, validation reports no blocking errors, and the same seed reproduces the same terrain after a close and reopen.
|
||||
|
||||
## Common author mistakes
|
||||
|
||||
| Mistake | What actually happens |
|
||||
|---------|-----------------------|
|
||||
| Empty or unresolvable `regions` | No biome can be selected, so the dimension has nothing to place |
|
||||
| Region file exists but is not listed in `regions` | It never generates; nothing warns you |
|
||||
| Treating `fluidHeight` as an offset from the build floor | It is world Y. The engine converts it to internal Y by subtracting `dimensionHeight.min` |
|
||||
| `dimensionHeight` span or `min` not a multiple of 16 | Passes `pack validate`, then fails when Iris compiles the dimension type |
|
||||
| `logicalHeight` greater than `max - min` | Rejected when the dimension type is constructed |
|
||||
| Editing height, logical height, environment, or the dimension file name mid-Studio | Hotload is refused by the runtime contract; close and reopen |
|
||||
| Expecting decoration or caves from `SUPERFLAT`, `ENCLOSURE`, or `ISLANDS` | Those modes register only terrain and biome stages |
|
||||
| Leaving `focus` or `focusRegion` set when packaging | The shipped pack generates exactly one biome or region |
|
||||
| Changing pack files and expecting an existing world to change | Production worlds run from `<world>/iris/pack/`; see `27 - Example - Configuring Overworld.md` |
|
||||
@@ -1,250 +0,0 @@
|
||||
# 12 - Regions
|
||||
|
||||
A region is the middle layer of a pack: the dimension picks a region for every column, and the region decides which biomes are allowed there. Files live at `regions/<loadKey>.json`. A region carries four biome lists (land, sea, shore, cave), a zoom for each list, the shoreline band width, and any content you want scoped to that part of the world.
|
||||
|
||||
Related: see `05 - Concepts & Pack Layout.md`, `11 - Dimensions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `15 - Caves & Carving.md`, `16 - Surfaces, Decorators & Deposits.md`, `20 - Object Placement.md`.
|
||||
|
||||
## The mental model
|
||||
|
||||
Every column in the world runs through the same chain. Regions sit in the middle of it:
|
||||
|
||||
```
|
||||
column (x, z)
|
||||
|
|
||||
regionStyle noise, zoomed by regionZoom -> one number in 0..1
|
||||
|
|
||||
weighted pick over the dimension's regions -> REGION
|
||||
|
|
||||
continentalStyle noise vs landChance -> LAND or SEA
|
||||
|
|
||||
<role>BiomeStyle noise, zoomed by biomeZoom * roleZoom * region's <role>BiomeZoom
|
||||
|
|
||||
weighted pick over that region's biome list for the role -> BIOME
|
||||
|
|
||||
height is computed, then the role is corrected against the water line
|
||||
(see "The shore band" below), and children are resolved
|
||||
```
|
||||
|
||||
Two things follow from this that trip up most new pack authors:
|
||||
|
||||
- The region does not decide *where* the sea is. The dimension's `continentalStyle` and `landChance` do that. A region only supplies the candidate biomes once the role is known. If a region has no `seaBiomes`, columns that fall below the water line have nothing to pick and the world will look broken there — see "Land-only dimensions" below.
|
||||
- A region's zooms multiply the dimension's zooms; they do not replace them. `landBiomeZoom: 3.5` in a region means land biomes there are 3.5x the size they would be at the dimension's base scale.
|
||||
|
||||
### How the weighted pick works
|
||||
|
||||
Region selection and biome selection use the same routine. Each candidate gets a weight of `1 / rarity`, the weights are laid out as contiguous bands across the 0..1 noise value in list order, and the noise value at the column picks the band.
|
||||
|
||||
That has three practical consequences:
|
||||
|
||||
- Rarity is a divisor, not a percentage. `rarity: 2` gets half the space of `rarity: 1`; `rarity: 10` gets a tenth. Region rarity is capped at 128, biome rarity at 512.
|
||||
- List order matters for adjacency. Entries next to each other in the array occupy neighbouring noise bands, so they tend to end up as neighbours in the world. Reordering a list changes which biomes border which.
|
||||
- Rarity alone cannot make a region appear where the noise never reaches its band. If a region never shows up, check `regionStyle` and `regionZoom` on the dimension before touching rarity. `/iris studio regions` samples an area and reports the measured share per region.
|
||||
|
||||
### The shore band
|
||||
|
||||
Shores are not chosen by noise. After the height for a column is known, Iris compares it to the dimension's `fluidHeight` and to the region's shore height at that column, then swaps the biome role if it disagrees:
|
||||
|
||||
| Column height (relative to `fluidHeight`) | Resulting role |
|
||||
|---|---|
|
||||
| below `fluidHeight` | sea |
|
||||
| exactly `fluidHeight` | shore |
|
||||
| `fluidHeight - 1` up to `fluidHeight + shoreHeight` | shore |
|
||||
| above `fluidHeight + shoreHeight` | land |
|
||||
|
||||
`shoreHeight` is per column: noise fitted between `shoreHeightMin` and `shoreHeightMax`, sampled at `x / shoreHeightZoom, z / shoreHeightZoom`. So the beach is the vertical slice of the world from one block below the water line up to a few blocks above it, and the width of the beach on the ground is however far that slice stretches across your terrain slope. Flat coastline plus a large `shoreHeightMax` gives wide beaches; a cliff gives none regardless of the setting.
|
||||
|
||||
The shore-height noise is seeded from the region name length, `landBiomeZoom` and the number of land biomes — not from the world seed. Renaming a region or adding a land biome changes the shoreline wobble pattern.
|
||||
|
||||
## Walkthrough: add a region and prove it generates
|
||||
|
||||
Prerequisites: a dimension that validates, and one biome that already validates and is listed as a root somewhere. Keep the new region content-free until selection works — that separates selection problems from placement problems.
|
||||
|
||||
1. Create `regions/tutorial.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial",
|
||||
"rarity": 1,
|
||||
"color": "#9BEE61",
|
||||
"landBiomes": ["starter"],
|
||||
"seaBiomes": [],
|
||||
"shoreBiomes": []
|
||||
}
|
||||
```
|
||||
|
||||
Replace `starter` with the exact load key of your existing biome. Root parents only — never list a biome that is already someone's child.
|
||||
|
||||
2. Add `"tutorial"` to the dimension's `regions` array.
|
||||
|
||||
3. Set `"focusRegion": "tutorial"` on the dimension. This pins every column to that region so nothing else can be blamed.
|
||||
|
||||
4. Validate the pack, then open Studio on seed `1337` (`/iris studio open <pack> 1337`; see `10 - Studio & VSCode Schemas.md` for the modded equivalent).
|
||||
|
||||
5. Fly into freshly generated chunks and run `/iris what region`.
|
||||
|
||||
Success: it reports `tutorial` everywhere, `/iris what biome` reports `starter`, and validation logs no unresolved keys.
|
||||
|
||||
If a different region appears, `focusRegion` does not match the file name. If the region resolves but terrain is missing, the problem is the biome or its generator, not the region — region rarity and zoom cannot repair a broken resource reference.
|
||||
|
||||
6. Remove `focusRegion`, reopen Studio, and travel until `tutorial` shows up on its own. Now add sea and shore biomes together, then cave biomes, validating after each group.
|
||||
|
||||
## Walkthrough: make biomes bigger in one region only
|
||||
|
||||
Suppose the tutorial region's biomes are too small and choppy, but the rest of the dimension is fine.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial",
|
||||
"rarity": 1,
|
||||
"landBiomes": ["starter", "starter-hills"],
|
||||
"landBiomeZoom": 3.5
|
||||
}
|
||||
```
|
||||
|
||||
Observable result: individual land biome patches in this region become roughly 3.5x wider. Nothing outside the region changes, and the dimension's `biomeZoom` still applies on top. The shipping `temperate` region uses `3.5` for land, `6` for sea and `0.15` for shore — shores are deliberately zoomed *down* so beach variants change every few dozen blocks along a coastline instead of running for hundreds of blocks.
|
||||
|
||||
Change one zoom at a time and regenerate a fresh area between comparisons. Zooms do not affect already-generated chunks.
|
||||
|
||||
## Walkthrough: widen the beaches
|
||||
|
||||
```json
|
||||
{
|
||||
"shoreHeightMin": 1,
|
||||
"shoreHeightMax": 5.2,
|
||||
"shoreHeightZoom": 1.14
|
||||
}
|
||||
```
|
||||
|
||||
Observable result: the shore role now claims everything from one block under the water line up to about 5 blocks above it, so gently sloped coasts get much wider sand. Lowering `shoreHeightZoom` makes the beach width vary more rapidly along the coast; raising it produces long, uniform stretches.
|
||||
|
||||
If your beaches stay one block wide no matter what, the coastline is too steep. That is a generator problem, not a shore problem — see `14 - Generators & Noise.md`.
|
||||
|
||||
## Land-only dimensions
|
||||
|
||||
`seaBiomes` and `shoreBiomes` are optional in the schema. Leaving them empty is only safe when no column can ever land at or below `fluidHeight` — that means every biome in the region uses positive generator `min` values, and the dimension's `landChance` keeps the continental noise on land. Otherwise the sea/shore role has an empty candidate list at those columns.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Highlands",
|
||||
"rarity": 2,
|
||||
"landBiomes": ["highlands/plateau"],
|
||||
"seaBiomes": [],
|
||||
"shoreBiomes": []
|
||||
}
|
||||
```
|
||||
|
||||
`landBiomes` is always required.
|
||||
|
||||
## Load key
|
||||
|
||||
| Rule | Detail |
|
||||
|------|--------|
|
||||
| Folder | `regions/` |
|
||||
| Key | Path relative to `regions/` with `.json` stripped |
|
||||
| Example | `regions/temperate.json` -> key `temperate` |
|
||||
| Referenced from | The dimension's `regions` array, using that key |
|
||||
|
||||
## Field reference (`IrisRegion`)
|
||||
|
||||
### Identity and selection
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `name` | string | `"A Region"` | Display name shown by `/iris what region` and the studio map. It is also mixed into the shore-height noise seed, so renaming a region shifts its coastline wobble. Required, minimum 2 characters. |
|
||||
| `rarity` | int 1-128 | `1` | Divides this region's share of the selection noise: `2` gives half the area of a `1`, `8` gives an eighth. Raise it for a region you want as an occasional surprise, leave it at `1` for the backbone of the world. |
|
||||
| `color` | string | `null` | Hex colour (`#9BEE61`) used by the studio map and `/iris studio map`. Set it when you are visually debugging region distribution; without it Iris derives a colour from the land biomes' vanilla derivatives. |
|
||||
|
||||
### Biome lists
|
||||
|
||||
| Field | Type | Required | What it does |
|
||||
|-------|------|----------|--------------|
|
||||
| `landBiomes` | string[] | Yes | Candidates for columns whose height ends up above the shore band. This is the list that defines the character of the region. |
|
||||
| `seaBiomes` | string[] | No | Candidates for columns below the water line. Empty is only valid if no column in this region can ever fall below `fluidHeight`. |
|
||||
| `shoreBiomes` | string[] | No | Candidates for the vertical band around the water line. Usually beaches and stony shores. |
|
||||
| `caveBiomes` | string[] | No | Candidates for the biome used underground for layers, decorators and cave-anchored structures. Empty means carved space keeps the surface biome's data. |
|
||||
|
||||
List root parents only. Child biomes are declared on their parent via `children` (see `13 - Biomes.md`); listing a child here makes it compete as a root as well and breaks the nesting you intended. Keys are load keys relative to `biomes/`, so `temperate/plains` means `biomes/temperate/plains.json`.
|
||||
|
||||
A biome does not declare its own role. The role (`LAND`, `SEA`, `SHORE`, `CAVE`) comes from which list selected it, and the same biome file can appear in more than one list, taking a different role in each.
|
||||
|
||||
### Zooms and the shore band
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `landBiomeZoom` | double >= 0.0001 | `1` | Multiplies the land biome patch size in this region only. Raise it for continent-scale biomes, lower it for a busy patchwork. |
|
||||
| `seaBiomeZoom` | double >= 0.0001 | `1` | Same for ocean-floor biomes. Oceans usually want a larger value than land so a single ocean type does not change every 200 blocks. |
|
||||
| `shoreBiomeZoom` | double >= 0.0001 | `1` | Same for beach variants. Values well below 1 give a coastline that alternates between beach types frequently, which reads as natural variety. |
|
||||
| `caveBiomeZoom` | double >= 0.0001 | `1` | Same for cave biomes. Larger values give long stretches of one cave character. |
|
||||
| `shoreHeightMin` | double >= 0 | `1.2` | Lower bound, in blocks above the water line, of the shore band. |
|
||||
| `shoreHeightMax` | double >= 0 | `3.2` | Upper bound of the shore band. Raise both to get taller, and therefore usually wider, beaches. |
|
||||
| `shoreHeightZoom` | double >= 0.0001 | `3.14` | Horizontal scale of the noise that picks the band height per column. Small values make the beach width vary rapidly along the coast; large values make it uniform. |
|
||||
|
||||
### Content attached to the region
|
||||
|
||||
Everything here applies anywhere this region is selected, on top of what the biome contributes.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `objects` | `IrisObjectPlacement[]` | empty | `.iob` placements that should exist across the whole region rather than in one biome — regional landmarks, scattered wrecks. Split at runtime into surface and carving sets by each placement's `carvingSupport`. See `20 - Object Placement.md`. |
|
||||
| `proceduralObjects` | `IrisProceduralObjects` | empty | Trees, ruins, formations, coral, fungi and crystals generated from parameters rather than `.iob` files. See `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`. |
|
||||
| `structures` | `IrisStructurePlacement[]` | empty | Jigsaw and native structure placements evaluated where this region owns the chunk centre. Use this instead of copying a placement onto every biome in the region. See `21 - Jigsaw Structures.md`. |
|
||||
| `entitySpawners` | string[] | empty | `IrisSpawner` keys that keep replenishing mobs while a player is in this region. See `23 - Loot, Entities, Spawners, Markers.md`. |
|
||||
| `effects` | `IrisEffect[]` | empty | Client-side ambience (potion effects, sounds, particles) delivered per player by packet. Use for regional mood; no two players see each other's effects. |
|
||||
| `loot` | `IrisLootReference` | empty | Loot tables that apply to containers generated in this region. |
|
||||
| `blockDrops` | `IrisBlockDrops[]` | empty | Overrides what blocks drop when broken inside this region. |
|
||||
| `deposits` | `IrisDepositGenerator[]` | empty | Blob-style deposits added on top of the dimension's deposits. Use for regional stone variants and ore pockets. |
|
||||
| `depositVariants` | `IrisDepositVariant[]` | empty | Remaps deposit blocks inside a Y band. Evaluated after the biome's variants and before the dimension's; first matching rule in this tier wins. |
|
||||
| `ores` | `IrisOreGenerator[]` | empty | Vein-style ores. Each generator declares whether it is a surface or underground generator and Iris keeps two separate lists. |
|
||||
| `caveProfile` | `IrisCaveProfile` | default profile | Cave density, thresholds and surface behaviour for this region. Biome profiles override this. See `15 - Caves & Carving.md`. |
|
||||
|
||||
Deposit precedence across tiers: biome variants, then region variants, then dimension variants. First match wins within each tier.
|
||||
|
||||
### Fields the generator does not read
|
||||
|
||||
| Field | Status |
|
||||
|-------|--------|
|
||||
| `riverStyle` | Accepted by the schema and the editor, never read at generation time. Rivers come from biomes (for example `temperate/sea/river`) with negative generator heights, not from this field. |
|
||||
| `lakeStyle` | Same. Setting it has no effect on generation. |
|
||||
|
||||
They are still valid JSON and will not fail validation, so do not spend time tuning them.
|
||||
|
||||
## Overworld sample: `temperate`
|
||||
|
||||
Path in the shipping pack: `packs/overworld/regions/temperate.json`.
|
||||
|
||||
| Field | Value | Why |
|
||||
|-------|-------|-----|
|
||||
| `name` / `color` | `Temperate` / `#9BEE61` | |
|
||||
| `rarity` | `1` | Most common region; the world's default character. |
|
||||
| `landBiomes` | 28 keys, e.g. `temperate/plains`, `temperate/cherry-blossom-forest`, `vanilla/cherry_grove` | A large list keeps a single region visually varied without needing many regions. |
|
||||
| `seaBiomes` | 8 keys, e.g. `ocean/deep`, `temperate/sea/ocean`, `temperate/sea/river` | Rivers are sea biomes here, not a separate system. |
|
||||
| `shoreBiomes` | 5 keys, e.g. `temperate/shore/beach`, `vanilla/stony_shore` | |
|
||||
| `caveBiomes` | 5 keys, e.g. `carving/drip`, `carving/moss-pillars` | |
|
||||
| `landBiomeZoom` / `seaBiomeZoom` | `3.5` / `6` | Oceans stay recognisable across long swims. |
|
||||
| `shoreBiomeZoom` / `caveBiomeZoom` | `0.15` / `3.3` | Beaches change type often; caves keep one character for a while. |
|
||||
| `shoreHeightMin` / `Max` / `Zoom` | `1` / `5.2` / `1.14` | Noticeably wide beaches with rapidly varying width. |
|
||||
| `deposits`, `loot`, `caveProfile` | present | Iron/coal bands, `FALLBACK` loot mode, an enabled cave profile. |
|
||||
|
||||
The file sets no `objects`, `structures`, `ores`, `entitySpawners` or `effects` — all of that lives on the biomes.
|
||||
|
||||
Region keys listed by the shipping overworld dimension: `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics`.
|
||||
|
||||
## Resolution details worth knowing
|
||||
|
||||
- `getAllBiomeIds()` is the union of the four lists. It is what the dimension uses to decide which biomes exist for a region.
|
||||
- Expanding a region to its full biome set walks each listed biome's `children` and its `carvingBiome`, then repeats until no new names appear. Cycles are safe; the walk stops when the pending name set empties.
|
||||
- Objects declared on the region are pre-split into a surface list and a carving list by each placement's `carvingSupport`, so a placement that only supports carving never gets evaluated on the surface.
|
||||
- Structure placements are gathered per chunk from the biome, the cave biome and the region at the chunk centre (block `chunkX*16+8`, `chunkZ*16+8`), plus the dimension. Cave biomes contribute only placements whose resolved anchor is a cave anchor.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | What you will see |
|
||||
|---------|-------------------|
|
||||
| Listing a child biome as a region root | The child generates as a full-size root as well as inside its parent, so the intended nesting disappears |
|
||||
| Region file not added to the dimension's `regions` array | The region never generates; validation may still pass |
|
||||
| Empty `landBiomes` | Invalid region; land columns have no candidates |
|
||||
| Wrong biome key path | `temperate/plains` must be `biomes/temperate/plains.json`, case and folder included |
|
||||
| Tuning region `rarity` when the region never appears | Selection also depends on the dimension's `regionStyle` and `regionZoom`; measure first with `/iris studio regions` |
|
||||
| Expecting `riverStyle` / `lakeStyle` to do something | They are read by nothing; make rivers and lakes as sea biomes with negative generator heights |
|
||||
| Empty `seaBiomes` in a dimension whose terrain dips below `fluidHeight` | Sea columns have no candidate biome |
|
||||
| Comparing changes in already-generated chunks | Region and zoom changes only affect newly generated chunks; always fly to fresh terrain |
|
||||
@@ -1,526 +0,0 @@
|
||||
# 13 - Biomes
|
||||
|
||||
A biome is where terrain height, surface materials, decoration and placement all come together. Files live at `biomes/<loadKey>.json`. Regions list root biomes; roots can nest children, swap themselves out under carvings, and publish custom datapack biomes for colours, tags and mob spawns.
|
||||
|
||||
Related: see `12 - Regions.md`, `14 - Generators & Noise.md`, `15 - Caves & Carving.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`.
|
||||
|
||||
## The mental model
|
||||
|
||||
A biome file answers two separate questions, and they fail in different ways.
|
||||
|
||||
**Where does this biome appear?** Not from anything in the file. The region lists it, the role (land, sea, shore, cave) comes from which list it was in, and a noise value picks between the siblings in that list weighted by `1 / rarity`. See `12 - Regions.md`.
|
||||
|
||||
**What does the world look like where it appears?** That is the whole rest of the file, and it runs top to bottom per column:
|
||||
|
||||
```
|
||||
column (x, z)
|
||||
|
|
||||
generators[] -> terrain height Y (each link maps 0..1 noise into min..max, relative to fluidHeight)
|
||||
|
|
||||
layers[] -> block stack downward from Y
|
||||
|
|
||||
remaining depth below the layers -> dimension rock palette
|
||||
|
|
||||
if Y < fluidHeight: seaLayers[] fill downward from the water surface, remainder is fluid
|
||||
|
|
||||
decorators, objects, structures, procedural content go on top
|
||||
|
|
||||
derivative / customDerivitives -> what Minecraft calls this biome (colours, mobs, structure eligibility)
|
||||
```
|
||||
|
||||
Three of those steps regularly surprise people:
|
||||
|
||||
- **Height is relative to `fluidHeight`, not to Y=0.** `min: 4, max: 10` means "4 to 10 blocks above the water line". Negative values put the surface under water, which is how ocean floors and river beds are made.
|
||||
- **A biome has no `type` field.** `carving/drip` is a cave biome only because a region put it in `caveBiomes`. The same file placed in `landBiomes` would generate as land.
|
||||
- **The role can be corrected after height is known.** If a land biome's height lands below the water line, Iris swaps in a sea biome from the same region; if it lands in the shore band, a shore biome. So a "land" biome with a negative generator will simply never render as itself.
|
||||
|
||||
### Children
|
||||
|
||||
`children` lets one biome dissolve into variants without adding entries to the region. At each column, Iris runs a second noise pass over the parent's children **plus the parent itself**, then repeats on whatever it picked, up to four times in total. The chain usually ends early because re-picking the parent stops it.
|
||||
|
||||
Child weighting is not the same as list rarity. Each candidate gets `(highestRarityInTheGroup + 1) - rarity` slots:
|
||||
|
||||
| Parent rarity | Child rarity | Parent slots | Child slots | Result |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 1 | 1 | 1 | Half and half |
|
||||
| 1 | 4 | 4 | 1 | Child covers a fifth of the parent |
|
||||
| 1 | 3 and 3 (two children) | 3 | 1 each | Parent 60%, each child 20% |
|
||||
|
||||
Because the weights are relative to the highest rarity present, setting every candidate to the same number (all `1`, all `9`) gives a uniform split. Only differences matter.
|
||||
|
||||
`childShrinkFactor` scales the child selection noise coordinates, so higher values make each child patch smaller inside the parent. `childStyle` decides the patch shape.
|
||||
|
||||
## Walkthrough: add a biome to a region and see it
|
||||
|
||||
Prerequisites: a validating dimension, a region it lists, and `generators/flat.json` (see `26 - Example - Minimal Dimension.md`).
|
||||
|
||||
1. Save this as `biomes/tutorial/meadow.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial Meadow",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"generators": [{ "generator": "flat", "min": 96, "max": 96 }],
|
||||
"layers": [
|
||||
{ "palette": [{ "block": "minecraft:grass_block" }] },
|
||||
{ "minHeight": 2, "maxHeight": 4, "palette": [{ "block": "minecraft:dirt" }] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`min` equal to `max` gives a dead-flat surface at 96 blocks above `fluidHeight`, which makes any height problem obvious later.
|
||||
|
||||
2. Add `"tutorial/meadow"` to that region's `landBiomes`.
|
||||
|
||||
3. Set `"focus": "tutorial/meadow"` on the dimension, validate, and open Studio on seed `1337`.
|
||||
|
||||
4. Fly into new chunks and run `/iris what biome`.
|
||||
|
||||
Success: the load key is `tutorial/meadow`, the surface is grass over 2-4 dirt over stone, the terrain is perfectly flat, and there are no unresolved generator warnings.
|
||||
|
||||
If nothing generates, compare the region entry, the file path and the `focus` string character for character. If the biome resolves but sits on void, the generator link is wrong — check `generators/flat.json` exists and the key matches.
|
||||
|
||||
5. Remove `focus`, reopen Studio, and travel until the biome turns up naturally. Only then add decorators, objects and children.
|
||||
|
||||
While `focus` is set, the focused biome is forced into the land role for the whole world, so sea and shore correction never runs. A sea biome under `focus` will render as if it were land.
|
||||
|
||||
## Walkthrough: make it hilly, then flatten part of it
|
||||
|
||||
The generator supplies the shape; the biome supplies the height band. To get hills, point at a generator with real relief and open the band:
|
||||
|
||||
```json
|
||||
{ "generators": [{ "generator": "plain", "min": 4, "max": 40 }] }
|
||||
```
|
||||
|
||||
Observable result: terrain now rolls between 4 and 40 blocks above the water line, with the shape coming from `generators/plain.json`.
|
||||
|
||||
To make one biome a plateau while its neighbours stay hilly, set `min` equal to `max` on that biome only:
|
||||
|
||||
```json
|
||||
{ "generators": [{ "generator": "plain", "min": 22, "max": 22 }] }
|
||||
```
|
||||
|
||||
Observable result: a flat table at 22, blending into its neighbours across the generator's interpolation range. The transition width is the generator's `interpolator.horizontalScale`, not anything on the biome.
|
||||
|
||||
To stack a rare feature on top of a base shape, use two links with different generators:
|
||||
|
||||
```json
|
||||
{
|
||||
"generators": [
|
||||
{ "generator": "smooth-dunes", "max": 12, "min": 5 },
|
||||
{ "generator": "rare-hills", "max": 40, "min": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Observable result: rolling dunes 5-12 above water, with occasional hills adding up to another 40. The bands add, so the biome's full range is 5 to 52. How the two shapes combine depends on their generators' interpolators — see `14 - Generators & Noise.md`.
|
||||
|
||||
## Walkthrough: turn it into an ocean floor
|
||||
|
||||
Same biome, negative band, added to `seaBiomes` instead of `landBiomes`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Temperate Ocean",
|
||||
"derivative": "minecraft:lukewarm_ocean",
|
||||
"vanillaDerivative": "minecraft:ocean",
|
||||
"generators": [{ "min": -32, "max": -10, "generator": "mountain" }],
|
||||
"layers": [{ "minHeight": 3, "maxHeight": 5, "palette": [{ "block": "minecraft:sand" }] }]
|
||||
}
|
||||
```
|
||||
|
||||
Observable result: the surface sits 10 to 32 blocks below the water line and the column above it fills with the dimension's fluid palette. `derivative` gives the water its warm colour; `vanillaDerivative` being an exact ocean key is what keeps ocean monuments and shipwrecks eligible here (see "Structure eligibility" below).
|
||||
|
||||
## Walkthrough: add a child variant
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Oak Forest",
|
||||
"derivative": "minecraft:forest",
|
||||
"vanillaDerivative": "minecraft:forest",
|
||||
"children": ["temperate/oak-forest-extended"],
|
||||
"childShrinkFactor": 1.5,
|
||||
"childStyle": { "style": "CELLULAR_IRIS_DOUBLE" }
|
||||
}
|
||||
```
|
||||
|
||||
Create `biomes/temperate/oak-forest-extended.json` as an ordinary biome file and do **not** add it to any region list. Observable result: patches of the child appear inside the parent's footprint, sized by `childShrinkFactor` and shaped by `childStyle`, at roughly half the parent's area when both rarities are `1`.
|
||||
|
||||
Raise the child's `rarity` to shrink its share. Raise `childShrinkFactor` to break it into smaller patches without changing its share.
|
||||
|
||||
## Load key
|
||||
|
||||
| Rule | Detail |
|
||||
|------|--------|
|
||||
| Folder | `biomes/` |
|
||||
| Key | Path relative to `biomes/` with `.json` stripped |
|
||||
| Examples | `starter` -> `biomes/starter.json`; `temperate/plains` -> `biomes/temperate/plains.json` |
|
||||
|
||||
## Field reference (`IrisBiome`)
|
||||
|
||||
### Identity
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `name` | string | `"Subterranean Land"` | Display name in tooling and `/iris what biome`. Required, minimum 2 characters. It is mixed into the biome's own noise seed, so renaming shifts scatter and custom-biome selection patterns. |
|
||||
| `rarity` | int 1-512 | `1` | Divides this biome's share of its region list: `4` gives a quarter the area of a `1`. Also used, on a different scale, when this biome competes with its own children (see "Children"). |
|
||||
| `color` | string | `null` | Hex colour for the studio map. Set it when debugging biome distribution visually. |
|
||||
|
||||
### Minecraft derivatives
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `derivative` | biome key | `"minecraft:the_void"` | The Minecraft biome this one presents as: grass and water tint, ambient sound, mob spawning, temperature effects. Required — leaving it at the default gives a void-tinted world. Bare names are namespaced automatically, so `plains` becomes `minecraft:plains`. |
|
||||
| `vanillaDerivative` | biome key | `null` (falls back to `derivative`) | The biome used when Minecraft asks "may this structure generate here". Set it when you want a decorative `derivative` (`lukewarm_ocean`) but a structure-standard one (`ocean`). |
|
||||
| `biomeScatter` | string[] | empty | Alternative derivatives mixed across the biome for the underground portion of the column. One entry is used as-is; several are picked per position by `biomeStyle` noise. Use it to break up flat colour. |
|
||||
| `biomeSkyScatter` | string[] | empty | Alternative derivatives for the surface and above. When this list is non-empty it takes over the visible biome for the column; when it is empty the column falls back to `biomeScatter`, then to `derivative`. |
|
||||
| `biomeStyle` | `IrisGeneratorStyle` | `SIMPLEX` | The noise that disperses the scatter lists and picks between multiple `customDerivitives`. Change its `zoom` to make the colour patches larger or smaller. |
|
||||
|
||||
Where the split between "underground" and "surface" applies depends on the generation path. On platforms where Iris supplies a 3D biome source to native worldgen, positions below the terrain surface resolve through the cave biome and `biomeScatter`, and positions above resolve through `biomeSkyScatter`. On the path where Iris writes biomes into the chunk itself, one biome is written for the whole column using the sky resolution. Either way, setting only `biomeSkyScatter` changes what players see; setting only `biomeScatter` may not.
|
||||
|
||||
### Structure eligibility
|
||||
|
||||
Native and datapack structures are filtered by the biome Minecraft sees, so Iris enforces the generated role before handing the key over:
|
||||
|
||||
| Situation | Key handed to structure selection |
|
||||
|---|---|
|
||||
| Land or cave role | `vanillaDerivative`, else `derivative` |
|
||||
| Sea role, key contains `ocean` or ends in `river` | unchanged |
|
||||
| Sea role, any other `minecraft:` key | `minecraft:the_void`, so no native structure is eligible |
|
||||
| Shore role, key ends in `beach` or `shore` | unchanged |
|
||||
| Shore role, any other `minecraft:` key | `minecraft:beach` |
|
||||
| Non-`minecraft:` namespace (mod biomes) | unchanged, always authoritative |
|
||||
|
||||
That is why a sea biome with `vanillaDerivative: "minecraft:plains"` gets no ocean structures at all. See `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
### Children and carving
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `children` | string[] | empty | Biome keys that portions of this biome morph into. Cycles are allowed; a column resolves at most four child hops. Do not also list these in a region. |
|
||||
| `childShrinkFactor` | double | `1.5` | Scales the child selection noise. Higher means smaller child patches inside the parent. Useful range is roughly 1 to 3. |
|
||||
| `childStyle` | `IrisGeneratorStyle` | `CELLULAR_IRIS_DOUBLE` | Shape of the child patches. Cellular styles give distinct blobs; simplex gives soft gradients. |
|
||||
| `carvingBiome` | string | `""` | Biome key used instead of this one under a carving. Reachability indexes follow it, so the referenced biome is loaded and registered even if no region lists it. |
|
||||
| `caveMinDepthBelowSurface` | int 0-256 | `0` | When this biome is used as a cave biome, columns less than this many blocks below the terrain surface fall back to the surface biome instead. Raise it to keep a deep-cave palette out of shallow openings. |
|
||||
|
||||
### Height (`generators`)
|
||||
|
||||
Type: `IrisBiomeGeneratorLink`, available as the `generator-layer` snippet.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `generator` | string | `"default"` | Load key under `generators/`. A missing or blank key resolves to `default`; an unresolvable key falls back to an empty generator, which contributes zero height. |
|
||||
| `min` | int -2032..2032 | `0` | Bottom of this link's height band, in blocks relative to `fluidHeight`. Required. |
|
||||
| `max` | int -2032..2032 | `0` | Top of the band. Required. |
|
||||
|
||||
Each link clamps its generator's output to 0..1 and maps it into `min`..`max`. Multiple links add, so a biome's total band is the sum of its links' bands. The final column height is clamped to the dimension's usable range.
|
||||
|
||||
The generator's raw 0..1 shape and the band are combined through the generator's interpolator, which is also what blends this biome's heights into its neighbours'. Generators sharing an interpolator are averaged together; generators with distinct interpolators add as independent layers. That behaviour and its tuning live in `14 - Generators & Noise.md`.
|
||||
|
||||
### Layers (block palettes)
|
||||
|
||||
Type: `IrisBiomePaletteLayer`, available as the `biome-palette` snippet.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `palette` | `IrisBlockData[]` | one grass block | The blocks this layer may use. Required, at least one entry. With several entries the choice is made per block by `style`. |
|
||||
| `minHeight` | int 0-2032 | `1` | Thinnest this layer can be at a column. `0` lets the layer vanish in places. |
|
||||
| `maxHeight` | int 1-2032 | `1` | Thickest it can be. Iris picks a per-column thickness between min and max using noise. |
|
||||
| `style` | `IrisGeneratorStyle` | `STATIC` | How multi-block palettes are distributed. `STATIC` is white noise, which reads as speckle; a coherent style like `IRIS` gives patches. |
|
||||
| `zoom` | double >= 0.0001 | `5` | Horizontal scale for both the thickness noise and the palette noise. Larger makes broader, smoother patches. |
|
||||
| `slopeCondition` | `IrisSlopeClip` | min `0`, max `10` | When narrowed, this layer is skipped entirely at columns whose slope falls outside the range. Use it for snow caps that avoid cliffs, or gravel that only appears on steep ground. The default range is inert. |
|
||||
|
||||
`IrisBlockData` entries:
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `block` | string | `"air"` | Block id. Namespaced or bare. Required. |
|
||||
| `weight` | int 1-1000 | `1` | Duplicates this entry in the palette, so `weight: 3` makes it three times as likely as a `weight: 1` sibling. |
|
||||
| `data` | map | empty | Block state properties, e.g. `{"waterlogged": true}`. |
|
||||
| `tileData` | map | empty | Tile-entity data for blocks that carry it. |
|
||||
| `backup` | `IrisBlockData` | null | Used when `block` does not exist on this Minecraft version. |
|
||||
| `debug` | boolean | `false` | Prints the resolved block to the console when Iris debug is on. Diagnostic only. |
|
||||
|
||||
The stacks:
|
||||
|
||||
| Field | What it fills |
|
||||
|-------|---------------|
|
||||
| `layers` | The column downward from the terrain surface. First entry is the top. Anything below the stack becomes the dimension's rock palette (or an ore, if an ore generator claims that block). Required; the default is a single grass layer. |
|
||||
| `seaLayers` | The water column, indexed **downward from the water surface**, not upward from the sea floor. Index 0 sits at `fluidHeight`. Anything the stack does not cover becomes the dimension fluid. This is how you get a layer of ice or a band of murky water on top of an ocean. |
|
||||
| `caveCeilingLayers` | The underside of carved ceilings, downward from the ceiling. Defaults to one grass layer, which is almost never what you want in a cave — set it explicitly. |
|
||||
| `slab` | Palette for the half-slabs the post processor adds on single-block steps. Default is an empty palette, meaning no slabs. |
|
||||
| `wall` | Palette for the vertical faces the post processor paints when a neighbouring column is more than two blocks lower. Default is empty. Set it to stone/andesite to stop cliffs showing dirt. |
|
||||
| `lockLayers` | When true, the stack repeats as horizontal bands keyed to world height instead of following the surface, giving mesa striping. |
|
||||
| `lockLayersMax` | Depth cap, in blocks, for locked layers. Default `7`. |
|
||||
|
||||
`caveCeilingLayers` reuses the per-layer thickness generators built from `layers`, so it must not have more entries than `layers` does. Give the biome at least as many surface layers as ceiling layers.
|
||||
|
||||
Slabs and walls only appear when the dimension has `postProcessing`, `postProcessingSlabs` and `postProcessingWalls` enabled. See `11 - Dimensions.md`.
|
||||
|
||||
### Custom biomes (`customDerivitives`)
|
||||
|
||||
The JSON key really is `customDerivitives`. The misspelling is baked into the engine field; `customDerivatives` is silently ignored.
|
||||
|
||||
Type: `IrisBiomeCustom`, available as the `custom-biome` snippet. Iris compiles these into a datapack and registers them as `<dimensionLoadKey>:<id>`.
|
||||
|
||||
When a biome has any custom derivative, that custom biome becomes the visible biome for the column and `derivative` / `biomeScatter` / `biomeSkyScatter` stop driving what players see. `vanillaDerivative` still drives structure eligibility and tag inheritance. With several entries, `biomeStyle` picks between them per position.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `id` | string | `""` | Resource path, lowercased on read. Must be unique in the pack. Required. |
|
||||
| `category` | `IrisBiomeCustomCategory` | `plains` | Vanilla category written into the biome JSON. Required. |
|
||||
| `temperature` | double -3..3 | `0.8` | Vanilla temperature: drives snow versus rain, water freezing and some mob behaviour. |
|
||||
| `humidity` | double -3..3 | `0.4` | Written as vanilla `downfall`. Affects foliage tint and fire spread. |
|
||||
| `downfallType` | `IrisBiomeCustomPrecipType` | `rain` | `none`, `rain` or `snow`. `none` also clears the `has_precipitation` flag. |
|
||||
| `spawnRarity` | int 0-20 | `0` | Written straight into `creature_spawn_probability`. Leave at `0` unless you are also supplying `spawns`. |
|
||||
| `spawns` | `IrisBiomeCustomSpawn[]` | empty | Mob spawn entries grouped by category. Only meaningful together with `spawnRarity`. |
|
||||
| `tags` | string[] | empty | Extra biome tags, e.g. `minecraft:allows_surface_slime_spawns`. |
|
||||
| `ambientParticle` | `IrisBiomeCustomParticle` | `null` | Client-rendered ambient particle. No server cost. |
|
||||
| `skyColor` | hex | `#79a8e1` | Upper sky colour. |
|
||||
| `fogColor` | hex | `#c0d8e1` | Horizon fog colour. |
|
||||
| `waterColor` | hex | `#3f76e4` | Water surface tint. |
|
||||
| `waterFogColor` | hex | `#050533` | Underwater fog tint. |
|
||||
| `grassColor` | hex | `""` | Forces a grass tint. Empty means "leave it to the category", which is usually what you want unless you are matching a specific look. |
|
||||
| `foliageColor` | hex | `""` | Same for leaves. Empty means inherit. |
|
||||
|
||||
On Minecraft 26.2, sky, fog, water-fog and ambient-particle values are published through the biome environment-attribute registry while water, grass and foliage stay biome effects. The conversion is automatic; the pack fields are unchanged.
|
||||
|
||||
Effective tags are your `tags` plus the direct tag membership of the vanilla derivative, deduplicated. Structure tags (`has_structure/*`) are deliberately not inherited, because native structure placement already resolves through the structure derivative and inheriting them would place structures twice.
|
||||
|
||||
Custom spawn entry (`IrisBiomeCustomSpawn`):
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `type` | entity key | `minecraft:cow` | Entity to spawn. Bare names are namespaced. |
|
||||
| `minCount` / `maxCount` | int >= 1 | `2` / `5` | Pack size range. |
|
||||
| `weight` | int 1-1000 | `1` | Relative chance against other entries in the same group. |
|
||||
| `group` | `IrisBiomeCustomSpawnType` | `MISC` | Vanilla spawn category, which also decides the mob cap the spawn counts against. |
|
||||
|
||||
Spawn groups: `MONSTER`, `CREATURE`, `AMBIENT`, `AXOLOTLS`, `UNDERGROUND_WATER_CREATURE`, `WATER_CREATURE`, `WATER_AMBIENT`, `MISC`.
|
||||
|
||||
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 | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `particle` | `minecraft:flash` | Particle id, namespaced automatically. |
|
||||
| `rarity` | `35` (1-10000) | Written as probability `1 / rarity`, so higher means fewer particles. |
|
||||
|
||||
Custom biomes are installed by datapack compilation, so a world usually has to be reopened (sometimes the server restarted) before newly added ids resolve. If a custom biome does not appear, check for a leftover `derivative` typo before blaming the datapack.
|
||||
|
||||
### Content attached to the biome
|
||||
|
||||
| Field | Type | What it does |
|
||||
|-------|------|--------------|
|
||||
| `decorators` | `IrisDecorator[]` | Grass, flowers, cactus, kelp and similar surface scatter, bucketed by `partOf` (surface, ceiling, shore line, sea surface, sea floor). See `16 - Surfaces, Decorators & Deposits.md`. |
|
||||
| `objects` | `IrisObjectPlacement[]` | `.iob` placements. Split at runtime into surface and carving sets by each placement's `carvingSupport`. See `20 - Object Placement.md`. |
|
||||
| `proceduralObjects` | `IrisProceduralObjects` | Trees, coral, fungi, crystals, ruins and formations generated from parameters. See `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`. |
|
||||
| `structures` | `IrisStructurePlacement[]` | Jigsaw and native placements evaluated where this biome owns the chunk centre. |
|
||||
| `floatingChildBiomes` | `IrisFloatingChildBiomes[]` | Floating islands above this biome's columns, drawn using another biome's materials. See below. |
|
||||
| `mergeFloatingChildBiomes` | boolean | When true every floating entry samples independently and islands can overlap; when false (default) one entry is chosen per column. |
|
||||
| `deposits` | `IrisDepositGenerator[]` | Blob deposits added on top of regional and dimension deposits. |
|
||||
| `depositVariants` | `IrisDepositVariant[]` | Y-banded ore remaps. This is the first tier evaluated, ahead of region and dimension; first match in the tier wins. |
|
||||
| `oreDepositFrequencyMultiplier` | double 0-1 | Scales how many ore veins have their centre in this biome. `0.4` keeps 40% of them. Non-ore deposits are untouched. Use it to make a biome ore-poor without editing the global generators. |
|
||||
| `oreDepositSizeMultiplier` | double 0.01-16 | Scales the block count of those veins. Use it for a biome with rare-but-huge veins (`frequency` down, `size` up). |
|
||||
| `ores` | `IrisOreGenerator[]` | Vein generators owned by this biome, each flagged surface or underground. |
|
||||
| `entitySpawners` | string[] | `IrisSpawner` keys replenished over time while a player is here. |
|
||||
| `effects` | `IrisEffect[]` | Per-player packet ambience. |
|
||||
| `loot` | `IrisLootReference` | Loot tables for containers generated here. |
|
||||
| `blockDrops` | `IrisBlockDrops[]` | Custom drops for blocks broken here. |
|
||||
| `caveProfile` | `IrisCaveProfile` | Overrides the region's cave profile for this biome. |
|
||||
|
||||
Placements are gathered per chunk from the biome at the chunk centre, the cave biome at the same point, the region and the dimension. A surface biome contributes all of its `structures[]`; the cave biome contributes only placements whose resolved anchor is `CAVE_FLOOR`, `CAVE_CEILING`, `CAVE_CENTER` or `CAVE_ANY`. Surface and height-band placements written into cave-biome files are ignored. A placement's own `caveBiomes` list is an additional allowlist rechecked at each candidate anchor. See `15 - Caves & Carving.md` and `21 - Jigsaw Structures.md`.
|
||||
|
||||
## Floating child biomes (`IrisFloatingChildBiomes`)
|
||||
|
||||
`floatingChildBiomes` builds islands in the air above columns owned by this biome. Each entry names a target biome whose generators, layers, derivative, decorators and objects supply the island's look, while the entry's own fields control size, shape, altitude, rarity and internal water. With `mergeFloatingChildBiomes: false` (the default), `pickerStyle` and `rarity` choose one entry per column; with it true, every entry samples independently and islands may intersect.
|
||||
|
||||
Reachability follows region roots, dimension carving biomes, ordinary children, carving replacements, floating targets and floating `carving` references, recursively and deduplicated, so every biome that generation can reach is registered for spawns, placements, structures and lookups. Custom-biome datapack installation still scans the pack's complete authored biome set, not just the reachable ones.
|
||||
|
||||
### Target, footprint and altitude
|
||||
|
||||
| Field | Default / range | What it does |
|
||||
|-------|-----------------|--------------|
|
||||
| `biome` | `""` | Target biome key. Empty, missing, or the parent's own key means reuse the parent. |
|
||||
| `rarity` | `1` (1-512) | Relative share when several entries compete for a column. Lower is more common. |
|
||||
| `footprintStyle` | `SIMPLEX` | 2D outline noise. `CELLULAR` gives angular shards, `VASCULAR` gives branching strips, `FRACTAL_FBM_SIMPLEX` gives large irregular blankets. Fracture it for swirled silhouettes. |
|
||||
| `footprintThreshold` | `0.5` (0-1) | Minimum footprint sample that counts as island. `0.0` is a continuous sky blanket, `0.8` is sparse scattered islands, `1.0` produces nothing. |
|
||||
| `pickerStyle` | `SIMPLEX` | Chooses which entry owns a column when entries are not merged. Use a large zoom so each entry owns broad coherent regions. |
|
||||
| `altitudeStyle` | `SIMPLEX` | Varies the island base between the two height bounds. Large zoom keeps one island at one altitude. |
|
||||
| `minHeightAboveSurface` / `maxHeightAboveSurface` | `160` / `210` (0-2032) | Despite the names, these are absolute world Y bounds for the island base, independent of the terrain below. |
|
||||
| `minAbsoluteY` | `null` | Optional clamp that pushes the base up so the hanging tail stays above this Y. |
|
||||
| `maxAbsoluteY` | `null` | Optional clamp that pulls the island top down. |
|
||||
|
||||
### Edge, top and underside shape
|
||||
|
||||
| Field | Default / range | What it does |
|
||||
|-------|-----------------|--------------|
|
||||
| `edgeTaperWidth` | `10` (2-32) | Width in blocks of the rounded transition from the outline to full thickness. Small values give a hard rim, large values a broad domed underside. |
|
||||
| `edgeTaperExponent` | `1.0` (0.25-4) | Curve of that transition. Below 1 fills the rim out; above 1 keeps it thin. |
|
||||
| `edgeTaperVariationStyle` | `SIMPLEX` at zoom `0.18` | Varies the taper width coherently without moving the outline. |
|
||||
| `edgeTaperVariationAmplitude` | `0` (0-8) | How much local widening or narrowing that style applies. `0` disables it. The runtime keeps the resulting width inside 2-32 so rims stay connected. |
|
||||
| `topShapeMode` | `BIOME` | `BIOME` runs the target biome's own generators, so a mountains target grows real peaks. `NOISE` uses `topShapeStyle` as a heightmap. `FLAT` is a constant slab. |
|
||||
| `maxTopHeight` | `40` (0-512) | Ceiling on how far the top rises above the base. |
|
||||
| `topShapeStyle` | `SIMPLEX` | Heightmap used when the mode is `NOISE`. |
|
||||
| `topShapeAmp` | `1` (0-1) | Scales that noise-driven profile down. |
|
||||
| `bottomStyle` | `SIMPLEX` | Underside noise. `VASCULAR` gives drippy roots, `FRACTAL_RM_SIMPLEX` crystalline spikes, `PERLIN` smooth bowls. |
|
||||
| `bottomDepthMin` / `bottomDepthMax` | `4` / `20` (0-512) | Tail depth range below the base. |
|
||||
| `bottomExponent` | `1` (0.1-8) | Bias on tail depth. Above 1 makes deep tails rare spikes; below 1 makes most of the underside deep. |
|
||||
| `maxThickness` | `96` (1-512) | Hard cap on total top-to-bottom thickness. |
|
||||
| `wallWarpStyle` | `null` | Optional 3D noise that offsets the footprint sample per Y layer, so walls meander instead of extruding straight. |
|
||||
| `wallWarpAmplitude` | `6` (0-64) | Maximum wall displacement. Ignored without `wallWarpStyle`. |
|
||||
|
||||
### Materials, fluids and carving
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `bottomPaletteMode` | `DEPTH` | `DEPTH` runs normal top-down layers. `MIRROR_TOP` mirrors the shallow palette onto the underside. `CUSTOM` uses `bottomPalette` near the underside and the target palette near the top. |
|
||||
| `bottomPalette` | `[]` | Layers used only by `CUSTOM`. |
|
||||
| `localFluidHeight` | `null` | Fluid surface relative to the island base. Set it to fill dips in the top profile with ponds. `null` means no internal water. |
|
||||
| `fluidBlock` | `minecraft:water` | Block used for those pools. |
|
||||
| `carveStyle` | `null` | Direct 3D pocket noise inside the island. |
|
||||
| `carving` | `""` | Dimension carving-entry id or biome key. Dimension entries resolve first, and their cave profile overrides `carveStyle`. |
|
||||
| `carveThreshold` | `1` (0-1) | With `carveStyle`, noise above this becomes air (`1` means no carving, `0.55` heavy swiss cheese). With `carving`, it biases the referenced cave profile instead. |
|
||||
|
||||
### Decoration and objects
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `inheritDecorators` | `true` | Apply the target biome's decorators to the island top. |
|
||||
| `inheritObjects` | `true` | Allow the target biome's surface objects on the island top. |
|
||||
| `objectShrinkFactor` | `1` (0.01-1) | Uniform scale for inherited, extra and free-floating objects. Drop it to about `0.5` so full-size trees do not dwarf a small island. |
|
||||
| `extraObjects` | `[]` | Extra placements anchored to the island top. |
|
||||
| `floatingObjects` | `[]` | Placements generated independently in mid-air, forced to floating placement mode. |
|
||||
| `topObjectMode` | `INHERIT_ONLY` | `INHERIT_ONLY` ignores `topObjectOverrides`; `MERGE` appends them after the inherited set; `REPLACE` uses only the overrides. |
|
||||
| `topObjectOverrides` | `[]` | Consumed according to `topObjectMode`. |
|
||||
| `bottomObjectMode` | `INHERIT_ONLY` | `INHERIT_ONLY` places nothing on the underside. `MERGE` and `REPLACE` behave identically because there is no inherited bottom set. |
|
||||
| `bottomObjectOverrides` | `[]` | Placements flipped 180 degrees around X and set flush against the lowest solid face. Directional blocks (stairs, doors, slabs) will not survive the flip; use logs, leaves, stone, ice or glass. |
|
||||
| `color` | `null` | Studio visualisation colour. |
|
||||
|
||||
```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,
|
||||
"objectShrinkFactor": 0.6,
|
||||
"inheritDecorators": true,
|
||||
"inheritObjects": true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Overworld samples
|
||||
|
||||
### Land biome — `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" }] },
|
||||
{ "minHeight": 1, "maxHeight": 3, "palette": [{ "block": "minecraft:dirt" }, { "block": "minecraft:coarse_dirt" }] },
|
||||
{ "minHeight": 6, "maxHeight": 18, "style": { "style": "STATIC" },
|
||||
"palette": [{ "block": "minecraft:dirt" }, { "block": "minecraft:stone" }] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A shallow 4-10 band, four layers ending in a thick speckled dirt/stone blend so the transition to bedrock rock is not a hard line, and a stone/andesite `wall` so cliff faces do not show dirt. The real file also carries `decorators` and `objects`.
|
||||
|
||||
### Parent with a child and a custom biome — `biomes/temperate/oak-forest.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Oak Forest",
|
||||
"derivative": "minecraft:forest",
|
||||
"vanillaDerivative": "minecraft:forest",
|
||||
"customDerivitives": [{
|
||||
"id": "oak_forest",
|
||||
"category": "forest",
|
||||
"grassColor": "#77A620",
|
||||
"foliageColor": "#64B233"
|
||||
}],
|
||||
"children": ["temperate/oak-forest-extended"],
|
||||
"generators": [
|
||||
{ "generator": "smooth-dunes", "max": 12, "min": 5 },
|
||||
{ "generator": "rare-hills", "max": 40, "min": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The custom derivative only changes colours; `derivative` and `vanillaDerivative` stay on `minecraft:forest` so forest structures and forest tags still apply.
|
||||
|
||||
### Colour-only custom biome — `biomes/vanilla/sunflower_plains.json` (excerpt)
|
||||
|
||||
```json
|
||||
{
|
||||
"customDerivitives": [{
|
||||
"category": "plains",
|
||||
"id": "sunflower_plains",
|
||||
"grassColor": "#91BD59",
|
||||
"foliageColor": "#77AB2F",
|
||||
"waterColor": "#44AFF5",
|
||||
"downfallType": "none"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Minimal biome JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Starter Plains",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"generators": [{ "generator": "flat", "min": 96, "max": 96 }],
|
||||
"layers": [{ "palette": [{ "block": "minecraft:grass_block" }] }]
|
||||
}
|
||||
```
|
||||
|
||||
Needs `generators/flat.json` to exist. Everything else in the file has a working default.
|
||||
|
||||
## Checklist for a new biome
|
||||
|
||||
1. Create `biomes/<path>/<name>.json`. The path is the load key regions will reference, so pick it before wiring anything.
|
||||
2. Set `name`, `derivative` and `vanillaDerivative`.
|
||||
3. Add one `generators` link and make sure the referenced generator file exists.
|
||||
4. Define `layers` from the top down: surface, subsoil, then a blend into stone.
|
||||
5. Add the key to exactly one region role first: land, sea, shore or cave.
|
||||
6. Set the dimension's `focus` to the key, validate, open Studio, and confirm surface blocks, terrain Y and the relationship to the water line.
|
||||
7. Add `wall` if the biome makes cliffs, then decorators, then objects, one group at a time.
|
||||
8. For variants, create the child file and list it in the parent's `children` — never in a region.
|
||||
9. For colours, tags or mob spawns, add `customDerivitives` with a unique `id` and `category`, then reopen the world so the datapack installs.
|
||||
10. Remove `focus` and confirm the biome still appears through ordinary region selection.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Mistake | What you will see |
|
||||
|---------|-------------------|
|
||||
| `derivative` left at `minecraft:the_void` | Void colours, no mob spawning, no structure eligibility |
|
||||
| Generator key that does not resolve | The link silently contributes zero height; terrain flattens instead of erroring |
|
||||
| Child biome also listed in a region | The child generates as a full-size root, so the nesting disappears |
|
||||
| Spelling `customDerivatives` | Field ignored entirely; the engine key is `customDerivitives` |
|
||||
| Sea biome with positive `min`/`max` | It generates above water, then gets replaced by a land biome anyway |
|
||||
| Sea biome with a non-ocean `vanillaDerivative` | No native ocean structures generate there |
|
||||
| Empty `palette` on the first layer | No surface block; the rock palette shows through |
|
||||
| More `caveCeilingLayers` entries than `layers` entries | Ceiling generation fails at that biome |
|
||||
| Expecting a `type` field on the biome | Role comes from the region list that selected it |
|
||||
| Expecting `slopeCondition` to thin a layer gradually | Out-of-range columns skip the layer entirely; there is no taper |
|
||||
| Judging changes in already-generated chunks | Biome and layer edits only apply to new chunks |
|
||||
@@ -1,355 +0,0 @@
|
||||
# 14 - Generators & Noise
|
||||
|
||||
Generators are the shape of your terrain. A generator file composites one or more noise layers into a single 0..1 value per column, optionally quantises it into cliffs and cracks it into cells, and declares how it blends across biome borders. Biomes reference generators by key and supply the height band the 0..1 value is mapped into. Styles, expressions and PNG image maps are the three things that can supply the raw noise.
|
||||
|
||||
Related: see `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 | Class | Role |
|
||||
|------|-------|------|
|
||||
| `generators/<key>.json` | `IrisGenerator` | Height-map composite that biomes reference |
|
||||
| `expressions/<key>.json` | `IrisExpression` | Math expression usable anywhere a style is accepted |
|
||||
| `images/<key>.png` | `IrisImage` | PNG sampled through `IrisImageMap` |
|
||||
| `snippet/style/<key>.json` | reusable `IrisGeneratorStyle` fragment | Shared style definitions (`snippet/style/bedrock.json` is a plain `STATIC`) |
|
||||
|
||||
Generators are never embedded in biome JSON. A biome links them:
|
||||
|
||||
```json
|
||||
{ "generators": [{ "generator": "plain", "min": 4, "max": 14 }] }
|
||||
```
|
||||
|
||||
## How a noise number becomes a block height
|
||||
|
||||
This is the part that is worth understanding before you touch any field, because the shape you get is not simply "the generator you named".
|
||||
|
||||
### Step 1 — a generator produces 0..1 for a column
|
||||
|
||||
Each `composite` entry is sampled at the column, then combined:
|
||||
|
||||
- **Additive** (default): sum the layers' outputs, divide by the sum of their `opacity` values, multiply by the generator's `opacity`.
|
||||
- **Multiplicative** (`"multiplicitive": true`): start at 1, multiply each layer's output, then multiply by the generator's `opacity`.
|
||||
|
||||
An empty `composite` returns 0 for every column, which is a flat world at the bottom of the biome's band. That is the silent failure mode when a generator file is malformed.
|
||||
|
||||
Then two optional post-passes:
|
||||
|
||||
- **Cliffs** run when `cliffHeightMax > 0`. The value is quantised to steps of a per-column cliff height drawn between `cliffHeightMin` and `cliffHeightMax`, which turns smooth slopes into terraces and mesa walls.
|
||||
- **Cell fracture** runs when `cellFractureHeight` is non-zero. A cell distance field is sampled; outside the cell cores the value is multiplied by `cellFractureHeight`, cutting canyon-like veins between plateaus.
|
||||
|
||||
### Step 2 — generators are grouped by interpolator, and averaged within a group
|
||||
|
||||
Iris collects every generator referenced by every biome the dimension can reach, and buckets them by `interpolator` — the pair of `function` and `horizontalScale`. **Two generators with the same function and the same `horizontalScale` land in the same bucket.**
|
||||
|
||||
For each bucket, at each column:
|
||||
|
||||
1. The interpolator samples the surrounding columns and blends their biomes' height bands for that bucket, giving a smoothed low and high.
|
||||
2. Every generator in the bucket is evaluated at the column and mapped into that smoothed low..high range.
|
||||
3. The results are averaged.
|
||||
|
||||
Bucket results are then added together to give the column height, and `fluidHeight` plus any dimension `overlayNoise` is added on top before the final clamp to the dimension's usable range.
|
||||
|
||||
Two practical consequences:
|
||||
|
||||
- **Generators that share an interpolator blend into one averaged shape.** If `plain` and `rare-hills` both use `BILINEAR_STARCAST_9` with `horizontalScale: 12`, then a biome referencing only `plain` still gets the average of both shapes inside its own band. The shipping overworld deliberately spreads generators across distinct `horizontalScale` values (`12`, `15`, `23`, `26`, `52`, ...) so that most of them stay independent.
|
||||
- **Generators with distinct interpolators stack additively.** That is why a biome can use one link for rolling dunes and another for rare hills and get the sum of both bands.
|
||||
|
||||
If you want a new generator to be its own independent layer, give it an interpolator nobody else uses. If you want it to blend with an existing one, match the existing one exactly.
|
||||
|
||||
### Step 3 — the biome maps it into blocks
|
||||
|
||||
The biome's link clamps the generator output to 0..1 and lerps it into `min`..`max`, in blocks relative to the dimension's `fluidHeight`. Negative bands put the surface under water. See `13 - Biomes.md`.
|
||||
|
||||
So: **generators own the shape and the smoothing radius; biomes own the height range.** Sharing one generator across many biomes and varying `min`/`max` per biome is the normal way to build height bands that still look like one continuous landscape.
|
||||
|
||||
## Walkthrough: add a generator and prove it is wired
|
||||
|
||||
Prerequisites: a validating pack, one biome you can `focus`, and a fixed seed.
|
||||
|
||||
1. Save this as `generators/tutorial-hills.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"interpolator": { "function": "NONE", "horizontalScale": 1 },
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{ "seed": 310, "style": { "style": "FLAT" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. Point the focused biome at it. This is a field in the biome file, not a new file:
|
||||
|
||||
```json
|
||||
{ "generators": [{ "generator": "tutorial-hills", "min": 16, "max": 48 }] }
|
||||
```
|
||||
|
||||
3. Validate, open Studio on seed `1337`, and fly into new chunks.
|
||||
|
||||
Observable result: a dead-flat surface at exactly 48 blocks above `fluidHeight`. `FLAT` returns 1.0 for every coordinate, so the link maps to `max`. Seeing 48 and not 16 or 32 proves the file path, the biome link and the band are all live.
|
||||
|
||||
4. Change only `style.style` from `FLAT` to `IRIS`, then generate a fresh area. Keep the seeds and the band fixed so any change in relief is attributable to the style.
|
||||
|
||||
Observable result: rolling terrain filling the whole 16-48 band.
|
||||
|
||||
5. Tune the generator's `zoom` for feature size. Higher `zoom` divides the sample coordinates, so features get wider and smoother; lower `zoom` packs more detail into the same space. Do not change the band in the same comparison.
|
||||
|
||||
6. Add a second biome using the same generator with a different band, and look at the border. Only after both biomes look right on their own should you tune `interpolator.horizontalScale`.
|
||||
|
||||
7. Add composite layers, `fracture`, expressions or image maps one at a time, and re-check chunk generation time after any nested fracture.
|
||||
|
||||
The tutorial passes when seed `1337` reproduces the same terrain after a Studio restart, borders blend the way you intended, and validation resolves every generator, expression and image key.
|
||||
|
||||
If it is still flat after switching to `IRIS`, the biome is not actually using this generator — check the key. If terrain drops to void, restore the baseline above and read validation output before changing noise values again.
|
||||
|
||||
## Walkthrough: make the mountains taller
|
||||
|
||||
Do not touch the generator. Raise the band on the biome:
|
||||
|
||||
```json
|
||||
{ "generators": [{ "generator": "mountain", "min": 8, "max": 160 }] }
|
||||
```
|
||||
|
||||
Observable result: the same mountain shape, stretched vertically, with the valley floors at 8 and the peaks at 160.
|
||||
|
||||
To make peaks sharper rather than taller, change the shape instead — add `"exponent": 2` to the composite layer, which pushes mid values down and leaves the highs alone, or raise `interpolator.horizontalScale` so the height band blends over a wider radius and gives long approach slopes.
|
||||
|
||||
To make the mountains rarer without shrinking them, split them into a second link with a low-probability shape and a wide band, as `temperate/oak-forest` does with `rare-hills` at `0..40`.
|
||||
|
||||
## Walkthrough: flatten an area
|
||||
|
||||
Two different jobs, two different tools:
|
||||
|
||||
- **Flat biome, natural borders**: set the biome's `min` equal to its `max`. The band collapses to one value, so the generator's shape has nowhere to go. The edges still blend into neighbours across the interpolation radius.
|
||||
- **Flat generator, reused anywhere**: build a generator whose composite is a single `FLAT` style, as `generators/flat.json` does. Any biome linking it gets a constant surface at its `max`.
|
||||
|
||||
Prefer the first when only one biome needs to be flat, and the second when you are building a flat dimension.
|
||||
|
||||
## Generator file (`IrisGenerator`)
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `seed` | long | `1` | Required. Mixed with the engine's height seed, so changing it re-rolls the terrain of every biome using this generator. |
|
||||
| `interpolator` | `IrisInterpolator` | `BILINEAR_STARCAST_6`, scale `7` | Required. Both the border smoothing and the bucket key. See "Step 2" above. |
|
||||
| `composite` | `IrisNoiseGenerator[]` | `[]` | The noise layers. Empty gives 0 everywhere. |
|
||||
| `zoom` | double >= 0.001 | `1` | Divides the sample coordinates before the layers see them, so higher values give larger, smoother features across the whole generator. |
|
||||
| `opacity` | double >= 0 | `1` | Multiplies the combined result. Below 1 compresses the generator into the bottom of the biome's band; above 1 pushes it past the top and clips. |
|
||||
| `multiplicitive` | boolean | `false` | Multiplies the composite layers instead of averaging them. Useful for masking one shape with another (a ridge times a mask leaves ridges only inside the mask). The field spelling is code-authoritative; the JSON must match. |
|
||||
| `offsetX` / `offsetZ` | double | `0` | Shifts where this generator samples the world. Use it to break the alignment between two generators that would otherwise peak in the same places. |
|
||||
| `cliffHeightMin` | double 0..8192 | `0` | Lower bound of the per-column cliff step height. |
|
||||
| `cliffHeightMax` | double 0..8192 | `0` | Upper bound. Cliffs are active whenever this is above 0; `cliffHeightMin` alone does nothing. Larger steps give taller terraces. |
|
||||
| `cliffHeightGenerator` | `IrisNoiseGenerator` | default layer | Picks the step height between min and max per column, so terrace heights can vary across the map. `CELLULAR_HEIGHT` is the usual choice because it gives one height per cell. |
|
||||
| `cellFractureHeight` | double | `0` | `0` disables cell cracks. Non-zero multiplies the height outside cell cores, so `0.2` drops the veins to a fifth of the plateau height and carves canyons. |
|
||||
| `cellFractureZoom` | double >= 0.001 | `1` | Size of the cells. |
|
||||
| `cellFractureShuffle` | double >= 0 | `12` | Randomises the cell centres. Low values give a regular lattice; high values look organic. |
|
||||
| `cellPercentSize` | double 0..1 | `0.75` | How much of a cell is core versus vein. `0.1` means thick veins and small plateaus. |
|
||||
|
||||
### Interpolator (`IrisInterpolator`)
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `function` | `InterpolationMethod` | `BILINEAR_STARCAST_6` | The kernel used to blend neighbouring columns' height bands. Required. |
|
||||
| `horizontalScale` | double 1..8192 | `7` | Radius, in blocks, of that blend. Small values keep detail but make biome borders abrupt; large values give long smooth transitions and wash out small features. Required. |
|
||||
|
||||
Available methods: `NONE`, `BILINEAR`, `STARCAST_3/6/9/12`, `BILINEAR_STARCAST_3/6/9/12`, `HERMITE_STARCAST_3/6/9/12`, `BILINEAR_BEZIER`, `BILINEAR_PARAMETRIC_1_5/2/4`, `BICUBIC`, `HERMITE`, `CATMULL_ROM_SPLINE`, `HERMITE_TENSE`, `HERMITE_LOOSE`, and the four `HERMITE_LOOSE_HALF/FULL_POSITIVE/NEGATIVE_BIAS` variants.
|
||||
|
||||
The shipping overworld uses `BILINEAR_STARCAST_9` almost everywhere and varies `horizontalScale` from 6 to 200. Higher starcast numbers cost more per column; `NONE` with scale `1` is the cheapest and gives hard borders, which is what `generators/flat.json` wants.
|
||||
|
||||
### Noise layer (`IrisNoiseGenerator`)
|
||||
|
||||
Available as the `generator` snippet.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `style` | `IrisGeneratorStyle` | `IRIS` | Where the raw noise comes from. Required. |
|
||||
| `seed` | long | `0` | Required. Offsets this layer's noise independently of the generator seed. |
|
||||
| `enabled` | boolean | `true` | When false the layer returns `offsetY` and nothing else, which is a cheap way to mute a layer while comparing. |
|
||||
| `zoom` | double >= 0.0001 | `1` | Divides this layer's sample coordinates. Give each octave-like layer a different zoom to build detail on top of large forms. |
|
||||
| `opacity` | double 0..1 | `1` | This layer's amplitude and its weight in the additive average. Two layers at `1` and `0.25` combine roughly 4:1. |
|
||||
| `negative` | boolean | `false` | Turns the output into `-noise + opacity`, mirroring the shape. Ridges become valleys. |
|
||||
| `offsetX` / `offsetZ` | double | `0` | Shifts the sample position after the zoom divide, so the unit is style space rather than blocks. |
|
||||
| `offsetY` | double | `0` | Added to the output, not the coordinates. Avoid it in terrain generators; it pushes the layer outside 0..1 and skews the average. |
|
||||
| `exponent` | double | `1` | Power curve on the output, sign-preserving. Above 1 pushes mid values down (flat basins, sharp peaks); below 1 lifts them (plateaus with narrow valleys). |
|
||||
| `octaves` | int >= 1 | `1` | Stacks the style at successively finer scales. Cheap way to add detail without more layers. |
|
||||
| `parametric` | boolean | `false` | S-curve remap; steepens the middle and softens both ends. |
|
||||
| `bezier` | boolean | `false` | Softer S-curve remap. `generators/plain.json` uses it to keep lowlands gentle. |
|
||||
| `sinCentered` | boolean | `false` | Maps 0 and 1 to 0 and 0.5 to 1 with a sine shape, turning a gradient into a ridge. |
|
||||
| `fracture` | `IrisNoiseGenerator[]` | `[]` | Child layers whose output warps this layer's input coordinates, producing the swirled, non-grid look. Each child costs a full extra noise evaluation, and children can nest. |
|
||||
|
||||
Remap order inside a layer: sample the style, multiply by `opacity`, apply `negative`, apply `exponent`, add `offsetY`, then `parametric`, `bezier`, `sinCentered` in that order.
|
||||
|
||||
## Generator style (`IrisGeneratorStyle`)
|
||||
|
||||
Available as the `style` snippet, and accepted anywhere Iris configures noise: generator layers, decorators, deposit palettes, cave profiles, biome child shapes, dimension placement noise.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `style` | `NoiseStyle` | `FLAT` | The built-in algorithm. Used only when neither `expression` nor `imageMap` produced a usable source. |
|
||||
| `zoom` | double >= 0.00001 | `1` | Feature scale. Applied as a coordinate multiplier of `1/zoom`, so larger zoom means larger features. |
|
||||
| `exponent` | double 0.01562..64 | `1` | Power curve on the style output before anything else consumes it. |
|
||||
| `multiplier` | double >= 0.00001 | `1` | Only read when this style is somebody's `fracture` child. It scales the coordinate displacement applied to the parent, roughly plus or minus half this value. `18` gives noticeable swirls; `55` heavily distorts. |
|
||||
| `fracture` | `IrisGeneratorStyle` | `null` | Warps the coordinates fed into this style. This is the main tool for making cellular and vascular styles look organic instead of geometric. |
|
||||
| `axialFracturing` | boolean | `false` | Fractures each axis with a different coordinate order. Looks better on large regional noise, and costs two to three times as much. |
|
||||
| `cellularFrequency` | double | `0` | Above 0, post-processes the style into cells, so continuous noise becomes flat-valued patches. |
|
||||
| `cellularZoom` | double | `1` | Cell size after cellularising. Ignored when `cellularFrequency` is 0. |
|
||||
| `expression` | expression key | `null` | Use `expressions/<key>.json` as the noise source instead of `style`. |
|
||||
| `imageMap` | `IrisImageMap` | `null` | Use a PNG as the noise source instead of `style`. |
|
||||
| `cacheSize` | int 0..8192 | `0` | Above 0, the built noise is cached to a `.cnm` file under the pack's `.cache` folder. Worth it for expensive expression or heavily fractured styles that are sampled repeatedly; wasted on cheap styles. |
|
||||
|
||||
Source priority: if `expression` is set, Iris loads it and uses it; if the expression fails to load, the style falls straight back to `NoiseStyle` — `imageMap` is not tried. `imageMap` is only consulted when `expression` is unset.
|
||||
|
||||
### Choosing a `NoiseStyle`
|
||||
|
||||
There are 171 constants; the Studio schema lists all of them. These are the ones that matter for terrain work:
|
||||
|
||||
| Purpose | Styles | Notes |
|
||||
|---------|--------|-------|
|
||||
| General terrain | `IRIS`, `IRIS_DOUBLE`, `IRIS_THICK`, `IRIS_HALF`, `SIMPLEX`, `PERLIN`, `PERLIN_IRIS` | `IRIS*` are pre-fractured signature noises and are the default choice for land. |
|
||||
| Large dramatic forms | `FRACTAL_SMOKE`, `FRACTAL_WATER`, `FRACTAL_FBM_SIMPLEX`, `FRACTAL_BILLOW_PERLIN` | `FRACTAL_SMOKE` at a large `horizontalScale` is what the shipping `mountain` generator uses. |
|
||||
| Coordinate warping (as a `fracture` child) | `NOWHERE`, `NOWHERE_CELLULAR`, `STATIC` | `NOWHERE` with a small zoom and a large `multiplier` is the standard swirl recipe. |
|
||||
| Plateaus and cliffs | `GLOB`, `CELLULAR_HEIGHT` | `CELLULAR_HEIGHT` gives one constant value per cell, which is what a cliff-height generator wants. |
|
||||
| Cells and veins | `CELLULAR`, `CELLULAR_IRIS_DOUBLE`, `CELLULAR_IRIS_THICK`, `VASCULAR`, `VASCULAR_THIN`, `SIMPLEX_VASCULAR`, `CLOVER`, the `HEX*` family | Used for region and biome placement more often than for height. |
|
||||
| Scatter and flat | `STATIC` (white noise), `STATIC_BILINEAR`, `FLAT` | `STATIC` is for per-block palette scatter, never terrain relief. `FLAT` returns 1.0 at every coordinate. |
|
||||
|
||||
## Expressions (`IrisExpression`)
|
||||
|
||||
An expression file is a Paralithic math expression that can be used anywhere a style is accepted, via a style's `expression` field.
|
||||
|
||||
| Field | Type | What it does |
|
||||
|-------|------|--------------|
|
||||
| `expression` | string | Required. The formula. `x`, `y` and `z` are pre-declared; do not redeclare them as variables. |
|
||||
| `variables` | `IrisExpressionLoad[]` | Named values bound before evaluation. |
|
||||
| `functions` | `IrisExpressionFunction[]` | Named callable functions available inside the formula. |
|
||||
|
||||
**Coordinate quirk.** Expressions are evaluated in two forms. In the 3D form the variables hold the real `x`, `y`, `z`. In the 2D form — which is what a style used for terrain height goes through — the arguments are packed as `x`, then the world Z coordinate, then `-1`. So in 2D sampling, `x` is the world X, `y` holds the world Z, and `z` is always `-1`. Write 2D expressions against `x` and `y`, and do not rely on `z` there.
|
||||
|
||||
### Variable (`IrisExpressionLoad`)
|
||||
|
||||
Available as the `expression-load` snippet.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `name` | `""` | The identifier used in the formula. Required. Must not be `x`, `y` or `z`, and must not repeat. |
|
||||
| `engineValue` | `null` | An engine scalar (`IrisEngineValueType`). Highest priority; requires an active engine. |
|
||||
| `engineStreamValue` | `null` | An engine procedural stream (`IrisEngineStreamType`) sampled at the coordinates. Second priority; requires an active engine. |
|
||||
| `styleValue` | `null` | A nested `IrisGeneratorStyle` sampled at the coordinates. Third priority. |
|
||||
| `staticValue` | `-1` | A constant. Used only when none of the above are set. Note the default is `-1`, not `0`. |
|
||||
|
||||
### Function (`IrisExpressionFunction`)
|
||||
|
||||
Available as the `expression-function` snippet.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `name` | none | The identifier called in the formula. Required. |
|
||||
| `styleValue` | `null` | Backs the function with a noise style, so `myNoise(a, b)` samples that style at `a, b`. |
|
||||
| `engineStreamValue` | `null` | Backs it with an engine stream instead. Takes priority over `styleValue`. |
|
||||
| `args` | `2` (minimum 2) | Argument count. Ignored when `engineStreamValue` is set, which always takes exactly 2. |
|
||||
|
||||
A function with neither `styleValue` nor `engineStreamValue` is skipped at parse time, so calling it fails to parse.
|
||||
|
||||
Parse and load failures are logged and leave the style falling back to its `NoiseStyle`. If an expression-based generator suddenly looks like plain noise, check the console for a script load error before editing the formula.
|
||||
|
||||
## Image maps (`IrisImageMap` + `IrisImage`)
|
||||
|
||||
Any PNG in `images/` becomes an `IrisImage` keyed by its path. A style points at one:
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `image` | image key | `""` | Which PNG to read. |
|
||||
| `coordinateScale` | double >= 1 | `32` | Blocks per pixel. `32` means one pixel covers a 32x32 block area. The style's own `zoom` still applies on top. |
|
||||
| `interpolationMethod` | `InterpolationMethod` | `BILINEAR_STARCAST_6` | How pixels are blended across their block area. Use `NONE` for hard nearest-neighbour edges, which is what you want for a mask. |
|
||||
| `channel` | `IrisImageChannel` | `COMPOSITE_ADD_HSB` | How a pixel becomes a number. |
|
||||
| `inverted` | boolean | `false` | Returns `1 - value`. |
|
||||
| `tiled` | boolean | `false` | Wraps coordinates modulo the image size, so the map repeats forever. Without it, everything outside the image reads as 0. |
|
||||
| `centered` | boolean | `true` | Puts world origin at the image centre instead of its top-left corner. |
|
||||
|
||||
Channels: `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`. All of them return 0..1 except `RAW`, which returns the packed pixel integer and is only useful as an input to an expression.
|
||||
|
||||
A missing image logs an error and reads as 0 everywhere, which produces a flat world rather than a crash.
|
||||
|
||||
## Dimension-level noise
|
||||
|
||||
Dimensions use styles for placement rather than height. These are listed here because they use the same `IrisGeneratorStyle` type; their behaviour belongs to `11 - Dimensions.md`.
|
||||
|
||||
| Dimension field | Role |
|
||||
|-----------------|------|
|
||||
| `regionStyle` + `regionZoom` | Which region owns a column |
|
||||
| `continentalStyle` + `continentZoom` + `landChance` | Land versus sea |
|
||||
| `landBiomeStyle` / `seaBiomeStyle` / `shoreBiomeStyle` / `caveBiomeStyle` | Which biome within the region's list for that role |
|
||||
| `biomeZoom`, `landZoom`, `seaZoom` | Global biome size multipliers applied before the region's own zooms |
|
||||
| `overlayNoise` | `IrisShapedGeneratorStyle[]` height offsets added on top of every column, each with its own `generator` style and `min`/`max` |
|
||||
| `coordFractureDistance` / `coordFractureZoom` | Global coordinate warp, the source of the large-scale "Iris swirls" |
|
||||
| `rockZoom` / `rockPalette` / `fluidPalette` | Fill materials below the biome layers and in water; see `16 - Surfaces, Decorators & Deposits.md` |
|
||||
|
||||
## Overworld examples
|
||||
|
||||
`generators/plain.json` — smooth lowlands, one warped layer softened by a bezier curve:
|
||||
|
||||
```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` — a single large-scale fractal with a very wide blend radius, so mountains have long approaches:
|
||||
|
||||
```json
|
||||
{
|
||||
"interpolator": { "horizontalScale": 52, "function": "BILINEAR_STARCAST_9" },
|
||||
"seed": 53551,
|
||||
"composite": [{
|
||||
"style": { "style": "FRACTAL_SMOKE", "zoom": 1 },
|
||||
"seed": 3356
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`generators/cracked-cliffs.json` — inverted glob shape quantised into terraces between 35 and 80 units, with the step height chosen per cell:
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The shipping overworld uses neither `expression` nor `imageMap` in any generator. It ships `images/prototype-rivers.png` and `images/vascularcliffs.png` for authors who want to try image-driven terrain.
|
||||
|
||||
## Practical notes
|
||||
|
||||
- Share one generator across many biomes and vary `min`/`max` per biome. That is what makes a mountain range and its foothills look like the same landform.
|
||||
- Match `interpolator.horizontalScale` between neighbouring biomes you want to blend smoothly, and deliberately mismatch it where you want a visible change in character.
|
||||
- Give a generator its own `horizontalScale` if you want its shape kept independent; reuse an existing one only when you want the shapes averaged together.
|
||||
- Do not ship two generator files whose settings are byte-for-byte identical, including `seed`. Generators are deduplicated by content when they are bucketed, so only one key survives and biomes referencing the other key silently get a zero height band.
|
||||
- Nested `fracture` multiplies cost. Keep fracture chains short on generators that run for every column, and reach for `cacheSize` before adding a third level.
|
||||
- `STATIC` is white noise. Use it for palette scatter, never for terrain relief.
|
||||
- `multiplicitive` and the deposit field `varience` are intentional code spellings. The JSON must match them exactly.
|
||||
- Terrain changes only apply to newly generated chunks. Always compare in fresh territory on a fixed seed.
|
||||
@@ -1,414 +0,0 @@
|
||||
# 15 - Caves & Carving
|
||||
|
||||
Iris carves its own caves during mantle generation and never runs the vanilla noise carvers. A cave profile's 3D density field marks which cells become carved space, and a later terrain pass turns those marks into air, dimension fluid, or lava and paints cave-biome floors, ceilings, walls, and decorators into the hollow. Everything is JSON on dimensions, regions, and biomes; there is no `caves/` or `ravines/` registrant type.
|
||||
|
||||
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`, `21 - Jigsaw Structures.md`, `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
## The mental model
|
||||
|
||||
Carving happens in two separate passes, and knowing which one you are looking at explains almost every "why did nothing change" question.
|
||||
|
||||
**Pass 1 — mantle carve (`MantleCarvingComponent` + `IrisCaveCarver3D`).** For each chunk Iris resolves which cave profile applies to every column, samples a 3D density field, and for each cell that falls below the carve threshold writes a *cavern mark* into the mantle. The mark carries an intent: plain air, dimension fluid, lava, or forced air. No blocks are touched yet — the mantle is a parallel voxel store that outlives the chunk, which is why caves line up across chunk borders and why cave objects can be anchored before terrain exists.
|
||||
|
||||
**Pass 2 — carve modifier (`IrisCarveModifier`).** After the terrain actuator has filled the column with stone and biome layers, this pass walks the chunk's cavern marks and replaces the real blocks: air marks become `cave_air`, fluid marks become the dimension `fluidPalette` block, lava marks become lava, forced-air marks become air even below the lava line. It then groups each column's carved cells into contiguous runs (a "zone"), and for each zone paints the cave biome's `layers` downward from the floor, `caveCeilingLayers` upward from the ceiling, the `wall` palette onto every solid block touching carved space, and finally runs the cave biome's surface and `CEILING` decorators.
|
||||
|
||||
Stage order in `OVERWORLD` mode, which is what everything below depends on:
|
||||
|
||||
1. biome actuator, mantle generation (carving, then objects), terrain actuator
|
||||
2. **carve modifier** — caverns become blocks; cave biome materials and decorators land here
|
||||
3. post modifier — surface slabs and cliff walls
|
||||
4. floating child biome solids
|
||||
5. deposit modifier, mantle object insertion, surface decorator actuator
|
||||
6. floating decoration, perfection, custom
|
||||
|
||||
Deposits run *after* carving and explicitly skip any cell that carries a cavern mark, so ore veins never dangle inside a cave. Surface decorators run after carving too, which is why a surface-breaking cave hole does not leave grass floating over the opening.
|
||||
|
||||
### Two different Y coordinates
|
||||
|
||||
This trips up nearly everyone. Iris generates internally from `0` to `(maxHeight - minHeight)` and shifts down on output. Fields split into two groups:
|
||||
|
||||
| Engine-local Y (0 = bottom of the world) | Absolute world Y |
|
||||
|---|---|
|
||||
| `caveProfile.verticalRange` | `carving[].worldYRange` |
|
||||
| `IrisCaveFieldModule.verticalRange` | structure placement `minHeight` / `maxHeight` |
|
||||
| dimension `caveLavaHeight`, `fluidHeight` | `depositVariants[].minHeight` / `maxHeight` |
|
||||
|
||||
With the default `dimensionHeight` of `-64..320`, engine-local `0` is world `-64` and engine-local `64` is world `0`. A profile `verticalRange` of `{ "min": 0, "max": 64 }` therefore covers the deepslate band, not the surface.
|
||||
|
||||
## Walkthrough: prove carving works before you tune it
|
||||
|
||||
Start from a validating `OVERWORLD` pack whose surface height and fluid level are already correct. The goal of this first pass is a visible void with nothing else changing, so a wrong palette or a leaking aquifer cannot be blamed on the density field.
|
||||
|
||||
1. Record seed `1337` and a surface coordinate in Studio before you touch anything, so you can compare the same spot afterwards.
|
||||
|
||||
2. Add this to the root object of `dimensions/<key>.json`. It uses production density defaults but seals the surface and disables both liquids:
|
||||
|
||||
```json
|
||||
{
|
||||
"carvingEnabled": true,
|
||||
"caveProfile": {
|
||||
"enabled": true,
|
||||
"verticalRange": { "min": 16, "max": 96 },
|
||||
"allowSurfaceBreak": false,
|
||||
"surfaceClearance": 8,
|
||||
"allowFluid": false,
|
||||
"allowLava": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Validate the pack and reopen Studio. Carving is written into the mantle, so only **freshly generated** chunks change; fly out past your previously generated area or use a new Studio world.
|
||||
|
||||
4. Dig down between world Y `-48` and `32` (engine-local 16 to 96 with the default height range). Success is open cave volume with intact grass overhead, no water pockets, and no lava at the bottom of the band.
|
||||
|
||||
5. If nothing is carved, work down this list before touching noise values: dimension `mode.type` is `OVERWORLD`; `carvingEnabled` and `useMantle` are both true; `CARVED` is not listed in `disabledComponents`; the profile you edited is the one that actually wins for those columns (see resolution order below); the chunks are new.
|
||||
|
||||
Once the void is right, turn the other switches back on one at a time — `allowLava`, then `allowFluid`, then `allowSurfaceBreak` — and regenerate between each. Each one changes a distinct, observable thing, and changing two at once makes it impossible to tell which produced the result.
|
||||
|
||||
## Walkthrough: give one biome its own cave shape
|
||||
|
||||
Profiles resolve per column, and the **last enabled profile in the chain wins**:
|
||||
|
||||
```
|
||||
dimension.caveProfile → region.caveProfile → surface biome.caveProfile → cave biome.caveProfile
|
||||
```
|
||||
|
||||
A disabled profile (`enabled: false`, which is the Java default) is skipped entirely rather than blocking the level above it, so a biome only overrides the dimension when its own profile is explicitly enabled.
|
||||
|
||||
To give one surface biome tight, dense tunnels while the rest of the world keeps the dimension's caverns, put an enabled profile on that biome:
|
||||
|
||||
```json
|
||||
{
|
||||
"caveProfile": {
|
||||
"enabled": true,
|
||||
"verticalRange": { "min": 8, "max": 110 },
|
||||
"baseDensityStyle": { "style": "SIMPLEX_VASCULAR", "zoom": 0.9 },
|
||||
"detailDensityStyle": { "style": "SIMPLEX", "zoom": 0.6 },
|
||||
"baseWeight": 1,
|
||||
"detailWeight": 0.2,
|
||||
"densityThreshold": { "min": -0.22, "max": -0.14 },
|
||||
"thresholdBias": 0.16,
|
||||
"allowSurfaceBreak": false,
|
||||
"allowFluid": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Iris blends profiles across the biome edge instead of cutting them off. Every column samples a 7x7 neighbourhood, weights each neighbour by how close it is to the centre, and normalises the result into a per-column weight. Columns below a weight of `0.08` are dropped, and columns between get their carve threshold pulled back proportionally, so a strong profile fades out over roughly three blocks rather than ending on a chunk-shaped seam.
|
||||
|
||||
At most **two** blended profiles run per chunk. When more than two are present, the lowest-weight ones are folded into whichever kept profile dominates each column. Profiles pulled in by dimension `carving` entries are added on top of that limit and are not subject to it.
|
||||
|
||||
## Walkthrough: paint the inside of a cave
|
||||
|
||||
Carving produces empty space and nothing else. Materials, plants, and props come from a **cave biome**, which is an ordinary biome JSON that Iris happens to look up underground.
|
||||
|
||||
1. Write `biomes/carving/mossy.json` as a normal biome. Cave biomes typically omit height generators — the carve step already removed solid, and nothing reads their terrain height.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Mossy Caverns",
|
||||
"derivative": "minecraft:lush_caves",
|
||||
"vanillaDerivative": "minecraft:lush_caves",
|
||||
"layers": [
|
||||
{ "minHeight": 1, "maxHeight": 1, "palette": [{ "block": "minecraft:moss_block" }] },
|
||||
{ "minHeight": 1, "maxHeight": 2, "palette": [{ "block": "minecraft:dirt" }] }
|
||||
],
|
||||
"caveCeilingLayers": [
|
||||
{ "minHeight": 1, "maxHeight": 1, "palette": [{ "block": "minecraft:moss_block" }] }
|
||||
],
|
||||
"wall": {
|
||||
"palette": [
|
||||
{ "block": "minecraft:stone" },
|
||||
{ "block": "minecraft:mossy_cobblestone" }
|
||||
]
|
||||
},
|
||||
"decorators": [
|
||||
{ "chance": 0.25, "palette": [{ "block": "minecraft:moss_carpet" }] },
|
||||
{
|
||||
"partOf": "CEILING",
|
||||
"chance": 0.12,
|
||||
"palette": [{ "block": "minecraft:cave_vines" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. List it in a region's `caveBiomes` pool:
|
||||
|
||||
```json
|
||||
{ "caveBiomes": ["carving/mossy"] }
|
||||
```
|
||||
|
||||
3. Regenerate and look inside a cave in that region. Success is a moss floor, a moss ceiling, mixed stone walls, carpets on the floor and vines on the roof.
|
||||
|
||||
Two rules decide whether any of this appears:
|
||||
|
||||
- **A carved run must be at least 3 blocks tall.** A zone is only processed when its air thickness (`ceiling - floor - 1`) is greater than zero. Two-block-tall crawlspaces get carved but keep raw stone and no decoration.
|
||||
- **Floor layers only overwrite solid blocks.** They descend from the block below the lowest carved cell. Where the layer stack runs into existing air, it stops. Ore blocks in the floor are converted to the deepslate variant matching the layer instead of being erased, so an iron vein exposed in a deepslate cave floor stays iron.
|
||||
|
||||
### How Iris picks the cave biome at a point
|
||||
|
||||
For a given `(x, y, z)`, in order:
|
||||
|
||||
1. If an enabled dimension `carving[]` entry's `worldYRange` contains the absolute world Y, its biome wins outright. Child entries subdivide that band into patches.
|
||||
2. Otherwise the region `caveBiomes` pool is sampled by `caveBiomeStyle` and biome `rarity`, zoomed by the dimension `biomeZoom` multiplied by the region's `caveBiomeZoom`.
|
||||
3. If the sampled cave biome's `caveMinDepthBelowSurface` is deeper than the point actually is, or if the point is at or above the surface, the **surface** biome is used instead.
|
||||
|
||||
Results are blended: the resolver samples the centre plus four points three blocks out, and where they disagree it picks the centre half the time and one of the four neighbours otherwise, seeded per block position. That produces a speckled transition band rather than a hard edge between two cave biomes.
|
||||
|
||||
`carvingBiome` on a surface biome is **not** part of this lookup. At runtime it only pulls the referenced biome into the pack's reachable-biome closure so its custom biome identity and spawn mappings get registered; it never selects a cave biome during generation. Use region `caveBiomes` or a dimension `carving` band instead.
|
||||
|
||||
Enabled dimension `carving` biomes are likewise included in the recursive reachable-biome closure even when no region lists them, so their identities are available wherever the Y band selects them.
|
||||
|
||||
## Fluids and lava inside caves
|
||||
|
||||
Aquifers and deep lava are two independent mechanisms, and neither changes cave geometry.
|
||||
|
||||
**Deep lava** is a straight Y test done while the carve marks are written: any carved cell at or below the dimension `caveLavaHeight` (engine-local, default `8`) is marked lava when `allowLava` is true. When `allowLava` is false those cells are marked *forced air*, which the carve modifier honours explicitly — that is how a dry lava-level cave stays dry. A cavern mark with plain air intent that reaches the carve modifier from some other source (structure boring, for example) below `caveLavaHeight` becomes lava, because plain air is the "use the default for this depth" intent.
|
||||
|
||||
**Aquifers** use the dimension `fluidPalette`, which accepts any weighted block palette and defaults to water. Swapping it for lava turns identical Overworld caverns into lava lakes without touching a single density value. A carved cell becomes fluid only when all of the following hold:
|
||||
|
||||
- it is at or below `min(fluidHeight, columnSurfaceY - fluidMinDepthBelowSurface)`
|
||||
- it is not already a lava cell
|
||||
- a detail-noise sample at that point clears a cutoff that **rises with depth** — about `0.35` at the fluid line, `0.55` some 48 blocks below it, topping out at `0.65` around 72 blocks down, so shallow aquifers are common and deep ones are rare
|
||||
- with `fluidRequiresFloor` on (the default), the cell sits in a cup: solid directly below, solid two below, and at least four of its five remaining neighbours (four horizontal plus above) solid
|
||||
|
||||
Set `allowFluid: false` for a completely dry cave system. `allowWater`, `waterMinDepthBelowSurface`, and `waterRequiresFloor` were removed; pack validation rejects them by name in inline dimension, region, and biome profiles and in `snippet/cave-profile` files, so an old dry-cave setting cannot silently fall back to the new `allowFluid: true` default.
|
||||
|
||||
## Surface openings
|
||||
|
||||
Whether a cave can reach daylight is decided per column before any density sampling:
|
||||
|
||||
```
|
||||
breakColumn = allowSurfaceBreak && surfaceBreakNoise2D(x, z) >= surfaceBreakNoiseThreshold
|
||||
```
|
||||
|
||||
In a break column, carving is allowed all the way up to the terrain surface, and within `surfaceBreakDepth` blocks of the surface the carve threshold is relaxed by `surfaceBreakThresholdBoost` so the opening actually punches through instead of pinching shut. In every other column, carving stops `surfaceClearance` blocks below the surface.
|
||||
|
||||
After materials are applied, an ore block sitting on the surface directly above a carved, unsupported cell is deleted. That prevents a floating ore cap over a cave mouth. Supported surface ores and underground ores are untouched.
|
||||
|
||||
For sealed caves: `allowSurfaceBreak: false` plus a larger `surfaceClearance`. For more openings: lower `surfaceBreakNoiseThreshold` (it is a signed noise cutoff, so `0.4` opens far more columns than `0.62`).
|
||||
|
||||
## Dimension gates
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `carvingEnabled` | boolean | `true` | Master switch. False adds `CARVED` to the disabled mantle components, so no profile anywhere carves. Use it to A/B a world against a solid version |
|
||||
| `caveProfile` | `IrisCaveProfile` | disabled | The fallback profile for every column no region or biome overrides. This is where most packs put their cave system |
|
||||
| `carving` | `IrisDimensionCarvingEntry[]` | `[]` | Absolute-world-Y bands that force a specific cave biome regardless of surface biome. Use for a global deep dark or a magma layer |
|
||||
| `caveBiomeStyle` | `IrisGeneratorStyle` | cellular iris double | Shape of the patches that pick between a region's `caveBiomes`. Cellular gives blobby cave regions; wispy styles give streaks |
|
||||
| `caveLavaHeight` | int 0..318 | `8` | Engine-local Y at or below which carved cells fill with lava (or forced air when `allowLava` is false). Raise it for a hellish lower world |
|
||||
| `requireObjectSurfaceSupport` | boolean | `true` | Refuses to place surface objects and trees over a carve opening. Turn off only if you want trees hanging over cave mouths |
|
||||
| `objectSurfaceSupportBuffer` | int 0..16 | `2` | Blocks of solid ground required around a surface object's footprint. A placement can ask for more but never less |
|
||||
| `upperDimensionCarving` | boolean | `false` | Lets caves cut into the inverted ceiling terrain of an `upperDimension`. Off leaves the canopy a solid slab |
|
||||
| `useMantle` | boolean | `true` | Turning this off disables carving, objects, and structures together |
|
||||
|
||||
## Cave profile (`IrisCaveProfile`)
|
||||
|
||||
Snippet key: `cave-profile`. Valid on **dimension**, **region**, and **biome**.
|
||||
|
||||
### Extent and shape
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `enabled` | boolean | `false` | Nothing carves until this is true. A listed but disabled profile is invisible to the resolver |
|
||||
| `verticalRange` | `IrisRange` | `0..384` | Engine-local Y window this profile may carve in. Clamp it to keep caves out of the deepslate floor or the sky |
|
||||
| `verticalEdgeFade` | int 0..128 | `20` | Blocks of smoothstep taper at both ends of `verticalRange`. Without it caves are sliced off flat at the boundary |
|
||||
| `verticalEdgeFadeStrength` | double 0..1 | `0.18` | How hard the taper pushes toward solid. Raise it if the top and bottom of your cave band still look cut |
|
||||
| `baseDensityStyle` | `IrisGeneratorStyle` | cellular iris double | The field that decides overall cave layout. Cellular reads as chambers and connecting tunnels; simplex reads as sponge |
|
||||
| `detailDensityStyle` | `IrisGeneratorStyle` | simplex | Added on top of the base field to roughen walls. Keep its weight low or it dissolves the base structure |
|
||||
| `warpStyle` | `IrisGeneratorStyle` | flat | Domain warp applied to the sample coordinates. Only has an effect when `warpStrength` is above zero |
|
||||
| `baseWeight` | double >= 0 | `1` | Contribution of the base field. All weights are normalised, so raising this is equivalent to lowering the others |
|
||||
| `detailWeight` | double >= 0 | `0.35` | Contribution of the detail field. Above about `0.5` the base layout stops being readable |
|
||||
| `warpStrength` | double >= 0 | `0` | Block distance the warp displaces samples. Small values (0.2 to 1) bend straight tunnels; large values scramble everything and cost a second noise lookup per sample |
|
||||
| `densityThreshold` | `IrisStyledRange` | `-0.2..0.2`, cellular iris double | The carve cutoff, itself noise-varied across the world so cave size differs region to region. Set `min` equal to `max` for a constant threshold |
|
||||
| `thresholdBias` | double 0..1 | `0.16` | Subtracted from the sampled threshold before the test. Lower it for more carved space, raise it for less. This is the single knob to reach for when caves are globally too big or too small |
|
||||
|
||||
### Sampling and cost
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `sampleStep` | int 1..8 | `1` | 1 and 2 use exact per-cell evaluation. **3 or higher switches to a lattice pass** that samples one column per 2x2 tile and stamps only two vertical blocks per sample, so values above 2 leave uncarved horizontal bands and blocky 2x2 walls. Treat 3+ as a deliberate low-fidelity mode, not a free speedup |
|
||||
| `adaptiveSampling` | boolean | `true` | With `sampleStep` 1 or 2, classifies a coarse grid first and only evaluates exactly where the plane is ambiguous. Leave it on; it is the main reason carving is affordable |
|
||||
| `adaptiveSampleStep` | int 2..4 | `2` | The runtime predictor grid is always 8 regardless of this value. What this field actually changes is the ambiguity margin: each step below 8 adds `0.015` to the margin, so `2` is the most conservative (widest margin, most exact fallback) and `4` is the loosest |
|
||||
| `adaptiveThresholdMargin` | double 0..1 | `0.04` | Base ambiguity band around the threshold where the predictor refuses to guess. Raise it if adaptive sampling is visibly clipping thin tunnels |
|
||||
|
||||
Adaptive classification also falls back to exact evaluation whenever fewer than 16 columns are active in a plane, so sparse chunk edges are never approximated.
|
||||
|
||||
### Surface interaction
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `surfaceClearance` | int 0..64 | `4` | Solid blocks kept below the terrain surface in non-breaking columns. This is your roof thickness |
|
||||
| `allowSurfaceBreak` | boolean | `true` | Master switch for cave mouths. False makes every column obey `surfaceClearance` |
|
||||
| `surfaceBreakStyle` | `IrisGeneratorStyle` | simplex, zoom `0.08` | 2D field that decides which columns may break through. Its zoom sets how large a single opening is |
|
||||
| `surfaceBreakNoiseThreshold` | double -1..1 | `0.62` | Signed cutoff on that field. Lower means more and wider openings |
|
||||
| `surfaceBreakDepth` | int 0..64 | `18` | How far below the surface the relaxed threshold applies. Too small and openings pinch shut just under the grass |
|
||||
| `surfaceBreakThresholdBoost` | double 0..1 | `0.2` | How much easier carving gets inside that depth window |
|
||||
|
||||
### Cave object anchoring
|
||||
|
||||
These apply to `.iob` placements and procedural objects marked `carvingSupport: CARVING_ONLY`. See `20 - Object Placement.md` and `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `objectMinDepthBelowSurface` | int 0..64 | `6` | Cave objects will not anchor closer than this to the surface, so props do not appear inside a cave mouth |
|
||||
| `defaultObjectAnchor` | `IrisCaveAnchorMode` | `FLOOR` | Anchor used by any cave placement that leaves its own anchor at `PROFILE_DEFAULT` |
|
||||
| `defaultObjectPlaceMode` | `ObjectPlaceMode` | unset | Overrides the place mode of cave placements, **but only for placements still on the default `CENTER_HEIGHT`**. Set it to `FAST_MIN_STILT` or `ORGANIC_STILT` so props tile down to the cave floor instead of hovering |
|
||||
| `anchorScanStep` | int 1..8 | `1` | Vertical step while scanning a column for anchors. Above 1 it can step over one-block ledges |
|
||||
| `anchorSearchAttempts` | int 1..64 | `6` | Random columns tried per chunk before giving up on a cave placement. Raise it when caves are sparse and props rarely appear |
|
||||
|
||||
### Liquids
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `allowFluid` | boolean | `true` | Enables aquifers from the dimension `fluidPalette` |
|
||||
| `fluidMinDepthBelowSurface` | int 0..64 | `12` | Aquifers stay at least this far below the terrain surface, which keeps water from bleeding out of a hillside |
|
||||
| `fluidRequiresFloor` | boolean | `true` | Requires the cup test described above. Turning it off gives far more fluid and far more of it pouring down shafts |
|
||||
| `allowLava` | boolean | `true` | When false, carved cells at or below `caveLavaHeight` are marked forced air rather than lava |
|
||||
|
||||
### Density module (`IrisCaveFieldModule`)
|
||||
|
||||
Snippet key: `cave-field-module`. Modules are extra density layers summed into the base and detail fields inside their own Y window. Adding a module with a distinct style is how you get two *kinds* of cave in one profile — wide chambers from the base field plus a wormy tunnel network from a module — which raising `detailWeight` alone cannot do.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `style` | `IrisGeneratorStyle` | cellular iris double | The module's own field. Pick something structurally different from `baseDensityStyle` or the module just adds noise |
|
||||
| `weight` | double >= 0 | `1` | Contribution relative to the base and detail fields. Start around `0.1` |
|
||||
| `threshold` | double -1..1 | `0` | Offset subtracted from the module sample before weighting, shifting the module toward carving or toward solid |
|
||||
| `verticalRange` | `IrisRange` | `0..384` | Engine-local Y window where the module contributes at all. Use it for a tunnel layer that exists only in the mid-depths |
|
||||
| `invert` | boolean | `false` | Flips the module's sign, so it *fills* where it would have carved. Useful for punching solid pillars and ribs through an otherwise open cavern |
|
||||
|
||||
### Anchor modes (`IrisCaveAnchorMode`)
|
||||
|
||||
| Value | Anchor it selects |
|
||||
|-------|-------------------|
|
||||
| `PROFILE_DEFAULT` | Defer to the active profile's `defaultObjectAnchor`; resolves to `FLOOR` if that is also unset |
|
||||
| `FLOOR` | Carved cell with solid directly below — standing props, stalagmites, chests |
|
||||
| `CEILING` | Carved cell with solid directly above — hanging roots, stalactites, `CEILING_HANG` objects |
|
||||
| `CENTER` | Carved cell with no solid immediately above or below — floating props in open air |
|
||||
| `ANY` | Any carved cell |
|
||||
|
||||
A placement using `ObjectPlaceMode.CEILING_HANG` is forced to the `CEILING` anchor regardless of what its anchor field says.
|
||||
|
||||
## Dimension carving entries (`IrisDimensionCarvingEntry`)
|
||||
|
||||
Snippet key: `dimension-carving-entry`. These override the cave biome inside an absolute world-Y band, independent of what the surface biome above is. Every enabled entry whose biome has an enabled `caveProfile` also contributes that profile as an extra carving pass restricted to the band, on top of the two-blended-profile budget.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `id` | string | `""` | Stable identifier. Other entries reference it through `children`, and floating child biomes can reference it by id |
|
||||
| `enabled` | boolean | `true` | Disabled entries are skipped and drop out of the reachable-biome closure |
|
||||
| `biome` | biome key | `""` | The cave biome applied throughout the band |
|
||||
| `worldYRange` | `IrisRange` | `-64..320` | **Absolute** world Y, unlike everything else on the profile |
|
||||
| `children` | string[] | `[]` | Ids of entries that carve patches inside this one. Cycles back to a parent id are allowed and bounded by depth |
|
||||
| `childShrinkFactor` | double | `1.5` | Larger values make child patches smaller relative to the parent band |
|
||||
| `childStyle` | `IrisGeneratorStyle` | cellular iris double | Shape of the child patches |
|
||||
| `childRecursionDepth` | int | `3` | How many levels of children are resolved before the walk stops |
|
||||
|
||||
```json
|
||||
{
|
||||
"carving": [
|
||||
{
|
||||
"id": "global-deepdark-band",
|
||||
"enabled": true,
|
||||
"biome": "carving/standard-deepdark",
|
||||
"worldYRange": { "min": -250, "max": -175 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Cave biome content reference
|
||||
|
||||
| Mechanism | Lives on | What it contributes |
|
||||
|-----------|----------|---------------------|
|
||||
| `caveBiomes` | region | The pool `caveBiomeStyle` samples from, weighted by biome `rarity` |
|
||||
| `caveBiomeZoom` | region | Multiplies the dimension `biomeZoom` for cave patches only, so cave regions can be a different size from surface biomes |
|
||||
| `caveMinDepthBelowSurface` | cave biome | Above this depth the surface biome is used instead, keeping cave materials out of shallow overhangs |
|
||||
| `caveProfile` | any biome | Local density override, only when enabled |
|
||||
| `layers` | cave biome | Floor materials, applied downward from the block below the lowest carved cell |
|
||||
| `caveCeilingLayers` | cave biome | Roof materials, applied upward from the block above the highest carved cell |
|
||||
| `wall` | cave biome | Painted onto every solid block horizontally adjacent to carved space that is below the terrain surface. The same palette also paints surface cliffs in the post pass, so a cave biome usually wants its own `wall` |
|
||||
| `decorators` | cave biome | Default-part decorators land on the cave floor; `partOf: CEILING` decorators hang from the roof |
|
||||
| `objects` / `proceduralObjects` | cave biome | Props, gated by `carvingSupport: CARVING_ONLY` and the profile's anchor settings |
|
||||
|
||||
Cave biomes still accept height generators, but nothing reads them underground. Leave them out or use a trivial filler.
|
||||
|
||||
## Cave-anchored jigsaw structures
|
||||
|
||||
Editable Iris jigsaws can resolve their start against carved space in the mantle instead of the surface or a blind Y band. Put the placement in `structures[]` on a dimension, region, surface biome, or cave biome and use one of the explicit cave anchors; a cave-biome `structures[]` list contributes cave anchors only.
|
||||
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"structures": ["stronghold/demo"],
|
||||
"placementId": "stronghold-demo-cave-floor",
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 24,
|
||||
"separation": 8,
|
||||
"salt": 984211,
|
||||
"anchor": "CAVE_FLOOR",
|
||||
"minHeight": -48,
|
||||
"maxHeight": 80,
|
||||
"caveBiomes": ["carving/deep"],
|
||||
"caveAnchorAttempts": 12,
|
||||
"caveAnchorScanStep": 1,
|
||||
"caveMinimumClearance": 5,
|
||||
"terrain": {"mode": "PRESERVE"}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default | Runtime behavior |
|
||||
|---|---|---|
|
||||
| `anchor` | `LEGACY` | `CAVE_FLOOR`, `CAVE_CEILING`, `CAVE_CENTER`, or `CAVE_ANY` search carved cells instead of terrain height |
|
||||
| `minHeight` / `maxHeight` | `-2032` / `2032` | Inclusive absolute world-Y scan band, clipped to one block inside the dimension's usable height |
|
||||
| `caveBiomes` | empty | Allowlist rechecked against the cave biome at each candidate anchor; keys are trimmed, case-normalized, and may include or omit the namespace |
|
||||
| `caveAnchorAttempts` | `8` | Unique columns tested inside the start chunk, clamped to `1..64`. Columns are visited by a seeded odd stride so no column repeats |
|
||||
| `caveAnchorScanStep` | `1` | Vertical scan increment, clamped to `1..16`. Above one it can skip valid single-block anchors |
|
||||
| `caveMinimumClearance` | `3` | Required contiguous vertical carved run, clamped to `1..64` |
|
||||
| `underwater` | `false` | For cave anchors, requires a dry cavern cell: ordinary cavern air must be above `caveLavaHeight`, explicit fluid and lava cells are rejected, and forced-air cavern matter counts as dry even below that line. `true` permits fluid cavern cells |
|
||||
|
||||
Geometry and alignment:
|
||||
|
||||
| Anchor | Candidate test | Alignment after assembly |
|
||||
|---|---|---|
|
||||
| `CAVE_FLOOR` | Candidate is carved, the cell below is not, and the clearance run continues upward | Lowest structure bound shifted to the anchor Y |
|
||||
| `CAVE_CEILING` | Candidate is carved, the cell above is not, and the clearance run continues downward | Highest structure bound shifted to the anchor Y |
|
||||
| `CAVE_CENTER` | Candidate is a midpoint of its contiguous carved run, and that run meets the clearance requirement | Bounding-box midpoint shifted to the anchor Y |
|
||||
| `CAVE_ANY` | A clearance-sized carved run is centred on the candidate | Bounding-box midpoint shifted to the anchor Y |
|
||||
|
||||
Selection is deterministic for the world seed, placement identity, and start chunk. Iris visits at most 64 of the chunk's 256 columns, stops at the first column with any match, and picks deterministically among every valid anchor in that column. When no candidate passes, the placement is skipped — there is no fallback to a surface or height-band start.
|
||||
|
||||
The test reads a single vertical cavern column. It proves local clearance, not that the assembled footprint fits. `SOURCE` and `PRESERVE` can therefore leave pieces embedded in surrounding rock; use `BORE` or `FORCE_CARVE` when the structure must make its own room.
|
||||
|
||||
Scope is decided at chunk centre: surface-biome, cave-biome, region, and dimension lists available there all contribute candidate placements. Cave lookup requires already-materialized mantle data, so a locator cannot resolve a distant ungenerated cave anchor until terrain generation has produced that mantle.
|
||||
|
||||
Cave anchors count as underground placement. Iris skips the surface-burial shift and does not clear intersecting surface trees. Piece placement resolves to `STRUCTURE_PIECE` underground except for authored `ORGANIC_STILT` and `CEILING_HANG` modes. The `anchor` field is rejected on `nativeStructures`. Full authoring detail is in `21 - Jigsaw Structures.md`.
|
||||
|
||||
## Vanilla carvers never run
|
||||
|
||||
Iris does not implement Minecraft `NoiseGeneratorSettings` carver sampling. The biome definitions Iris emits carry empty carver entries, and on the Fabric, Forge, and NeoForge adapters `applyCarvers` is an explicit no-op. Pack authors must use `caveProfile` and cave biomes; datapack carver features have no effect on Iris terrain. See `30 - Platform Differences.md`.
|
||||
|
||||
## Tuning quick reference
|
||||
|
||||
| Goal | Change |
|
||||
|------|--------|
|
||||
| Bigger caverns everywhere | Raise `thresholdBias` toward `0.25`, or widen `densityThreshold` downward |
|
||||
| Thinner tunnels | Lower `thresholdBias`, lower `detailWeight`, add an inverted module to fill the middles |
|
||||
| Two distinct cave types in one profile | Add a module with a different `style` and its own `verticalRange` |
|
||||
| Fewer surface holes | Raise `surfaceBreakNoiseThreshold`, lower `surfaceBreakDepth`, or `allowSurfaceBreak: false` with a larger `surfaceClearance` |
|
||||
| Cave props stop floating | Set `defaultObjectPlaceMode` to a stilt mode and raise `objectMinDepthBelowSurface` |
|
||||
| Completely dry caves | `allowFluid: false` and `allowLava: false` |
|
||||
| Lava-filled caverns instead of water | Change the dimension `fluidPalette` to lava; leave the profile alone |
|
||||
| Cheaper carving | Keep `adaptiveSampling` on and simplify the styles. Prefer this over raising `sampleStep`, which degrades shape |
|
||||
|
||||
## Practical notes
|
||||
|
||||
- `enabled: false` is the Java default on every cave profile, including the dimension's. Listing cave biomes without enabling a profile produces no caves at all.
|
||||
- Carving is written into the mantle. Existing chunks never gain caves from a settings change; only fresh chunks do.
|
||||
- Cave biome layers do not create voids. They only replace blocks that carving already exposed.
|
||||
- Upper-dimension carving is off by default, which leaves an `upperDimension` ceiling as an untouched solid mass.
|
||||
@@ -1,403 +0,0 @@
|
||||
# 16 - Surfaces, Decorators & Deposits
|
||||
|
||||
Three systems dress the terrain once its shape exists. Biome `layers` decide what the top few blocks of each column are made of, with the dimension `rockPalette` filling everything below. Decorators scatter a single block or stack on top of a surface — grass, flowers, cane, lily pads, cave vines. Deposits stamp pre-baked clumps of ore and stone into already-solid rock underground.
|
||||
|
||||
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`, `19 - Objects.md`, `20 - Object Placement.md`, `24 - Pack Mods & Snippets.md`.
|
||||
|
||||
## The mental model
|
||||
|
||||
The terrain actuator walks each column downward from `max(fluidHeight, terrainHeight)` to bedrock and asks, at every Y:
|
||||
|
||||
1. Does a **surface ore** generator claim this cell? If so it wins outright, above or below the waterline.
|
||||
2. Is this cell above the terrain but below the fluid level? Fill from `seaLayers`, indexed by distance down from the water surface; fall through to the dimension `fluidPalette`.
|
||||
3. Otherwise it is inside the terrain. Fill from the biome `layers` stack, indexed by distance down from the surface. Once the stack runs out, try **underground ore** generators, and failing that use the dimension `rockPalette`.
|
||||
|
||||
Layers are therefore a *depth-indexed list*, not a set of absolute Y bands. The first layer covers the topmost block, the second covers whatever depth the first did not, and so on.
|
||||
|
||||
Everything else lands in later stages. Using the `OVERWORLD` pipeline order:
|
||||
|
||||
| Stage | What runs |
|
||||
|---|---|
|
||||
| 1 | biome actuator, mantle generation, **terrain actuator** (layers, sea layers, ores, rock) |
|
||||
| 2 | carve modifier — cave biome floor/ceiling/wall materials and cave decorators (`15 - Caves & Carving.md`) |
|
||||
| 3 | post modifier — biome `slab` and `wall` on surface terrain |
|
||||
| 4 | floating child biome solids |
|
||||
| 5 | **deposit modifier**, mantle object insertion, **surface decorator actuator** — these three run concurrently |
|
||||
| 6 | floating decoration, perfection, custom |
|
||||
|
||||
Two consequences worth internalising: deposits go in *after* caves are cut and skip any cell carrying a cavern mark, so veins never hang in open air; and decorators run after carving, so a cave that broke the surface does not get flowers planted over the hole.
|
||||
|
||||
## Walkthrough: a surface, a flower scatter, and an ore vein
|
||||
|
||||
Start from the flat generator in `26 - Example - Minimal Dimension.md`. Save this as `biomes/tutorial/surface-test.json`, list `tutorial/surface-test` in one region's `landBiomes`, and set the dimension `focus` to the same key so it is the only biome generated.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Surface Test",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"generators": [
|
||||
{ "generator": "flat", "min": 16, "max": 16 }
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [{ "block": "minecraft:grass_block" }]
|
||||
},
|
||||
{
|
||||
"minHeight": 3,
|
||||
"maxHeight": 3,
|
||||
"palette": [{ "block": "minecraft:dirt" }]
|
||||
}
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"chance": 0.05,
|
||||
"palette": [{ "block": "minecraft:dandelion" }]
|
||||
}
|
||||
],
|
||||
"deposits": [
|
||||
{
|
||||
"minHeight": 0,
|
||||
"maxHeight": 96,
|
||||
"minSize": 3,
|
||||
"maxSize": 6,
|
||||
"minPerChunk": 1,
|
||||
"maxPerChunk": 2,
|
||||
"palette": [{ "block": "minecraft:coal_ore" }],
|
||||
"varience": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1. Validate the pack and open Studio on seed `1337`.
|
||||
2. Generate fresh chunks and cut a cross-section. Success is exactly one grass block over three dirt blocks over stone, dandelions scattered on the grass, and coal clumps only in the lower part of the column.
|
||||
3. If the column is wrong, delete `decorators` and `deposits` and get `layers` right first. Layer thickness is noise-fit between `minHeight` and `maxHeight` per column, so equal min and max is the way to get a guaranteed thickness while debugging.
|
||||
4. If flowers never appear, raise `chance` to `0.5` temporarily and confirm the dimension has `decorate: true` (the default). `chance` is a noise-field cutoff, not a dice roll, so a low value can genuinely produce nothing within one chunk.
|
||||
5. If deposits never appear, check the Y band. Deposit `minHeight` and `maxHeight` are **engine-local Y** (0 = bottom of the world), and a clump centre is forced at least 9 blocks below the column's terrain surface. On a surface at engine-local 80, a band of `0..96` really means `0..71`.
|
||||
6. Remove `focus` once the biome behaves, then tune each system on its own.
|
||||
|
||||
Keep the code spelling `varience`. It is the field name.
|
||||
|
||||
## Surfaces and material layers
|
||||
|
||||
### Biome-level fields
|
||||
|
||||
| Field | What it controls |
|
||||
|-------|------------------|
|
||||
| `layers` | The land column, top down. Grass, then dirt, then whatever; stone below is the dimension `rockPalette` |
|
||||
| `seaLayers` | The water column above the seafloor, indexed down from the water surface. Empty means plain fluid |
|
||||
| `caveCeilingLayers` | Roof materials inside carved space. Applied by the carve modifier, not the terrain actuator |
|
||||
| `slab` | Palette used by the post pass to soften single-block steps. An empty palette (the default) means no slabs |
|
||||
| `wall` | Painted onto exposed vertical faces. Used twice: by the post pass on surface cliffs, and by the carve modifier on cave walls. An empty palette (the default) disables both |
|
||||
| `lockLayers` | Switches to mesa banding, described below |
|
||||
| `lockLayersMax` | Caps how many blocks deep the banded stack goes. Default `7` |
|
||||
|
||||
`layers` and `caveCeilingLayers` both default to a single grass-block layer, so a biome that never declares them still produces grass floors and grass cave roofs.
|
||||
|
||||
### Palette layer (`IrisBiomePaletteLayer`)
|
||||
|
||||
Snippet key: `biome-palette`.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `palette` | `IrisBlockData[]` | grass block | Blocks this layer may use. With more than one entry, `style` and `zoom` decide which appears where; `weight` on an entry duplicates it in the pick list |
|
||||
| `minHeight` | int 0..2032 | `1` | Thinnest this layer can be in any column |
|
||||
| `maxHeight` | int 1..2032 | `1` | Thickest it can be. Iris noise-fits per column between the two, so unequal values give a wandering soil depth |
|
||||
| `style` | `IrisGeneratorStyle` | `STATIC` | Picks between palette entries. `STATIC` scatters per block; wispy or cellular styles produce visible patches of one material |
|
||||
| `zoom` | double >= 0.0001 | `5` | Scale of that pick. Larger zoom means larger single-material patches |
|
||||
| `slopeCondition` | `IrisSlopeClip` | 0..10 | Where the slope is outside this range, the layer is **skipped entirely** for that column. It does not thin gradually — the block below takes over. Use it to strip grass off cliff faces |
|
||||
|
||||
`IrisSlopeClip` (snippet `slope-clip`) has `minimumSlope` (default `0`) and `maximumSlope` (default `10`). The default clip accepts every slope and short-circuits before the slope stream is sampled. Slope is measured over a 3-block radius.
|
||||
|
||||
`caveCeilingLayers` ignores `slopeCondition` — cave roofs have no meaningful slope.
|
||||
|
||||
**`lockLayers` (mesa mode).** Instead of indexing the stack from the surface, Iris builds the full expanded layer stack once and then reads it with an offset derived from the column's own terrain height. Bands therefore stay at fixed world heights across the whole biome and line up horizontally into stripes, and the stack repeats cyclically rather than running out. `lockLayersMax` limits how deep the banded region goes before the rock palette takes over.
|
||||
|
||||
### Material palette (`IrisMaterialPalette`)
|
||||
|
||||
Snippet key: `palette`. The general-purpose weighted palette used for rock and fluid on the dimension, tree trunks and leaves, formation strata, ruin weathering, and procedural accents.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `palette` | `IrisBlockData[]` | stone | The blocks. Each entry is repeated `weight` times in the pick list |
|
||||
| `style` | `IrisGeneratorStyle` | `STATIC` | How the pick varies through space. Sampled in 3D, so a palette can band vertically as well as horizontally |
|
||||
| `zoom` | double >= 0.0001 | `5` | Scale of that variation |
|
||||
|
||||
Dimension-level palettes:
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `rockPalette` | stone | Fills every land cell below the biome layer stack. Swap it for deepslate or a stone/granite/andesite blend to change the entire underground |
|
||||
| `fluidPalette` | water | Ocean and river columns, and cave aquifers when `allowFluid` is on. Setting it to lava turns every ocean and aquifer into lava with no other change |
|
||||
| `rockZoom` | `5` | Scale of the rock palette variation |
|
||||
|
||||
### Block data and reusable `blocks/*.json`
|
||||
|
||||
`IrisBlockData` is the entry type used by every palette, plus decorators, deposits, drop filters, and object replacement rules. A vanilla id works with or without the `minecraft:` prefix.
|
||||
|
||||
```json
|
||||
{
|
||||
"block": "minecraft:oak_log",
|
||||
"weight": 2,
|
||||
"data": { "axis": "y" },
|
||||
"backup": { "block": "minecraft:spruce_log" },
|
||||
"debug": false
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `block` | `air` | A block id, or the load key of a file under `blocks/`. Required by the schema |
|
||||
| `weight` | `1` (1–1000) | How many times this entry appears in the containing palette's pick list. Weight 3 against weight 1 is a 3:1 split |
|
||||
| `data` | `{}` | Block-state properties such as `axis`, `waterlogged`, `facing`, `half` |
|
||||
| `backup` | `null` | Tried when the requested state does not resolve on this version. Without one, an unresolvable entry becomes air |
|
||||
| `debug` | `false` | Prints the resolved state to console when general debug logging is on. Use it when a palette silently produces air |
|
||||
| `tileData` | `{}` | Block-entity payload, applied only when the resolved state actually has a tile entity |
|
||||
|
||||
Files under `blocks/<key>.json` use the same shape and act as reusable aliases. Reference one with `"block": "<key>"`; properties on the referencing entry override properties from the alias. Aliases may chain but must not form cycles.
|
||||
|
||||
`tileData` is read from the entry being placed and does **not** inherit from a referenced alias, so put block-entity data on the referencing entry. For spawners, a legacy `data.entitySpawn` value is converted into `tileData.SpawnData.entity.id` before placement. Invalid tile data, or tile data on a block with no tile entity, produces no payload rather than an error.
|
||||
|
||||
### Surface example
|
||||
|
||||
```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`)
|
||||
|
||||
Snippet key: `decorator`. Biome field: `decorators`. The dimension must have `decorate: true` (the default).
|
||||
|
||||
### How a decorator is chosen
|
||||
|
||||
Decorators are bucketed by `partOf`, and each bucket is evaluated independently per column. Within a bucket:
|
||||
|
||||
1. Every decorator's noise field is sampled at that column. It passes when `noise(x/zoom, z/zoom)` fitted to 0..1 is at or below `chance`.
|
||||
2. Among all decorators that passed, **exactly one** is chosen, uniformly, seeded per column.
|
||||
|
||||
So `chance` is a share of the noise field, not an independent probability, and two decorators in the same bucket compete rather than stacking. Ten flower decorators at `chance: 0.1` do not carpet the ground — they subdivide roughly the same 10% of columns between them. Give distinct plants distinct `style` fields when you want them in visibly different patches.
|
||||
|
||||
### Where the block lands
|
||||
|
||||
The block is written one above the surface block (`height + 1`), and only into air. A palette entry carrying a `half` property is treated as a two-block plant: both `height + 1` and `height + 2` must be air or nothing is placed.
|
||||
|
||||
By default the surface block must have a sturdy full up-face. `forcePlace: true` skips that test entirely; `forceBlock` replaces the surface block with the given block first and implies `forcePlace`. When not force-placing, `whitelist` and `blacklist` are matched against the surface block. An explicitly empty `whitelist` matches nothing and blocks all placement — omit the field rather than setting it to `[]`.
|
||||
|
||||
Vines get their attachment faces recomputed against surrounding blocks. `minecraft:pointed_dripstone` gets `thickness` and `vertical_direction` assigned automatically along a stack (tip at the far end, then frustum, then base).
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `chance` | double 0..1 | `0.1` | Fraction of the noise field that qualifies. Raise it while debugging, then dial back |
|
||||
| `palette` | `IrisBlockData[]` | grass | Blocks to place. An empty resolved palette places nothing and never even tests the gate |
|
||||
| `topPalette` | `IrisBlockData[]` | `[]` | Used for the upper part of a stack — bamboo tips, cactus flowers. Empty falls back to `palette` |
|
||||
| `topThreshold` | double 0.01..1 | `1` | Normalized stack position where `topPalette` takes over. `0.8` gives a tip roughly a fifth of the stack tall |
|
||||
| `style` | `IrisGeneratorStyle` | `STATIC` | The field that gates `chance`. `STATIC` gives even scatter; wispy or cellular styles give meadows and bare patches |
|
||||
| `variance` | `IrisGeneratorStyle` | `STATIC` | Chooses between palette entries once a column has passed. Scattered variance mixes flowers per block; wispy variance gives single-species drifts |
|
||||
| `heightVariance` | `IrisGeneratorStyle` | `STATIC` | Shapes stack height across the terrain when `stackMin` and `stackMax` differ |
|
||||
| `stackMin` / `stackMax` | int 1..2032 | `1` / `1` | Stack height range. When they differ the resolved height is the noise fit **plus one**, so `1..4` actually produces stacks 2 to 5 tall |
|
||||
| `scaleStack` | boolean | `false` | Reinterprets `stackMin`/`stackMax` as a percentage of the available vertical space instead of a block count. Meant for cave stalagmites that should scale with cavern height |
|
||||
| `absoluteMaxStack` | int | `30` | Hard cap when `scaleStack` is on, so a huge cavern does not produce a 60-block column |
|
||||
| `partOf` | `IrisDecorationPart` | `NONE` | Which pass places this decorator. See the table below |
|
||||
| `forcePlace` | boolean | `false` | Ignores the sturdy-surface test, the slope clip, the whitelist, and the blacklist |
|
||||
| `forceBlock` | `IrisBlockData` | `null` | Replaces the surface block before placing. Implies `forcePlace` |
|
||||
| `whitelist` / `blacklist` | `IrisBlockData[]` | `null` | Allow/deny lists matched against the surface block. Omit rather than empty |
|
||||
| `slopeCondition` | `IrisSlopeClip` | 0..10 | Rejects columns outside the slope range. Not applied on the sea-surface or ceiling passes |
|
||||
|
||||
### Decoration parts (`IrisDecorationPart`)
|
||||
|
||||
| Value | Pass, and exactly when it fires |
|
||||
|-------|--------------------------------|
|
||||
| `NONE` | The ordinary surface pass, on every column. Also reused by the carve modifier for cave floors |
|
||||
| `SHORE_LINE` | Only where the terrain height equals `fluidHeight` exactly **and** at least one of the four cardinal neighbours is below the fluid line. This is a one-block-wide waterline ring, which is why sugar cane grows only at the edge |
|
||||
| `SEA_SURFACE` | Where the terrain is below the fluid line and there is water directly above the seafloor block. Places at `fluidHeight + 1`, so lily pads sit on top of the water |
|
||||
| `SEA_FLOOR` | Same gate as `SEA_SURFACE`, but writes into the first water block above the seafloor, replacing it. Use it for kelp, seagrass, and coral fans |
|
||||
| `CEILING` | Applied by the carve modifier at cave and overhang ceilings, stacking downward |
|
||||
|
||||
### Examples
|
||||
|
||||
Flower scatter with patchy dispersion:
|
||||
|
||||
```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, 2 to 5 blocks tall:
|
||||
|
||||
```json
|
||||
{
|
||||
"partOf": "SHORE_LINE",
|
||||
"chance": 0.18,
|
||||
"stackMin": 1,
|
||||
"stackMax": 4,
|
||||
"palette": [{ "block": "minecraft:sugar_cane" }],
|
||||
"style": { "style": "NOWHERE", "zoom": 0.65 }
|
||||
}
|
||||
```
|
||||
|
||||
A reusable snippet at `snippet/decorator/bush.json`, using an air-weighted palette to thin the result without lowering `chance`:
|
||||
|
||||
```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 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Weighting air into the palette is a useful trick: the column still wins the bucket contest, it just places nothing. That lets one decorator hold a patch against competing decorators while still reading as sparse.
|
||||
|
||||
## Deposits (`IrisDepositGenerator`)
|
||||
|
||||
Snippet key: `deposit`. Declared on **dimension**, **region**, and **biome**; all three lists run, so biome deposits add to regional and global ones rather than replacing them.
|
||||
|
||||
### What a deposit actually does
|
||||
|
||||
For each generator, once per chunk:
|
||||
|
||||
1. Roll `spawnChance` for the whole generator.
|
||||
2. Pick a clump count between `minPerChunk` and `maxPerChunk`. Each clump then rolls `perClumpSpawnChance` on its own.
|
||||
3. Pick one of `varience` pre-baked clump objects. A clump is a solid-ish blob of up to `maxSize` blocks inside a cube no larger than 11x11x11.
|
||||
4. Pick a random position in the chunk. The centre Y is drawn from the deposit band, clipped to the column's surface limit.
|
||||
5. Stamp the clump block by block.
|
||||
|
||||
A block is written only when the target is not air, not fluid, not carrying a cavern mark, and (unless `replaceBedrock`) not bedrock. That combination is why deposits never appear inside caves, in water, or floating in the open.
|
||||
|
||||
**Depth limits are stricter than the configured band.** The surface limit for any column is `terrainHeight - 7`, and the clump centre must sit at least 9 blocks below the surface. Individual clump blocks above their own column's `terrainHeight - 7` are skipped, which keeps veins from breaking through a slope.
|
||||
|
||||
**Deepslate conversion is automatic.** When no `depositVariants` rule matches, Iris converts the ore to the deepslate form of whatever block it is replacing. A vanilla pack usually does not need a manual deepslate remap at all; `depositVariants` exists for modded ores and for deliberate substitutions.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `minHeight` / `maxHeight` | int 0..8192 | `1` / `75` | **Engine-local** Y band for the clump centre, further clamped by the surface limit above |
|
||||
| `minSize` / `maxSize` | int 0..8192 | `0` / `128` | Blocks per clump. Sizes above about 1300 saturate the 11-cube and produce a solid block instead of a vein |
|
||||
| `minPerChunk` / `maxPerChunk` | int 0..2048 | `0` / `3` | Clumps attempted per chunk. This is the main frequency knob |
|
||||
| `spawnChance` | double 0..1 | `1` | Rolled once per chunk for the whole generator. Use it for rare deposits that should be absent from most chunks entirely |
|
||||
| `perClumpSpawnChance` | double 0..1 | `1` | Rolled per clump, thinning within a chunk rather than between chunks |
|
||||
| `palette` | `IrisBlockData[]` | required | Clump materials, picked uniformly per block. **`weight` is ignored here**, unlike every other palette — list a block twice to double it |
|
||||
| `varience` | int 1..64 | `3` | How many distinct clump shapes are baked. Low values make repeated vein silhouettes visible; the field name is spelled this way in code |
|
||||
| `replaceBedrock` | boolean | `false` | Allows overwriting bedrock |
|
||||
|
||||
### Biome ore multipliers
|
||||
|
||||
| Field | Range | Default | What it does |
|
||||
|-------|-------|---------|--------------|
|
||||
| `oreDepositFrequencyMultiplier` | 0..1 | `1` | Drops that fraction of ore clumps whose centre lands in this biome. `0.4` keeps 40%. It cannot increase frequency — the range stops at 1 |
|
||||
| `oreDepositSizeMultiplier` | 0.01..16 | `1` | Rescales ore clump block counts in this biome. This one can go up, to 16x |
|
||||
|
||||
Both apply only to deposits whose palette resolves to at least one ore block, and both are read from the **cave**-biome lookup at the clump position, which falls back to the surface biome above `caveMinDepthBelowSurface`. A deep cave biome can therefore enrich or starve ore at depth independently of the biome on the surface.
|
||||
|
||||
### Deposit variants (`IrisDepositVariant`)
|
||||
|
||||
Snippet key: `deposit-variant`. Rewrites ore ids at placement time inside an absolute world-Y band. Available on dimension, region, and biome. Order is biome rules, then region, then dimension; the first band that contains the block's world Y and has a matching source id wins.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `minHeight` / `maxHeight` | int -2048..8192 | `0` / `0` | Inclusive **absolute world Y**, not engine-local. This differs from the deposit's own band |
|
||||
| `remap` | map string→string | `{}` | Source block id to replacement block id. Source matching is by material only, so properties on the source key are ignored. Unresolvable ids on either side are dropped silently |
|
||||
|
||||
```json
|
||||
{
|
||||
"depositVariants": [
|
||||
{
|
||||
"minHeight": -64,
|
||||
"maxHeight": 0,
|
||||
"remap": {
|
||||
"yourmod:iron_ore": "yourmod:deepslate_iron_ore"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The dimension also exposes `hideOresForHiddenOre`. When true, every ore the generator would place — terrain ores, deposits, and ores inside objects — is emitted as its base material instead, so a drop-control plugin such as HiddenOre owns ore rewards. See `28 - Integrations.md`.
|
||||
|
||||
## Noise ores (`IrisOreGenerator`)
|
||||
|
||||
Separate from deposits and much cheaper: no clumps, no per-chunk budget, just a 3D noise test per cell during the terrain pass. Declared as `ores` on **dimension, region, and biome** (all three, despite the older docs listing only the dimension).
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `palette` | empty | Material palette for the ore. An empty palette makes the generator inert |
|
||||
| `chanceStyle` | `STATIC` | The 3D field tested against `threshold`. Cellular styles give clustered pockets; static gives evenly sprinkled specks |
|
||||
| `threshold` | `0.5` | The cell becomes ore when the noise value is at or below this. Higher means more ore |
|
||||
| `range` | `30..80` | Engine-local Y band. Cells outside it are skipped before the noise sample |
|
||||
| `generateSurface` | `false` | `false` runs the generator in the underground pass, after the biome layer stack has been exhausted, so it replaces rock. `true` runs it in a pass that executes **before** layers and fluid at every Y in the column, so it can overwrite soil, water, and anything else |
|
||||
|
||||
Precedence within a pass is biome, then region, then dimension; the first generator that returns a block wins. Y bands are precomputed, so generators outside the current Y are skipped without evaluating noise.
|
||||
|
||||
## Post-processing slabs and walls
|
||||
|
||||
The post modifier (stage 3) reads the biome `slab` and `wall` palettes and is gated by the dimension `postProcessingSlabs` and `postProcessingWalls` flags, both true by default.
|
||||
|
||||
- **Slabs** are added where at least one cardinal neighbour column is exactly one block higher and that neighbour's top block is a solid non-slab. The `slab` palette's `slopeCondition` is honoured, and a resolved air block cancels the placement. A snow slab is suppressed at or below the fluid line.
|
||||
- **Walls** are painted where a cardinal neighbour is three or more blocks lower, running down the exposed face until it hits air or water. This is the same `wall` palette the carve modifier uses for cave walls, so a biome that wants different cliff and cave rock needs a dedicated cave biome.
|
||||
|
||||
Both palettes default to empty, which disables the corresponding effect for that biome.
|
||||
|
||||
## Tuning order
|
||||
|
||||
Do these one at a time, on a focused biome that already produces correct height. Each stage makes a wrong palette, filter, or Y band independently visible.
|
||||
|
||||
**Surface.** Define one to three `layers` covering soil down to subsoil and leave stone to `rockPalette`. Add `wall` for cliff biomes and `seaLayers` for oceans. Use `lockLayers` only for mesa stripes. Inspect flat ground, a steep slope, an exposed cliff face, and an underwater column before moving on.
|
||||
|
||||
**Decorators.** Start with a single decorator, `STATIC` style, low `chance`. Confirm it appears, then switch to a wispy or cellular style to get patches. Add `partOf` variants for shore, sea, and ceiling content. Set `stackMin`/`stackMax` and `topPalette` for cane, cactus, and bamboo, remembering the resolved height is one taller than the fit. Extract repeated definitions into `snippet/decorator/*.json` (see `24 - Pack Mods & Snippets.md`). Always check somewhere the filter should *reject* the decorator, not just somewhere it should accept it.
|
||||
|
||||
**Deposits.** Put broad stone blobs and common ores on the dimension, regional minerals on regions, signature ores on biomes. Verify the band actually intersects generated terrain given the engine-local offset and the 9-block surface margin. Raise `varience` if repeated vein shapes are noticeable. Add `depositVariants` last, and only for ids automatic deepslate conversion does not already handle.
|
||||
|
||||
## Practical notes
|
||||
|
||||
- Decorators place blocks only. Trees, boulders, and structures come from object placements (`20 - Object Placement.md`) and procedural objects (`17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`).
|
||||
- One decorator per `partOf` bucket places per column. More decorators in a bucket means each appears less often, not more total coverage.
|
||||
- Deposit `varience` is not `variance`, and generator styles use `multiplicitive`, not `multiplicative`. Both spellings are the actual field names.
|
||||
- Deposit palettes ignore `weight`; every other palette in the pack honours it.
|
||||
- The dimension `explodeBiomePalettes` flag inserts barrier blocks between layer groups so you can count layer boundaries in a cross-section. It is a debug aid and should never ship enabled.
|
||||
@@ -1,453 +0,0 @@
|
||||
# 17 - Trees, Fungi, Coral, Crystals, Formations, Ruins
|
||||
|
||||
Procedural objects are structures Iris builds from JSON parameters instead of loading from `.iob` files. Each entry bakes a small pool of deterministic variant objects at engine start, then scatters them exactly like an object placement. Six families exist — trees, fungi, coral, crystals, formations, ruins — and they all live under `proceduralObjects` on a biome or a region.
|
||||
|
||||
Related: `12 - Regions.md`, `13 - Biomes.md`, `15 - Caves & Carving.md`, `16 - Surfaces, Decorators & Deposits.md`, `18 - Structures Overview.md`, `19 - Objects.md`, `20 - Object Placement.md`.
|
||||
|
||||
## The mental model
|
||||
|
||||
Two things happen at completely different times.
|
||||
|
||||
**Bake.** The first time an entry is touched — normally during the engine's cache warm-up — Iris runs the family's generator `variants` times and produces that many in-memory `IrisObject` instances. The bake is a pure function of the entry's fields and its `seed`, so the same JSON always produces byte-identical variants on every platform and every restart. An entry that bakes nothing (impossible dimensions, an empty palette) is silently skipped at placement time, so a family that never appears is worth checking in the logs first.
|
||||
|
||||
**Place.** During mantle object generation, per chunk, for each entry in scope:
|
||||
|
||||
1. Roll `chance` once, with a small ±0.005 jitter, for the whole entry.
|
||||
2. If it passes, make `density` attempts.
|
||||
3. Each attempt picks a random baked variant, picks a random X/Z inside the chunk (or searches for a cave anchor, see below), and hands the variant to the ordinary object placer with the entry's `mode`, `rotation`, `clamp`, `translate`, `underwater`, and stilt or vacuum settings.
|
||||
|
||||
So `chance` is per chunk and `density` is per chunk-that-passed. `chance: 0.5, density: 4` gives you four objects in half the chunks and none in the rest — clumpier than `chance: 1, density: 2`, which gives two everywhere.
|
||||
|
||||
Placement runs in the same mantle stage as `.iob` object placement, before the terrain blocks exist, which is why procedural objects can be anchored to carved cave space and why they respect the same surface-support rules as regular objects (`20 - Object Placement.md`).
|
||||
|
||||
**Scope.** Three lists are read per chunk, all resolved at the chunk centre: the surface biome's `proceduralObjects`, the region's, and the cave biome's (only when it differs from the surface biome). Everything in all three is evaluated; they add rather than override.
|
||||
|
||||
**Cost.** The mantle object component's radius grows to cover the largest baked variant across the whole pack. One 60-block formation therefore widens the generation footprint for every chunk in the world, not just the biome that uses it. Keep large shapes rare and large *entries* rarer.
|
||||
|
||||
## Walkthrough: procedural trees in a biome
|
||||
|
||||
Use a validating `OVERWORLD` pack with `useMantle` and `decorate` on. Save this as `biomes/tutorial/tree-test.json`, list `tutorial/tree-test` as a region land biome, and temporarily set the dimension `focus` to the same key.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Procedural Tree Test",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{ "palette": [{ "block": "minecraft:grass_block" }] }
|
||||
],
|
||||
"generators": [
|
||||
{ "generator": "flat", "min": 16, "max": 16 }
|
||||
],
|
||||
"proceduralObjects": {
|
||||
"trees": [
|
||||
{
|
||||
"name": "tutorial-oak",
|
||||
"chance": 1,
|
||||
"density": 2,
|
||||
"variants": 4,
|
||||
"seed": 9001,
|
||||
"trunk": "minecraft:oak_log",
|
||||
"leaves": "minecraft:oak_leaves",
|
||||
"profile": "OAK",
|
||||
"heightMin": 7,
|
||||
"heightMax": 11,
|
||||
"plausible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Reuse `generators/flat.json` from `26 - Example - Minimal Dimension.md`, validate the pack, and open Studio on seed `1337`.
|
||||
2. Fly out and generate fresh chunks. Success is two oaks per chunk, drawn from four distinct silhouettes, rooted on the grass, with leaves carrying normal decay distances.
|
||||
3. If nothing appears: confirm the biome is focused and the chunks are new; confirm `useMantle` is true; check the console for bake failures. Keep `chance: 1` until you have seen a tree.
|
||||
4. Break a leaf block and watch the canopy. With `plausible: true`, leaves more than six blocks from wood are permanently persistent, and everything closer decays normally when you cut the trunk.
|
||||
5. Tune height, profile, trunk shape, and canopy before touching `chance` and `density`. Reopen Studio and confirm the same seed reproduces the same four shapes, then drop `chance` to something forest-like and remove `focus`.
|
||||
|
||||
Variant heights are not random per variant. Iris spreads them evenly across `heightMin..heightMax` and adds up to ±30% of one step of jitter, so four variants over `7..11` give roughly 7, 8, 10, 11 rather than four coin flips. Raising `variants` therefore fills in the height range rather than just adding randomness.
|
||||
|
||||
## Container (`IrisProceduralObjects`)
|
||||
|
||||
Snippet key: `procedural-objects`. Valid on biomes and regions.
|
||||
|
||||
| Field | Type | Family |
|
||||
|-------|------|--------|
|
||||
| `trees` | `IrisProceduralTree[]` | Trunk plus canopy, optionally branches, roots and forks |
|
||||
| `fungi` | `IrisFungus[]` | Stem plus cap, or sideways shelf brackets |
|
||||
| `coral` | `IrisCoral[]` | Waterlogged reef structures in five forms |
|
||||
| `crystals` | `IrisCrystal[]` | Budding base with tapered shards; cave-first |
|
||||
| `formations` | `IrisFormation[]` | Natural rock landmarks with strata and erosion |
|
||||
| `ruins` | `IrisRuin[]` | Man-made shapes with weathering, erosion and burial |
|
||||
|
||||
```json
|
||||
{
|
||||
"proceduralObjects": {
|
||||
"trees": [ { "...": "..." } ],
|
||||
"crystals": [ { "...": "..." } ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Shared placement fields
|
||||
|
||||
Every family carries this same block of fields and converts them into an `IrisObjectPlacement` at placement time. Defaults differ per family, so the values below are noted where they diverge.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `name` | family name | Used in logs and as the variant load key. Must be unique within a pack if you want to identify variants in debug output |
|
||||
| `chance` | 0.4 (trees, fungi, coral), 0.2 (crystals), 0.05 (ruins), 0.02 (formations) | Probability the entry attempts anything at all in a given chunk |
|
||||
| `density` | `1` | Attempts once the chance roll passes. Raising this clusters objects; raising `chance` spreads them |
|
||||
| `variants` | 8 (trees), 6 (all others) | How many distinct shapes to bake, 1 to 64. Below about 4 the repetition is visible; above about 16 you are paying memory for variation nobody sees |
|
||||
| `seed` | `1337` | Bake seed. Change it to get an entirely different set of shapes from identical settings |
|
||||
| `mode` | `CENTER_HEIGHT`, except ruins `MIN_HEIGHT` | Terrain anchor mode. `MIN_HEIGHT` plants the lowest footprint corner, good for slabs and rubble on slopes; `CENTER_HEIGHT` averages, good for tall pillars |
|
||||
| `rotation` | identity | Rotates placements so variants do not all face the same direction |
|
||||
| `clamp` | unlimited | Min and max terrain height at which the entry may place |
|
||||
| `carvingSupport` | `SURFACE_ONLY`, except crystals `CARVING_ONLY` | `SURFACE_ONLY` places on terrain, `CARVING_ONLY` searches carved cave space, `ANYWHERE` uses the surface path without the surface-only rejection |
|
||||
| `underwater` | `false`, except coral `true` | Anchors on terrain height ignoring the water surface, so the object grows from the seafloor instead of the waterline |
|
||||
| `translate` | zero | XYZ offset. A negative Y sinks the object into the ground |
|
||||
| `stiltSettings` | unset | Configuration for `STILT`, `MIN_STILT`, `FAST_STILT`, `FAST_MIN_STILT`, `CENTER_STILT`, `ERODE_STILT`, `ORGANIC_STILT` |
|
||||
| `vacuumSettings` | unset | Configuration for `VACUUM`, `VACUUM_HIGH`, `VACUUM_FAST`, `VACUUM_ORGANIC`, `VACUUM_WAVY` |
|
||||
| `surfaceSupportBuffer` | `3` (formations only) | Solid ground required around the footprint. Iris uses the larger of this and the dimension `objectSurfaceSupportBuffer` |
|
||||
|
||||
Variant load keys are `procedural/tree/<name>#<i>` for trees and `procedural/<name>#<i>` for every other family.
|
||||
|
||||
Only `CARVING_ONLY` entries take the cave path. Those search the chunk for an anchor using the active cave profile's `defaultObjectAnchor`, `anchorScanStep`, `anchorSearchAttempts`, and `objectMinDepthBelowSurface`; an entry that finds no anchor is skipped for that attempt. The profile's `defaultObjectPlaceMode` overrides the entry's `mode`, but only when the entry left `mode` at the default `CENTER_HEIGHT`. See `15 - Caves & Carving.md`.
|
||||
|
||||
`plausible` is a **tree-only** field. Every other family reports `false`, which means their objects are placed with decay prevention active. This matters only for blocks that have leaf-style decay properties.
|
||||
|
||||
## Procedural trees (`IrisProceduralTree`)
|
||||
|
||||
Snippet key: `procedural-tree`. Built by the trunk builder (which returns one or more limbs), then a canopy per limb, then decorators, then roots, then leaf plausibility.
|
||||
|
||||
### Materials and size
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `trunk` | `minecraft:oak_log` | The log block. Ignored entirely when `trunkPalette` is set |
|
||||
| `trunkPalette` | unset | Noise-driven palette for the trunk, for streaked or mixed-species wood |
|
||||
| `leaves` | `minecraft:oak_leaves` | The leaf block. Ignored when `leavesPalette` is set |
|
||||
| `leavesPalette` | unset | Noise-driven leaf palette. Only entries that are genuinely leaves receive decay distances |
|
||||
| `plausible` | `true` | See below |
|
||||
| `heightMin` / `heightMax` | `8` / `12` | Trunk height range spread across the variant pool. Heights below 2 are clamped up |
|
||||
| `trunkWidth` | `1` | Base thickness. 1 is a single column, 2 is a 2x2, 3 a 3x3 |
|
||||
| `profile` | `OAK` | Named silhouette driving default crown radii and layer placement |
|
||||
|
||||
`IrisTreeProfile`: `OAK`, `BIRCH`, `SPRUCE`, `JUNGLE`, `ACACIA`, `DARK_OAK`, `DARK_OAK_FLAT`, `DARK_OAK_FLAT_WIDE`, `CHERRY`, `PALM`, `WILLOW`, `COLUMNAR`, `BUSH`, `MEGA_SPRUCE`.
|
||||
|
||||
**What `plausible` actually does.** With `true`, Iris runs a breadth-first search from the trunk through the leaf volume. Leaves within 6 steps of wood get `persistent=false` and their real `distance`, so vanilla decay works exactly as it would on a grown tree. Leaves 7 or more steps away, or unreachable entirely, get `persistent=true` and `distance=7` so they never vanish. It also runs a support pass that prunes orphaned leaf clumps. With `false`, every leaf is forced `persistent=true, distance=1` — a raw dump that never decays and reads as built rather than grown.
|
||||
|
||||
### Trunk shaping
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `trunkShape` | `CONSTANT` | Function mapping normalized height to a trunk width multiplier. `TAPER`-like effects come from `LINEAR` with `shapeEnd` below 1 |
|
||||
| `shapeStart` / `shapeEnd` | `1` / `1` | Width multipliers at base and top for `LINEAR` |
|
||||
| `shapeSteepness` | `5` | Transition sharpness for `SIGMOID` |
|
||||
| `shapeBase` | `2.718281828` | Logarithm base for `LOG` |
|
||||
| `shapePeriod` / `shapeAmplitude` | `1` / `0.2` | Wobble frequency and depth for `SINE`, for lumpy or knotted trunks |
|
||||
| `shapePeakOffset` / `shapeFloor` | `0.5` / `0.5` | For `PARABOLIC`: where the waist sits (0 base, 1 top) and how thin it gets |
|
||||
| `leanAngle` | `0` | Degrees off vertical. Non-zero produces a leaning trunk; combine with rotation for wind-swept stands |
|
||||
| `leanAzimuth` | `0` | Compass direction of the lean |
|
||||
| `trunkCurve` | `LINEAR` | How lean accumulates over height. `SIGMOID` bends mostly in the middle; `CONSTANT` shears uniformly |
|
||||
| `curveSteepness` | `8` | Sharpness of that bend for `SIGMOID` |
|
||||
| `leanAzimuthMode` | `CONSTANT` | Lets the lean *direction* change with height, which is what turns a lean into a spiral or a wander |
|
||||
| `azimuthStart` / `azimuthEnd` | `0` / `0` | Endpoints for `LINEAR` azimuth |
|
||||
| `azimuthTurns` | `1` | Full rotations over the trunk for `SPIRAL` |
|
||||
| `azimuthAmplitude` / `azimuthPeriod` / `azimuthOffset` | `90` / `1` / `0` | Sine azimuth wobble controls, for S-curved trunks |
|
||||
| `azimuthScale` | `1` | Noise scale for `NOISE` azimuth |
|
||||
| `azimuthWhorlCount` | `5` | Positions per ring for `WHORL` azimuth |
|
||||
| `trunkForks` | `1` (1–6) | Splits the trunk into this many limbs above `forkHeight`. Each limb gets its own canopy |
|
||||
| `forkHeight` | `0.5` | Normalized height of the split |
|
||||
| `forkAngle` | `25` | Degrees each fork leans outward |
|
||||
| `secondaryTrunk` | unset | Optional second wood block for a banded trunk. Ignored when `secondaryTrunkPalette` is set |
|
||||
| `secondaryTrunkPalette` | unset | Noise palette for that band |
|
||||
| `secondaryTrunkStart` / `secondaryTrunkEnd` | `0.5` / `1` | Normalized band bounds |
|
||||
| `roots` | `true` | Builds a root system so the tree meets uneven ground instead of hovering |
|
||||
| `rootStyle` | `BUTTRESS` | `TAPROOT` drives one thick root down, `BUTTRESS` flares several out at the base, `STILT` lifts the trunk on legs |
|
||||
| `rootDepth` | `0` | Explicit reach in blocks; 0 scales automatically with tree height |
|
||||
| `rootFlare` | `0` | Explicit flare radius; 0 scales automatically |
|
||||
|
||||
`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`)
|
||||
|
||||
Snippet key: `tree-canopy`. Stacked discs, sized by the profile unless you override them.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `startAngle` | `90` | Elevation of each disc in degrees. Exactly 90 is a flat disc, below 90 domes downward toward a sphere, above 90 flares out into an umbrella |
|
||||
| `squish` | `1` | Vertical scale of the crown volume. Below 1 flattens it |
|
||||
| `mode` | `TRIMMED` | How each disc fills with leaves |
|
||||
| `leafDensity` | `0.85` | Fill probability for the `DENSITY` and `NOISE` modes |
|
||||
| `crownStretchX` / `crownStretchZ` | `1` / `1` | Elliptical crowns for wind-shaped or asymmetric trees |
|
||||
| `layers` | `[]` | Explicit discs. Any entry here replaces the profile-driven layers entirely |
|
||||
| `branches` | unset | When set, branches build most of the canopy and only the topmost profile disc is still placed |
|
||||
|
||||
`IrisTreeLayer` (snippet `tree-layer`) is `yOffset` (blocks above the trunk base, default `0`) and `radius` (default `2`).
|
||||
|
||||
`IrisTreeLeafMode`: `TRIMMED`, `FILLED`, `DENSITY`, `NOISE`, `HOLLOW`, `GRADIENT`, `CLUMPED`, `TATTERED`, `SPARSE`.
|
||||
|
||||
### Branches (`IrisTreeBranches`)
|
||||
|
||||
Snippet key: `tree-branches`. Adding a `branches` object switches the tree from a stack of leaf discs to real limbs with leaf balls at their tips — the difference between a vanilla oak and an old-growth silhouette.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `probabilityFunction` | `TOP_HEAVY` | Where branches spawn along the trunk. `TOP_HEAVY` for a crown, `PERIODIC` for conifer whorls, `BAND` for a single tier |
|
||||
| `probabilityConstant` | `0.5` | Chance for `CONSTANT` |
|
||||
| `probabilityBase` / `probabilityCrown` | `0` / `1` | Endpoints for `LINEAR` |
|
||||
| `probabilitySteepness` / `probabilityMidpoint` | `10` / `0.7` | `SIGMOID` sharpness and where the crown starts |
|
||||
| `probabilityExponent` | `2` | `TOP_HEAVY` bias; higher pushes branches further up |
|
||||
| `probabilityMean` / `probabilityStd` | `0.7` / `0.15` | `GAUSSIAN` centre and spread, for a single dense tier |
|
||||
| `probabilityScale` | `1` | Noise scale for `NOISE` |
|
||||
| `probabilityPeriods` | `5` | Number of whorl rings for `PERIODIC` |
|
||||
| `lengthFunction` | `LINEAR` | How branch length varies with height. `LINEAR` with a large `lengthCrown` gives the classic wide top |
|
||||
| `lengthBase` / `lengthCrown` | `1` / `4` | Endpoints for `LINEAR` |
|
||||
| `lengthConstant` / `lengthMax` | `3` / `4` | Value for `CONSTANT`, and the ceiling for `SIGMOID`, `LOG` and `PARABOLIC` |
|
||||
| `lengthSteepness` | `5` | `SIGMOID` length sharpness |
|
||||
| `azimuthMode` | `RANDOM` | Compass distribution of branches. `GOLDEN_ANGLE` gives even spiral phyllotaxis, `WHORL` gives rings |
|
||||
| `azimuth` | `0` | Fixed direction when `azimuthMode` is `CONSTANT` |
|
||||
| `elevation` | `0` | Starting angle from horizontal. Positive points up, negative droops |
|
||||
| `sag` | `0` | Catenary droop along the branch. Small values read as weight; large values give willow arcs |
|
||||
| `branchDepth` | `1` (0–6) | Recursion levels. 2 and above produce fractal branching and a much larger block count |
|
||||
| `leafStartUp` | `false` | Clamps primary branches so they never droop below horizontal |
|
||||
| `clusterRadius` | `2` | Leaf ball radius at each branch tip |
|
||||
| `clusterMode` | `TRIMMED` | Fill mode for that ball |
|
||||
| `clusterDensity` | `0.85` | Fill probability when `clusterMode` is density- or noise-based |
|
||||
| `subBranches` | unset | One extra level of sub-branches from each tip |
|
||||
|
||||
`IrisTreeBranchProbability`: `CONSTANT`, `LINEAR`, `SIGMOID`, `TOP_HEAVY`, `GAUSSIAN`, `NOISE`, `BOTTOM_HEAVY`, `PERIODIC`, `BAND`, `INVERSE_GAUSSIAN`, `EXPONENTIAL_DECAY`.
|
||||
|
||||
`IrisTreeSubBranches` (snippet `tree-sub-branches`): `count` (`1`), `pitchDelta` (`0`, positive bends up), `yawDelta` (`45`, horizontal fan spread), `lengthScale` (`0.5` of the parent), `sag` (`0`), `clusterRadius` (`1`), `clusterMode` (`TRIMMED`), `clusterDensity` (`0.85`).
|
||||
|
||||
### Accents
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `secondaryLeaves` | unset | A single accent block scattered through the canopy — blossoms, shroomlight, berries |
|
||||
| `weightedSecondaryLeaves` | `[]` | Weighted list of accent blocks (`block` plus `weight`), overriding the single block |
|
||||
| `secondaryLeavesPalette` | unset | Noise palette, overriding both of the above |
|
||||
| `secondaryLeafFraction` | `0.35` | Share of leaves replaced by the accent. Values near 1 recolour the whole crown |
|
||||
| `decorators` | `[]` | `IrisTreeDecorator` entries applied after the tree is built |
|
||||
|
||||
### Tree decorator (`IrisTreeDecorator`)
|
||||
|
||||
Snippet key: `tree-decorator`.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `target` | `BRANCH_TIP` | Which set of positions is eligible |
|
||||
| `block` | required | Block id to place. Ignored when `palette` is set |
|
||||
| `palette` | unset | Noise palette, wins over `block` |
|
||||
| `chance` | `0.5` | Per eligible position. Use low values for sparse fruit, 1 for full coverage such as snow on the crown |
|
||||
| `length` | `1` | Maximum downward strand length for `CANOPY_HANG`; each column picks 1 to `length` |
|
||||
| `axisAware` | `false` | Orients the block's facing away from the trunk, for fences, gates and banners mounted on wood |
|
||||
|
||||
Targets: `BRANCH_TIP`, `TRUNK_SURFACE`, `CANOPY_TOP`, `CANOPY_BOTTOM`, `TRUNK_BASE`, `LEAF_SURFACE`, `CANOPY_HANG`, `BRANCH_SURFACE`, `TRUNK_TOP`, `GROUND_SCATTER`.
|
||||
|
||||
Branch endpoints are only collected when at least one decorator exists, so an empty `decorators` list costs nothing.
|
||||
|
||||
### A complete tree
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fungi (`IrisFungus`)
|
||||
|
||||
Snippet key: `fungus`. A stem column with a cap grown on top, or a sideways shelf bracket.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `stem` / `stemPalette` | `minecraft:mushroom_stem` | Stem material; palette wins |
|
||||
| `cap` / `capPalette` | `minecraft:red_mushroom_block` | Cap material; palette wins |
|
||||
| `stemHeightMin` / `stemHeightMax` | `5` / `9` | Stem height range spread over the variant pool |
|
||||
| `stemWidth` | `1` (1–3) | 1 is a single column, 3 a chunky trunk |
|
||||
| `stemCurve` | `0` | Degrees of lean off vertical |
|
||||
| `stemLeanAzimuth` | `0` | Direction of that lean |
|
||||
| `stemWaveAmplitude` | `0.4` | Blocks of sideways wobble up the stem, so it is not a ruler |
|
||||
| `stemWavePeriods` | `1` | Full sine wobbles over the stem height |
|
||||
| `capShape` | `DOME` | `DOME`, `FLAT`, `FUNNEL`, `CONICAL`, `FLAT_WIDE` |
|
||||
| `capRadiusMin` / `capRadiusMax` | `3` / `5` | Cap radius from centre to rim |
|
||||
| `capThickness` | `1` (1–3) | Shell thickness. 1 is a thin skin, 3 a fleshy slab |
|
||||
| `capSquish` | `0.4` | Vertical flatten, 0 full height and 1 a flat disc |
|
||||
| `capDroop` | `20` | Degrees the rim curls toward the ground |
|
||||
| `capOverhang` | `2` | Blocks the cap extends past the stem before the rim begins |
|
||||
| `gillBlock` / `gillPalette` | unset | Underside layer — gills, or shroomlight for a glowing cap |
|
||||
| `gillChance` | `0.85` | Share of underside blocks replaced when a gill block is set |
|
||||
| `spotBlock` / `spotPalette` | unset | Speckles across the cap top |
|
||||
| `spotChance` | `0.18` | Share of top blocks replaced, selected by value noise so spots cluster |
|
||||
| `shelf` | `false` | Switches to a bracket polypore: a flat sideways fan off a very short or absent stem |
|
||||
| `shelfRadius` | `3` | Fan radius in shelf mode |
|
||||
|
||||
## Coral (`IrisCoral`)
|
||||
|
||||
Snippet key: `coral`. Defaults to `underwater: true` and `waterlogged: true`, so it grows from the seafloor and stays alive.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `waterlogged` | `true` | Forces every waterloggable block in the structure waterlogged. Set false for dead, dry coral on a beach |
|
||||
| `form` | `BRANCHING` | `BRANCHING`, `FAN`, `BRAIN`, `PILLAR`, `TENDRIL`. Each runs a different generator |
|
||||
| `block` / `blockPalette` | `minecraft:tube_coral_block` | Structural body; a palette mixes tube/brain/bubble/fire/horn tones across one reef |
|
||||
| `tipBlock` / `tipPalette` | unset | Placed at branch tips and the top — fans, sea pickles |
|
||||
| `tipChance` | `0.6` | Per eligible tip position |
|
||||
| `heightMin` / `heightMax` | `4` / `8` | Overall height |
|
||||
| `spread` | `3` | Horizontal reach. Arm length for `BRANCHING`, base footprint for the others |
|
||||
| `sway` | `0.5` | Lateral wobble, 0 ruler-straight and 1 heavily wandering |
|
||||
| `branchCount` | `4` (1–12) | `BRANCHING`: arms off the central stalk |
|
||||
| `branchLength` | `3` | `BRANCHING`: arm length before the tip |
|
||||
| `branchElevation` | `55` | `BRANCHING`: degrees up from horizontal. 90 is straight up |
|
||||
| `branchAzimuth` | `GOLDEN_ANGLE` | `BRANCHING`: `GOLDEN_ANGLE`, `EVEN`, or `RANDOM` distribution around the stalk |
|
||||
| `subBranches` | `true` | `BRANCHING`: split each arm once for a bushier reef |
|
||||
| `subBranchCount` | `2` (1–5) | Sub-arms per arm |
|
||||
| `subBranchScale` | `0.5` | Sub-arm length as a fraction of the parent |
|
||||
| `tipClusterRadius` | `1` (0–4) | `BRANCHING` and `PILLAR`: tip cluster size |
|
||||
| `brainRadius` | `3` (1–8) | `BRAIN`: blob radius |
|
||||
| `brainRoughness` | `0.35` | `BRAIN`: surface wrinkling. 0 is a smooth dome |
|
||||
| `pillarRadius` | `1` (1–12) | `PILLAR`: column radius |
|
||||
| `fanWidth` | `3` (1–8) | `FAN`: half-width of the upright plane |
|
||||
| `tendrilCount` | `4` (1–12) | `TENDRIL`: number of thin wavy stalks |
|
||||
|
||||
## Crystals (`IrisCrystal`)
|
||||
|
||||
Snippet key: `crystal`. A budding base blob with tapered shards radiating from it. Defaults to `carvingSupport: CARVING_ONLY` and `chance: 0.2`, so it needs carved cave space to place at all — see `15 - Caves & Carving.md` for the anchor settings that govern it.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `growthSurface` | `FLOOR` | `FLOOR` points shards up, `CEILING` down, `WALL` outward. This orients the baked geometry only; use the cave anchor mode to actually land it on a ceiling |
|
||||
| `block` / `blockPalette` | `minecraft:amethyst_block` | Shard body. A palette mixes amethyst, calcite and tinted glass into one prismatic cluster |
|
||||
| `tipBlock` / `tipPalette` | unset | Different block at the very point of each shard |
|
||||
| `tipChance` | `0.6` | Per shard |
|
||||
| `glow` | `false` | With no tip block set, sprinkles `glowBlock` among the tips instead |
|
||||
| `glowBlock` | `minecraft:glowstone` | The light source used by `glow` |
|
||||
| `baseBlock` / `basePalette` | `minecraft:budding_amethyst` | The blob the shards grow from |
|
||||
| `baseRadius` | `1.6` | Blob radius. 0 makes shards spring from a single point |
|
||||
| `baseNoise` | `0.35` | Surface lumpiness of the blob, so it is not a clean sphere |
|
||||
| `shardCountMin` / `shardCountMax` | `5` / `11` | Shards per cluster |
|
||||
| `shardLengthMin` / `shardLengthMax` | `3` / `8` | Shard length from base to tip |
|
||||
| `shardBaseRadius` | `1.4` | Thickness at the shard's base end |
|
||||
| `shardTaper` | `0.85` | How aggressively it narrows. 0 is a near-constant column, 1 a sharp spike. Every shard ends in a single block regardless |
|
||||
| `spreadAngle` | `45` | Half-angle of the cone the shards fan within. 0 makes them all parallel; large values give a starburst |
|
||||
| `distribution` | `GOLDEN_ANGLE` | `GOLDEN_ANGLE` for an evenly spaced rosette, `RANDOM` for a chaotic clump |
|
||||
| `jitter` | `0.25` | Angular randomness on top of the distribution, so the cluster never looks mechanical |
|
||||
|
||||
## Formations (`IrisFormation`)
|
||||
|
||||
Snippet key: `formation`. Natural rock landmarks. Default `chance: 0.02` — these are meant to be rare, and they are the family most likely to widen the pack's mantle radius.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `form` | `SPIRE` | `SPIRE`, `HOODOO`, `ARCH`, `SEA_STACK`, `BOULDER`, `BASALT_COLUMN` |
|
||||
| `block` / `blockPalette` | `minecraft:stone` | Main rock body |
|
||||
| `capBlock` / `capPalette` | unset | Caprock on the crown, and the overhanging cap for `HOODOO`. Unset means the main rock everywhere |
|
||||
| `strataPalette` | unset | Horizontal colour bands. Every `strataThickness` blocks the palette advances, which is what produces the badlands look |
|
||||
| `strataThickness` | `3` (1–32) | Band thickness |
|
||||
| `heightMin` / `heightMax` | `14` / `26` | Total height |
|
||||
| `baseWidthMin` / `baseWidthMax` | `3` / `6` | Base radius |
|
||||
| `topWidth` | `0` | Radius at the very top before the profile applies. 0 tapers to a point |
|
||||
| `profile` | `TAPER` | `CONSTANT`, `LINEAR`, `TAPER`, `PARABOLIC`, `BULGE` — how the radius changes with height |
|
||||
| `profileWaist` | `0.55` | Normalized height of the pinch for `PARABOLIC`, used by hoodoos |
|
||||
| `profileWaistFloor` | `0.35` | Minimum radius fraction at that waist. Lower pinches tighter |
|
||||
| `lean` | `0` | Degrees off vertical; the whole body is sheared |
|
||||
| `leanAzimuth` | `0` | Lean direction |
|
||||
| `roughness` | `0.3` | 3D noise perturbation of the radius, 0 clean and 1 heavily eroded. This is the main "does it look like rock" control |
|
||||
| `jitter` | `0.15` | Per-block surface noise that adds and removes isolated edge blocks |
|
||||
| `surfaceSupportBuffer` | `3` (0–16) | Solid ground required beyond the lowest footprint blocks. Iris takes the larger of this and the dimension `objectSurfaceSupportBuffer` |
|
||||
| `hoodooCapRadius` | `3` | `HOODOO`: extra cap radius for the mushroom overhang. 0 disables it |
|
||||
| `hoodooCapHeight` | `3` (1–6) | `HOODOO`: cap slab thickness |
|
||||
| `archSpan` | `10` | `ARCH`: gap width between the legs |
|
||||
| `archThickness` | `3` | `ARCH`: leg and span thickness |
|
||||
| `basaltColumns` | `5` (2–12) | `BASALT_COLUMN`: columns per cluster |
|
||||
| `basaltColumnRadius` | `1` | `BASALT_COLUMN`: radius of each column |
|
||||
| `basaltHeightVariance` | `0.45` | `BASALT_COLUMN`: how much column heights differ, 0 all equal and 1 highly varied |
|
||||
|
||||
## Ruins (`IrisRuin`)
|
||||
|
||||
Snippet key: `ruin`. Man-made shapes that are weathered, eroded, and partly buried. Default `chance: 0.05` and `mode: MIN_HEIGHT`.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `form` | `PILLAR` | `PILLAR` broken column, `WALL` gapped segment, `ARCH` two legs and a span, `FLOOR_SLAB` foundation patch, `RUBBLE` low scattered pile |
|
||||
| `block` / `blockPalette` | `minecraft:cobblestone` | Bulk material before weathering |
|
||||
| `heightMin` / `heightMax` | `4` / `9` | Structure height, or slab thickness and mound height for the flat forms |
|
||||
| `widthMin` / `widthMax` | `1` / `3` | Footprint along X |
|
||||
| `lengthMin` / `lengthMax` | `3` / `7` | Footprint along Z |
|
||||
| `weatheredBlock` | `minecraft:mossy_cobblestone` | The weathered swap, used when no palette is set |
|
||||
| `weatheringPalette` | unset | Palette of weathered variants, overriding the single block |
|
||||
| `mossiness` | `0.45` | Share of the structure that weathers. The mask is noise-driven and biased toward lower rows, so moss climbs from the ground |
|
||||
| `weatheringScale` | `1.0` (0–8) | Weathering noise scale. Higher gives busy speckles, lower gives broad mossy zones |
|
||||
| `erosion` | `0.25` | How crumbled it is. Blocks below this noise threshold are deleted. The bottom row and core legs are never eroded, so the shape does not collapse into confetti |
|
||||
| `erosionScale` | `1.5` (0–8) | Erosion noise scale. Higher knocks out small holes, lower carves large missing chunks |
|
||||
| `buriedFraction` | `0.2` | Fraction of the height that sits below the surface, so the ruin reads as settled |
|
||||
| `accents` | `[]` | `IrisRuinDecorator` entries applied after erosion |
|
||||
|
||||
### Ruin decorator (`IrisRuinDecorator`)
|
||||
|
||||
Snippet key: `ruin-decorator`.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `target` | `TOP` | `TOP` sits on the highest block of each column, `SURFACE` clings to air-facing vertical faces, `BASE_SCATTER` rings the ground around the base |
|
||||
| `block` | required | Block id. Ignored when `palette` is set |
|
||||
| `palette` | unset | Noise palette, wins over `block` |
|
||||
| `chance` | `0.4` | Per candidate position |
|
||||
| `scatterRadius` | `2` | For `BASE_SCATTER`, how far beyond the footprint the ring extends |
|
||||
|
||||
## Sapling overrides (`IrisTree`) — a different system
|
||||
|
||||
`IrisTree` lives on **object placements** (`IrisObjectPlacement.trees`), not under `proceduralObjects`. It maps a grown sapling to that placement's objects. This is a gameplay growth replacement, not worldgen scatter, and it never touches procedural trees.
|
||||
|
||||
| Field | What it does |
|
||||
|-------|--------------|
|
||||
| `treeTypes` | Bukkit `TreeType` names this placement replaces, matched case-insensitively |
|
||||
| `sizes` | `IrisTreeSize` entries (`width` by `depth`) describing the sapling footprints it applies to. Width and depth are matched either way round, so a 1x2 entry also matches 2x1 |
|
||||
| `anyTree` | Declared but never read at runtime. Matching is by `treeTypes` only |
|
||||
| `anySize` | Declared but never read at runtime. Matching is by `sizes` only |
|
||||
|
||||
Dimension `treeSettings` gates the whole feature:
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `enabled` | `false` | Off by default. Nothing replaces grown trees until this is true |
|
||||
| `mode` | `FIRST` | `FIRST` uses biome matches and only falls back to region matches when the biome has none. `ALL` pools biome and region matches and picks randomly from the combined list. Dimension-level object placements are not consulted in either mode |
|
||||
|
||||
## Extending to the other families
|
||||
|
||||
1. Pick the family that matches the shape: trees for forests, fungi for mushroom biomes, coral for warm oceans, crystals for cave biomes, formations for deserts and coastlines, ruins for sparse land.
|
||||
2. Add one entry with a single material, `chance: 1`, `density: 1`, `variants: 4`, and a fixed `seed`. Focus the biome and generate.
|
||||
3. Get the silhouette right before touching frequency. Dimensions, profile, and roughness all change what the thing *is*; chance and density only change how often you meet it.
|
||||
4. Add palettes, accents, and decorators once the shape holds up from several angles.
|
||||
5. Match `carvingSupport` to the environment. Cave props also want a stilt place mode, either on the entry or through the cave profile's `defaultObjectPlaceMode`.
|
||||
6. Drop `chance` to production values, remove the dimension `focus`, and verify the family stays inside the biomes and regions that declare it.
|
||||
|
||||
The pass condition: the same seed reproduces the same shapes across a Studio restart, the placement leaves believable negative space, and no variant is skipped in the log. When exact hand-authored geometry matters more than variation, use an `.iob` object instead (`19 - Objects.md`).
|
||||
|
||||
## Practical notes
|
||||
|
||||
- Procedural objects are independent of `objects` placements, decorators, and jigsaw structures. Nothing is shared except the placement machinery.
|
||||
- `plausible` exists only on trees. The other five families always place with decay prevention active.
|
||||
- `IrisTree` (sapling to object) and `IrisProceduralTree` (worldgen bake) are unrelated types with similar names.
|
||||
- An entry whose bake produces no blocks is skipped silently at placement time. If a family never appears and `chance: 1` did not help, look for bake warnings in the console.
|
||||
- Every baked variant enlarges the mantle object radius for the entire engine. A single very large formation costs generation time everywhere, not just where it spawns.
|
||||
@@ -1,174 +0,0 @@
|
||||
# 18 - Structures Overview
|
||||
|
||||
Iris puts built content into the world three different ways: single `.iob` objects stamped by chance, Iris jigsaw structures assembled from many pieces, and native structures generated by Minecraft's own machinery. They share pack folders and some JSON, but they are separate systems with different authoring tools and different failure modes. This page picks one; the linked guides do the work.
|
||||
|
||||
## The short version
|
||||
|
||||
Ask what the thing you want actually is.
|
||||
|
||||
- **One build, repeated.** A rock, a tree, a ruined tower, a bandit camp — always the same geometry, scattered around. That is an **object**.
|
||||
- **A build that is different every time.** A village that grows its own road network, a stronghold with rooms and corridors, a fort whose towers vary. That is an **Iris jigsaw structure**.
|
||||
- **Something Minecraft already knows how to build.** A vanilla village, a datapack's town from Terralith or Towns and Towers, an ocean monument with its real guardians and loot. That is a **native structure**, and your job is to control it, not rebuild it.
|
||||
|
||||
Do not convert between systems as a first move. Objects are not "small jigsaws", and a native structure re-imported as objects loses its processors, entities, and spawners.
|
||||
|
||||
## 1. Objects
|
||||
|
||||
An object is one `.iob` file and one entry in a biome's or region's `objects[]` array. The engine rolls a chance per chunk, then attempts that many stamps at random columns. Objects have by far the richest terrain-fitting tools — stilts, terrain vacuuming, surface painting, cave floor and ceiling anchors, snow, block find-and-replace, loot, and entity markers.
|
||||
|
||||
Reach for an object when the geometry is fixed and you mostly care about *where* and *how it sits*.
|
||||
|
||||
Example — scatter one boulder through a biome, seated one block into the ground:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"place": ["clutter/boulder1", "clutter/boulder2"],
|
||||
"chance": 0.08,
|
||||
"density": 2,
|
||||
"mode": "CENTER_HEIGHT",
|
||||
"translate": { "y": -1 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Building and saving the `.iob` is `19 - Objects.md`. Every placement field is `20 - Object Placement.md`.
|
||||
|
||||
Objects attach to biomes and regions only. A dimension has no `objects[]`.
|
||||
|
||||
## 2. Iris jigsaw structures
|
||||
|
||||
A jigsaw structure is a set of pieces — each piece is itself an object — that the assembler snaps together through matching connectors until it runs out of depth, space, or candidate pieces. Villages, forts, and strongholds are the shape of the problem. You author the pieces and the graph in Jigsaw Studio, and the saved resources run on every platform through the shared core assembler.
|
||||
|
||||
Two layout modes:
|
||||
|
||||
- `PLANAR_JIGSAW` constrains pieces to a grid, which is what you want for roads, halls, and courtyards. Iris classifies every piece by which of its north/east/south/west sides carry connectors, giving six rotation-independent archetypes: blank, end, straight, corner, tee, and cross. Each archetype gets its own workcell with its own width, height, depth, and enabled flag — they do not have to match each other and they do not have to be square.
|
||||
- `SPATIAL_JIGSAW` is freeform in all three axes, for towers, shafts, stacked rooms, and stronghold-style sprawl. It uses one shared workcell.
|
||||
|
||||
Separately from the mode, a structure declares a compatibility contract. `IRIS_EXTENDED` (the default) gets the full Iris feature set. `VANILLA_PORTABLE` restricts the project so `/iris jigsaw export` can emit it as a real Minecraft 26.2 datapack — the exporter is strict and rejects the project outright rather than quietly dropping anything it cannot represent.
|
||||
|
||||
Example — create a planar project and place the finished structure from a region:
|
||||
|
||||
```text
|
||||
/iris jigsaw create dimension=<pack> key=forts/border mode=planar width=15 height=15 depth=15
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"structures": ["forts/border"],
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 48,
|
||||
"separation": 16
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The whole workflow is `21 - Jigsaw Structures.md`.
|
||||
|
||||
`/iris jigsaw` is Bukkit-only and player-only. Fabric, Forge, and NeoForge generate saved jigsaw structures perfectly well — they just have no authoring commands, so author on a Bukkit server and ship the pack.
|
||||
|
||||
## 3. Native structures
|
||||
|
||||
Native means anything in Minecraft's live structure registry: vanilla, a datapack, or a mod. Minecraft builds it, so you get full fidelity — processors, entities, spawners, and real loot tables — and Iris's job is to decide whether it generates, where, and how it meets Iris terrain.
|
||||
|
||||
Four things you can do:
|
||||
|
||||
- Leave it generating natively and fix how it sits, with `importedStructures.adjustments` (y shift, stilts, encasing, Y bands).
|
||||
- Turn a structure or a whole family off, with `importedStructures.disabled` (prefix and family matching) or `importedStructures.disabledExact` (one key, leaving its variants alone).
|
||||
- Ingest a datapack into one dimension with `datapackImports`, so its structures generate in that Iris world.
|
||||
- Take positioning away from Minecraft entirely: list registered keys in a placement's `nativeStructures` and let Iris choose the start points on its own grid. Add `nativeSuppression: "REPLACE_SOURCE"` on a dimension-level placement to stop the original from also generating on its own.
|
||||
|
||||
Example — stop vanilla villages generating on their own, then place the plains variant yourself on an Iris grid, in the dimension file:
|
||||
|
||||
```json
|
||||
{
|
||||
"importedStructures": {
|
||||
"disabled": ["minecraft:village"]
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"nativeStructures": [{ "structure": "minecraft:village_plains" }],
|
||||
"nativeSuppression": "REPLACE_SOURCE",
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 40,
|
||||
"separation": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Ingest, adjustment, disabling, and conversion back into editable Iris resources are all in `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Goal | Use | Guide |
|
||||
|---|---|---|
|
||||
| Scatter a fixed build across a biome — trees, rocks, ruins, clutter | Object + `objects[]` | `20 - Object Placement.md` |
|
||||
| Put pack loot in an object's chests | Placement `loot` / `vanillaLoot` | `20 - Object Placement.md` |
|
||||
| Spawn mobs from a placed build | Placement `markers[]` plus a `markers/` resource | `20 - Object Placement.md` |
|
||||
| Multi-room build that should differ every time | Iris jigsaw structure | `21 - Jigsaw Structures.md` |
|
||||
| Village-style roads, halls, corners, tees, and caps | `PLANAR_JIGSAW` through `/iris jigsaw` | `21 - Jigsaw Structures.md` |
|
||||
| Towers, shafts, stacked rooms, stronghold sprawl | `SPATIAL_JIGSAW` through `/iris jigsaw` | `21 - Jigsaw Structures.md` |
|
||||
| One authored graph that must also ship as a vanilla 26.2 datapack | `compatibility: VANILLA_PORTABLE`, then `/iris jigsaw export` | `21 - Jigsaw Structures.md` |
|
||||
| A vanilla structure sits badly in Iris terrain | `importedStructures.adjustments` | `22 - Native Structures & Datapacks.md` |
|
||||
| Remove vanilla villages or another whole family | `importedStructures.disabled` | `22 - Native Structures & Datapacks.md` |
|
||||
| Remove one key without touching its variants | `importedStructures.disabledExact` | `22 - Native Structures & Datapacks.md` |
|
||||
| Make a datapack's structures generate natively | `datapackImports` plus ingest | `22 - Native Structures & Datapacks.md` |
|
||||
| Keep a datapack's buildings but choose the locations | Disable the namespace, then a `nativeStructures` placement | `22 - Native Structures & Datapacks.md` |
|
||||
| Replace a vanilla structure with Iris-positioned starts | Dimension placement with `nativeSuppression: REPLACE_SOURCE` | `22 - Native Structures & Datapacks.md` |
|
||||
| Edit one registered vanilla or datapack jigsaw | `/iris jigsaw convert`, then edit the Iris copy | `22 - Native Structures & Datapacks.md` |
|
||||
| Bulk-import registered structures and templates | `/iris structure import`, then inspect the copies | `22 - Native Structures & Datapacks.md` |
|
||||
| Edit an existing Iris graph you do not own | `/iris jigsaw adopt inspect`, then apply the reviewed plan | `21 - Jigsaw Structures.md` |
|
||||
| Builds shipped as vanilla `.nbt` | Ship them in a datapack, or import them as objects | `22 - Native Structures & Datapacks.md` |
|
||||
|
||||
The systems compose, and shipping packs use all three: objects for decoration, jigsaws for authored multi-piece content, and native placements with suppression for the vanilla keys they want to reposition.
|
||||
|
||||
## How the pieces connect
|
||||
|
||||
```
|
||||
build in world ──wand──> object (.iob) ──┬── objects[] on a biome/region ──> scattered by chance
|
||||
│
|
||||
└── jigsaw piece ──> pool ──> structure ──┐
|
||||
│
|
||||
registered structure (vanilla / datapack / mod) │
|
||||
│ │
|
||||
├── generates natively, shaped by importedStructures │
|
||||
└── listed in a placement's nativeStructures ──────────┐ │
|
||||
▼ ▼
|
||||
structures[] on a dimension / region / biome
|
||||
```
|
||||
|
||||
An Iris structure and a native structure both arrive through the same `structures[]` array — the difference is whether the entry names Iris assembly resources (`structures`) or registry keys (`nativeStructures`).
|
||||
|
||||
## Rules that apply to all three
|
||||
|
||||
**Keys are relative paths.** Every pack resource is keyed by its path under its own folder, without the extension: `objects/trees/oak/big1.iob` is the object `trees/oak/big1`, and `jigsaw-pools/fort/starts.json` is the pool `fort/starts`.
|
||||
|
||||
**Only new chunks change.** Editing a placement or a structure control never rewrites terrain that already generated. Every check you run has to happen in chunks generated after the edit.
|
||||
|
||||
**Worlds carry their own copy of the pack.** On world create the pack is copied to `<world>/iris/pack`, and that copy is what the world loads from then on. Editing `packs/<pack>` changes nothing in an existing world until you run `/iris developer update-world world=<world> pack=<dimension> confirm=true` and the world is loaded again. Studio worlds are the exception: they read the live pack and hotload JSON and `.iob` edits into newly generated chunks about once a second.
|
||||
|
||||
**Placement scope is explicit.** `structures[]` can sit on a dimension, a region, or a biome. Scope is sampled at the center of the start chunk. Cave-biome entries only participate when the placement's resolved anchor is `CAVE_FLOOR`, `CAVE_CEILING`, `CAVE_CENTER`, or `CAVE_ANY`, and an optional `caveBiomes` allowlist is rechecked at the anchor Iris actually picked. See `15 - Caves & Carving.md`.
|
||||
|
||||
**Objects never write into native structure pieces.** If any block of an object placement would land inside a live native structure volume, the whole placement is dropped — `forcePlace: true` does not override this. Pieces of an Iris structure are exempt, because they route through the internal structure path.
|
||||
|
||||
**Command shape.** On Bukkit, Director optional parameters are always `key=value` — `/iris structure verify overworld radius=200`, never a bare trailing number. The modded loaders use brigadier literals instead.
|
||||
|
||||
**Validate before shipping.** `/iris pack validate pack=<pack>` on Bukkit (the argument is required there; pass `pack=` with an empty value to cover every pack). On a modded loader use `/iris pack validate <pack>`, or omit the pack to validate all of them. It compiles the jigsaw graph, checks native-replacement placements, and runs object surface-support, loot, spawner, and block-property checks.
|
||||
|
||||
## Proving a structure path works
|
||||
|
||||
1. Say in one sentence what you want: one repeated build, an assembled graph, or a registered Minecraft structure.
|
||||
2. Pick the matching system above and do that guide's smallest worked example in a disposable Studio or test world. Do not start by converting assets between systems.
|
||||
3. Validate the pack.
|
||||
4. Place or locate one instance directly. On Bukkit that is `/iris object paste <key>` for objects and `/iris structure place <dimension> <structure>` for structures.
|
||||
5. Then generate a natural instance in fresh chunks, and check again after a restart.
|
||||
|
||||
Steps 4 and 5 are both required. A direct place proves geometry and nothing else — it does not prove spacing, biome eligibility, height gates, or native registry scope. Only natural generation does that.
|
||||
|
||||
Add terrain adaptation, loot, markers, and suppression after the plain case generates. Debugging a stack of five features at once is how a broken key looks like a broken engine.
|
||||
@@ -1,237 +0,0 @@
|
||||
# 19 - Objects
|
||||
|
||||
An Iris object is a sparse voxel volume — block states plus block-entity data — stored as a `.iob` file under a pack's `objects/` folder. You build it in a world, select it with a wand, and save it into the pack; nothing about the object itself says where it generates. Wiring it into generation is `20 - Object Placement.md`; using objects as jigsaw pieces is `21 - Jigsaw Structures.md`.
|
||||
|
||||
## Capture and save an object
|
||||
|
||||
Prerequisites: a writable pack, operator access on a Bukkit-family server, and something built to capture. A Studio world is the shortest path because it hotloads pack edits and starts you in creative.
|
||||
|
||||
```text
|
||||
/iris studio open <pack> seed=1337
|
||||
/iris object wand
|
||||
```
|
||||
|
||||
1. **Select.** Left-click one corner of the build, right-click the opposite corner. The selection lives on the wand item, and particles outline the box out to 256 blocks from you.
|
||||
2. **Tighten.** Run `/iris object x+y`. It walks the selection upward until the slab is empty air, backs off one, then pulls the four side faces in until each touches a block. Use `/iris object x&y` instead if the selection also needs to find its own floor. The saved volume is exactly the selection box, so any air you leave in it moves the object's origin.
|
||||
3. **Save into the pack.** `/iris object save tutorial/lookout`. Inside an Iris world the target pack resolves automatically; anywhere else pass `dimension=<pack>`. Add `overwrite=true` to replace an existing file — there is no backup. If the build contains chests, signs, banners, or spawners you care about, add `legacy=false` (section 3).
|
||||
Success looks like: a chat line naming the pack and the object, and a new file at `<data>/packs/<dimension load key>/objects/tutorial/lookout.iob`.
|
||||
4. **Verify it loads.** `/iris object analyze tutorial/lookout` reads the file back and reports width x height x depth, total block count, and the ten most common materials. If those numbers match what you selected, the file is good.
|
||||
5. **Verify it pastes.** `/iris object paste tutorial/lookout edit=true` stamps a copy where you are looking and hands you a wand already fitted to it. Walk the copy: check the orientation, check that chests still have contents and signs still have text, fix anything wrong in place, then re-save the same key with `overwrite=true`. `/iris object undo` removes the pasted copy.
|
||||
6. **Prove it survives a reload.** Close and reopen Studio, then paste again. Block states and block-entity data must come back identical; if a chest is empty now, the object was captured through a path that drops tile data (section 6).
|
||||
|
||||
The object is finished when `analyze` reports the dimensions you expect, a fresh paste lines up on the target block, and a save/reopen cycle changes nothing. Then continue with `20 - Object Placement.md`.
|
||||
|
||||
Use a throwaway key like `scratch/test1` until the bounds and origin are right. Overwriting an object does not rewrite copies already generated into existing chunks.
|
||||
|
||||
## 1. What an object is
|
||||
|
||||
An object stores a bounding box (`w x h x d`), a sparse map of block states, and a sparse map of tile data. The origin is always the **center** of the bounding box — `w/2, h/2, d/2` with integer division. It is derived from the dimensions, never written to the file, and recomputed every load.
|
||||
|
||||
Stored:
|
||||
|
||||
- Every block in the selection except plain `minecraft:air`.
|
||||
- Block-entity data, at one of two fidelities. `/iris object save` defaults to `legacy=true`, which writes a reduced record for the block types it has a handler for: a sign keeps its front four lines and color and **loses its back side**, a spawner keeps only the entity type it spawns, a banner keeps its patterns and base color, and a container with a vanilla loot table keeps the table key and loot seed. Anything without a matching handler — including a hand-filled chest, a furnace with contents, a decorated pot — falls through to the full block-entity NBT. Pass `legacy=false` to serialize everything in full. Object studio click-to-save always writes full NBT.
|
||||
|
||||
Not stored:
|
||||
|
||||
- **Entities.** Armor stands, item frames, paintings, and mobs are dropped on save. Runtime entity spawns come from placement markers instead (`20 - Object Placement.md`).
|
||||
- **Biomes.**
|
||||
- **Jigsaw blocks, structure blocks, and structure voids.** These are filtered out when the `.iob` is *read*, not when it is written, so they can sit in a file and still never reach the world. Only the block is dropped; a tile-data entry saved at that position still loads. Iris jigsaw connectors are JSON metadata, not blocks (`21 - Jigsaw Structures.md`).
|
||||
|
||||
**Air is not one thing.** `minecraft:air` is skipped at capture. `cave_air` and `void_air` are both captured — but at placement time Iris skips `air` and `cave_air` blocks and only writes `void_air`. So `void_air` is the block to use when you want an object to carve terrain away, and `cave_air` inside an object is dead weight in every mode except the internal `STRUCTURE_PIECE` path.
|
||||
|
||||
### Format limits
|
||||
|
||||
`.iob` V2 writes an `Iris V2 IOB;` header, then short-typed centered coordinates and a short-counted palette. That gives a working range of +/- 32,767 blocks per axis from the center and 32,767 distinct block states per object. Neither limit is checked on save — exceed either and the shorts wrap silently, so the file writes and then reads back wrong. Files written before V2 still load through a legacy reader, tried automatically when the V2 header is missing. Nothing caps the block count; the practical limits are memory and the wand's scan budget, which processes 30 ms of blocks per tick by default (`-Diris.ms_per_tick`).
|
||||
|
||||
### Where objects live and how they are named
|
||||
|
||||
```
|
||||
<platform data folder>/packs/<pack>/objects/**/*.iob
|
||||
```
|
||||
|
||||
On a Bukkit-family server that is `plugins/Iris/packs/<pack>/objects/`. The object key is the path under `objects/`, slash-separated, without the extension:
|
||||
|
||||
```
|
||||
objects/light.iob -> "light"
|
||||
objects/trees/oak/big_oak_1.iob -> "trees/oak/big_oak_1"
|
||||
```
|
||||
|
||||
Keys resolve within the current pack first. Outside an Iris world, lookups fall back to scanning every visible pack and take the **first** one that resolves the key, with no warning that others also matched. That matters most for `/iris object shrink`, which resolves this way and then overwrites whatever it found. Give objects distinctive subfolder paths.
|
||||
|
||||
## 2. Building and selecting in-game
|
||||
|
||||
### Build surfaces
|
||||
|
||||
- **Pack studio** — a world generated from your pack, so you can build against real terrain: `/iris studio open <dimension> [seed=1337]`, closed with `/iris studio close`.
|
||||
- **Object studio** — a flat gallery laying out every object in the pack on a grid, with click-to-save editing (section 5): `/iris object studio [dimension=<dim>] [seed=1337]`. Omit `dimension` and it aggregates the objects of every visible pack.
|
||||
|
||||
Any flat world works too. `/iris object save` only resolves the target pack automatically inside an Iris world; elsewhere pass `dimension=<pack>`.
|
||||
|
||||
### The wand
|
||||
|
||||
`/iris object wand` gives a Blaze Rod named "Wand of Iris". The two corners are written into the item's lore, so dropping or replacing the wand loses the selection, and two wands are two independent selections.
|
||||
|
||||
| Action | Effect |
|
||||
|---|---|
|
||||
| Left-click a block | Sets corner 1 |
|
||||
| Right-click a block | Sets corner 2 |
|
||||
|
||||
Main hand only. Both clicks are cancelled so you never break or place while selecting. Setting a corner in a different world clears the other corner rather than producing a cross-world box. Outline particles are drawn for the selection while you are in the same world, out to 256 blocks from you, thinning out with distance.
|
||||
|
||||
**WorldEdit interop.** With the `worldEditWandCUI` setting on (the default), a live WorldEdit selection stands in for an Iris wand across the `/iris object` selection and save commands. `/iris object we` converts the current WorldEdit selection into a real Iris wand, and it works whether or not that setting is on. `position2` is the exception: it needs an actual Iris wand, so run `/iris object we` first if you have only a WorldEdit selection.
|
||||
|
||||
### Adjusting the selection
|
||||
|
||||
| Command | Alias | Effect |
|
||||
|---|---|---|
|
||||
| `/iris object position1 [here=true]` | `p1` | Moves a corner to the block under your feet, or to the block you are looking at with `here=false` |
|
||||
| `/iris object position2 [here=true]` | `p2` | Same for the other corner; requires an Iris wand |
|
||||
| `/iris object shift [amount=1]` | | Slides the whole box one step per `amount` along the axis you are facing |
|
||||
| `/iris object contract [amount=1]` | `-` | Pulls the face you are facing inward by `amount` |
|
||||
| `/iris object expand [amount=1]` | modded only | Pushes that face outward; the Bukkit command tree has no `expand` |
|
||||
| `/iris object x&y` | | Finds the empty air above and below the selection to set its top and bottom, then pulls the four sides in until they touch blocks |
|
||||
| `/iris object x+y` | | Same, but only searches upward — the current bottom is kept |
|
||||
|
||||
Rough-select the base of a build, then run `x+y` to wrap it tightly.
|
||||
|
||||
## 3. Saving
|
||||
|
||||
```
|
||||
/iris object save [dimension=<pack>] <name> [overwrite=false] [legacy=true]
|
||||
```
|
||||
|
||||
- `name` is required and positional. It is the path under `objects/`, and `/` creates subfolders.
|
||||
- `dimension` resolves from the Iris world you are standing in; pass `dimension=<pack>` anywhere else.
|
||||
- Without `overwrite=true` (alias `force=true`) an existing file aborts the save. There is no backup.
|
||||
- `legacy` defaults to **true**, which writes reduced tile records for signs, spawners, banners, and loot containers (section 1). Pass `legacy=false` when full block-entity fidelity matters — most obviously for double-sided signs.
|
||||
- The saved volume is the **full selection box**. Nothing is shrinkwrapped, so deliberate air padding is preserved — and moves the center.
|
||||
|
||||
The file lands at `<data>/packs/<dimension load key>/objects/<name>.iob`.
|
||||
|
||||
**Footgun:** the target folder is the **dimension's load key**, not the folder the dimension came from. A pack in `packs/mypack/` whose dimension file is `dimensions/overworld.json` writes its objects into `packs/overworld/`. Keep the dimension JSON filename equal to the pack folder name and this never bites.
|
||||
|
||||
```
|
||||
/iris object wand
|
||||
# left-click one corner, right-click the other
|
||||
/iris object x+y
|
||||
/iris object save trees/birch/tall_birch_1
|
||||
```
|
||||
|
||||
## 4. Pasting, editing, and inspecting
|
||||
|
||||
```
|
||||
/iris object paste <object> [edit=false] [rotate=0] [scale=1]
|
||||
```
|
||||
|
||||
The paste lands on the block you are looking at, with the object's bottom resting on it — small foliage (grass, snow layers, vines, torches, dead bushes, poppies, dandelions) is ignored by the raycast so you target the ground, not the plant. `rotate` is degrees around Y. `scale` resizes with tricubic interpolation and is clamped down for large objects, so a big object silently pastes at a smaller factor than you asked for.
|
||||
|
||||
```
|
||||
/iris object undo [amount=1]
|
||||
```
|
||||
|
||||
Alias `u`. It reverts pastes, not blocks you placed by hand.
|
||||
|
||||
`paste ... edit=true` additionally hands you a wand fitted to the pasted bounds, which is the normal way to edit an existing object: paste it, change it, re-save the same key with `overwrite=true`.
|
||||
|
||||
Inspection and maintenance:
|
||||
|
||||
- `/iris object analyze <object>` — dimensions, block count, and the top ten materials with their most common block-data variant. Read-only, and the fastest check that a file loads at all.
|
||||
- `/iris object shrink <object>` — shrinkwraps to the tightest box and **overwrites the file in place with no confirmation**. It re-centers, so any deliberate off-center padding is lost.
|
||||
- `/iris object plausibilize <target> [dryrun=false] [reach=12]` — tree-specific. It grows organic branch connections through the canopy so leaves survive vanilla decay; leaf clusters farther than `reach` blocks from wood are pinned persistent instead, and `reach=0` grows without a limit. `target` accepts an object key, a folder prefix ending in `/`, or a filesystem path. `dryrun=true` reports and writes nothing.
|
||||
- `/iris object dust` (alias `d`) — gives Glowstone Dust named "Dust of Revealing". Right-click a block in an Iris world and Iris names the placement that owns it.
|
||||
- `/iris find object <object> [teleport=true]` — `/iris goto object` is the same command under an alias. You have to be standing in an Iris world. In an object studio it teleports to that object's grid cell; otherwise it spirals outward looking for a generated instance, giving up after 120 seconds. `teleport=false` prints the coordinates instead of moving you.
|
||||
|
||||
## 5. Object studio: click-to-save
|
||||
|
||||
Inside `/iris object studio`, left- or right-clicking a block in a grid cell writes that cell straight back to its `.iob`. It is the quickest loop for touching up a library of small objects.
|
||||
|
||||
- The saved volume is the cell's original bounding box, so the center is preserved and nothing is shrinkwrapped. Tile data is always written in full, unlike `/iris object save`.
|
||||
- Each cell carries a content hash; clicking a cell you did not change reports no changes and writes nothing. The hashes are in memory only, so the first click on any cell after a world load always writes.
|
||||
- The save aborts **silently** if any chunk covering the cell is unloaded. If a click seems to do nothing, walk closer and click again.
|
||||
|
||||
## 6. Importing existing builds
|
||||
|
||||
### 6.1 `.schem` files: `/iris object convert`
|
||||
|
||||
1. Drop Sponge `.schem` files into `<data>/convert/` (created on first use).
|
||||
2. Run `/iris object convert`.
|
||||
3. Each `foo.schem` becomes `foo.iob` **in that same folder**, shrinkwrapped. Move it into a pack yourself — convert never writes into a pack.
|
||||
|
||||
What survives and what does not:
|
||||
|
||||
- **Sponge Schematic v2 and v3 only.** Anything else — MCEdit `.schematic`, `.litematic`, Sponge v1 — is rejected. No WorldEdit or FAWE needed; Iris parses the NBT itself.
|
||||
- **Blocks only.** The converter reads the palette and block indices and nothing else, so **block entities, entities, and biomes are all lost**: chests come out empty, signs blank, spawners default.
|
||||
- The source `.schem` is **deleted** after a successful conversion. Keep a copy elsewhere.
|
||||
- Files outside that folder, or not ending in `.schem`, produce no output at all.
|
||||
|
||||
### 6.2 Keeping block entities: paste, then wand
|
||||
|
||||
The converter cannot preserve tile data, so anything with chests, signs, or spawners has to go through the world:
|
||||
|
||||
1. Open a build world — `/iris studio open <pack>` or any flat world.
|
||||
2. `//schem load <name>` and `//paste` with WorldEdit or FAWE.
|
||||
3. Keep the WorldEdit selection (Iris accepts it directly) or run `/iris object we`, or re-select with the Iris wand.
|
||||
4. Tighten with `/iris object x+y` or `contract`.
|
||||
5. `/iris object save <name> [overwrite=true]`.
|
||||
|
||||
The wand save reads live blocks, so it captures full block-entity NBT. This is the only import route that preserves it.
|
||||
|
||||
### 6.3 Vanilla `.nbt` templates
|
||||
|
||||
- `/iris structure import <dimension>` — imports registered structures and their templates into the pack as objects plus jigsaw graphs.
|
||||
- `/iris studio importvanilla <dimension> [variants=3] [structures=true]` (aliases `importv`, `iv`) — captures vanilla trees, mushrooms, and object features into `objects/vanilla/`, taking `variants` samples of each, and also imports vanilla and datapack structures and jigsaws when `structures=true`.
|
||||
|
||||
Details are in `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
## 7. Practical details
|
||||
|
||||
**Center and anchoring.** The center is always the middle of the bounding box. Wand saves keep the selection box, so asymmetric padding survives and shifts the center; `convert` and `shrink` shrinkwrap and re-center. At generation the object is centered on its column in X and Z and lifted so its bottom sits on the anchor — everything past that is the placement's `mode` (`20 - Object Placement.md`).
|
||||
|
||||
**Rotation is never baked in.** A `.iob` stores exactly one orientation. Rotation ranges belong to the placement; `paste rotate=90` rotates a throwaway copy and does not touch the file.
|
||||
|
||||
**Two separate loot mechanisms.** A chest saved into the `.iob` with a vanilla loot table on it keeps that table. Pack loot tables are attached by the placement instead (`loot`, `vanillaLoot`, `overrideGlobalLoot`) — see `20 - Object Placement.md`.
|
||||
|
||||
**Caches and hotload.** Objects are cached per pack. Studio worlds watch the pack folder for `.iob` and `.json` changes and hotload at most once a second, backing off to about four seconds while the world is busy generating or running maintenance. A hotload swaps the engine's whole pack runtime, so already-generated chunks are untouched and only later ones see the edit. Ordinary worlds never hotload; they serve the cached copy until the pack reloads.
|
||||
|
||||
## 8. Common failure modes
|
||||
|
||||
1. **"You need to hold your wand!"** — no Iris wand selection and no WorldEdit selection.
|
||||
2. **"File already exists."** — pass `overwrite=true` (or `force=true`).
|
||||
3. **Save complains about a missing `dimension`** — you are not standing in a loaded Iris world; pass `dimension=<pack>`.
|
||||
4. **Objects landed in the wrong pack folder** — the dimension's load key is not the pack folder name (section 3).
|
||||
5. **`convert` did nothing** — the files are not in `<data>/convert/`, or do not end in `.schem`.
|
||||
6. **Converted objects have empty chests** — the converter never reads block entities. Use the paste-then-wand route (6.2).
|
||||
7. **The converter ate the schematic** — that is by design after a successful conversion.
|
||||
8. **The selection vanished** — corners live on the wand item, and `paste edit=true` overwrites the held wand's selection.
|
||||
9. **`position2` does nothing** — you have a WorldEdit-only selection. Run `/iris object we` first.
|
||||
10. **Entities are gone** — objects never store entities. Use placement markers.
|
||||
11. **Jigsaw or structure-void blocks are gone** — they are filtered out when the file is read. Connectors are JSON, not blocks.
|
||||
12. **A paste is offset from where you expected** — the origin is the bounding-box center, so air padding inside the selection moves it. Re-select tightly or run `shrink`.
|
||||
13. **A sign lost its back side, or a spawner lost its settings** — saved with the default `legacy=true`. Re-save with `legacy=false`.
|
||||
14. **`shrink` rewrote a file in a pack you were not thinking about** — outside an Iris world, a bare key resolves to the first visible pack that has it, silently (section 1).
|
||||
|
||||
## Command reference
|
||||
|
||||
All under `/iris object` (alias `/iris o`). On Bukkit, optional parameters are `key=value`; the modded loaders expose the same commands as brigadier literals instead (`/iris object save overwrite <name>`, `/iris object paste rotate <degrees> <key>`, `/iris object paste at <x> <y> <z> <key>`).
|
||||
|
||||
| Command | Aliases | Parameters |
|
||||
|---|---|---|
|
||||
| `studio` | | `dimension=<dim>` (optional), `seed=1337` |
|
||||
| `wand` | | |
|
||||
| `we` | | Bukkit only |
|
||||
| `position1` / `position2` | `p1` / `p2` | `here=true` |
|
||||
| `x&y` / `x+y` | `xay` / `xpy` | |
|
||||
| `shift` | | `amount=1` |
|
||||
| `contract` | `-` | `amount=1` |
|
||||
| `expand` | | `amount=1`, modded only |
|
||||
| `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` | | Bukkit only |
|
||||
| `dust` | `d` | |
|
||||
|
||||
Related: `/iris studio open <dimension> [seed=]`, `/iris studio close`, `/iris studio importvanilla <dimension> [variants=3] [structures=true]` (aliases `importv`, `iv`), `/iris find object <object> [teleport=true]` (alias `/iris goto object`), `/iris structure import <dimension>`.
|
||||
@@ -1,497 +0,0 @@
|
||||
# 20 - Object Placement
|
||||
|
||||
An object placement is one entry in a biome's or region's `objects[]` array. It names the objects to stamp, how often to try, where they are allowed to land, and how they should meet the terrain. Building the objects themselves is `19 - Objects.md`; multi-piece assemblies are `21 - Jigsaw Structures.md`.
|
||||
|
||||
## Tutorial: get one object into the world
|
||||
|
||||
Prerequisites: a saved object such as `objects/tutorial/lookout.iob`, a biome or region the target dimension actually uses, and a Studio or disposable test world. Placements only affect chunks generated after the JSON existed, so every check below needs fresh terrain.
|
||||
|
||||
Merge this into one focused biome, keeping the biome's other fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"place": ["tutorial/lookout"],
|
||||
"chance": 1,
|
||||
"density": 1,
|
||||
"mode": "CENTER_HEIGHT",
|
||||
"rotation": { "enabled": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1. Paste the object by hand first: `/iris object paste tutorial/lookout`. If the geometry or the origin is wrong, fix the object before you touch placement JSON.
|
||||
2. Validate the pack: `/iris pack validate pack=<pack>` on Bukkit, `/iris pack validate <pack>` on a modded loader.
|
||||
3. Open or hotload Studio and fly into fresh chunks whose center column uses the edited biome. At `chance: 1` you get one in nearly every such chunk.
|
||||
4. Confirm ownership: run `/iris find object tutorial/lookout`, or get `/iris object dust` and right-click a placed block. Iris names the placement that owns that block.
|
||||
5. Pick the terrain mode that fixes what you see: `PAINT` for ground-hugging clutter, a stilt mode for support over uneven ground, `CEILING_HANG` for cave roofs, a `VACUUM` mode to pull the terrain up to a flat base.
|
||||
6. Test the negative cases: steep slopes, water, cave mouths, and the neighbouring biomes where the object should not appear at all.
|
||||
7. Drop `chance` and `density` to production values, validate again, generate one more fresh area.
|
||||
|
||||
The placement is done when a manual paste and natural generation agree on orientation, the object sits on the ground the way you want, and it stays absent outside its configured scope.
|
||||
|
||||
If validation cannot resolve the object, the `place` key does not match the path under `objects/`. If nothing generates, work section 10 top to bottom. If a non-Studio world still generates the old placement, it is reading its own pack snapshot — see `18 - Structures Overview.md`.
|
||||
|
||||
## 1. Which files carry placements
|
||||
|
||||
`objects[]` exists on exactly two resource types:
|
||||
|
||||
- **Biome** (`biomes/**.json`) — wherever that biome generates.
|
||||
- **Region** (`regions/**.json`) — every biome in the region.
|
||||
|
||||
**A dimension has no `objects[]`.** Adding one to a dimension file does nothing. Dimensions do carry two knobs that tighten every placement underneath them (`requireObjectSurfaceSupport`, `objectSurfaceSupportBuffer`, section 4) and two that gate the inverted upper dimension (`upperDimensionObjects`, `upperObjectsForcePlace`), but no placements of their own.
|
||||
|
||||
Per chunk, Iris samples the biome and region once at the chunk center (block 8,8), then walks four lists in this fixed order: biome surface, biome cave, region surface, region cave. Every entry rolls independently, so a biome entry and a region entry can both fire in the same chunk. A biome that only clips the corner of a chunk contributes nothing there, and the center biome's objects can spill a few blocks into its neighbours.
|
||||
|
||||
A whole placement can be replaced by a snippet reference — `"objects": ["snippet/object-placer/my-camp"]` loads `snippet/object-placer/my-camp.json`.
|
||||
|
||||
Minimal entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"place": ["clutter/boulder1"],
|
||||
"chance": 0.05
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`place` entries are object keys: the path under `objects/` without `.iob`.
|
||||
|
||||
## 2. How often does it spawn?
|
||||
|
||||
Two numbers do the work. `chance` decides whether the entry runs at all in this chunk; `density` decides how many stamps it then attempts.
|
||||
|
||||
```json
|
||||
{
|
||||
"place": ["clutter/boulder1", "clutter/boulder2"],
|
||||
"chance": 0.05,
|
||||
"density": 3
|
||||
}
|
||||
```
|
||||
|
||||
Roughly `chance x density` objects per chunk, before rejections. `chance: 0.002` is about one chunk in five hundred.
|
||||
|
||||
Each of the `density` attempts independently picks a random object out of `place` and a random column inside the chunk, then runs every gate in sections 3 to 5. Density is a budget of tries, not a guarantee of placements.
|
||||
|
||||
`place` is unweighted — a uniform pick. To bias one object, list its key more than once or split the entry in two. A key that does not resolve costs the attempt and logs `Couldn't find Object: <key>` once per lookup.
|
||||
|
||||
Surface entries add uniform `+/- 0.005` jitter to `chance` before rolling; cave entries roll the raw value. Two consequences, each worth about one chunk in eight hundred: `chance: 1` occasionally fails, and `chance: 0` occasionally succeeds. Omit an entry to disable it; do not set it to zero.
|
||||
|
||||
`densityStyle` replaces `density` with a noise-driven count, so density varies across the world instead of being flat:
|
||||
|
||||
```json
|
||||
{
|
||||
"densityStyle": { "min": 1, "max": 4, "style": { "style": "IRIS_THICK", "zoom": 8 } }
|
||||
}
|
||||
```
|
||||
|
||||
Always set `min` and `max`. The class defaults are 16 and 32, so a bare `"densityStyle": {}` asks for 16 to 32 attempts in every chunk that passes the chance roll.
|
||||
|
||||
## 3. Where is it allowed to land?
|
||||
|
||||
By default a placement is surface-only, unlimited in height, unlimited in slope, and refuses to sit over a cave mouth. Each field below narrows one of those.
|
||||
|
||||
**Surface or cave.** `carvingSupport` sorts the entry into the surface list, the cave list, or both.
|
||||
|
||||
```json
|
||||
{ "carvingSupport": "CARVING_ONLY", "caveAnchorMode": "FLOOR" }
|
||||
```
|
||||
|
||||
`SURFACE_ONLY` (the default) rejects any anchor that lands in carved space. `CARVING_ONLY` requires carved space at the anchor or within three blocks below it, and the engine hunts for an anchor Y inside the cave column instead of using the terrain surface. `ANYWHERE` sits in both lists, which means it rolls `chance` twice per chunk — once for the surface pass and once for the cave pass. Cave passes resolve their biome by sampling 48, 80, and 112 blocks below the surface and taking the deepest sample that differs from the surface biome and has carving objects; if none does, the surface biome is used.
|
||||
|
||||
`caveAnchorMode` picks which carved cells count: `FLOOR` needs solid support below, `CEILING` needs solid above, `CENTER` needs neither, `ANY` takes anything carved, and `PROFILE_DEFAULT` defers to the cave profile (`15 - Caves & Carving.md`). `CEILING_HANG` overrides this to `CEILING` regardless of what you wrote.
|
||||
|
||||
**Height band.** `clamp` rejects placements whose resolved top or bottom leaves the band.
|
||||
|
||||
```json
|
||||
{ "clamp": { "minimumHeight": 40, "maximumHeight": 225 } }
|
||||
```
|
||||
|
||||
These are engine-internal Y values — 0 to (dimension height), not world Y. In a `-64..320` dimension, world Y 0 is internal Y 64.
|
||||
|
||||
**Slope.** `slopeCondition` gates on the terrain slope sampled around the anchor column.
|
||||
|
||||
```json
|
||||
{ "slopeCondition": { "minimumSlope": 0, "maximumSlope": 2 } }
|
||||
```
|
||||
|
||||
The default (`0` to `10`) is treated as "no condition" and skips the check entirely. `maximumSlope: 2` keeps buildings off hillsides; going below about 1 excludes nearly all natural terrain. `rotateTowardsSlope: true` finds whichever of the four footprint edges sits lowest and rotates the object to face that way, in 90 degree steps, adding the `yAxis` `min` — it discards `yAxis` `max` and `interval`.
|
||||
|
||||
**Neighbours.** `forbiddenCollisions` lists object keys this object refuses to intersect; if any block of an already-placed object with that key falls inside this object's bounding box, the attempt is dropped. `allowedCollisions` names exceptions that win over the forbidden list. Both are empty by default, and the check only runs when at least one of them is non-empty.
|
||||
|
||||
**Overrides.** `forcePlace: true` (JSON also accepts `"force"`) skips the slope gate, the carving-anchor check, surface support, the underwater rejection, the fluid-height and cave-height checks, `clamp`, the bedrock guard, and the collision lists. It does **not** skip the native-structure veto: an object whose blocks would land inside a vanilla or datapack structure piece is always rejected, forced or not.
|
||||
|
||||
## 4. Surface support: the silent rejection
|
||||
|
||||
Iris refuses surface objects that roof over, bridge, or overhang a carved opening. It takes the object's lowest solid non-foliage layer, rasterizes those columns, dilates the stencil by `surfaceSupportBuffer`, and requires every column in the result to have `surfaceSupportDepth` blocks of un-carved, surface-solid ground. A failure drops the placement with **no log line at all**, which makes it the usual cause of "my object never appears" near caves, canyon rims, and ravines.
|
||||
|
||||
```json
|
||||
{ "requireSurfaceSupport": true, "surfaceSupportBuffer": 2, "surfaceSupportDepth": 2 }
|
||||
```
|
||||
|
||||
The dimension acts as a floor, not a ceiling: the effective buffer is `max(placement, dimension)` and the guard is on only if both the placement and the dimension ask for it. A dimension can therefore widen the ring or force the guard on pack-wide, never loosen it. `surfaceSupportDepth` is not merged — the placement value is used as written.
|
||||
|
||||
The check is skipped entirely for `forcePlace`, `fromBottom`, `mode: FLOATING`, `mode: STRUCTURE_PIECE`, `underwater`, `onwater`, cave-anchored placements, and `requireSurfaceSupport: false`.
|
||||
|
||||
Loosen in this order, stopping as soon as the object appears: `surfaceSupportBuffer: 0`, then `surfaceSupportDepth: 1`, then `requireSurfaceSupport: false`, then `force: true`, and only then the dimension-wide switch.
|
||||
|
||||
A second guard rejects surface-anchored placements that resolve to y <= 1 in a bedrock dimension. That one does log, throttled to one line per object and mode every five seconds: `Implausible object placement rejected`.
|
||||
|
||||
## 5. How does it sit on the terrain?
|
||||
|
||||
`mode` decides how the object's Y is chosen and whether the terrain moves to meet it. The default, `CENTER_HEIGHT`, takes a single height sample at the anchor column and uses it for the whole object — cheap, and fine for anything small or anything on flat ground.
|
||||
|
||||
**Height sampling.** These four modes only change which sample wins.
|
||||
|
||||
```json
|
||||
{ "mode": "MAX_HEIGHT" }
|
||||
```
|
||||
|
||||
`MAX_HEIGHT` samples every column in the footprint and takes the highest, so nothing gets buried but the object floats off cliffs. `MIN_HEIGHT` takes the lowest, so nothing overhangs but slopes swallow it. The `FAST_` variants sample four points instead of the full footprint. `PAINT` is the outlier: it drops each column of the object to that column's own surface height, melting the object over the terrain rather than placing it as a rigid block. Vines are exempt so they keep hanging.
|
||||
|
||||
**Stilts.** Stilt modes take a height mode, then repeat the object's bottom blocks downward until they hit ground.
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "CENTER_STILT",
|
||||
"stiltSettings": { "yMax": 4, "yRand": 1, "overStilt": 1 }
|
||||
}
|
||||
```
|
||||
|
||||
`STILT` is `MAX_HEIGHT` plus columns, `MIN_STILT` is `MIN_HEIGHT` plus columns, `CENTER_STILT` is `CENTER_HEIGHT` plus columns and is the cheapest one worth using. The `FAST_` variants are cheaper and less accurate, so pair them with `overStilt` to drive the legs further under the surface. `ERODE_STILT` tapers the legs like a cone — deepest at the footprint centroid, dropping off toward the edges, with the lower portion randomly broken up.
|
||||
|
||||
Only occluding blocks stilt, and stairs, slabs, and dirt paths are excluded. Grass, mycelium, podzol, and dirt-path bottoms are substituted with dirt so you do not get grass columns; a `palette` overrides the column material entirely. A column stops as soon as it hits a fluid, so stilts never punch through a lake floor.
|
||||
|
||||
`ORGANIC_STILT` and `CEILING_HANG` are for caves. `ORGANIC_STILT` scans down to the first solid block and fills the gap with noise-varied roots; `CEILING_HANG` flips the object vertically, anchors its top to the roof, and grows the same organic column up into the ceiling. Both read `organicMaxScan`, `organicJitter`, and `organicScratch`.
|
||||
|
||||
**Terrain shaping.** The vacuum modes bend the terrain instead of extending the object.
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "VACUUM_ORGANIC",
|
||||
"vacuumSettings": { "radius": 14, "falloff": 2.0, "organicJitter": 4 }
|
||||
}
|
||||
```
|
||||
|
||||
They anchor like `CENTER_HEIGHT`, then raise or carve every column out to a radius so the surface meets the object's lowest placed block, easing off by `falloff` (1 is a cone, 2 a parabolic bowl, higher stays flat near the object then drops). Raised columns are filled with the biome's rock; lowered columns are cleared to air, and inside the footprint the carve never eats into the object. `VACUUM` uses radius 12, `VACUUM_HIGH` 20, `VACUUM_FAST` 8 with every other column sampled. `VACUUM_ORGANIC` jitters the radius per column for a ragged edge; `VACUUM_WAVY` adds a smooth simplex wave that fades to zero under the object and at the rim.
|
||||
|
||||
**Two cases where Iris overrides your `mode`.** An object whose key contains `imports/` — anything brought in by `/iris structure import` or `/iris studio importvanilla` — is forced to `FAST_MIN_STILT` unless you asked for `FLOATING` or `STRUCTURE_PIECE`. And a cave placement left at the default `CENTER_HEIGHT` takes the active cave profile's `defaultObjectPlaceMode` instead, if the profile sets one. Write any other mode and it is honoured as-is.
|
||||
|
||||
**Special.** `FLOATING` ignores terrain entirely: Y comes from the rotated object center plus `translate.y` and `translate.yRandom`, and the terrain, water, cave-anchor, and surface-support checks are all skipped. Use it for sky islands and anything that must not fall to the ground. `STRUCTURE_PIECE` is a raw stamp at caller-supplied coordinates used internally for native structure pieces — do not write it into `objects[]`.
|
||||
|
||||
**Fine positioning.** `translate` shifts the object after rotation, so the offsets rotate with it.
|
||||
|
||||
```json
|
||||
{ "translate": { "x": 0, "y": -1, "z": 0, "yRandom": 0 } }
|
||||
```
|
||||
|
||||
`translate.y: -1` seats clutter one block into the ground and is the standard fix for objects riding on top of grass. `yRandom` adds a random `0..yRandom` per placement; a negative value randomizes downward instead.
|
||||
|
||||
`rotation` is on by default, and its default is exactly what most packs want: free Y spin in 90 degree steps.
|
||||
|
||||
```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` means any multiple of `interval`; `min == max` at some other value locks the object to that angle; anything else picks a multiple of `interval` and clips it into `[min, max]`. In the free case an `interval` below 1 is treated as 1 (one-degree steps); in a clipped range always set a real `interval`. Non-90-degree angles look bad at block resolution. Turn rotation off with `"rotation": { "enabled": false }`. X and Z rotation are incompatible with `bottom: true`.
|
||||
|
||||
`scale` is inert until you ask for something other than 1.
|
||||
|
||||
```json
|
||||
{ "scale": { "size": 1, "minimumScale": 0.75, "maximumScale": 1.25, "variations": 7, "interpolation": "TRILINEAR" } }
|
||||
```
|
||||
|
||||
`size` is a fixed multiplier and overrides the range when it is not 1. With `size: 1` and a min/max spread, Iris pre-builds `variations` evenly spaced copies across the range and picks one per placement — the copies are cached and shared, so a large `variations` costs memory. `interpolation` only matters when scaling up: `NONE` gives blocky output, `TRILINEAR` smooths it, `TRICUBIC` and `TRIHERMITE` are smoother and much slower.
|
||||
|
||||
`heightmap` replaces terrain height sampling with a noise generator, so the object seats against a virtual surface. Surface support still samples the real terrain.
|
||||
|
||||
## 6. Water, snow, and air pockets
|
||||
|
||||
**Water.** A surface placement whose anchor column is submerged is rejected unless you opt in.
|
||||
|
||||
```json
|
||||
{ "underwater": true, "waterloggable": true, "isDolphinTarget": true }
|
||||
```
|
||||
|
||||
`underwater: true` places on the terrain floor and ignores the water surface — and additionally rejects the placement if the resolved Y is at or above the fluid height, so it really is an underwater-only switch. `onwater: true` places on the fluid surface instead, for boats and docks. `waterloggable: true` waterlogs any placed block that can be waterlogged and lands in water; `underwater` implies the same behavior. `isDolphinTarget: true` combined with `underwater` marks placed storage chests as buried-treasure points of interest so dolphins swim players to them.
|
||||
|
||||
**Snow.** `snow` caps the snow layer depth dripped over the top of every column the object writes.
|
||||
|
||||
```json
|
||||
{ "snow": 0.5 }
|
||||
```
|
||||
|
||||
The value scales to vanilla's eight layers: each column gets a random count from 0 to `floor(snow * 7)`, placed one block above the object's highest block in that column. Small values are effectively fixed — `snow: 0.1` is always a single layer.
|
||||
|
||||
**Air pockets and interiors.** By default the object only writes its own blocks, so terrain left standing inside a hollow object stays there.
|
||||
|
||||
```json
|
||||
{ "bore": true, "boreExtendMaxY": 4, "boreExtendMinY": 0 }
|
||||
```
|
||||
|
||||
`bore: true` clears the whole bounding cuboid to air before the object writes, which is blunt but predictable; `boreExtendMaxY` and `boreExtendMinY` grow that box upward and downward. `smartBore: true` instead raytraces the volume on three axes and fills only the enclosed interior, so a house keeps its rooms clear without erasing the trees around it. Smart boring is a one-time cost per object at load, not per placement.
|
||||
|
||||
`meld: true` inverts the rule — the object only writes where a solid block already exists, which carves the object into terrain rather than adding to it. It is expensive; the placer samples the world per block.
|
||||
|
||||
`edit` rewrites materials at placement time, so one saved object can serve several biomes:
|
||||
|
||||
```json
|
||||
{
|
||||
"edit": [
|
||||
{ "find": [{ "block": "minecraft:oak_planks" }], "replace": { "palette": ["minecraft:spruce_planks"] }, "chance": 1, "exact": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`exact: false` matches on material alone; `exact: true` requires a full block-data match. When the replacement resolves to the *same* material as the block it matched, Iris merges the two block states rather than overwriting, so facing and other properties survive; a different material replaces outright. `chance` is rolled once per rule per block.
|
||||
|
||||
`warp` displaces each block's X and Z through a noise field, so a rigid object ripples like a flag. The displacement range is `+/- multiplier / 2` and is truncated to whole blocks, so the default `multiplier: 1` gives at most a one-block jitter — raise `multiplier` for a warp you can actually see.
|
||||
|
||||
```json
|
||||
{ "warp": { "style": "IRIS_DOUBLE", "zoom": 0.4, "multiplier": 6 } }
|
||||
```
|
||||
|
||||
## 7. Loot, markers, and saplings
|
||||
|
||||
**Loot.** Two arrays attach loot tables to container blocks inside the placed object. Only storage chests receive them.
|
||||
|
||||
```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 key under the pack's `loot/` folder; `vanillaLoot[].name` is a vanilla or datapack loot-table key. For each chest, an `exact: true` full block-data match wins over a material match, which wins over an entry with no `filter` at all; among the survivors one table is picked by `weight`. `overrideGlobalLoot: true` makes the placement's table the only one, suppressing dimension, region, and biome loot for the containers it matched. An unresolvable name logs `Couldn't find loot table <name>` and is skipped.
|
||||
|
||||
Iris fills these chests during the post-generation chunk update pass, reading the placement recorded in the mantle at that block — not when a player opens the chest. Separately, a chest saved into the `.iob` with a vanilla loot table already on it keeps that table (`19 - Objects.md`).
|
||||
|
||||
**Markers.** Placements have no entity field. `markers[]` tags matching blocks with a marker resource, and the marker carries the spawners.
|
||||
|
||||
```json
|
||||
{
|
||||
"markers": [
|
||||
{ "mark": [{ "block": "minecraft:mossy_cobblestone" }], "marker": "camp-spawns", "maximumMarkers": 4, "exact": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Candidate blocks are shuffled, so which ones get marked varies per placement, and `maximumMarkers` (default 8, hard max 16) caps the count. `markers/camp-spawns.json` lists the spawner resources; its `emptyAbove` (default true) requires two air blocks above the marked block inside the object.
|
||||
|
||||
**Saplings.** When a sapling grows, Iris scans the biome's `objects[]` for placements whose `trees[]` matches the grown tree type and the sapling square size, falls back to the region's list if the biome had no match, picks one, and stamps it. A single placement can serve both natural generation and sapling override. Procedural tree generation (`proceduralObjects`) is a separate system — see `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md`.
|
||||
|
||||
## 8. Reference
|
||||
|
||||
### Frequency
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `place` | required | Object keys to stamp; one is picked at random per attempt, with no weighting |
|
||||
| `chance` | `1` | Probability that the entry runs in a given chunk; surface entries jitter by `+/- 0.005`, cave entries do not |
|
||||
| `density` | `1` | Stamp attempts once the entry runs, each at its own random column with its own random object |
|
||||
| `densityStyle` | unset | Noise-driven attempt count that supersedes `density`; class defaults are 16 to 32 |
|
||||
|
||||
### Scope and rejection
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `carvingSupport` | `SURFACE_ONLY` | Whether the entry runs on the terrain surface, inside carved space, or in both passes |
|
||||
| `caveAnchorMode` | `PROFILE_DEFAULT` | Which carved cells qualify: floor, ceiling, open middle, anything, or whatever the cave profile says |
|
||||
| `clamp` | `-2048` / `2048` | Height band the resolved object must fit inside, in engine-internal Y |
|
||||
| `slopeCondition` | `0` / `10` | Terrain steepness window; the default pair disables the check outright |
|
||||
| `forbiddenCollisions` | `[]` | Object keys already in the world that veto this placement when they fall inside its bounding box |
|
||||
| `allowedCollisions` | `[]` | Keys exempted from the veto above |
|
||||
| `requireSurfaceSupport` | `true` | Whether to refuse to bridge or roof a carved opening |
|
||||
| `surfaceSupportBuffer` | `2` (0..16) | How far past the footprint the ground must stay solid and un-carved |
|
||||
| `surfaceSupportDepth` | `2` (1..16) | How thick that un-carved ground must be under each column |
|
||||
| `forcePlace` | `false` | Skips every gate above plus water and bedrock guards; never skips the native-structure veto |
|
||||
| `heightmap` | unset | Substitutes a noise field for terrain height when resolving Y |
|
||||
|
||||
### Terrain fit
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `mode` | `CENTER_HEIGHT` | How Y is chosen and whether the terrain moves — see section 5 |
|
||||
| `translate` | `0,0,0` | Post-rotation offset; `y: -1` is the usual fix for clutter riding on grass |
|
||||
| `translate.yRandom` | `0` | Random vertical spread per placement, downward if negative |
|
||||
| `rotation` | Y free, 90 degree steps | Random orientation per placement |
|
||||
| `rotateTowardsSlope` | `false` | Turns the object to face downhill, in 90 degree steps |
|
||||
| `scale` | `1` | Resizes the object; a min/max spread pre-builds `variations` cached copies |
|
||||
| `bottom` | `false` | On explicit-Y paths (cave, structure, sapling), seats the object bottom-up instead of centered |
|
||||
| `fromBottom` | `false` | Anchors near the world floor; an unfinished code path, avoid it |
|
||||
| `warp` | flat | Noise displacement of each block's X and Z; inert until `multiplier` is raised |
|
||||
|
||||
### Terrain interaction
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `bore` | `false` | Empties the whole bounding cuboid before writing |
|
||||
| `boreExtendMaxY` / `boreExtendMinY` | `0` | Grows that emptied box upward and downward |
|
||||
| `smartBore` | `false` | Raytraces the object once at load and clears only its enclosed interior |
|
||||
| `meld` | `false` | Writes only where solid terrain already exists, carving the object in rather than adding it |
|
||||
| `edit` | `[]` | Material find-and-replace applied as the object is written |
|
||||
| `snow` | `0` | Depth cap for a snow dusting laid over the object's top blocks |
|
||||
| `underwater` | `false` | Ignores the water surface and seats on the sea floor; also refuses to place above fluid height |
|
||||
| `onwater` | `false` | Seats on the fluid surface instead of the floor |
|
||||
| `waterloggable` | `false` | Waterlogs placed blocks that end up in water |
|
||||
| `isDolphinTarget` | `false` | With `underwater`, registers placed chests as buried treasure for dolphins |
|
||||
|
||||
### `stiltSettings`
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `yMax` | `0` | Caps how far a leg extends before the random and over-stilt terms apply |
|
||||
| `yRand` | `0` | Random extra leg length per column |
|
||||
| `overStilt` | `0` | Pushes every leg this much further under the surface; use it with the `FAST_` modes |
|
||||
| `palette` | unset | Material for the legs; without it Iris repeats the object's bottom block and swaps grass-family blocks for dirt |
|
||||
| `organicMaxScan` | `48` | How far `ORGANIC_STILT` and `CEILING_HANG` search for solid rock before giving up |
|
||||
| `organicJitter` | `3` | Random shortening per column, so the underside is ragged instead of a flat disc |
|
||||
| `organicScratch` | `0.55` | Fraction of the deepest part randomly punched out for a broken, rooty tip |
|
||||
|
||||
### `vacuumSettings`
|
||||
|
||||
| Field | Default | Effect |
|
||||
|---|---|---|
|
||||
| `radius` | `0` (mode default: 12 / 20 high / 8 fast) | How far past the footprint the terrain is bent before returning to its natural height |
|
||||
| `falloff` | `2.0` | Shape of that blend: 1 is a straight cone, 2 a gentle bowl, higher stays flat near the object then drops sharply |
|
||||
| `organicJitter` | `4` | Per-column wobble in the meeting edge, for `VACUUM_ORGANIC` |
|
||||
| `waveAmplitude` | `3` | Height of the rolling wave in `VACUUM_WAVY`; 0 removes it |
|
||||
| `waveScale` | `5.0` | Tightness of that wave; wavelength is roughly `100 / waveScale` blocks |
|
||||
|
||||
### `mode` values
|
||||
|
||||
| Mode | In-world result |
|
||||
|---|---|
|
||||
| `CENTER_HEIGHT` | One height sample under the middle carries the whole object; cheap, and correct on flat ground |
|
||||
| `MAX_HEIGHT` / `FAST_MAX_HEIGHT` | Nothing gets buried, but the object hangs off the downhill side of cliffs |
|
||||
| `MIN_HEIGHT` / `FAST_MIN_HEIGHT` | Nothing overhangs, but slopes swallow the uphill side |
|
||||
| `PAINT` | Every column drops to its own surface, so the object melts over whatever it lands on |
|
||||
| `STILT` / `FAST_STILT` | Highest-point seating with legs dropped to the ground under every bottom block |
|
||||
| `MIN_STILT` / `FAST_MIN_STILT` | Lowest-point seating with the same legs |
|
||||
| `CENTER_STILT` | Center seating with legs; the cheapest stilt worth using |
|
||||
| `ERODE_STILT` | Legs taper away from the centroid and break up near the tips, like eroded rock |
|
||||
| `ORGANIC_STILT` | Legs grow down to the first solid block with noise-varied lengths, connecting cave-floor objects to the floor |
|
||||
| `CEILING_HANG` | Object is flipped, hung from the cave roof, and rooted upward into it |
|
||||
| `VACUUM` | Terrain around the object is pulled up or cut down to meet its base, easing back out to radius 12 |
|
||||
| `VACUUM_HIGH` | Same, blended out to radius 20 for the smoothest transition |
|
||||
| `VACUUM_FAST` | Same at radius 8 with every other column sampled, for bulk use |
|
||||
| `VACUUM_ORGANIC` | Same with a per-column jittered edge, so the bowl reads as natural ground |
|
||||
| `VACUUM_WAVY` | Same with a smooth wave rolling across the slope, flat under the object and at the rim |
|
||||
| `FLOATING` | Placed in mid-air at an absolute height; terrain, water, cave, and support checks all skipped |
|
||||
| `STRUCTURE_PIECE` | Raw stamp at caller coordinates; internal to native structure routing, not for `objects[]` |
|
||||
|
||||
## 9. Worked examples
|
||||
|
||||
Rare surface camp:
|
||||
|
||||
```json
|
||||
{
|
||||
"place": ["clutter/camp1"],
|
||||
"chance": 0.00175,
|
||||
"rotation": { "enabled": true, "yAxis": { "enabled": true, "min": 0, "max": 270, "interval": 90 } }
|
||||
}
|
||||
```
|
||||
|
||||
Pine forest, kept off the steep ground and lightly dusted:
|
||||
|
||||
```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 } },
|
||||
"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
|
||||
{
|
||||
"place": ["clutter/stoneclutt4", "clutter/stoneclutt5"],
|
||||
"chance": 0.21,
|
||||
"density": 6,
|
||||
"carvingSupport": "CARVING_ONLY",
|
||||
"caveAnchorMode": "PROFILE_DEFAULT",
|
||||
"translate": { "y": -1 },
|
||||
"bottom": true,
|
||||
"force": true,
|
||||
"scale": { "size": 0.75 }
|
||||
}
|
||||
```
|
||||
|
||||
Ground-hugging mushroom carpet:
|
||||
|
||||
```json
|
||||
{
|
||||
"place": ["trees/mushroom/mushclut1", "trees/mushroom/mushclut2"],
|
||||
"chance": 0.15,
|
||||
"density": 2,
|
||||
"mode": "PAINT",
|
||||
"translate": { "y": -1 },
|
||||
"rotation": { "enabled": true, "yAxis": { "enabled": true, "min": 0, "max": 270, "interval": 90 } }
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Troubleshooting
|
||||
|
||||
**It never appears.** Work these in order.
|
||||
|
||||
1. Wrong key. `place` paths are case-sensitive and relative to `objects/`. A miss logs `Couldn't find Object: <key>` and burns the attempt.
|
||||
2. Chance too low to see. Prove the wiring with `"chance": 1, "density": 4` first, then dial back.
|
||||
3. Not actually in that biome. The chunk-center sample decides, not what you are standing on.
|
||||
4. Wrong list. `CARVING_ONLY` never places on the surface; the default `SURFACE_ONLY` never places in caves.
|
||||
5. Surface support (section 4). This one is silent. If the object appears with `force: true` but not without, a guard rejected it, and surface support is the likely one.
|
||||
6. Water. A submerged surface placement needs `underwater` or `onwater`.
|
||||
7. `clamp` too tight, or written in world Y instead of engine-internal Y.
|
||||
8. `slopeCondition` too strict — `maximumSlope` under about 1 excludes most terrain.
|
||||
9. Native structure overlap. Objects never write into a vanilla or datapack structure piece, not even with `force`.
|
||||
10. Console lines worth grepping: `Implausible object placement rejected` (bedrock-row anchor) and `Couldn't find loot table`.
|
||||
|
||||
**It appears but sits wrong.**
|
||||
|
||||
- Floating off a cliff: you are on `MAX_HEIGHT`. Switch to a stilt mode or a `VACUUM` mode.
|
||||
- Buried in a hillside: `MIN_HEIGHT`, or too much negative `translate.y`.
|
||||
- Hovering in a cave: `ORGANIC_STILT` for the floor or `CEILING_HANG` for the roof, with `carvingSupport: "CARVING_ONLY"`.
|
||||
- Standing on a flat disc of stilt blocks: use `VACUUM`, `VACUUM_ORGANIC`, or `VACUUM_WAVY` instead.
|
||||
- Riding one block above the grass: `translate.y: -1`, or `PAINT` for clutter.
|
||||
- Interior packed with terrain: `bore` for a blunt clear, `smartBore` to keep only the rooms.
|
||||
- `warp` does nothing: raise `warp.multiplier`; at the default of 1 the displacement truncates to roughly nothing.
|
||||
|
||||
**Loot never fills.** Only storage chests receive placement loot. Check the console for `Couldn't find loot table`, and drop `exact: true` if the filter's block data does not match the saved block exactly.
|
||||
|
||||
**Iterating quickly.**
|
||||
|
||||
- Studio worlds hotload both JSON and `.iob` edits within about a second, into newly generated chunks only. Non-studio worlds do not hotload at all.
|
||||
- Separate the object from the placement: `/iris object paste` proves geometry, natural generation proves the placement.
|
||||
- Deep forensics: write `chunkX,chunkZ[,radius]` into `plugins/Iris/goldendebug.txt` (or set `-Diris.goldendebug=`), then restart — the target is read once at startup. Every attempt, height query, pick, and rejection in those chunks is logged. Extremely verbose; use radius 0.
|
||||
@@ -1,725 +0,0 @@
|
||||
# 21 - Jigsaw Structures
|
||||
|
||||
An Iris jigsaw structure is a set of objects (pieces) that the assembler snaps together through matching connectors until it runs out of depth, space, or candidates. Jigsaw Studio is the Bukkit in-game workflow for authoring those pieces, and the resources it saves run on every supported platform through the shared core assembler. Planar mode is a constrained grid for village-like layouts; spatial mode is freeform for strongholds, towers, and multi-level rooms. A project created with the `VANILLA_PORTABLE` contract can also be exported as a strict Minecraft 26.2 vanilla datapack.
|
||||
|
||||
This page replaces the former in-game jigsaw instructions. General Studio behavior is in "10 - Studio & VSCode Schemas.md", placement context is in "18 - Structures Overview.md", and native/datapack structures are in "22 - Native Structures & Datapacks.md".
|
||||
|
||||
## Build a village kit
|
||||
|
||||
This walkthrough creates a planar project, adds a piece, wires its connectors, puts it in a pool, tunes the graph, and then generates it in a world.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- A Bukkit-family Iris server. The `/iris jigsaw` tree is Bukkit-only and player-only, and every subcommand uses the `iris.all` permission.
|
||||
- A writable pack under the Iris `packs/` directory. Use a disposable pack or a version-controlled copy: a committed graph transaction is persistent and there is no undo command.
|
||||
- No other Studio world opening or closing. Bukkit has one global Studio project/world lifecycle and one owning Jigsaw player session.
|
||||
|
||||
Inside an active Jigsaw Studio world, non-owner block edits and recognized mutating commands are cancelled.
|
||||
|
||||
### Step 1 — Create the project
|
||||
|
||||
```text
|
||||
/iris jigsaw create overworld village/demo
|
||||
```
|
||||
|
||||
`village/demo` is the structure key. It writes `structures/village/demo.json`, is the string Iris placements reference, and is what you pass to `open` later. `structure=` and `name=` are aliases for `key=`, not references to a separate vanilla structure.
|
||||
|
||||
With no optional arguments you get planar mode, Iris-native compatibility, 15x15x15 workcells, and Studio seed `1337`. `mode=` tab-completes `planar` or `spatial`, `compatibility=` completes `iris` or `vanilla`, and existing structure keys complete for `open`, `edit`, and `reopen`. Planar width and depth must each be at least `3`, X and Z cannot exceed `128`, Y must stay within `1..192`, and one workcell cannot exceed `2,097,152` blocks. Width and depth may differ.
|
||||
|
||||
Creation is add-only. Iris refuses any occupied or conflicting target rather than overwriting it.
|
||||
|
||||
**What lands on disk:**
|
||||
|
||||
```text
|
||||
structures/village/demo.json
|
||||
jigsaw-pools/village/demo/start.json
|
||||
jigsaw-pools/village/demo/pieces.json
|
||||
jigsaw-pools/village/demo/caps.json
|
||||
jigsaw-pieces/village/demo/{blank,end,straight,corner,tee,cross}.json
|
||||
objects/village/demo/{blank,end,straight,corner,tee,cross}.iob
|
||||
.iris/structure-manifests/key-<sha256>.json
|
||||
```
|
||||
|
||||
The start pool holds Cross Junction at weight `1`. The pieces pool holds End Cap, Hallway, L Junction, T Junction, and Cross Junction, and its direct fallback is the caps pool. The caps pool holds End Cap plus an empty termination entry. Resource keys stay `end`, `straight`, `corner`, `tee`, and `cross`. Every default piece is rotatable with weight and chance `1` wherever it is a pool member.
|
||||
|
||||
In an Iris-compatible project every piece belongs to theme `variant-1`, End Cap is terminal, mandatory caps are off, and an unresolved optional branch fails the whole assembly. A vanilla-compatible project omits Iris theme and terminal-rule metadata and terminates only the unresolved branch.
|
||||
|
||||
**What you should see:** Studio opens with you in creative above the Blank workcell, and all six workcells have their default variant loaded.
|
||||
|
||||
### Step 2 — Look around the Studio
|
||||
|
||||
```text
|
||||
/iris jigsaw status
|
||||
/iris jigsaw particles true
|
||||
```
|
||||
|
||||
A planar Studio has exactly six rotation-independent workcells in three columns by two rows: Blank, End Cap, and Hallway on the first row, then L Junction, T Junction, and Cross Junction on the second. Their stable IDs are `blank`, `end`, `straight`, `corner`, `tee`, and `cross`. Neighboring capacity columns and rows keep at least one clear block between them even when their sizes differ.
|
||||
|
||||
Each floor is light-gray wool, the canonical connector path is red wool, and every canonical face-center socket is capped with a sea lantern. There is no orientation, permutation, authored-piece, or derived-rotation gallery: rotations are solved at runtime.
|
||||
|
||||
Every workcell sits inside a physical white-concrete edge cage one block outside its editable capacity. Player-local particle trails outline the focused and nearby editable bounds inside those cages and draw a 1.75-block direction line out of each focused connector: lime for complete metadata with no Iris channel, red for incomplete identity metadata, or a deterministic per-channel color. Jigsaw Studio spawns no display entities for workcell bounds. `/iris jigsaw particles <true|false>` controls the workcell, connector, and temporary assembly diagnostics.
|
||||
|
||||
The Iris scoreboard switches to Jigsaw context automatically and reports the structure, workcell, variant, and one of Loading, Saving, Disabled, Read-only, Invalid, Unsaved, or Saved. `/iris studio scoreboard` toggles that sidebar for the current login.
|
||||
|
||||
### Step 3 — Make a piece
|
||||
|
||||
Open the controls three ways: right-click the generated control chest with the main hand, run `/iris jigsaw menu`, or sneak three times within 1.5 seconds. The six-row GUI shows the six workcells and pages only the variants belonging to the selected rotational archetype. Walking into a workcell selects it for your next menu open. Left-clicking a workcell in the GUI selects it, closes the menu, and teleports you to its horizontal center.
|
||||
|
||||
Left-click **Hallway**, then click **New Blank Variant**. Iris clones the active owned piece's complete metadata and every exact pool-entry membership into a service-named owned piece, creates an empty object with the source object's dimensions, closes the GUI while the graph transaction runs, and loads the new variant into Hallway. Reopen the menu after the completion message.
|
||||
|
||||
**What you should see:** a deterministic new key such as `village/demo/variants/straight/variant-1`, and an empty Hallway-sized volume you can build in.
|
||||
|
||||
Related actions on the same page:
|
||||
|
||||
- **Rename This Variant** and **Rename This Workcell** use an anvil text input. Labels are author-facing only; they never change piece keys, stable workcell IDs, or solver archetypes.
|
||||
- **Duplicate This Cell's Variant** clones the same complete metadata and every exact membership, and also copies the source object's bytes and its author-facing label. An End Cap clone keeps both its pieces- and caps-pool entries; a Cross Junction clone keeps both its start- and pieces-pool entries, including each entry's weight, chance, and other fields. Neither action guesses a first or lexicographically sorted pool.
|
||||
- Both actions need an active owned variant with at least one owned membership. For an empty or unassigned workcell, run `/iris jigsaw piece create <poolKey> <pieceKey>` so the pool is explicit. A non-owned variant cannot be duplicated or mutated.
|
||||
|
||||
### Step 4 — Build the piece and wire its connectors
|
||||
|
||||
Walk into Hallway and build inside its white-concrete cage. The workcell shows the active object's real blocks with connector blocks hidden by default. **Workcell Settings** toggles real `minecraft:jigsaw` overlays back on when you need to edit markers. If a shown connector gets broken, **Reset Connector Blocks** restores every saved connector coordinate without touching your other edits.
|
||||
|
||||
A planar piece authored facing some other direction is rotated into canonical orientation automatically. Block states, connectors, positions, and final states rotate with it, and capture applies the inverse rotation so the source resources stay coherent.
|
||||
|
||||
Configure each `minecraft:jigsaw` marker through Mojang's own block UI. A freshly generated Hallway variant already has north and south markers set to:
|
||||
|
||||
| Mojang field | Generated planar value |
|
||||
|---|---|
|
||||
| Name | `iris:planar` |
|
||||
| Target name | `iris:planar` |
|
||||
| Pool shown in the marker UI | `iris:village/demo/pieces` |
|
||||
| Joint | `ALIGNED` |
|
||||
| Final state | `minecraft:structure_void` |
|
||||
| Selection priority | `0` |
|
||||
| Placement priority | `0` |
|
||||
|
||||
The jigsaw block's `orientation` block state supplies its front and top directions. Marker pools must be written as `iris:<owned-pool-key>` in Mojang's UI; capture checks that namespace and stores the internal key without `iris:`. Do not move a generated planar marker off its face-center socket. For `VANILLA_PORTABLE`, use vanilla-valid namespaced connector identities and leave the Iris-only channel empty.
|
||||
|
||||
### Step 5 — Let autosave capture the work
|
||||
|
||||
Change one block, then wait two seconds without another workcell update. Iris marks the workcell dirty immediately and schedules capture after a 40-tick quiet period. A later change replaces the pending capture identity, and a busy capture retries until the save/load/graph barrier lets it through.
|
||||
|
||||
Autosave watches far more than block placement. Container inventory click, drag, and close events; internal inventory moves and hopper pickups; and furnace, brewing-stand, dispenser, and crafter activity are captured alongside block, fluid, growth, piston, redstone, explosion, interaction, and recognized command changes. Opening Mojang's jigsaw-block UI starts a five-tick owning-region NBT poll; a detected tile change marks the workcell dirty, and a following command, tool use, teleport or world change, quit, graph operation, **Flush Autosave Now**, close, or enabled-world unload first requests a final tile snapshot.
|
||||
|
||||
```text
|
||||
/iris jigsaw status
|
||||
/iris jigsaw save
|
||||
```
|
||||
|
||||
`status` reports whether an autosave is pending. `/iris jigsaw save` and the GUI's **Flush Autosave Now** request an immediate flush; if the final marker snapshot, another operation, or scheduler availability blocks capture from starting, the same pending ticket is kept and retried. Neither is required in the normal loop. Fresh untouched workcells report **Autosaved**, not pending. Every successful atomic workcell save plays one short bell for the owner.
|
||||
|
||||
**What connector order survives.** With connector blocks visible, autosave preserves the authored connector order for every marker still at the same source-local position, including markers whose metadata or orientation changed there. Removed markers disappear; new or moved markers append in deterministic X/Y/Z order. With connector blocks hidden, connector identity and order stay fixed while the exact ordinary block state and tile NBT placed at that coordinate are captured into the object and become that connector's final state. Duplicate source or captured positions reject the save.
|
||||
|
||||
**What a capture actually writes.** Iris reads the active owned variant across its exact displayed dimensions, converts jigsaw blocks into connector metadata, writes each connector's `final_state` into the object cell, replaces only the piece JSON connector array so omitted defaults and extension fields stay intact, compiles the complete owned graph, then commits the JSON, `.iob`, and manifest together. The Jigsaw service invalidates, reloads, evaluates, and rematerializes these graph resources directly instead of running ordinary Studio's full-engine pack hotloader. If an object crosses chunks, Iris snapshots every intersection on that chunk's owning region and starts the write only after the complete capture validates. A failed or incomplete capture writes nothing.
|
||||
|
||||
**Rewinding.** Each changed autosave keeps the previous complete owned closure in one per-project history file. Content blobs are deduplicated and only the newest five iterations are retained. **Undo Last Autosave** restores and removes the newest retained iteration through the atomic writer, so clicking it repeatedly rewinds up to five saves.
|
||||
|
||||
**When capture keeps failing.** Persistent validation or atomic-writer failures leave that mutation dirty, emit one console report with request, structure, workcell, and piece context, and retry after 2, 4, 8, 16, then at most every 30 seconds. A planar connector-topology mismatch is ordinary authoring validation, not a crash: it names the required and edited shape without a stack trace and points you at **Reset Connector Blocks**. A later edit clears that failure state, and a manual flush retries immediately without discarding it. Pending tickets resolve their workcell by stable ID after every committed graph reload, so saving one workcell cannot strand sibling autosaves on replaced layout objects.
|
||||
|
||||
### Step 6 — Read the preview
|
||||
|
||||
Every committed mutation triggers a background compile and a seed-`1337` assembly. The menu reports `PENDING`, `VALID`, `WARNING`, `INVALID`, or `STALE`, plus the selected theme, piece count, and current diagnostic.
|
||||
|
||||
Iris renders the assembled blocks on the negative-X side of the workcells and keeps them until replacement or Studio close, updating that read-only area after each later commit. Planar previews sit on the editing floor; spatial previews are lifted 48 blocks above it so a connected three-dimensional blob stays visually separate from the one-row editor. Click **Go to Preview** or run `/iris jigsaw preview goto` to teleport above it. The preview bounds are protected from players, fluids, pistons, explosions, growth, fire, entities, and redstone. The renderer accepts at most 250,000 explicit blocks; a larger assembly becomes `INVALID` with the render-limit diagnostic and is not rendered.
|
||||
|
||||
For a one-off diagnostic at another seed:
|
||||
|
||||
```text
|
||||
/iris jigsaw preview assemble seed=4242
|
||||
```
|
||||
|
||||
That command places no blocks. It draws bounded purple particle boxes for 10 seconds and does not replace the automatic seed-`1337` evaluation or the permanent block preview.
|
||||
|
||||
### Step 7 — Build pools and wire fallbacks
|
||||
|
||||
The three generated pools cover a basic village. Add more before you target them from new markers:
|
||||
|
||||
```text
|
||||
/iris jigsaw pool create village/demo/rooms
|
||||
/iris jigsaw pool create village/demo/end fallbackPoolKey=none
|
||||
/iris jigsaw rules fallback village/demo/rooms village/demo/end
|
||||
```
|
||||
|
||||
`pool create` makes an empty pool and can point it at an already owned direct fallback. `rules fallback <pool> none` clears one. Every change compiles the full owned graph before commit, so a missing pool or a fallback cycle is rejected rather than saved.
|
||||
|
||||
To place an existing piece into another pool, or to drop it out of one, use `/iris jigsaw piece add <poolKey> <pieceKey> [weight=1]` and `/iris jigsaw piece remove <poolKey>`. Removing a membership never deletes the owned piece or object.
|
||||
|
||||
### Step 8 — Tune variation
|
||||
|
||||
From the GUI:
|
||||
|
||||
- Each exact pool membership carries a positive relative weight and an independent `0%..100%` eligibility chance. GUI chance adjustments move in five-percentage-point steps. Chance is rolled before weighted selection.
|
||||
- **Themes & Piece Rules** sets a loaded owned variant's theme membership, allowed depth `0..30`, required and maximum placement count `0..512` (maximum `0` means unbounded), and terminal role.
|
||||
- **Duplicate All Enabled Cells as Family** allocates the next `variant-<n>` family, clones the currently loaded owned variant from every enabled workcell, duplicates their pool memberships, and atomically loads and assigns all clones to that one new theme. It either commits the complete family or changes nothing.
|
||||
- **Structure Themes & Caps** shows each family's current whole-assembly percentage and adjusts its relative weight.
|
||||
- **Mandatory Caps** requires every unresolved connector pool to go through its direct fallback and place a compatible terminal piece. The default End piece is terminal and the default pieces pool already points at the caps pool, so a new Iris-compatible project can turn this on without editing End first.
|
||||
|
||||
An assembly selects exactly one theme by positive theme weight, so it comes out entirely `variant-1` or entirely `variant-2`. Pieces do not mix families unless a piece belongs to both or has an empty theme list. Membership chance stays independent and is rolled per candidate after the family is chosen.
|
||||
|
||||
Piece themes, non-default chance, piece rules, and mandatory caps are Iris-only metadata. A graph that uses them is not `VANILLA_PORTABLE`.
|
||||
|
||||
### Step 9 — Size workcells and variants
|
||||
|
||||
Open **Workcell Settings** and stage capacity width, height, or depth by 1 or 8 without closing the menu. Click **Apply Cell Size** once all three values are ready. Iris then performs one live regeneration: it moves the white-concrete cages and active variants, keeps one clear block between capacity rows and columns (or spatial cells), rehydrates tile data, and moves you with the selected workcell. **Discard Size Changes** cancels the staged values without writing. Reopening Studio is only the recovery path if live regeneration fails.
|
||||
|
||||
Every planar workcell persists its own capacity, and changing it never rewrites a variant object. Capacity cannot shrink below any variant already assigned to that cell.
|
||||
|
||||
**Variant Size** gives the selected owned variant its own exact width, height, and depth within that capacity. Growth adds air. A safe shrink preserves in-bounds blocks and moves canonical connector payloads and sockets to the new face centers; cropping or a collision rejects the transaction without writes. **Resize This Variant to Capacity** is the one-click exact-size shortcut. A loaded resized variant reloads in place after commit, and sibling variants keep their own dimensions and bytes.
|
||||
|
||||
Disabling a planar workcell removes all pieces of that archetype from assembly and vanilla export while preserving its size and variants for later editing. Its translucent cuboid turns from light blue to red; Iris removes the tracked display when the origin chunk unloads and recreates it after that chunk loads again. Re-enable the workcell from the same settings page to restore participation.
|
||||
|
||||
### Step 10 — Take Toolbox sticks for repeated actions
|
||||
|
||||
Use the **Toolbox** page when you want an action available without reopening the chest. Clicking an entry gives you a named stick bound to the current Studio request and its workcell, variant, pool entry, or action. Right-click to use it. Resize, themes, and rules sticks open the relevant GUI context; other sticks run their exact bound action. A stick from a closed or replaced Studio is rejected. Destructive sticks need two right-clicks within 10 seconds.
|
||||
|
||||
### Step 11 — Set expansion limits
|
||||
|
||||
```text
|
||||
/iris jigsaw rules limits 12 8
|
||||
```
|
||||
|
||||
The first number is depth, the second is horizontal radius in chunks. Extended graphs accept depth `1..30` and radius `1..32`. A `VANILLA_PORTABLE` session restricts this to depth at most `20` and radius at most `8` chunks.
|
||||
|
||||
### Step 12 — Register the structure in a world
|
||||
|
||||
Attach the structure to a dimension, region, or biome with a `structures[]` placement, validate the pack, and generate new chunks. A complete placement example is under **Natural placement** below.
|
||||
|
||||
### Step 13 — Close Studio
|
||||
|
||||
```text
|
||||
/iris jigsaw close
|
||||
```
|
||||
|
||||
Wait for autosave, variant load, evaluation, or graph-update messages before replacing or closing Studio. Pending autosave blocks conflicting variant and graph operations. `close` refuses tracked work unless it is clean; `discard=true` deliberately abandons pending edits. Non-owners can use only Iris's strict informational and communication command allowlist. An integration that bypasses Bukkit mutation events must call `JigsawStudioService.markDirty(...)` or `markAllDirty(...)`.
|
||||
|
||||
**The walkthrough passes when** autosave commits the edited object and marker data, the automatic evaluation reaches `VALID` or an understood `WARNING`, the permanent seed-`1337` preview renders the expected family, pack validation succeeds, and a natural instance appears in newly generated chunks.
|
||||
|
||||
### How the Studio world opens
|
||||
|
||||
Jigsaw Studio does not run the ordinary Studio entry teleport and does not open a VSCode workspace before you enter the workcell.
|
||||
|
||||
It reuses the startup-loaded datapack runtime only while the pinned compiler-input fingerprint still matches every dimension, biome, and snippet JSON plus the compiler build and height policy. Jigsaw's structure, pool, piece, object, and ownership writes never alter those generated registries. A relevant input edit, an unavailable registry, or a changed, failed, or removed external datapack ingest invalidates reuse and falls back to the normal recovery and installation check, while a verified no-change check restores the prior pin.
|
||||
|
||||
The dedicated synthetic generator skips the procedural generation-cache warm, complete mantle-radius preparation, and native structure-start generation. Paper requests the one entry chunk urgently and asynchronously, and the owner is teleported once through the Jigsaw destination path after that chunk is retained and ready. The transient world sets `spawn_mobs=false` and independently cancels natural creature-spawn events; explicitly summoned test entities still work.
|
||||
|
||||
## Re-edit an existing Studio jigsaw
|
||||
|
||||
Do not run `create` again — creation is add-only. Reopen a Studio-owned graph by its original dimension and structure key:
|
||||
|
||||
```text
|
||||
/iris jigsaw open overworld village/demo
|
||||
```
|
||||
|
||||
`/iris jigsaw edit ...` and `/iris jigsaw reopen ...` are aliases. All three reconstruct workcell capacities and labels, enabled states, variant dimensions and labels, themes, rules, and pool memberships from the saved graph. Changes inside loaded owned variants autosave as usual; **Flush Autosave Now** only requests an immediate recovery flush and leaves blocked work queued for retry. The automatic seed-`1337` evaluation and permanent preview rebuild after each committed change. A loaded variant without editable ownership shows as Read-only and cannot be changed.
|
||||
|
||||
## Adopt an existing Iris graph
|
||||
|
||||
An existing Iris graph with no ownership manifest must be inspected and claimed before Studio will edit it:
|
||||
|
||||
```text
|
||||
/iris jigsaw adopt inspect overworld legacy/village target=auto strategy=auto
|
||||
/iris jigsaw adopt apply <plan-uuid>
|
||||
```
|
||||
|
||||
`inspect` reads the complete structure, pool, piece, object, and referenced loot closure asynchronously. It reports a plan UUID, target, resource and byte counts, structured warnings and errors, and one of `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED`. The default `auto` strategy claims an exclusive unowned closure in place; if any resource is shared with another structure, it plans a private clone instead. `target=auto` names that clone `<source>-studio`, then tries numbered suffixes without overwriting an existing target. Use `strategy=in-place` to require a claim with no resource-byte rewrites, or `strategy=clone target=<new-key>` to require a specific private copy.
|
||||
|
||||
Plans belong to the inspecting player, live in memory for 15 minutes, and are consumed once. Close any active or opening Jigsaw Studio before `apply`. Apply takes the pack mutation lock, re-hashes the pinned source and target read set, rejects an expired or stale plan without writes, and atomically commits the ownership manifest plus an adoption receipt. Success opens the owned target at Studio seed `1337`. Adoption metadata records source and target hashes and mappings for provenance; it does not give you a rollback command or a restorable preimage.
|
||||
|
||||
Automatic datapack imports carry `MANAGED_DATAPACK` ownership because removing or refreshing the source may clean or replace them. Iris detects that provenance during inspect, forbids in-place adoption, and plans a private clone while leaving the managed graph untouched:
|
||||
|
||||
```text
|
||||
/iris jigsaw adopt inspect overworld imported/key target=my-edits/key strategy=clone
|
||||
/iris jigsaw adopt apply <plan-uuid>
|
||||
```
|
||||
|
||||
## Convert a registered vanilla or datapack jigsaw
|
||||
|
||||
Raw registered structures are not Iris graph files, so they cannot go through `adopt`. Convert one registered jigsaw structure into a new add-only owned Iris graph, which then opens automatically:
|
||||
|
||||
```text
|
||||
/iris jigsaw convert overworld minecraft:village_plains target=village/plains seed=1337
|
||||
```
|
||||
|
||||
The source must be a live namespaced registry key and a jigsaw structure. With `target=auto`, `minecraft:village_plains` becomes `minecraft_village_plains`.
|
||||
|
||||
Conversion follows the registered start pool, reachable template pools, templates, connectors, weights, empty entries, and fallbacks, and stores source provenance and fidelity warnings in the ownership manifest. A native list pool entry stays one weighted choice: Iris keeps its recursively first physical template and outer connectors, while additional colocated children and their processors are omitted and recorded as `LIST_ELEMENTS` fidelity loss. A captured template with no non-air states is marked `collidable: false` so its connector-scaffold bounds can overlap an attached physical piece; nonempty converted pieces stay collidable.
|
||||
|
||||
Conversion does not preserve native placement settings beyond start pool, depth, and maximum distance. Feature pool elements, palette alternatives, processors, entities, and other native-only behavior can be omitted or merely reported. Keep the source native when those capabilities matter. Conversion is add-only and refuses occupied or conflicting targets rather than overwriting them.
|
||||
|
||||
## Build a spatial stronghold kit
|
||||
|
||||
Spatial projects use the same lifecycle without the planar cell constraints:
|
||||
|
||||
```text
|
||||
/iris jigsaw create overworld stronghold/demo mode=spatial width=32 height=24 depth=32
|
||||
```
|
||||
|
||||
A new spatial project starts with seven owned 15x15x15 variants laid out left to right in one horizontal row: **0 Connectors**, then **1 Connector** through **6 Connectors**. The connector sequence is cumulative north, south, east, west, up, then down, so each adjacent cell adds exactly one face-center socket. The first cell uses `workcell/spatial`; later cells use `workcell/spatial/<piece-key>`. Every cell sits one clear block from the next, and adding, deleting, or resizing variants regenerates the live row without reopening Studio.
|
||||
|
||||
The start pool contains all seven variants. The generated pieces pool contains variants 1 through 6 plus an explicit empty terminator; the connectorless editing piece is excluded because nothing can reach it as a child. Each generated piece defaults to at most 16 placements, so seed `1337` renders a bounded connected blob in the elevated spatial preview instead of one isolated piece.
|
||||
|
||||
Right-click the control chest or triple-sneak to select a cell, create another service-named variant, or duplicate the active owned variant. Build inside that variant's dedicated cell and configure its doorway, stair, shaft, floor, or ceiling connectors. Connector blocks are hidden by default, so ordinary blocks and block-entity data stay directly editable at the socket; **Workcell Settings** can show or reset the saved jigsaw blocks. Spatial connectors may use all 12 front/top orientations the jigsaw block supports.
|
||||
|
||||
Studio sizes the shared capacity to contain every reachable object and the horizontal footprint of its cardinal rotations, but automatic capture and per-variant resize preserve each variant's independent exact dimensions. Use **Resize to Capacity** or `/iris jigsaw piece expand` when only the selected object should become the full workcell size. Spatial workcell and variant labels are author metadata; `cellSize` and labels do not constrain runtime assembly.
|
||||
|
||||
Use `ROLLABLE` when the candidate's top direction should not constrain the join, and `ALIGNED` when it must match the source top after rotation. Iris still tries only cardinal Y rotations. A piece with `rotatable: false` is tried only at its authored rotation. The control-chest details view toggles that property, and Studio does not render separate rotation cells. Vanilla-portable variants must stay rotatable, so their GUI toggle is disabled once rotation is enabled.
|
||||
|
||||
Create additional owned pools before targeting them from new spatial markers:
|
||||
|
||||
```text
|
||||
/iris jigsaw pool create stronghold/demo/rooms
|
||||
/iris jigsaw pool create stronghold/demo/end fallbackPoolKey=none
|
||||
/iris jigsaw rules fallback stronghold/demo/rooms stronghold/demo/end
|
||||
```
|
||||
|
||||
**New Blank Variant** and **Duplicate This Cell's Variant** copy every exact owned pool entry assigned to the loaded source variant. The first creates empty same-sized geometry; the second copies the source object bytes. Neither selects a first or lexicographically sorted fallback pool. If the workcell has no active owned assigned variant, use `/iris jigsaw piece create <poolKey> <pieceKey>` to choose the pool explicitly.
|
||||
|
||||
## Studio workcells and canonical planar display
|
||||
|
||||
The surrounding platform uses a four-block checker pattern. Every complete workcell capacity is surrounded by a physical white-concrete edge cage one block outside the editable volume; enabled and disabled planar cells use the same material, and participation state stays visible in the GUI and scoreboard. Player-local particle trails outline focused and nearby editable bounds inside those cages, connector direction lines, the permanent live-preview bounds, and the explicit temporary arbitrary-seed diagnostic. No workcell-bound display entity is created.
|
||||
|
||||
The first workcell origin is `(16, 65, 16)`. Every workcell's bounds begin at Y 65, one block above its floor, and that origin is the displayed object's lowest unsigned corner. Planar projects use six cells in this exact three-by-two order. Each column uses the widest workcell in that column and each row uses the deepest workcell in that row, with one clear block between adjacent column and row envelopes. A smaller workcell can therefore have extra open space beside it, because its row and column stay aligned to the largest workcell in that envelope.
|
||||
|
||||
| Row | Workcell | Stable ID | Canonical open sides |
|
||||
|---|---|---|---|
|
||||
| 1 | Blank | `workcell/blank` | none |
|
||||
| 1 | End Cap | `workcell/end` | north |
|
||||
| 1 | Hallway | `workcell/straight` | north and south |
|
||||
| 2 | L Junction | `workcell/corner` | north and east |
|
||||
| 2 | T Junction | `workcell/tee` | north, east, and west |
|
||||
| 2 | Cross Junction | `workcell/cross` | north, east, south, and west |
|
||||
|
||||
Every planar footprint at Y 64 is light-gray wool. A one-block-wide red-wool glyph runs from its center toward each canonical side, and the endpoint on that workcell face is a sea lantern. The Blank workcell has no red path or connector cap. A disabled workcell keeps this floor while its translucent cuboid turns red; it stays selectable and editable but contributes no pieces to assembly or export. Spatial Studio lays every variant out as a dedicated cell in one row, has no topology glyph or enable toggle, and keeps `workcell/spatial` for its first cell.
|
||||
|
||||
The GUI groups planar pieces by rotational topology kind. West, east, south, and north end pieces are all variants of the one End Cap workcell; east-west and north-south pieces are variants of Hallway. When a variant loads, its source orientation is rotated clockwise into the archetype's canonical display, including directional block states, connector orientation and position, and connector final state. Capture applies the inverse rotation before writing the original piece and object resources. Pool memberships, weights, dimensions, labels, and the separate underlying piece resources are not merged by this display compaction.
|
||||
|
||||
`/iris jigsaw goto <workcell>` accepts a stable ID or the workcell name, case-insensitively. `/iris jigsaw select` selects the cell you are standing in, and simply entering a cell updates your next menu selection. An empty workcell, a read-only variant, an invalid render, incomplete marker hydration, a conflicting operation, or a stale Studio request is not capturable.
|
||||
|
||||
### Canonical planar sockets
|
||||
|
||||
For a planar piece whose source object dimensions are `X x Y x Z`, every connector must be horizontal, every connector top must be `UP_POSITIVE_Y`, and the canonically rotated object must fit its archetype workcell. Width and depth need not be equal. Integer division is floor division.
|
||||
|
||||
| Side | Position | Direction | Top |
|
||||
|---|---|---|---|
|
||||
| North | `(X / 2, Y / 2, 0)` | `NORTH_NEGATIVE_Z` | `UP_POSITIVE_Y` |
|
||||
| East | `(X - 1, Y / 2, Z / 2)` | `EAST_POSITIVE_X` | `UP_POSITIVE_Y` |
|
||||
| South | `(X / 2, Y / 2, Z - 1)` | `SOUTH_POSITIVE_Z` | `UP_POSITIVE_Y` |
|
||||
| West | `(0, Y / 2, Z / 2)` | `WEST_NEGATIVE_X` | `UP_POSITIVE_Y` |
|
||||
|
||||
New blank planar variants inherit the active source variant's exact dimensions and use those dimensions for these positions. Workcell capacity changes never rewrite sockets or object bytes. **Variant Size** and `variant resize` change only the selected owned object and move its canonical sockets to the new face centers; **Resize to Capacity** is the one-click version of the same thing.
|
||||
|
||||
Planar mode is a horizontal topology and validation contract, not a global wave-function-collapse solver. It does not backtrack across an entire map.
|
||||
|
||||
## Marker capture and connector rules
|
||||
|
||||
Jigsaw markers are real `minecraft:jigsaw` blocks while you edit. Saving reads their tile data and orientation and stores connectors in `jigsaw-pieces/<key>.json`; the marker itself is not retained as a jigsaw block in the `.iob`.
|
||||
|
||||
| Connector field | Studio source | Runtime rule |
|
||||
|---|---|---|
|
||||
| `position` | Marker offset from the workcell origin, inverse-rotated to source orientation during planar capture | Must be inside the object's unsigned `0..size-1` bounds |
|
||||
| `direction` | Jigsaw block front | Candidate must face the reverse direction after rotation |
|
||||
| `top` | Jigsaw block top | Must also match after rotation when the source joint is `ALIGNED` |
|
||||
| `pool` | Mojang Pool | UI value must be `iris:<owned-pool-key>`; Studio strips `iris:` and stores the internal pool used to choose the next piece |
|
||||
| `name` | Mojang Name | Identity this connector exposes to a source connector |
|
||||
| `targetName` | Mojang Target name | Must equal the candidate connector's stored `name` exactly; matching is case- and whitespace-sensitive at runtime, while Studio marker capture trims both values |
|
||||
| `channel` | `/iris jigsaw connector channel <channel\|none>` on a saved marker's exact local position | Values match exactly, including case and whitespace; empty matches only empty, and any non-empty value blocks vanilla export |
|
||||
| `joint` | Mojang Joint | `ROLLABLE` ignores candidate top; `ALIGNED` requires it to match |
|
||||
| `finalState` | Mojang Final state | Canonical block state written into the `.iob` at the marker cell; `minecraft:structure_void` leaves the cell absent, while explicit air remains an authored block state |
|
||||
| `selectionPriority` | Mojang Selection priority | Signed integer; higher-priority connectors within one piece are processed first, and ties keep authored order |
|
||||
| `placementPriority` | Mojang Placement priority | Signed integer on the source connector; higher-priority attached child pieces expand first, and ties keep attachment order |
|
||||
|
||||
Mojang's jigsaw UI has no field for the Iris `channel`. Let autosave capture the marker, look directly at it from within eight blocks in the loaded workcell, then run `/iris jigsaw connector channel <channel|none>`. The command maps the canonical display coordinate back to the source piece coordinate and transactionally updates that exact saved connector. It rejects a workcell with no active owned variant, a missing connector offset, whitespace inside a channel, and channels longer than 128 characters. It trims outer whitespace, `none` clears the channel, and all remaining characters keep their exact case. Runtime matching never trims either side, so whitespace in schema-authored data stays significant even though this command cannot author it. Reopen Studio to refresh the workcell and particle diagnostics. A non-empty update is rejected without a write in `VANILLA_PORTABLE`; vanilla marker fields stay owned by Mojang's UI and ordinary capture.
|
||||
|
||||
`final_state` must be a valid canonical Minecraft block state. Use `minecraft:structure_void` for an absent cell in a portable template, and the exact solid block state when the connector should leave a block behind. A final state of air is accepted and retained explicitly by capture, so it is not the same as an absent cell.
|
||||
|
||||
## How assembly chooses pieces
|
||||
|
||||
1. Iris selects one declared structure theme by positive relative weight. With no declared themes the assembly is unthemed, and a piece with no theme list is eligible for every selected theme.
|
||||
2. It filters the start pool by enabled planar workcell, selected theme, and depth and placement rules, then rolls each exact pool membership's independent chance. No passing membership is an intentional empty result, and an explicit `empty: true` winner also produces no structure.
|
||||
3. It chooses one positively weighted passing start entry and applies a random cardinal rotation when the piece is rotatable. A terminal start is placed but does not expand.
|
||||
4. It processes connectors on the current piece in descending `selectionPriority` order. For each connector it filters the primary pool by enabled workcell, theme, depth, maximum placements, terminal requirement, and chance, then tries passing entries in weighted random order. An eligible piece can still fail because its connectors are incompatible, it collides, or it exceeds bounds.
|
||||
5. When any eligible entry still needs its declared minimum placement count, those required entries take precedence over other entries. After expansion, an unmet graph-wide minimum produces `FAILED_RULES` rather than silently accepting the assembly.
|
||||
6. A candidate connector is compatible when source `targetName` exactly equals candidate `name`, source `channel` exactly equals candidate `channel` with case and whitespace preserved, the faces oppose after rotation, and an `ALIGNED` source also has a matching top direction.
|
||||
7. Two pieces whose `collidable` values are both `true` may not have overlapping bounding boxes. A piece with `collidable: false` neither blocks nor is blocked by another piece, but every piece must still stay inside `maxSizeChunks x 16` blocks of the assembly origin. Attached children are queued by the source connector's signed `placementPriority`, and Iris finishes one piece's connectors before expanding its children.
|
||||
8. Before maximum depth, Iris tries the primary pool and then that pool's one direct fallback; at maximum depth it skips the primary and tries only the direct fallback. An allowed explicit empty entry or an empty primary pool ends the branch immediately and does not continue into the fallback. When structure `requireCaps` or pool `mandatoryFallback` is true, the direct fallback must place a compatible terminal piece and an empty entry cannot satisfy it. Otherwise ordinary primary-plus-fallback exhaustion returns `FAILED_UNCAPPED` under `FAIL_ASSEMBLY`, or ends only that connector branch under `TERMINATE_BRANCH`. A fallback's own fallback is never traversed in the same selection. The runtime hard cap is 512 pieces.
|
||||
|
||||
The compiler reports missing resources, invalid workcells, bounds, connectors, themes, chances, and rules, plus fallback cycles, unreachable resources, uncappable required connectors, incompatible candidates, and sampled hard-cap failures. Studio reevaluates automatically after open and after every committed mutation, so `status`, the scoreboard, and the control GUI already show the current evaluation without a separate validation action.
|
||||
|
||||
## Commands and transactional ownership
|
||||
|
||||
`/iris jigsaw` aliases are `/iris jig` and `/iris jgs`. The tree is player-only and Bukkit-only, and all commands use the root `iris.all` permission.
|
||||
|
||||
The create/open `<key>` is the root structure's internal lowercase resource path, not a display name or a namespaced ID. `village/demo` maps to `structures/village/demo.json`, is referenced as `"village/demo"` by Iris placements, and is reused by `open`, `edit`, and `reopen`. Pool and piece keys use the same grammar: one or more slash-separated segments containing only `a-z`, `0-9`, `.`, `_`, or `-`, such as `village/demo/hall`. Only the marker UI adds the required `iris:` namespace to pool keys.
|
||||
|
||||
| Command | Behavior |
|
||||
|---|---|
|
||||
| `create <dimension> <key> [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic creation of a complete owned graph followed by an open request; `mode` completes `planar`/`spatial`, `compatibility` completes `iris`/`vanilla`, existing keys complete for `open`/`edit`/`reopen`, planar X/Z are `3..128`, spatial X/Z are `1..128`, Y is `1..192`, and one workcell volume is at most `2,097,152` |
|
||||
| `convert <dimension> <registered-key> [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, followed by Studio open; aliases `import`, `import-vanilla` |
|
||||
| `adopt inspect <dimension> <source> [target=auto] [strategy=auto]` | Asynchronously inspect a complete existing Iris closure and issue a 15-minute, hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; `strategy` completes `auto`, `in-place`, or `clone` |
|
||||
| `adopt apply <planId>` | Revalidate and atomically apply a plan owned by that player, then open the target with seed `1337`; no Studio may be active or opening |
|
||||
| `open <dimension> <key> [seed=1337]` | Map an existing graph into compact workcells; aliases `edit` and `reopen`; another owner, dirty work, or a conflicting lifecycle operation blocks replacement |
|
||||
| `close [discard=false]` | Close the transient Studio; refuses active autosave/load/graph work or a pending dirty capture unless `discard=true` deliberately abandons it |
|
||||
| `status` | Show structure, mode, compatibility, selected workcell dimensions and enabled state, variant count, whether autosave is pending, and the seed-`1337` evaluation/theme/piece result |
|
||||
| `menu` | Open the same workcell/variant/rules/toolbox GUI as the control chest or triple-sneak gesture |
|
||||
| `select` | Select the workcell containing the player |
|
||||
| `goto <workcell>` | Select and teleport above a stable workcell ID; alias `teleport` |
|
||||
| `particles <visible>` | Toggle player-local workcell-bound, connector, live-preview, and temporary assembly-preview particle trails |
|
||||
| `save [bay=selected]` | Flush automatic capture now for one dirty ready workcell; ordinary block and container changes already schedule this |
|
||||
| `connector channel <channel\|none>` | Look at a saved marker in the active owned workcell within 8 blocks and set or clear its Iris-only channel at the inverse-mapped source position; reopen to refresh the workcell and particles |
|
||||
| `bounds <width> <height> <depth>` | Set the selected workcell capacity without rewriting any variant object; all variants must fit, and the live aligned layout regenerates and rehydrates without close/reopen; aliases `cell`, `resize` |
|
||||
| `workcell capacity <width> <height> <depth>` | Explicit nested form of `bounds`; planar capacity belongs to one canonical archetype, and spatial capacity is the shared envelope for its one-row variant cells |
|
||||
| `workcell label <displayName>` | Set the selected workcell's author-facing label; quote spaces; canonical solver identity is unchanged |
|
||||
| `workcell label-reset` | Reset the selected workcell to its canonical solver label; alias `reset-label` |
|
||||
| `pool create <poolKey> [fallbackPoolKey=none]` | Create a new empty owned pool; a non-`none` fallback must already be owned by this project |
|
||||
| `piece create <poolKey> <pieceKey> [weight=1]` | Create and load a new owned variant; planar derives canonical connectors from the contextual workcell, spatial creates a connectorless blank |
|
||||
| `piece add <poolKey> <pieceKey> [weight=1]` | Re-add and load an existing piece/object already owned by this project |
|
||||
| `piece remove <poolKey>` | Remove the active variant from that pool without deleting its owned piece/object resources |
|
||||
| `piece rotatable <true\|false>` | Persist whether the active variant may use cardinal rotations; portable sessions reject `false` |
|
||||
| `piece expand` | Resize only the selected planar or spatial owned variant exactly to workcell capacity; planar sockets move to the resized faces |
|
||||
| `variant weight <poolKey> <weight>` | Set every matching entry for the active variant in that owned pool; weight must be positive |
|
||||
| `variant resize <width> <height> <depth>` | Resize only the active owned variant within workcell capacity; a safe shrink rejects cropped content and the active cell reloads in place |
|
||||
| `variant label <displayName>` | Set the active variant's author-facing label; quote spaces |
|
||||
| `variant label-reset` | Reset the active variant to its resource-key fallback; alias `reset-label` |
|
||||
| `variant duplicate` | Copy the active variant's object bytes, metadata, and exact pool memberships into one new variant in this workcell |
|
||||
| `variant duplicate-family [themeKey=next]` | Atomically clone every enabled workcell's active owned variant into one coherent Iris family and load the complete family; alias `family` |
|
||||
| `rules limits <maxDepth> <maxSizeChunks>` | Atomically set expansion depth and horizontal radius; portable sessions enforce `<=20` and `<=8` |
|
||||
| `rules fallback <poolKey> <fallbackPoolKey\|none>` | Atomically set or clear one owned pool's direct fallback after compiling the complete graph |
|
||||
| `preview goto` | Teleport above the permanent seed-`1337` block preview; alias `teleport` |
|
||||
| `preview assemble [seed=1337]` | Compute a deterministic read-only assembly at the player's coordinates, report its complete piece count, and show in-range bounds as purple particles for 10 seconds within the shared particle budget, without placing blocks |
|
||||
| `export [namespace=iris] [output=jigsaw-export] [format=zip] [replace=false]` | Start a background strict export of the clean on-disk graph as a Minecraft 26.2 directory or zip; completion is reported with the originating structure key |
|
||||
| `delete [confirm=false]` | With `confirm=true`, inspect reverse references, close Studio, and atomically remove the complete hash-pinned owned project; external references or changed ownership bytes block deletion; alias `remove` |
|
||||
|
||||
The control chest is the primary workflow. Its six-row GUI rechecks the exact Studio request before every callback and manages independent workcell capacities and labels, per-variant dimensions and labels, enabled states, rotation, exact pool-entry weights and chances, coherent themes, piece rules, mandatory caps, automatic evaluation, preview navigation, toolbox sticks, and destructive deletion. **Duplicate This Cell's Variant** creates one independent variant; **Duplicate All Enabled Cells as Family** clones and atomically loads one matching variant across every enabled cell. Accepted asynchronous actions close the GUI while work runs. Variant geometry and details are editable only for owned variants, and building and capture apply only to the loaded variant.
|
||||
|
||||
The Toolbox issues schema-`2` named sticks bound to the exact request, workcell, variant, and membership. Rename a variant or workcell stick in an anvil, right-click to apply its trimmed label, or sneak-right-click to reset. Labels allow at most 64 Unicode code points and reject control characters and section-sign formatting. Schema-`1` sticks and bindings from a closed or replaced request are stale. The active variant icon is a jigsaw block, a valid evaluation is an emerald, and lime dye is used only for the explicitly labeled theme-membership toggle.
|
||||
|
||||
### Ownership model
|
||||
|
||||
The ownership manifest stores the exact resource set, content hashes, source provenance, capabilities, and fidelity losses. Each mutation loads the complete owned graph, verifies current files against ownership, applies the change, compiles the result, stages backups, then commits the graph and updated manifest together.
|
||||
|
||||
Persisted graph mutations wait for autosave to clear dirty work so a pool, rule, size, theme, or variant transaction cannot erase blocks. Duplicate-one and duplicate-family requests clicked during dirty or active autosave queue once, expedite capture, and resume automatically only if the pinned request, session, and source variants still match.
|
||||
|
||||
Project deletion resolves a symbolic pack root to its real directory before both the reverse-reference scan and the hash-pinned removal. The final ownership and reference scan holds the same in-process and cross-process authoring locks through removal, so a coordinated write cannot add a dangling reference between validation and commit. A symbolic JSON resource or symbolic directory inside that pack fails the safety scan instead of hiding references.
|
||||
|
||||
A hash mismatch or an outside edit produces an ownership conflict and leaves authored files unchanged. Do not hand-edit transaction-owned resources between Studio transactions. There is no world-edit undo command for Jigsaw Studio: recover a wrong result from version control or a pack backup.
|
||||
|
||||
### Dirty tracking and platform notes
|
||||
|
||||
The session tracks dirty state per active owned workcell variant. Bukkit coverage includes block place/break/multi-place, buckets, inventory click/drag/close and internal move/pickup, furnace cook/burn/smelt, brewing start/fuel/complete, dispenser and crafter activity, block and entity explosions, entity block changes, right-click and physical interactions, redstone, liquid movement, form/grow/spread/fade/burn, pistons, structure growth, and recognized mutating vanilla or WorldEdit-like commands. A persistent owning-region watch compares jigsaw tile NBT after Mojang's UI opens, and transition commands and enabled lifecycle operations request one final snapshot and wait behind that watch before graph mutation or clean close. Interaction coverage deliberately prefers a harmless dirty false positive over losing a door, marker, container, machine, or switch edit.
|
||||
|
||||
Each workcell has a mutation generation: an autosave clears only the captured generation, so a later edit stays dirty for the next capture. Paper drains pending work synchronously during plugin disable. A forced Folia plugin disable is too late to schedule a new cross-region capture, so close Studio or wait for a clean `status` before reload or shutdown. External plugins that bypass these events must call `JigsawStudioService.markDirty(world, x, y, z)` or `markAllDirty(world)`.
|
||||
|
||||
Jigsaw Studio is globally single-project on Bukkit and belongs to one owning player session. Only that owner can open controls, switch variants, mutate the graph, or flush autosave. Non-owner direct block edits and recognized mutating commands are cancelled across the whole active Studio world. The control chest and permanent preview are protected against players, explosions, pistons, entities, fluids, growth, fire, and redstone. Generic Studio lifecycle calls cannot bypass the Jigsaw owner transition. Autosave, load, graph-mutation, open, close, and deletion barriers reject conflicts, and capture is project-global so concurrent workcells cannot produce stale full-graph lost updates.
|
||||
|
||||
### Capacity and per-variant object size
|
||||
|
||||
`bounds` and `workcell capacity` target the selected workcell. Planar workcells persist independent width, height, depth, enabled state, and display label; spatial mode persists one shared `cellSize` plus `spatialWorkcellDisplayName`. Planar capacity width and depth are `3..128`, spatial width and depth are `1..128`, height is `1..192`, and volume is at most `2,097,152`. Capacity is an upper bound for every variant in that workcell. A successful capacity change updates only structure JSON, verifies the complete graph, leaves every object byte unchanged, regenerates and rehydrates the affected live layout, and teleports the owner to the selected cell's new horizontal center when that cell moves. A failed live regeneration restores the prior layout and requires reopen only as an explicit recovery boundary.
|
||||
|
||||
`variant resize` and the **Variant Size** screen target one owned variant. The requested width, height, and depth must fit its workcell capacity. Growth and shrink preserve blocks and tiles at their in-bounds canonical coordinates, account for rectangular source rotations, and relocate each planar canonical connector and its stored block payload to the new face center. Shrink is lossless only: any stored block (including explicit air) or tile outside the target, a connector destination collision, connector tile data that cannot move safely, a read-only object, or an object shared by another piece rejects the transaction before any authored file changes. New growth volume is air. A loaded variant reloads in place after commit, and siblings keep their dimensions and bytes. Marker block-entity data is applied on its owning region before Iris verifies either the candidate or its rollback, so a live resize cannot reject a valid marker just because its NBT merge was deferred to the next tick.
|
||||
|
||||
Spatial capacity changes only the shared workcell envelope and rejects dimensions that do not contain every variant object. **Resize to Capacity** and `/iris jigsaw piece expand` change one selected spatial or planar object exactly to that envelope. The exact-size editor also permits a safe lossless shrink. Non-connector air and `minecraft:structure_void` cells are omitted from block entries; explicit authored air and connector final-state air stay distinct and are preserved.
|
||||
|
||||
Capture may cross chunks. Iris schedules each chunk intersection on its owning region, rejects unloaded or incomplete snapshots, aggregates them deterministically, then validates and performs one atomic owned-graph write. A scheduling failure, a Studio replacement or unload, a marker or tile read failure, a duplicate or missing snapshot, or a graph validation error aborts the whole capture before authored files change. This path has automated chunk-intersection and coordinator coverage; live multi-region Folia gameplay validation is still required.
|
||||
|
||||
For a rotated planar variant, capture moves each block-entity payload back to the inverse-rotated object coordinate while inverse-rotating the block state. The payload itself is unchanged, matching Iris object placement: modern Bukkit capture omits source position metadata and applies the payload at the explicit destination block. Directional behavior stored in block data rotates normally, and semantic values inside a tile payload remain author data.
|
||||
|
||||
## Resource reference
|
||||
|
||||
### Structure: `structures/<key>.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"startPool": "village/demo/start",
|
||||
"maxDepth": 7,
|
||||
"maxSizeChunks": 8,
|
||||
"mode": "PLANAR_JIGSAW",
|
||||
"compatibility": "IRIS_EXTENDED",
|
||||
"branchFailurePolicy": "FAIL_ASSEMBLY",
|
||||
"cellSize": {"x": 15, "y": 15, "z": 15},
|
||||
"spatialWorkcellDisplayName": "",
|
||||
"planarWorkcells": [
|
||||
{"displayName": "", "archetype": "BLANK", "width": 3, "height": 3, "depth": 3, "enabled": true},
|
||||
{"displayName": "Village Entrances", "archetype": "END", "width": 16, "height": 8, "depth": 16, "enabled": true},
|
||||
{"displayName": "", "archetype": "STRAIGHT", "width": 16, "height": 3, "depth": 3, "enabled": true},
|
||||
{"displayName": "", "archetype": "CORNER", "width": 3, "height": 3, "depth": 3, "enabled": true},
|
||||
{"displayName": "", "archetype": "TEE", "width": 3, "height": 3, "depth": 3, "enabled": true},
|
||||
{"displayName": "", "archetype": "CROSS", "width": 3, "height": 3, "depth": 3, "enabled": true}
|
||||
],
|
||||
"themeSets": [
|
||||
{"key": "variant-1", "weight": 1}
|
||||
],
|
||||
"requireCaps": false,
|
||||
"placeMode": "STRUCTURE_PIECE",
|
||||
"edit": [],
|
||||
"loot": []
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Default / range | What it does in world |
|
||||
|---|---|---|
|
||||
| `startPool` | required | Everything grows out of whatever piece this pool produces first |
|
||||
| `maxDepth` | `7`, range `1..30` | How many connector hops the assembler may chain outward before it stops adding rooms; higher values make sprawling settlements and cost more generation time |
|
||||
| `maxSizeChunks` | `8`, range `1..32` | A hard leash: no piece may sit further than this many chunks (times 16 blocks) from the start, no matter how much depth is left |
|
||||
| `mode` | Hand-authored schema fallback `SPATIAL_JIGSAW`; Studio `create` default `PLANAR_JIGSAW` | `PLANAR_JIGSAW` forces every piece onto a flat grid with face-center sockets and validates that shape; `SPATIAL_JIGSAW` lets pieces stack and branch in three dimensions |
|
||||
| `compatibility` | `IRIS_EXTENDED` | `VANILLA_PORTABLE` locks the graph down to what vanilla jigsaw resources can express, which is what makes datapack export possible |
|
||||
| `branchFailurePolicy` | `FAIL_ASSEMBLY` | Decides what a dead-end arm costs you: throw away the whole structure, or leave a stub where that arm stopped (`TERMINATE_BRANCH`, and required for vanilla portability) |
|
||||
| `cellSize` | `15 x 15 x 15`; Studio X/Z `1..128`, Y `1..192`, volume `<=2,097,152` | The editing volume for spatial cells, and the uniform editing volume for a legacy planar graph that has no `planarWorkcells`; it does not constrain runtime assembly |
|
||||
| `spatialWorkcellDisplayName` | empty | Text shown over the spatial editing cell; empty shows `Spatial` |
|
||||
| `planarWorkcells` | Six unique archetypes; width/depth `3..128`, height `1..192`, volume `<=2,097,152`; `displayName` empty | How large each connector shape may be built, what it is called in the GUI, and whether pieces of that shape are allowed into assemblies and exports at all |
|
||||
| `themeSets` | Empty means implicit unthemed; positive unique key weights | Splits the kit into visual families so one assembly comes out all-stone or all-timber instead of a mix; the weight is that family's share of assemblies |
|
||||
| `requireCaps` | `false` | Forces every open doorway to be closed off with a real terminal piece instead of being left hanging |
|
||||
| `placeMode` | `STRUCTURE_PIECE` | How each piece object meets the ground when stamped |
|
||||
| `edit` | empty | Block find-and-replace applied across every piece, for reskinning a kit without re-authoring objects; not portable |
|
||||
| `loot` | empty | Loot tables injected into containers the pieces place; not portable |
|
||||
| `vanillaSource` | empty | Records which registered structure this graph was imported from; not something you author |
|
||||
|
||||
`rules limits` owns `maxDepth` and `maxSizeChunks`. `rules fallback` owns direct pool fallback. The GUI owns workcell capacity and labels, theme weights, `requireCaps`, per-piece size, labels, themes and rules, chance, rotation, and deletion. `connector channel` owns a saved connector's Iris-only channel. Rules with no in-game control — `branchFailurePolicy`, `placeMode`, structure `edit`, structure `loot`, pool `mandatoryFallback`, and empty entries — stay schema-backed JSON fields. Transaction-owned projects reject outside resource edits on the next mutation, so use the Studio controls or recreate/adopt the project through an ownership-aware workflow.
|
||||
|
||||
### Pool: `jigsaw-pools/<key>.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"pieces": [
|
||||
{"piece": "village/demo/hall", "weight": 4, "chance": 0.75, "empty": false},
|
||||
{"weight": 1, "chance": 1.0, "empty": true}
|
||||
],
|
||||
"fallback": "village/demo/end",
|
||||
"mandatoryFallback": false
|
||||
}
|
||||
```
|
||||
|
||||
`weight` must be positive. `chance` is a finite `0..1` value that independently gates that exact membership before weighting; zero never passes and one always passes.
|
||||
|
||||
An `empty: true` entry canonically omits `piece`, though omitted and blank piece keys are both accepted for existing graphs. It terminates its branch only when empty termination is allowed, and it stops later primary or fallback candidates from being tried.
|
||||
|
||||
`fallback` names one direct pool tried after ordinary primary failure, or used alone at maximum depth; its own fallback is not chained into the same selection. A pool with no entries terminates when no fallback is required and does not continue into its declared fallback. `mandatoryFallback: true` applies the physical-terminal requirement to this pool even when structure `requireCaps` is false.
|
||||
|
||||
Native conversion never rewrites a start-pool member as empty and never omits it solely because it has no connectors. Every non-start connectorless member in a pool with a distinct fallback also stays physical, so weighted failed primary attachments still reach that fallback. Conversion emits `empty: true` only when a non-start pool has one all-air connectorless source member and either no fallback or a self-fallback. The same all-air connectorless member in a mixed no/self-fallback pool is omitted with an explicit selection-weight and RNG-consumption fidelity loss, rather than becoming an empty choice that could cut off later valid candidates. Other connectorless nonempty members in no/self-fallback non-start pools are omitted as inert with exact block, fallback, selection-weight, and RNG-consumption loss. Converted native graphs set `branchFailurePolicy: TERMINATE_BRANCH`, so ordinary optional candidate exhaustion ends only that connector branch.
|
||||
|
||||
### Piece: `jigsaw-pieces/<key>.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "village/demo/hall",
|
||||
"displayName": "Market Hall",
|
||||
"connectors": [
|
||||
{
|
||||
"position": {"x": 8, "y": 8, "z": 0},
|
||||
"direction": "NORTH_NEGATIVE_Z",
|
||||
"top": "UP_POSITIVE_Y",
|
||||
"pool": "village/demo/start",
|
||||
"name": "iris:planar",
|
||||
"targetName": "iris:planar",
|
||||
"channel": "",
|
||||
"joint": "ALIGNED",
|
||||
"finalState": "minecraft:structure_void",
|
||||
"selectionPriority": 0,
|
||||
"placementPriority": 0
|
||||
}
|
||||
],
|
||||
"rotatable": true,
|
||||
"collidable": true,
|
||||
"themes": ["variant-1"],
|
||||
"rules": {
|
||||
"minimumDepth": 0,
|
||||
"maximumDepth": 30,
|
||||
"minimumPlacements": 0,
|
||||
"maximumPlacements": 0,
|
||||
"terminal": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Positions are unsigned object coordinates, so `(0,0,0)` is the object's minimum corner. The referenced `.iob` is the geometry source and owns this variant's exact width, height, and depth; Studio materializes connector markers only in the authoring world.
|
||||
|
||||
| Field | Default / range | What it does in world |
|
||||
|---|---|---|
|
||||
| `object` | required | The blocks this piece stamps |
|
||||
| `displayName` | empty, at most 64 code points | Label shown in the Studio GUI; falls back to the piece key's final segment |
|
||||
| `connectors` | empty | Where other pieces may attach, which way they face, and which pool they come from |
|
||||
| `rotatable` | `true` | Lets the assembler spin the piece to cardinal Y rotations so it can meet a connector; `false` pins it to the orientation you built |
|
||||
| `collidable` | `true` | Whether this piece's volume reserves space against other pieces; set `false` only for a connector scaffold meant to sit inside a physical piece |
|
||||
| `themes` | empty | Which visual families may use this piece; empty makes it usable by every family |
|
||||
| `rules.minimumDepth` / `rules.maximumDepth` | `0` / `30`, range `0..30` | Keeps a piece out of the town square or out of the far outskirts; the start piece is depth zero |
|
||||
| `rules.minimumPlacements` | `0`, range `0..512` | Forces at least this many copies to exist, so a required well or church is not missing; an unmet minimum fails the assembly |
|
||||
| `rules.maximumPlacements` | `0`, range `0..512` | Caps how many copies appear; `0` means unbounded within the 512-piece safety cap |
|
||||
| `rules.terminal` | `false` | Marks the piece as a dead end: it can be placed on a connector but never opens new ones |
|
||||
|
||||
## Natural placement
|
||||
|
||||
Place an Iris jigsaw by adding an `IrisStructurePlacement` object to `structures[]` on a dimension, region, or biome. Surface-biome placements apply where that surface biome owns the start chunk. A cave biome contributes only placements whose resolved anchor is one of the cave modes. Region and dimension placements stay broader scopes.
|
||||
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"structures": ["village/demo"],
|
||||
"placementId": "village-demo-surface",
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 32,
|
||||
"separation": 8,
|
||||
"salt": 165745296,
|
||||
"anchor": "SURFACE",
|
||||
"minHeight": -64,
|
||||
"maxHeight": 320,
|
||||
"terrain": {"mode": "SOURCE"},
|
||||
"underwater": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Placement rule | Fields | Behavior |
|
||||
|---|---|---|
|
||||
| Random spread | `spacing`, `separation`, `salt` | One deterministic attempt per spacing grid cell; `spacing` must exceed `separation` |
|
||||
| Density | `density` | Independent deterministic per-chunk probability `0..1` |
|
||||
| Concentric rings | `ringCount`, `ringDistance`, `ringSpread` | Stronghold-like deterministic rings around world origin |
|
||||
| Surface | `anchor: SURFACE` | Surface Y must pass the inclusive `minHeight..maxHeight` gate |
|
||||
| Height band | `anchor: HEIGHT_BAND` | Deterministic random Y inside the inclusive band |
|
||||
| Legacy | `anchor: LEGACY` | `underground=false` resolves to `SURFACE`; `underground=true` resolves to `HEIGHT_BAND` |
|
||||
|
||||
`placementId` is the stable authored identity used for distribution. Set it when multiple placements share the same structure, or when you want unrelated field and list reordering to leave existing starts where they are. A placement listing several `structures` keys chooses one uniformly; pool weights control pieces inside the chosen graph, not world-level start frequency.
|
||||
|
||||
Only newly generated chunks use a changed placement. Direct `/iris structure place` and the Jigsaw Studio preview prove nothing about spacing, biome scope, height gates, or natural generation.
|
||||
|
||||
### Cave anchors
|
||||
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"structures": ["stronghold/demo"],
|
||||
"placementId": "stronghold-demo-deep-caves",
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 24,
|
||||
"separation": 8,
|
||||
"salt": 984211,
|
||||
"anchor": "CAVE_FLOOR",
|
||||
"minHeight": -48,
|
||||
"maxHeight": 80,
|
||||
"caveBiomes": ["carving/deep"],
|
||||
"caveAnchorAttempts": 12,
|
||||
"caveAnchorScanStep": 1,
|
||||
"caveMinimumClearance": 5,
|
||||
"terrain": {"mode": "PRESERVE"}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Anchor | Required carved-space geometry | Assembly alignment |
|
||||
|---|---|---|
|
||||
| `CAVE_FLOOR` | Solid/non-carved cell immediately below plus an upward carved run | Lowest assembled piece bound moves to the anchor Y |
|
||||
| `CAVE_CEILING` | Solid/non-carved cell immediately above plus a downward carved run | Highest assembled piece bound moves to the anchor Y |
|
||||
| `CAVE_CENTER` | Candidate is the actual midpoint of its contiguous carved cavern run, which must meet the clearance requirement | Assembly bounding-box midpoint moves to the anchor Y |
|
||||
| `CAVE_ANY` | A clearance-sized carved run is centered around the candidate | Assembly bounding-box midpoint moves to the anchor Y |
|
||||
|
||||
Iris tests up to `caveAnchorAttempts` deterministic, unique X/Z columns in the start chunk and scans the clipped `minHeight..maxHeight` band in increments of `caveAnchorScanStep`. It stops at the first column with matches and chooses deterministically among all valid anchors in that column. Runtime clamps attempts to `1..64`, scan step to `1..16`, and clearance to `1..64`, and visits at most 64 of the chunk's 256 columns. `caveMinimumClearance` is the required vertical carved run. Empty `caveBiomes` accepts any resolved cave biome; otherwise trimmed, case-normalized keys with or without a namespace are rechecked against the cave/mantle biome at the actual X/Y/Z anchor.
|
||||
|
||||
For cave anchors, `underwater` checks `MatterCavern` at the actual anchor rather than the surface ocean height. A null or non-cavern cell never qualifies. With `underwater: false`, ordinary cavern air must be above the dimension's `caveLavaHeight`, explicit water and lava are rejected, and forced-air cavern matter stays dry even below that threshold. With `underwater: true`, fluid cavern cells are allowed but the cell must still be carved cavern matter.
|
||||
|
||||
Cave placement scope is sampled at the start chunk's center. A cave-biome `structures[]` list contributes cave anchors only; region and dimension placements stay broader, and a placement-level `caveBiomes` list revalidates the actual anchor. Lookup uses existing Iris carved-space mantle data, so a locator cannot resolve an ungenerated distant cave anchor until terrain generation has produced that mantle.
|
||||
|
||||
The anchor test reads one vertical `MatterCavern` column, not the complete assembled volume, so `SOURCE` and `PRESERVE` can leave pieces intersecting cave walls. Use `BORE` or `FORCE_CARVE` when the structure must create a reliable envelope, or inspect the full volume in gameplay when preserving the cave. Cave anchors apply to editable Iris `structures`, not the `nativeStructures` backend.
|
||||
|
||||
## Vanilla datapack export
|
||||
|
||||
Create the project with `compatibility=vanilla`, then keep the graph inside the strict subset below. An existing graph is exportable only when its saved compatibility is `VANILLA_PORTABLE` and its branch policy is `TERMINATE_BRANCH`; Studio has no compatibility-toggle or branch-policy control. A vanilla-compatible Studio project writes that policy and omits the default Iris theme and terminal-rule metadata.
|
||||
|
||||
Export reads the committed graph, not pending workcell blocks. Wait for autosave to finish and confirm the automatic evaluation is no longer `PENDING`, `STALE`, or `INVALID`:
|
||||
|
||||
```text
|
||||
/iris jigsaw export namespace=demo output=village-demo format=zip replace=false
|
||||
```
|
||||
|
||||
Output is written under `<Iris data>/packs/exports/`. Compilation, NBT encoding, compression, and publication run off the server thread, so wait for the final result rather than treating the initial background-start message as success. One player cannot start a second export while their first is running, and the same normalized output cannot be published by two concurrent commands. Completion names the originating structure even if that Studio was closed or replaced while export was running.
|
||||
|
||||
`output` is one direct artifact name. It must be 1-128 characters, start with a letter, number, `_`, or `-`, and then use only letters, numbers, `.`, `_`, or `-`. Leading and trailing whitespace, `.`, absolute paths, slashes and backslashes, nested paths, and traversal names are rejected before export runs. `format=zip` adds `.zip` when needed. The publisher stages the complete directory or zip and replaces the destination atomically only when `replace=true`; an existing output is otherwise rejected.
|
||||
|
||||
The command emits a Minecraft 26.2 datapack whose `pack.mcmeta` uses `min_format: [107, 1]` and `max_format: 107`, plus a default `minecraft:plains` biome tag, an empty processor list, template pools, compressed structure-template NBT, one jigsaw worldgen structure, and one random-spread structure set. Command-level export defaults:
|
||||
|
||||
| Vanilla setting | Export default |
|
||||
|---|---|
|
||||
| Biomes | `minecraft:plains` |
|
||||
| Start height | absolute `0`, projected to `WORLD_SURFACE_WG` |
|
||||
| Generation step | `surface_structures` |
|
||||
| Terrain adaptation | `none` |
|
||||
| Expansion hack | `false` |
|
||||
| Maximum vertical distance | `4064` |
|
||||
| Structure-set placement | random spread: spacing `32`, separation `8`, salt `0`, frequency `1`, linear spread |
|
||||
|
||||
### Strict export blockers
|
||||
|
||||
Export fails rather than dropping or approximating any of these:
|
||||
|
||||
- Structure compatibility is not `VANILLA_PORTABLE`.
|
||||
- Structure `branchFailurePolicy` is not `TERMINATE_BRANCH`.
|
||||
- Structure themes, piece theme membership, non-default depth/placement/terminal rules, structure `requireCaps`, pool `mandatoryFallback`, or a membership `chance` other than `1` are present.
|
||||
- `placeMode` is not `STRUCTURE_PIECE`, or structure-wide `edit` or `loot` is non-empty.
|
||||
- `maxDepth` is outside `1..20`, or `maxSizeChunks x 16` exceeds Minecraft's 128-block horizontal limit.
|
||||
- A piece has `rotatable: false`.
|
||||
- A piece has `collidable: false`; vanilla templates have no per-piece collision flag.
|
||||
- A pool weight is outside `1..150`.
|
||||
- A resource key, connector name or target, namespace, orientation, block state, or final state is not vanilla-valid.
|
||||
- A connector has a non-empty Iris channel, duplicates another connector position, or has a `finalState` that does not exactly match the `.iob` block at that cell (`minecraft:structure_void` for an absent cell).
|
||||
- An object contains tile payloads, a block entity, a custom-content block, or retained `jigsaw`, `structure_block`, or `structure_void` marker blocks.
|
||||
|
||||
The exporter does not export tile or block-entity NBT, so a chest, spawner, sign, or other tile-bearing object blocks strict export even though it works fine inside Iris. The command exposes only namespace, one direct output filename, directory or zip format, and the replacement choice; biome, height projection, generation step, terrain adaptation, and structure-set placement stay at the fixed defaults above. Edit the emitted datapack afterward if those defaults are not the vanilla placement you want.
|
||||
|
||||
Test the exported artifact on an unmodded Minecraft 26.2 server or client: stop the disposable world, install the pack in that world's `datapacks/`, restart so the worldgen registries load it, confirm it is enabled without data errors, locate `<namespace>:<resourcePath>`, and generate fresh chunks around the located start. `/reload` can list a newly copied pack as enabled without registering its worldgen structure in the running world, so it is not a substitute for the restart. Iris validation and NBT round-trip tests do not substitute for the vanilla load and generation check.
|
||||
|
||||
## Failure recovery
|
||||
|
||||
| Symptom | Meaning | Recovery |
|
||||
|---|---|---|
|
||||
| Create reports occupied/conflicting files | Add-only ownership refused to overwrite existing resources | Choose a new structure key, or deliberately remove/migrate the old graph outside this workflow |
|
||||
| Create reports success but Studio does not open | The complete graph was created before the follow-up open request hit another owner, pending autosave, or a lifecycle transition | Resolve the active Studio guard, then run `open` for the new structure; do not rerun `create` against its now-owned files |
|
||||
| A loaded variant is Read-only | Its graph is unowned or has managed datapack provenance | Close Studio, run `adopt inspect`, review the disposition and diagnostics, then apply the plan; managed input must use a clone target |
|
||||
| Adoption plan is expired, unknown, or stale | Its 15-minute in-memory plan was consumed or expired, or a pinned source/target changed | Run `adopt inspect` again and review the new plan; no stale plan is written |
|
||||
| Conversion refuses the source | The key is absent, is not a live registered jigsaw, has an incomplete graph, or the add-only target is occupied | Keep it native, choose a valid registered jigsaw, repair its source datapack, or choose a new target; use `/iris structure import` for non-jigsaw templates |
|
||||
| Ownership conflict on capture/edit | An owned file changed outside the last committed transaction | Restore the exact owned graph from version control or backup; Studio will not overwrite the mismatch |
|
||||
| Close refuses with pending work | An owned workcell is dirty or autosave/graph work is running | Wait for autosave, use **Flush Autosave Now** to expedite it, or use `discard=true` only when losing pending edits is deliberate |
|
||||
| An external plugin edit is not captured | The plugin bypassed Bukkit's covered mutation events | Have the integration call `JigsawStudioService.markDirty(...)` for affected coordinates or `markAllDirty(...)`; autosave then follows normally |
|
||||
| Autosave has no active/editable variant | The workcell is empty or its loaded variant is read-only | Load an owned variant, or adopt/clone the graph first |
|
||||
| Autosave reports Loading, Invalid, or not hydrated | Variant materialization or real jigsaw block-entity hydration is incomplete or failed | Wait for completion, reopen or reload the variant, and do not build until the scoreboard reports a stable state |
|
||||
| Capacity succeeds but live regeneration reports a failure | The metadata committed, but one owning-region repaint or hydration step failed | Close and reopen Studio before editing; the persisted capacity remains authoritative |
|
||||
| Autosave says a chunk is not loaded | Part of the capture volume is unloaded | Visit or load the whole workcell; the autosave retry stays pending, or use **Flush Autosave Now** after loading it |
|
||||
| Multi-chunk autosave aborts | An owning-region schedule or snapshot failed, a chunk unloaded, Studio changed, marker/tile capture failed, or aggregation was incomplete | Keep the complete capture volume loaded and fix the reported cause; no graph file is written from a partial capture |
|
||||
| Marker capture fails | Marker NBT is incomplete, the final state is invalid, or active NMS cannot serialize the tile | Fix the named marker field, or use the matching supported Bukkit/NMS build |
|
||||
| The chest GUI closes after an action | The accepted operation is asynchronous and the GUI does not live-refresh | Wait for its player message, then right-click the chest again |
|
||||
| A named stick stops working | It uses schema `1`, its request ID belongs to a closed or replaced Studio, or the bound workcell/variant/pool entry changed | Discard the stale stick and take a schema-`2` replacement from the current Toolbox |
|
||||
| A queued duplicate cancels | The Studio request/session or one pinned source variant changed before autosave completed | Reopen the current controls, confirm the intended loaded source variants, and request the duplicate again |
|
||||
| Another player cannot edit or run a mutating command | The active Jigsaw Studio belongs to its activation owner | Have the owner do the work or close the Studio; do not bypass world protection |
|
||||
| Evaluation is `STALE` | A workcell edit is waiting for autosave | Wait for capture; evaluation reruns from the new committed graph automatically |
|
||||
| Evaluation is `INVALID` | Compilation or the seed-`1337` assembly failed | Fix the displayed first diagnostic; wrong pool, name, or facing, impossible rules, or an uncappable required fallback are the common causes |
|
||||
| Permanent preview is empty | Evaluation is pending or invalid, or seed `1337` intentionally produced no structure | Read the evaluation detail; fix invalid data, or change chance/start rules if an empty result was not intended |
|
||||
| Project deletion is blocked | Another JSON resource or ownership manifest still references a resource owned by the project | Remove or repoint the reported external reference, let autosave finish, then inspect deletion again |
|
||||
| Studio closes but project deletion fails | The hash-pinned removal failed after a successful close | The project files remain on disk; reopen or back them up before retrying |
|
||||
| Transaction reports cleanup required | The authored graph committed but staging cleanup failed | Preserve console output and remove or recover only the named transaction with operator care; do not re-author blindly |
|
||||
| Export is rejected | At least one strict portability blocker remains | Fix each reported diagnostic; do not bypass by deleting diagnostics or assuming Iris runtime success proves vanilla fidelity |
|
||||
| Export output name is rejected | The value is not one direct safe artifact name | Remove whitespace, separators, traversal, and unsupported characters, and keep the name within 128 characters |
|
||||
|
||||
## Verification checklist
|
||||
|
||||
Run this in a purpose-named disposable pack and world, and record each gate separately.
|
||||
|
||||
1. **Creation:** create a planar `IRIS_EXTENDED` project without optional mode, compatibility, dimensions, or seed. Confirm planar/Iris/15x15x15/1337 defaults, one structure, three pools, six pieces, six objects, one ownership manifest, tab completion of its key for `open`/`edit`/`reopen`, and no partial files after a duplicate-create rejection.
|
||||
2. **Default catalog:** confirm all six workcells have one loaded owned variant, `variant-1` is the selected theme family, End is terminal, and mandatory caps start off.
|
||||
3. **Workcell layout:** verify Blank/End Cap/Hallway then L Junction/T Junction/Cross Junction, one clear block between capacity rows and columns, light-gray floors, red canonical glyphs, sea-lantern endpoints, and no orientation or permutation gallery.
|
||||
4. **Controls and context:** confirm every untouched workcell starts **Autosaved**. Walk outside and into End Cap, verify the Iris scoreboard context and `Triple-sneak for controls`, then open the menu through the control chest, `/iris jigsaw menu`, and triple-sneak, confirming End Cap is selected each time without an inventory-view linkage error. Rename its workcell and active variant sticks in an anvil, apply them, verify the scoreboard shows the author names plus canonical role, then reset both labels.
|
||||
5. **Autosave:** change a solid block, a marker field, and container contents. Immediately click **Duplicate This Cell's Variant** and confirm autosave is expedited and the duplicate runs once automatically with no wait or retry instruction. Repeat with edits in multiple enabled cells and **Duplicate All Enabled Cells as Family**. Wait for the final clean state, reopen Studio, and verify all authored changes plus both clone operations round-trip.
|
||||
6. **Capacity and independent sizes:** stage Hallway capacity `16x3x3` in the open Workcell Settings menu, apply it once, and make another workcell capacity `16x8x16`. Confirm no existing object byte changes and that the live relayout moves only the white-concrete cages without close/reopen. In the larger workcell, resize one variant to `16x3x16` and another to `3x3x3`; confirm exact independent dimensions, live reload of the loaded variant, and unchanged siblings. Confirm cropped authored content, connector collision, and shared or read-only objects each reject the single-variant resize without writes.
|
||||
7. **Disable:** disable Tee, confirm its white-concrete cage remains while the GUI and scoreboard report Disabled, and confirm the seed-`1337` evaluation excludes Tee pieces. Re-enable it and confirm participation returns; test export filtering separately on the portable fixture.
|
||||
8. **Dynamic preview:** confirm evaluation moves through pending and stale to valid or an understood warning, reports theme and piece count, and renders the same protected block assembly on the negative-X side after reopen. Reach it through both **Go to Preview** and `/iris jigsaw preview goto`, and verify edits, fluids, pistons, explosions, growth, fire, entities, and redstone cannot alter it.
|
||||
9. **Variants and rules:** create a blank variant and duplicate one active variant; adjust one exact weight and chance; create `variant-2` through the all-enabled family action and confirm one exact-size clone per enabled workcell, duplicated memberships, and atomic active-family rebind. Change theme membership, depth and count rules, terminal status, and mandatory caps. Confirm only the selected resources change and invalid rules fail atomically.
|
||||
10. **Toolbox:** take schema-`2` named sticks for selection, capacity, per-variant size, labels, duplicate-one and family, preview, Flush Autosave, themes and rules, membership changes, caps, and deletion. Confirm bindings target the named context, active and valid icons are jigsaw and emerald, lime dye only labels theme membership, destructive tools require two uses, and schema-`1` or replaced-Studio tools are rejected.
|
||||
11. **Deletion:** delete one owned inactive planar variant only after another remains; a spatial variant removes its dedicated active cell as long as another spatial variant remains. Add an external placement reference and confirm project deletion is blocked; remove it, confirm deletion, and verify the complete owned closure plus manifest are removed.
|
||||
12. **Ownership protection:** have a second player attempt a direct edit, chest use, `/setblock`, `/fill`, and a WorldEdit-style mutation. Confirm each is denied across the active Studio world and the owner remains able to edit.
|
||||
13. **Adoption:** apply an exclusive unowned graph in place without changing resource bytes; require a clone for a shared graph; reject a stale plan without writes; and clone a managed datapack import without changing the managed source.
|
||||
14. **Registered conversion:** convert one registered jigsaw to an unused target, review fidelity warnings and provenance, and open the owned target. A non-jigsaw source and an occupied target must both fail without overwrite.
|
||||
15. **Folia multi-region boundary:** use a workcell crossing chunks and regions. A fully loaded capture commits once; an unloaded intersection aborts the entire write. The unit tests do not cover this — it has to be checked on a live Folia server.
|
||||
16. **Natural Iris placement:** attach the graph with a unique `placementId`, generate and inspect one natural start, restart, then repeat in new chunks. For cave placement, verify each requested anchor and a no-anchor skip.
|
||||
17. **Vanilla export:** create a separate `VANILLA_PORTABLE` graph with no Iris-only themes, chance, rules, or caps, export to zip, restart a clean Minecraft 26.2 world with it, locate the key, and inspect a natural instance. Do not substitute `/reload`.
|
||||
18. **Platform runtime:** copy the saved Iris pack to Fabric, Forge, and NeoForge, validate it, and prove natural shared-core assembly. Bukkit-only authoring controls are not expected on those loaders.
|
||||
|
||||
Automated tests, plugin startup, Bukkit gameplay, cross-loader generation, and vanilla datapack loading each prove something different — keep track of which of them you actually ran.
|
||||
@@ -1,624 +0,0 @@
|
||||
# 22 - Native Structures & Datapacks
|
||||
|
||||
Structures that come from outside Iris packs: vanilla structures in Iris worlds, datapack structures, the Minecraft structure-block and `.nbt` system, and converting native structures into editable Iris resources. Objects and Iris jigsaws are covered in `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, or datapack). Keys are namespaced: `minecraft:village_plains`, `towns_and_towers:village_ocean`.
|
||||
- **Iris structure** — an editable `structures/<key>.json` inside a pack.
|
||||
|
||||
Command listings are Bukkit/Paper; modded loaders expose a reduced set.
|
||||
|
||||
## Pick a task
|
||||
|
||||
The four things people actually want are separate workflows. None of them requires the others.
|
||||
|
||||
| Goal | Go to |
|
||||
|---|---|
|
||||
| A vanilla structure generates but sits badly in Iris terrain | Task 1 |
|
||||
| Add a third-party datapack (Terralith-style) to one Iris dimension | Task 2 |
|
||||
| Stop a structure or a whole namespace from generating | Task 3 |
|
||||
| Keep a datapack's buildings but choose where they go yourself | Task 4 |
|
||||
| Edit a registered structure's blocks or graph inside Iris | Task 5 |
|
||||
|
||||
Every task below changes newly generated chunks only. Nothing rewrites existing terrain or existing starts.
|
||||
|
||||
## Task 1: Make a vanilla structure fit Iris terrain
|
||||
|
||||
Native placement stays in charge of where and how often; you only adjust how the structure meets the ground.
|
||||
|
||||
Prerequisite: the structure appears in `/iris structure list <dimension>`.
|
||||
|
||||
1. Find the registered key with `/iris structure list <dimension>`.
|
||||
2. Run `/iris structure verify <dimension> radius=48` and confirm the key reports `[native-eligible]`, not `[disabled]` or `[unreachable]`.
|
||||
3. Merge one narrow `importedStructures.adjustments` entry into the declaring dimension. Start with one exact key and one operation such as `yShift`, `preserveSourceY`, or a terrain mode. This example changes only plains villages:
|
||||
|
||||
```json
|
||||
{
|
||||
"importedStructures": {
|
||||
"adjustments": [
|
||||
{
|
||||
"match": ["minecraft:village_plains"],
|
||||
"terrain": { "mode": "VACUUM" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Validate the pack, reopen Studio or update the test world's pack snapshot, then generate new chunks.
|
||||
5. Locate the key and inspect several starts.
|
||||
|
||||
**Success:** newly generated starts keep their native blocks, entities, processors, and loot, and the requested terrain operation is visible. Existing chunks are unchanged.
|
||||
|
||||
Widen from an exact key to a prefix only after the exact-key test passes, because a namespace or family prefix can hit many variants at once. If verify flips to `[disabled]`, remove the matching disable entry. If it says `[unreachable]`, fix the biome derivative mapping (see 1.2) before touching terrain.
|
||||
|
||||
Field details are in 1.5.
|
||||
|
||||
## Task 2: Install a third-party datapack for one Iris dimension
|
||||
|
||||
This Bukkit-family workflow keeps the datapack installed in Minecraft's global registry while Iris scopes the managed structure sets to the dimensions that declare the source.
|
||||
|
||||
Prerequisites: a disposable Bukkit-family server, one declaring Iris dimension, one nondeclaring Iris dimension, and a vanilla control world.
|
||||
|
||||
1. Add the Modrinth or direct archive URL to `datapackImports` in the declaring dimension only:
|
||||
|
||||
```json
|
||||
{
|
||||
"datapackImports": [
|
||||
"https://modrinth.com/datapack/towns-and-towers"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. Leave the URL out of the second test dimension, and keep the vanilla world as a control.
|
||||
3. Validate the pack, then run:
|
||||
|
||||
```text
|
||||
/iris datapack ingest restart=true
|
||||
```
|
||||
|
||||
4. After the full restart, run `/iris datapack list`, then `/iris structure list <declaring-dimension>` and pick one registered structure key from that source.
|
||||
5. Run `/iris structure verify <declaring-dimension> radius=48`. If the key is `[unreachable]`, set a compatible `vanillaDerivative` on an Iris biome before the generation test (see 1.2).
|
||||
6. Create fresh declaring and nondeclaring Iris worlds. In all three worlds, run `/locate structure <key>` and generate new chunks.
|
||||
|
||||
**Success:** the declaring Iris world locates and naturally generates the structure; the nondeclaring Iris world and the vanilla world do neither. Restart without deleting the installed datapack and confirm the same result.
|
||||
|
||||
If the key is absent after ingest, check that the managed pack appears in `/iris datapack list` and that the requested restart actually completed. Registry keys are never live on the boot that installs them. Removing a URL changes future per-world scope after a restart; it does not delete existing chunks or generated structures. Declaring the same URL in two Iris dimensions deliberately enables the source in both.
|
||||
|
||||
## Task 3: Turn a structure off
|
||||
|
||||
1. Get the exact key from `/iris structure list <dimension>`.
|
||||
2. Add it to `importedStructures.disabled` (family match) or `importedStructures.disabledExact` (that key only) in the declaring dimension:
|
||||
|
||||
```json
|
||||
{
|
||||
"importedStructures": {
|
||||
"disabled": ["minecraft:village", "minecraft:pillager_outpost"],
|
||||
"disabledExact": ["minecraft:ruined_portal"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Validate the pack, update the world snapshot or open a fresh world, and restart.
|
||||
4. Run `/iris structure verify <dimension> radius=48`.
|
||||
|
||||
**Success:** the key reports `[disabled]`, and `/iris goto structure <key>` answers that it is disabled by this dimension's `importedStructures` settings. New chunks no longer contain it; old chunks keep whatever already generated.
|
||||
|
||||
Two things catch people out. A namespace disable needs the trailing colon — `"nova_structures:"` works, `"nova_structures"` does not (see the prefix rules in 1.3). And neither deny list blocks an explicit `nativeStructures` placement, which is exactly what Task 4 relies on.
|
||||
|
||||
## Task 4: Place registered structures only where Iris says
|
||||
|
||||
Use this when a datapack's buildings should exist but its own structure sets should not decide where. Complete Task 2 first so the source is ingested and the server has restarted.
|
||||
|
||||
1. Confirm the keys with `/iris structure list <dimension>`.
|
||||
2. Merge this into that dimension. The `disabled` entry kills the datapack's own generation; the placement puts the building back on an Iris grid:
|
||||
|
||||
```json
|
||||
{
|
||||
"datapackImports": [
|
||||
"https://modrinth.com/datapack/dungeons-and-taverns"
|
||||
],
|
||||
"importedStructures": {
|
||||
"disabled": ["nova_structures:"]
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"placementId": "tutorial-native-tavern",
|
||||
"nativeStructures": [
|
||||
{ "structure": "nova_structures:tavern_oak", "weight": 1 }
|
||||
],
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 24,
|
||||
"separation": 6,
|
||||
"salt": 776215551
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. Validate the pack: `/iris pack validate pack=<dimension>`.
|
||||
4. Open a fresh test world, or update the world's pack snapshot and restart.
|
||||
5. Run `/iris structure verify <dimension> radius=48`. The tavern must report `[iris-planned]`, not `[disabled]`, because an explicit placement bypasses the deny list.
|
||||
6. Run `/iris goto structure nova_structures:tavern_oak`, generate the planned chunk, and inspect its native processors, entities, spawners, and loot.
|
||||
7. Generate several more grid cells.
|
||||
|
||||
**Success:** the structure appears at Iris-planned starts with full native fidelity and nowhere else.
|
||||
|
||||
If validation cannot resolve the key, the datapack is not live in the registry for that dimension — go back to Task 2 and finish the restart and scope check. If verify reports `[iris-not-found]`, raise the radius or lower the spacing for the test. Existing natural starts stay in old chunks after the namespace is disabled.
|
||||
|
||||
Field details are in section 3.
|
||||
|
||||
## Task 5: Convert a registered structure for editing
|
||||
|
||||
Conversion is deliberately less faithful than Minecraft's native runtime, so use it only when block geometry or graph topology has to change. `nativeStructures` keeps native processors, entities, spawners, loot, and placement behavior; conversion does not.
|
||||
|
||||
1. Confirm the registered source generates natively first.
|
||||
2. Back up the target pack. For one registered jigsaw graph, run:
|
||||
|
||||
```text
|
||||
/iris jigsaw convert <dimension> <namespace:path> target=auto seed=1337
|
||||
```
|
||||
|
||||
3. Conversion follows the registered start pool and the reachable template-pool closure, writes a new add-only owned Iris graph, reports the imported piece and pool counts plus the fidelity-warning count, then opens Jigsaw Studio. `target=auto` turns `minecraft:village_plains` into `minecraft_village_plains`; pass `target=<iris-path>` for a deliberate key.
|
||||
4. Load each variant from the Studio control chest or triple-sneak menu and inspect its real blocks and Mojang marker fields. Once a workcell finishes loading and hydrating, block and container changes autosave; **Save Now** only forces an immediate flush. Review the automatic seed-`1337` evaluation and the permanent read-only block preview before accepting the fidelity.
|
||||
5. For a non-jigsaw template or a bulk pass, use `/iris structure import <dimension>` instead (section 5). Review every per-structure result: successful bundles can sit alongside failures.
|
||||
|
||||
**Success:** the owned copy reopens with `/iris jigsaw open <dimension> <target>` and its preview matches the native original closely enough for your purpose.
|
||||
|
||||
`/iris jigsaw convert` accepts only a live registered jigsaw structure and refuses an occupied target. Both it and `/iris structure import` record source provenance and fidelity losses.
|
||||
|
||||
### What conversion changes about pools
|
||||
|
||||
A native list pool entry stays one weighted choice. Iris keeps its recursively first physical template and outer connectors, and omits later colocated children and their processors with a `LIST_ELEMENTS` warning instead of turning them into separate alternatives.
|
||||
|
||||
Every start-pool member stays a physical Iris piece even when it has no connectors. An all-air template with at least one connector stays a non-collidable scaffold so its bounds may overlap attached physical pieces. Every non-start connectorless member in a pool with a distinct fallback also stays physical, regardless of pool size or air content, so weighted primary no-match attempts still reach that fallback; a retained all-air member stays non-collidable.
|
||||
|
||||
A singleton all-air connectorless member with no fallback or a self-fallback becomes an explicit empty entry, recorded as `connectorless_all_air_member_normalized_empty`. The observed waystone form is the self-fallback case. The same member in a mixed no/self-fallback pool is omitted instead, recorded as `connectorless_all_air_mixed_member_omitted`, because converting it to empty could terminate the branch before later candidates get a chance; that loss records the changed selection weights and RNG consumption. Other connectorless nonempty members in no/self-fallback non-start pools are omitted as unattachable with `connectorless_non_air_member_omitted`, and that block loss also records exact fallback context plus selection-weight and RNG-consumption drift.
|
||||
|
||||
Converted graphs explicitly use `branchFailurePolicy: TERMINATE_BRANCH`: once ordinary primary and direct-fallback candidates are exhausted only that optional branch ends, while required physical fallbacks still fail. Explicit empty members and empty optional primary pools end the branch before the direct fallback is tried.
|
||||
|
||||
Native placement settings beyond start pool, maximum depth, and maximum distance may not survive conversion, and neither may feature pool elements, alternate palettes, processors, entities, or other native-only behavior.
|
||||
|
||||
## 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 affect newly generated chunks only.
|
||||
|
||||
### 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, and biome tags. Wins when set. |
|
||||
|
||||
Refinements: a sea-role biome whose derivative is not ocean- or river-like resolves to `minecraft:the_void`, and a shore-role biome falls back to `minecraft:beach`. **Non-`minecraft:` namespaces pass through** — point `vanillaDerivative` at a datapack or 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 through `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 and prefixes to deny. |
|
||||
| `disabledExact` | `[]` | Complete structure keys to deny without matching related variants. |
|
||||
| `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). |
|
||||
| `frequencyOverrides` | `[]` | Exact structure-set placement-density multipliers (1.4). |
|
||||
| `adjustments` | `[]` | Per-structure adjustments for structures still generating natively (1.5). |
|
||||
|
||||
#### Prefix matching
|
||||
|
||||
Used by `disabled` and `adjustments[].match`. Both sides are trimmed and lowercased, the key must start with the pattern, and then:
|
||||
|
||||
- Equal length → match.
|
||||
- Pattern ends with `:`, `/`, or `_` → match (so `"nova_structures:"` disables a namespace).
|
||||
- Otherwise the next character after the pattern must be `/` or `_`.
|
||||
|
||||
`"minecraft:village"` matches every village variant. `"nova_structures"` without the trailing colon does **not** match the namespace.
|
||||
|
||||
`disabledExact` trims and lowercases each complete key and then compares for equality only. `"minecraft:ruined_portal"` there disables the Overworld variant while leaving `"minecraft:ruined_portal_nether"` enabled. Use `disabled` when the whole family should go.
|
||||
|
||||
### 1.4 `frequencyOverrides[]`
|
||||
|
||||
Use this to make a registered native structure set more or less common without converting its structures into explicit Iris placements. Each entry is `{ "structureSet": "namespace:path", "multiplier": 0.01..16 }`. `structureSet` is an exact registered **structure-set key**, not a structure key, and the last normalized duplicate wins. Bukkit/Paper, Fabric, Forge, and NeoForge apply the same dimension-scoped contract to newly generated chunks.
|
||||
|
||||
```json
|
||||
{
|
||||
"importedStructures": {
|
||||
"frequencyOverrides": [
|
||||
{ "structureSet": "minecraft:nether_complexes", "multiplier": 1.1 },
|
||||
{ "structureSet": "minecraft:ruined_portals", "multiplier": 1.1 },
|
||||
{ "structureSet": "minecraft:nether_fossils", "multiplier": 1.1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Iris keeps the registered set's entries, weights, biome eligibility, placement algorithm, salt, exclusion zones, structure start and Y logic, processors, entities, mobs, loot, and native locate path. For random-spread placement it first scales Minecraft's placement probability up to `1`, then derives the nearest integer spacing with `round(oldSpacing / sqrt(remainingMultiplier))`, never below `separation + 1`.
|
||||
|
||||
Integer rounding means the realized change can land slightly under or over the request. At `1.1`, Nether complexes move from spacing `27` to `26` (about `7.8%` denser), ruined portals from `40` to `38` (about `10.8%` denser), and Nether fossils stay at `2/1` because no smaller legal spacing exists.
|
||||
|
||||
Concentric-ring sets can scale only their placement probability, so a ring placement already at probability `1` cannot get denser this way. Minecraft or modded custom placement types outside the affected override and exclusion-zone graph are untouched. An exact override, or an exclusion dependency on an overridden set, that would require copying an unsupported placement fails world binding instead of silently leaving stale exclusion behavior. Existing chunks and existing starts are never rewritten.
|
||||
|
||||
### 1.5 `adjustments[]`
|
||||
|
||||
Each entry (`match` selects targets by the same prefix rule):
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `match` | `[]` | Keys and 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}`: the structure midpoint lands in the band, deterministic per start chunk. |
|
||||
| `preserveSourceY` | `false` | Skip Iris burial repositioning and keep the 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 are removed.
|
||||
|
||||
**Merge:** `yShift` adds; `preserveSourceY` is OR-ed; `stilt`, `terrain`, and `yBand` are last-match-wins. Put the broad prefix first and the 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 the lowest surface Y of its footprint), and `minecraft:jungle_pyramid` (one block above the average surface Y).
|
||||
|
||||
#### Terrain modes
|
||||
|
||||
| Mode | Behavior |
|
||||
|---|---|
|
||||
| `SOURCE` (default) | Replay the structure's registered terrain adaptation, including vanilla BURY/ENCAPSULATE fill reimplemented with surrounding terrain material. |
|
||||
| `PRESERVE` | Disable terrain integration. |
|
||||
| `BORE` | Clear the padded piece volume (box) before placement. |
|
||||
| `FORCE_CARVE` | Clear the padded envelope using `shape`: `BOX`, `ROUNDED`, or `ERODED`. |
|
||||
| `VACUUM` | Raise surface terrain to the structure's ground planes with a fixed 12-block falloff. Never lowers ground. |
|
||||
| `ENCASE` | Fill the padded volume with solid blocks before placement (air and liquid only). The structure then carves its own interiors. `encasePalette` is optional; defaults are stone/deepslate in the Overworld, netherrack in the Nether, end stone in the End. |
|
||||
|
||||
Padding: `horizontalPadding` (0..128), `ceilingPadding` (0..128), `floorPadding` (0..64, where 0 preserves the floor). ERODED adds `erosionStrength` (default 0.8), `erosionFrequency` (0.07), `lobeFrequency`, and `lobeStrength` (0.85).
|
||||
|
||||
#### Examples
|
||||
|
||||
Stronghold pushed into a deep band and encased:
|
||||
|
||||
```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`
|
||||
|
||||
A 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"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A `datapackImports` URL belongs to the dimension that declares it. Bukkit exposes installed resources through the server-wide registry, but before initial chunks load Iris removes disallowed managed structure sets and structure definitions from each world's generation state. Vanilla worlds, and Iris worlds whose active dimension does not declare the source, therefore neither generate nor locate those structures. Declaring the same URL in multiple dimensions deliberately shares its structures. If multiple managed sources claim the same key, every owner must be declared, because the registry winner cannot be inferred safely.
|
||||
|
||||
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 and hash.
|
||||
|
||||
Downloads are checksum-verified when Modrinth publishes a hash, and size-capped.
|
||||
|
||||
### 2.2 Where files land, and when ingest runs
|
||||
|
||||
Installed datapacks are real Minecraft datapacks at `<level root>/datapacks/<id>/`, each carrying `.iris-managed.json`. Unmanaged datapacks are never touched, and the id `iris` is reserved. Cache, staging, and manifest live under `plugins/Iris/datapacks/`.
|
||||
|
||||
Ingest and recovery run synchronously inside Iris's startup admission gate when `general.autoIngestDatapacks` is enabled (default true). Players and every Iris world and Studio creation path stay locked until that phase is valid.
|
||||
|
||||
A persisted manifest, configuration, and content fingerprint lets an unchanged boot skip remote resolution and full revalidation, and Iris refreshes that fingerprint after its own authorized post-start import maintenance. A change to the URL, the Minecraft or Iris version, the override policy, external manifest edits, staging, the transaction, installed content, or cache corruption invalidates reuse and runs the full fail-closed path.
|
||||
|
||||
Minecraft builds worldgen registries at server start, so a **newly installed or repaired** datapack needs a clean restart before admission. After that returns, its keys are live only in the per-world structure state of declaring Iris dimensions.
|
||||
|
||||
Cache reuse is a local validation decision and does not poll remote sources; run `/iris datapack ingest` when you want an update check. Every successful ingest persists fresh staging and installed-target receipts, so unchanged bootstrap recovery leaves the manifest stable and the next startup can reuse the cached fingerprint.
|
||||
|
||||
Scratch validation rejects links, junction-like special files, and real cross-volume entries. On Windows with Java 25, Iris also verifies the drive root and volume serial when the JDK reports unequal `FileStore` identities only because a path crossed the legacy 247-character prefix boundary. Unresolved cleanup, identity, transaction, or validation failures stay blocking and create no world artifacts.
|
||||
|
||||
### 2.3 Manual commands
|
||||
|
||||
```
|
||||
/iris datapack ingest [restart=false] (alias: pull)
|
||||
/iris datapack list (alias: ls)
|
||||
/iris datapack remove <id> (alias: rm)
|
||||
```
|
||||
|
||||
`ingest` downloads each distinct URL declared by any loaded dimension while keeping the per-dimension ownership relationship used by generation and locate state. `restart` defaults to false, and Iris tells you a restart is required. `remove` refuses unmanaged datapacks — also delete the URL, or a later startup ingest reinstalls it. Scope changes do not delete installed datapacks, previously generated chunks, or existing structures.
|
||||
|
||||
### 2.4 Usage patterns
|
||||
|
||||
**(a) Natural generation.** Import, restart. Check with `/iris structure list <dimension>` (which writes `<pack>/.iris/structure-index.json`) and `/iris structure verify <dimension>` (`[native-eligible]` versus `[unreachable]`). Fix unreachable biomes with `vanillaDerivative`, or use (c).
|
||||
|
||||
**(b) Replace vanilla.** Disable the vanilla families and let the 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` — see 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. This resolves **globally**: one dimension setting it `false` strips for all. Non-`minecraft:` content is unaffected, so disable those keys explicitly.
|
||||
|
||||
## 3. Placing specific native structures (`nativeStructures`)
|
||||
|
||||
`structures[]` on a dimension, region, or biome hosts two backends, and each placement uses exactly one:
|
||||
|
||||
- `structures: ["<iris key>"]` — Iris assemblies (`21 - Jigsaw Structures.md`).
|
||||
- `nativeStructures: [{ structure, weight, jigsaw }]` — registered structures run through Minecraft's own machinery at Iris-chosen points, with 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 the sources in this placement. |
|
||||
| `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 and ceiling), and `liquidSettings`. Null or unset values preserve the registered definition. |
|
||||
|
||||
Placement grid fields (`distribution`, `spacing`/`separation`/`salt`, `density`, rings, heights, `underground`, `underwater`, `placementId`) match the **Natural placement** section of `21 - Jigsaw Structures.md`, except that 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 to stay inside Minecraft's 128-block (8-chunk) structure reference range.
|
||||
|
||||
### 3.2 `disabled` and `disabledExact` never block an explicit placement
|
||||
|
||||
The placement injector generates planned starts without consulting either deny list, and it bypasses the structure's own biome filter. Both "disable the namespace, re-place explicitly" and "deny an exact key, replace it explicitly" are supported.
|
||||
|
||||
### 3.3 `nativeSuppression: REPLACE_SOURCE`
|
||||
|
||||
- **Dimension-level placements only** — anywhere else it is a blocking pack error.
|
||||
- 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 guarantee output, because there is no native fallback. An Iris-backend `REPLACE_SOURCE` that produces nothing throws at runtime. Native-backend unusable starts are recorded invalid and skipped silently, and the source stays 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` or `[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 and load `.nbt` templates; jigsaw blocks wire pools together. Iris Jigsaw Studio is the documented in-game workflow for editable Iris graphs, so reach for external vanilla Structure Block and datapack references only when authoring raw `.nbt` assets outside Iris.
|
||||
|
||||
How an authored `.nbt` reaches an Iris world:
|
||||
|
||||
**(a) Through a datapack (native generation).** Ship it under `data/<ns>/structure/`, add `worldgen/template_pool`, `worldgen/structure`, and `worldgen/structure_set`, zip it, host it or publish to Modrinth, add the URL to `datapackImports`, then `/iris datapack ingest restart=true`. From there use natural generation, `adjustments`, or `nativeStructures`.
|
||||
|
||||
**(b) Import into Iris resources.** `/iris structure import <dimension>` (section 5). The template pass enumerates **registered** templates only — loose saves in `<world>/generated/` are not enumerated, so package them into a datapack first.
|
||||
|
||||
Template-import fidelity is lossy by design: first palette only; structure voids and structure blocks dropped; jigsaw blocks resolved to `final_state` (the graph is rebuilt by the separate jigsaw pass); entities not converted; block entities captured.
|
||||
|
||||
## 5. Importing native structures into Iris resources
|
||||
|
||||
You do not need import just to place something — `nativeStructures` places any registered key with full fidelity. Import only when you need Iris object, pool, or piece resources. Manual imports are editable transaction-owned copies; automatic datapack imports stay managed by ingest and must be cloned before Jigsaw Studio will edit them.
|
||||
|
||||
### 5.1 `/iris structure import <dimension>`
|
||||
|
||||
Four passes, always overwriting its own previous output:
|
||||
|
||||
1. **Jigsaw rebuild** — registered jigsaw structures become editable pools, pieces, and objects. Connector `final_state`, signed `selection_priority`, and signed `placement_priority` values are retained in the Iris piece metadata, and the generated root writes `branchFailurePolicy: TERMINATE_BRANCH` so unmatched optional branches keep native termination behavior.
|
||||
2. **Template import** — registered `.nbt` templates become `objects/<name>.iob` plus a single-piece `jigsaw-pieces/<name>.json`.
|
||||
3. **Template groups** — fixed multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils) become one Iris structure each, with every variant in the pool.
|
||||
4. **Capture** — only non-jigsaw registry keys for which the first pass found no same-key template are captured through a scratch world. This pass never rewrites a successful or failed jigsaw conversion; the standalone `/iris structure capture <dimension>` command stays unfiltered. Structures spanning more than **48 blocks** on any axis are skipped, so strongholds, mansions, and monuments stay native-only.
|
||||
|
||||
Naming: `minecraft:village_plains` becomes `minecraft_village_plains`. Generated structures carry `vanillaSource` for locate and `REPLACE_SOURCE`.
|
||||
|
||||
`/iris studio importvanilla <dimension> [variants=3] [structures=true]` also imports vanilla trees and features as objects, plus the structure passes when `structures=true`.
|
||||
|
||||
### 5.2 Ownership, manual editing, and `unowned_resource`
|
||||
|
||||
Imports use per-bundle ownership manifests under `<pack>/.iris/structure-manifests/`. A failure looks like:
|
||||
|
||||
```
|
||||
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, and the hash no longer matches. Rename the target, restore the exact owned bytes, or leave the key native.
|
||||
|
||||
A successfully converted or manually imported jigsaw opens directly with `/iris jigsaw open <dimension> <key>` because its ownership manifest is editable. A pre-existing Iris graph with no manifest uses the `adopt inspect` then `adopt apply` workflow in `21 - Jigsaw Structures.md`; no import command is needed for that case.
|
||||
|
||||
### 5.3 Automatic datapack import
|
||||
|
||||
`general.autoImportDatapackStructures` (default **false**) converts each ingested datapack's structures into pack resources on ingest. Those bundles carry `MANAGED_DATAPACK` provenance: ingest refresh owns them, and removing the source URL may clean them. Jigsaw Studio therefore shows their variants as read-only and refuses an in-place ownership claim. Inspect and apply a private clone before editing:
|
||||
|
||||
```text
|
||||
/iris jigsaw adopt inspect <dimension> <managed-iris-key> target=<editable-key> strategy=clone
|
||||
/iris jigsaw adopt apply <plan-uuid>
|
||||
```
|
||||
|
||||
Inspect verifies the existing manifest is exactly a managed vanilla or datapack Iris assembly, pins the complete source and target read set, and reports `CLONE_REQUIRED` or a blocking diagnostic. Apply re-hashes under the pack mutation lock, atomically writes a deep clone with deterministic internal reference rewrites plus its ownership receipt, leaves the managed source unchanged, and opens the editable clone. An expired, consumed, or stale plan writes nothing. There is no adoption rollback command, so keep the pack backup you made before converting.
|
||||
|
||||
Automatic import is off by default because native generation and `nativeStructures` never need the copies, and conversion can write thousands of files. Deterministic source-content and graph-validation failures retain the bundles that did write and record the attempted source, importer format, and target-pack revision, so the same failures do not repeat every boot; a source update, importer-format change, or different target retries them. Unexpected reflection, I/O, transaction, and runtime failures stay pending and retry. Removing a URL from `datapackImports` cleans only bundles still owned by that managed source, and an adopted editable clone is independent.
|
||||
|
||||
Third-party jigsaw templates using the legacy slab property `half=top|bottom`, or the exact known misspelling `minecraft:chisled_polished_blackstone`, are normalized to current Minecraft block data during editable conversion. Other invalid final-state values are recorded as fidelity loss and omitted without internal-error telemetry. Invalid structure graphs stay per-structure failures, and expected graph-contract rejections are reported as concise import results instead of internal Iris stack traces; unexpected reflection, I/O, and runtime failures keep full diagnostic traces.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
`structure place` resolves the graph and edit resources from the named dimension pack, then stamps the assembled pieces into the player's current world. The pack's Studio and generation engine do not need to stay open for this explicit placement.
|
||||
|
||||
`verify` tags: `[iris-planned]`, `[iris-not-found]`, `[iris-search-limit]`, `[disabled]`, `[unreachable]`, `[native-eligible]`, `[error]`. Placements are checked first, so a disabled-but-placed key shows as `[iris-planned]`.
|
||||
|
||||
### Traps
|
||||
|
||||
- Worlds snapshot the pack. Push changes with `/iris developer update-world world=<w> pack=<dim> confirm=true`, then restart. Back up first.
|
||||
- Optional args are keyed: `radius=200`, not a bare `200`.
|
||||
- New datapack structures need a restart before the registry knows them.
|
||||
- Only new chunks change.
|
||||
- Namespace disables need the colon: `"nova_structures:"`.
|
||||
- `REPLACE_SOURCE` has no fallback, so 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 jigsaw convert <dimension> <source>` | `import`, `import-vanilla` | `target=auto seed=1337`; Bukkit player only; source is a registered jigsaw key |
|
||||
| `/iris jigsaw adopt inspect <dimension> <source>` | | `target=auto strategy=auto`; Bukkit player only; source is an existing Iris graph |
|
||||
| `/iris jigsaw adopt apply <planId>` | | Bukkit player only; no active or opening Jigsaw Studio |
|
||||
| `/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 in `plugins/Iris/settings.json`: `general.autoIngestDatapacks` (default true), `general.autoImportDatapackStructures` (default false).
|
||||
@@ -1,576 +0,0 @@
|
||||
# 23 - Loot, Entities, Spawners, Markers
|
||||
|
||||
Loot tables decide what appears inside generated chests and what custom mobs drop. Entities describe a mob and its gear. Spawners decide when and where those entities appear. Markers pin spawners to specific blocks inside placed objects. This page explains how each system fires at runtime, then documents every field.
|
||||
|
||||
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`.
|
||||
|
||||
## The mental model
|
||||
|
||||
Two independent pipelines share the loot table format.
|
||||
|
||||
**Containers.** When a chunk finishes generating, Iris walks the blocks it recorded and fills every storage chest it placed. It builds a list of loot tables for that exact block, then rolls each one and drops the results into the inventory. The list comes from up to four sources, in this order: the object placement that owns the block, then the dimension, region, surface biome, and cave biome the block sits in. Each source can add to the list, wipe it, or only contribute when nothing else did.
|
||||
|
||||
**Ambient mobs.** A background loop ticks each Iris world roughly twice a second. Each tick it measures how crowded the world is, and if there's room it picks a handful of loaded chunks and tries one spawn in each. A spawn attempt gathers every spawner the dimension, region, and surface biome list, throws out the ones whose time/weather/rate/crowding gates fail, pools their entries, picks exactly one, and places one to a few mobs.
|
||||
|
||||
Markers bolt the second pipeline onto the first: an object placement can tag specific blocks it places, and a marker definition attaches spawners to whatever carries that tag. That's how you get mobs that appear inside a specific ruin rather than anywhere in the biome.
|
||||
|
||||
Everything below is deterministic from the world seed and the block position, so the same chest at the same coordinates always contains the same items.
|
||||
|
||||
## Walkthrough: put custom loot in a dungeon chest
|
||||
|
||||
The goal is a chest inside one placed object that rolls your table and ignores the pack's global tables. Prerequisites: a validating pack, an `.iob` object containing at least one chest, and a biome that places it.
|
||||
|
||||
**1. Write the table.** `loot/tutorial/dungeon-cache.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial Dungeon Cache",
|
||||
"rarity": 1,
|
||||
"minPicked": 2,
|
||||
"maxPicked": 4,
|
||||
"maxTries": 24,
|
||||
"loot": [
|
||||
{ "type": "iron_ingot", "rarity": 1, "minAmount": 2, "maxAmount": 6 },
|
||||
{ "type": "bread", "rarity": 1, "minAmount": 1, "maxAmount": 4 },
|
||||
{ "type": "diamond", "rarity": 6, "minAmount": 1, "maxAmount": 2 },
|
||||
{
|
||||
"type": "iron_sword",
|
||||
"rarity": 8,
|
||||
"minDurability": 0.4,
|
||||
"maxDurability": 0.9,
|
||||
"displayName": "&7Rusted Blade",
|
||||
"enchantments": [{ "enchantment": "sharpness", "minLevel": 1, "maxLevel": 2, "chance": 0.5 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`maxTries` needs headroom over `maxPicked`, because a try that lands on a rare entry and fails is spent. With four entries and rare items at 1-in-6 and 1-in-8, 24 tries reliably reaches 2-4 picks.
|
||||
|
||||
**2. Bind it to the placement.** In the biome that places your object, on the `objects[]` entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"place": ["tutorial/dungeon"],
|
||||
"chance": 0.02,
|
||||
"overrideGlobalLoot": true,
|
||||
"loot": [
|
||||
{ "name": "tutorial/dungeon-cache", "weight": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`overrideGlobalLoot: true` means chests inside this object use only this table. Drop it if you want the pack's dimension and region tables mixed in.
|
||||
|
||||
**3. Verify.** Open the pack in Studio, find a placement, and open the chest. Every chest in that object should hold 2-4 stacks drawn from the four entries, with diamonds and swords showing up in roughly one placement in six and one in eight. To check without hunting for a placement, stand on a block and run `/iris studio loot` — it previews the tables that would fill a chest at your feet and adds debug lore naming the source table and its combined chance. That command is Bukkit and Studio only.
|
||||
|
||||
**4. If chests come up empty.** Check that the block is a storage chest — Iris only fills chest-family containers, and only ever requests the `STORAGE` slot type, so `FUEL`/`FURNACE`/`BLAST_FURNACE`/`SMOKER` entries never land in a generated container. Check `world.postLoadBlockUpdates` is on in `settings.json`; the container fill runs as part of that post-load pass. Check that the loot table key resolves — a missing table logs a warning and contributes nothing. Double chests fill from one half only (the one with the lower X, then lower Z), and that half fills the combined inventory, so an empty-looking half is normal.
|
||||
|
||||
## Walkthrough: make a custom mob spawn in one biome
|
||||
|
||||
The goal is a geared zombie that appears at night in one biome and drops a custom item. Prerequisites: a validating pack, a land biome with load key `tutorial/meadow`, and `world.ambientEntitySpawningSystem: true` (the default).
|
||||
|
||||
**1. Drop table.** `loot/tutorial/zombie-drops.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial Zombie Drops",
|
||||
"minPicked": 1,
|
||||
"maxPicked": 1,
|
||||
"maxTries": 4,
|
||||
"loot": [
|
||||
{ "type": "iron_nugget", "rarity": 1, "minAmount": 1, "maxAmount": 3 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**2. Entity.** `entities/tutorial/zombie.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "minecraft:zombie",
|
||||
"surface": "LAND",
|
||||
"customName": "&cMeadow Stalker",
|
||||
"helmet": { "type": "leather_helmet", "rarity": 3, "leatherColor": "#3B5323" },
|
||||
"loot": { "tables": ["tutorial/zombie-drops"] }
|
||||
}
|
||||
```
|
||||
|
||||
The entity's `loot` replaces the mob's vanilla drop table outright. Only `tables` is read here — `mode` and `multiplier` on an entity are ignored.
|
||||
|
||||
**3. Spawner.** `spawners/tutorial/night-zombies.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"group": "NORMAL",
|
||||
"maxEntitiesPerChunk": 3,
|
||||
"timeBlock": { "startHour": 20, "endHour": 5 },
|
||||
"weather": "ANY",
|
||||
"allowedLightLevels": { "min": 0, "max": 7 },
|
||||
"maximumRate": { "amount": 4, "per": { "seconds": 30 } },
|
||||
"spawns": [
|
||||
{ "entity": "tutorial/zombie", "rarity": 1, "minSpawns": 1, "maxSpawns": 2 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`timeBlock` hours are clock hours where 6 is sunrise and 18 is sunset, so `20` to `5` is night wrapping past midnight. `maximumRate` of 4 per 30 seconds becomes one attempt every 7.5 seconds for this spawner across the whole world.
|
||||
|
||||
**4. Attach it.** In `biomes/tutorial/meadow.json`, add the spawner key. This is a field excerpt, not a new file:
|
||||
|
||||
```json
|
||||
{
|
||||
"entitySpawners": ["tutorial/night-zombies"]
|
||||
}
|
||||
```
|
||||
|
||||
**5. Verify.** Validate the pack first — the validator resolves the spawner-to-entity edge and will name the broken link before you load a world. Then open Studio, focus `tutorial/meadow`, set night, and stand somewhere with block light under 8. Success is named zombies appearing within a few seconds and dropping iron nuggets when killed. On Bukkit you can prove the entity file loads on its own with `/iris studio spawn tutorial/zombie`; on Fabric/Forge/NeoForge that command is not registered and reports that it is Bukkit-only, so go straight to ambient spawning.
|
||||
|
||||
**6. If nothing spawns.** Work down the gate list in order rather than raising `rarity` or the rate:
|
||||
|
||||
- World-wide crowding. If living entities divided by loaded chunks exceeds `world.targetSpawnEntitiesPerChunk` (0.95 by default, scaled by 1.28), Iris stops spawning for five seconds and logs it under debug. A test world full of mobs will starve your spawner.
|
||||
- Chunk crowding. `maxEntitiesPerChunk` is compared against the living entities already in that chunk.
|
||||
- Time and weather. Both are read from the world at attempt time.
|
||||
- Light. The check only runs when `allowedLightLevels` is narrower than 0-15, and it reads the combined maximum of sky and block light, not block light alone. A `max: 7` spawner will not fire on a surface block in daylight.
|
||||
- Group versus biome. A `NORMAL` spawner listed on a *dimension* is rejected in sea, shore, and cave biomes. Region- and biome-level spawners skip that check entirely and fire wherever their parent applies, so a mismatched `group` there produces mobs at odd heights rather than no mobs.
|
||||
- Placement viability. The chosen block's `surface` must match the entity's `surface`, and the entity's bounding box must be clear air.
|
||||
|
||||
## Where files live
|
||||
|
||||
| Path | Class | Role |
|
||||
|------|-------|------|
|
||||
| `loot/<key>.json` | `IrisLootTable` | A pool of items with per-entry rarity, rolled as a unit |
|
||||
| `entities/<key>.json` | `IrisEntity` | One mob: type, gear, flags, drops, passengers |
|
||||
| `spawners/<key>.json` | `IrisSpawner` | The gates and rates that decide when entity keys appear |
|
||||
| `markers/<key>.json` | `IrisMarker` | A tag that attaches spawners to individual blocks |
|
||||
|
||||
Keys are the pack-relative path without `.json`, so `loot/tutorial/dungeon-cache.json` is referenced as `tutorial/dungeon-cache`.
|
||||
|
||||
## What references what
|
||||
|
||||
| Holder | Field | Effect |
|
||||
|--------|-------|--------|
|
||||
| Dimension / region / biome | `loot` (`IrisLootReference`) | Contributes tables to every container in that scope |
|
||||
| Object placement | `loot` (`IrisObjectLoot[]`) | Contributes one table to containers the object placed |
|
||||
| Object placement | `vanillaLoot` (`IrisObjectVanillaLoot[]`) | Same, but the table is a real Minecraft loot table |
|
||||
| Object placement | `overrideGlobalLoot` | Discards the dim/region/biome contribution for that placement |
|
||||
| Entity | `loot` | Replaces the mob's vanilla drop table |
|
||||
| Dimension / region / biome | `entitySpawners` | Runs those spawners for chunks in that scope |
|
||||
| Object placement | `markers` (`IrisObjectMarker[]`) | Tags matching blocks the object placed |
|
||||
| Marker | `spawners` | Runs those spawners at every block carrying the tag |
|
||||
| Dimension / region / biome | `blockDrops` (`IrisBlockDrops[]`) | Adds or replaces drops when a player breaks a matching block |
|
||||
|
||||
Ambient spawning needs `world.ambientEntitySpawningSystem` true; marker spawning needs `world.markerEntitySpawningSystem` true. Both default to true. Iris spawners run alongside vanilla and mod spawning rather than replacing it, and nothing deduplicates between the two — a zombie spawner in your pack adds to whatever the server would have spawned anyway.
|
||||
|
||||
The shipping Overworld pack wires its spawners at region scope (`regions/*.json` list `<climate>/cave`, `/hostile`, `/passive`, `/water`) and ships no `markers/` folder. Its dimension-level loot uses `FALLBACK`, so `global-clutter` only reaches chests that nothing else claimed.
|
||||
|
||||
## How a container actually gets filled
|
||||
|
||||
Worth reading before tuning rarities, because two of these steps surprise people.
|
||||
|
||||
1. **Trigger.** After a chunk's mantle materializes tiles and custom blocks, the post-load update pass visits every block Iris flagged. Only storage chests proceed, and only the `STORAGE` slot type is ever requested. Objects placed into an already-live world (Studio placement, WorldEdit-driven placement) fill their chests immediately instead, on the region thread that owns the chunk.
|
||||
|
||||
2. **Source list.** Iris asks the object placement that owns the block for at most one table. Candidates are bucketed by how specifically they match: entries with an `exact` block-data filter that matches win outright; failing that, entries whose filter matches the block's material; failing that, entries with no filter. Within the winning bucket the pick is weighted by `weight`. `loot` and `vanillaLoot` entries compete in the same buckets. If the placement sets `overrideGlobalLoot` and a table was picked, that's the entire list.
|
||||
|
||||
3. **Environment sources.** Otherwise the dimension, region, and surface biome each inject their tables in that order, and a cave biome injects too when the container is below terrain height and resolves to a different biome than the surface. `ADD` appends. `CLEAR` and `REPLACE` both wipe the list first and then append their own tables — despite the field description, `CLEAR` does not suppress its own tables. `FALLBACK` injects only when nothing already claimed the container, meaning neither the placement's `loot`/`vanillaLoot` nor a pre-existing vanilla loot table on the block.
|
||||
|
||||
4. **Multiplier.** The multipliers from every contributing scope are multiplied together, and the resulting factor scales the *length of the table list*, not stack sizes. A factor of 0.5 randomly drops half the tables; a factor of 2 randomly duplicates entries until the list doubles. The list is capped at 256 sources and Iris throws rather than silently truncating past that.
|
||||
|
||||
5. **Event hook.** On Bukkit, `art.arcane.iris.core.events.IrisLootEvent` fires with the engine, block, slot type, and the resolved table list. The list is mutable, so a plugin can add, remove, or clear tables before the roll. Iris also bridges the rolled items through Bukkit's `LootGenerateEvent`, so plugins that already listen for vanilla loot generation see Iris chests too. This is not part of the documented public API surface in the `90`-series pages.
|
||||
|
||||
6. **Roll.** For each table: pick a random target count between `minPicked` and `maxPicked`, then loop up to `maxTries`. Each try picks a random entry index. The entry only counts if its `slotTypes` is `STORAGE` and it passes a 1-in-(table `rarity` x entry `rarity`) check. That check is derived from the loot seed, the table's key, the entry index, and the block coordinates — not from a running random sequence. **The same entry at the same block always gives the same answer**, so tries that re-roll a rare entry that already failed are wasted. This is why `maxTries` should sit well above `maxPicked` when a table has rare entries.
|
||||
|
||||
7. **Scatter.** Items are inserted, then one multi-item stack is split into a free slot and all slots are shuffled, so a chest reads like a hand-placed one instead of a left-packed block of stacks.
|
||||
|
||||
Modded servers run the same resolver and the same rarity math, so container contents match Bukkit for a given seed and position. Modded resolves `vanillaLoot` names against the server's loot-table registry directly; Bukkit resolves them through `Bukkit.getLootTable` and delegates the roll to Minecraft.
|
||||
|
||||
## Loot tables (`IrisLootTable`)
|
||||
|
||||
Folder: `loot/`.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `name` | string | `""` | Human label shown in Studio debug lore. Required, at least 2 characters. Not the lookup key — that's the file path |
|
||||
| `rarity` | int >= 1 | `1` | Multiplied into every entry's rarity. Raise it to make a whole table rare without editing each entry; leave at 1 and tune entries individually |
|
||||
| `minPicked` | int 0..64 | `1` | Floor of the random target count. Set to 0 when a chest is allowed to come out empty |
|
||||
| `maxPicked` | int 1..64 | `5` | Ceiling of the random target count. This is the most items one table can contribute |
|
||||
| `maxTries` | int 1..256 | `10` | How many entry draws are allowed before the table gives up. Raise it when entries have high rarity, or the table will routinely undershoot `minPicked` |
|
||||
| `loot` | `IrisLoot[]` | `[]` | The entries. An empty table contributes nothing and is not an error |
|
||||
|
||||
### Loot entry (`IrisLoot`, snippet type `loot`)
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `type` | string | `""` | Required. A plain material name (`diamond`, `DIAMOND_SWORD`). Namespaced values are reserved for items from other plugins or mods, resolved through the external-data service — `minecraft:` prefixes are not the pattern here |
|
||||
| `slotTypes` | `InventorySlotType` | `STORAGE` | Which inventory slot family the entry targets. Generated containers only ever request `STORAGE`, so `FUEL`, `FURNACE`, `BLAST_FURNACE`, and `SMOKER` entries are inert in world generation |
|
||||
| `rarity` | int >= 1 | `1` | 1-in-N chance for this entry, multiplied by the table's rarity. Use it to make one entry rare inside an otherwise common table |
|
||||
| `minAmount` / `maxAmount` | int 1..64 | `1` / `1` | Inclusive stack-size range rolled per pick |
|
||||
| `displayName` | string | null | Item name. `&` colour codes are translated |
|
||||
| `lore` | string[] | `[]` | Lore lines. Lines over 24 characters are word-wrapped into several lines |
|
||||
| `minDurability` / `maxDurability` | 0..1 | `0` / `1` | Fraction of durability *remaining*, rolled per pick. `0`/`1` gives anything from nearly broken to pristine; set both to 1 for undamaged gear |
|
||||
| `customModel` | int | null | Custom model data, written into the item's model-data component as a float. For resource packs that key off model data |
|
||||
| `unbreakable` | boolean | `false` | Marks the item unbreakable |
|
||||
| `itemFlags` | string[] | `[]` | Bukkit `ItemFlag` names, for hiding enchantments or attributes in the tooltip. Unrecognised names are skipped silently |
|
||||
| `enchantments` | `IrisEnchantment[]` | `[]` | Each has its own level range and application chance |
|
||||
| `attributes` | `IrisAttributeModifier[]` | `[]` | Attribute modifiers baked into the item |
|
||||
| `dyeColor` | string | null | A `DyeColor` name, applied to items that are colourable |
|
||||
| `leatherColor` | string | null | `#RRGGBB`, applied to leather armour |
|
||||
| `customNbt` | object | null | Raw platform NBT merged into the item. Also carries the payload for namespaced third-party items |
|
||||
|
||||
When Studio debug is on (`/iris studio loot`, or any roll in a Studio world), each item gains lore naming the source table and the combined 1-in-N chance. That lore is not written in production worlds.
|
||||
|
||||
### Loot reference (`IrisLootReference`, snippet type `loot-registry`)
|
||||
|
||||
The shape used by dimension, region, biome, and entity `loot` fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"loot": {
|
||||
"mode": "FALLBACK",
|
||||
"multiplier": 0.5,
|
||||
"tables": ["temperate/clutter", "temperate/food"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `mode` | `IrisLootMode` | `ADD` | How this scope's tables combine with the scopes above it — see below |
|
||||
| `tables` | string[] | `[]` | Loot table keys. A key that doesn't resolve is skipped |
|
||||
| `multiplier` | double 0..16 | `1` | Scales how many tables end up in the final list for containers in this scope. Below 1 randomly drops tables, above 1 randomly duplicates them. Multiplied across every contributing scope |
|
||||
|
||||
| Mode | Behaviour |
|
||||
|------|-----------|
|
||||
| `ADD` | Append these tables to whatever the outer scopes contributed. The default and the right choice most of the time |
|
||||
| `REPLACE` | Wipe the list, then append these. Use on a biome that should ignore the dimension's global tables |
|
||||
| `CLEAR` | Identical to `REPLACE` in code: wipe, then append these. The field description claims it also suppresses its own tables; it does not |
|
||||
| `FALLBACK` | Only contribute when nothing already claimed the container — no object `loot`, no object `vanillaLoot`, no native loot table on the block. This is how a pack ships a broad filler table without stepping on structure chests |
|
||||
|
||||
Entities read only `tables` from this object. Setting `mode` or `multiplier` on an entity's `loot` has no effect.
|
||||
|
||||
### Object loot
|
||||
|
||||
On `IrisObjectPlacement` (see `20 - Object Placement.md`):
|
||||
|
||||
| Field | What it does |
|
||||
|-------|--------------|
|
||||
| `loot` | `IrisObjectLoot[]` — Iris tables offered to containers this placement created |
|
||||
| `vanillaLoot` | `IrisObjectVanillaLoot[]` — Minecraft loot table keys offered the same way |
|
||||
| `overrideGlobalLoot` | When a table was picked from this placement, stop there and skip dimension, region, and biome tables |
|
||||
|
||||
Both entry types share the same fields:
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `name` | required | The Iris loot table key, or for `vanillaLoot` a namespaced Minecraft loot table key such as `minecraft:chests/simple_dungeon` |
|
||||
| `weight` | `1` | Relative odds inside its match bucket. Entries with weight 0 or below are ignored entirely |
|
||||
| `filter` | `[]` | Blocks this entry applies to. Empty means every container the object placed |
|
||||
| `exact` | `false` | Compare the whole block state instead of just the material. An exact match outranks a material match, which outranks an unfiltered entry |
|
||||
|
||||
One table is picked per container, not one per entry — the buckets decide which entries are eligible and `weight` decides between them.
|
||||
|
||||
Authored container contents and deferred custom-block identifiers stay in the mantle until the platform's post-load materialization pass runs for that chunk. Generic cleanup and pregeneration cleanup preserve those sparse payloads for chunks that have not reached the pass yet, and on Bukkit a region-scheduled pass that fails stays retryable without repeating the passes that already completed.
|
||||
|
||||
## Entities (`IrisEntity`)
|
||||
|
||||
Folder: `entities/`. A minimal entity is one field:
|
||||
|
||||
```json
|
||||
{ "type": "ZOMBIE" }
|
||||
```
|
||||
|
||||
`entities/standard/hostile/zombie.json` is referenced as `standard/hostile/zombie`.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `type` | string | null | Required. The entity type key (`minecraft:zombie`, `alexsmobs:grizzly_bear`, or a bare name). Set it to `unknown` and fill `specialType` when the mob comes from a plugin instead |
|
||||
| `specialType` | string | `""` | `PluginName:MobName`, spawned through the external-data service. Mythic Mobs and similar providers plug in here |
|
||||
| `applySettingsToCustomMobAnyways` | boolean | `false` | By default Iris hands a `specialType` mob straight back to its provider untouched. Turn this on to layer Iris gear, names, and flags on top |
|
||||
| `reason` | string | null | The `SpawnReason` reported to other plugins. Unset or unrecognised becomes `NATURAL`. Change it when another plugin gates on spawn reason |
|
||||
| `customName` | string | `""` | Name tag, with `&` colour codes |
|
||||
| `customNameVisible` | boolean | `false` | Show the name without looking at the mob |
|
||||
| `aware` | boolean | `true` | Whether the mob reacts to the world. Off makes a decorative mob that stands still but still animates |
|
||||
| `ai` | boolean | `true` | Whether the mob has AI goals at all. Off is a harder freeze than `aware: false` |
|
||||
| `glowing` | boolean | `false` | Outline visible through blocks |
|
||||
| `gravity` | boolean | `true` | Off leaves the mob floating where it spawned |
|
||||
| `invulnerable` | boolean | `false` | Only creative-mode players can damage it |
|
||||
| `silent` | boolean | `false` | Suppresses the mob's sounds |
|
||||
| `pickupItems` | boolean | `false` | Whether it can pick up dropped gear |
|
||||
| `removable` | boolean | `false` | Whether the server may despawn it when players leave. Off keeps a set-piece mob alive |
|
||||
| `keepEntity` | boolean | `false` | Forces persistence. Also forced globally by `world.forcePersistEntities` |
|
||||
| `baby` | boolean | `false` | Spawns the baby variant for ageable types |
|
||||
| `helmet` / `chestplate` / `leggings` / `boots` / `mainHand` / `offHand` | `IrisLoot` | null | One equipment slot each, built like a loot entry. The entry's own `rarity` is a 1-in-N roll for whether the slot gets filled at all — that's how you get "one in five wears a helmet" |
|
||||
| `passengers` | `IrisEntity[]` | `[]` | Riders, spawned and mounted after the host. Nests, so a rider can carry a rider |
|
||||
| `attributes` | `IrisAttributeModifier[]` | `[]` | Attribute modifiers applied to the mob |
|
||||
| `loot` | `IrisLootReference` | empty | Drop tables. Replaces the mob's vanilla drops. Only `tables` is read |
|
||||
| `leashHolder` | `IrisEntity` | null | Spawns a second entity and leashes this one to it. No effect on ender dragons, withers, players, or bats |
|
||||
| `spawnEffect` | `IrisEffect` | null | A one-shot effect fired at the spawn position |
|
||||
| `spawnEffectRiseOutOfGround` | boolean | `false` | Spawns the mob five blocks lower when a player is nearby and walks it up out of the ground with block-crack particles. The mob is invulnerable and AI-less for up to five seconds while rising |
|
||||
| `pandaMainGene` / `pandaHiddenGene` | string | null | Panda genes. Unrecognised names fall back to `NORMAL` |
|
||||
| `surface` | `IrisSurface` | `LAND` | What the block under the spawn point must be — see below. Marker-driven spawns skip this check |
|
||||
| `rawCommands` | `IrisCommand[]` | `[]` | Console commands run after the mob spawns |
|
||||
|
||||
`IrisSurface` values, checked against the block directly below the spawn position:
|
||||
|
||||
| Value | Matches |
|
||||
|-------|---------|
|
||||
| `LAND` | Any solid block |
|
||||
| `ANIMAL` | Grass block, dirt, dirt path, coarse dirt, rooted dirt, podzol, mycelium, or snow block. Narrower than `LAND` on purpose, for passive mobs |
|
||||
| `WATER` | Water, seagrass, kelp, or any waterlogged block |
|
||||
| `OVERWORLD` | Any solid block or any water block — use it when a spawner should work on shore and in shallows |
|
||||
| `LAVA` | Lava |
|
||||
|
||||
### Entity drops in practice
|
||||
|
||||
Both platforms replace the mob's vanilla drop table rather than adding to it, and the tables are rolled at the mob's **spawn** coordinates with the `STORAGE` slot type. Because entry rarity is position-derived, every mob spawned on the same block rolls identical drops. Vary `minAmount`/`maxAmount` if you want visible variation from a single-entry table.
|
||||
|
||||
On Bukkit a synthetic loot table is bound to the mob. On modded the mob carries an `iris_loot|…` tag and Iris emits the items on death instead of the base table. Modded also fills chest-carrying vehicles directly at spawn; for any other entity type that exposes no lootable path it logs one warning per type and skips.
|
||||
|
||||
### Entity commands (`IrisCommand`, snippet type `command`)
|
||||
|
||||
`rawCommands` runs console commands after a mob spawns. The same object is used by ambient effect command registries.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `commands` | `[]` | Required. Command strings. A leading `/` is stripped, and `{x}`, `{y}`, `{z}` are replaced with the spawn block coordinates |
|
||||
| `delay` | `0` | Server ticks before the first run. Negative values clamp to zero |
|
||||
| `repeat` | `false` | Repeat forever after the first run. There is no cancel handle, and repeats do not survive a restart — they only exist for as long as the server stays up after the chunk generated |
|
||||
| `repeatDelay` | `100` | Server ticks between repeats. Values below 1 clamp to 1 |
|
||||
| `timeBlock` | any time | World-time window the command is allowed in |
|
||||
| `weather` | `ANY` | Required weather: `NONE`, `DOWNFALL`, `DOWNFALL_WITH_THUNDER`, or `ANY` |
|
||||
|
||||
`timeBlock` and `weather` are evaluated once, when the command object first runs. A repeating command keeps repeating after its window closes.
|
||||
|
||||
Bukkit and modded both apply AI and awareness flags, spawn effects, and raw commands, and both enforce spawner time and weather gates.
|
||||
|
||||
## Ambient effects (`IrisEffect`, snippet type `effect`)
|
||||
|
||||
Biomes and regions accept `effects[]`. Each entry runs at most once per `interval` milliseconds and, when it runs, has a 1-in-`chance` shot at firing. A single entry can apply a potion, play a sound, emit particles, and run commands. The whole system is gated by `world.effectSystem` in `settings.json`.
|
||||
|
||||
| Field | Default / range | What it does |
|
||||
|-------|-----------------|--------------|
|
||||
| `interval` | `150` ms, >= 0 | Minimum gap between attempts. Raise it for anything expensive or loud |
|
||||
| `chance` | `50`, >= 1 | One attempt in this many actually fires. Combined with `interval` this is your real frequency |
|
||||
| `potionEffect` | `""` | Potion effect registry key. An unknown key falls back to `LUCK` and logs a warning |
|
||||
| `potionStrength` | `-1` (-1..1024) | Amplifier. `-1` disables potion application entirely, which is the default |
|
||||
| `potionTicksMin` / `potionTicksMax` | `75` / `155` | Random potion duration in ticks |
|
||||
| `sound` | null | Sound registry key |
|
||||
| `soundDistance` | `12` (0..512) | How far from the player the sound origin can be offset. Larger values make the source feel distant and directionless |
|
||||
| `minPitch` / `maxPitch` | `0.5` / `1.5` (0.01..1.99) | Random pitch range |
|
||||
| `volume` | `1.5` (0.001..512) | Sound volume |
|
||||
| `particleEffect` | null | Particle registry key. Modded supports particle types that need no extra particle data |
|
||||
| `particleOffset` | `0` (-32..32) | Random vertical offset from the sampled surface |
|
||||
| `particleCount` | `0` (0..512) | Particle count. Zero is meaningful: on Bukkit it makes the alt XYZ values behave as velocity instead of spread |
|
||||
| `particleDistance` | `20` (0..64) | How far ahead of the player particles are sampled |
|
||||
| `particleDistanceWidth` | `24` (0..128) | Sampling radius left and right of the player |
|
||||
| `particleAway` | `5` (0..16) | Minimum forward offset, so particles don't spawn in the player's face |
|
||||
| `particleAltX` / `particleAltY` / `particleAltZ` | `0` (-8..8) | Spread, or velocity when `particleCount` is 0 |
|
||||
| `randomAltX` / `randomAltY` / `randomAltZ` | `true` / `false` / `true` | Randomize each alt component between its negative and positive value. Y defaults off so vertical drift stays deliberate |
|
||||
| `extra` | `0` | Particle-specific extra value, meaningful only for some particle types |
|
||||
| `commandRegistry` | null | Commands to cast alongside the effect |
|
||||
|
||||
`IrisCommandRegistry` (snippet type `command-registry`):
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `rawCommands` | `[]` | `IrisCommand[]` to run |
|
||||
| `commandOffsetX` / `commandOffsetY` / `commandOffsetZ` | `0` (-8..8) | Offsets from the player for the `{x} {y} {z}` substitutions |
|
||||
| `commandRandomAltX` / `commandRandomAltY` / `commandRandomAltZ` | `true` / `false` / `true` | Randomize each coordinate within its signed offset |
|
||||
| `commandAllRandomLocations` | `true` | Re-roll the coordinate for each command object. Set false to fire every command at one shared point |
|
||||
|
||||
```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 | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `spawns` | `IrisEntitySpawn[]` | `[]` | The ongoing spawn pool. Every entry competes with entries from every other eligible spawner in the same chunk |
|
||||
| `initialSpawns` | `IrisEntitySpawn[]` | `[]` | A separate pool used once per chunk, the first time that chunk is maintained. For set dressing that should exist from the moment a chunk appears |
|
||||
| `maxEntitiesPerChunk` | int | `1` | Skip this spawner when the target chunk already holds this many living entities. The single most common reason a spawner looks dead — the default of 1 means almost any occupied chunk blocks it |
|
||||
| `timeBlock` | `IrisTimeBlock` | any time | World-time window. Clock hours, 6 = sunrise, 18 = sunset |
|
||||
| `weather` | `IrisWeather` | `ANY` | `NONE`, `DOWNFALL`, `DOWNFALL_WITH_THUNDER`, or `ANY` |
|
||||
| `maximumRate` | `IrisRate` | infinite | World-wide throttle for this spawner. Stamped only when a spawn actually succeeds |
|
||||
| `maximumRatePerChunk` | `IrisRate` | infinite | Same throttle, tracked per chunk. Use it to stop one chunk hogging a generous global rate |
|
||||
| `allowedLightLevels` | `IrisRange` | `0`..`15` | Inclusive light range. Skipped entirely when left at the full range. Measured as the combined maximum of sky and block light |
|
||||
| `group` | `IrisSpawnGroup` | `NORMAL` | Where in the column mobs are placed, and which biomes accept this spawner at dimension scope |
|
||||
|
||||
`IrisSpawnGroup`:
|
||||
|
||||
| Value | Position chosen | Biome check (dimension scope only) |
|
||||
|-------|-----------------|------------------------------------|
|
||||
| `NORMAL` | Random x/z in the chunk, one block above the fluid-inclusive surface | Land biomes only |
|
||||
| `CAVE` | A random `cave_floor` mantle marker in the chunk, one block up | Accepted in every biome type |
|
||||
| `UNDERWATER` | Random x/z, random Y between the solid top and the water surface | Sea biomes only |
|
||||
| `BEACH` | Same water-column position as `UNDERWATER` | Shore biomes only |
|
||||
|
||||
The biome check only applies to spawners listed on a **dimension**. Region and biome `entitySpawners` bypass it, so a `CAVE`-group spawner listed on a surface biome will still try to find cave floor markers there and quietly do nothing if there are none.
|
||||
|
||||
On Folia, `CAVE` group spawners never fire on Bukkit — the cave-floor marker lookup returns nothing off the region thread and the spawn is skipped.
|
||||
|
||||
`IrisRate` (snippet type `rate`):
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `amount` | `0` | How many firings the duration allows. The effective cooldown is `per` divided by `amount` (or by 1 when `amount` is 0) |
|
||||
| `per` | empty | The window. **An empty `per` means unlimited** — that's what makes a rate infinite, not `amount` |
|
||||
|
||||
`IrisDuration` (snippet type `duration`) sums every field you fill in:
|
||||
|
||||
| Field | Real time per unit |
|
||||
|-------|--------------------|
|
||||
| `milliseconds` | 1 ms |
|
||||
| `minecraftTicks` | 50 ms |
|
||||
| `seconds` / `minutes` / `hours` / `days` | Real-world units |
|
||||
| `minecraftHours` | 50 s |
|
||||
| `minecraftDays` | 20 min |
|
||||
| `minecraftWeeks` | 2 h 20 min (7 Minecraft days) |
|
||||
| `minecraftLunarCycles` | 2 h 40 min (8 Minecraft days) |
|
||||
|
||||
`IrisTimeBlock` (snippet type `time-block`): `startHour` and `endHour` in 24-hour clock time, where the world's tick 0 reads as hour 6. Setting both to the same value means any time; setting both to `-1` means never. A `startHour` greater than `endHour` wraps past midnight.
|
||||
|
||||
### Entity spawn entry (`IrisEntitySpawn`, snippet type `entity-spawn`)
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `entity` | string | `""` | Required. The entity key |
|
||||
| `rarity` | int >= 1 | `1` | Inverse weight. All eligible entries from all eligible spawners go into one pool and each entry gets `totalRarity / rarity` slots, so low numbers are common and high numbers are rare. Exactly one entry wins per chunk attempt |
|
||||
| `minSpawns` / `maxSpawns` | int >= 1 | `1` / `1` | Inclusive range of placement attempts once this entry wins. Each attempt can still fail the surface, light, or clearance check, so this is a ceiling not a guarantee |
|
||||
|
||||
Modded applies `rarity` twice — once as the pool weight and again as a 1-in-N roll at each candidate position — so high-rarity entries spawn somewhat less often on Fabric/Forge/NeoForge than on Bukkit for the same numbers.
|
||||
|
||||
Real Overworld spawner, `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 },
|
||||
{ "entity": "standard/neutral/enderman", "rarity": 60, "maxSpawns": 2, "minSpawns": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Attach it on a dimension, region, or biome:
|
||||
|
||||
```json
|
||||
{
|
||||
"entitySpawners": ["temperate/hostile", "temperate/passive"]
|
||||
}
|
||||
```
|
||||
|
||||
### The ambient tick
|
||||
|
||||
The loop runs once per Iris world every `world.asyncTickIntervalMS` milliseconds (700 by default, 3000 when both spawn systems are off). Each pass:
|
||||
|
||||
1. Recount living entities in the world, throttled so it doesn't run every tick. If the count can't be completed — the scheduler refuses the task, or it times out — Iris pauses spawning entirely rather than guessing. This is deliberate: an incomplete count must never authorize a spawn.
|
||||
2. Compute saturation as living entities divided by loaded chunks plus one, scaled by 1.28. Above `world.targetSpawnEntitiesPerChunk` the pass sleeps 5 seconds and returns.
|
||||
3. Pick between 2 and 12 random loaded chunks and run one spawn attempt in each, on the region thread that owns the chunk.
|
||||
4. Pregeneration and world maintenance suppress spawning for that world entirely while they run.
|
||||
|
||||
In Studio worlds, spawning additionally requires `studio.entitySpawning`.
|
||||
|
||||
`initialSpawns` runs from the chunk-maintenance pass, once per chunk, guarded by a mantle flag so it never repeats. That pass returns early when `world.markerEntitySpawningSystem` is off, so `initialSpawns` needs both spawn settings enabled even though it isn't marker-driven.
|
||||
|
||||
## Markers (`IrisMarker`)
|
||||
|
||||
Folder: `markers/`. A marker is a tag written into the mantle at a block position; the marker file says which spawners fire there.
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `spawners` | string[] | `[]` | Spawner keys. One is picked at random each time the marker fires |
|
||||
| `removeOnChange` | boolean | `true` | Delete the marker when a player breaks the block it sits on. Leave on unless you want a spawn point that survives being mined out |
|
||||
| `emptyAbove` | boolean | `true` | Require two non-solid blocks above. Checked twice — see below |
|
||||
| `exhaustionChance` | double | `0` | Odds the marker deletes itself when it fires. `0.25` averages four uses. Anything at or below 0 never exhausts; 1 or higher exhausts on the first use |
|
||||
|
||||
`emptyAbove` is checked at two different times against two different things. When the object is placed, Iris asks whether *the object itself* defines blocks one and two above the candidate — so a marker on a floor block under the object's own ceiling is never written. When the marker later fires, the scanner re-checks against the *live world*, and a marker that has since been buried is deleted from the mantle rather than skipped.
|
||||
|
||||
### Placing markers from objects (`IrisObjectMarker`, snippet type `object-marker`)
|
||||
|
||||
On an object placement's `markers[]`:
|
||||
|
||||
| Field | Type | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| `mark` | `IrisBlockData[]` | required | Block types to tag. Candidate blocks are shuffled, so which matching blocks get tagged varies by placement |
|
||||
| `marker` | string | required | The marker key to attach |
|
||||
| `maximumMarkers` | int 1..16 | `8` | Cap for this entry across all its `mark` types. Keep it low — each marker is a per-block mantle write and a per-chunk scan cost |
|
||||
| `exact` | boolean | `false` | Match the full block state instead of just the material |
|
||||
|
||||
```json
|
||||
{
|
||||
"place": ["dungeons/crypt"],
|
||||
"chance": 0.01,
|
||||
"markers": [
|
||||
{ "mark": [{ "block": "minecraft:spawner" }], "marker": "crypt-spawns", "maximumMarkers": 4 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When marker spawning is on, each chunk pass reads the mantle markers in that chunk, skips the engine's internal `cave_floor` and `cave_ceiling` tags, loads each `IrisMarker`, drops obstructed ones, picks one of that marker's spawners at random, and fires it at the marker position. Marker spawns bypass the entity's `surface` check and the bounding-box clearance check — the marker is taken as authoritative about the position being valid — but they still honour `allowedLightLevels` and the spawner's time, weather, and rate gates. `exhaustionChance` is rolled once per firing, before the mobs are placed.
|
||||
|
||||
## Custom block drops (`IrisBlockDrops`, snippet type `block-drops`)
|
||||
|
||||
Dimensions, regions, and biomes accept `blockDrops[]`. When a player breaks a block, matching providers from the biome run first; unless a matching biome provider sets `skipParents`, matching region and then dimension providers are appended.
|
||||
|
||||
| Field | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `blocks` | `[]` | Required. Block types this rule reacts to |
|
||||
| `exactBlocks` | `false` | False matches on material alone, so any barrel matches `minecraft:barrel`. True requires the full block state, so `minecraft:barrel[axis=x]` matches only that orientation |
|
||||
| `drops` | `[]` | `IrisLoot[]`. Each entry rolls its own `rarity` independently — unlike loot tables there's no pick count or try budget, so every entry gets exactly one chance |
|
||||
| `skipParents` | `false` | On a matching biome provider, stops region and dimension providers running for this break. Use it when a biome needs to fully own a block's drops |
|
||||
| `replaceVanillaDrops` | `false` | If any matching provider sets this, vanilla drops are suppressed while Iris drops from every selected provider still fire |
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [{ "block": "minecraft:stone" }],
|
||||
"exactBlocks": false,
|
||||
"drops": [{ "type": "flint", "rarity": 4 }],
|
||||
"skipParents": false,
|
||||
"replaceVanillaDrops": false
|
||||
}
|
||||
```
|
||||
|
||||
This runs on Bukkit and on Fabric, Forge, and NeoForge. Each loader hooks its own block-break path and routes the result through the same rule evaluation.
|
||||
|
||||
## Runtime settings that gate these systems
|
||||
|
||||
From `settings.json` under `world` (see `03 - Configuration.md`):
|
||||
|
||||
| Key | Default | Effect |
|
||||
|-----|---------|--------|
|
||||
| `postLoadBlockUpdates` | `true` | Drives the pass that fills generated containers. Off means no chest loot |
|
||||
| `ambientEntitySpawningSystem` | `true` | Dimension, region, and biome `entitySpawners` |
|
||||
| `markerEntitySpawningSystem` | `true` | Marker-driven spawners, and the chunk pass that runs `initialSpawns` |
|
||||
| `effectSystem` | `true` | Biome and region `effects[]` |
|
||||
| `targetSpawnEntitiesPerChunk` | `0.95` | Saturation ceiling. Lower it on busy servers to stop Iris adding to entity load |
|
||||
| `asyncTickIntervalMS` | `700` | How often the spawn loop runs per world |
|
||||
| `forcePersistEntities` | `true` | Marks every Iris-spawned entity persistent regardless of `keepEntity` |
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Write `loot/<key>.json` tables. Give `maxTries` room above `maxPicked` if any entry is rare.
|
||||
2. Reference them from `loot.tables` on a dimension, region, or biome, or from `loot[].name` on an object placement.
|
||||
3. Write `entities/<key>.json` with at least `type`. Set `surface` to match where the mob belongs.
|
||||
4. Write `spawners/<key>.json`. Raise `maxEntitiesPerChunk` above the default of 1 unless you genuinely want one mob per chunk.
|
||||
5. Add spawner keys to `entitySpawners` on a dimension, region, or biome, or attach them through markers on an object placement.
|
||||
6. Validate the pack — the spawner-to-entity edge is a blocking check, so a typo is caught before you load a world.
|
||||
7. Open Studio and confirm in-world. Use `/iris studio loot` for chest previews on Bukkit.
|
||||
@@ -1,275 +0,0 @@
|
||||
# 24 - Pack Mods & Snippets
|
||||
|
||||
Snippets let you write a nested JSON value once and reference it by path from as many places as you like. Any pack type annotated `@Snippet` accepts either an inline object or a string pointing at a file under `snippet/<type>/`. Iris also still loads the older `IrisMod` schema from `mods/`, but nothing in the engine applies those injectors or replacers — treat that folder as dead weight.
|
||||
|
||||
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`.
|
||||
|
||||
## The mental model
|
||||
|
||||
Most of a pack is nested objects: a decorator inside a biome, a noise style inside a generator, a palette inside a decorator. When two biomes want the same decorator you'd normally copy the JSON, and then you have two copies to keep in sync.
|
||||
|
||||
Snippets fix that at the deserializer level. Iris registers a Gson type adapter for every class carrying `@Snippet("some-name")`. When that adapter reads a field and finds a **string** instead of an object, it treats the string as a path, opens `snippet/some-name/<path>.json`, and parses the file's contents as the field's value. Nothing else changes: the biome still ends up holding a real decorator object, the engine never knows the difference, and the value is resolved once at load time rather than looked up per chunk.
|
||||
|
||||
Two consequences worth internalising:
|
||||
|
||||
- **Snippets are load-time only.** Editing a snippet file does nothing until the pack reloads — Studio hotload, world reload, or a restart. There is no live indirection.
|
||||
- **Snippets vanish on serialization.** When Iris writes a pack back out (Studio saves, the Bukkit packager), the adapter writes the resolved object, not the string. Snippet references get inlined. See "Packaging" below.
|
||||
|
||||
## Walkthrough: share a palette across biomes
|
||||
|
||||
The goal is one decorator definition placing wildflowers in several biomes, with a single file to edit. Prerequisites: a validating pack and a biome that already generates.
|
||||
|
||||
**1. Write the snippet.** The folder name must match the `@Snippet` value of the field you'll use it in — a decorator field wants `snippet/decorator/`. Save `snippet/decorator/tutorial-wildflowers.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"chance": 0.08,
|
||||
"style": {
|
||||
"style": "CLOVER_HERMITE",
|
||||
"zoom": 0.52,
|
||||
"exponent": 2.5
|
||||
},
|
||||
"slopeCondition": { "maximumSlope": 4 },
|
||||
"palette": [
|
||||
{ "block": "minecraft:dandelion", "weight": 2 },
|
||||
{ "block": "minecraft:poppy", "weight": 1 },
|
||||
{ "block": "minecraft:air", "weight": 4 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**2. Reference it.** In the biome, replace the inline decorator with the path. No `.json` suffix:
|
||||
|
||||
```json
|
||||
{
|
||||
"decorators": ["snippet/decorator/tutorial-wildflowers"]
|
||||
}
|
||||
```
|
||||
|
||||
**3. Verify one call site.** Validate the pack, then open Studio on a fixed seed and generate fresh chunks in that biome. Success is both flowers appearing only on slopes the snippet allows, with no "Couldn't find snippet" line in the console. If the field resolves to null, the console names the path it tried — compare it against the file on disk.
|
||||
|
||||
**4. Add the second call site.** Only once the first one works. Paste the same string into another biome's `decorators`.
|
||||
|
||||
**5. Prove they're linked.** Change one value inside the snippet — raise `chance` to `0.3` — hotload, and generate fresh chunks in both biomes. Both should get denser. Restore the value afterwards. That round trip is the actual test that you have one definition and not two.
|
||||
|
||||
Generate the VSCode workspace (`/iris studio vscode`) so schema completion offers valid snippet paths for each field. See `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
## How resolution works
|
||||
|
||||
1. Iris registers a type adapter for every class annotated `@Snippet("type-name")`.
|
||||
2. On read, if the JSON token is an object, the adapter parses it normally. Nothing snippet-specific happens.
|
||||
3. If the token is a **string**, the adapter treats it as a path:
|
||||
- The string must start with `snippet/`. If it doesn't, the field resolves to **null with no error message** — this is the failure mode that looks like the field was ignored.
|
||||
- The literal `snippet/` prefix is stripped and replaced with `snippet/<type-name>/` for the field being read. Only the prefix is rewritten; the rest of the path is kept verbatim. So writing `"snippet/style/bedrock"` in a `decorator` field becomes a lookup for `snippet/decorator/style/bedrock.json`, which won't exist. The rewrite is a convenience for the common `snippet/<correct-type>/…` case, not a search across type folders.
|
||||
- The file is read from the pack root. A missing file logs `Couldn't find snippet <path>` and the field resolves to null.
|
||||
4. Snippet files are parsed with the same adapters, so a snippet can reference other snippets in its own nested fields.
|
||||
5. Files may sit in subfolders under the type folder; the path in the reference is everything after `snippet/<type>/`, with forward slashes.
|
||||
|
||||
Studio schema generation exposes every snippet type as an `anyOf` of "object or string", and fills the string branch's enum from the files actually present under `snippet/<type>/`.
|
||||
|
||||
### Disk layout
|
||||
|
||||
```
|
||||
pack/
|
||||
snippet/
|
||||
decorator/
|
||||
bush.json
|
||||
dry_grass.json
|
||||
forest/
|
||||
fern.json
|
||||
style/
|
||||
bedrock.json
|
||||
deepslate.json
|
||||
```
|
||||
|
||||
The folder is singular `snippet/`, not `snippets/`. Subfolder names must match the `@Snippet` value exactly.
|
||||
|
||||
### Overworld usage
|
||||
|
||||
Dimension ore deposits reference a style snippet:
|
||||
|
||||
```json
|
||||
{
|
||||
"chanceStyle": "snippet/style/bedrock"
|
||||
}
|
||||
```
|
||||
|
||||
`snippet/style/bedrock.json`:
|
||||
|
||||
```json
|
||||
{ "style": "STATIC" }
|
||||
```
|
||||
|
||||
Biome decorator lists take snippet strings as array elements, mixed freely with inline objects:
|
||||
|
||||
```json
|
||||
{
|
||||
"decorators": [
|
||||
"snippet/decorator/wildflowers",
|
||||
"snippet/decorator/bush",
|
||||
{ "chance": 0.01, "palette": [{ "block": "minecraft:sweet_berry_bush" }] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`biomes/dev.json` in the shipping pack uses the same pattern for a minimal decorator list.
|
||||
|
||||
## When to use a snippet
|
||||
|
||||
Use one when the same value genuinely appears in more than one place and should change in lockstep: decorators shared across a climate's biomes, a noise style reused by several generators, a palette that defines a pack's stone. That's what the shipping Overworld pack uses them for.
|
||||
|
||||
Skip it when a value appears once. A snippet reference costs a file open and adds a place to look when something goes wrong, and it buys nothing if there's a single call site. It also makes the failure mode worse: a wrong-type or missing snippet resolves to null after logging, so a field that should have had a value silently has none. Treat pack validation and a clean console as required gates whenever you add or move snippet files.
|
||||
|
||||
## Packaging and snippets
|
||||
|
||||
Because the adapter writes resolved objects rather than strings, exported packs handle snippets differently per platform:
|
||||
|
||||
- **Bukkit `/iris studio package`** re-serializes the loaded object graph, so snippet references are inlined into the dimension, region, biome, and generator JSON. The export has no `snippet/` folder and doesn't need one.
|
||||
- **Modded `/iris studio package`** copies the source JSON files verbatim and does **not** copy `snippet/`, so snippet references in a modded export are dangling.
|
||||
|
||||
See `25 - Pack Management.md` for the full export contents and the gaps in both compilers.
|
||||
|
||||
## `@Snippet` type names
|
||||
|
||||
Each value is the folder name under `snippet/` and the required prefix for references to that field.
|
||||
|
||||
| Snippet value | Class |
|
||||
|---------------|-------|
|
||||
| `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` |
|
||||
|
||||
Whole-file registrants — dimensions, regions, biomes, generators, loot tables, entities, spawners, markers, mods, objects, structures — are not snippet types. They already have their own folders and are referenced by key. Only nested field types appear above.
|
||||
|
||||
## Pack mods (`IrisMod`) — schema only, not applied
|
||||
|
||||
Folder: `mods/`. The load key is the path under `mods/` without `.json`. `IrisData` registers a loader for these files, so they parse, appear in tooling, and show up in generated schemas — but no engine path reads them. Neither world creation nor Studio hotload consumes an `IrisMod`. A `mods/*.json` file that looks correct will change nothing about the terrain you generate.
|
||||
|
||||
To get the same effect, edit the target dimension, region, biome, generator, or object placement directly. If you need the same edit applied to several packs, keep the edits in version control rather than expecting the mod schema to layer them at runtime.
|
||||
|
||||
The fields below are documented because they still appear in schema completion and because packs in the wild contain them, not because they work.
|
||||
|
||||
| Field | Type | Default | Intended meaning |
|
||||
|-------|------|---------|------------------|
|
||||
| `name` | string | `"A Pack Modification"` | Human name, at least 2 characters |
|
||||
| `forDimension` | string | `""` | Dimension load key to scope to; empty means any |
|
||||
| `overrideFluidHeight` | int -1..512 | `-1` | Fluid height override; `-1` leaves it alone |
|
||||
| `removeBiomes` | string[] | `[]` | Biome keys to strip |
|
||||
| `removeObjects` | string[] | `[]` | Object keys to strip |
|
||||
| `removeRegions` | string[] | `[]` | Region keys to strip |
|
||||
| `injectRegions` | string[] | `[]` | Region keys to add to the dimension |
|
||||
| `biomeInjectors` | `IrisModBiomeInjector[]` | `[]` | Add biomes to a region |
|
||||
| `biomeReplacers` | `IrisModBiomeReplacer[]` | `[]` | Swap one biome for another |
|
||||
| `objectReplacers` | `IrisModObjectReplacer[]` | `[]` | Swap object keys |
|
||||
| `biomeObjectPlacementInjectors` | `IrisModObjectPlacementBiomeInjector[]` | `[]` | Add object placements to a biome |
|
||||
| `regionObjectPlacementInjectors` | `IrisModObjectPlacementRegionInjector[]` | `[]` | Add object placements to a region |
|
||||
| `regionReplacers` | `IrisModRegionReplacer[]` | `[]` | Swap regions |
|
||||
| `blockReplacers` | `IrisObjectReplace[]` | `[]` | Block find/replace, same shape as object material replacers |
|
||||
| `styleReplacers` | `IrisModNoiseStyleReplacer[]` | `[]` | Replace `NoiseStyle` usages |
|
||||
|
||||
Shapes of the nested types, all of which are also registered snippet types:
|
||||
|
||||
```json
|
||||
{ "region": "temperate", "inject": ["temperate/meadows"] }
|
||||
```
|
||||
```json
|
||||
{ "find": ["temperate/plains"], "replace": "temperate/lush-plains" }
|
||||
```
|
||||
```json
|
||||
{ "find": ["temperate"], "replace": "forests" }
|
||||
```
|
||||
```json
|
||||
{ "find": ["clutter/camp1"], "replace": "clutter/camp3" }
|
||||
```
|
||||
```json
|
||||
{ "biome": "temperate/plains", "place": [{ "chance": 0.01, "place": ["clutter/camp1"] }] }
|
||||
```
|
||||
|
||||
`IrisModObjectPlacementRegionInjector` uses the field name `biome` even though the value is a region load key. `IrisModNoiseStyleReplacer` takes `find` (a `NoiseStyle` enum value), `replace` (a full `IrisGeneratorStyle`), and `replaceTypeOnly` (swap only the style type and keep the rest of the style's fields).
|
||||
|
||||
## Other registered schemas with no runtime consumer
|
||||
|
||||
Schema registration alone doesn't prove there's a consumer. These are visible to loaders or schema generation but are not supported pack features:
|
||||
|
||||
| Surface | Status |
|
||||
|---------|--------|
|
||||
| `mods/*.json` (`IrisMod`) | Parsed and registered, never applied. See above |
|
||||
| `potion-effect` / `IrisPotionEffect` | The snippet type exists, but no production field is typed as `IrisPotionEffect`. Use the `potionEffect`, `potionStrength`, and `potionTicks*` fields on `IrisEffect` instead |
|
||||
| `matter/` resources | A loader exists for Matter binaries, but no generation or runtime path reads pack `matter/` resources |
|
||||
| `IrisObjectPlacement.translateCenter` | Serialized and carried through placement copies, but no placement path reads the value |
|
||||
|
||||
## Related commands
|
||||
|
||||
- Pack validation: `/iris pack validate` — see `25 - Pack Management.md` and `04 - Commands & Permissions.md`.
|
||||
- Studio open, hotload, and VSCode schema generation: `10 - Studio & VSCode Schemas.md`.
|
||||
@@ -1,273 +0,0 @@
|
||||
# 25 - Pack Management
|
||||
|
||||
This page covers the lifecycle of a pack outside the editor: pulling one down from GitHub, validating it, clearing out resources nothing references, packaging it for distribution, and — carefully — swapping the pack snapshot inside a world that already exists. Authoring packs live under the platform packs root; production worlds hold their own copy at `<world>/iris/pack`.
|
||||
|
||||
See also: `03 - Configuration.md`, `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `06 - Worlds & Lifecycle.md`, `10 - Studio & VSCode Schemas.md`, `24 - Pack Mods & Snippets.md`, `27 - Example - Configuring Overworld.md`.
|
||||
|
||||
## The mental model
|
||||
|
||||
A pack exists in up to three places at once, and confusing them is the usual source of "my edit did nothing":
|
||||
|
||||
- **The authoring copy**, at `packs/<key>/`. This is what Studio edits and what `/iris create` copies from.
|
||||
- **The world snapshot**, at `<world>/iris/pack`. Every Iris world holds a full copy of the pack it was created with. It is frozen at creation time. Editing the authoring copy never touches an existing world.
|
||||
- **The export**, at `exports/<key>.iris`. A zip of the dimension's dependency closure, for handing to somebody else.
|
||||
|
||||
Validation runs against a directory, not a key, so a pack can be valid in the workspace and stale in a world. Iris caches startup validation results and re-uses them only when the pack bytes, the visible pack set, the platform, and the relevant game registries all still match — otherwise it revalidates.
|
||||
|
||||
## Walkthrough: take a pack from workspace to release
|
||||
|
||||
Run this after the pack works in Studio and before you create or update a production world.
|
||||
|
||||
**1. Place the authoring tree.** `packs/<key>/` must contain at least one `dimensions/*.json`. On Bukkit that's the plugin data folder's `packs/`; on Fabric/Forge/NeoForge it's `config/irisworldgen/packs/`.
|
||||
|
||||
**2. Validate and read the result.**
|
||||
|
||||
```
|
||||
# Bukkit
|
||||
/iris pack validate pack=<key>
|
||||
/iris pack status pack=<key>
|
||||
|
||||
# Modded
|
||||
/iris pack validate <key>
|
||||
/iris pack status <key>
|
||||
```
|
||||
|
||||
`validate` re-runs every check and republishes the result. `status` prints the currently published result, which may be a reused startup result — run `validate` first if you've edited files. Continue only when the pack reports loadable with zero blocking errors. Warnings are informational, but read them: unresolved content keys become blocking the moment strict content mode is on.
|
||||
|
||||
**3. Preview cleanup without writing anything.**
|
||||
|
||||
```
|
||||
# Bukkit
|
||||
/iris pack cleanup <key> mode=preview
|
||||
|
||||
# Modded
|
||||
/iris pack cleanup <key>
|
||||
```
|
||||
|
||||
Preview is the default on both platforms and touches nothing. Read every candidate. Cleanup finds resources with no inbound reference, which includes resources you load dynamically or reference from something it doesn't scan — if a candidate is intentional, stop here and leave cleanup unapplied.
|
||||
|
||||
**4. Apply cleanup only if the preview was clean.**
|
||||
|
||||
```
|
||||
# Bukkit
|
||||
/iris pack cleanup <key> mode=apply
|
||||
|
||||
# Modded
|
||||
/iris pack cleanup <key> apply
|
||||
```
|
||||
|
||||
Files move into `<pack>/.iris-trash/<timestamp>/` rather than being deleted, and the pack's cached validation result is dropped. Validate again afterwards. If cleanup took something you needed, `/iris pack restore <key> mode=apply` (Bukkit) or `/iris pack restore <key> apply` (modded) moves the most recent quarantine dump back.
|
||||
|
||||
**5. Package the closure.**
|
||||
|
||||
```
|
||||
# Bukkit
|
||||
/iris studio package dimension=<key>
|
||||
|
||||
# Modded
|
||||
/iris studio package <key>
|
||||
```
|
||||
|
||||
Success is `exports/<key>.iris` plus a completion message. The source pack and every world snapshot are untouched. **Read the "What the package actually contains" section below before shipping** — neither compiler writes `spawners/` or `markers/`, so a pack that uses ambient spawning needs those folders added back by hand.
|
||||
|
||||
**6. Test on a disposable world.** Create a fresh world from the release pack, walk it, restart the server, and walk it again. For breaking pack changes, ship a new world rather than updating an old one.
|
||||
|
||||
**7. Only then consider replacing an existing world's snapshot.** Take a backup first and use the update-world procedure at the bottom of this page.
|
||||
|
||||
The loop passes when the source closure validates, the package command produces the expected export, a fresh world from that export reloads cleanly, and the world's snapshot matches what you intended to ship. If your release process distributes the `.iris` file rather than the source tree, unpack it and validate it separately — the packager's output is not automatically validated.
|
||||
|
||||
## Pack workspace
|
||||
|
||||
| Item | Rule |
|
||||
|------|------|
|
||||
| Packs root | Bukkit: plugin data folder `packs/`. Modded: `config/irisworldgen/packs/` |
|
||||
| Visible packs | Non-hidden directories directly under the packs root |
|
||||
| "Present" | The directory resolves inside the packs root, passes a safe-tree check (no symlinks escaping), has a real `dimensions/` directory, and contains at least one regular `.json` file in it. Content is never parsed, so a pack with broken JSON is still "present" and will not be re-downloaded |
|
||||
| Safe key | Download destination folder names must match `[a-z0-9_-]+` |
|
||||
|
||||
## Download
|
||||
|
||||
| Command | Syntax |
|
||||
|---------|--------|
|
||||
| Bukkit | `/iris download <pack> [branch=stable] [overwrite=false]` (alias `/iris dl`) |
|
||||
| Modded | `/iris download <pack> [branch] [overwrite]` (alias `/iris dl`) |
|
||||
|
||||
| Param | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `pack` | required | The folder key, or the repository short name under the `IrisDimensions` GitHub org |
|
||||
| `branch` | `stable` | The Git ref to pull. Ignored for the managed beta packs, which always use their release zip |
|
||||
| `overwrite` | `false` | Replace a pack that is already present. Without it, a present pack short-circuits before any network call. Alias `force` on Bukkit |
|
||||
|
||||
`overworld` and `underworld` are **managed beta packs**: they resolve to fixed release zips rather than branch archives.
|
||||
|
||||
| Pack | Source |
|
||||
|------|--------|
|
||||
| `overworld` | `https://github.com/IrisDimensions/overworld/releases/download/beta/overworld.zip` |
|
||||
| `underworld` | `https://github.com/IrisDimensions/underworld/releases/download/beta/underworld.zip` |
|
||||
|
||||
Anything else resolves to `IrisDimensions/<pack>/<branch>` and downloads the GitHub archive for that ref.
|
||||
|
||||
### Install pipeline (`PackDownloader`)
|
||||
|
||||
1. Take a per-key/per-ref lock, so a startup bootstrap and a manual command can't fetch the same pack twice.
|
||||
2. If the pack is already present and `overwrite` is off, return without touching the network.
|
||||
3. Download the zip under hard limits: archive at most 512 MiB, at most 100,000 entries, at most 2 GiB total uncompressed, at most 256 MiB per file. Exceeding any of these aborts the install.
|
||||
4. Unpack into a temporary staging directory and require exactly one pack home directory inside the archive.
|
||||
5. Open the staging tree with the datapack-compiler loader and pick the dimension. A plain download with no expected key requires exactly one dimension in the archive. A managed or listing-driven download names its expected key, requires exactly one dimension matching it, and keeps any additional dimension resources in the pack.
|
||||
6. Run `PackValidator.validate` against the staging tree. Any blocking error aborts the install and prints the errors — nothing is published.
|
||||
7. Publish atomically into `packs/<key>/`, refusing symlinked targets and refusing a key that conflicts with a different folder's dimension key. The validation result is published to the registry as part of the same step.
|
||||
|
||||
Startup treats the two managed packs independently: an operator who has edited or symlinked one of them keeps their version, and that doesn't stop the other from updating.
|
||||
|
||||
## Validate
|
||||
|
||||
| Command | Syntax |
|
||||
|---------|--------|
|
||||
| Bukkit | `/iris pack validate [pack=<key>]` (alias `v`) |
|
||||
| Modded | `/iris pack validate [<pack>]` (alias `v`) |
|
||||
| Bukkit | `/iris pack status [pack=<key>]` (alias `s`) |
|
||||
| Modded | `/iris pack status [<pack>]` (alias `s`) |
|
||||
|
||||
Omitting the pack validates every visible pack and reports how many are broken. `status` reads the published result instead of re-running anything, so after editing files `status` can be stale until you run `validate`.
|
||||
|
||||
### What gets checked (`PackValidator`)
|
||||
|
||||
| Check | Blocking or warning |
|
||||
|-------|---------------------|
|
||||
| Pack folder missing, `dimensions/` missing, or no dimension JSON in it | Blocking, and stops the rest of validation |
|
||||
| Dimension JSON integrity | Blocking errors and warnings, as emitted |
|
||||
| Legacy cave-profile field names, in dimensions/regions/biomes and in `snippet/cave-profile/` | Blocking, with the replacement name named |
|
||||
| Loot graph — every referenced loot table resolves | Blocking |
|
||||
| Removed worldgen fields (currently `fluidBodies`) | Blocking |
|
||||
| Object surface support | Blocking |
|
||||
| `rotation` / `translate` / `scale` on surfaces that don't support them | Blocking |
|
||||
| Structure graph and compiled structure graph | Errors blocking, warnings advisory |
|
||||
| Native structure replacement envelopes | Blocking |
|
||||
| Spawner entries pointing at entities that exist, across both `spawns` and `initialSpawns` | Blocking |
|
||||
| Custom biome spawn category resolution | Blocking |
|
||||
| Content keys and block properties | Blocking when `general.strictContentKeys` is on or `-Diris.strictContent` is set, otherwise warnings. Palette-sourced findings stay advisory either way |
|
||||
|
||||
A pack is loadable when it has zero blocking errors. `status` prints the blocking count and up to ten warnings plus a "more" count.
|
||||
|
||||
### The validation cache
|
||||
|
||||
Bukkit persists both successful and failed startup validation results and reuses them only when everything below still matches:
|
||||
|
||||
- the exact set of visible pack names,
|
||||
- a content fingerprint over the pack bytes,
|
||||
- the validator's own schema version,
|
||||
- strict-content mode,
|
||||
- the platform name, Minecraft version, and Iris version,
|
||||
- the sorted key sets of the live block, biome, item, and entity registries,
|
||||
- the sorted key sets of the live structure, jigsaw-structure, template-pool, structure-set, and object-feature hooks.
|
||||
|
||||
Any mismatch — changed bytes, a mod added or removed, a version bump, a missing or extra pack, a malformed or oversized cache file, or a manual `validate` — discards the cache. Cached *failures* stay blocking, so a pack that failed validation at startup will not authorize world or Studio creation until it validates clean.
|
||||
|
||||
## Cleanup (unused resources)
|
||||
|
||||
| Command | Mode | Behaviour |
|
||||
|---------|------|-----------|
|
||||
| Bukkit `/iris pack cleanup <pack> [mode=preview]` (alias `c`) | `preview` (default) | List candidates. No writes |
|
||||
| | `apply` | Move candidates to quarantine |
|
||||
| Modded `/iris pack cleanup <pack> [apply]` (alias `c`) | no literal (default) | Preview |
|
||||
| | `apply` | Move candidates to quarantine |
|
||||
|
||||
Folders scanned for unreferenced JSON: `biomes`, `regions`, `entities`, `spawners`, `loot`, `generators`, `expressions`, `markers`, `blocks`, `mods`.
|
||||
|
||||
Excluded from the reference corpus entirely: `.iris-trash`, `datapack-imports`, `externaldatapacks`, `internaldatapacks`, `datapacks`, `cache`, `objects`, `.iris`.
|
||||
|
||||
Applying re-scans from scratch rather than trusting an earlier preview, so a preview you ran an hour ago can't quarantine something you've since started using. Quarantined files land under `<pack>/.iris-trash/<yyyyMMdd-HHmmss-SSS>/`. A failed apply rolls back what it can and reports any paths that are still quarantined so you can restore them by hand. A successful apply drops the pack's cached validation result.
|
||||
|
||||
## Restore
|
||||
|
||||
| Command | Mode | Behaviour |
|
||||
|---------|------|-----------|
|
||||
| Bukkit `/iris pack restore <pack> [mode=preview]` (alias `r`) | `preview` (default) | List the files in the most recent quarantine dump, plus any destination conflicts |
|
||||
| | `apply` | Move them back |
|
||||
| Modded `/iris pack restore <pack> [apply]` (alias `r`) | no literal (default) | Preview |
|
||||
| | `apply` | Move them back |
|
||||
|
||||
Restore operates on the **latest** dump only. It refuses the whole operation when any destination path already exists and reports the conflict list instead of merging — resolve those by hand first. With no quarantine dump present, nothing is restored and nothing is reported as an error.
|
||||
|
||||
## Package (export)
|
||||
|
||||
| Command | Syntax |
|
||||
|---------|--------|
|
||||
| Bukkit | `/iris studio package [dimension=default] [obfuscate=false] [minify=true]` (method alias `pkg`) |
|
||||
| Modded | `/iris studio package [<pack>]` (alias `pkg`) |
|
||||
|
||||
| Param | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `dimension` | contextual, else `default` | The dimension to package. The closure is walked from here |
|
||||
| `obfuscate` | `false` | Rename every object to a random UUID in the export and rewrite placement references to match. Bukkit only |
|
||||
| `minify` | `true` | Write JSON with no indentation. Bukkit only; the modded packager always minifies |
|
||||
|
||||
Output is `exports/<dimensionKey>.iris` — under the plugin data folder on Bukkit, under `config/irisworldgen/exports/` on modded. The staging folder is deleted after zipping (compression level 9). Neither the source pack nor any world snapshot is modified.
|
||||
|
||||
### What the package actually contains
|
||||
|
||||
Both compilers walk the dimension, its regions, their biomes, and collect generators, loot tables, entity keys, object keys, and the structure closure. Both write `package.json` with a content hash, a timestamp, and the dimension's `version`.
|
||||
|
||||
Written to the export:
|
||||
|
||||
`dimensions/`, `regions/`, `biomes/`, `generators/`, `blocks/` (all block definitions in the pack, not just referenced ones), `loot/`, `entities/`, `objects/`, the structure closure, `package.json`.
|
||||
|
||||
**Not written by either compiler:**
|
||||
|
||||
- `spawners/` — spawner keys are resolved only to collect the entity keys they reference. The spawner JSON files themselves never reach the export, so a pack that uses ambient spawning produces an export whose `entitySpawners` references all dangle.
|
||||
- `markers/` — never collected or written.
|
||||
- `mods/` — never collected or written. Harmless, since nothing applies them (see `24 - Pack Mods & Snippets.md`).
|
||||
- `expressions/`, `caves/`, `images/` and other folders outside the collected set.
|
||||
|
||||
Platform differences beyond that:
|
||||
|
||||
- Bukkit re-serializes from the loaded object graph, which inlines snippet references. Modded copies the source JSON verbatim and does not copy `snippet/`, so snippet references in a modded export dangle too.
|
||||
- Bukkit collects entity keys from both `spawns` and `initialSpawns`; modded collects from `spawns` only.
|
||||
|
||||
If you distribute the `.iris` artifact, unpack it, add the missing folders, and validate the unpacked tree before you publish. If you distribute the source pack directory, none of this applies.
|
||||
|
||||
## Developer update-world (unsafe)
|
||||
|
||||
| Command |
|
||||
|---------|
|
||||
| `/iris developer update-world world=<world> pack=<dimension> confirm=true [fresh-download=false]` |
|
||||
|
||||
Aliases: the command group is `/iris developer` or `/iris dev`; the subcommand is `update-world` or `^world`. `pack` also accepts the alias `dimension`, `confirm` accepts `c`, and `fresh-download` accepts `fresh` or `new`.
|
||||
|
||||
| Param | Default | What it does |
|
||||
|-------|---------|--------------|
|
||||
| `world` | contextual | The world whose `iris/pack` snapshot gets replaced |
|
||||
| `pack` | contextual | The source dimension, resolved from the live packs root |
|
||||
| `confirm` | `false` | Required. Without it the command only prints the warning and exits |
|
||||
| `fresh-download` | `false` | Re-download the pack from GitHub before installing |
|
||||
|
||||
What it does:
|
||||
|
||||
1. Refuse unless `confirm=true`.
|
||||
2. Optionally re-download the pack.
|
||||
3. Take a `PACK_MUTATION` / `PACK_PUBLISH` lease, so it can't race another pack publish. If the lease is busy it reports that and stops.
|
||||
4. Copy the pack into a staging directory next to the target, confirm the dimension loads from staging, then publish atomically over `<world>/iris/pack`.
|
||||
5. Invalidate the previous validation result for that exact root and validate the newly published snapshot. **If validation fails, the publication rolls back** and the world keeps its old pack.
|
||||
6. If an engine is still holding the old pack data, restart the server with the reason `"An active Iris world pack was replaced."`
|
||||
|
||||
This is unsafe for production without a backup for reasons the command can't fix: chunks that already exist keep their old terrain. Only future generation and pack-driven systems — loot, spawners, effects, block drops — see the new content. A pack change that alters terrain shape leaves a visible seam at the edge of the generated region. Prefer staging a new world whenever the pack's terrain contract changes.
|
||||
|
||||
## Related operations
|
||||
|
||||
| Task | Where |
|
||||
|------|-------|
|
||||
| Create a studio project from a template | `/iris studio create` — `10 - Studio & VSCode Schemas.md` |
|
||||
| Open VSCode with generated schemas | `/iris studio vscode` — `10 - Studio & VSCode Schemas.md` |
|
||||
| Import vanilla objects and structures into a pack | `/iris studio importvanilla` — `19 - Objects.md` |
|
||||
| Structure import and conversion | `/iris structure …` — `22 - Native Structures & Datapacks.md` |
|
||||
| Strict content key enforcement | `settings.general.strictContentKeys` — `03 - Configuration.md` |
|
||||
| Datapack bootstrap and install | `/iris datapack` — `22 - Native Structures & Datapacks.md` |
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Place or download the pack under `packs/<key>/` with at least one `dimensions/*.json`.
|
||||
2. Validate until loadable: `/iris pack validate pack=<key>` on Bukkit, `/iris pack validate <key>` on modded.
|
||||
3. Optionally preview cleanup, review every candidate, then apply and validate again. Restore if it took something needed.
|
||||
4. Create a world with `/iris create …`, which copies the pack into the world, or open Studio for live editing.
|
||||
5. Package with `/iris studio package dimension=<key>` (Bukkit) or `/iris studio package <key>` (modded), then add back `spawners/` and `markers/` if the pack uses them.
|
||||
6. Replace an existing world's snapshot only after a backup, with `/iris dev update-world world=<world> pack=<dimension> confirm=true`.
|
||||
@@ -1,244 +0,0 @@
|
||||
# 26 - Example - Minimal Dimension
|
||||
|
||||
This is a guided build of the smallest pack Iris will actually generate: one dimension, one region, one biome, one generator. You will write four files, validate them, prove them in Studio on a fixed seed, and then prove them again in a real world across a server restart. Keep this four-file state as a rollback point before adding anything else.
|
||||
|
||||
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`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Iris is running and its data folders exist.
|
||||
- Operator access on Bukkit, or gamemaster access on a mod loader.
|
||||
- No pack or world already uses the keys `minimal` or `minimal-test`.
|
||||
- You can watch the server console while validation, Studio open, world create, and restart run.
|
||||
|
||||
Do not add objects, caves, structures, custom biomes, or datapacks until this baseline generates and reloads cleanly. Every one of those introduces its own failure mode, and they are far easier to diagnose one at a time.
|
||||
|
||||
## 1. Create the pack root
|
||||
|
||||
**What you do.** Make this tree under the platform packs root:
|
||||
|
||||
```
|
||||
minimal/
|
||||
dimensions/minimal.json
|
||||
regions/starter.json
|
||||
biomes/starter.json
|
||||
generators/flat.json
|
||||
```
|
||||
|
||||
- Bukkit-family packs root: `plugins/Iris/packs/`
|
||||
- Fabric / Forge / NeoForge packs root: `config/irisworldgen/packs/`
|
||||
|
||||
**Why.** The folder name is the pack key. The dimension file name without `.json` is the dimension load key — here both are `minimal`, which is the convention worth keeping because commands take the key, not the path. Iris resolves every other resource by its path under its type folder, so `biomes/starter.json` is biome key `starter` and `biomes/plains/dry.json` would be key `plains/dry`.
|
||||
|
||||
**What you should see.** Nothing yet; Iris does not notice new folders until something loads the pack.
|
||||
|
||||
You can also have Iris write the skeleton for you:
|
||||
|
||||
| Platform / method | Command or action |
|
||||
|-------------------|-------------------|
|
||||
| Bukkit starter (no template) | `/iris studio create name=minimal` |
|
||||
| Bukkit template copy | `/iris studio create name=minimal template=overworld` |
|
||||
| Modded (always uses a template; `example` by default) | `/iris studio create minimal` |
|
||||
| Modded template copy | `/iris studio create minimal overworld` |
|
||||
|
||||
The Bukkit starter writes the same four resource types described below. Modded studio create always copies a template, so create the tree by hand when you want exactly this four-file baseline on a mod loader.
|
||||
|
||||
## 2. Write the dimension
|
||||
|
||||
**What you do.** Save `dimensions/minimal.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "minimal",
|
||||
"version": 1,
|
||||
"mode": { "type": "OVERWORLD" },
|
||||
"regions": ["starter"],
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": { "min": -64, "max": 320 },
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 63
|
||||
}
|
||||
```
|
||||
|
||||
**Why each field is here:**
|
||||
|
||||
| Field | Why |
|
||||
|-------|-----|
|
||||
| `name` | Display name used by commands and the studio scoreboard. Marked required in the schema; the file name is what actually identifies the dimension |
|
||||
| `regions` | The only mandatory content link. Without at least one loadable region key, no biome can ever be selected |
|
||||
| `mode` | `OVERWORLD` is the only mode that registers caves, objects, decoration and deposits. The other three register terrain and biome only. Omitting the field also yields `OVERWORLD`, but writing it out makes the choice visible |
|
||||
| `environment` | Picks the vanilla dimension template the generated dimension type is built from — sky, fog and gameplay attributes, not terrain |
|
||||
| `dimensionHeight` | Build floor -64, ceiling 320. The span (384) and the minimum (-64) are both multiples of 16, which Minecraft requires for the generated dimension type |
|
||||
| `logicalHeight` | 384, equal to the total height. It must not exceed the total height or dimension-type construction throws |
|
||||
| `fluidHeight` | World Y of sea level — 63, the vanilla value. Biome generator heights are measured from here |
|
||||
| `version` | A stamp you control so pack generations are distinguishable. Iris never acts on it |
|
||||
|
||||
`dimensionHeight`, `logicalHeight`, `environment`, and the file name are the world contract. Once a world exists on this pack, changing any of them means recreating the world; Studio hotload refuses them outright. Everything else in this guide is safe to iterate on.
|
||||
|
||||
Useful while testing, and removed before shipping: `"focus": "starter"` forces a single biome and `"focusRegion": "starter"` forces a single region.
|
||||
|
||||
## 3. Write the region
|
||||
|
||||
**What you do.** Save `regions/starter.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Starter",
|
||||
"landBiomes": ["starter"],
|
||||
"seaBiomes": ["starter"],
|
||||
"shoreBiomes": ["starter"]
|
||||
}
|
||||
```
|
||||
|
||||
**Why.** A region is the biome pool for one area of the world. Iris decides land versus sea first, then picks from the matching list, so a region with an empty `seaBiomes` cannot fill an ocean column. Listing the same biome in all three lists means every column resolves no matter which category the terrain lands in — exactly what you want for a first test world.
|
||||
|
||||
| Field | Why |
|
||||
|-------|-----|
|
||||
| `name` | Required display name |
|
||||
| `landBiomes` | Required. Root-level biome keys only |
|
||||
| `seaBiomes` / `shoreBiomes` | Optional for genuinely land-only packs; included here so no column can fail to resolve |
|
||||
| `caveBiomes` | Optional; not needed until caves are enabled |
|
||||
|
||||
List only root parents here. Child biomes are declared on their parent biome's `children`, not on the region.
|
||||
|
||||
**What you should see.** Nothing yet — but if you validate now, an unresolvable biome key is reported as a blocking error, which is the fastest way to catch a typo.
|
||||
|
||||
## 4. Write the biome
|
||||
|
||||
**What you do.** Save `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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Why.** The biome supplies two things: a height, from its generator links, and a surface material stack, from its layers.
|
||||
|
||||
| Field | Why |
|
||||
|-------|-----|
|
||||
| `name` | Required display name; `/iris what biome` prints it |
|
||||
| `derivative` | Required. The vanilla biome this maps to for client-side coloring, mob spawning tables and vanilla feature eligibility. The engine default is `minecraft:the_void`, which generates nothing useful, so always set it |
|
||||
| `vanillaDerivative` | The derivative used for native structure selection. When left undefined it falls back to `derivative`; set it explicitly when a biome should look like one thing and attract another thing's structures |
|
||||
| `layers` | Required. The surface stack from the top down. Each layer's `minHeight`/`maxHeight` are **thickness in blocks**, not Y coordinates, and default to 1. Everything below the declared layers is filled with the dimension's rock palette |
|
||||
| `generators` | Links to `generators/<key>.json` with a height band. `min` and `max` are offsets **from `fluidHeight`**, not absolute Y |
|
||||
|
||||
With `fluidHeight` 63 and `min` = `max` = 96, every column resolves to exactly 96 above sea level, so the surface lands at world Y 159 — a high flat plateau with the ocean far below it. That is deliberate: it makes the terrain obviously generated rather than accidentally matching vanilla. For plains near sea level use small values instead; the shipping overworld's plains biome uses `min` 4 / `max` 10 on generator `plain`.
|
||||
|
||||
## 5. Write the generator
|
||||
|
||||
**What you do.** Save `generators/flat.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"interpolator": { "function": "NONE", "horizontalScale": 1 },
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": { "style": "FLAT" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Why.** A generator turns coordinates into a 0-to-1 noise value, which the biome's `min`/`max` band then maps into a height. `FLAT` returns a constant, and because `min` equals `max` the mapping is constant anyway, so the result is a perfectly level surface. The interpolator controls how neighbouring biomes blend their heights together; `NONE` gives hard edges, which is what you want while proving the plumbing. This file matches the shipping overworld's `generators/flat.json` and the studio starter byte for byte.
|
||||
|
||||
| Field | Why |
|
||||
|-------|-----|
|
||||
| `seed` | Required. Changing it re-rolls this generator's noise independently of the world seed |
|
||||
| `interpolator` | Required. `NONE` for a hard flat baseline; swap to a bilinear or starcast function once real terrain matters |
|
||||
| `composite` | The noise layers that are summed into the final value. One `FLAT` layer here |
|
||||
|
||||
## 6. Validate
|
||||
|
||||
**What you do.**
|
||||
|
||||
- Bukkit: `/iris pack validate pack=minimal`
|
||||
- Modded: `/iris pack validate minimal`
|
||||
|
||||
**Why.** Studio refuses to open a pack whose validation result is not loadable, and fails closed if validation never ran. Catching a broken key here costs seconds; catching it after a world exists costs a world.
|
||||
|
||||
**What you should see.** No blocking errors. If validation reports a missing region or biome, the key in the parent file and the file path under the type folder disagree — compare them character for character, including the folder prefix.
|
||||
|
||||
Validation checks that the dimension load key resolves, that every region key in `regions` loads, that every biome key on a region loads, and that structure placements fit inside the declared height range. It does **not** check the multiple-of-16 rule on `dimensionHeight`; a bad height passes here and fails later when Iris compiles the dimension type.
|
||||
|
||||
## 7. Prove it in Studio
|
||||
|
||||
**What you do.**
|
||||
|
||||
1. Open: Bukkit `/iris studio open minimal seed=1337`, modded `/iris studio open minimal 1337`.
|
||||
2. Walk into chunks that have never generated.
|
||||
3. Run `/iris what region` and `/iris what biome`.
|
||||
4. Close Studio, reopen it on the same seed, and generate another new area.
|
||||
|
||||
**Why.** Studio runs directly off `packs/minimal/`, so it is the only place where an edit is visible without recreating anything. The fixed seed is what makes step 4 meaningful.
|
||||
|
||||
**What you should see.** A uniform grass surface at world Y 159, region `Starter`, biome `Starter Plains`, and no missing-resource or parse errors in console. After the reopen, the terrain in a fresh area must be identical to what the same coordinates produced before — if it is not, something in the pack is reading a non-deterministic input.
|
||||
|
||||
## 8. Prove it in a real world
|
||||
|
||||
**What you do.**
|
||||
|
||||
1. Create: Bukkit `/iris create minimal-test type=minimal seed=1337`, modded `/iris create minimal-test minimal 1337`. On Folia, creation stages the world and requires the instructed restart before you can enter it.
|
||||
2. Teleport: Bukkit `/iris tp minimal-test`, modded `/iris tp irisworldgen:minimal-test`.
|
||||
3. Generate ordinary new chunks and confirm the same flat grass result you saw in Studio.
|
||||
4. Stop the server cleanly, start it again, teleport back, and generate another new area.
|
||||
5. Confirm `<world>/iris/pack/` contains the four-file snapshot.
|
||||
|
||||
**Why.** World creation copies the pack into the world folder. From then on that world generates from its own copy, so later edits under `packs/minimal/` do not reach it. The restart in step 4 is what proves the generated dimension type survives a registry reload, which is the most common way a height or environment mistake surfaces.
|
||||
|
||||
**What you should see.** Identical terrain in Studio and in the world, no pack or registry errors on the restart, and a real `iris/pack/` directory inside the world folder.
|
||||
|
||||
The walkthrough passes only when validation, the Studio reopen, world creation, teleport, and the server restart all succeed.
|
||||
|
||||
## 9. Extend without breaking the baseline
|
||||
|
||||
Add one thing at a time and re-validate after each, so a broken key is always attributable to the last edit.
|
||||
|
||||
| Add | Where |
|
||||
|-----|-------|
|
||||
| Second biome | New `biomes/*.json`, then append its key to `regions/starter.json` → `landBiomes` |
|
||||
| Sea and shore variety | Distinct biome keys on `seaBiomes` / `shoreBiomes` |
|
||||
| Real terrain | Replace `generators/flat.json` with a composite noise generator, or add a second generator and give the biome a wider `min`/`max` band (`14 - Generators & Noise.md`) |
|
||||
| Caves | `caveProfile` on the dimension, plus cave biomes on the region's `caveBiomes` (`15 - Caves & Carving.md`) |
|
||||
| Decorators | Biome `decorators` array, inline or `snippet/decorator/...` (`16 - Surfaces, Decorators & Deposits.md`) |
|
||||
| Loot | `loot/*.json` plus a `loot` reference on the dimension, region or biome (`23 - Loot, Entities, Spawners, Markers.md`) |
|
||||
| Objects | Biome or region `objects` placements plus `objects/*.iob` (`19 - Objects.md`, `20 - Object Placement.md`) |
|
||||
| Entity spawning | `entities/`, `spawners/`, then `entitySpawners` on the dimension, region or biome |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|---------|-------|
|
||||
| Pack is not listed | Platform packs root, the `minimal/` folder name, and `dimensions/minimal.json` |
|
||||
| Validation reports a missing region | `dimensions/minimal.json` must reference `starter` and `regions/starter.json` must exist |
|
||||
| Validation reports a missing biome | Every region list entry must match a file under `biomes/` with the `.json` removed, including any folder prefix |
|
||||
| Terrain is empty or at the wrong height | Confirm the biome's generator key is `flat`, that `generators/flat.json` parses, and that `min`/`max` are the offsets from `fluidHeight` you intended |
|
||||
| The world is all void | `derivative` is probably still the `minecraft:the_void` default on some biome |
|
||||
| Studio shows old terrain | Move to untouched chunks; close and reopen after a contract change |
|
||||
| Dimension type fails to compile | `dimensionHeight` span or minimum is not a multiple of 16, or `logicalHeight` exceeds the span |
|
||||
| Production world ignores your edits | It runs from `<world>/iris/pack/`; create a new world, or follow the backed-up update procedure in `25 - Pack Management.md` |
|
||||
| Baseline stops working | Restore these exact four files and validate before reintroducing extensions |
|
||||
|
||||
## Next steps
|
||||
|
||||
- Full dimension field reference: `11 - Dimensions.md`
|
||||
- Region zooms, deposits, caves: `12 - Regions.md`
|
||||
- Layers, decorators, children: `13 - Biomes.md`
|
||||
- Noise composites and interpolators: `14 - Generators & Noise.md`
|
||||
- Editing the full shipping overworld: `27 - Example - Configuring Overworld.md`
|
||||
@@ -1,302 +0,0 @@
|
||||
# 27 - Example - Configuring Overworld
|
||||
|
||||
The shipping `overworld` pack is what Iris downloads on first boot and what most servers generate from. This is a guided build: you will fork it, add one visible biome, prove the biome in Studio and in a disposable world, and leave the original pack untouched. It exercises the parts of the workflow that actually bite — references, hotload, world snapshots, and rollback — without touching height or registries.
|
||||
|
||||
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`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- The `overworld` pack is installed and validates.
|
||||
- Operator access on Bukkit, or gamemaster access on a mod loader.
|
||||
- The keys `my-overworld`, `overworld-test`, and `tutorial/meadow` are unused.
|
||||
- The fork is in source control or has a filesystem backup before you rely on it.
|
||||
|
||||
## Where everything lives before you start
|
||||
|
||||
| Platform | Authoritative packs root |
|
||||
|----------|--------------------------|
|
||||
| Bukkit / Paper / Folia / Purpur | `plugins/Iris/packs/overworld/` |
|
||||
| Fabric / Forge / NeoForge | `config/irisworldgen/packs/overworld/` |
|
||||
|
||||
A world created from a pack stores its own **copy** at `<world>/iris/pack/`. `StudioSVC.installIntoWorld` and `replaceIntoWorld` write that copy; normal world generation reads it and never looks at the global `packs/` tree again. Studio worlds are the exception — they run directly off `packs/<key>/`, which is why Studio is where authoring happens.
|
||||
|
||||
On first install Iris downloads the managed Overworld and Underworld beta releases into `packs/`. `/iris download overworld` pulls the same Overworld asset (see `02 - Getting Started.md`, `25 - Pack Management.md`).
|
||||
|
||||
The pack's shape:
|
||||
|
||||
```
|
||||
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/
|
||||
```
|
||||
|
||||
## 1. Fork the pack
|
||||
|
||||
**What you do.**
|
||||
|
||||
- Bukkit: `/iris studio create name=my-overworld template=overworld`
|
||||
- Modded: `/iris studio create my-overworld overworld`
|
||||
|
||||
Wait for the command to report the completed project path — pack creation runs asynchronously and may report that a restart is required before the new pack can be opened.
|
||||
|
||||
Then validate and open:
|
||||
|
||||
- Bukkit: `/iris pack validate pack=my-overworld`, then `/iris studio open my-overworld seed=1337`
|
||||
- Modded: `/iris pack validate my-overworld`, then `/iris studio open my-overworld 1337`
|
||||
|
||||
**Why.** Forking copies the whole tree under a new pack key so upstream Overworld updates cannot clobber your work, and so a mistake is one folder deletion away from being undone. Create your worlds from the fork, not from `overworld`.
|
||||
|
||||
**What you should see.** A `my-overworld` folder next to `overworld` with the same structure, a loadable validation result, and a Studio world that looks exactly like the shipping overworld.
|
||||
|
||||
## 2. Add the biome file
|
||||
|
||||
**What you do.** Save this complete biome as `packs/my-overworld/biomes/tutorial/meadow.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Tutorial Meadow",
|
||||
"rarity": 1,
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [{ "block": "minecraft:grass_block" }]
|
||||
},
|
||||
{
|
||||
"minHeight": 3,
|
||||
"maxHeight": 3,
|
||||
"palette": [{ "block": "minecraft:dirt" }]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{ "generator": "plain", "min": 18, "max": 24 }
|
||||
],
|
||||
"decorators": ["snippet/decorator/wildflowers"]
|
||||
}
|
||||
```
|
||||
|
||||
**Why.** Every piece of this is chosen so the result is unmistakable in game:
|
||||
|
||||
- `generators` reuses the fork's existing `generators/plain.json` (an `IRIS_DOUBLE` composite behind a `BILINEAR_STARCAST_9` interpolator) but at `min` 18 / `max` 24 instead of the 4-to-10 band the shipping plains uses. Those numbers are offsets from `fluidHeight`, which the overworld sets to 50, so this meadow sits roughly 68 to 74 blocks up while ordinary plains sit around 54 to 60. The height difference is what makes it visible from a distance.
|
||||
- `layers` are **thicknesses**, not Y coordinates: one block of grass over three blocks of dirt, with the dimension's rock palette filling everything below.
|
||||
- `decorators` uses a snippet reference. Any field whose type is a snippet type accepts the string form `snippet/<type>/<name>`, and Iris loads `snippet/decorator/wildflowers.json` in its place at parse time. The fork already contains that file.
|
||||
- `rarity` 1 makes it as common as the region's other biomes so you do not have to search for it later.
|
||||
|
||||
Do not copy this file into the original `overworld` folder.
|
||||
|
||||
**What you should see.** With the workspace open, the editor should autocomplete `generator` values against the fork's real generator keys and flag a typo in `derivative` immediately. If it does not, run `/iris studio update dimension=my-overworld` (see `10 - Studio & VSCode Schemas.md`).
|
||||
|
||||
## 3. Attach it and focus on it
|
||||
|
||||
**What you do.** Append `"tutorial/meadow"` to `landBiomes` in `regions/temperate.json`, then merge these two fields into the existing object in `dimensions/my-overworld.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"focusRegion": "temperate",
|
||||
"focus": "tutorial/meadow"
|
||||
}
|
||||
```
|
||||
|
||||
These are field excerpts. Merge them into the existing files; do not replace either file with the fragment. Validate again after both edits.
|
||||
|
||||
**Why.** A biome file that no region lists never generates — nothing warns you about it, it just never gets picked. `regions/temperate.json` already carries 28 land biomes, so a new one would be rare enough to be annoying to find; the two focus fields force the entire world to that region and biome so you can confirm the biome itself is correct before worrying about selection frequency.
|
||||
|
||||
**What you should see.** Validation still loadable. If it cannot resolve the biome, compare `tutorial/meadow` against the actual path and the region entry character for character — the folder prefix is part of the key.
|
||||
|
||||
## 4. Prove the authoring result
|
||||
|
||||
**What you do.** Generate untouched Studio chunks and run `/iris what region` and `/iris what biome`.
|
||||
|
||||
**What you should see.** Region `Temperate`, biome `Tutorial Meadow`, a grass-over-dirt surface, terrain visibly higher than the surrounding shipping plains, wildflower decoration, and no missing-key errors in console.
|
||||
|
||||
If terrain is empty, confirm `generators/plain.json` still exists in the fork. If flowers are missing, confirm `snippet/decorator/wildflowers.json` exists and remove the decorator reference until the terrain baseline passes — one variable at a time.
|
||||
|
||||
## 5. Prove natural selection and restart behavior
|
||||
|
||||
**What you do.**
|
||||
|
||||
1. Remove `focus` and `focusRegion`, close Studio, and reopen on seed `1337`.
|
||||
2. Locate the biome naturally: `/iris find biome tutorial/meadow` (available on Bukkit and on mod loaders; `/iris goto biome <key>` is the same command on modded).
|
||||
3. Create a disposable world: Bukkit `/iris create overworld-test type=my-overworld seed=1337`, modded `/iris create overworld-test my-overworld 1337`.
|
||||
4. Teleport: Bukkit `/iris tp overworld-test`, modded `/iris tp irisworldgen:overworld-test`. On Folia, honor the required restart immediately after the create command before teleporting.
|
||||
5. Generate new chunks, stop the server cleanly, restart, and verify another new area.
|
||||
|
||||
**Why.** Focus mode proves the biome renders; only unfocused generation proves it is actually reachable through region selection. The disposable world proves the pack snapshot works outside Studio, and the restart proves the generated dimension type and custom biomes survive a registry reload.
|
||||
|
||||
**What you should see.** The meadow appearing naturally in temperate regions, `<world>/iris/pack/` present in the world folder, and a clean restart with no pack or registry errors.
|
||||
|
||||
## 6. Package or recover
|
||||
|
||||
**What you do.** Package with Bukkit `/iris studio package dimension=my-overworld` or modded `/iris studio package my-overworld`.
|
||||
|
||||
**Why.** The validated fork under `packs/` is the source of truth. The `.iris` export and the world snapshot are outputs, and both are reproducible from it.
|
||||
|
||||
| Failure | Recovery |
|
||||
|---------|----------|
|
||||
| Fork creation fails or is partial | Move only the newly created incomplete `my-overworld` folder aside, confirm the source pack validates, then rerun |
|
||||
| Studio still shows old content | Generate untouched chunks; close and reopen after a dimension-contract or registry change |
|
||||
| Natural selection cannot find the biome | Confirm it is still in `regions/temperate.json`, that both focus fields are gone, and sample a broader new area |
|
||||
| Disposable world differs from Studio | Inspect `<world>/iris/pack/`; recreate the world from the current validated fork |
|
||||
| A production update would change height, registries, or large terrain systems | Do not update in place; create a new world and migrate deliberately |
|
||||
|
||||
## What the shipping dimension actually sets
|
||||
|
||||
From `dimensions/overworld.json`:
|
||||
|
||||
| Field | Shipping value | Why it matters when you edit |
|
||||
|-------|----------------|------------------------------|
|
||||
| `name` / `version` | `"Overworld"` / `4000` | Bump `version` on your fork so pack generations stay distinguishable |
|
||||
| `dimensionHeight` | `min` -256, `max` 512 | 768 blocks tall. Contract field — do not change it on a fork that already has worlds |
|
||||
| `logicalHeight` | `512` | Contract field |
|
||||
| `fluidHeight` | `50` | World Y of sea level, and the baseline every biome generator band is measured from. Change it and every biome's apparent height moves |
|
||||
| `environment` | `NORMAL` | Contract field |
|
||||
| `landChance` | `0.69` | Land-heavy world |
|
||||
| `regionZoom` | `16.15` | Continent-sized climate regions |
|
||||
| `coordFractureZoom` | `0.15` | Aggressive coordinate warping; this is the source of the swirled borders |
|
||||
| `dimensionAngleDeg` | `69` | Off-axis rotation that hides grid artifacts |
|
||||
| `regions` | `frozen`, `hot`, `terralost`, `mushroom`, `forests`, `tundra`, `magnetics`, `temperate`, `estranged`, `tropical`, `swamp`, `prismatics` | The twelve climate regions your biome must be attached to one of |
|
||||
| `loot` | mode `FALLBACK`, tables `["global-clutter"]` | Fallback only — objects that declare their own loot keep it |
|
||||
| `preventLeafDecay` | `true` | Custom trees keep their canopies |
|
||||
| `useMantle` / `carvingEnabled` / `decorate` | `true` | All content passes on |
|
||||
| `caveProfile` | enabled | Dimension-wide 3D caves, overridden per region |
|
||||
| `carving` | one deep-dark band at world Y -250 to -175 | Depth-banded cave biome |
|
||||
| `mode` | omitted | Runs `OVERWORLD` |
|
||||
|
||||
Also present: region/continental/biome noise styles, 11 ore generators, 20 deposits, one deposit variant that remaps ores to their deepslate forms below Y 0, imported-structure adjustments for stronghold, trial chambers, mineshaft and village, and one ancient-city structure placement with `nativeSuppression: REPLACE_SOURCE`. Field-by-field meanings are in `11 - Dimensions.md`.
|
||||
|
||||
Do not invent region or biome keys. List the directories under `regions/` and `biomes/` and use what is actually there.
|
||||
|
||||
## Reading the region and biome graph
|
||||
|
||||
`regions/temperate.json` is a representative region:
|
||||
|
||||
- `landBiomes` — 28 keys including `temperate/plains`, `temperate/oak-forest`, `mountain/plains`, `vanilla/cherry_grove`
|
||||
- `shoreBiomes` — `temperate/shore/beach`, `ocean/shore/beach`, `vanilla/stony_shore`, others
|
||||
- `seaBiomes` — `ocean/deep`, `temperate/sea/ocean`, `temperate/sea/river`, others
|
||||
- `caveBiomes` — `carving/rocky-cavebiome`, `carving/drip`, `carving/deep`, others
|
||||
- `loot` — mode `FALLBACK`, multiplier `0.5`, tables `temperate/clutter` and `temperate/food`
|
||||
- Per-category zooms (`landBiomeZoom` 3.5, `seaBiomeZoom` 6, `shoreBiomeZoom` 0.15, `caveBiomeZoom` 3.3) and its own enabled `caveProfile`
|
||||
|
||||
`biomes/temperate/plains.json` is a representative biome:
|
||||
|
||||
- `derivative` and `vanillaDerivative` are both `minecraft:plains`
|
||||
- `generators` is `[{ "generator": "plain", "min": 4, "max": 10 }]` — 4 to 10 blocks above sea level
|
||||
- `layers` is one block of grass over two blocks of dirt; the dimension's rock palette fills below
|
||||
- `objects` places `clutter/...` keys in `PAINT` mode at fractions of a percent per column
|
||||
- `decorators` place flowers with a `TRIOCTAVE_SIMPLEX` variance and a fractured `STATIC` style
|
||||
|
||||
`generators/plain.json` is the height source both that biome and your meadow use: a single `IRIS_DOUBLE` composite layer behind a `BILINEAR_STARCAST_9` interpolator at horizontal scale 12.
|
||||
|
||||
## Editing safely
|
||||
|
||||
### Author in Studio, on a fork
|
||||
|
||||
1. Confirm `overworld` exists under `packs/overworld/`.
|
||||
2. Fork it: `/iris studio create name=my-overworld template=overworld`.
|
||||
3. Open Studio: `/iris studio open my-overworld seed=1337`.
|
||||
4. Edit under `packs/my-overworld/` with the generated VSCode workspace and schemas (`10 - Studio & VSCode Schemas.md`).
|
||||
5. Save; hotload picks the change up. Generate new chunks to see it — existing blocks are never rewritten.
|
||||
6. Isolate with `"focus": "temperate/plains"` or `"focusRegion": "temperate"` while testing, and remove both afterwards.
|
||||
7. Make one small change at a time — nudge `biomes/temperate/plains.json` generator `min`/`max` by a few blocks, validate, and compare the same seed in fresh chunks.
|
||||
8. Close Studio, create a disposable world from the fork, and restart-test it before touching anything real.
|
||||
|
||||
### Do not treat the world copy as the source
|
||||
|
||||
Editing `<world>/iris/pack/` changes only that world and is overwritten by the next pack install or update. Author under `packs/`.
|
||||
|
||||
## Practical recipes
|
||||
|
||||
### Change sea level
|
||||
|
||||
Set `fluidHeight` in `dimensions/my-overworld.json` — shipping value `50`. It is world Y, and every biome generator band is measured from it, so lowering it lowers the sea while leaving relative terrain heights intact and raising it drowns low biomes. Only newly generated chunks change, so expect a visible shoreline seam on an existing world.
|
||||
|
||||
### Add a biome to a region
|
||||
|
||||
1. Create `biomes/temperate/my-biome.json` with at least `name`, `derivative`, `layers`, and `generators` (`26 - Example - Minimal Dimension.md`, `13 - Biomes.md`).
|
||||
2. Append `"temperate/my-biome"` to the appropriate list in `regions/temperate.json` — `landBiomes`, `seaBiomes`, `shoreBiomes`, or `caveBiomes`.
|
||||
3. Hotload, then sample with `/iris what biome` and `/iris find biome`.
|
||||
|
||||
Region lists must match real biome load keys. A key that does not resolve is a blocking validation error; a biome file that no region lists is silently dead.
|
||||
|
||||
### Change plains height
|
||||
|
||||
Edit `generators` `min`/`max` on `biomes/temperate/plains.json` to affect only that biome, or edit `generators/plain.json` to affect every biome that references `plain` — which is a lot of them. Prefer the biome-level change unless you mean the global one.
|
||||
|
||||
### Loot
|
||||
|
||||
- Dimension fallback: `dimensions/overworld.json` → `loot.tables`
|
||||
- Region: `regions/temperate.json` → `loot`
|
||||
- Tables live under `loot/` (`global-clutter`, `global-treasure`, `temperate/food`, …)
|
||||
|
||||
Mode `FALLBACK` only supplies tables when the object itself declared none; `ADD` stacks onto the parent scopes; `CLEAR` and `REPLACE` drop them.
|
||||
|
||||
### Decorators via snippets
|
||||
|
||||
Reuse `snippet/decorator/*` and `snippet/style/*` by string reference as in `24 - Pack Mods & Snippets.md`. Existing examples: `biomes/vanilla/old_growth_birch_forest.json` and the dimension's ore `chanceStyle` fields.
|
||||
|
||||
### Entities and spawners
|
||||
|
||||
The pack ships `entities/standard/**` and `spawners/**`. Ambient Iris spawning requires listing spawner keys on `entitySpawners` at dimension, region or biome scope. Marker-based spawning needs markers plus a `markers` array on an object placement. See `23 - Loot, Entities, Spawners, Markers.md`.
|
||||
|
||||
## Pushing changes into an existing world
|
||||
|
||||
World creation installs the pack copy once. Changing `packs/` does **not** update existing worlds.
|
||||
|
||||
### `/iris dev update-world` (Bukkit)
|
||||
|
||||
```
|
||||
/iris dev update-world world=<world> pack=my-overworld confirm=true
|
||||
```
|
||||
|
||||
Optional `fresh-download=true` re-downloads the pack first. Behavior (`CommandDeveloper.updateWorld` → `StudioSVC.replaceIntoWorld`):
|
||||
|
||||
1. Without `confirm=true` it prints the warning and does nothing.
|
||||
2. Optionally re-downloads the pack.
|
||||
3. Replaces `<world>/iris/pack/` with a fresh copy of the source pack.
|
||||
4. It is described as UNSAFE in the command itself. Already-generated chunks keep their old terrain; for most features only newly generated chunks use the new content. Back the world up first.
|
||||
|
||||
### Choosing between update-world and a new world
|
||||
|
||||
| Goal | Approach |
|
||||
|------|----------|
|
||||
| Live design iteration | Studio open on `packs/` |
|
||||
| Ship pack changes into an existing survival world | Back up, then `update-world … confirm=true` |
|
||||
| Guaranteed consistent terrain | New world from the updated pack |
|
||||
| Experimental or partial changes | Fork the pack with `studio create` |
|
||||
|
||||
Never use `update-world` for a change to `dimensionHeight`, `logicalHeight`, `environment`, or the dimension file name. Those are the world contract; the world will not load against a different one.
|
||||
|
||||
## Validation and packaging
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Validate | Bukkit `/iris pack validate pack=my-overworld`; modded `/iris pack validate my-overworld` |
|
||||
| Preview unused-resource cleanup | Bukkit `/iris pack cleanup my-overworld mode=preview`, then `mode=apply`; modded uses the same `preview`/`apply` literals |
|
||||
| Package for distribution | Bukkit `/iris studio package dimension=my-overworld`; modded `/iris studio package my-overworld` |
|
||||
| Version stamp | The dimension `version` field; the shipping pack uses large integers such as `4000` |
|
||||
|
||||
## Checklist before a production update
|
||||
|
||||
1. Verify in Studio, not by reading JSON.
|
||||
2. Run pack validate and fix every broken key.
|
||||
3. Back up the target world folder.
|
||||
4. Run `update-world` with `confirm=true`, plus `fresh-download=true` if the source should be re-pulled.
|
||||
5. Explore **new** chunks; do not expect existing terrain to change.
|
||||
6. Record operator-facing changes in the workspace changelog when releasing.
|
||||
|
||||
## Cross-links
|
||||
|
||||
- Minimal greenfield pack: `26 - Example - Minimal Dimension.md`
|
||||
- Dimension field reference: `11 - Dimensions.md`
|
||||
- Commands and permissions: `04 - Commands & Permissions.md`
|
||||
- Download, validate, package: `25 - Pack Management.md`
|
||||
@@ -1,166 +0,0 @@
|
||||
# 28 - Integrations
|
||||
|
||||
Iris hooks into a handful of Bukkit plugins: WorldEdit for selections, Multiverse-Core for world management, nine item/block/entity plugins so packs can place their content, MythicMobs for skill conditions, and PlaceholderAPI for scoreboard values. All of them are optional, and Iris checks whether the plugin is actually enabled before touching it — a soft-depend only fixes load order, it never guarantees anything is there. Tree felling is a separate feature that ships on both Bukkit and the mod loaders. See also `04 - Commands & Permissions.md`, `06 - Worlds & Lifecycle.md`, `09 - PlaceholderAPI.md`, `19 - Objects.md`, and `93 - API - Tree Feller.md`.
|
||||
|
||||
## Confirm an integration is actually live
|
||||
|
||||
Add integrations one at a time and prove each one before adding the next. Iris failing to link a plugin is usually silent by design, so absence of an error is not evidence of success.
|
||||
|
||||
1. Start from a server where Iris alone generates a fresh world cleanly.
|
||||
2. Install one integration and its own dependencies, then do a **full restart**. A plugin-manager reload does not reproduce real enable order and will give you a false result either way.
|
||||
3. Watch the startup log. Each external data provider logs `<Plugin> found, loading <Provider>...` followed by `Enabled ExternalDataProvider for <Plugin>.` The other links log nothing on success.
|
||||
4. Run the positive proof for that boundary from the table below, then the negative control. The negative control is the part people skip and the part that tells you whether Iris is really gating on the plugin or just happening to work.
|
||||
|
||||
| Boundary | Positive proof | Negative control |
|
||||
|---|---|---|
|
||||
| WorldEdit | Make a cuboid selection with the WorldEdit wand, run `/iris object we`, and save a disposable object | Clear the selection and run `/iris object we` again — Iris must say you have no WorldEdit selection in this world |
|
||||
| Multiverse-Core | Create a disposable Iris world and confirm Multiverse lists it with generator `Iris:<pack>` | On a separate disposable copy, restart without Multiverse installed. Iris world creation must still succeed |
|
||||
| Item/block/entity provider | Reference one exact namespaced key from a pack and generate a fresh chunk containing it | Reference a key that does not exist. Iris must log `No matching Provider found` or a missing-resource error and keep generating |
|
||||
| MythicMobs conditions | `irisbiome{b=<load key>}` returns true inside that biome | The same condition returns false in a vanilla world |
|
||||
| PlaceholderAPI | Run the parse sequence in `09 - PlaceholderAPI.md` | A player outside an Iris world gets `world.available` = `false` |
|
||||
| Tree feller | A sneaking survival player with the permission and an axe fells an Iris-generated tree | A tree the player grew from a sapling stays intact — only the broken log drops |
|
||||
|
||||
5. Restart and repeat the positive proof once. Some link failures only appear on the second boot, when caches are warm and enable order shifts.
|
||||
|
||||
When something misbehaves, collect both plugin versions and the enable order from the log before you start editing pack JSON. Most integration failures are version or ordering problems, not pack problems.
|
||||
|
||||
## Load order declarations
|
||||
|
||||
Iris ships both a legacy `plugin.yml` and a Paper `paper-plugin.yml`; Paper servers use the latter. The two declare the same relationships in different syntax.
|
||||
|
||||
| Plugin | Relation | Role in Iris |
|
||||
|---|---|---|
|
||||
| PlaceholderAPI | softdepend | The `%iris_...%` expansion |
|
||||
| CraftEngine | softdepend | External blocks and items |
|
||||
| Nexo | softdepend | External blocks and items |
|
||||
| ItemsAdder | softdepend | External blocks and items |
|
||||
| SCore | softdepend | Runtime dependency of the ExecutableItems ecosystem. Iris has no provider class for it |
|
||||
| ExecutableItems | softdepend | External items |
|
||||
| MythicLib | softdepend | Runtime dependency of the MMOItems ecosystem. Iris has no provider class for it |
|
||||
| MMOItems | softdepend | External blocks and items |
|
||||
| eco | softdepend | Runtime dependency of EcoItems. Iris has no provider class for it |
|
||||
| EcoItems | softdepend | External items |
|
||||
| MythicMobs | softdepend | External entities, plus the two skill conditions |
|
||||
| MythicCrucible | softdepend | External blocks and items |
|
||||
| KGenerators | softdepend | External blocks and items |
|
||||
| WorldEdit | softdepend | Selection reading for the object and wand workflows |
|
||||
| Multiverse-Core | `loadbefore` | Iris enables *before* Multiverse so Multiverse can see Iris as a registered generator plugin |
|
||||
|
||||
In `paper-plugin.yml` the softdepends appear as `load: BEFORE, required: false` and Multiverse-Core as `load: AFTER`. Every entry joins the classpath except ExecutableItems, which does not.
|
||||
|
||||
## WorldEdit
|
||||
|
||||
`WorldEditLink` reaches into WorldEdit entirely by reflection, so Iris compiles and runs without it on the classpath.
|
||||
|
||||
| Use | Behavior |
|
||||
|---|---|
|
||||
| Reading a selection | Returns an Iris `Cuboid` for the player's current WorldEdit selection **in the world they are standing in**. Returns `null` if WorldEdit is absent, the player has no session, or there is no selection in that world |
|
||||
| Accepting a selection | With `world.worldEditWandCUI` (default `true`), a WorldEdit selection satisfies Iris anywhere Iris asks for one, without holding the Iris wand |
|
||||
| `/iris object we` | Checks WorldEdit is enabled, reads your current selection, and puts a **new Iris object wand into your inventory** already carrying those two corners |
|
||||
| Limitation | `position2` will not operate on a WorldEdit-only selection. Run `/iris object we` first so a real Iris wand exists |
|
||||
|
||||
Two behaviors worth knowing. First, whether WorldEdit is present is cached the first time Iris asks, so a WorldEdit installed or enabled after that point is not picked up until restart. Second, if a reflective call throws, Iris logs `Could not get selection`, reports the error, and latches the cached answer to "no WorldEdit" for the rest of the process — a stack trace of that shape means the whole link is now off, not just that one call.
|
||||
|
||||
WorldEdit is not needed to import `.schem` files. Iris parses schematic NBT itself. See `19 - Objects.md`.
|
||||
|
||||
## Multiverse-Core
|
||||
|
||||
`MultiverseCoreLink` calls the Multiverse Core API directly, guarded by `isPluginEnabled("Multiverse-Core")`.
|
||||
|
||||
| Operation | Behavior |
|
||||
|---|---|
|
||||
| World create or update | If Multiverse does not know the world, imports it with generator `Iris:<pack>`, the Bukkit world's environment, and spawn-adjust off. Then sets `autoLoad = false`, forces the generator string if it drifted, and saves the worlds config |
|
||||
| World remove | Looks the world up, removes it through Multiverse, and saves the worlds config. Throws `IllegalStateException` naming Multiverse's refusal reason if the removal fails |
|
||||
| Multiverse absent or disabled | Create/update returns without doing anything; remove returns `false` |
|
||||
|
||||
`autoLoad = false` is deliberate: Iris owns the load lifecycle of its worlds, and letting Multiverse also load them at startup produces a double-load race.
|
||||
|
||||
Correcting a drifted generator string is done by reflection into Multiverse's world config, because the public API has no setter for it. If a Multiverse update breaks that, the symptom is a world that keeps its old generator string rather than a crash.
|
||||
|
||||
World creation, removal, and Studio open/close all route through this same link. See `06 - Worlds & Lifecycle.md`.
|
||||
|
||||
## External item, block, and entity plugins
|
||||
|
||||
`ExternalDataSVC` instantiates a provider for each supported plugin that is enabled, and also listens for `PluginEnableEvent` so a plugin that enables after Iris is still picked up. Packs then reference external content by namespaced id, and Iris routes each lookup to the first active provider that claims that namespace for that data type (`ITEM`, `BLOCK`, or `ENTITY`).
|
||||
|
||||
| Plugin id | Provider class | Claims | Types |
|
||||
|---|---|---|---|
|
||||
| CraftEngine | `CraftEngineDataProvider` | Any namespace, but only if CraftEngine actually has an item, block, or furniture with that exact key | ITEM, BLOCK |
|
||||
| Nexo | `NexoDataProvider` | Namespace `nexo` | ITEM, BLOCK |
|
||||
| ItemsAdder | `ItemAdderDataProvider` | The item and block namespaces ItemsAdder reports at init, tracked separately | ITEM, BLOCK |
|
||||
| ExecutableItems | `ExecutableItemsDataProvider` | Namespace `executable_items` | ITEM |
|
||||
| MMOItems | `MMOItemsDataProvider` | Blocks: namespace `mmoitems`. Items: any namespace containing one `_`, read as `<type>_<subtype>` | ITEM, BLOCK |
|
||||
| EcoItems | `EcoItemsDataProvider` | Namespace `ecoitems` | ITEM |
|
||||
| MythicMobs | `MythicMobsDataProvider` | Namespace `mythicmobs` | ENTITY |
|
||||
| MythicCrucible | `MythicCrucibleDataProvider` | Namespace `crucible` | ITEM, BLOCK |
|
||||
| KGenerators | `KGeneratorsDataProvider` | Namespace `kgenerators` | ITEM, BLOCK |
|
||||
|
||||
Block ids may carry a vanilla-style state suffix, `namespace:key[facing=north,waterlogged=true]`. A malformed state entry logs and resolves to nothing rather than throwing into generation.
|
||||
|
||||
Failures are contained. An unresolvable key logs `No matching Provider found for modded material "<key>"` or a missing-resource error naming the namespace and key, and the caller gets an empty result. Generation continues. A provider that throws during activation is logged and skipped; the rest still load.
|
||||
|
||||
Third parties can add their own with `ExternalDataSVC#registerProvider`. It throws `IllegalArgumentException` if the plugin id belongs to a built-in provider or one already registered, so you cannot silently displace an existing one. A provider that also implements `Listener` is registered as one automatically.
|
||||
|
||||
## MythicMobs skill conditions
|
||||
|
||||
When MythicMobs is active, its `MythicConditionLoadEvent` gives Iris two location conditions.
|
||||
|
||||
| Condition | Fields | What it checks |
|
||||
|---|---|---|
|
||||
| `irisbiome` | `biome` / `b` — comma-separated biome load keys. `surface` / `s` — boolean, default `false` | With `s=true`, the surface biome at the target's X/Z. With the default `s=false`, the biome at the target's actual Y, which includes cave and mantle biomes |
|
||||
| `irisregion` | `region` / `r` — comma-separated region load keys | The region load key at the target's X/Z |
|
||||
|
||||
Both compare against **load keys**, not display names, and both return `false` when the target world is not an Iris world or its engine is unavailable. That means a condition can quietly fail while a world is still booting, so avoid making a mob's only spawn gate an Iris condition during startup.
|
||||
|
||||
## PlaceholderAPI
|
||||
|
||||
Expansion id `iris`, soft-depended. Registration timing, all sixteen keys, and the pre-2.0 migration table are in `09 - PlaceholderAPI.md`.
|
||||
|
||||
## Tree feller
|
||||
|
||||
Breaking one log of an Iris-generated tree while sneaking with an axe removes the whole tree. This runs on Bukkit-family **and** on Fabric/Forge/NeoForge, from the same settings and the same traversal code — only the permission plumbing differs.
|
||||
|
||||
### Settings (`settings.json`)
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `treeFeller.enabled` | `false` | Turns the player-facing feature on. Does not affect other plugins driving the feller through the API |
|
||||
| `treeFeller.durabilityPreservationChance` | `0` | Percent chance that felling a log costs no axe durability. Clamped to `0..100` when read, so an out-of-range value in the file is harmless |
|
||||
|
||||
### Permission
|
||||
|
||||
| Platform | Node | Default |
|
||||
|---|---|---|
|
||||
| Bukkit-family | `iris.treefeller` | `op` |
|
||||
| Fabric | `irisworldgen:treefeller` | Permission level GAMEMASTERS (op level 2) |
|
||||
| Forge / NeoForge | `irisworldgen:treefeller` (Forge `PermissionNode`) | Permission level GAMEMASTERS (op level 2) |
|
||||
|
||||
### What has to be true to fell a tree
|
||||
|
||||
All of these, on every platform:
|
||||
|
||||
- `treeFeller.enabled` is `true`
|
||||
- The player has the platform's tree-feller permission
|
||||
- The player is in survival mode
|
||||
- The player is sneaking
|
||||
- The broken block is in the vanilla logs tag
|
||||
- The main-hand item is in the axes tag
|
||||
- The block carries Iris tree provenance in the mantle, and that provenance is not part of a structure
|
||||
|
||||
That last condition is what separates a generated tree from a player-planted one. Iris stamps trees it places and clears the stamp when a player places a block, so saplings grown by players and hand-built trunks are never felled.
|
||||
|
||||
On Bukkit, Iris listens for `BlockBreakEvent` at `EventPriority.HIGHEST` to register the request and finalizes at `MONITOR`, where it cancels the vanilla break, suppresses its drops and XP, and starts its own paced run instead.
|
||||
|
||||
On Bukkit, other plugins can drive a fell with `TreeFellerAccess.INTEGRATION_OVERRIDE` through `IrisTreeFellerService`. That bypasses the `enabled` switch and the permission check only — every provenance and block-state requirement still applies, and an override request supersedes a standalone one on the same event. That API is Bukkit-only. See `93 - API - Tree Feller.md`.
|
||||
|
||||
### Runtime notes for operators
|
||||
|
||||
- Discovery is a flood fill over matching mantle provenance markers, bounded at 131,072 members, 1,000,000 visited positions, and 256 blocks on any axis from the broken block. If a tree exceeds any bound, the run degrades to removing only the block the player broke — an unusually large custom tree quietly behaving like vanilla is this, not a bug.
|
||||
- Removal is paced across ticks and dispatched per region, so it is Folia-safe and does not stall the main thread on a large tree.
|
||||
- A run ends early if the player stops sneaking, changes hotbar slot, swaps hands, leaves survival or the world, breaks the axe, or swaps to a different axe item.
|
||||
- Logs consume axe durability, one point each, subject to the preservation chance and to unbreakable items. Leaves cost nothing and skip the durability path entirely.
|
||||
- A tree already being felled cannot be claimed twice. A second player breaking into the same tree has their break cancelled with no drops rather than starting a competing run.
|
||||
|
||||
## Platforms
|
||||
|
||||
WorldEdit, Multiverse-Core, the external data providers, the Mythic conditions, and PlaceholderAPI are Bukkit-family only; mod loaders do not use these plugin declarations. The tree feller is the exception — it runs on every platform. See `30 - Platform Differences.md`.
|
||||
@@ -1,159 +0,0 @@
|
||||
# 29 - Client HUD & Protocol
|
||||
|
||||
Installed on a Minecraft client, the Iris mod adds a native pregeneration HUD, a full-screen Vision map, a What overlay for the block under your cursor, Studio toasts, and Iris world types in singleplayer. It talks to Iris servers over one channel, `irisworldgen:main`, which works identically whether the server is a mod loader or a Bukkit-family plugin. Vanilla clients never see the channel and fall back to server-side progress reporting. See also `07 - Pregeneration.md`, `08 - Localization.md`, `10 - Studio & VSCode Schemas.md`, and `30 - Platform Differences.md`.
|
||||
|
||||
## Get the HUD working
|
||||
|
||||
There is no separate client download. The Fabric, Forge, and NeoForge mod jars each contain the client code, gated to the client distribution, so the same jar you run on a server is the one you install on a client. The Iris and Minecraft versions on both ends must match — a mismatch is the single most common reason the HUD never appears.
|
||||
|
||||
1. Install the Iris mod on your client and join an Iris server.
|
||||
2. Start a small pregen from the server, for example `/iris pregen start 512 center=0,0 gui=false`.
|
||||
3. Look at the top-left of your screen.
|
||||
|
||||
Success is a dark panel at 6,6 with a green title, a `done / total (percent%)` line, a green progress bar, and a rate-and-ETA line under it. If region deltas are arriving you also get a small region grid below the panel.
|
||||
|
||||
Then check the other two surfaces:
|
||||
|
||||
4. Press `M`. The Vision map should open full-screen. Drag to pan, scroll to zoom, Esc to close.
|
||||
5. Press `J` to toggle the What overlay, then look at a block. It should list the biome, the region, the cave biome if there is one, and the height.
|
||||
6. Walk into a non-Iris world. Vision and What stop reporting Iris data; the client clears its cached tiles and markers when the server tells it the dimension changed.
|
||||
7. Disconnect and reconnect, then repeat step 3. This proves the handshake actually reruns rather than the UI showing stale state.
|
||||
|
||||
If the panel never appears, work through this order: confirm the versions match on both sides, reconnect to force a fresh handshake, then read the server log. A protocol version mismatch and a rejected frame both leave a trace there.
|
||||
|
||||
One thing that is not evidence: a working boss bar on a modded server proves the pregen job is running and that the server can talk to your client's *vanilla* surface. It does not prove the Iris payload path works. In fact the boss bar appearing at all means the payload path did **not** come up for you — see below.
|
||||
|
||||
## What each combination gives you
|
||||
|
||||
| Server | Vanilla client | Client with the Iris mod |
|
||||
|---|---|---|
|
||||
| Modded Iris (Fabric/Forge/NeoForge) | Boss bar for whoever started the pregen | Native HUD, Vision, What, and toasts over custom payloads. No boss bar |
|
||||
| Bukkit-family Iris | Console output and `/iris pregen status`. No boss bar, no Iris client features | The same native HUD, Vision, What, and toasts, carried over plugin messaging on the same channel |
|
||||
| Non-Iris server | Nothing | The mod goes inert once the handshake times out |
|
||||
|
||||
In singleplayer, installed packs register world generator presets under the `irisworldgen` namespace and appear as selectable World Types in the create-world screen. The integrated server runs the same engine as a dedicated one.
|
||||
|
||||
## Keybinds
|
||||
|
||||
Category **Iris** (`key.categories.irisworldgen.iris`). All three are rebindable in Controls.
|
||||
|
||||
| Action | Default | 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` |
|
||||
|
||||
The HUD starts visible; the What overlay starts hidden. Both toggles are pure client state and are not remembered across restarts.
|
||||
|
||||
Pressing F1 hides the whole layered HUD, so the pregen panel and What overlay disappear. Toasts are pumped from a separate per-tick hook precisely so they still advance and expire while the GUI is hidden, instead of piling up until you press F1 again.
|
||||
|
||||
## Pregen HUD
|
||||
|
||||
The panel draws at 6,6 whenever a tracked job exists and has not expired.
|
||||
|
||||
| Element | Content |
|
||||
|---|---|
|
||||
| Title | The localized pregen header |
|
||||
| Stats | `done / total (percent%)`, thousands-separated, percent to one decimal |
|
||||
| Bar | Green while running, amber while paused, gray once stale |
|
||||
| Tail | Rate and ETA while running, `PAUSED` while paused, or "no updates for N s" once stale |
|
||||
| Region grid | Only when region deltas have arrived. Each cell is a region: gray pending, amber generating, green done |
|
||||
|
||||
Two client-side timers, both measured from the last progress frame received:
|
||||
|
||||
| Threshold | Value | Effect |
|
||||
|---|---|---|
|
||||
| Stale | 5 s | Every color mutes to gray and the tail switches to the stale label |
|
||||
| Expire | 30 s | The panel stops drawing entirely |
|
||||
|
||||
Because both timers key off frame arrival, a stale panel means the frames stopped, not that the job stopped. A paused job keeps sending progress frames and stays amber rather than going gray. A `PregenEnd` frame clears the job and the region grid immediately, with no wait for either timer.
|
||||
|
||||
## Boss bar fallback
|
||||
|
||||
On modded servers, `ModdedPregenBossBar` shows a boss bar to the player who started the pregen — green while running, yellow while paused, updated every 10 ticks, titled from `iris.runtime.pregen.bossbar.running` / `.paused`.
|
||||
|
||||
It is suppressed if that player already has a ready protocol session with `CAPABILITY_PREGEN`. That is the deliberate rule: you get the boss bar **or** the native HUD, never both. So if you installed the client mod and still see a boss bar, the handshake did not complete.
|
||||
|
||||
Bukkit-family servers have no equivalent boss bar. Players without the mod get `/iris pregen status`, the console, and the desktop pregen window; players with the mod get the same native HUD as on modded, delivered over plugin messaging.
|
||||
|
||||
## Vision map and What overlay
|
||||
|
||||
| Feature | Requires | Notes |
|
||||
|---|---|---|
|
||||
| Vision map (`M`) | Ready session, an Iris dimension, and `CAPABILITY_VISION` from the server | Full-screen. Drag to pan, scroll to zoom, Esc to close |
|
||||
| What overlay (`J`) | Ready session, an Iris dimension, and `CAPABILITY_CURSOR` | Reports biome, region, cave biome when present, and height for the cursor column |
|
||||
| Studio toasts | The client advertises `CAPABILITY_STUDIO` | Hotload and toast frames render when the server sends them |
|
||||
| Dimension status | Only a completed handshake | Carries pack key, dimension key, seed, and height bounds. A world that is not Iris-generated clears the client's tiles and markers |
|
||||
|
||||
Zoom level is part of the tile cache key, so changing zoom invalidates every cached tile and the map repaints progressively as new tiles arrive under the 8-per-second request budget. That is expected behavior, not a stall.
|
||||
|
||||
Vision tiles are split across frames: 24000 bytes of payload per chunk after a 25-byte header. One markers frame carries at most 256 markers.
|
||||
|
||||
## Protocol
|
||||
|
||||
| Constant | Value |
|
||||
|---|---|
|
||||
| Channel | `irisworldgen:main` |
|
||||
| Protocol version | `1` |
|
||||
| Transport (modded) | Custom payloads on the play channel |
|
||||
| Transport (Bukkit) | Incoming and outgoing plugin messaging on the same channel name |
|
||||
| Max frame | 24576 bytes |
|
||||
| Max inbound frames per client per second | 32 |
|
||||
| Max vision tile requests per second | 8 |
|
||||
| Max cursor lookups per second | 4 |
|
||||
| Max queryable block coordinate | ±29,999,999 |
|
||||
|
||||
Cursor lookups get their own budget rather than a slice of the frame budget because each one costs three engine column queries; a client that spent its whole frame allowance on them would be 32 column resolves per second per player.
|
||||
|
||||
Message types (`IrisProtocol.TYPE_*`):
|
||||
|
||||
| Id | Direction | Message | Purpose |
|
||||
|---|---|---|---|
|
||||
| 1 | C→S | `ClientHello` | Opens the session; carries the client's protocol version and capability mask |
|
||||
| 2 | S→C | `ServerHello` | Answers with the server's version, granted capabilities, brand string, and whether Iris is active |
|
||||
| 3 | S→C | `PregenProgress` | Chunks done, chunks total, rate, ETA, and run state for one job |
|
||||
| 4 | S→C | `PregenEnd` | Job finished or cancelled; clears the HUD and region grid |
|
||||
| 5 | S→C | `DimensionStatus` | Pack, dimension, seed, and height bounds for the world the player is in, or a flag saying it is not Iris |
|
||||
| 6 | C→S | `CursorInfoRequest` | Asks about one X/Z column, for the What overlay |
|
||||
| 7 | S→C | `CursorInfo` | Biome, region, cave biome, and height for that column |
|
||||
| 8 | C→S | `VisionTileRequest` | Asks for one map tile at a zoom level |
|
||||
| 9 | S→C | `VisionTile` | Tile image data, split into chunks when it exceeds one frame |
|
||||
| 10 | S→C | `VisionMarkers` | Labelled points overlaid on one map tile; each carries a block position, an icon kind, and a label |
|
||||
| 11 | S→C | `PregenRegionDelta` | Per-region state changes that drive the HUD's region grid |
|
||||
| 12 | S→C | `StudioHotload` | Pack reloaded: which files changed and whether it failed |
|
||||
| 13 | S→C | `Toast` | A one-off notification with a kind, title, and body |
|
||||
|
||||
Capability bits:
|
||||
|
||||
| Bit | Name | Gates |
|
||||
|---|---|---|
|
||||
| `1 << 0` | `CAPABILITY_PREGEN` | Progress frames, end frames, and region deltas |
|
||||
| `1 << 1` | `CAPABILITY_VISION` | Vision tile requests and marker frames |
|
||||
| `1 << 2` | `CAPABILITY_CURSOR` | Cursor column lookups |
|
||||
| `1 << 3` | `CAPABILITY_STUDIO` | Studio hotload notifications |
|
||||
|
||||
The client advertises all four. Bukkit and modded servers both grant all four. Negotiation is effectively an intersection, but it is enforced from both sides rather than stored as one mask: the server checks the client's advertised bits before serving a request, and the client checks the server's granted bits before offering a feature.
|
||||
|
||||
## Handshake
|
||||
|
||||
1. On joining a world the client clears its world-local state and sends `ClientHello` with protocol version `1` and its capability mask.
|
||||
2. It retries every 2 s, up to 5 attempts. After that the session goes `UNSUPPORTED` and the mod stays quiet — this is what happens on a non-Iris server.
|
||||
3. If the versions differ, the server still replies with a `ServerHello` naming its version so the client can land in `INCOMPATIBLE` and report the mismatch, rather than timing out and claiming the server does not run Iris. The session stays un-ready, and every subsequent frame from that client is dropped.
|
||||
4. On a version match the session becomes `READY`, and dimension status and feature frames follow.
|
||||
5. On disconnect the session resets and all world-local state — pregen job, tiles, markers, cursor, toasts, region grid — is cleared.
|
||||
|
||||
Session states are `IDLE`, `AWAITING_HELLO`, `READY`, `UNSUPPORTED`, and `INCOMPATIBLE`.
|
||||
|
||||
Server-side, frames arriving before a successful hello are counted and dropped, frames over the per-second budget are counted and dropped, oversized or malformed frames are rejected by the decoder, and a cursor query outside ±29,999,999 is rejected rather than clamped so a spoofed frame is recorded instead of served. Every one of these keeps a counter, which is why an unexplained absence of client features is worth checking against the server log before you suspect the client.
|
||||
|
||||
## Localization
|
||||
|
||||
The server's `general.language` drives the boss bar and every shared UI string, including the HUD stats and What overlay rows, which resolve through `ClientUiMessages` on whichever process renders them. Only the three keybind labels and their category come from the client jar's `assets/irisworldgen/lang/*.json`. See `08 - Localization.md`.
|
||||
|
||||
## Operator verification checklist
|
||||
|
||||
- Modded server plus Iris client: progress on the native HUD, and **no** boss bar for that player
|
||||
- Bukkit Iris plus Iris client: the same HUD, over plugin messaging
|
||||
- Vanilla client on either server: no protocol traffic, and on modded the boss bar as described
|
||||
- Non-Iris server plus Iris client: silent after roughly 10 s of hello retries
|
||||
- `H` toggles the HUD, `M` opens Vision where the capability is granted, `J` toggles What where the capability is granted
|
||||
@@ -1,183 +0,0 @@
|
||||
# 30 - Platform Differences
|
||||
|
||||
Iris runs the same generation core on Bukkit-family servers and on Fabric, Forge, and NeoForge. Terrain output is identical; everything around it differs, and this page is the reference matrix for those differences. Shared configuration is `settings.json`; mod loaders add `modded.json`. Related detail lives in `01 - Installation & Platforms.md`, `03 - Configuration.md`, and `04 - Commands & Permissions.md`.
|
||||
|
||||
## What actually differs
|
||||
|
||||
Five categories cover almost everything an operator runs into:
|
||||
|
||||
- **Command syntax.** Bukkit uses VolmLib Director, where optional arguments are `key=value` in any order. Mod loaders use Brigadier, where arguments are positional and options are bare literals. The same feature reads very differently on each.
|
||||
- **World lifecycle.** Bukkit creates named worlds and can stage an exact replacement of a configured vanilla slot on restart. Mod loaders register dimension ids and enable or disable them.
|
||||
- **Authoring tools that need Bukkit.** Anything built on NMS, WorldEdit, or inventory GUIs is Bukkit-only: Jigsaw Studio, structure import and capture, vanilla import, schematic conversion, and the Studio loot and entity GUIs. Packs authored there run fine everywhere.
|
||||
- **Permissions.** Bukkit gates the entire `/iris` tree behind one permission. Mod loaders gate mutating commands at gamemaster level but leave inspection open to any player.
|
||||
- **File locations.** Both platforms have an Iris data directory, but the modded side splits it: settings live under one root and packs under another.
|
||||
|
||||
Terrain, biomes, objects, jigsaw runtime, caves, and structures behave the same on all four. If generated terrain differs between platforms, that is a determinism defect, not a platform difference — see `32 - Determinism & Goldenhash.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` |
|
||||
| GoldenHash baselines | `plugins/Iris/golden/` | `<configDir>/irisworldgen/golden/` |
|
||||
| Studio pack exports | `plugins/Iris/packs/exports/` | `<configDir>/irisworldgen/exports/` |
|
||||
| Generated datapack | world `datapacks/` + Iris ingest | `<configDir>/irisworldgen/generated/datapack/`; dimension-type pack name `iris` under `data/irisworldgen/dimension_type/` |
|
||||
| Parity / developer dumps | under plugin data folder | `<configDir>/iris/parity/` |
|
||||
| Persistent dynamic-world registry | `plugins/Iris/worlds.json` | `<world-root>/iris/iris-dimensions.json` |
|
||||
|
||||
On mod loaders only `settings.json` and the parity dumps use the `iris/` root; every pack, config, and generated artifact uses `irisworldgen/`. Both roots sit under the loader config directory.
|
||||
|
||||
Hotload is polled on both platforms, at different rates:
|
||||
|
||||
| Watcher | Bukkit | Modded |
|
||||
|---------|--------|--------|
|
||||
| Pack / studio content | 1 s scan over the shared reactive folder | 250 ms scan, 1 s check latch, 2 s hold-off after recent generation |
|
||||
| `settings.json` | Reloaded through the same reactive path | Dedicated 3 s poll |
|
||||
|
||||
Both use the same invalidate, reload, and locale path once a change is detected.
|
||||
|
||||
## World model
|
||||
|
||||
| Concern | Bukkit | Modded |
|
||||
|---------|--------|--------|
|
||||
| Create | `/iris create` → managed world name, generator Iris, optional main-world; on Paper-family servers `overwrite=true` stages replacement of an existing exact Iris or vanilla slot for the next restart | `/iris create` or `/iris world enable` → dimension id plus pack injection |
|
||||
| Load / unload | `/iris load` (alias `import`), `/iris unload` | `/iris world disable` unloads; there is no separate load command |
|
||||
| Remove / delete | `/iris remove`, optionally deleting the folder | `/iris world delete` wipes chunk and mantle data |
|
||||
| Primary / main world | `main=true` for a new level root, or name the configured main world with `overwrite=true` for journaled in-place replacement | `modded.json` `primaryWorld` plus `routePlayersToPrimaryWorld`; `/iris world mainworld` (and `mainworld off`), `/iris world replace-overworld` |
|
||||
| Evacuate | `/iris evacuate <world>` — world argument required, player-only origin | `/iris evacuate [dimension]` — defaults to the sender's current level; destination is always the vanilla overworld, and evacuating the overworld itself is refused |
|
||||
| Studio world | Transient studio world via StudioSVC; `/iris jigsaw` can select the Jigsaw Studio generator for one activation | Studio dimension under `irisworldgen:studio_*`; no Jigsaw Studio authoring tree |
|
||||
| Folia | Regionized schedulers; pregen `runtimeSchedulerMode` always resolves to `FOLIA` on a regionized runtime | Not applicable |
|
||||
|
||||
Startup installs the IrisDimensions Overworld and Underworld beta releases into `packs/overworld` and `packs/underworld` when missing. Paper bootstrap publishes both in one rollback scope before compiling the aggregate datapack; legacy Bukkit and modded startup use the same managed release sources.
|
||||
|
||||
Modded startup quarantines a corrupt persistent-dimension registry as `iris-dimensions.json.broken-<timestamp>` and continues without those dynamic worlds. Recovery: `06 - Worlds & Lifecycle.md`.
|
||||
|
||||
Exact vanilla-slot replacement requires a full Paper-family plugin bootstrap. Without it the staging call fails outright, which is why the feature is Paper/Purpur/Leaf/Folia only.
|
||||
|
||||
## Commands and permissions
|
||||
|
||||
| Concern | Bukkit | Modded |
|
||||
|---------|--------|--------|
|
||||
| Parser | VolmLib Director; `key=value` optionals in any order | Brigadier; positional arguments and bare flag literals |
|
||||
| Root aliases | `iris`, `ir`, `irs` | `iris`, with `ir` and `irs` registered as redirects |
|
||||
| Staff gate | `iris.all` (declared in `plugin.yml` and `paper-plugin.yml`, default `op`) — required for every `/iris` subcommand | `LEVEL_GAMEMASTERS` for anything that mutates, downloads, opens Studio, or starts a pregen |
|
||||
| Open to any player | Nothing | `LEVEL_ALL`: `help`, `version`, `info`, `worlds`, `height`, `metrics` (alias `measure`), and the whole `what` subtree |
|
||||
| Deliberately gated reads | — | `seed` and `accesslist` stay at gamemaster level even though `worlds` shows similar output without the seed field |
|
||||
| Tree feller | `iris.treefeller` (`plugin.yml` and `paper-plugin.yml`, default `op`) | Fabric `irisworldgen:treefeller`; Forge and NeoForge PermissionAPI node `irisworldgen.treefeller`, defaulting to gamemaster level |
|
||||
| Help | Director mini-menu | `ModdedCommandHelp` sections with 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 |
|
||||
| Saved planar/spatial Iris jigsaw runtime | yes | yes | yes | yes |
|
||||
| Jigsaw Studio (`/iris jigsaw` authoring tree) | yes | not registered | not registered | not registered |
|
||||
| Pack validate / cleanup / restore / status | yes | yes | yes | yes |
|
||||
| Pack download (`/iris download`, root-level on both) | yes | yes | yes | yes |
|
||||
| Exact restart replacement of configured Overworld/Nether/End slots | Paper/Purpur/Leaf/Folia | no | no | no |
|
||||
| Pregen | yes (Paper-like / Folia modes) | yes (`moddedPregenInFlight`) | yes | yes |
|
||||
| Studio open / close / vscode / package | yes | yes | yes | yes |
|
||||
| Studio importvanilla | yes | message: run on Bukkit | same | same |
|
||||
| Studio loot GUI / entity spawn / profile / objects report | yes | message only | message only | message only |
|
||||
| Object wand / paste / save / undo | yes | yes | yes | yes |
|
||||
| Object contract / shift selection | yes | yes | yes | yes |
|
||||
| Object expand selection | no | yes | yes | yes |
|
||||
| Object WorldEdit import (`we`) | yes (WorldEdit soft depend) | message only | message only | message only |
|
||||
| Object studio world | yes | message only | message only | message only |
|
||||
| Schematic convert (`.schem` → `.iob`) | yes | message only | message only | message only |
|
||||
| Structure import / capture | yes (v26 NMS binding) | message only | message only | message only |
|
||||
| Structure list / info / place / verify | yes | yes | yes | yes |
|
||||
| Datapack Modrinth ingest / list / remove | yes | message only | message only | message only |
|
||||
| Dimension-type datapack install / status | not applicable | yes | yes | yes |
|
||||
| PlaceholderAPI | soft depend | no | no | no |
|
||||
| Multiverse-Core | soft depend / loadbefore | no | no | no |
|
||||
| Item plugins (ItemsAdder, Mythic, and similar) | 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 + loader permission | same | same |
|
||||
| Auto Spigot/Paper timeout and watchdog config | yes | no | no | no |
|
||||
| Custom biome restart prompts | yes (`iris.all` / op) | different datapack flow | same | same |
|
||||
|
||||
"Message only" means the command exists and prints an explanation of where to run it instead — it is not a silent failure.
|
||||
|
||||
Jigsaw pack resources are shared runtime data; only the in-game authoring surface is Bukkit-only. Bukkit exposes one global Studio project and world and one owning Jigsaw session: non-owner block, inventory, interaction, and mutating-command changes are cancelled across that Studio world, while autosave and graph-operation barriers serialize the owner's changes. On Folia a save schedules every intersecting chunk snapshot on its owning region and writes only after the complete capture validates. These protections have automated coverage but still need the live multi-region runbook in `31 - Operator Runbooks.md`. A strict `VANILLA_PORTABLE` export targets unmodded Minecraft 26.2 and is a separate compatibility gate.
|
||||
|
||||
## Platform-sensitive settings
|
||||
|
||||
| Setting | Where it matters |
|
||||
|---------|------------------|
|
||||
| `pregen.runtimeSchedulerMode` | Bukkit only; resolved from Bukkit/Folia detection, and a regionized runtime always resolves to Folia |
|
||||
| `pregen.paperLikeBackendMode` | Bukkit only; ticket versus service chunk acquisition |
|
||||
| `pregen.moddedPregenInFlight` | Mod loaders only; concurrent pregen chunk budget |
|
||||
| `autoConfiguration.*` | Bukkit only; Spigot keep-alive, Paper watchdog, custom-biome restart |
|
||||
| `world.worldEditWandCUI` | Bukkit only; requires WorldEdit |
|
||||
| `general.autoIngestDatapacks` / `general.autoImportDatapackStructures` | Bukkit datapack ingest pipeline is the primary consumer |
|
||||
| `gui.useServerLaunchedGuis` | Both, but the host implementation differs (`BukkitGuiHost` versus `ModdedGuiHost`) |
|
||||
|
||||
`modded.json` keys exist only on mod loaders.
|
||||
|
||||
## Integrations
|
||||
|
||||
| Integration | Bukkit | Modded |
|
||||
|-------------|--------|--------|
|
||||
| WorldEdit | soft depend; object `we` import | not wired |
|
||||
| Multiverse-Core | load order / link | not used |
|
||||
| PlaceholderAPI | `%iris_…%` | no |
|
||||
| MythicMobs and item plugins | paper-plugin optional deps | not the Bukkit pipeline |
|
||||
| Tree feller | plugin permission | loader permission node |
|
||||
|
||||
See `28 - Integrations.md` and `09 - PlaceholderAPI.md`.
|
||||
|
||||
## NMS and version binding
|
||||
|
||||
- The Bukkit plugin binds to a specific Paper/CraftBukkit revision (the in-tree v26 NMS module).
|
||||
- Structure import and capture, and the vanilla import studio path, require that binding. This is why they cannot be ported to mod loaders as-is.
|
||||
- Mod adapters use Minecraft mappings for the same game version line, without the Bukkit plugin APIs.
|
||||
|
||||
## Determinism and parity
|
||||
|
||||
GoldenHash exists on both surfaces; only the command placement differs (Bukkit under `developer`, modded at the root). Use disposable worlds. Bukkit exposes mantle-reset and deep-dump options that modded does not, and modded always resets mantle. Cross-platform comparisons always emit a Minecraft-version warning because the two platforms report the version string differently. Full procedure: `32 - Determinism & Goldenhash.md`.
|
||||
|
||||
## Moving a pack between platform families
|
||||
|
||||
1. Freeze the pack bytes and seed. Validate and package on the source platform.
|
||||
2. Finish anything Bukkit-only first: structure and vanilla imports, schematic conversion, WorldEdit imports, and Jigsaw Studio work. Complete the atomic saves before copying.
|
||||
3. Copy only `packs/<key>/` into the destination packs root. Never copy a Bukkit world folder into a modded world or the reverse.
|
||||
4. Restart so destination registries and forced datapacks are built before any world is created.
|
||||
5. Align the `settings.json` keys that affect generation (`generator`, `performance`, `treeFeller`). Bukkit-only `autoConfiguration` keys can be ignored.
|
||||
6. On mod loaders, set `modded.json` `primaryWorld` if you need overworld replacement.
|
||||
7. Re-run `/iris pack validate`, then `/iris datapack status` on modded or the ingest flow on Bukkit.
|
||||
8. Create a disposable world with the same seed and run the same small GoldenHash inputs plus the platform's fresh-install runbook.
|
||||
|
||||
The move passes when validation, world creation, restart, and the deterministic comparison all pass. Matching screenshots are useful context, but the hash comparison is what counts.
|
||||
|
||||
## Related
|
||||
|
||||
- `01 - Installation & Platforms.md`
|
||||
- `03 - Configuration.md`
|
||||
- `04 - Commands & Permissions.md`
|
||||
- `06 - Worlds & Lifecycle.md`
|
||||
- `07 - Pregeneration.md`
|
||||
- `21 - Jigsaw Structures.md`
|
||||
- `22 - Native Structures & Datapacks.md`
|
||||
- `28 - Integrations.md`
|
||||
- `29 - Client HUD & Protocol.md`
|
||||
- `31 - Operator Runbooks.md`
|
||||
- `32 - Determinism & Goldenhash.md`
|
||||
- `94 - API - Modded.md`
|
||||
@@ -1,447 +0,0 @@
|
||||
# 31 - Operator Runbooks
|
||||
|
||||
Manual verification sequences to run after an install, an upgrade, a pack change, or a release-candidate build. Each runbook is a numbered checklist with the exact command, what you should see, and what to do when you do not see it. Command trees and permissions are in `04 - Commands & Permissions.md`, pregen options in `07 - Pregeneration.md`, platform capability differences in `30 - Platform Differences.md`.
|
||||
|
||||
## How to use these
|
||||
|
||||
Use a purpose-named disposable world. Before you start, write down the Iris artifact, platform build, Java version, pack hash, and seed — a result without those is not reproducible later.
|
||||
|
||||
Run only the sections a local change touched. Run the full platform set for a release candidate.
|
||||
|
||||
A passing Gradle test, a clean boot, and a player actually walking through generated chunks tell you different things — don't let one stand in for another. When you're done, delete the worlds and instances you created for the run.
|
||||
|
||||
## Fixed inputs for parity work
|
||||
|
||||
Use the same values whenever you compare platforms or runs:
|
||||
|
||||
| Input | Value | Why |
|
||||
|-------|-------|-----|
|
||||
| Pack | Shipping default `overworld`, or a frozen copy | Must be byte-identical on every platform under test |
|
||||
| Seed | `1337` | World seed on Bukkit, Iris engine seed on modded |
|
||||
| GoldenHash radius | `22` chunks (`8` for a quick check) | Radius 22 covers `(2×22+1)² = 2,025` chunks |
|
||||
| GoldenHash threads | `1` strict, `8` for the multi-thread run | `threads=1` is what catches order dependence |
|
||||
| Pregen radius | `352` blocks centered at `0,0` | Pregen radius is in **blocks**; 352 blocks is 22 chunks, giving the same 2,025-chunk square |
|
||||
|
||||
GoldenHash file layout and interpretation: `32 - Determinism & Goldenhash.md`.
|
||||
|
||||
## A. Fresh install and first world (Bukkit-family)
|
||||
|
||||
1. Drop the CraftBukkit-family jar into `plugins/` on a Java 25 server (Paper, Purpur, Folia, Spigot, Leaf, or Canvas as advertised). See `01 - Installation & Platforms.md`.
|
||||
2. Start the server once.
|
||||
|
||||
Expect: Iris enables; the managed `overworld` and `underworld` beta packs download when absent; `settings.json` appears in the Iris data directory.
|
||||
|
||||
If Iris does not enable, check Java version and platform artifact before anything else. If the downloads fail, the server has no outbound access — install the packs manually (`25 - Pack Management.md`).
|
||||
|
||||
3. Create a world with a fixed seed and go to it:
|
||||
|
||||
```
|
||||
/iris create test-ow type=overworld seed=1337
|
||||
/iris tp test-ow
|
||||
```
|
||||
|
||||
Expect: the world is created and loaded as an Iris world, and you land in generated terrain.
|
||||
|
||||
4. Walk or fly a few hundred blocks.
|
||||
|
||||
Expect: non-empty terrain, surface biomes, no repeating stack traces on first chunks.
|
||||
|
||||
If chunks come back empty or void, stop and check pack validation (section C) before anything else.
|
||||
|
||||
5. **Expected:** the world is an Iris world, chunks generate, and the console shows no fatal engine init failure.
|
||||
|
||||
## A.1 Exact vanilla-slot replacement (Paper-family)
|
||||
|
||||
Prerequisites: a disposable server whose configured level name is `world`, a valid `NETHER` Iris pack, and a generated vanilla Nether containing a unique marker chunk. Before staging, hash the existing Nether `region`, `entities`, and `poi` files and keep copies of `data/paper/metadata.dat`, `data/paper/level_overrides.dat`, and `data/minecraft/world_gen_settings.dat`.
|
||||
|
||||
1. Stage the replacement:
|
||||
|
||||
```
|
||||
/iris create world_nether type=<nether-pack> seed=1337 overwrite=true
|
||||
```
|
||||
|
||||
Expect: the command reports the replacement is staged. The loaded Nether and its files are unchanged, `bukkit.yml` now names `Iris:<dimension>`, and exactly one pending replacement journal plus one sibling stage exists.
|
||||
|
||||
2. Optionally stage the configured main name with a `NORMAL` pack and the End alias with a `THE_END` pack.
|
||||
|
||||
Expect: each distinct slot gets its own transaction, and no live dimension folder is moved.
|
||||
|
||||
3. Restart normally.
|
||||
|
||||
Expect: Iris publishes before aggregate-datapack compilation and before Bukkit world loading. `minecraft:the_nether` loads with the selected Iris dimension, the prior Paper per-world metadata files, and the authoritative shared level seed. Its frozen `iris/pack` exists, no old `region`, `entities`, or `poi` file was merged into the target, and the marker chunk is gone.
|
||||
|
||||
4. Watch the `WorldLoad` boundary.
|
||||
|
||||
Expect: the journal advances to committed cleanup only after identity, environment, seed, dimension, and pack-fingerprint verification all pass. Cleanup then removes the retained sibling backup and journal asynchronously, without stalling the world thread.
|
||||
|
||||
5. Restart again and generate fresh Nether chunks.
|
||||
|
||||
Expect: the vanilla identity and the Iris generator both persist, ordinary Nether portals still target `minecraft:the_nether`, and no pending stage, backup, or journal reappears.
|
||||
|
||||
6. Repeat once with a deliberately corrupted staged pack or a conflicting `bukkit.yml` value before restart.
|
||||
|
||||
Expect: early Paper bootstrap aborts before registry and world loading, preserves the recoverable artifacts, and never guesses a target. For a failure after publication, Iris journals a rollback, requests the controlled restart, and restores the retained original directory and prior configuration before datapack compilation or world loading.
|
||||
|
||||
If Iris silently proceeds past a corrupted stage, that is a blocking defect — capture the journal and stage directories before touching anything.
|
||||
|
||||
## B. Fresh install and first world (Fabric / Forge / NeoForge)
|
||||
|
||||
1. Install the matching mod jar into `mods/`. Fabric needs Loader at or above the declared floor; Forge and NeoForge need theirs. See `01 - Installation & Platforms.md` and `30 - Platform Differences.md`.
|
||||
2. Start the dedicated server, or singleplayer if you are also testing the client mod.
|
||||
|
||||
Expect: Iris boots, both managed beta packs install, and datapack and biome registration complete.
|
||||
|
||||
3. Create a world (arguments are positional here, not keyed):
|
||||
|
||||
```
|
||||
/iris create test-ow overworld 1337
|
||||
```
|
||||
|
||||
4. Enter the dimension.
|
||||
|
||||
Expect: non-empty generation, and custom-biome registration where the pack defines custom biomes.
|
||||
|
||||
5. **Expected:** same generation health as section A. Capability gaps are acceptable only where `30 - Platform Differences.md` documents them.
|
||||
|
||||
## C. Pack validation
|
||||
|
||||
1. Validate everything installed. On Bukkit the pack argument is required, so pass it with an empty value to cover every pack; on modded, omitting the pack validates all of them:
|
||||
|
||||
```
|
||||
/iris pack validate pack= (Bukkit)
|
||||
/iris pack validate (modded)
|
||||
```
|
||||
|
||||
Single pack: `/iris pack validate pack=<pack>` on Bukkit, `/iris pack validate <pack>` on modded.
|
||||
|
||||
2. Read the output and separate blocking errors from warnings.
|
||||
|
||||
Expect: zero blocking errors for a pack you intend to ship. Warnings are advisory.
|
||||
|
||||
If there are blocking errors, fix them before treating the pack as production-ready — create and load will refuse the pack later anyway.
|
||||
|
||||
3. Replay the startup result:
|
||||
|
||||
```
|
||||
/iris pack status
|
||||
```
|
||||
|
||||
Expect: the result published at startup, including a persisted result reused for unchanged content.
|
||||
|
||||
4. Restart without changing packs or registry context.
|
||||
|
||||
Expect: the console logs `External datapacks match the persisted startup validation` and does **not** log another external-datapack `Validating` or `Ingesting` pass. Pack validation reuses its persisted result instead of re-parsing, player admission opens only after both phases are ready, and the target stays loadable.
|
||||
|
||||
5. Change one byte in a pack and restart.
|
||||
|
||||
Expect: the content fingerprint invalidates the reuse and validation runs in full again.
|
||||
|
||||
Restore the pack before continuing. Cleanup and restore flows are separate and opt-in (`25 - Pack Management.md`).
|
||||
|
||||
## D. Bukkit datapack dimension scope
|
||||
|
||||
Prerequisites: a disposable server with one managed datapack source, a vanilla world, one Iris dimension that declares that source, and one Iris dimension that does not. Install or ingest the datapack, restart so its registries are live, then create both Iris worlds so scope is applied before their spawn chunks load.
|
||||
|
||||
1. In each of the three worlds, locate a managed structure. Get a valid key from `/iris structure list <declaring-dimension>`:
|
||||
|
||||
```
|
||||
/locate structure <managed-structure-key>
|
||||
```
|
||||
|
||||
2. Generate new chunks in all three worlds. Checking existing chunks proves nothing — a scope change does not rewrite them.
|
||||
|
||||
3. **Expected:** locate and natural generation both keep the managed structure in the declaring Iris world, and neither the vanilla world nor the non-declaring Iris world locates or generates it.
|
||||
|
||||
4. Restart with the datapack still installed and repeat locate plus new-chunk generation.
|
||||
|
||||
Expect: identical per-world results and no ownership or structure-state failure during world initialization.
|
||||
|
||||
5. Break one required dimension or native-structure reference, restart, and try to create or load that pack.
|
||||
|
||||
Expect: validation blocks before admission completes, create and load report the failure before datapack preparation, and no dimension folder, pack snapshot, `bukkit.yml` registration, registry entry, or loaded world is left behind.
|
||||
|
||||
Restore and revalidate the pack before running the next section.
|
||||
|
||||
## E. Pregeneration control
|
||||
|
||||
Pregen radius is always in **blocks**. Use a disposable world.
|
||||
|
||||
**Bukkit** (radius positional, everything else keyed; `gui` defaults to `true`):
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=test-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 (Paper-compatible servers only):
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=test-ow center=0,0 gui=false serial=true
|
||||
```
|
||||
|
||||
**Modded** (positional radius, then optional dimension, then literal flags in any order — `at <x> <z>`, `gui`, `sync`, `nocache`):
|
||||
|
||||
```
|
||||
/iris pregen start 352 irisworldgen:test-ow at 0 0 sync
|
||||
/iris pregen status
|
||||
```
|
||||
|
||||
Expect, in order:
|
||||
|
||||
1. Start reports the correct world, center, and size.
|
||||
2. `status` shows generated/total, percent, chunks per second, ETA, elapsed, method, and a failed count when any chunk failed.
|
||||
3. The first `pause` freezes progress; the second resumes it (`resume` is an alias for the same toggle).
|
||||
4. `stop` cancels and finishes active work without claiming completion when chunks remain.
|
||||
5. For a release candidate, also run a full serial or sync 2,025-chunk run (radius 352 at 0,0) and confirm zero failed chunks.
|
||||
|
||||
If `serial=true` is rejected, the server is not Paper-compatible for strict serial pregen — that is expected, not a bug. If chunks fail, capture the console before retrying; failures are the finding, not the retry.
|
||||
|
||||
Client HUD: with the Iris client mod, pregen progress arrives on channel `irisworldgen:main`. Vanilla clients get the boss bar and console only (`29 - Client HUD & Protocol.md`).
|
||||
|
||||
## F. GoldenHash determinism
|
||||
|
||||
Run on a **disposable** world. The scan generates into buffers and never writes world blocks, but it deletes the world's mantle by default.
|
||||
|
||||
**Bukkit** (always `AUTO` — captures when the file is missing, verifies when it exists):
|
||||
|
||||
```
|
||||
/iris developer goldenhash world=test-ow radius=22 threads=1
|
||||
```
|
||||
|
||||
Optional: `center-x=0 center-z=0 reset-mantle=true deep=false`. Defaults are radius `8`, threads `8`, center `0,0`, reset-mantle `true`, deep `false`.
|
||||
|
||||
**Modded** (center fixed at chunk 0,0, mantle always reset):
|
||||
|
||||
```
|
||||
/iris goldenhash 22 1 capture
|
||||
/iris goldenhash 22 1 verify
|
||||
```
|
||||
|
||||
Alias `/iris gold …`. With no arguments: radius `8`, threads `8`, mode `AUTO`.
|
||||
|
||||
Expect:
|
||||
|
||||
1. The capture run writes a `.hashes` file in the platform golden directory and prints its path plus a short combined hash.
|
||||
2. A second run with the same pack, seed, radius, and center reports **MATCH** with the same hash.
|
||||
3. The same pack, seed, radius, and center produce the same combined hash on Bukkit, Fabric, Forge, and NeoForge for identical artifacts and pack bytes.
|
||||
|
||||
A Minecraft-version warning on cross-platform comparisons is expected and harmless — Bukkit and mod loaders report the version string differently. A seed or dimension mismatch is a hard failure and stops the comparison.
|
||||
|
||||
On MISMATCH, read the `.new` file and the first `.diag-*` file before changing anything. Triage table: `32 - Determinism & Goldenhash.md`.
|
||||
|
||||
## G. Restart and existing worlds
|
||||
|
||||
1. After some pregen or free exploration, stop the server cleanly.
|
||||
2. Start again without deleting world data.
|
||||
3. Load the same Iris world and generate new chunks outside the pregenerated area.
|
||||
4. **Expected:** the world loads, new chunks generate, no blank-chunk regression appears at the restart boundary, and a resumed pregen job behaves as documented (`07 - Pregeneration.md`).
|
||||
|
||||
## H. Studio (authoring path)
|
||||
|
||||
### General pack Studio
|
||||
|
||||
```
|
||||
/iris studio open overworld seed=1337
|
||||
```
|
||||
|
||||
While moving through fresh chunks so Moonrise has active generation stages, edit a pack file on disk — or through the VSCode workspace opened by `/iris studio vscode dimension=overworld` on Bukkit. Then close while fresh chunks are still queued:
|
||||
|
||||
```
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
Expect: the studio world opens; hotload applies without a server restart; already-admitted stages finish before the transition and later stages resume after it; close discards the transient studio world cleanly.
|
||||
|
||||
**Expected:** no generation-session rejection, no partial chunk-stage failure, no chunk-system crash. A failed hotload must fail closed without poisoning the live engine for non-studio worlds. Studio details: `10 - Studio & VSCode Schemas.md`.
|
||||
|
||||
### Jigsaw Studio: planar authoring and atomicity (Bukkit)
|
||||
|
||||
Use a disposable pack and structure key, and the owning builder account. Bukkit has one global Studio project and world and one owning Jigsaw session. Non-owner block edits and recognized mutating commands must be denied throughout this world. This command tree is not registered on Fabric, Forge, or NeoForge.
|
||||
|
||||
1. Create a project with no optional arguments so the defaults are exercised:
|
||||
|
||||
```text
|
||||
/iris jigsaw create overworld test/jigsaw
|
||||
/iris jigsaw status
|
||||
```
|
||||
|
||||
Expected: the add-only transaction owns one structure, three pools, six pieces, six objects, and one manifest before Studio opens. On Paper, native-structure scope must report success and reject any active-generator ownership or asynchronous game-rule event error. The player enters creative above Blank. `status` reports `PLANAR_JIGSAW`, `IRIS_EXTENDED`, six workcells, 15×15×15 for the selected workcell, six variants, no pending autosave, and the seed-`1337` evaluation. The key tab-completes for `open`, `edit`, and `reopen`. The GUI and owned resources show one loaded variant per archetype, theme `variant-1`, terminal End, and mandatory caps off.
|
||||
|
||||
2. Inspect the Blank, End Cap, Hallway, L Junction, T Junction, and Cross Junction layout. Floors are light-gray wool, topology paths are red wool, and canonical endpoints are sea lanterns. There are no orientation, permutation, piece, or derived-rotation cells. Toggle player-local particles:
|
||||
|
||||
```text
|
||||
/iris jigsaw goto workcell/blank
|
||||
/iris jigsaw goto workcell/straight
|
||||
/iris jigsaw goto workcell/cross
|
||||
/iris jigsaw particles false
|
||||
/iris jigsaw particles true
|
||||
```
|
||||
|
||||
Expected: every cell has one physical white-concrete edge cage, no workcell-bound display entity exists, and focused plus nearby particle trails outline the editable bounds inside those cages. Focused connectors draw 1.75-block direction lines while particles are on. The Iris scoreboard replaces the general Studio context with Structure, Workcell, Variant, State, and `Triple-sneak for controls`, with no orientation or mask fields. All six untouched cells report **Autosaved** initially. Enter End Cap, triple-sneak, and confirm the menu selects End Cap rather than the previously selected cell.
|
||||
|
||||
3. Open the same six-row controls three ways: right-click the protected chest, run `/iris jigsaw menu`, and sneak three times within 1.5 seconds.
|
||||
|
||||
Expected: each path opens without an `InventoryView` linkage error on the target Paper-family runtime. Select Hallway and click **New Blank Variant**; wait for its atomic graph result and load, then reopen the controls. Rename the loaded variant and the Hallway workcell through their anvil inputs and confirm the labels round-trip while the piece key, `straight` stable ID, and solver role stay unchanged. Load End Cap and use **Duplicate This Cell's Variant**, then load Cross Junction and duplicate it too.
|
||||
|
||||
Expected: the new key follows `test/jigsaw/variants/straight/variant-<n>` and loads into Hallway with the source piece's complete metadata and exact pool entries but an empty same-sized object. At the default 15×15×15 its two real markers sit at `(7,7,0)` and `(7,7,14)`, face north and south with top `UP_POSITIVE_Y`, show pool `iris:test/jigsaw/pieces`, use name and target `iris:planar`, `ALIGNED`, `minecraft:structure_void`, and signed priorities `0`. Mojang's UI is usable after hydration. Break one marker and click **Reset Connector Blocks** before autosave: both saved markers return while another edited block stays as you left it. Each duplicate copies the active object's bytes, display label, and complete piece metadata. The End Cap duplicate has exact matching entries in both `test/jigsaw/pieces` and `test/jigsaw/caps`; the Cross Junction duplicate has them in both `test/jigsaw/start` and `test/jigsaw/pieces`. An empty or unassigned workcell refuses both GUI actions and points at `/iris jigsaw piece create <poolKey> <pieceKey>` instead of picking a fallback pool.
|
||||
|
||||
4. Change one permanent block, one marker field, and one chest inventory inside Hallway. Keep the permanent block and the chest inside the later 16×3×3 target, for example Y/Z offsets `1,1`. After changing a marker field in Mojang's UI, immediately run `/iris jigsaw status`; change it again and immediately run `/iris jigsaw close`. Trigger an internal inventory transfer or hopper pickup, and at least one furnace, brewing-stand, dispenser, or crafter update inside the workcell. Do not flush autosave. Wait at least 40 ticks after the last update, then:
|
||||
|
||||
```text
|
||||
/iris jigsaw status
|
||||
```
|
||||
|
||||
Expected: the status command and the close attempt each request a final owning-region marker snapshot, and close waits behind marker finalization and autosave rather than losing the last UI change. State moves dirty → saving → clean on its own; inventory and machine changes also mark it dirty; one complete multi-resource commit occurs; one owner-local bell sounds; no partial resource appears.
|
||||
|
||||
Then make six distinct saved block edits and click **Undo Last Autosave** five times. Expected: each prior block state and manifest hash returns in reverse order, the sixth-oldest state is no longer available, one `.iris/jigsaw-history/key-<sha256>.json` file held the stack, and no transaction debris remains.
|
||||
|
||||
Then make another edit while capture is pending and immediately click **Duplicate This Cell's Variant**. Expected: Iris expedites the autosave and performs that one duplicate exactly once, with no wait-and-retry instruction. Repeat with dirty edits in several enabled cells and **Duplicate All Enabled Cells as Family**. Invoke **Flush Autosave Now** while capture cannot start: the same ticket stays pending, retries, and eventually becomes clean. Close, reopen, load the variant, and confirm blocks, marker NBT, inventory, explicit-air final state where used, and `structure_void` absence all round-trip. **Flush Autosave Now** and `/iris jigsaw save` are conveniences, not requirements.
|
||||
|
||||
On Paper, repeat one dirty edit immediately before plugin disable and confirm the synchronous final drain persists it. On Folia, confirm an enabled-world unload or unregister stays deferred and retries until autosave finishes. Record the forced-disable boundary separately: once Folia has disabled the plugin it rejects new region tasks, so a new final cross-region capture cannot be guaranteed. Close Studio, or wait for `status` to report no pending autosave, before any reload or shutdown.
|
||||
|
||||
5. Stage Hallway's capacity as 16×3×3 from **Workcell Settings**, confirm the menu stays open during every axis click, then click **Apply Cell Size** once. Also test the command form:
|
||||
|
||||
```text
|
||||
/iris jigsaw bounds 16 3 3
|
||||
```
|
||||
|
||||
Expected: only structure capacity metadata changes, one live relayout runs after Apply, and every Hallway variant keeps its object bytes and exact dimensions. A capacity shrink below any assigned variant is rejected atomically.
|
||||
|
||||
Then resize one loaded Hallway variant to 16×3×3 from **Variant Size** or `/iris jigsaw variant resize 16 3 3`. Expected: only that object changes and it reloads in place, its canonical connector payloads and sockets move to `(8,1,0)` and `(8,1,2)`, and sibling Hallway variants keep their prior dimensions and bytes. Resize a second Hallway variant to 3×3×3 (raise capacity first if required) to prove variants in one cell can differ. Before one shrink, persist a block outside the target: the resize must be rejected without an owned-file change; remove the block and retry. A shared or read-only object must also be rejected. **Resize This Variant to Capacity** affects only the selected variant.
|
||||
|
||||
6. Open the loaded variant's details. Change one exact pool entry's weight and chance, use **Duplicate This Cell's Variant**, toggle rotation, and use the two-click unlink confirmation.
|
||||
|
||||
Expected: only that entry changes, chance moves in five-percentage-point steps, the duplicate has a new key with a copied label and an independent object, and every stale callback is rejected by request ID.
|
||||
|
||||
7. Use **Duplicate All Enabled Cells as Family** to create `variant-2`.
|
||||
|
||||
Expected: one owned clone is created from the active variant of every enabled workcell; pool memberships, labels, and independent object dimensions are duplicated; every clone is atomically loaded and assigned to `variant-2`; a failure leaves both files and all live bindings unchanged. Seed `1337` selects one complete weighted theme without mixing families. Change a loaded piece's depth, count, and terminal rules, theme membership, theme weight, and mandatory caps: invalid combinations must fail atomically and surface in the automatic evaluation with no manual validation command.
|
||||
|
||||
8. Disable Tee.
|
||||
|
||||
Expected: its white-concrete cage remains, the GUI and scoreboard report Disabled, the workcell stays editable, and Tee pieces disappear from assembly. The permanent seed-`1337` preview on the negative-X side updates in place and is protected from players, fluids, pistons, explosions, growth, fire, entities, and redstone. The GUI, scoreboard, or `status` shows its selected theme and piece count. Reach it through both **Go to Preview** and `/iris jigsaw preview goto`. Re-enable Tee and confirm participation returns.
|
||||
|
||||
9. Open **Toolbox** and take the schema-`2` named sticks: selection, capacity, per-variant size, variant and workcell rename, duplicate-one and duplicate-family, preview, membership, rules and themes, caps, variant deletion, and project deletion.
|
||||
|
||||
Expected: right-click uses the exact bound context; rename sticks open an anvil and sneak-right-click resets the label; other context sticks open the matching GUI; destructive tools require a second use within 10 seconds; schema-`1` and replaced-Studio sticks are rejected. The active variant uses a jigsaw icon, a valid evaluation uses emerald, minimum placements does not use dye, and lime dye appears only as an explicitly labeled theme-membership boolean.
|
||||
|
||||
10. Have a second player try the chest, triple-sneak controls, a direct block edit, `/setblock`, `/fill`, `/execute run setblock`, `/function`, `/data merge block`, `/item replace block`, and an arbitrary plugin mutation command. Have them try to break, move, or explode the chest and the preview.
|
||||
|
||||
Expected: non-owner mutations and commands outside the strict informational and communication allowlist are cancelled throughout the Studio world, protected content stays intact, and owner edits still work.
|
||||
|
||||
11. Test ownership onboarding with prepared fixtures outside an active Studio:
|
||||
|
||||
```text
|
||||
/iris jigsaw adopt inspect overworld test/unowned target=auto strategy=auto
|
||||
/iris jigsaw adopt apply <reported-plan-uuid>
|
||||
```
|
||||
|
||||
Expected: an exclusive closure reports `IN_PLACE`, apply leaves every resource byte unchanged while atomically adding ownership and a receipt, and the target opens editable. A shared closure reports `CLONE_REQUIRED`, and `target=auto` picks a free `-studio` key and rewrites its internal references without touching the source. Mutate a pinned source after inspect: apply must report stale and write nothing. An auto-ingested `MANAGED_DATAPACK` fixture must block in-place and succeed only as a private named clone, and removing or refreshing the source must not remove that editable clone.
|
||||
|
||||
12. Convert one live registered jigsaw into an unused target:
|
||||
|
||||
```text
|
||||
/iris jigsaw convert overworld minecraft:village_plains target=test/converted-village seed=1337
|
||||
```
|
||||
|
||||
Expected: the command reports piece and pool counts plus any fidelity-warning count, writes an owned add-only graph with source provenance, and opens it in compact workcells. A non-jigsaw registered key and an occupied target both fail without overwriting. Inspect blocks, connectors, unsupported or native-only losses, and automatic display rotation before calling the conversion faithful.
|
||||
|
||||
13. Test variant and project deletion. Create a second variant, load it, and delete the now-inactive first variant through the two-click GUI; the last or currently loaded variant must stay protected. Add an external JSON placement or reference to the project and confirm project deletion is blocked with its owner path and location. Remove the reference, wait for autosave, then:
|
||||
|
||||
```text
|
||||
/iris jigsaw delete confirm=true
|
||||
```
|
||||
|
||||
Expected: Studio closes and the hash-pinned complete owned closure plus manifest are removed. If removal fails after close, the files remain recoverable.
|
||||
|
||||
14. On Folia, create a spatial project with one active workcell crossing several chunks and regions:
|
||||
|
||||
```text
|
||||
/iris jigsaw create overworld test/jigsaw-folia mode=spatial compatibility=iris width=32 height=24 depth=32 seed=1337
|
||||
/iris jigsaw bounds 48 24 32
|
||||
/iris jigsaw close
|
||||
/iris jigsaw open overworld test/jigsaw-folia seed=1337
|
||||
/iris jigsaw goto workcell/spatial
|
||||
/iris jigsaw piece expand
|
||||
```
|
||||
|
||||
Expected: spatial capacity and its author-facing workcell label persist while the live layout regenerates and rehydrates without a reopen. `piece expand` resizes only the active object to 48×24×32 while a smaller sibling variant keeps its dimensions. Change blocks in separated chunks of the expanded workcell without manually flushing autosave: with all intersections loaded, automatic capture schedules each intersection on its owning region and commits once after complete validation. Repeat with one intersection unloaded and confirm no owned file changes. The unit tests do not cover this; it has to be checked on a live Folia server.
|
||||
|
||||
15. Reopen a retained Iris project, attach it to a dimension, region, or biome placement with a unique `placementId`, validate the pack, and generate new chunks. Gate natural occurrence separately from Studio preview. For cave work, generate the mantle first, then confirm no-anchor chunks skip and actual anchors align as described in `15 - Caves & Carving.md`.
|
||||
|
||||
### Jigsaw Studio: strict vanilla export
|
||||
|
||||
Create a separate portable project. Its six default planar pieces must carry no Iris theme or terminal-rule metadata, and no chance, piece-rule, required-cap, channel, edit, loot, custom-block, or tile metadata. Wait for autosave and the automatic evaluation to settle.
|
||||
|
||||
```text
|
||||
/iris jigsaw create overworld test/jigsaw-portable mode=planar compatibility=vanilla width=16 height=16 depth=16 seed=1337
|
||||
/iris jigsaw export namespace=test output=test-jigsaw format=zip replace=false
|
||||
/iris jigsaw close
|
||||
```
|
||||
|
||||
1. Confirm `<Iris data>/packs/exports/test-jigsaw.zip` was published and contains `pack.mcmeta`, a biome tag, a processor list, template pools, compressed structure templates, the jigsaw structure, and the structure set.
|
||||
2. Run the same export with `output=../escape`.
|
||||
|
||||
Expect: the traversal name is rejected with no escape artifact and no new output created.
|
||||
|
||||
3. Stop a disposable **unmodded** Minecraft 26.2 world, put the zip in its `datapacks/` directory, and restart it. Do not use `/reload` for this gate — it can list the pack as enabled without rebuilding the running world's worldgen registries.
|
||||
4. Confirm the pack is enabled with no datapack or data errors, then:
|
||||
|
||||
```text
|
||||
/locate structure test:test/jigsaw
|
||||
```
|
||||
|
||||
Generate fresh chunks around the result.
|
||||
|
||||
5. **Expected:** the vanilla server loads the pack, locate resolves the exported key, and a natural assembled instance appears. Passing graph tests, a clean plugin boot, and a clean NBT decode do not tell you this — only a real vanilla server does.
|
||||
|
||||
This is a required manual runtime gate. Record it as untested until a disposable vanilla server or client completes all five steps.
|
||||
|
||||
Strict export must reject coherent themes, membership chance, non-default piece rules, required caps, non-portable channels, fixed rotation, structure edits or loot, tile payloads and block entities, custom blocks, retained marker blocks, invalid or duplicate connectors, weights outside `1..150`, depth above `20`, and radius above `8`. Disabled planar archetypes are omitted from the export. Full authoring and recovery detail: `21 - Jigsaw Structures.md`.
|
||||
|
||||
## I. Offline probe module (no live server)
|
||||
|
||||
Run from the Iris project root on JDK 25. These are CI gates, not in-game commands.
|
||||
|
||||
| Task | What it proves |
|
||||
|------|----------------|
|
||||
| `./gradlew :probe:run` | Loads compiled `core` classes with no `org.bukkit` on the runtime classpath; fails on purity violations outside the allowlist |
|
||||
| `./gradlew :probe:deserializationProbe` | Deserializes fixture entity, spawner, and loot JSON through the real Iris loaders on a Bukkit-free JVM |
|
||||
| `./gradlew :probe:genProbe -PprobePack=/path/to/packs/overworld` | Builds a real engine for dimension `overworld` at seed `1337` and generates a chunk spiral into buffers |
|
||||
|
||||
`genProbe` properties: `probePack` (the pack path — the built-in default is a developer-local path, so always pass your own), `probeRadius` (default `2`), and `probeCenterChunkX` / `probeCenterChunkZ` (default `0`). The task copies the pack to a temp directory, runs `PackValidator`, then generates and prints per-chunk hashes.
|
||||
|
||||
**Expected:** each probe exits 0. The classload and deserialization probes are part of the release verify job when CI is green (`86 - Maintainer - Release Checklist.md`).
|
||||
|
||||
## J. Minimal post-upgrade checklist
|
||||
|
||||
After replacing only the jar or mod:
|
||||
|
||||
1. Boot on the same world data.
|
||||
|
||||
Expect: no enable crash.
|
||||
|
||||
2. Run `/iris pack validate pack=<pack>` (Bukkit) or `/iris pack validate <pack>` (modded) against production packs.
|
||||
|
||||
Expect: no new blocking errors. New errors after an upgrade mean a schema change — check `03 - Configuration.md` and the relevant content doc before editing packs.
|
||||
|
||||
3. Generate a few new chunks in an existing Iris world.
|
||||
4. Optionally run a short GoldenHash verify against a stored baseline, if the pack and seed are unchanged (`32 - Determinism & Goldenhash.md`).
|
||||
5. If a pregen job was mid-run, confirm it reports status and resumes, or cancel it cleanly (`07 - Pregeneration.md`).
|
||||
|
||||
**Expected:** no enable crash, packs still loadable, generation continues.
|
||||
|
||||
## K. Failure triage order
|
||||
|
||||
Work top to bottom. Most reported "Iris broke" cases resolve in the first three.
|
||||
|
||||
1. Confirm Java 25 and the correct platform artifact (`01 - Installation & Platforms.md`).
|
||||
2. Confirm the pack validates and the dimension key exists (`25 - Pack Management.md`, `05 - Concepts & Pack Layout.md`).
|
||||
3. Confirm the target really is an Iris world with a live engine (`06 - Worlds & Lifecycle.md`).
|
||||
4. Capture GoldenHash with `threads=1` and `reset-mantle=true`. On mismatch, read the written `.new` and `.diag-…` files (`32 - Determinism & Goldenhash.md`).
|
||||
5. For throughput or memory problems, tune settings before changing packs (`33 - Performance Tuning.md`).
|
||||
6. For a release candidate, escalate to the maintainer gates (`87 - Maintainer - Release Readiness.md`).
|
||||
@@ -1,189 +0,0 @@
|
||||
# 32 - Determinism & Goldenhash
|
||||
|
||||
GoldenHash is the cross-platform determinism gate. It regenerates a square of chunks into memory buffers, hashes the blocks and biomes, and either writes that fingerprint to a baseline file or compares against one. The same pack bytes, Iris seed, radius, center, and height range must produce the same combined hash on Bukkit-family servers and on every mod loader. The operator runbooks that use this gate are in `31 - Operator Runbooks.md`.
|
||||
|
||||
## What determinism means here
|
||||
|
||||
Iris generation is a pure function of the pack plus a seed. Feed the same pack and the same seed to the engine and every block and biome must come back identical — same chunk, same platform, next week, other machine. Nothing in that chain is allowed to depend on wall-clock time, hash iteration order, thread scheduling, or which chunks happened to be generated first.
|
||||
|
||||
That property is what makes an Iris world portable. A player's base survives a server restart, a Bukkit world can be reproduced on Fabric, and a pack author can hand someone a seed and get the same terrain back. When determinism breaks, it usually breaks quietly: a handful of chunks differ at a biome boundary or inside a cave, nobody notices until a world is regenerated months later and the terrain no longer lines up with the buildings on it.
|
||||
|
||||
GoldenHash exists because eyeballing terrain cannot catch that. Two screenshots can look identical while thousands of blocks differ. A hash catches a single changed block state.
|
||||
|
||||
## What GoldenHash actually does
|
||||
|
||||
1. Optionally deletes the world's mantle so nothing cached from an earlier run can leak into the comparison.
|
||||
2. Walks a center-out spiral of chunks and calls `engine.generate` for each one into a scratch buffer. The world's region files are never touched.
|
||||
3. SHA-256s every block state key in the chunk, and separately SHA-256s biome samples taken on a 4-block grid.
|
||||
4. Sorts the per-chunk lines by chunk coordinate and SHA-256s the whole ordered body into one `#combined` value. Sorting is why thread count does not change the result.
|
||||
5. Writes that as a baseline, or compares it to an existing one and reports MATCH or MISMATCH.
|
||||
|
||||
Because the scan regenerates rather than reading the world, it also proves the engine can reproduce what it already wrote — not just that the world file on disk is stable.
|
||||
|
||||
## Capture a baseline
|
||||
|
||||
Prerequisites: a disposable Iris world, frozen pack bytes, a known seed (`1337` is the convention across these docs), center chunk `0,0`, and a small radius such as `8` for a first run. Write down the Iris artifact version, Minecraft version, pack hash, dimension height range, and JVM before you start — a baseline without that provenance is not usable later.
|
||||
|
||||
1. On Bukkit, run the command twice. The Bukkit command is always `AUTO`: the first run captures because no file exists, the second verifies against it.
|
||||
|
||||
```text
|
||||
/iris developer goldenhash world=<world> radius=8 threads=1 center-x=0 center-z=0 reset-mantle=true deep=false
|
||||
/iris developer goldenhash world=<world> radius=8 threads=1 center-x=0 center-z=0 reset-mantle=true deep=false
|
||||
```
|
||||
|
||||
Expected: the first run prints a captured line with the chunk count, a 12-character short hash, and the absolute path of the `.hashes` file. The second prints MATCH with the same short hash.
|
||||
|
||||
If the first run prints nothing but an error about the world, the target is not a loaded Iris world with a live engine. Check `/iris worlds`.
|
||||
|
||||
2. On Fabric, Forge, or NeoForge, ask for the modes explicitly:
|
||||
|
||||
```text
|
||||
/iris goldenhash 8 1 capture
|
||||
/iris goldenhash 8 1 verify
|
||||
```
|
||||
|
||||
Expected: `capture` writes the file, `verify` reports MATCH. Progress lines appear per chunk while the total is 64 chunks or fewer, then every 32nd chunk above that.
|
||||
|
||||
3. Keep the `.hashes` file next to the exact artifact build and pack hash it came from. It is only meaningful with that provenance.
|
||||
|
||||
## Compare a second platform
|
||||
|
||||
1. Copy the `.hashes` file into the other platform's golden directory (see the table below). Do not rename it — the filename encodes the dimension key, seed, center, and radius the comparison depends on.
|
||||
2. Create a disposable world there from the same pack bytes and the same seed, with the same dimension height range.
|
||||
3. Run `verify` (modded) or the same `AUTO` command (Bukkit).
|
||||
4. Expected: MATCH, and the same `#combined` value on every platform. Screenshots that look alike prove nothing.
|
||||
|
||||
Expect one warning on every cross-platform comparison: the `#mc` metadata line differs because Bukkit records `Bukkit.getBukkitVersion()` (for example `26.2-R0.1-SNAPSHOT`) while mod loaders record the plain Minecraft version (`26.2`). That mismatch is a warning only and does not stop the comparison. A seed or dimension mismatch is a hard failure and does stop it.
|
||||
|
||||
## Prove the gate can fail
|
||||
|
||||
A gate nobody has seen fail is not a gate. Once, in a disposable copy:
|
||||
|
||||
1. Change one pack input or one engine input.
|
||||
2. Re-run verify. Expected: MISMATCH, a `.new` file next to the golden file, and a `.diag-c<x>x<z>.txt` for the first differing chunk.
|
||||
3. Restore the original input and confirm MATCH returns before you treat the baseline as a release artifact.
|
||||
|
||||
## Reading a result
|
||||
|
||||
| Result | Meaning |
|
||||
|--------|---------|
|
||||
| Captured | No baseline existed; one was written. The path is printed. |
|
||||
| MATCH | Every chunk line is byte-identical to the baseline. The short combined hash is shown. |
|
||||
| MISMATCH | At least one chunk differs. Up to 10 chunk keys are listed, then a count of the rest. `.new` and a diagnosis file are written. |
|
||||
| Aborted | Some chunk failed to generate. Nothing is written; fix the generation failure first. |
|
||||
| Wrong world | The baseline's `#seed` or `#dim` does not match the live engine. No comparison is attempted. |
|
||||
|
||||
The diagnosis file regenerates the first mismatched chunk twice back to back, then a third time after deleting mantle chunks around it, and labels what it found:
|
||||
|
||||
- **Repeat-generation STABLE** — two consecutive generations agree. The divergence depends on order or accumulated state, not on the generate call itself.
|
||||
- **Repeat-generation UNSTABLE** — the same chunk differs between two consecutive calls with nothing in between. That is pure nondeterminism and is always an engine defect.
|
||||
- **Mantle-reset** — compares the scan result against a generation with the surrounding mantle deleted. A difference here points at mantle carryover rather than the terrain math.
|
||||
|
||||
The file ends with a full non-air block dump of the first generation, which is what you diff against the other platform's dump.
|
||||
|
||||
## When a check fails
|
||||
|
||||
| Symptom | What to do |
|
||||
|---|---|
|
||||
| Bukkit run says the target is not an Iris world | The world is not loaded or has no engine. Load it and retry; `AUTO` cannot capture from a dead engine. |
|
||||
| `verify` says the baseline file is missing | The filename is built from dimension key, seed, center, and radius. One of those differs from the capture. Compare the name in the golden directory against your command. |
|
||||
| Wrong world (seed or dimension) | Recreate the disposable world with the recorded seed and pack. Never hand-edit the metadata lines to force a comparison. |
|
||||
| Mismatch only with multiple threads | Re-run both sides with `threads=1`. If serial matches and parallel does not, that is an engine defect, not a tuning problem — the result is sorted before hashing, so thread count must not matter. |
|
||||
| Stable mismatch on both sides | Compare pack bytes, dimension height range, Iris artifact, and whether both runs reset mantle. Then read `.new` and the first `.diag-*` file. |
|
||||
| Repeat-generation UNSTABLE | Stop the release comparison. Preserve the diagnosis artifacts; consecutive generation is nondeterministic and no baseline is trustworthy until that is fixed. |
|
||||
|
||||
## Rules that keep comparisons honest
|
||||
|
||||
1. **Same pack bytes everywhere.** Same pack key, same files. For a release baseline, freeze the default overworld download to a commit or tag.
|
||||
2. **Same Iris seed.** Create with `seed=1337` (Bukkit) or the positional `1337` (modded), as in `02 - Getting Started.md`.
|
||||
3. **Same radius and center.** Modded center is fixed at chunk `0,0`, so use `center-x=0 center-z=0` on Bukkit.
|
||||
4. **Same height range.** The dimension's min and max Y define the hash window; changing height changes the hash by definition.
|
||||
5. **Use `threads=1` when hunting order dependence.** Multi-threaded scans must still match the serial result.
|
||||
6. **Reset mantle on both sides** when you are comparing regeneration purity. Bukkit defaults to `reset-mantle=true`; modded always resets.
|
||||
7. **Do not confuse pregen with hashing.** GoldenHash regenerates into buffers and ignores whatever pregen wrote. Pregen is still worth running as a stress gate before or after (`07 - Pregeneration.md`).
|
||||
8. **Disposable worlds only.** Buffers mean no block writes, but `reset-mantle` deletes the world's mantle files, and the diagnosis path deletes mantle chunks around the failing chunk.
|
||||
|
||||
## Reference
|
||||
|
||||
### What is hashed
|
||||
|
||||
- **Blocks:** every local column `x,z` in `0..15` and every `y` from the engine or world minimum height (inclusive) to the maximum (exclusive). Each block state key (for example `minecraft:stone`) is fed into a per-chunk SHA-256 digest.
|
||||
- **Biomes:** the same height span sampled every 4 blocks in x, y, and z (`BIOME_STEP = 4`). A null biome sample hashes as `minecraft:plains` (`GoldenHashEngine.FALLBACK_BIOME_KEY`).
|
||||
- **Combined:** SHA-256 over the per-chunk lines sorted by packed chunk key, stored as `#combined=<hex>`.
|
||||
- **Not touched:** Minecraft region files. The scan generates into buffers only.
|
||||
- **Deleted when `reset-mantle` is true:** every file directly inside the engine's mantle data folder, after a `saveAll`. This is the whole world's mantle, not only the scanned square, despite what the in-game help text implies.
|
||||
|
||||
### Golden file location and name
|
||||
|
||||
| Platform | Golden directory |
|
||||
|----------|------------------|
|
||||
| Bukkit-family plugin | Iris data folder `golden/` (for example `plugins/Iris/golden/`) |
|
||||
| Fabric / Forge / NeoForge | `<configDir>/irisworldgen/golden/` |
|
||||
|
||||
```
|
||||
<dimensionLoadKey>-s<seed>-c<centerChunkX>x<centerChunkZ>-r<radius>.hashes
|
||||
```
|
||||
|
||||
Example: `overworld-s1337-c0x0-r22.hashes`.
|
||||
|
||||
The seed in that name is `World.getSeed()` on Bukkit and the Iris engine seed (`engine.getSeedManager().getSeed()`) on mod loaders. For a world created through `/iris create` with an explicit seed they are the same value.
|
||||
|
||||
File layout:
|
||||
|
||||
```
|
||||
#iris-goldenhash v1
|
||||
#world=<world name>
|
||||
#dim=<dimension load key>
|
||||
#seed=<seed>
|
||||
#mc=<platform Minecraft version string>
|
||||
#minY=<min> maxY=<max>
|
||||
#center=<cx>,<cz>
|
||||
#radius=<chunks>
|
||||
<chunkX> <chunkZ> <blockSha256> <biomeSha256>
|
||||
...
|
||||
#combined=<sha256>
|
||||
```
|
||||
|
||||
On mismatch the engine also writes:
|
||||
|
||||
- `<file>.new` — the current body plus its combined hash
|
||||
- `<file>.diag-c<x>x<z>.txt` — diagnosis of the first mismatched chunk
|
||||
- `<file>.deep/` or `<file>.deep-verify/` — per-chunk non-air block dumps when `deep=true` (Bukkit only); the `-verify` suffix is used when a golden file already exists
|
||||
|
||||
### Modes
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `CAPTURE` | Always write a new golden file |
|
||||
| `VERIFY` | Fail if no golden file exists; otherwise compare and report MATCH or MISMATCH |
|
||||
| `AUTO` | Capture when the file is missing, verify when it exists |
|
||||
|
||||
The Bukkit command is hard-wired to `AUTO`. Modded exposes `capture` and `verify` literals and falls back to `AUTO`.
|
||||
|
||||
### 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 at least 0. The target must be a loaded Iris world with a live engine. Metadata comes from `World.getSeed()`, `Bukkit.getBukkitVersion()`, and the world's min and max height. Nothing stops a second Bukkit scan from starting while one is running; do not start one.
|
||||
|
||||
Modded (Fabric / Forge / NeoForge):
|
||||
|
||||
```
|
||||
/iris goldenhash [radius] [threads] [capture|verify]
|
||||
```
|
||||
|
||||
Alias `gold`. Defaults: radius `8`, threads `8`, mode `AUTO`. The command tree accepts radius `0..256` and threads `1..64`. Center is always chunk `0,0`, mantle is always reset, and deep dumps are not exposed. A second scan is refused while one is running.
|
||||
|
||||
While any scan is active, Iris suspends engine mantle maintenance (trimming and plate unloading) process-wide so maintenance cannot perturb the comparison.
|
||||
|
||||
### Offline generation probe (not a GoldenHash file)
|
||||
|
||||
`./gradlew :probe:genProbe -PprobePack=…` builds an offline engine for dimension key `overworld` at seed `1337`, validates the pack, generates a chunk spiral into buffers, and prints per-chunk hashes to stdout. It never reads or writes `iris-goldenhash v1` files, so it cannot be compared against a baseline. Use it as a headless regression signal; use in-game GoldenHash for cross-platform gates. Probe details: `31 - Operator Runbooks.md`.
|
||||
|
||||
### Release gate
|
||||
|
||||
Release 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 change in that hash blocks the release (`87 - Maintainer - Release Readiness.md`).
|
||||
@@ -1,145 +0,0 @@
|
||||
# 33 - Performance Tuning
|
||||
|
||||
Iris throughput is bounded by four things: how many chunks the platform will let Iris generate at once, how much mantle stays resident in heap, how often pack resources are reloaded from disk, and whether the JVM has the incubator Vector API. This page is organized by the symptom you are looking at, not by settings file order. Every knob lives in `settings.json` under the Iris data directory (`03 - Configuration.md`); pregen operations are in `07 - Pregeneration.md`. Any change here must leave GoldenHash unchanged (`32 - Determinism & Goldenhash.md`).
|
||||
|
||||
## Before you turn any knob
|
||||
|
||||
Most bad tuning comes from changing three things, seeing a better number once, and keeping all three. Do this instead:
|
||||
|
||||
1. Freeze the inputs: Iris artifact, pack bytes, seed, center, radius, JVM flags, and server population.
|
||||
2. Run one warmup, then three measured runs. Record chunks/second, wall time, peak heap, GC behavior, and failed chunk count.
|
||||
3. Change exactly one setting. Restart if the setting is read once at startup — thread pools, caches, and SIMD kernel selection all are.
|
||||
4. Repeat the warmup and three runs over the same area. A comparison across different terrain is not a comparison.
|
||||
5. Keep the change only if the median improves with no determinism mismatch, no new failures, no unacceptable heap growth, and no worse tick latency.
|
||||
6. Restore the old value before testing the next knob.
|
||||
|
||||
Reach for JProfiler when the numbers move without an obvious cause: stalls, allocation pressure, or scheduler behavior. A faster pregen status line on its own doesn't tell you why.
|
||||
|
||||
## Symptom: pregen is slow
|
||||
|
||||
Work through these in order. The first two are free; the rest trade something.
|
||||
|
||||
1. **Check whether the platform is the limit, not Iris.** On Fabric, Forge, and NeoForge without a parallel chunk system, pregen runs through the vanilla main-thread chunk pipeline and throughput is capped there regardless of settings. Iris logs this at pregen start and names the fix: install C2ME on Fabric, or run Paper if you want Bukkit-level throughput. No Iris setting recovers that gap.
|
||||
2. **Confirm SIMD is on.** On Bukkit, the startup log prints one of `SIMD: vector kernels enabled (…)`, `SIMD: scalar kernels active; add --add-modules jdk.incubator.vector …`, or `SIMD: vector kernels disabled (performance.simdKernels=false)`. If you see the scalar message, add the JVM flag and restart. See the SIMD section for what it actually accelerates and how small that surface is. Mod loaders never print this line, so check the JVM flag directly there.
|
||||
3. **Leave concurrency alone unless it is warning at you.** Bukkit pregen concurrency is derived, not configured: Iris sizes it from the detected chunk-system worker pool (or CPU count) times 8, clamped to 16–128 on Paper-like servers and 64–192 on Folia. Raising it is not an option, and the adaptive limiter already lowers it when mantle backpressure engages. The only concurrency lever on Bukkit is `serial=true`, which drops to one chunk in flight — use it for profiling and determinism isolation, never for throughput.
|
||||
4. **On mod loaders, size `pregen.moddedPregenInFlight` to the chunk system.** Default `0` resolves to `clamp(16, cpu*2, 48)`, and whatever value comes out is floored at 8. Raise it only if the loader has a parallel chunk system and the CPU is not saturated; lower it if you see chunk-load timeouts. Positive values are capped at 512.
|
||||
5. **Raise the object cache if the same objects keep reloading.** `performance.objectLoaderCacheSize` (default 4096) bounds the loader caches for `.iob` objects, matter objects, and images. Object-heavy packs on large pregens hit this. The tradeoff is retained heap, so only do this if heap has room — see the memory section.
|
||||
6. **Give the process more heap before touching mantle caps.** Resident mantle plates are budgeted against process memory, so a bigger heap raises the effective plate count without any settings change.
|
||||
|
||||
`performance.noiseCacheSize` is not worth tuning for pregen: starting a pregen raises it to at least 4096 in memory, hotloads the engine, and sets `iris.cache.fast` as a system property. Neither is lowered again for the life of the process. The Bukkit plugin already sets `iris.cache.fast` during startup; on mod loaders it only comes on with the first pregen, so pass `-Diris.cache.fast=true` on the JVM command line there if you want it covering ordinary generation too.
|
||||
|
||||
## Symptom: TPS dips or chunk-load timeouts while generating
|
||||
|
||||
Generation competing with the server tick shows up as timeout warnings, region scheduler complaints, or players reporting lag near the pregen frontier.
|
||||
|
||||
| Do this | Effect | Cost |
|
||||
|---|---|---|
|
||||
| Run pregen with `serial=true` (Bukkit, Paper-compatible) or `sync` (modded) | One chunk in flight at a time; the tick thread stops competing with a wide generation front | Much slower pregen; this is an isolation tool, not a production mode |
|
||||
| Lower `pregen.moddedPregenInFlight` (modded only) | Fewer concurrent chunk generations, so the chunk system keeps headroom for player chunks | Proportionally slower pregen |
|
||||
| Raise `pregen.chunkLoadTimeoutSeconds` (default 15, clamped 5–120) | Iris waits longer before declaring a chunk load stuck and warning | Hides a real stall instead of fixing it; try it last. Modded pregen ignores anything below 120 seconds |
|
||||
| Raise `pregen.timeoutWarnIntervalMs` (default 500, minimum 250) | Spaces out repeated timeout warnings in console | Log noise only; changes nothing about the stall |
|
||||
| Raise `pregen.saveIntervalMs` (default 30000, clamped 5000–900000) | Less frequent pregen state flushing, so less periodic IO | More work replayed if the job is interrupted |
|
||||
|
||||
`pregen.runtimeSchedulerMode` (`AUTO`, `PAPER_LIKE`, `FOLIA`) and `pregen.paperLikeBackendMode` (`AUTO`, `TICKET`, `SERVICE`) exist for platform mismatches, not throughput. A Folia runtime always resolves to Folia scheduling regardless of the setting, and `AUTO` on Paper-like servers resolves to the ticket backend. Change these only when diagnosing a scheduler-specific defect.
|
||||
|
||||
## Symptom: heap pressure, long GC pauses, or OOM risk
|
||||
|
||||
Mantle is the largest thing Iris keeps in heap. Iris already reacts to heap pressure on its own: as used heap climbs from 82% to 92%, the idle window before a mantle plate is trimmed shrinks linearly to zero, and above 96% Iris requests a reclaim (at most once every 30 seconds). If you are seeing pressure, that machinery is already running — you are deciding how much less mantle to hold.
|
||||
|
||||
1. **Raise heap first if the machine has it.** The resident-plate budget is computed from process memory: roughly 60% of the heap, against a per-plate cost of about 48 MB at a 384-block world height, scaled by your actual dimension height. More heap means more plates without changing a setting.
|
||||
2. **Lower `pregen.maxResidentTectonicPlates`** (default 96). This is a soft cap on how many mantle tectonic plates stay resident. The effective number is the smaller of that cap, a height-scaled version of it, and the heap budget above — with a hard floor of 16. Taller worlds get fewer plates automatically. Lowering it cuts retained heap at the cost of more mantle reload work.
|
||||
3. **Lower `performance.mantleKeepAlive`** (default 30). This is how many seconds an idle mantle plate survives before maintenance trims it. Lower means memory comes back sooner; it also means recently-touched regions get re-read more often.
|
||||
4. **Lower the loader caches** if a heap dump shows retained pack data rather than mantle: `performance.objectLoaderCacheSize` (default 4096) and `performance.resourceLoaderCacheSize` (default 1024).
|
||||
5. **Slow the pregen down.** Backpressure knobs decide how long a generation thread waits when the mantle plate budget is full: `pregen.mantleBackpressureWaitMs` (default 25, clamped 5–1000) is the wait between retries, and `pregen.mantleBackpressureTimeoutMs` (default 60000, clamped 5000–600000) is how long it waits before giving up on that chunk. Raising the timeout buys a slow job time to finish instead of failing chunks; it does not reduce memory use.
|
||||
|
||||
`performance.engineSVC.forceMulticoreWrite` (default false) makes mantle plate unloading use the parallel path all the time instead of only under heap pressure. It returns memory faster during sustained generation and costs CPU that would otherwise go to generating.
|
||||
|
||||
## Symptom: Studio memory keeps growing during editing
|
||||
|
||||
Studio worlds deliberately skip mantle trimming and per-chunk mantle cleanup, so a long authoring session accumulates mantle that a normal world would have released. Set `performance.trimMantleInStudio` to `true` to make studio worlds maintain mantle like any other world. The cost is that hotloaded pack edits will regenerate more from scratch because less is cached. A/B this in Studio only; it has no effect on production worlds.
|
||||
|
||||
## Symptom: the same pack resources reload constantly
|
||||
|
||||
`performance.resourceLoaderCacheSize` (default 1024) bounds the cache of parsed JSON pack resources; `performance.objectLoaderCacheSize` (default 4096) bounds `.iob`, matter, and image loaders. If profiling shows repeated parse or disk work for resources you know are in use, raise the one that is actually missing, one at a time. Both trade heap for fewer reloads, and neither changes generation output.
|
||||
|
||||
## Reference: `performance` section
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `performance.simdKernels` | `true` | Allows vector kernels when `jdk.incubator.vector` is on the module path; `false` forces scalar. Read once at class initialization, so a restart is required |
|
||||
| `performance.mantleKeepAlive` | `30` | Seconds an idle mantle plate survives before maintenance trims it. Shrinks toward zero as used heap climbs from 82% to 92% |
|
||||
| `performance.mantleCleanupDelay` | `200` | Ticks a loaded chunk waits before its mantle cleanup runs (200 = 10 s). Raising it keeps mantle data resident longer after chunk loads; see "03 - Configuration.md" |
|
||||
| `performance.trimMantleInStudio` | `false` | Whether studio worlds get mantle trimming and per-chunk cleanup at all; false means they keep everything resident |
|
||||
| `performance.noiseCacheSize` | `1024` | Noise sample cache capacity per engine. Starting a pregen raises it to at least 4096 for the rest of the process |
|
||||
| `performance.resourceLoaderCacheSize` | `1024` | Parsed pack resource entries held before eviction |
|
||||
| `performance.objectLoaderCacheSize` | `4096` | `.iob`, matter, and image loader entries held before eviction |
|
||||
| `performance.engineSVC.useVirtualThreads` | `true` | Maintenance workers run on virtual threads; `false` uses platform threads |
|
||||
| `performance.engineSVC.forceMulticoreWrite` | `false` | Always unload mantle plates on the parallel path instead of only under heap pressure |
|
||||
| `performance.engineSVC.priority` | `5` (`Thread.NORM_PRIORITY`) | Priority of maintenance platform threads, clamped to the legal Java range. Ignored entirely when virtual threads are on |
|
||||
| `performance.engineSVC.parallelism` | `-1` | Size of the engine maintenance worker pool. A positive value is capped at `2 × CPU`; zero or negative means `ceil(sqrt(CPU))` |
|
||||
|
||||
`engineSVC` sizes the maintenance service — mantle trimming, plate unloading, periodic saves — not chunk generation. Raising `parallelism` will not generate chunks faster; it makes mantle housekeeping finish sooner and take more CPU while it does. Generation parallelism is derived separately (see below).
|
||||
|
||||
## Reference: `pregen` section
|
||||
|
||||
| Key | Default | What it does |
|
||||
|-----|---------|--------------|
|
||||
| `pregen.runtimeSchedulerMode` | `AUTO` | Which scheduler the Bukkit pregen driver uses: `AUTO`, `PAPER_LIKE`, `FOLIA`. A Folia runtime always resolves to Folia |
|
||||
| `pregen.paperLikeBackendMode` | `AUTO` | How Paper-like pregen acquires chunks: `AUTO`, `TICKET`, `SERVICE`. `AUTO` resolves to `TICKET` |
|
||||
| `pregen.chunkLoadTimeoutSeconds` | `15` | How long a pregen worker waits for a chunk before warning. Clamped 5–120; modded pregen raises anything below 120 to 120 |
|
||||
| `pregen.timeoutWarnIntervalMs` | `500` | Minimum gap between repeated timeout warnings. Minimum 250 |
|
||||
| `pregen.saveIntervalMs` | `30000` | Gap between pregen progress flushes. Clamped 5000–900000 |
|
||||
| `pregen.maxResidentTectonicPlates` | `96` | Ceiling on resident mantle plates before the height and heap budgets narrow it further. Never drops below 16 |
|
||||
| `pregen.mantleBackpressureWaitMs` | `25` | Pause between retries when the plate budget is full. Clamped 5–1000 |
|
||||
| `pregen.mantleBackpressureTimeoutMs` | `60000` | How long a chunk waits on backpressure before failing. Clamped 5000–600000 |
|
||||
| `pregen.moddedPregenInFlight` | `0` | Concurrent pregen chunks on mod loaders. `0` resolves to `clamp(16, cpu*2, 48)`; positive values cap at 512; the result is floored at 8 |
|
||||
|
||||
Related: `world.globalPregenCache` (default `false`) — see `03 - Configuration.md` and `07 - Pregeneration.md`.
|
||||
|
||||
## Reference: derived concurrency
|
||||
|
||||
The `concurrency` section in `settings.json` has no writable keys. The values are computed from CPU count at runtime and cannot be overridden from the file:
|
||||
|
||||
- Generation burst pool: `max(2, availableProcessors)`
|
||||
- IO burst pool: `max(2, availableProcessors / 2)`
|
||||
- Bukkit pregen in-flight cap: worker threads × 8, clamped 16–128 on Paper-like servers and 64–192 on Folia, then lowered adaptively under mantle backpressure down to `max(4, min(16, cap / 4))`
|
||||
|
||||
If you need less generation concurrency, use `serial=true` (Bukkit) or `sync` (modded) rather than looking for a knob that does not exist.
|
||||
|
||||
## SIMD
|
||||
|
||||
What actually uses vector kernels today is narrow: an array rounding path in the chunked double data cache, and array operations in mantle carving. The 2D fractal noise vector kernels (`VectorNoiseKernels2D`) exist and are correct, but nothing in the production worldgen path calls `SimdSupport.noiseKernels2D()` yet. Treat noise SIMD as unfinished and do not size hardware around it.
|
||||
|
||||
Selection happens once, at class initialization:
|
||||
|
||||
1. `performance.simdKernels` false → scalar kernels.
|
||||
2. Otherwise, if the `jdk.incubator.vector` module is present and the vector kernel class loads → vector kernels.
|
||||
3. Otherwise → scalar kernels, with a startup log line telling you to add the flag.
|
||||
|
||||
The 2D noise kernels add one more gate: they are only selected when the preferred `double` and `long` vector species have matching lane counts and at least 4 double lanes. Apple Silicon NEON, at 2 lanes, does not qualify.
|
||||
|
||||
The JVM flag is required for any vector path:
|
||||
|
||||
```
|
||||
--add-modules jdk.incubator.vector
|
||||
```
|
||||
|
||||
The Iris Gradle build passes it for core compilation and tests and for every `probe` task, and `tools/simd-bench/` passes it in its own scripts. Nothing adds it to a production server's start script — a server operator must add it there.
|
||||
|
||||
To A/B on a real server: set `performance.simdKernels` false, restart, measure pregen chunks/second, set it true, restart, measure again, and confirm GoldenHash is unchanged (`32 - Determinism & Goldenhash.md`). `tools/simd-bench/` (`./run.sh` or `run.bat`) measures kernels in isolation and deliberately ignores the profitability gate, so its speedups do not predict end-to-end pregen gains.
|
||||
|
||||
## Measurement checklist
|
||||
|
||||
Record for every experiment: pack identity, seed, radius, serial/sync flags, JVM version and flags, heap size, CPU, the `performance` and `pregen` excerpts you changed, chunks/second, duration, failed chunks, peak heap, and the GoldenHash combined value. Reject any optimization that changes the hash unless the behavior change was intended and is documented. Release-scale baselines (5k–10k chunks with JProfiler) are tracked in `87 - Maintainer - Release Readiness.md`.
|
||||
|
||||
Never tune by editing pack content. Pack edits change terrain, which changes the hash, which means you are no longer comparing the same thing.
|
||||
|
||||
## Offline tools
|
||||
|
||||
| Tool | Command | Use |
|
||||
|------|---------|-----|
|
||||
| Generation probe | `./gradlew :probe:genProbe -PprobePack=…` | Headless engine generation; a correctness signal, not a throughput benchmark |
|
||||
| Classload probe | `./gradlew :probe:run` | Core-purity gate; fails if `org.bukkit` leaks into engine classes |
|
||||
| SIMD microbench | `tools/simd-bench/run.sh` | Kernel-only scalar versus vector timing |
|
||||
|
||||
Runbooks that combine pregen and GoldenHash: `31 - Operator Runbooks.md`.
|
||||
@@ -1,126 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## How to run the bump
|
||||
|
||||
Use a dedicated branch or worktree and begin from a green build. Record the old and new Minecraft, Bukkit API, loader, datapack-format, and NMS binding values before editing. Complete the ordered steps without skipping ahead, run the focused check after each platform boundary, then run the full all-platform build and the disposable-server lifecycle runbooks.
|
||||
|
||||
Stop when an upstream API, mapping, or loader artifact is unavailable; do not hide the gap behind a legacy fallback. The bump is complete only when generated artifact names, metadata ranges, data fixers, NMS selection, tests, and documentation all agree on the same target.
|
||||
|
||||
## 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 the three loader jar names. The CraftBukkit jar name comes from `bukkitMinecraftRange` instead (step 1).
|
||||
- `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. `bukkitMinecraftRange` is deliberately absent from `gradle.properties`; it defaults to `'26.1.2-26.2'` in `build.gradle` and is the version string the CraftBukkit jar filename carries, so edit that default (or pass `-PbukkitMinecraftRange=`) whenever the supported Bukkit range moves.
|
||||
|
||||
2. Edit `gradle/libs.versions.toml`:
|
||||
- `paper-api` — the compile pin for `:core` and `:adapters:bukkit:plugin` (currently `26.1.2.build.74-stable`). Like `apiVersion`, it is held at the lowest supported release line so one plugin jar loads across the whole range; raise it only when dropping that line.
|
||||
- `spigot` — the Spigot API pin (`<mc>-R0.1-SNAPSHOT`). Used `compileOnly` by the three modded adapters and `:probe` only, never by the Bukkit plugin. Bump it with `minecraftVersion`.
|
||||
- `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 named for the MC version with dots replaced by underscores: `V<mc>("<mc>", <packFormat>, <DataFixer>::new)`. The current tail is `V26_1_2("26.1.2", 101, DataFixerV1217::new), V26_2("26.2", 107, DataFixerV1217::new)`.
|
||||
- `packFormat` comes from https://minecraft.wiki/w/Pack_format.
|
||||
- `getLatest()` returns the last enum constant, so append at the end; do not reorder, and leave `UNSUPPORTED` first.
|
||||
- Reuse the existing fixer when the datapack format did not change (26.1.2 and 26.2 both use `DataFixerV1217`). If it did change, add a new `IDataFixer` implementation in a versioned subpackage under `core/src/main/java/art/arcane/iris/core/nms/datapack/` (existing ones are `v1192`, `v1206`, `v1213`, `v1217`).
|
||||
|
||||
4. Register **and** select the new Bukkit NMS binding. The revision tag is spelled out by hand in five places plus the sources; miss any one and the build fails or the plugin refuses to bind at boot.
|
||||
- `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>: '<paper-dev-bundle-version>'`. That value is passed to paperweight's `paperDevBundle(...)`, not to Spigot BuildTools; it is currently `'26.1.2.build.74-stable'`, held at the same low pin as `paper-api`.
|
||||
- `build.gradle` — update `art/arcane/iris/core/nms/v26_2_R1/NMSBinding.class` in `requiredBukkitArtifactEntries` to the new tag, or `verifyBukkitArtifact` fails against the shipped jar.
|
||||
- `.github/workflows/ci.yml` — update the hardcoded `:adapters:bukkit:nms:v26_2_R1:test` task path in the `verify` job.
|
||||
- `core/src/main/java/art/arcane/iris/core/nms/NmsBindingSelector.java` — the runtime half, fully hand-maintained. Update `SUPPORTED_TAG`, `SUPPORTED_VERSIONS`, and the `isSameRelease(...)` guard. `INMS.bindExact` reflects on `art.arcane.iris.core.nms.<tag>.NMSBinding`, so an unedited selector rejects the new server before the new binding is ever loaded. Update `core/src/test/java/art/arcane/iris/core/nms/NmsBindingSelectorTest.java` in the same edit; it asserts the tag and the accepted/rejected versions.
|
||||
- Create the binding sources under `adapters/bukkit/nms/v<major>_<minor>_R<rev>/src/main/java/art/arcane/iris/core/nms/v<major>_<minor>_R<rev>/`, matching the existing set: `NMSBinding`, `IrisChunkGenerator`, `CustomBiomeSource`, `VanillaStructureBiomes`, `DatapackStructureStateFilter`, `ImportedFeatureStage`.
|
||||
|
||||
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,66)`) and the `forge` dependency versionRange (also `[65,66)`). Both are hand-maintained and both are upper-bounded, so both must move on every Forge line bump.
|
||||
- `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,26.3)`) and is *not* templated from `minecraftVersion` — hand-edit it on every bump or the mod will refuse to load on the new 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(...) }` in `adapters/forge/build.gradle` and `neoForge { accessTransformers.from(...) }` in `adapters/neoforge/build.gradle`; NeoForge additionally declares the file in an `[[accessTransformers]]` block in `neoforge.mods.toml`.
|
||||
|
||||
Mixin configs — three JSONs, eight mixin classes, all targeting Mojang-mapped members. The JSONs only list class names; the mapped method names and descriptors below live in the `.java` mixin classes, so check those files, not the configs:
|
||||
|
||||
- `adapters/fabric/src/main/resources/irisworldgen.mixins.json`
|
||||
(package `art.arcane.iris.fabric.mixin`, `compatibilityLevel` `JAVA_21`, 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`, `compatibilityLevel` `JAVA_21`, classes listed under `client` rather than `mixins`)
|
||||
- `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; FML 26.2 reads neither `[[mixins]]` nor `accessTransformers` from a Forge toml. The jar manifest attribute `MixinConfigs` in `adapters/forge/build.gradle` is the only shipping registration, and the `minecraft { runs { … } }` block passes `--mixin.config` separately (`client` gets entity + client, `server` gets entity only). Adding a mixin config on Forge means editing the manifest attribute *and* the relevant 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 across three places that must agree exactly. `adapters/fabric/build.gradle` adds every Fabric API module to the `jij` configuration, and `shadowJar` copies it into `META-INF/jars`, renaming each artifact through the `nestedFabricApiJars` map (an explicit module-to-filename table, not a version-stripping regex — a bundled jar with no map entry fails the build with `Undeclared Fabric API nested jar`). The `jij` configuration is `transitive = false`, so the bundled set is exactly the declared set. Adding or removing a module means editing the `jij` dependency list, `nestedFabricApiJars`, and the `jars` array in `fabric.mod.json` together. 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, in this order:
|
||||
- `./gradlew :core:check` — includes `bukkitPurityRatchet` and the NMS selector tests. Green before touching the platform builds.
|
||||
- `./gradlew buildBukkit` — runs `verifyBukkitArtifact` first, then drops the jar in `dist/`.
|
||||
- `./gradlew buildFabric`
|
||||
- `./gradlew buildForge`
|
||||
- `./gradlew buildNeoforge`
|
||||
|
||||
Each `build*` task runs its artifact verifier as a dependency, so a missing resource, an unapplied mixin config, or a new Bukkit-coupled class fails the task rather than shipping. `./build-all.sh` runs all four serialized with local VolmLib substitution off; use it for the final pass. Good looks like: four jars in `dist/` carrying the new version strings, and a `Verified …` line for every platform.
|
||||
|
||||
9. After a successful bump, re-run the operator runbooks and GoldenHash parity on all platforms (`31 - Operator Runbooks.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` expands `apiVersion` into both `plugin.yml` and `paper-plugin.yml` at `processResources`.
|
||||
- `BuildConstants.MINECRAFT_VERSION` — stamped by the `generateTemplates` task in `core/build.gradle` from `minecraftVersion` (template at `core/src/main/templates/BuildConstants.java`); consumed by `Tasks.supportedVersions`.
|
||||
- Mod-metadata `minecraft` version ranges — templated from `minecraftVersion` at `processResources`.
|
||||
- The three loader jar names and the `com.mojang:minecraft` coordinate — composed from `minecraftVersion` in the build scripts. The CraftBukkit jar name is the exception: it carries `bukkitMinecraftRange`, not `minecraftVersion` (step 1).
|
||||
|
||||
## Notes
|
||||
|
||||
- `build.gradle`, the adapter `build.gradle` files, and `settings.gradle` carry defensive `.getOrElse(...)` 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. `bukkitMinecraftRange` is the one exception — it has no `gradle.properties` entry, so its `'26.1.2-26.2'` default is the live value.
|
||||
- The Java literals `"26.2"` and `"26.1.2"` intentionally remain in `DataVersion.java` (structural enum constants), `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.
|
||||
- `NmsBindingSelector.java` and `NmsBindingSelectorTest.java` also hardcode MC version literals, but those are *not* intentional leftovers — they are required edits (step 4).
|
||||
@@ -1,69 +0,0 @@
|
||||
# 86 - Maintainer — Release Checklist
|
||||
|
||||
Manual release procedure. Nothing publishes, tags, or announces automatically 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.
|
||||
|
||||
## How to execute this checklist
|
||||
|
||||
Use one immutable commit and one JDK 25 environment from preflight through publication. Track build, tests, startup, gameplay, determinism, and publication as separate results; a later pass does not erase an earlier unexplained failure. Stop at the first failed required item, fix it on a new candidate commit, and restart the checklist from preflight.
|
||||
|
||||
Publication remains manual. Commands in this document produce local artifacts until the explicit publish section; do not upload, tag, or announce from an unclean or differently tested tree.
|
||||
|
||||
## 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 single `verify` job (`.github/workflows/ci.yml`) runs on JDK 25: `:core:check`, the Bukkit plugin and NMS binding tests, `:spi:build`, the probe tests plus `:probe:run` and `:probe:deserializationProbe`, the shared modded tests on all three loaders, the `buildSrc` artifact-verifier tests, then `verifyBukkitArtifact` and `verifyModdedArtifacts` (which build all four artifacts). 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). The file lives at the workspace root, one level above this repo (`../MasterChangelog.MD`); the section header is `## Plugin: Iris`.
|
||||
- [ ] 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`. It runs `buildAllToOut --no-parallel -PuseLocalVolmLib=false`, so local VolmLib substitution is off, the immutable `volmLibCoordinate` from `gradle.properties` is used, and the all-platform build is serialized. Each platform task runs its artifact verifier first, so the run either fails or prints a `Verified …` line per jar plus the `=== Iris buildAllToOut -> dist/ ===` summary.
|
||||
- [ ] `dist/` contains the four platform jars (exact names for this release):
|
||||
- [ ] `Iris v4.0.0-26.2 [CraftBukkit] 26.1.2-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>] <target>.jar`. For the loader jars `<target>` is `<minecraftVersion>+<loaderDisplay>`; for CraftBukkit it is `bukkitMinecraftRange` (the supported Minecraft *range*, currently `26.1.2-26.2`), not `minecraftVersion`.
|
||||
- [ ] 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; `verifyModdedArtifacts` scans outer classes and nested jars for those packages and their relocated forms, so trust its pass rather than re-inspecting by hand.
|
||||
|
||||
## 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>` (mode is hard-wired to `AUTO`, so it verifies when the matching capture already exists)
|
||||
- [ ] Fabric mod: `/iris goldenhash <radius> <threads> verify`
|
||||
- [ ] Forge mod: `/iris goldenhash <radius> <threads> verify`
|
||||
- [ ] NeoForge mod: `/iris goldenhash <radius> <threads> verify`
|
||||
- The modded mode literal comes last, after radius and threads; a bare `/iris goldenhash <radius> <threads>` runs `AUTO` instead.
|
||||
- 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: 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.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. Game version is 26.2 for the three loader jars; the plugin jar covers the whole `bukkitMinecraftRange` (26.1.2 and 26.2), so tag both.
|
||||
- [ ] 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.1.2-26.2.jar`) where the plugin already ships.
|
||||
- [ ] Sentry: register the release so incoming reports map to this version (`irisVersion` is the Sentry release tag). `./gradlew release` does this against `sentry.volmit.com` — it downloads `sentry-cli`, runs `releases new` and `releases set-commits --auto`, and needs `SENTRY_AUTH_TOKEN` (or `-Psentry.auth.token=`). It does not finalize the release; do that by hand if needed.
|
||||
- [ ] Storepage staleness review: check the portal listing copy and the `storepage/` images for pre-4.0 content (Bukkit-only framing, old feature lists, stale 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 and push the tag. Existing tags in this repo carry no `v` prefix and encode the supported MC span (`3.9.2-1.20.1-1.21.11`), so pick the tag deliberately rather than assuming a format. 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,347 +0,0 @@
|
||||
# 87 - Maintainer — Release Readiness
|
||||
|
||||
**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 the run history 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 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 runbooks (`31 - Operator Runbooks.md`), performance knobs (`33 - Performance Tuning.md`), MC bump (`85 - Maintainer - MC Version Bump.md`).
|
||||
|
||||
## How to maintain this tracker
|
||||
|
||||
Work from the first incomplete blocking section. For each checked item, preserve the exact commit, platform artifact, input pack/seed, command or workload, and result outside this document; summarize only stable conclusions here. When a fix supersedes an earlier note, rewrite the note instead of stacking contradictory history.
|
||||
|
||||
Automated tests, server startup, real-player gameplay, profiler captures, and publishing are distinct checks. Mark only what was actually observed, and leave client-operated or cross-server checks open until they have been run.
|
||||
|
||||
## Completion rules
|
||||
|
||||
- [ ] Work through the sections in order. A later section does not override a failed earlier gate.
|
||||
- [ ] Add a failing regression test or deterministic reproduction before each P0/P1 correctness fix.
|
||||
- [ ] Run the focused test while developing, then run the full gate for the affected platform.
|
||||
- [ ] Compare fixed-pack, fixed-seed golden hashes before and after every world-generation change.
|
||||
- [ ] Treat an unexpected deterministic output change as a release blocker until explained.
|
||||
- [ ] Keep loader-specific behavior behind the platform boundary; reusable behavior belongs in core or SPI.
|
||||
- [ ] Do not add compatibility shims, temporary adapters, or swallowed failure paths.
|
||||
- [ ] Preserve full stack traces for engine, lifecycle, persistence, and operator-critical failures.
|
||||
- [ ] Update `MasterChangelog.MD` (workspace root, `../MasterChangelog.MD` from this repo; section `## Plugin: Iris`) as operator-visible fixes become final; merge superseded entries.
|
||||
- [ ] Do not publish while any required release gate is failed, pending, or waived without an explicit reason.
|
||||
|
||||
## 0. Secure and freeze the release baseline
|
||||
|
||||
- [ ] Rotate the GitHub credential that was embedded in the local origin URL.
|
||||
- [x] Replace the local origin with a credential-free SSH or HTTPS URL.
|
||||
- [ ] Regenerate the dev-server management secret before enabling the management interface.
|
||||
- [ ] Select and record the exact release commit, Minecraft version, JDK, and loader versions.
|
||||
- [x] Pin VolmLib to an immutable release/tag/commit rather than `master-SNAPSHOT`.
|
||||
- [x] Ensure `useLocalVolmLib` and `volmLibCoordinate` propagate into every nested adapter build.
|
||||
- [x] Make the manual release build disable local VolmLib substitution by default.
|
||||
- [x] Capture a baseline build and test record, running these as separate invocations:
|
||||
- [x] `./gradlew :core:check :spi:build :probe:deserializationProbe -PuseLocalVolmLib=false`
|
||||
- [x] `./gradlew :adapters:bukkit:plugin:test --rerun-tasks -PuseLocalVolmLib=false`
|
||||
- [x] `./build-all.sh`
|
||||
- [x] Confirm all four baseline jars pass archive integrity checks.
|
||||
- [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.
|
||||
|
||||
Done when: the source, dependencies, generated terrain baseline, and test results 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.
|
||||
- [ ] 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.
|
||||
- [x] Remove the shared mutable `chunkContext`/session state from the engine-wide `IrisContext` path.
|
||||
- [x] Give each active generation thread or lease an isolated context with explicit lifetime cleanup.
|
||||
- [ ] Verify maintenance, pregen, Bukkit multicore, and modded generation use the isolated context.
|
||||
- [ ] 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.
|
||||
|
||||
Done when: 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.
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] Build candidate dimension, loader, complex, mode, mantle, and world-manager state privately.
|
||||
- [ ] Validate the complete candidate before changing the live engine.
|
||||
- [ ] Seal new generation and drain active leases before clearing or replacing live resources.
|
||||
- [ ] Publish the validated candidate atomically, then activate the next generation session.
|
||||
- [ ] Keep the previous engine state intact when candidate loading or setup fails.
|
||||
- [ ] Make `setupEngine()` fail closed and propagate fatal initialization failures.
|
||||
- [ ] Route `hotloadComplex()` through the same generation-session and transactional rules.
|
||||
- [ ] Ensure Bukkit exclusive-control permits are released after success, failure, and interruption.
|
||||
- [ ] 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.
|
||||
|
||||
Done when: 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.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
- [ ] Implement the modded import/unpack path or explicitly remove the unsupported claim from the UI/docs.
|
||||
- [ ] Add a minimal pack fixture containing at least one resource from every supported category.
|
||||
- [ ] Add Bukkit export -> import -> export round-trip tests.
|
||||
- [ ] Add modded export -> import -> export round-trip tests.
|
||||
- [ ] Compare normalized JSON, binary objects, dependency counts, and final resource hashes.
|
||||
|
||||
Done when: 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
|
||||
|
||||
- [ ] Add a test cell that crosses multiple chunks and multiple Folia regions.
|
||||
- [ ] Capture each chunk/region snapshot only on its owning region thread.
|
||||
- [ ] Assemble the final `IrisObject` only after all owned snapshots complete successfully.
|
||||
- [ ] Serialize the object once and reuse the bytes for hashing and persistence.
|
||||
- [ ] Write to a temporary file, flush/close it, then atomically move it over the destination.
|
||||
- [ ] Commit the saved hash only after the atomic move succeeds.
|
||||
- [ ] Leave the prior hash and file untouched after capture, serialization, or write failure.
|
||||
- [ ] 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.
|
||||
|
||||
Done when: 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
|
||||
|
||||
- [x] Split `PackValidator` into a read-only validator and an explicit cleanup command.
|
||||
- [x] Make unused-resource cleanup preview changes before moving files.
|
||||
- [x] Prevent restore from overwriting a newer live file without an explicit conflict decision.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
- [ ] Make the schema executor lifecycle-owned and restartable after Bukkit reload and integrated-server stop/start.
|
||||
- [ ] Add validator tests for nested resources, malformed dependencies, cleanup preview, and restore conflicts.
|
||||
- [ ] Add schema tests for vanilla shorthand and fully namespaced modded values.
|
||||
|
||||
Done when: 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
|
||||
|
||||
- [ ] Make generation-session teardown cancel/retry the chunk stage instead of completing an empty chunk.
|
||||
- [ ] Add a test proving a sealed engine cannot persist a blank chunk.
|
||||
- [ ] Bound the modded chunk-generation queue and expose queue/backpressure metrics.
|
||||
- [ ] Complete or cancel every queued future during shutdown; leave no unresolved chunk pipeline.
|
||||
- [ ] Stop and await maintenance work before closing the engines it can access.
|
||||
- [ ] Await executor termination and report tasks that exceed the shutdown deadline.
|
||||
- [ ] Add negative-min-Y tests for sea level, base height, and base-column stone/water/air spans.
|
||||
- [ ] Verify custom biome cache invalidation after a successful Studio hotload.
|
||||
- [ ] 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.
|
||||
- [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.
|
||||
|
||||
Done when: 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
|
||||
|
||||
- [ ] Remove direct world/chunk/IO fallback when a Folia region scheduling call fails.
|
||||
- [ ] Retry, defer, or fail the operation without touching region-owned state from the wrong thread.
|
||||
- [ ] Wrap pregenerator initialization and total-count calculation in the cleanup 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.
|
||||
- [ ] 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.
|
||||
- [ ] Verify chunk tickets, regions, files, protocol sessions, and executor threads are released afterward.
|
||||
|
||||
Done when: pregeneration remains thread-correct and bounded under saturation, cancellation, failure, and restart.
|
||||
|
||||
## 8. Performance and regression proof
|
||||
|
||||
### Current verification coverage
|
||||
|
||||
These runs validate packaged-artifact generation and establish a profiling candidate. They are not the final 5,000-10,000-chunk performance baseline required by this section. They also predate the current `gradle.properties` loader pins on two platforms — captured against Forge `65.0.3` and NeoForge `26.2.0.8-beta`, while the tree now pins `forgeVersion=26.2-65.0.4` and `neoForgeVersion=26.2.0.12-beta` — so re-run those two loaders before treating any of it as valid for the shipping artifacts.
|
||||
|
||||
- [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.
|
||||
|
||||
- [ ] Choose one fixed release pack, seed, world height, radius, JVM configuration, and hardware profile.
|
||||
- [ ] Warm at least 256 chunks before measuring.
|
||||
- [ ] Run a 5,000-10,000 chunk pregeneration baseline on Paper.
|
||||
- [ ] Run the same workload on Fabric; repeat on Forge and NeoForge before final release.
|
||||
- [ ] Capture JProfiler CPU, allocation, GC, retained-object, thread, and executor-queue profiles.
|
||||
- [ ] Record chunks/second, total duration, p50/p95 chunk time, allocations/chunk, peak heap, and GC pause time.
|
||||
- [ ] Profile nested chunk prefill parallelism before changing it.
|
||||
- [ ] 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.
|
||||
- [ ] Confirm optimized and baseline runs produce identical golden hashes where behavior should be unchanged.
|
||||
|
||||
Done when: 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.
|
||||
|
||||
- [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.
|
||||
- [ ] Move the core Bukkit purity ratchet below its current ceiling. `core/purity-allowlist.txt` holds 188 entries; `:core:bukkitPurityRatchet` (wired into `:core:check`) fails on any new `org.bukkit`-coupled file outside it and prints how far under the ceiling the tree sits.
|
||||
- [ ] 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.
|
||||
|
||||
Done when: 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 runs completed so far prove 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
|
||||
- [ ] Purpur current target
|
||||
- [ ] Folia current target
|
||||
- [ ] Spigot/CraftBukkit if still advertised as supported
|
||||
- [ ] Mod-loader matrix:
|
||||
- [ ] Fabric declared minimum loader
|
||||
- [ ] Fabric latest compatible loader
|
||||
- [ ] Forge declared minimum loader
|
||||
- [ ] Forge latest compatible loader
|
||||
- [ ] NeoForge declared minimum loader
|
||||
- [ ] NeoForge latest compatible loader
|
||||
- [ ] On every server target:
|
||||
- [ ] Fresh Iris world creation and non-empty chunk generation
|
||||
- [ ] Existing Iris world restart and new-chunk generation
|
||||
- [ ] Custom biome registration and client synchronization
|
||||
- [ ] Structures, objects, loot, spawners, and entities
|
||||
- [ ] Golden-hash match for the shared pack/seed
|
||||
- [ ] 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.
|
||||
- [ ] Client matrix:
|
||||
- [ ] Modded Iris server + Iris client mod
|
||||
- [ ] Modded Iris server + client without Iris where loader rules permit
|
||||
- [ ] Bukkit Iris server + Iris client mod over plugin messaging
|
||||
- [ ] Non-Iris server + Iris client mod remains inert
|
||||
- [ ] Integrated singleplayer create, leave, and create/join again in the same client process
|
||||
- [ ] Pregen HUD, Vision map, cursor overlay, keybinds, and Studio toasts
|
||||
|
||||
Done when: 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
|
||||
|
||||
- [x] Make runtime splash/version identity match the artifact version; remove stale `4.0 RC.1.1.6` text.
|
||||
- [x] Correct README pregen syntax, including the required radius.
|
||||
- [ ] Document how to select an Iris world preset on each mod loader.
|
||||
- [ ] Distinguish automatic pack installation from automatic Iris main-world selection.
|
||||
- [ ] Publish an accurate Bukkit-versus-modded Studio capability matrix.
|
||||
- [ ] Document intentional entity-spawn and tooling differences that remain.
|
||||
- [x] Remove tracked generated SIMD benchmark `.class` files and jar outputs.
|
||||
- [x] Keep generated server worlds, caches, credentials, and build artifacts ignored.
|
||||
- [ ] Consolidate the Iris section of `MasterChangelog.MD` to the final shipped behavior.
|
||||
- [ ] Review store/listing copy, screenshots, commands, supported platforms, and Java requirements.
|
||||
- [ ] Write release notes with upgrade instructions, known limitations, and rollback guidance.
|
||||
|
||||
### Confirmed release blockers and follow-ups
|
||||
|
||||
- [ ] Publish and retain anonymously downloadable `beta` assets for both managed pack repositories before shipping a build that requires dual bootstrap. Runtime uses the mutable Overworld and Underworld beta release URLs; record each downloaded asset checksum for a release baseline, and move production installs to immutable release inputs when the beta streams are promoted.
|
||||
- [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 trusting it for a release: 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 does not count.
|
||||
- [ ] 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.
|
||||
|
||||
Done when: documentation and distribution metadata describe the behavior users will actually receive.
|
||||
|
||||
## 12. Final GO/NO-GO gate
|
||||
|
||||
- [x] `unit-tests`: pass.
|
||||
- [ ] `qa-validation`: pass across the full matrix.
|
||||
- [ ] `edge-case-review`: pass or all remaining risks explicitly accepted.
|
||||
- [ ] `perf-regression`: pass against the recorded baseline.
|
||||
- [ ] `release-dry-run`: pass using final packaged artifacts.
|
||||
- [ ] `changelog-ready`: pass.
|
||||
- [x] `manual-runbooks`: pass.
|
||||
- [ ] `docs-updated`: pass.
|
||||
- [ ] `known-issues-reviewed`: pass.
|
||||
- [ ] Working tree is clean on the exact release commit.
|
||||
- [ ] CI is green on that commit and all supporting artifacts are retained.
|
||||
- [ ] Complete every item in `86 - Maintainer - Release Checklist.md` without rebuilding from different source.
|
||||
|
||||
Release decision:
|
||||
|
||||
- [ ] **GO** - every required check passes and no unresolved warning remains.
|
||||
- [ ] **GO-WARN** - every required check passes and each warning is documented and explicitly accepted.
|
||||
- [x] **NO-GO** - any required check fails or remains pending.
|
||||
|
||||
## 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] 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] 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] 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] 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] 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.
|
||||
|
||||
These completed items remain subject to the final packaged-artifact, Bukkit/Folia, concurrency, and performance gates above.
|
||||
@@ -1,174 +0,0 @@
|
||||
# 90 - API - Getting Started
|
||||
|
||||
`art.arcane.iris.api` is the Bukkit plugin surface another plugin compiles against: terrain reads, world-engine and pregen observation, and tree-feller integration. It is built from `java.*`/`javax.*`, Bukkit types, and its own types only — no VolmLib, Adventure, or shaded types — so it links against a plain Spigot or Paper compile classpath. A build test walks every class in the package and fails if any exported signature mentions anything else. PlaceholderAPI keys are operator-facing, not compile-time: see `09 - PlaceholderAPI.md`.
|
||||
|
||||
Reach for this API when your plugin needs to know what Iris *will* generate before the server generates it — a map renderer, a spawn or settlement picker, a pregen planner, a HUD that names the pack biome — or when you need to act at the moment an Iris world's generator becomes usable or goes away. Everything here is read-only except the tree feller, which you can drive and charge.
|
||||
|
||||
| Package | Purpose | Document |
|
||||
|---|---|---|
|
||||
| `art.arcane.iris.api.terrain` | Generator opinion at a coordinate: Iris world?, biome, region, surface height/kind | `91 - API - Terrain.md` |
|
||||
| `art.arcane.iris.api.world` | Engine ready / hotloaded / closing | `92 - API - World Events.md` |
|
||||
| `art.arcane.iris.api.pregen` | Pregeneration job progress | `92 - API - World Events.md` |
|
||||
| `art.arcane.iris.api.tree` | Drive and charge the tree feller | `93 - API - Tree Feller.md` |
|
||||
|
||||
Writing a **mod** rather than a plugin? Fabric, Forge, and NeoForge jars expose `art.arcane.iris.modded.api` instead: `94 - API - Modded.md`.
|
||||
|
||||
Anything outside `art.arcane.iris.api` is internal. `art.arcane.iris.core.*`, `art.arcane.iris.engine.*`, `art.arcane.iris.util.*`, and `art.arcane.iris.spi.*` change without notice. The separately built SPI jar is for Iris platform adapters, not downstream plugin integrations. Importing `Engine`, `IrisBiome`, or `IrisToolbelt` means you are outside the stable contract.
|
||||
|
||||
---
|
||||
|
||||
## Platform limitation
|
||||
|
||||
`art.arcane.iris.api` ships in the **Bukkit plugin jar only**. Fabric, Forge, and NeoForge mod jars carry the generator but not this package — there is no Bukkit `World`, `ServicesManager`, or `Event` bus to hang it on.
|
||||
|
||||
The mod jars carry `art.arcane.iris.modded.api` (`94 - API - Modded.md`): detect Iris levels, drive pregeneration, read/write mantle data, and register providers so packs can place mod blocks, items, and mobs. It is absent from the Bukkit jar and shares no types with `art.arcane.iris.api`.
|
||||
|
||||
Everything in these API docs assumes Paper, Purpur, Leaf, Canvas, Folia, or Spigot; Minecraft 26.2; Java 25.
|
||||
|
||||
---
|
||||
|
||||
## Depending on Iris
|
||||
|
||||
Iris is not published to Maven Central. Two routes work.
|
||||
|
||||
**Against the jar you already have.** The jar you compile against is the jar you run against.
|
||||
|
||||
```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.
|
||||
|
||||
A complete integration — resolve lazily, handle `null`, answer:
|
||||
|
||||
```java
|
||||
package com.example.integration;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisTerrainService;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class ExamplePlugin extends JavaPlugin {
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage("Players only.");
|
||||
return true;
|
||||
}
|
||||
|
||||
IrisTerrainService terrain = terrain();
|
||||
|
||||
if (terrain == null) {
|
||||
player.sendMessage("Iris is not installed or not enabled.");
|
||||
return true;
|
||||
}
|
||||
|
||||
player.sendMessage(terrain.surfaceBiomeName(
|
||||
player.getWorld(),
|
||||
player.getLocation().getBlockX(),
|
||||
player.getLocation().getBlockZ()).orElse("not an Iris world"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private IrisTerrainService terrain() {
|
||||
RegisteredServiceProvider<IrisTerrainService> provider =
|
||||
getServer().getServicesManager().getRegistration(IrisTerrainService.class);
|
||||
return provider == null ? null : provider.getProvider();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Resolve on every use, as above, or cache and invalidate on `PluginDisableEvent`. A cached reference after Iris disables does not throw — terrain queries answer absent and tree-feller calls return `false` — but it never becomes useful again, and a later enable registers a different object.
|
||||
|
||||
Neither service is a functional interface and neither is meant for third-party implementation. `ServicesManager#getRegistration` returns the highest-priority registration; registering your own `IrisTerrainService` above `Normal` shadows Iris for every other plugin. It does not shadow Iris for Iris itself (internal registry), so PlaceholderAPI would still read the real service while other plugins would not.
|
||||
|
||||
---
|
||||
|
||||
## The shared library is not relocated
|
||||
|
||||
Iris bundles `art.arcane.volmlib` **unrelocated**. Sibling Volmit plugins may relocate it (Adapt → `art.arcane.adapt.util.arcane.volmlib`, React → `art.arcane.react.util.arcane.volmlib`). Consequences:
|
||||
|
||||
1. **You do not need VolmLib to use this API.** No type in `art.arcane.iris.api` mentions it.
|
||||
2. **If you use VolmLib yourself, shade and relocate your own copy.** Do not bind to Iris's version via `join-classpath`.
|
||||
3. **A relocated sibling and Iris do not share those classes.** Never pass objects across relocated package boundaries.
|
||||
|
||||
---
|
||||
|
||||
## Threading, at a glance
|
||||
|
||||
This suite runs on Folia (region threads own chunks; entity schedulers own entities). Each document states its contract; summary:
|
||||
|
||||
| Call | Which thread may call it | Where the callback lands |
|
||||
|---|---|---|
|
||||
| Every `IrisTerrainService` read | Any thread, including async | Returns inline |
|
||||
| `IrisColumnSink.accept` | — | The thread that called `sampleColumns` |
|
||||
| `IrisTreeFellerService.tryFell` | The region thread delivering the `BlockBreakEvent` | Returns inline |
|
||||
| `IrisTreeFellerService.isManagedBreak` | Any thread | Returns inline |
|
||||
| `IrisTreeFellerService.isTreeBlock` | The region thread owning the block; can block on disk — see `93 - API - Tree Feller.md` | Returns inline |
|
||||
| `TreeFellerRunHooks.onActivationAccepted` | — | Region thread that owns the broken block |
|
||||
| `TreeFellerRunHooks.reserveLogCost` / `commitLogCost` / `refundLogCost` | — | Player entity scheduler on Folia; may run inline on the server main thread on Paper when already primary |
|
||||
| `IrisWorldEngineEvent` handlers | — | Main thread; on Folia, the global region thread |
|
||||
| `IrisPregenerationEvent` handlers | — | Main thread; on Folia, the global region thread |
|
||||
|
||||
Terrain reads may use any thread because they only read the world generator reference and evaluate cached procedural noise — no chunk, block state, entity, or mantle storage. See `91 - API - Terrain.md`. That claim does not apply to the rest of this API.
|
||||
|
||||
---
|
||||
|
||||
## Switching over the enums
|
||||
|
||||
`IrisSurfaceKind`, `IrisColumnField`, `IrisWorldPhase`, `IrisPregenPhase`, and `TreeFellerAccess` may gain constants. A `switch` **expression** without `default` stops compiling (and throws `IncompatibleClassChangeError` on an already-compiled jar) when a constant is added.
|
||||
|
||||
Always write a `default` arm in third-party code:
|
||||
|
||||
```java
|
||||
String label = switch (kind) {
|
||||
case LAND -> "land";
|
||||
case OCEAN -> "water";
|
||||
default -> "";
|
||||
};
|
||||
```
|
||||
@@ -1,443 +0,0 @@
|
||||
# 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.
|
||||
|
||||
Reach for it when you need the pack's intent for a coordinate without paying to generate it: picking a base or settlement site, rendering a map or minimap, planning a pregen region, gating a feature on "is this an Iris world", or labelling a HUD with the pack biome. If you need the blocks a player can actually see — including objects, structures, and edits — you want Bukkit's world API instead.
|
||||
|
||||
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`.
|
||||
|
||||
Null `fields` throws `NullPointerException`.
|
||||
|
||||
`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` | surface 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.
|
||||
|
||||
The sink's `biomeKey` is the **surface** biome — the same value `surfaceBiomeKey` returns for that column, never a cave biome. There is no 3D equivalent of `biomeKey(world, x, y, z)` in a column walk.
|
||||
|
||||
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.
|
||||
|
||||
`Player#getScheduler()` is Paper/Folia only. On Spigot, hop back with `Bukkit.getScheduler().runTask(plugin, () -> player.sendMessage(result))`.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
@@ -1,377 +0,0 @@
|
||||
# 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.
|
||||
|
||||
Reach for these when your plugin has per-world setup that must happen exactly when Iris can answer for that world — caching the dimension key, building a map layer, warming a spawn candidate list — or when you want to mirror pregeneration progress somewhere Iris does not draw it (a boss bar, a web panel, a Discord relay).
|
||||
|
||||
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.
|
||||
|
||||
Closing is also not a crash guarantee. It is delivered on the normal plugin-disable path, where teardown runs on the main thread and the event is called inline. If the JVM exits without a clean plugin disable, Iris's shutdown hook still parks the generators, but nothing dispatches to your listener — persist anything you cannot rebuild as you go, not at closing.
|
||||
|
||||
---
|
||||
|
||||
## 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 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 -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register it from your `onEnable`:
|
||||
|
||||
```java
|
||||
getServer().getPluginManager().registerEvents(new IrisWorldRegistry(), this);
|
||||
```
|
||||
|
||||
`default` is required because enums can grow: `90 - API - Getting Started.md`. Map is concurrent because readers may be off the event thread.
|
||||
|
||||
Iris also fires the internal `art.arcane.iris.core.events.IrisEngineHotloadEvent` alongside `ENGINE_HOTLOADED`. It exposes the internal `Engine` type, is not covered by the API purity test, and can change without notice — listen for `IrisWorldEngineEvent` instead.
|
||||
|
||||
---
|
||||
|
||||
## 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` early in a job: below 1024 generated chunks it needs a non-zero rolling chunks/second average, and above that it extrapolates from elapsed time per generated chunk. 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 job has no bound world | No event for any phase of that job; a null `worldIdentity` is dropped at the source |
|
||||
| Pregen handler throws | Logged; job not slowed/paused/stopped |
|
||||
| Iris shuts down mid-pregen | Terminal phase is `CANCELLED`, but delivery is not guaranteed — the event is scheduled onto the main thread and sync scheduling refuses once the plugin is disabled |
|
||||
| Iris shuts down with worlds registered | Every announced world gets `ENGINE_CLOSING` before worker drain, on the clean disable path |
|
||||
|
||||
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`.
|
||||
@@ -1,434 +0,0 @@
|
||||
# 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. Reach for this package when a skill, class, or economy system should decide who may fell and what each log costs — the integration 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`, and `INTEGRATION_OVERRIDE` bypasses both.
|
||||
|
||||
Build and service acquisition: `90 - API - Getting Started.md`. Service: `IrisTreeFellerService` at `ServicePriority.Normal`.
|
||||
|
||||
This package is Bukkit-only. Mod loaders run the same feller as a player-facing feature (settings plus the `irisworldgen:treefeller` node) but expose no integration API — see `30 - Platform Differences.md` and `94 - API - Modded.md`.
|
||||
|
||||
| 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. The claim is keyed by world plus tree marker: a second player on the same tree gets the break cancelled with drops suppressed and no hooks.
|
||||
- **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; runs inline on Paper when already on the main thread |
|
||||
| `commitLogCost` | Player entity path; inline when the caller already owns that thread, otherwise scheduled onto the player |
|
||||
| `refundLogCost` | Same as commit, 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.
|
||||
|
||||
```java
|
||||
package com.example.woodcutting;
|
||||
|
||||
import art.arcane.iris.api.tree.IrisTreeFellerService;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
|
||||
public final class TreeProbeListener implements Listener {
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInspect(PlayerInteractEvent event) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block block = event.getClickedBlock();
|
||||
IrisTreeFellerService feller = FellerAccess.service();
|
||||
|
||||
if (block == null || feller == null || !feller.isTreeBlock(block)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.getPlayer().sendMessage("Iris tree — sneak and break a log with an axe to fell it.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`isTreeBlock` answers provenance only. It does not check gamemode, sneak, or the held item, and it returns true for leaves as well as logs.
|
||||
|
||||
---
|
||||
|
||||
## 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 (identity, not `equals`).
|
||||
|
||||
| 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, off the main thread. 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 |
|
||||
|
||||
Only a same-marker block found beyond the axis limit aborts discovery; neighbours outside world height are skipped without marking it incomplete. Discovery also falls back to the trigger block if a member's chunk is unloaded during preflight.
|
||||
|
||||
Removal is paced in batches with tick yields: Iris targets about 60 pulses per run and clamps the batch to 4–64 blocks per pulse, so 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 collects per-block drops and XP instead, merges equal stacks, and delivers them at the feller's feet in batches; if the player logs out or leaves the world, delivery falls back to the location where the run started.
|
||||
|
||||
---
|
||||
|
||||
## 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 service disabled mid-run (plugin disable) | 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 |
|
||||
|
||||
Both keys are read per request, so `/iris reload` takes effect on the next break. Turning `treeFeller.enabled` off does not stop a run already in flight on Bukkit.
|
||||
|
||||
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`.
|
||||
@@ -1,491 +0,0 @@
|
||||
# 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. Reach for it when your mod's content has to appear in Iris-generated terrain, or when it needs to know whether a `ServerLevel` is Iris at all. It ships only in the mod jars, is absent from the Bukkit plugin jar, and shares no types with `art.arcane.iris.api`.
|
||||
|
||||
Assumes Minecraft 26.2, Java 25, and Fabric, Forge, or NeoForge. Mod id is `irisworldgen` on all three.
|
||||
|
||||
The Bukkit terrain, world-event, pregen-event, and tree-feller **services** have no counterpart here — see `90 - API - Getting Started.md` through `93 - API - Tree Feller.md` for that surface. The tree feller itself does run on mod loaders as a player-facing feature (settings plus the `irisworldgen:treefeller` node), but exposes no integration hooks.
|
||||
|
||||
| Goal | Entry |
|
||||
|---|---|
|
||||
| Detect Iris, read engine token, start pregen, mantle R/W | `IrisModdedAPI` |
|
||||
| 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 task shells out to the adapter's own wrapper and copies the jar into `dist/`. The loader segment is that adapter's loader version verbatim — for example `26.2+0.19.3` on Fabric and `26.2+26.2.0.12-beta` on NeoForge.
|
||||
|
||||
Each adapter is a standalone Gradle build (`adapters/<loader>/settings.gradle` `includeBuild('../..')` plus a dependency substitution for `art.arcane:core` and `art.arcane:spi`). Compile against the jar you run:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
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 centerBlockX, int centerBlockZ, boolean sync, boolean cached)` | Same with centre and write mode |
|
||||
| `getMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Read per-block mantle |
|
||||
| `setMantleData(ServerLevel, int x, int y, int z, T)` | Write (creates region; can touch disk) |
|
||||
| `deleteMantleData(ServerLevel, int x, int y, int z, Class<T>)` | Remove type at position (creates region) |
|
||||
| `retainMantleDataForSlice(Class<?>)` | Mark a mantle slice type as retained |
|
||||
| `registerProvider(ModdedDataProvider)` | Imperative provider registration |
|
||||
| `registerCustomBlockData(String namespace, String key, String state)` | Static block-state alias |
|
||||
|
||||
### Threading (API entry)
|
||||
|
||||
| Method | Thread guidance |
|
||||
|---|---|
|
||||
| `isIrisLevel` | Any thread; a reference read on the chunk source |
|
||||
| `isStudioLevel`, `getEngine` | Any thread, but not free — see below |
|
||||
| Mantle get/set/delete | Safe off server thread but touch engine storage; writes can disk I/O |
|
||||
| `pregenerate`, `registerProvider` | Server thread / mod setup / command |
|
||||
| `retainMantleDataForSlice`, `registerCustomBlockData` | Mod setup |
|
||||
|
||||
### `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.
|
||||
|
||||
`getEngine` is **not** a plain field read: for a level whose engine is not bound yet it binds one, which loads the pack and builds the biome complex under the generator's monitor. `isIrisLevel` is the cheap probe; use it first and call `getEngine` only when you need the token. `isStudioLevel` and every mantle accessor route through `getEngine` and inherit this.
|
||||
|
||||
Never cache an `Engine`. Pack hotload or level unload replaces it. `getEngine` returns null while binding fails and during shutdown.
|
||||
|
||||
### Pregeneration
|
||||
|
||||
Returns as soon as the job is queued. Progress is Iris logging and boss bar only — no caller callback (no modded equivalent of `IrisPregenerationEvent`; see limitations). One job server-wide; returns `false` if a job is already active **or** the level is not Iris — check `isIrisLevel` first to distinguish. Call on the server thread.
|
||||
|
||||
`radiusBlocks` is the half-extent of a square measured from the centre. `cached = true` uses the on-disk pregen cache under `<world>/iris/pregen` for resume. `sync = true` writes chunks synchronously (slower; bypasses async write queue). The simple overload is `pregenerate(level, radius, 0, 0, false, true)`.
|
||||
|
||||
Operator pregen concepts: `07 - Pregeneration.md`. Platform matrix: `30 - Platform Differences.md`.
|
||||
|
||||
### Mantle data
|
||||
|
||||
The mantle is Iris's per-block sidecar, independent of chunk NBT.
|
||||
|
||||
1. **Coordinates are world-space.** `y` is translated by engine min height internally. Out-of-range `y` reads null / writes no-op.
|
||||
2. **Reads never create storage; writes do.** `getMantleData` returns null when no mantle region exists for that column. `set`/`delete` create the region (possible disk).
|
||||
3. **Value types are fixed.** The mantle stores values in typed slices, and only registered slice types exist. A type with no slice raises `IllegalArgumentException: Unsupported matter slice type <name>` — from `set`/`delete` always, and from `get` once a mantle region exists for that column. Iris's own slice types are internal and must not be written by a mod.
|
||||
|
||||
Types a mod can use:
|
||||
|
||||
| Type | Survives normal generation | Survives pregeneration |
|
||||
|---|---|---|
|
||||
| `Boolean`, `Integer`, `Long` | Yes | Yes |
|
||||
| `String` | Yes | **No** — pregen's chunk cleanup deletes the `String` slice |
|
||||
|
||||
`retainMantleDataForSlice(Class<?>)` records a canonical class name in a process-wide, irreversible set. It guards `Mantle.deleteChunkSlice`, which Iris does not currently call; the per-chunk cleanup that runs after generation deletes a fixed slice list and does not consult it. Declaring a type is therefore harmless insurance, not a guarantee — do not rely on it to keep `String` values through a pregen.
|
||||
|
||||
All three mantle accessors throw `IllegalStateException` if the engine mantle is already closed.
|
||||
|
||||
### Worked example: setup, pregen, mantle
|
||||
|
||||
```java
|
||||
package com.example.yourmod.iris;
|
||||
|
||||
import art.arcane.iris.modded.api.IrisModdedAPI;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
public final class IrisBridge {
|
||||
private IrisBridge() {
|
||||
}
|
||||
|
||||
/** Mod setup, before any level resolves a pack key. */
|
||||
public static void onModSetup() {
|
||||
IrisModdedAPI.registerProvider(new YourIrisProvider());
|
||||
IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]");
|
||||
IrisModdedAPI.retainMantleDataForSlice(Integer.class);
|
||||
}
|
||||
|
||||
/** Server thread. False when the level is not Iris or a pregen job is already running. */
|
||||
public static boolean warmUp(ServerLevel level, int radiusBlocks) {
|
||||
if (!IrisModdedAPI.isIrisLevel(level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return IrisModdedAPI.pregenerate(level, radiusBlocks);
|
||||
}
|
||||
|
||||
public static void markShrineTier(ServerLevel level, int x, int y, int z, int tier) {
|
||||
IrisModdedAPI.setMantleData(level, x, y, z, tier);
|
||||
}
|
||||
|
||||
public static int shrineTier(ServerLevel level, int x, int y, int z) {
|
||||
Integer tier = IrisModdedAPI.getMantleData(level, x, y, z, Integer.class);
|
||||
return tier == null ? 0 : tier;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `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
|
||||
}
|
||||
```
|
||||
|
||||
`Identifier` is `net.minecraft.resources.Identifier`. `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 the parsed `[prop=value]` map, never null, possibly empty. Null return declines (next provider, then air). `ModdedBlockData.direct(state)` for final; `deferred(placeholder)` for a second pass.
|
||||
- `processBlockPlacement` — finishes deferred placement on the server thread with the chunk loaded. Only the first **ready** provider claiming the id runs for a position. The placeholder should match the final block's shape and occlusion.
|
||||
- `spawnMob` — server thread. **You must add the entity to the level yourself**; Iris uses the returned reference as-is and does not call `addFreshEntity`. Null declines. Iris applies pack entity settings (name, attributes, loot, passengers) to the result only when the pack sets `applySettingsToCustomMobAnyways`.
|
||||
- `init()` — once after accept.
|
||||
|
||||
`ModdedBlockPlacementContext` record: `engine`, `level`, `position`, `blockId`, `state` (defensive copy, unmodifiable), `blockState` (state currently at the position). Immutable; constructed by Iris; every component non-null. `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 | |
|
||||
|
||||
### Worked example: provider
|
||||
|
||||
```java
|
||||
package com.example.yourmod.iris;
|
||||
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.modded.api.ModdedBlockPlacementContext;
|
||||
import art.arcane.iris.modded.api.ModdedDataProvider;
|
||||
import art.arcane.iris.modded.api.ModdedDataType;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class YourIrisProvider implements ModdedDataProvider {
|
||||
private static final String NAMESPACE = "yourmod";
|
||||
private static final Identifier ALTAR = Identifier.parse("yourmod:altar");
|
||||
|
||||
@Override
|
||||
public String modId() {
|
||||
return NAMESPACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Identifier> getTypes(ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK ? List.of(ALTAR) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(Identifier id, ModdedDataType type) {
|
||||
return NAMESPACE.equals(id.getNamespace());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
|
||||
if (!ALTAR.equals(blockId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The altar carries a block entity, so write a solid placeholder now and
|
||||
// finish once the chunk is loaded.
|
||||
return ModdedBlockData.deferred(Blocks.COBBLESTONE.defaultBlockState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
Block altar = BuiltInRegistries.BLOCK.getValue(ALTAR);
|
||||
|
||||
context.level().setBlock(
|
||||
context.position(),
|
||||
altar.defaultBlockState(),
|
||||
Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
|
||||
EntityType<?> type = BuiltInRegistries.ENTITY_TYPE.getValue(entityId);
|
||||
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// EntityType.spawn adds the entity to the level and returns it.
|
||||
return type.spawn(level, BlockPos.containing(x, y, z), EntitySpawnReason.NATURAL);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ServiceLoader registration
|
||||
|
||||
`META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider` — binary names, one per line. Nested classes use `$`. Public no-arg constructor required.
|
||||
|
||||
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.bind()`, which `ModdedEngineBootstrap.bootCommon(...)` calls at loader entry (`IrisFabricBootstrap.onInitialize`, the Forge and NeoForge bootstrap constructors) — before chunk-generator registration and long before a server starts.
|
||||
|
||||
- ServiceLoader providers are available before any world resolves blocks.
|
||||
- Discovery once per process; a second `discover()` is a no-op returning an inert handle.
|
||||
- `init()` must not assume a server, level, or full game registry. Gate late work with `isReady()`.
|
||||
|
||||
### Imperative registration
|
||||
|
||||
`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'
|
||||
Iris registered custom block data yourmod:fancy_log -> minecraft:oak_log[axis=y]
|
||||
```
|
||||
|
||||
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 listing BLOCK types
|
||||
Iris custom content provider 'yourmod' failed to initialize
|
||||
```
|
||||
|
||||
A deferred placement with no ready provider, or an unparseable key, warns and is skipped:
|
||||
|
||||
```
|
||||
Iris deferred custom block placement has no provider for yourmod:thing
|
||||
Iris deferred custom block placement rejected invalid id yourmod:Thing
|
||||
```
|
||||
|
||||
ServiceLoader `init` failure is all-or-nothing: aborts discovery, rolls back that pass, and rethrows. A `RuntimeException` or `Error` propagates unchanged; anything else is wrapped in `IllegalStateException`.
|
||||
|
||||
```
|
||||
Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider)
|
||||
```
|
||||
|
||||
### Static aliases
|
||||
|
||||
```java
|
||||
IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]");
|
||||
```
|
||||
|
||||
`state` is parsed immediately with the vanilla block-state parser against the block registry, so call it after blocks are registered; an invalid identifier or unparseable state is logged and the registration dropped at startup rather than showing up as missing blocks. Aliases are matched on the identifier alone and win over providers for the same key, so `[prop=value]` in a pack key is ignored for an aliased block — the alias's fixed state is what gets written. Null arguments are ignored.
|
||||
|
||||
Go through `IrisModdedAPI`, not `ModdedCustomContentRegistry` resolution methods. `hasProviders()`, `aliasBlockKeys()`, and `providerKeys(ModdedDataType)` on that class are read-only snapshots intended for pack tooling; everything else on it is Iris internal and public only because the adapter's generation code lives in another package.
|
||||
|
||||
---
|
||||
|
||||
## 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.json` and `worlds.json` |
|
||||
|
||||
Pack install root is `config/irisworldgen/packs`, not `config/iris`. Per-level engine state that must travel with the save (`iris-dimensions.json`) lives in the world folder under `<world>/iris/`, not in `config/`. Missing pack at world open is a hard failure with the expected absolute path (no silent vanilla terrain).
|
||||
|
||||
`modded.json` keys: `defaultPack`, `autoDownloadDefaultPack`, `primaryWorld`, `routePlayersToPrimaryWorld`, `mainWorldPack`, `mainWorldSeed`, `mainWorldAutoRestart`.
|
||||
|
||||
When `autoDownloadDefaultPack` is enabled, startup installs missing packs on a daemon thread: the managed beta releases `overworld` and `underworld`, plus a configured non-managed `defaultPack`. Managed names always resolve to their release assets rather than the requested branch. A failure warns with a pointer to `/iris download <pack>`.
|
||||
|
||||
Forced datapack id is `iris_worldgen`; it contributes presets, dimension types, and biomes under the `irisworldgen` namespace (ids derived from pack/dimension names). Regenerated on pack change / studio hotload. Failure to inject (mixin/event not applied) logs once at startup:
|
||||
|
||||
```
|
||||
Iris forced datapack 'iris_worldgen' was never loaded by this server.
|
||||
```
|
||||
|
||||
That is a loader/environment problem, not a pack problem.
|
||||
|
||||
### Commands (modded)
|
||||
|
||||
`/iris` (`/ir`, `/irs`). The root literal is ungated; each subcommand carries its own check — read-only nodes (`version`, `info`, `what`, `height`, `worlds`, `help`) require `LEVEL_ALL`, everything else `LEVEL_GAMEMASTERS` (op level 2 or the equivalent permission). Full tree: `04 - Commands & Permissions.md`. Notable:
|
||||
|
||||
| Command | Role |
|
||||
|---|---|
|
||||
| `/iris pack validate [pack]` | Validate packs on a worker; blank validates all |
|
||||
| `/iris pack status [pack]` | Recorded validation results |
|
||||
| `/iris pack cleanup <pack> [apply]` / `restore <pack>` | Unused resource preview/apply |
|
||||
| `/iris datapack status` | Active vs pack dimension-type heights |
|
||||
| `/iris datapack install` | Write pack dimension type override into world datapacks |
|
||||
| `/iris datapack list` | Configured and installed world datapacks |
|
||||
| `/iris download <pack>` | Install pack (`dl` alias) |
|
||||
| `/iris version` | Version and loader |
|
||||
|
||||
`/iris datapack ingest` (`pull`) and `remove` (`rm`) exist but always refuse on modded — the Modrinth tooling is Bukkit-only. Native/datapack structure placement still works: install into `<world>/datapacks/` and restart. Structures overview: `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
---
|
||||
|
||||
## 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 families with `importedStructures.disabled`, one complete key with `disabledExact`, or scale an exact structure set with `frequencyOverrides` |
|
||||
| Placed features (ores, trees, plants, …) | **Yes**, **off** by default | Dimension `importedFeatures.enabled` |
|
||||
| Carvers | **Never** | `applyCarvers` is an empty override; no noise router or aquifer for vanilla carvers |
|
||||
| Mod biomes as sources | Only as derivative / scatter targets | Iris chooses biomes from the pack |
|
||||
| Mob spawning (incl. mod mobs) | **Yes** | Pack biome table merged ahead of the vanilla derivative's |
|
||||
| Surface builders / rules | **Never** | `buildSurface` is an empty override; pack palettes decide |
|
||||
|
||||
`importedStructures.frequencyOverrides` has Bukkit parity on all three mod loaders. Entries use `{ "structureSet": "namespace:path", "multiplier": 0.01..16 }`; keys are exact registered structure-set keys, last duplicate wins, and changes affect new chunks only. Random-spread sets scale probability first (clamped at 1) and then derive the nearest legal integer spacing, while concentric rings can scale probability only. Sets outside the affected override and exclusion-zone graph are returned untouched; an unsupported placement type that must be copied throws during level structure-state construction, so binding fails rather than a partial override being applied. Full semantics and the Nether `1.1` example are in `22 - Native Structures & Datapacks.md`.
|
||||
|
||||
### `importedFeatures`
|
||||
|
||||
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. The feature pass runs on the worldgen thread owning the chunk (not the Iris gen pool), after Iris structures — early-step features can cut into placed structures. A feature-order cycle detected at bind degrades `importedFeatures` to off with an ERROR log rather than failing the chunk. Same control exists on Bukkit with the same semantics.
|
||||
|
||||
### Carvers
|
||||
|
||||
`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 |
|
||||
| Configured / placed feature | `minecraft:pointed_dripstone` unchanged |
|
||||
| Feature **type** | `minecraft:speleothem` |
|
||||
|
||||
Pack palette / object / `importedFeatures.disabled` keys using the block or placed-feature id remain correct. Only code registering or matching a feature *type* sees the new name.
|
||||
|
||||
### 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** — `ModdedPlatform.callEvent` is an empty method on mod loaders. No modded `IrisWorldEngineEvent` / `IrisPregenerationEvent`; poll `isIrisLevel` / `getEngine`.
|
||||
- **No `art.arcane.iris.api`** in mod jars (verified: zero classes under that package in each loader jar). No modded terrain-query service, and no tree-feller integration API.
|
||||
- **No PlaceholderAPI** — `09 - PlaceholderAPI.md` is Bukkit-only.
|
||||
- **Datapack ingest/remove commands** Bukkit-only.
|
||||
- **Vanilla carvers and surface rules never run.**
|
||||
- **No custom mantle slice types** — the mantle accepts only registered value types.
|
||||
|
||||
Install and platform notes: `01 - Installation & Platforms.md`, `30 - Platform Differences.md`.
|
||||
Reference in New Issue
Block a user