# 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//` with namespace `iris`; modded dimensions persist through `iris-dimensions.json`. Non-Studio worlds carry a frozen pack at `iris/pack`, while Studio worlds bind the live packs directory. See also: `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`. ## Tutorial: promote a tested pack to a persistent world 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. ### 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. 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 `/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. ### 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`. 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. ### Lifecycle recovery | Symptom | Meaning | Recovery | |---|---|---| | 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 | | 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-` | 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 | ## Identity and storage | Item | Rule | |------|------| | Managed namespace | `iris` only for Iris-managed create/load/remove targets | | Logical name | For `iris:foo` the logical name is `foo` | | Storage root | Level root (`Server#getLevelDirectory` on Paper; else `world-container/level-name`) | | Dimension folder | `/dimensions/iris//` | | Pack snapshot | `/iris/pack/` | | Pregen cache dir | `/iris/pregen/` | | Registry | `worlds.json` in Iris data + `bukkit.yml` worlds section for production worlds | | Name constraints | Safe single path segment `[a-z0-9_-]+`; no `/`, `\`, `..`; reserved create names `iris` and `benchmark` rejected | Vanilla main/nether/end map to minecraft keys from `level-name` / `level-name_nether` / `level-name_the_end` and are not Iris-managed dimension folders. ### Modded persistent-dimension registry Fabric, Forge, and NeoForge persist dynamic Iris worlds in `/iris/iris-dimensions.json`: ```json { "dimensions": [ { "id": "irisworldgen:myworld", "pack": "overworld", "dimension": "overworld", "seed": 1337 } ] } ``` `id` is the registered dimension id, `pack` is the installed pack folder, `dimension` is its dimension load key, and `seed` is the generation seed. Writes use a temporary file plus atomic replacement when the filesystem supports it. Invalid individual entries are logged and preserved verbatim during ordinary updates; duplicate ids keep the first valid entry. If the whole registry cannot be parsed during startup, Iris moves it to `iris-dimensions.json.broken-`, logs any ids it can recover from the raw text, and continues with no persistent Iris dimensions. Keep the quarantined file, repair or recreate each reported world with `/iris world create`, and verify pack/dimension/seed values before deleting the backup. ## Command surface (Bukkit) | Command | Effect | |---------|--------| | `/iris create [type=default] [seed=1337] [main=false]` | Create or Folia-stage a managed world | | `/iris load ` / `/iris import ` | Load a disk Iris world via reconciler | | `/iris unload ` | Evacuate → unload → close generator | | `/iris remove [delete=true]` | Unregister / delete managed world | | `/iris evacuate ` | Move players out of the Iris world | | `/iris tp [player=]` | Teleport to world spawn | | `/iris worlds` | List Iris vs non-Iris loaded worlds | Full permission table: `04 - Commands & Permissions.md`. ### Create parameters | Param | Default | Notes | |-------|---------|-------| | `name` | required | Becomes `iris:`; folder must not already exist | | `type` | `default` | Pack/dimension selector: `default` → `settings.generator.defaultWorldType` (`overworld`); else pack name or `pack:dimensionKey` | | `seed` | `1337` | World seed | | `main` | `false` | Schedule main-world promotion on JVM shutdown (Paper path) or promote during Folia staging | Create refuses the primary Bukkit thread. Lifecycle domain `WORLD_MUTATION` / kind `WORLD_CREATE` must be free or create fails busy. ## Production create flow (non-Folia) 1. Resolve managed key and empty dimension root. 2. Resolve dimension via `IrisToolbelt.getDimension` (may download pack if missing). 3. Ensure datapacks for the dimension types are installed; queue restart if types not yet loaded. 4. Copy pack into `/iris/pack` (`StudioSVC.installIntoWorld`) — atomic stage → publish; refuses primary thread. 5. Build `WorldCreator` with Iris generator (`studio=false`). 6. Create world through `WorldLifecycleService` / NMS async create (timeout 120s; timeout triggers server restart). 7. Register world in `bukkit.yml` with generator `Iris` dimension key and seed; Multiverse link update when present. 8. Optional creation-time pregen if a `PregenTask` was attached by the creator API. ## Folia staging Runtime world creation is disabled on Folia. `/iris create` instead: 1. Acquires `WORLD_CREATE` lease. 2. Installs datapacks if changed. 3. Stages pack into the managed dimension root via `installIntoWorld`. 4. Registers the world in `bukkit.yml` (`BukkitWorldConfiguration.register`). 5. If `main=true`, promotes main-world files immediately under lease (failure rolls back bukkit.yml + deletes staged folder). 6. Instructs operator to restart; generation/load happens on next startup. `WorldLifecycleStaging` holds staged generators/biome providers for the backend that consumes them at load. ## Studio create Studio uses `IrisCreator.studio(true)`: - Does **not** copy the pack into the world folder (except benchmark). - Engine data folder is the live pack path; hotloader starts after engine setup. - 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. ## 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 [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. ## Pack snapshot vs studio (lifecycle view) | Operation | Pack effect | |-----------|-------------| | Production create | Full pack tree installed under world `iris/pack` | | Studio open | Engine reads live packs root; no world pack install | | `/iris studio package` | Export only; does not change world | | `/iris dev update-world` | Replaces world `iris/pack` (unsafe; restart if engine active) | | Hotload | Studio only; production snapshot stays fixed | ## Concurrent lifecycle guards `LifecycleOperationCoordinator` serializes domains including `WORLD_MUTATION` and `PACK_MUTATION`. Overlapping create/load/unload/remove/pack-publish returns busy to the operator. World create also refuses if the dimension root already exists or the world is already loaded.