mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-29 13:30:50 +00:00
d
This commit is contained in:
+276
-172
@@ -1,63 +1,287 @@
|
||||
# 06 - Worlds & Lifecycle
|
||||
|
||||
Iris manages world identity, storage paths, pack installation, creation, persistence, and removal across Bukkit-family servers and the three mod loaders. Bukkit-managed Iris worlds live under the level root as `dimensions/iris/<key>/` with namespace `iris`; modded dimensions persist through `iris-dimensions.json`. Non-Studio worlds carry a frozen pack at `iris/pack`, validated by its exact normalized root rather than the common `pack` folder name, while Studio worlds bind the live packs directory.
|
||||
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: `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`.
|
||||
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`.
|
||||
|
||||
## Tutorial: promote a tested pack to a persistent world
|
||||
## Create a world you intend to keep
|
||||
|
||||
Prerequisites: a validated pack, a fixed test seed, a current backup, and no active lifecycle or pack-publish operation. The commands below use the installed `overworld` pack and disposable world name `release_candidate`; substitute one pack key consistently when promoting a different pack.
|
||||
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
|
||||
|
||||
1. Validate the live pack: `/iris pack validate pack=overworld`.
|
||||
2. Open it with `/iris studio open overworld seed=1337`, generate representative terrain, then run `/iris studio close` after the final hotload succeeds.
|
||||
3. Create a new world with explicit identity: `/iris create release_candidate type=overworld seed=1337`.
|
||||
4. On Folia, stop and restart after the staging message. On other Bukkit-family servers, continue after `/iris worlds` lists `release_candidate` as loaded.
|
||||
5. Enter it: `/iris tp release_candidate`.
|
||||
6. Generate a bounded baseline: `/iris pregen start 352 world=release_candidate center=0,0 gui=false`.
|
||||
7. Wait for completion, restart cleanly, return with `/iris tp release_candidate`, and generate one new boundary chunk.
|
||||
```text
|
||||
/iris pack validate pack=overworld
|
||||
/iris studio open overworld seed=1337
|
||||
```
|
||||
|
||||
The workflow passes when the world reloads with the same seed and dimension, the pregenerated area loads without generation failures, and new terrain still comes from `<world>/iris/pack`. Never replace or delete that snapshot while its world is loaded. Continued edits under `packs/overworld/` affect Studio only; publish deliberately through `25 - Pack Management.md` or create a new world for breaking height/type changes.
|
||||
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
|
||||
|
||||
1. Validate the installed pack: `/iris pack validate overworld`.
|
||||
2. Enable a persistent dimension: `/iris world enable irisworldgen:release_candidate overworld 1337`.
|
||||
3. Confirm it in `/iris world status`, then enter it with `/iris tp irisworldgen:release_candidate`.
|
||||
4. Run `/iris pregen start 352 irisworldgen:release_candidate at 0 0` and wait for completion.
|
||||
5. Restart the server. Confirm `/iris world status` restores the same dimension and pack, then run `/iris info irisworldgen:release_candidate` as a gamemaster to verify seed `1337` from `iris-dimensions.json`.
|
||||
```text
|
||||
/iris pack validate overworld
|
||||
/iris world enable irisworldgen:release_candidate overworld 1337
|
||||
/iris world status
|
||||
/iris tp irisworldgen:release_candidate
|
||||
```
|
||||
|
||||
This workflow passes when the dimension is re-injected after restart and generates normally. `/iris world disable` unloads while retaining persistent data; `/iris world delete` is the destructive removal path.
|
||||
The seed argument is optional and defaults to `1337`. `enable` also accepts the alias `create`, and the whole group is reachable as `/iris w`.
|
||||
|
||||
### Lifecycle recovery
|
||||
```text
|
||||
/iris pregen start 352 irisworldgen:release_candidate at 0 0
|
||||
```
|
||||
|
||||
| Symptom | Meaning | Recovery |
|
||||
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 |
|
||||
|---|---|---|
|
||||
| Command reports busy | Another `WORLD_MUTATION` or `PACK_MUTATION` lease owns the lifecycle coordinator | Let that operation finish; do not retry concurrent create/remove/update commands |
|
||||
| Login or create reports startup validation pending/failed/restart-required | External datapacks or dimension-pack validation has not reached a safe state | Fix the first logged failure or complete the requested restart; do not create folders or add `bukkit.yml` entries manually |
|
||||
| Folia create succeeds but teleport cannot find the world | Creation staged files and registration only | Restart, then load/teleport as instructed by the staging result |
|
||||
| Bukkit load reports missing or inconsistent data | Managed dimension root, registration, or `iris/pack` snapshot is incomplete | Keep the directory, restore from backup, and reconcile registration before retrying; load never redownloads the snapshot |
|
||||
| Unload reaches its terminal timeout | World, generator, or scheduler work did not settle within 150 seconds | Allow the requested restart; do not force-delete the live directory |
|
||||
| Remove returns `DELETE_QUEUED` | Files were quarantined for startup deletion | Restart and confirm the target is gone before reusing its name |
|
||||
| Modded registry is quarantined as `.broken-<timestamp>` | Whole-file JSON could not be parsed | Keep the backup, recreate or repair each logged id with the original pack/dimension/seed, then verify status |
|
||||
| "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 | `iris` only for Iris-managed create/load/remove targets |
|
||||
| Logical name | For `iris:foo` the logical name is `foo` |
|
||||
| Storage root | Level root (`Server#getLevelDirectory` on Paper; else `world-container/level-name`) |
|
||||
|---|---|
|
||||
| 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 dir | `<dimensionRoot>/iris/pregen/` |
|
||||
| Registry | `worlds.json` in Iris data + `bukkit.yml` worlds section for production worlds |
|
||||
| Name constraints | Safe single path segment `[a-z0-9_-]+`; no `/`, `\`, `..`; reserved create names `iris` and `benchmark` rejected |
|
||||
| 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 |
|
||||
|
||||
Vanilla main/nether/end map to minecraft keys from `level-name` / `level-name_nether` / `level-name_the_end` and are not Iris-managed dimension folders.
|
||||
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.
|
||||
|
||||
### Modded persistent-dimension registry
|
||||
## 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`:
|
||||
|
||||
@@ -69,146 +293,26 @@ Fabric, Forge, and NeoForge persist dynamic Iris worlds in `<world-root>/iris/ir
|
||||
}
|
||||
```
|
||||
|
||||
`id` is the registered dimension id, `pack` is the installed pack folder, `dimension` is its dimension load key, and `seed` is the generation seed. Writes use a temporary file plus atomic replacement when the filesystem supports it. Invalid individual entries are logged and preserved verbatim during ordinary updates; duplicate ids keep the first valid entry.
|
||||
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.
|
||||
|
||||
If the whole registry cannot be parsed during startup, Iris moves it to `iris-dimensions.json.broken-<timestamp>`, logs any ids it can recover from the raw text, and continues with no persistent Iris dimensions. Keep the quarantined file, repair or recreate each reported world with `/iris world create`, and verify pack/dimension/seed values before deleting the backup.
|
||||
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.
|
||||
|
||||
## Command surface (Bukkit)
|
||||
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.
|
||||
|
||||
| Command | Effect |
|
||||
|---------|--------|
|
||||
| `/iris create <name> [type=default] [seed=1337] [main=false] [overwrite=false]` | Create/Folia-stage a managed world, or stage an exact restart replacement |
|
||||
| `/iris load <name>` / `/iris import <name>` | Load a disk Iris world via reconciler |
|
||||
| `/iris unload <world>` | Evacuate → unload → close generator |
|
||||
| `/iris remove <name> [delete=true]` | Unregister / delete managed world |
|
||||
| `/iris evacuate <world>` | Move players out of the Iris world |
|
||||
| `/iris tp <world> [player=<name>]` | Teleport to world spawn |
|
||||
| `/iris worlds` | List Iris vs non-Iris loaded worlds |
|
||||
## Pack snapshot vs studio
|
||||
|
||||
Full permission table: `04 - Commands & Permissions.md`.
|
||||
|
||||
### Create parameters
|
||||
|
||||
| Param | Default | Notes |
|
||||
|-------|---------|-------|
|
||||
| `name` | required | Normally becomes `iris:<logical>`; with overwrite it may also be the exact configured main, `_nether`, or `_the_end` alias |
|
||||
| `type` | `default` | Pack/dimension selector: `default` → `settings.generator.defaultWorldType` (`overworld`); else pack name or `pack:dimensionKey` |
|
||||
| `seed` | `1337` | World seed; exact vanilla-slot overwrite preserves the existing level's shared authoritative seed instead |
|
||||
| `main` | `false` | Schedule main-world promotion on JVM shutdown (Paper path) or promote during Folia staging |
|
||||
| `overwrite` (`force`) | `false` | Stage a validated exact-slot replacement for the next restart; never deletes a loaded world live |
|
||||
|
||||
Create refuses the primary Bukkit thread. Startup datapack validation must be ready and the selected source pack must have a loadable validation result before the lifecycle lease, datapack preparation, dimension folder, pack snapshot, registration, or Bukkit/NMS create path is entered; lifecycle domain `WORLD_MUTATION` / kind `WORLD_CREATE` must then be free or create fails busy.
|
||||
|
||||
## Production create flow (non-Folia)
|
||||
|
||||
1. Resolve the managed key and dimension without creating the dimension root.
|
||||
2. Require startup datapack readiness and a loadable validation result for the dimension's owning pack.
|
||||
3. Ensure datapacks for the dimension types are installed; queue restart if types not yet loaded.
|
||||
4. Copy the pack into `<world>/iris/pack` (`StudioSVC.installIntoWorld`) — atomic stage → publish; refuses primary thread. Iris invalidates any prior result for that exact root and validates the final published tree before generator creation; failure rolls the publication back.
|
||||
5. Build `WorldCreator` with Iris generator (`studio=false`).
|
||||
6. Create the world through `WorldLifecycleService` / NMS async create (timeout 120s; timeout triggers server restart).
|
||||
7. Register the world in `bukkit.yml` with generator `Iris` dimension key and seed; update the Multiverse link when present.
|
||||
8. Run optional creation-time pregen if a `PregenTask` was attached by the creator API.
|
||||
|
||||
## Folia staging
|
||||
|
||||
Runtime world creation is disabled on Folia. `/iris create` instead:
|
||||
|
||||
1. Requires startup datapack readiness and a loadable validation result for the selected pack; refusal leaves no dimension folder or registration.
|
||||
2. Acquires the `WORLD_CREATE` lease.
|
||||
3. Installs datapacks if changed.
|
||||
4. Stages the pack into the managed dimension root via `installIntoWorld`; the final published snapshot must pass exact-root validation before registration.
|
||||
5. Registers the world in `bukkit.yml` (`BukkitWorldConfiguration.register`).
|
||||
6. If `main=true`, promotes main-world files immediately under lease (failure rolls back bukkit.yml + deletes staged folder).
|
||||
7. Instructs the operator to restart; generation/load happens on next startup.
|
||||
|
||||
`WorldLifecycleStaging` holds staged generators/biome providers for the backend that consumes them at load.
|
||||
|
||||
## Exact world-slot replacement
|
||||
|
||||
`overwrite=true` uses lifecycle kind `WORLD_REPLACE` and always stages for a complete Paper-family restart, including Paper, Purpur, Leaf, and Folia; Spigot has no early registry bootstrap and rejects this mode. The exact target dimension folder must already exist; ordinary create remains the path for a new world. It accepts safe `iris:*` keys and only the three exact vanilla slots resolved from the configured level name: `minecraft:overworld`, `minecraft:the_nether`, and `minecraft:the_end`. A vanilla slot requires a matching pack environment (`NORMAL`, `NETHER`, or `THE_END`), and Nether/End replacement requires the server's matching allow setting to be enabled; foreign namespaces, other `minecraft:*` keys, path traversal, links, and special filesystem entries fail closed. `main=true` may accompany overwrite only for the configured main-world name. Minecraft stores one authoritative seed for the existing level, so all three exact vanilla slots preserve that loaded primary-world seed and report when it differs from the command's `seed`; changing the level seed remains the ordinary new-main promotion workflow.
|
||||
|
||||
The transaction copies and validates a fresh frozen pack under a same-filesystem sibling stage, fingerprints it, binds its journal to the canonical level root and logical world name, records the original target and `bukkit.yml` generator/seed, then compare-and-swaps that one configuration entry. Distinct slots can be queued before one restart. Paper bootstrap reconciles each authorized transaction before Iris compiles its aggregate datapack or Minecraft builds registries: it atomically moves the old exact dimension directory to a retained sibling backup and publishes the stage. The filesystem must support atomic replacement for the world directories, journal, and `bukkit.yml`; Iris refuses the operation without falling back to a non-atomic 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 replacement keeps the exact slot metadata and authoritative seed. Old `region`, `entities`, `poi`, and Iris runtime data are never merged; they remain only in the backup while the target starts with the staged pack snapshot.
|
||||
|
||||
The backup is eligible for asynchronous deletion only after `WorldLoad` proves the exact namespaced identity, Iris generator, selected dimension, seed, vanilla-slot environment, and unchanged pack fingerprint; cleanup failure retains its committed journal and retries without rolling back a verified world. A failed runtime check journals rollback and requests another restart, then cold bootstrap restores the retained directory and prior `bukkit.yml` generator/seed before registry or world loading. A crash between any move, configuration write, or journal phase is retried idempotently. Conflicting manual configuration, changed roots or logical names, changed staged bytes, unsafe storage, duplicate/corrupt journals, or irreconcilable transaction state abort the early bootstrap and preserve recoverable artifacts instead of guessing or deleting.
|
||||
|
||||
## Studio create
|
||||
|
||||
Studio uses `IrisCreator.studio(true)`:
|
||||
|
||||
- Startup datapack validation and the selected pack's validation must be loadable before a Studio project/world folder, snapshot, generator, or Bukkit world is created. Missing validation fails closed.
|
||||
- Does **not** copy the pack into the world folder (except benchmark).
|
||||
- Engine data folder is the live pack path; hotloader starts after engine setup.
|
||||
- Biome Buffet prepares a changed focus before opening the chunk generation session. Its exclusive fair-stage admission downgrades directly to the retained chunk permit, so no other transition can enter between the focus hotload and that chunk.
|
||||
- Studio worlds are transient: unloaded studio worlds are cleaned; `bukkit.yml` studio entries are removed on shutdown cleanup paths.
|
||||
- Studio open/close uses `StudioSVC` transition queue (see `10 - Studio & VSCode Schemas.md`).
|
||||
- Ordinary Studio suppresses native structure starts only while its initial FULL entry chunk is loading, then restores them for later preview chunks. A failed open never unloads or closes the generator while that exact asynchronous entry request remains active; another Studio open is rejected, cleanup begins after it settles, or its transient world is queued for deletion at the next clean startup if it remains active for another 120 seconds.
|
||||
|
||||
## Load
|
||||
|
||||
`/iris load` / `/iris import`:
|
||||
|
||||
1. Parses managed key; requires dimension root directory on disk.
|
||||
2. `BukkitWorldReconciler.loadWorld(bukkit.yml, worldKey)`.
|
||||
3. Reports success, busy, restart-required, or failure.
|
||||
|
||||
Load does not re-download packs; the world must already have `iris/pack` content and registration data consistent with Iris. Reconciliation checks startup readiness, then lazily validates that world's exact snapshot root before touching `bukkit.yml` or calling a world backend. Results are path-scoped, so separate worlds whose snapshot folders are both named `pack` cannot authorize or reject one another.
|
||||
|
||||
## Unload
|
||||
|
||||
`/iris unload` (player origin, sync):
|
||||
|
||||
1. Requires Iris world; acquires `WORLD_UNLOAD` lease.
|
||||
2. Marks world maintenance.
|
||||
3. `IrisToolbelt.evacuateAsync` → `WorldLifecycleService.unloadAsync(world, true)` → `generator.closeAsync()`.
|
||||
4. Terminal timeout **150 seconds**: if unload has not settled, marks timeout, requests server restart (`ServerConfigurator.restart`), and fails the future.
|
||||
|
||||
`WorldUnloadEvent` stops Iris engine maintenance immediately, but it is not treated 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 retains one generation lease through terrain generation and worldgen-heightmap priming.
|
||||
|
||||
## Evacuate
|
||||
|
||||
`/iris evacuate` moves all players out of the Iris world into another loaded world (or kicks if none). Used as a step inside unload and removal.
|
||||
|
||||
## Remove
|
||||
|
||||
`/iris remove <name> [delete=true]` delegates to `IrisWorldRemovalService`:
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `UNREGISTERED` | Unloaded/unregistered; files kept (`delete=false`) |
|
||||
| `DELETED` | Files deleted |
|
||||
| `DELETE_QUEUED` | Quarantined for delete at next startup |
|
||||
| `BUSY` | Another world/pack mutation holds the coordinator |
|
||||
| `INVALID_IDENTIFIER` / `PROTECTED_WORLD` / `NOT_IRIS_WORLD` / `UNSAFE_PATH` / `NOT_FOUND` | Refused |
|
||||
| Other failure statuses | Partial registry change without delete; quarantine path may remain |
|
||||
|
||||
Only safe `iris` namespace dimension paths are mutable. Phase timeouts use 120s and can request restart on stuck phases.
|
||||
|
||||
With `delete=true`, Iris records the exact quarantine name in the durable startup queue before moving the world directory. Immediate cleanup and startup retry both snapshot every directory's direct children before deleting them, reject symbolic links and special filesystem entries, and retain the queue entry with the full error when a concurrent writer or filesystem failure leaves content behind.
|
||||
|
||||
## Main world promotion
|
||||
|
||||
When create sets `main=true` (non-Folia), a shutdown hook rewrites `server.properties` `level-name` / `level-seed` and publishes files:
|
||||
|
||||
1. Stage temp directory under world container.
|
||||
2. Copy shared `data`, `datapacks`, `players` from current level root.
|
||||
3. Copy Iris dimension tree into staged overworld dimension path.
|
||||
4. Atomic move stage → new level root; write `server.properties`.
|
||||
|
||||
Promotion requires absent target level folder and refuses symlink world data. Folia with `main=true` performs the same publish during staging instead of deferring to shutdown.
|
||||
|
||||
To replace the currently configured main slot in place, name that exact main world and use `overwrite=true`; this keeps the top-level level root, shared datapacks, player data, and non-target dimensions intact. Ordinary `main=true` without overwrite remains the new-level-root promotion workflow above.
|
||||
|
||||
## Pack snapshot vs studio (lifecycle view)
|
||||
|
||||
| Operation | Pack effect |
|
||||
|-----------|-------------|
|
||||
| Production create | Full pack tree installed under world `iris/pack` |
|
||||
| Studio open | Engine reads live packs root; no world pack install |
|
||||
| `/iris studio package` | Export only; does not change world |
|
||||
| `/iris dev update-world` | Replaces world `iris/pack` (unsafe; restart if engine active) |
|
||||
| Hotload | Studio only; production snapshot stays fixed |
|
||||
| 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` serializes domains including `WORLD_MUTATION` and `PACK_MUTATION`. Overlapping create/load/unload/remove/replace/pack-publish returns busy to the operator. Ordinary world create refuses if the dimension root already exists or the world is already loaded; exact replacement uses a separately journaled restart transaction and never relaxes removal-path protection.
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user