# Iris on Fabric, Forge and NeoForge `art.arcane.iris.modded.api` is the surface a **mod** compiles against. It answers three questions: is this level generated by Iris, how do I drive Iris from my mod, and how do I get my own blocks, items and mobs placed by an Iris pack. It ships in the Fabric, Forge and NeoForge jars only. It is absent from the Bukkit plugin jar, shares no types with `art.arcane.iris.api`, and is not covered by [terrain.md](terrain.md), [world-events.md](world-events.md) or [tree-feller.md](tree-feller.md) - those describe the Bukkit surface, which does not exist on a mod loader. Everything here assumes Minecraft 26.2, Java 25, and one of Fabric, Forge or NeoForge. The mod id is `irisworldgen` on all three. | What you want | Where | |---|---| | Detect Iris, read the engine, start a pregeneration, read/write mantle data | `IrisModdedAPI` | | Have Iris place *your* blocks, items and mobs | `ModdedDataProvider` | | Alias one custom key onto a fixed vanilla state, with no provider class | `IrisModdedAPI.registerCustomBlockData` | --- ## Depending on Iris **There is no published Maven artifact for the mod jars.** No module in this repository applies `maven-publish`, and the JitPack route documented in [README.md](README.md) resolves the Bukkit sources, not the modded adapter. Until that changes, building from source is the only path. The three adapters are standalone Gradle builds - each `adapters//settings.gradle` does `includeBuild('../..')` to substitute `art.arcane:core` and `art.arcane:spi` from the root build, which is what keeps Loom, ForgeGradle and ModDevGradle off one plugin classpath. The root build drives them through their own wrappers: ```bash ./gradlew buildFabric # -> dist/Iris v [Fabric] +.jar ./gradlew buildForge # -> dist/Iris v [Forge] +.jar ./gradlew buildNeoforge # -> dist/Iris v [NeoForge] +.jar ``` Then compile against the jar you will actually run: ```gradle dependencies { compileOnly(files('libs/Iris-fabric.jar')) } ``` The adapters are **not** in the root `settings.gradle` by default. Add `-PincludeModdedAdapters=true` for IDE import only; it closes a composite build cycle (root -> adapter -> root) and Gradle may reject it. ### Soft dependency Declare the optional relationship, then do not rely on load order - the ServiceLoader path below works regardless of it, because Iris does the discovering. Fabric (`fabric.mod.json`) - `suggests`, not `depends`; a hard `depends` makes Iris mandatory: ```json { "suggests": { "irisworldgen": "*" } } ``` NeoForge (`META-INF/neoforge.mods.toml`): ```toml [[dependencies.yourmod]] modId = "irisworldgen" type = "optional" ordering = "AFTER" side = "BOTH" ``` Forge (`META-INF/mods.toml`) is the same with `mandatory = false` instead of `type`. ### Detecting Iris Two checks, and they answer different questions. **Is the mod present?** Ask the loader - `FabricLoader.getInstance().isModLoaded("irisworldgen")`, or `ModList.get().isLoaded("irisworldgen")` on Forge and NeoForge. Cheap, but tells you nothing about whether Iris actually generates anything. **Are Iris classes on the classpath?** If your integration lives in a class that imports `art.arcane.iris.modded.api.*`, that class must not load unless Iris is present. Keep the imports behind a presence check and a separate class, or probe reflectively: ```java private static final boolean IRIS_PRESENT = irisPresent(); private static boolean irisPresent() { try { Class.forName("art.arcane.iris.modded.api.IrisModdedAPI"); return true; } catch (Throwable absent) { return false; } } ``` Do not gate on a version string. Probe for the class or method you need. **Is *this* level Iris?** `IrisModdedAPI.isIrisLevel(level)`. A server can mix Iris and vanilla dimensions freely, so presence of the mod is not presence of an Iris world. --- ## `IrisModdedAPI` All static, all null-tolerant: a null or non-Iris `ServerLevel` yields `false`, `null` or a no-op. You never have to pre-check. | Method | What it does | |---|---| | `isIrisLevel(ServerLevel)` | Whether the level's chunk generator is `IrisModdedChunkGenerator`. The cheapest check | | `isStudioLevel(ServerLevel)` | Whether it is a throwaway pack-authoring world. Persist nothing against one | | `getEngine(ServerLevel)` | The `Engine` behind the level, or null. See the stability warning below | | `pregenerate(ServerLevel, int radiusBlocks)` | Starts a cached async pregeneration around the origin | | `pregenerate(ServerLevel, int, int centerX, int centerZ, boolean sync, boolean cached)` | Same, with a centre and write mode | | `getMantleData(ServerLevel, int x, int y, int z, Class)` | Reads Iris's per-block sidecar storage | | `setMantleData(ServerLevel, int x, int y, int z, T)` | Writes it | | `deleteMantleData(ServerLevel, int x, int y, int z, Class)` | Removes a value of that type | | `retainMantleDataForSlice(Class)` | Declares a mantle type Iris must keep rather than discard | | `registerProvider(ModdedDataProvider)` | Registers a custom content provider imperatively | | `registerCustomBlockData(String namespace, String key, String state)` | Aliases one key onto a fixed block state | ### `Engine` is internal `getEngine` returns `art.arcane.iris.engine.framework.Engine`. That type is **internal to Iris** and changes without a deprecation cycle - as do `art.arcane.iris.core.*`, `art.arcane.iris.util.*` and `art.arcane.iris.spi.*`. Treat the returned `Engine` as an opaque token to hand back to Iris. Every method in the table above that needs one resolves it for you; prefer those. Never cache an `Engine`. A pack hotload or a level unload replaces it, and the old instance goes inert. `getEngine` already returns null while the generator is binding and during shutdown. ### Pregeneration `pregenerate` returns as soon as the job is queued. Progress goes to Iris's own logging and boss bar, not to your caller. Only one job runs server-wide, so it returns `false` if one is already active - and `false` also means "not an Iris level", so check `isIrisLevel` first if you need to tell those apart. Call it on the server thread. `cached = true` writes an on-disk pregeneration cache so an interrupted run resumes instead of regenerating. `sync = true` writes chunks synchronously: slower, but it bypasses the async write queue. ### Mantle data The mantle is Iris's own per-block storage, independent of chunk NBT, and it is how Iris carries data between generation stages. Three things to know: 1. **Coordinates are world-space.** `y` is translated by the engine's minimum height internally. A `y` outside the engine's height range reads as null and writes as a no-op - no exception. 2. **Reads never create storage; writes do.** `getMantleData` returns null when no mantle region exists for that column yet. `setMantleData` and `deleteMantleData` create the region, which can touch disk - do not call them per block in a tick loop on the server thread. 3. **Declare your types or lose them.** Iris discards mantle slices it does not need once a region's generation data has served its purpose. Any type you write and expect to read back later must be declared once during mod setup: ```java IrisModdedAPI.retainMantleDataForSlice(MyMarker.class); ``` Registration is by canonical class name, process-wide across every Iris world, and cannot be undone. All three mantle methods throw `IllegalStateException` if the engine's mantle has already been closed. --- ## `ModdedDataProvider` The extension point. Implement it and a pack can name `yourmod:something` in a block palette, a loot table or a spawn entry, and Iris will ask you to resolve it. ```java public interface ModdedDataProvider { String modId(); default boolean isReady(); // default true Collection getTypes(ModdedDataType type); boolean isValidProvider(Identifier id, ModdedDataType type); default ModdedBlockData getBlockData(Identifier blockId, Map state); // default null default void processBlockPlacement(ModdedBlockPlacementContext context); // default no-op default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId); // default null default void init(); // default no-op } ``` `ModdedDataType` is `BLOCK`, `ITEM` or `ENTITY`. Constants may be added - write a `default` arm in any switch expression over it. ### The contract `modId()` is your identity, not decoration. It de-duplicates registrations and labels every log line Iris emits about your provider. It must be non-null and stable; returning null aborts discovery. `isValidProvider(id, type)` is the gate. Iris calls it before every resolution callback, on generation threads, for every key it could not resolve itself. Keep it to a namespace comparison or a set lookup. `isReady()` is how a provider whose registries populate late excuses itself. Iris **skips** a provider that returns false rather than treating it as absent, so returning false is strictly better than returning wrong answers. `getTypes(type)` feeds command suggestion and pack tooling. It is not the resolution path - return an empty collection rather than null, and do not do work here that `isValidProvider` should do. `getBlockData(blockId, state)` resolves a claimed block. `state` holds the `[prop=value]` pairs from the pack's key, already parsed, possibly empty, never null. Return null to decline and Iris tries the next provider, then falls back to air. Return `ModdedBlockData.direct(blockState)` when the state is final. `processBlockPlacement(context)` finishes a **deferred** placement. Return `ModdedBlockData.deferred(placeholder)` from `getBlockData` when the real block needs a loaded level - a block entity, neighbour state, or mod registries not reachable from a generation thread. Iris writes your placeholder during generation and calls you back later on the server thread with the chunk loaded. Pick a placeholder with the same shape and occlusion as the final block so terrain around it generates correctly. Only the *first* provider claiming the identifier is called for a given position. `ModdedBlockPlacementContext` is an immutable record: `engine`, `level`, `position`, `blockId`, `state`, `blockState`. `blockState` is what is currently at `position` - normally your placeholder, though a later generation stage may have replaced it. `state` is defensively copied and unmodifiable. `spawnMob(...)` spawns a claimed custom entity on the server thread. Return null to decline. `init()` runs once, immediately after Iris accepts your provider. ### Threading | Callback | Thread | Notes | |---|---|---| | `isValidProvider`, `getBlockData` | Generation threads, many at once | Must be fast. Must not touch world state | | `processBlockPlacement` | Server thread, chunk loaded | Safe to write blocks and attach block entities | | `spawnMob` | Server thread | | | `init` | Whichever thread registered you | Mod init for ServiceLoader, your caller otherwise | | `modId`, `isReady`, `getTypes` | Any | | Implementations must be thread-safe. `getBlockData` in particular is called concurrently for every unresolved key a pack names, which during a pregeneration is a lot. ### Registration by ServiceLoader Iris discovers providers with `java.util.ServiceLoader`. Ship a service file naming your implementation's binary name; the class needs a public no-argument constructor. `src/main/resources/META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider`: ``` com.example.yourmod.iris.YourIrisProvider ``` One binary name per line. Nested classes use `$`, for example `com.example.yourmod.YourMod$IrisProvider`. Iris loads the service with **its own** class loader: `ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader())`. Your service file therefore has to be visible from Iris's loader, which is the normal case on all three loaders but is the thing to suspect first if nothing happens. Confirm with the registration log line below; if it never appears, fall back to `registerProvider`. ### When discovery runs `ModdedCustomContentRegistry.discover()` runs inside `ModdedEngineBootstrap.bootCommon(...)`, which is the very first thing each loader's entrypoint calls - `IrisFabricBootstrap.onInitialize`, `IrisForgeBootstrap`/`IrisNeoForgeBootstrap` construction. That is **before** the Iris chunk generator is registered and long before any server starts. Consequences: - A ServiceLoader-declared provider is available before any world could resolve a block. This is the safe path. - Discovery runs **once per process**. A second `discover()` is a no-op. - Your `init()` must not assume a server, a level, or a fully populated game registry. Defer that work and gate it behind `isReady()`. ### Registration imperatively `IrisModdedAPI.registerProvider(provider)` works at any time and is the option if you would rather not ship a service file, or need to build the provider from your own config. Ordering is the catch: Iris only consults providers registered **before** a pack resolves the block in question, and blocks already resolved are not revisited. Register during mod setup. Registering after Iris's own ServiceLoader pass is fine; registering after a world has generated is not. A second registration under a `modId()` already present is logged and ignored. `init()` runs during the call. ### How discovery and failures are reported Everything below is logged under the `Iris` logger. One line per accepted provider confirms registration - this is what to grep for when checking whether your service file was seen: ``` Iris registered custom content provider 'yourmod' ``` A duplicate `modId()` is rejected with `already registered; ignoring duplicate`. Iris catches throwables from every provider callback, logs them against your `modId()`, and continues with the remaining providers - one broken provider does not stop world generation: ``` Iris custom content provider 'yourmod' failed resolving block yourmod:thing Iris custom content provider 'yourmod' failed post-placement for yourmod:thing at BlockPos{...} Iris custom content provider 'yourmod' failed spawning mob yourmod:critter Iris custom content provider 'yourmod' failed to initialize # registerProvider path only ``` `init()` during ServiceLoader discovery is the one exception, and it is all-or-nothing: a throwable there aborts the pass, restores the registry to its pre-discovery state, and rethrows. The log line names the provider that failed, by mod id and class name: ``` Iris custom content provider discovery failed at provider 'yourmod' (com.example.yourmod.iris.YourIrisProvider) ``` If your provider's own `modId()` throws while Iris is building that message, the class name alone is logged; if the failure happened outside any provider, it reads `the provider service loader`. A `null` provider or a null `modId()` from the ServiceLoader fails the pass with a message naming which. ### Static aliases, no provider class For the common case of "my key is really this vanilla block", skip the provider: ```java IrisModdedAPI.registerCustomBlockData("yourmod", "fancy_log", "minecraft:oak_log[axis=y]"); ``` The state string uses the same syntax packs use and is parsed **immediately** - a typo is logged at startup and the registration dropped, rather than surfacing later as missing blocks. Aliases take precedence over provider lookups for the same key. Null arguments are ignored. --- ## How pack resolution works on modded Paths are relative to the loader's config directory (`config/` on a normal server install). | Path | What it is | |---|---| | `config/irisworldgen/packs//` | Installed packs. A pack is valid when `dimensions/.json` exists | | `config/irisworldgen/generated/datapack/iris/` | The generated forced datapack. Iris owns this - do not edit it | | `config/irisworldgen/modded.json` | Mod-side config: default pack, auto-download, primary world routing | | `config/iris/` | Engine data directory: settings and per-world engine state | Note the split: the engine's data folder is `config/iris`, but every modded pack path - installer, validator, command suggestions, forced datapack, engine creation - resolves under `config/irisworldgen/packs`. Install packs there. At `bootCommon`, Iris kicks off an async default-pack prefetch. If `modded.json` has `autoDownloadDefaultPack` enabled and the configured `defaultPack` is missing, Iris downloads `IrisDimensions/` from the `master` branch into the packs folder. If that fails it logs a pointer to `/iris download `. A pack that is already installed is left alone. When a level asks for its pack, `ModdedWorldEngines.packFolder(pack)` resolves `config/irisworldgen/packs/`. A missing pack is a hard failure with the expected absolute path printed - Iris does not silently generate vanilla terrain in its place. ### The forced datapack Iris cannot register dimension types and per-pack biomes through a mod registry, because vanilla world creation reads them from the datapack layer. So Iris **generates a datapack** from the installed packs and injects it as a built-in, top-priority repository source: - Fabric: a `PackRepository` mixin, via `FabricForcedDatapackSources` - Forge and NeoForge: `event.addRepositorySource(ModdedForcedDatapack.repositorySource())` It contributes world presets, dimension types and biomes under the `irisworldgen` namespace, with ids derived from the pack and dimension names - `irisworldgen:packs//dimensions//preset`, `.../dimension_type`, `.../biomes/`. This is why an Iris dimension shows up in the vanilla world creation screen as `IRIS:`. It is regenerated when the pack changes: a studio hotload calls `ModdedForcedDatapack.regenerate()`. If regeneration fails and a previously published pack is still readable, Iris keeps the last known-good one and logs the failure rather than starting with no dimension types. **The failure you need to recognise.** If injection did not happen for your loader - a mixin that failed to apply, an event that never fired - Iris logs this once at startup and world creation will fail no matter how many times you restart: ``` Iris forced datapack 'iris_worldgen' was never loaded by this server. N installed pack(s) at contributed no dimension types or custom biomes. Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it. ``` That is a loader/environment problem, not a pack problem. Nothing an integrating mod does can fix it. ### Commands worth knowing The modded command tree is `/iris`, aliased `/ir` and `/irs`, gated at gamemaster permission level. | Command | What it tells you | |---|---| | `/iris pack validate [pack]` | Validates every installed pack, or one. Runs on a worker thread, reports per pack, and counts unloadable packs | | `/iris pack status [pack]` | Replays the **recorded** validation results - blocking errors and warnings per pack. Says so and returns nothing if `validate` has not run this session | | `/iris pack cleanup [apply]` | Previews unused pack resources; `apply` deletes them | | `/iris pack restore [apply]` | Previews a restore of pack resources; `apply` performs it | | `/iris datapack status` | Per Iris dimension: active dimension type, its min/max/logical height, what the pack wants, and whether they match | | `/iris datapack install` | Writes the pack's dimension type into `/datapacks/iris/data/irisworldgen/dimension_type/` as an override | | `/iris datapack list` | Datapack URLs declared by installed pack dimensions, plus the datapacks actually present in `/datapacks/` | | `/iris download [branch] [overwrite]` | Installs a pack from `IrisDimensions/` into the packs folder. Aliased `dl` | | `/iris version` | Iris version and loader | `/iris datapack status` is the first thing to run when an Iris dimension generates at the wrong height. A mismatch means the level's active dimension type disagrees with the pack, which happens when a world was created before a pack's height range changed. `install` writes the override; the world still needs a restart. `/iris datapack ingest`, `pull` and `remove` exist but refuse on modded, with a message explaining why: the Modrinth ingest workflow is Bukkit tooling. Native vanilla and datapack structure placement **does** work on modded - install the datapack into `/datapacks/` and restart, and its registered structures generate. --- ## Native worldgen passthrough: what generates over Iris terrain Iris replaces the chunk generator outright. Every piece of vanilla or mod worldgen therefore only runs if Iris runs it. This is the honest matrix. | Vanilla / mod worldgen | Runs over Iris terrain? | How | |---|---|---| | Structures (vanilla, datapack, mod) | **Yes**, on by default | Iris runs its own structure pass with vertical fitting, foundation stilts and vegetation clearing. Deny with `importedStructures.disabled` | | Placed features: ores, trees, plants, springs, geodes (vanilla, datapack, mod) | **Yes**, off by default | Set `importedFeatures.enabled` on the dimension | | Carvers (caves, canyons, mod carvers) | **Never** | Architectural. See below | | Mod biomes | Only as a `derivative` / `vanillaDerivative` / `biomeScatter` target | Iris chooses biomes from the pack, not from a biome source | | Mob spawning, including mod mobs | **Yes** | Iris merges the biome's own spawn table with the vanilla derivative's | | Surface builders / surface rules | **Never** | Iris generates its own surface from the pack palettes | ### `importedFeatures` A dimension-level control block, disabled by default. With it absent or `enabled: false` chunk output is byte-for-byte what Iris has always produced, and no feature table is built. Biome tags are not part of that guarantee, and are not gated on this flag at all: Iris custom biomes inherit the biome tags of their vanilla derivative on every platform, so the emitted datapack tag files differ from older Iris builds regardless of `importedFeatures`. Anything driven by biome tags - mob variants, spawn rules, mod content selecting on `#minecraft:is_overworld` and friends - therefore applies to Iris custom biomes. ```json { "importedFeatures": { "enabled": true, "steps": ["UNDERGROUND_ORES"], "disabledSteps": ["VEGETAL_DECORATION"], "disabled": ["minecraft:ore_diamond", "minecraft:trees"] } } ``` | Field | Meaning | |---|---| | `enabled` | Master switch. Default `false` | | `steps` | Allow-list of decoration steps. Empty (default) means every step | | `disabledSteps` | Deny-list of decoration steps, applied after `steps` | | `disabled` | Placed-feature key deny-list. A `namespace:path` prefix matches on namespace, slash and underscore boundaries, so `minecraft:ore` denies every vanilla ore | Steps are the vanilla ones, in order: `RAW_GENERATION`, `LAKES`, `LOCAL_MODIFICATIONS`, `UNDERGROUND_STRUCTURES`, `SURFACE_STRUCTURES`, `STRONGHOLDS`, `UNDERGROUND_ORES`, `UNDERGROUND_DECORATION`, `FLUID_SPRINGS`, `VEGETAL_DECORATION`, `TOP_LAYER_MODIFICATION`. Ores live in `UNDERGROUND_ORES`; trees, grass and flowers in `VEGETAL_DECORATION`. What you get, and what it costs: - Features are read from the biome's **vanilla derivative**. An Iris custom biome declares no features of its own by design (its generated datapack JSON has empty `features` and `carvers` arrays); passthrough comes from the chunk generator's generation-settings getter, which maps the custom biome onto the derivative. - Iris terrain is not vanilla terrain. A feature that assumes a vanilla surface can land oddly - floating sugar cane, ore veins in unexpected rock, trees on a slope Iris carved. Turn it on per dimension and look before shipping. - Feature seeds are derived exactly as vanilla derives them, so the same pack plus the same registries places the same features. Denying one feature never shifts another: each takes its seed from its own global index. - The pass runs on the worldgen thread that owns the chunk, never on the Iris generation pool. The vanilla FEATURES step writes into neighbouring chunks and is not parallel-safe. - The whole feature pass runs after Iris has placed its structures, not interleaved per step the way vanilla orders them. An early-step feature - `RAW_GENERATION`, `LAKES`, `LOCAL_MODIFICATIONS` - therefore sees placed structures and can cut into one, so a lake can open into a structure vanilla would have flooded before placing it. - Both platforms behave identically: the Bukkit plugin has the same control with the same semantics. **Feature order cycles.** Vanilla topologically sorts every placed feature across every biome. Content that declares mutually inconsistent orderings makes that sort fail with `Feature order cycle found`. Iris builds the table at bind and catches that failure: `importedFeatures` degrades to off for the dimension and Iris logs an ERROR naming the involved sources. It never becomes a chunk-generation crash. ### Why carvers can never be imported A carver runs against `NoiseGeneratorSettings` - the noise router, aquifer state and surface rules of a `NoiseBasedChunkGenerator`. Iris has none of those; its terrain comes from the pack's own generators and its caves from the Iris carving system. There is nothing for a vanilla carver to sample, so `applyCarvers` is empty by design and there is no flag to change that. Use Iris `caves` and `carvings` in the pack instead. ### 26.2 pack-content note: `pointed_dripstone` and `speleothem` 26.2 renamed the *feature type* `minecraft:pointed_dripstone` to `minecraft:speleothem`, and `minecraft:dripstone_cluster` to `minecraft:speleothem_cluster`. Verified against the 26.2 built-in data: | Registry | 26.2 key | |---|---| | Block (`minecraft:block`) | `minecraft:pointed_dripstone` - **unchanged** | | Placed feature (`minecraft:worldgen/placed_feature`) | `minecraft:pointed_dripstone` - **unchanged** | | Configured feature (`minecraft:worldgen/configured_feature`) | `minecraft:pointed_dripstone` - **unchanged** | | Feature type (`minecraft:worldgen/feature`) | `minecraft:speleothem` - **renamed** | So a pack that lists `minecraft:pointed_dripstone` in a palette, an object, or an `importedFeatures.disabled` entry is still correct - those are block and placed-feature keys. Only content that names the *feature type* directly, which is a datapack-authoring concern rather than an Iris pack concern, needs updating. ### Biome tags Generated Iris custom biomes inherit the **biome tag membership of their vanilla derivative**, on top of any tags the pack declares in `tags`. That is what makes `#minecraft:is_overworld` and mod-authored tag selectors resolve against Iris terrain; without it a custom biome sits in no tag at all. Structure tags (`#minecraft:has_structure/*`) are deliberately **not** inherited - Iris resolves native structure placement through the biome's structure derivative, so inheriting them would place a structure twice. Tag files are written with `"replace": false`, so vanilla tags are extended, never replaced. --- ## What is not supported - **No published artifact.** Build from source, as above. There is no Maven coordinate for the modded jars and no `maven-publish` in this build. - **Core types are internal.** `art.arcane.iris.engine.*`, `art.arcane.iris.core.*`, `art.arcane.iris.util.*` and `art.arcane.iris.spi.*` change without notice. `Engine`, reachable through `getEngine` and `ModdedBlockPlacementContext.engine()`, is the one internal type this surface exposes, and it is exposed as a token to hand back rather than an API to call. - **No event bus.** `IrisPlatform.callEvent` is a no-op on every mod loader adapter. There is no modded equivalent of `IrisWorldEngineEvent` or `IrisPregenerationEvent`; poll `isIrisLevel`/`getEngine` instead. - **No `art.arcane.iris.api`.** The Bukkit terrain, world-event, pregen and tree-feller interfaces are not in the mod jars. There is no modded terrain-query surface yet. - **No PlaceholderAPI.** [placeholders.md](placeholders.md) is Bukkit-only. - **Datapack ingest is Bukkit-only**, per the command note above. - **Vanilla carvers and surface rules never run.** See the passthrough matrix above. - **`ModdedCustomContentRegistry`'s resolution methods are Iris internals.** They are public only because the adapter's generation code lives in another package. Go through `IrisModdedAPI`.