This commit is contained in:
Brian Neumann-Fopiano
2026-08-12 13:52:16 -04:00
parent 12b97b7994
commit 365205ad0a
82 changed files with 7978 additions and 5625 deletions
+124 -173
View File
@@ -1,25 +1,44 @@
# 24 - Pack Mods & Snippets
Snippets are active reusable JSON fragments for types annotated `@Snippet`; fields accept either an inline object or a path under `snippet/<type>/`. Iris also loads the legacy `IrisMod` JSON schema from `mods/`, but no engine path applies those injector or replacer fields at runtime.
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`.
## Tutorial: reuse one active decorator snippet
## The mental model
Snippets are the executable reuse mechanism on this page. Start with a validating pack and a biome that already generates correctly. Save this complete decorator as `snippet/decorator/tutorial-wildflowers.json`:
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" },
{ "block": "minecraft:poppy" }
],
"slopeCondition": { "maximumSlope": 4 }
{ "block": "minecraft:dandelion", "weight": 2 },
{ "block": "minecraft:poppy", "weight": 1 },
{ "block": "minecraft:air", "weight": 4 }
]
}
```
Reference it from the existing biome's `decorators` array without `.json`:
**2. Reference it.** In the biome, replace the inline decorator with the path. No `.json` suffix:
```json
{
@@ -27,127 +46,26 @@ Reference it from the existing biome's `decorators` array without `.json`:
}
```
1. Validate the pack and open it in Studio on a fixed seed.
2. Generate new chunks in the target biome. Success is both flower types appearing only on slopes accepted by the snippet, with no missing-snippet error.
3. If the field resolves to null, confirm the singular `snippet/` folder, the exact `decorator` type folder, and the suffix-free reference. If the snippet loads but does not place, raise `chance` temporarily and verify dimension `decorate` is true.
4. Reuse the same string in another biome only after the first placement works. Generate the VSCode workspace so schema completion exposes valid snippet paths.
**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.
Do not implement this workflow with `mods/*.json`. Pack-mod files remain parseable schema data but are not applied by engine creation or Studio hotload.
**4. Add the second call site.** Only once the first one works. Paste the same string into another biome's `decorators`.
## Pack mod schema (`IrisMod`, inactive)
**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.
Folder: `mods/`. The loader key is the path under `mods/` without `.json`. `IrisData` can parse and expose these registrants to schema and tooling paths, but engine creation and Studio hotload do not consume them. Treat the fields below as an inactive schema, not a supported way to modify a dimension.
Generate the VSCode workspace (`/iris studio vscode`) so schema completion offers valid snippet paths for each field. See `10 - Studio & VSCode Schemas.md`.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `name` | string | `"A Pack Modification"` | Required human name (min length 2) |
| `forDimension` | string | `""` | Optional dimension load key; empty = any dimension |
| `overrideFluidHeight` | int -1..512 | `-1` | `-1` leaves fluid height unchanged |
| `removeBiomes` | string[] | `[]` | Biome keys to remove |
| `removeObjects` | string[] | `[]` | Object keys to remove |
| `removeRegions` | string[] | `[]` | Region keys to remove |
| `injectRegions` | string[] | `[]` | Region keys to inject into the dimension |
| `biomeInjectors` | `IrisModBiomeInjector[]` | `[]` | Inject biomes into a region |
| `biomeReplacers` | `IrisModBiomeReplacer[]` | `[]` | Swap biomes |
| `objectReplacers` | `IrisModObjectReplacer[]` | `[]` | Swap object keys |
| `biomeObjectPlacementInjectors` | `IrisModObjectPlacementBiomeInjector[]` | `[]` | Inject object placements into a biome |
| `regionObjectPlacementInjectors` | `IrisModObjectPlacementRegionInjector[]` | `[]` | Inject object placements into a region |
| `regionReplacers` | `IrisModRegionReplacer[]` | `[]` | Swap regions |
| `blockReplacers` | `IrisObjectReplace[]` | `[]` | Block find/replace rules (same shape as object material replacers) |
| `styleReplacers` | `IrisModNoiseStyleReplacer[]` | `[]` | Replace `NoiseStyle` usages |
## How resolution works
### Injector and replacer shapes
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.
**Biome injector** (`@Snippet("biome-injector")`):
```json
{ "region": "temperate", "inject": ["temperate/meadows"] }
```
**Biome replacer** (`biome-replacer`):
```json
{ "find": ["temperate/plains"], "replace": "temperate/lush-plains" }
```
**Region replacer** (`region-replacer`):
```json
{ "find": ["temperate"], "replace": "forests" }
```
**Object replacer** (`object-replacer`):
```json
{ "find": ["clutter/camp1"], "replace": "clutter/camp3" }
```
**Object placement biome injector** (`object-placement-biome-injector`):
```json
{
"biome": "temperate/plains",
"place": [{ "chance": 0.01, "place": ["clutter/camp1"] }]
}
```
**Object placement region injector** (`object-placement-region-injector`): field name is `biome` in code but the registry type is `IrisRegion` (region load key):
```json
{
"biome": "temperate",
"place": [{ "chance": 0.01, "place": ["clutter/camp1"] }]
}
```
**Noise style replacer** (`noise-style-replacer`):
| Field | Notes |
|-------|-------|
| `find` | `NoiseStyle` enum value to match |
| `replaceTypeOnly` | When true, only swap the style type and keep other style fields |
| `replace` | Full `IrisGeneratorStyle` replacement |
**Block replacer** (reuses `IrisObjectReplace`, snippet `object-block-replacer`): `find` block list, `replace` palette, optional `exact`, `chance` 0..1.
### Schema example
`mods/example-swap.json`:
```json
{
"name": "Example Temperate Swap",
"forDimension": "overworld",
"biomeReplacers": [
{
"find": ["temperate/plains"],
"replace": "temperate/meadows"
}
],
"biomeInjectors": [
{
"region": "temperate",
"inject": ["temperate/shattered-plains"]
}
]
}
```
The example is parseable as `IrisMod`, but it has no effect on generated terrain. Apply equivalent changes directly to the target dimension, region, biome, generator, or object-placement JSON.
## Snippets
### Mechanism
1. Many nested pack types carry `@Snippet("type-name")`.
2. Gson type adapters in `IrisData` intercept those types on read.
3. A field may be either:
- an inline JSON object of that type, or
- a **string** `"snippet/<type-name>/<path>"` that loads `snippet/<type-name>/<path>.json` from the pack root.
4. If the string starts with `snippet/` but uses a different type folder, the loader rewrites to the expected `snippet/<type-name>/` prefix for that field.
5. Missing snippet files log an error and yield null for that value.
Studio schemas (`SchemaBuilder`) expose every snippet as `anyOf` object-or-string and list files under `snippet/<type>/` in the workspace enum.
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
@@ -157,21 +75,22 @@ pack/
decorator/
bush.json
dry_grass.json
...
forest/
fern.json
style/
bedrock.json
deepslate.json
```
Folder is singular `snippet/`, not `snippets/`. Subfolders match the `@Snippet` value exactly.
The folder is singular `snippet/`, not `snippets/`. Subfolder names must match the `@Snippet` value exactly.
### Overworld usage
Dimension ores reference style snippets:
Dimension ore deposits reference a style snippet:
```json
{
"chanceStyle": "snippet/style/bedrock"
"chanceStyle": "snippet/style/bedrock"
}
```
@@ -181,44 +100,41 @@ Dimension ores reference style snippets:
{ "style": "STATIC" }
```
Biome decorators accept snippet strings in arrays:
Biome decorator lists take snippet strings as array elements, mixed freely with inline objects:
```json
{
"decorators": [
"snippet/decorator/wildflowers",
"snippet/decorator/bush"
]
}
```
`snippet/decorator/bush.json`:
```json
{
"chance": 0.03,
"style": {
"style": "CLOVER_HERMITE",
"zoom": 0.52,
"exponent": 2.5,
"axialFracturing": true
},
"slopeCondition": { "maximumSlope": 5 },
"palette": [
{ "block": "minecraft:bush", "weight": 1 },
{ "block": "minecraft:air", "weight": 4 }
"decorators": [
"snippet/decorator/wildflowers",
"snippet/decorator/bush",
{ "chance": 0.01, "palette": [{ "block": "minecraft:sweet_berry_bush" }] }
]
}
```
`biomes/dev.json` uses the same pattern for a minimal decorator list.
`biomes/dev.json` in the shipping pack uses the same pattern for a minimal decorator list.
### `@Snippet` type names (engine/object)
## When to use a snippet
Each value is the folder name under `snippet/` and the string prefix after `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.
| Snippet value | Class (representative) |
|---------------|------------------------|
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` |
@@ -294,31 +210,66 @@ Each value is the folder name under `snippet/` and the string prefix after `snip
| `tree-sub-branches` | `IrisTreeSubBranches` |
| `vacuum-settings` | `IrisVacuumSettings` |
Registrants that are whole files (dimensions, regions, biomes, generators, loot tables, entities, spawners, markers, mods, objects, structures) are not snippet types; only nested field types listed above are.
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.
### Registered schemas without a production authoring path
## Pack mods (`IrisMod`) — schema only, not applied
Schema registration alone does not prove a runtime consumer. The following types are discoverable by loaders or Studio schema generation but are not supported pack features:
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.
| Surface | Current status |
|---------|----------------|
| `potion-effect` / `IrisPotionEffect` | Snippet schema exists, but no production field consumes this type; use the potion fields on `IrisEffect` instead |
| `matter/` resources | A loader exists for Matter binaries, but generation and runtime code do not consume pack `matter/` resources |
| `IrisObjectPlacement.translateCenter` | Serialized and copied by `toPlacement`, but no placement path reads the value |
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 `mods/*.json` family is likewise schema/tooling-only as documented above.
The fields below are documented because they still appear in schema completion and because packs in the wild contain them, not because they work.
### Tutorial: author and verify a snippet
| 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 |
1. Copy one working inline value into `snippet/<type>/<name>.json`; the folder must match the field's `@Snippet` type.
2. Replace one original value with `"snippet/<type>/<name>"` (no `.json` suffix).
3. Validate and open Studio. Confirm schema completion lists the path and fixed-seed output matches the inline version.
4. Replace the second duplicate only after the first call site passes.
5. Change one value inside the snippet and confirm both call sites change on newly generated chunks, then restore the intended value.
Shapes of the nested types, all of which are also registered snippet types:
Use snippets for values genuinely shared across biomes, such as decorators, styles, and palettes. A missing or wrong-type snippet resolves to null after an error, so treat validation and console output as required gates.
```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`, `04 - Commands & Permissions.md`.
- Studio open/hotload: `10 - Studio & VSCode Schemas.md`.
- 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`.